diff --git a/src/MaaCore/MaaCore.vcxproj b/src/MaaCore/MaaCore.vcxproj
index 2d882528bd..e2ea60b4ec 100644
--- a/src/MaaCore/MaaCore.vcxproj
+++ b/src/MaaCore/MaaCore.vcxproj
@@ -85,7 +85,7 @@
-
+
@@ -238,7 +238,7 @@
-
+
diff --git a/src/MaaCore/MaaCore.vcxproj.filters b/src/MaaCore/MaaCore.vcxproj.filters
index 7482bf6f5e..f95d5fe6ad 100644
--- a/src/MaaCore/MaaCore.vcxproj.filters
+++ b/src/MaaCore/MaaCore.vcxproj.filters
@@ -570,7 +570,7 @@
源文件\Vision\Battle
-
+
源文件\Vision\Battle
@@ -971,7 +971,7 @@
源文件\Vision\Battle
-
+
源文件\Vision\Battle
diff --git a/src/MaaCore/Task/BattleHelper.cpp b/src/MaaCore/Task/BattleHelper.cpp
index 95b39d40ec..2bb2932c1d 100644
--- a/src/MaaCore/Task/BattleHelper.cpp
+++ b/src/MaaCore/Task/BattleHelper.cpp
@@ -11,8 +11,8 @@
#include "Utils/ImageIo.hpp"
#include "Utils/Logger.hpp"
#include "Utils/NoWarningCV.h"
-#include "Vision/Battle/BattleImageAnalyzer.h"
#include "Vision/Battle/BattleSkillReadyImageAnalyzer.h"
+#include "Vision/Battle/BattlefieldMatcher.h"
#include "Vision/BestMatcher.h"
#include "Vision/Matcher.h"
#include "Vision/RegionOCRer.h"
@@ -96,9 +96,10 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable)
auto draw_future = std::async(std::launch::async, [&]() { save_map(image); });
}
- BattleImageAnalyzer oper_analyzer(image);
- oper_analyzer.set_target(BattleImageAnalyzer::Target::Oper);
- if (!oper_analyzer.analyze()) {
+ BattlefieldMatcher oper_analyzer(image);
+ oper_analyzer.set_object_of_interest({ .deployment = true });
+ auto oper_result_opt = oper_analyzer.analyze();
+ if (!oper_result_opt) {
check_in_battle(image);
return false;
}
@@ -123,7 +124,7 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable)
oper.is_unusual_location = battle::get_role_usual_location(oper.role) == oper.location_type;
};
- auto cur_opers = oper_analyzer.get_opers();
+ auto& cur_opers = oper_result_opt->deployment;
std::vector unknown_opers;
for (auto& oper : cur_opers) {
@@ -224,28 +225,29 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable)
bool asst::BattleHelper::update_kills(const cv::Mat& reusable)
{
cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable;
- BattleImageAnalyzer analyzer(image);
+ BattlefieldMatcher analyzer(image);
+ analyzer.set_object_of_interest({ .kills = true });
if (m_total_kills) {
- analyzer.set_pre_total_kills(m_total_kills);
+ analyzer.set_total_kills_prompt(m_total_kills);
}
- analyzer.set_target(BattleImageAnalyzer::Target::Kills);
- if (!analyzer.analyze()) {
+ auto result_opt = analyzer.analyze();
+ if (!result_opt || !result_opt->kills) {
return false;
}
- m_kills = analyzer.get_kills();
- m_total_kills = analyzer.get_total_kills();
+ std::tie(m_kills, m_total_kills) = result_opt->kills.value();
return true;
}
bool asst::BattleHelper::update_cost(const cv::Mat& reusable)
{
cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable;
- BattleImageAnalyzer analyzer(image);
- analyzer.set_target(BattleImageAnalyzer::Target::Cost);
- if (!analyzer.analyze()) {
+ BattlefieldMatcher analyzer(image);
+ analyzer.set_object_of_interest({ .costs = true });
+ auto result_opt = analyzer.analyze();
+ if (!result_opt || !result_opt->costs) {
return false;
}
- m_cost = analyzer.get_cost();
+ m_cost = result_opt->costs.value();
return true;
}
@@ -373,8 +375,10 @@ bool asst::BattleHelper::check_pause_button(const cv::Mat& reusable)
Matcher battle_flag_analyzer(image);
battle_flag_analyzer.set_task_info("BattleOfficiallyBegin");
bool ret = battle_flag_analyzer.analyze().has_value();
- BattleImageAnalyzer battle_flag_analyzer_2(image);
- ret &= battle_flag_analyzer_2.analyze() && battle_flag_analyzer_2.get_pause_button();
+
+ BattlefieldMatcher battle_flag_analyzer_2(image);
+ auto battle_result_opt = battle_flag_analyzer_2.analyze();
+ ret &= battle_result_opt && battle_result_opt->pause_button;
return ret;
}
@@ -382,8 +386,8 @@ bool asst::BattleHelper::check_in_battle(const cv::Mat& reusable, bool weak)
{
cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable;
if (weak) {
- BattleImageAnalyzer analyzer(image);
- m_in_battle = analyzer.analyze();
+ BattlefieldMatcher analyzer(image);
+ m_in_battle = analyzer.analyze().has_value();
}
else {
m_in_battle = check_pause_button(image);
diff --git a/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp b/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp
index 4f6de94d45..e41f8416e1 100644
--- a/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp
+++ b/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp
@@ -9,9 +9,9 @@
#include "Utils/Ranges.hpp"
#include "Vision/Battle/BattleDeployDirectionImageAnalyzer.h"
#include "Vision/Battle/BattleFormationImageAnalyzer.h"
-#include "Vision/Battle/BattleImageAnalyzer.h"
#include "Vision/Battle/BattleOperatorsImageAnalyzer.h"
#include "Vision/Battle/BattleSkillReadyImageAnalyzer.h"
+#include "Vision/Battle/BattlefieldMatcher.h"
#include "Vision/BestMatcher.h"
#include "Vision/RegionOCRer.h"
@@ -193,7 +193,7 @@ bool asst::CombatRecordRecognitionTask::analyze_stage()
show_img(stage_analyzer);
if (!analyzed) {
- // BattleImageAnalyzer battle_analyzer(frame);
+ // BattlefieldMatcher battle_analyzer(frame);
// if (battle_analyzer.analyze()) {
// Log.error(i, "already start button, but still failed to analyze stage name");
// m_stage_ocr_end_frame = i;
@@ -237,9 +237,10 @@ bool asst::CombatRecordRecognitionTask::analyze_deployment()
const int skip_count = m_video_fps > m_deployment_fps ? static_cast(m_video_fps / m_deployment_fps) - 1 : 0;
- BattleImageAnalyzer oper_analyzer;
- oper_analyzer.set_target(BattleImageAnalyzer::Target::Oper);
+ BattlefieldMatcher oper_analyzer;
+ oper_analyzer.set_object_of_interest({ .deployment = true });
+ std::vector deployment;
for (size_t i = m_stage_ocr_end_frame; i < m_video_frame_count; i += skip_frames(skip_count) + 1) {
cv::Mat frame;
*m_video_ptr >> frame;
@@ -252,14 +253,15 @@ bool asst::CombatRecordRecognitionTask::analyze_deployment()
cv::resize(frame, frame, cv::Size(), m_scale, m_scale, cv::INTER_AREA);
oper_analyzer.set_image(frame);
- bool analyzed = oper_analyzer.analyze() && oper_analyzer.get_pause_button();
+ auto oper_result_opt = oper_analyzer.analyze();
+ bool analyzed = oper_result_opt && oper_result_opt->pause_button;
show_img(oper_analyzer);
if (analyzed) {
m_battle_start_frame = i;
+ deployment = std::move(oper_result_opt->deployment);
break;
}
}
- const auto& deployment = oper_analyzer.get_opers();
auto avatar_task_ptr = Task.get("BattleAvatarDataForFormation");
for (const auto& [name, formation_avatar] : m_formation) {
@@ -352,14 +354,18 @@ bool asst::CombatRecordRecognitionTask::slice_video()
}
cv::resize(frame, frame, cv::Size(), m_scale, m_scale, cv::INTER_AREA);
- BattleImageAnalyzer oper_analyzer(frame);
- oper_analyzer.set_target(BattleImageAnalyzer::Target::Oper | BattleImageAnalyzer::Target::DetailPage |
- BattleImageAnalyzer::Target::Kills);
- oper_analyzer.set_pre_total_kills(total_kills);
- bool analyzed = oper_analyzer.analyze();
- show_img(oper_analyzer);
+ BattlefieldMatcher analyzer(frame);
+ analyzer.set_object_of_interest({
+ .deployment = true,
+ .kills = true,
+ .in_detail = true,
+ });
- if (!analyzed) {
+ analyzer.set_total_kills_prompt(total_kills);
+ auto result_opt = analyzer.analyze();
+ show_img(analyzer);
+
+ if (!result_opt) {
battle_over();
if (++not_in_battle_count > 10) {
break;
@@ -369,14 +375,16 @@ bool asst::CombatRecordRecognitionTask::slice_video()
m_battle_end_frame = 0;
not_in_battle_count = 0;
- int cur_kills = oper_analyzer.get_kills();
- if (cur_kills != latest_kills) {
- m_frame_kills.emplace_back(std::make_pair(i, cur_kills));
+ if (result_opt->kills) {
+ auto& [cur_kills, cur_total_kills] = *result_opt->kills;
+ if (cur_kills != latest_kills) {
+ m_frame_kills.emplace_back(std::make_pair(i, cur_kills));
+ }
+ latest_kills = cur_kills;
+ total_kills = cur_total_kills;
}
- total_kills = oper_analyzer.get_total_kills();
- latest_kills = oper_analyzer.get_kills();
- const auto& cur_opers = oper_analyzer.get_opers();
+ const auto& cur_opers = result_opt->deployment;
bool continuity = true;
int pre_distance = 0;
for (auto iter = cur_opers.begin(); iter != cur_opers.end(); ++iter) {
@@ -390,7 +398,7 @@ bool asst::CombatRecordRecognitionTask::slice_video()
pre_distance = distance;
}
- bool in_detail_page = oper_analyzer.get_in_detail_page() || !oper_analyzer.get_pause_button() || !continuity;
+ bool in_detail_page = result_opt->in_detail || !result_opt->pause_button || !continuity;
bool oper_auto_retreat =
in_segment && continuity && !m_clips.empty() && cur_opers.size() != m_clips.back().deployment.size();
@@ -821,22 +829,22 @@ json::object asst::CombatRecordRecognitionTask::analyze_action_condition(ClipInf
{ "kills", kills },
};
- BattleImageAnalyzer analyzer;
- analyzer.set_target(BattleImageAnalyzer::Target::Cost);
-
- analyzer.set_image(pre_clip_ptr->end_frame); // 开始执行这次操作的画面
- if (!analyzer.analyze()) {
+ BattlefieldMatcher analyzer(pre_clip_ptr->end_frame); // 开始执行这次操作的画面
+ analyzer.set_object_of_interest({ .costs = true });
+ auto end_result_opt = analyzer.analyze();
+ if (!end_result_opt || !end_result_opt->costs) {
m_pre_action_costs = -1;
return condition;
}
- int start_costs = analyzer.get_cost();
+ int start_costs = end_result_opt->costs.value();
int cost_changes = start_costs - m_pre_action_costs;
if (m_pre_action_costs >= 0 && cost_changes > 0) {
condition.emplace("cost_changes", cost_changes);
}
analyzer.set_image(clip.start_frame); // 这次操作执行完了的画面
- m_pre_action_costs = analyzer.analyze() ? analyzer.get_cost() : start_costs;
+ auto start_result_opt = analyzer.analyze();
+ m_pre_action_costs = (start_result_opt && start_result_opt->costs) ? start_result_opt->costs.value() : start_costs;
return condition;
}
diff --git a/src/MaaCore/Task/Interface/DebugTask.cpp b/src/MaaCore/Task/Interface/DebugTask.cpp
index 207fceed0f..46990e6a1c 100644
--- a/src/MaaCore/Task/Interface/DebugTask.cpp
+++ b/src/MaaCore/Task/Interface/DebugTask.cpp
@@ -6,8 +6,8 @@
#include "Utils/ImageIo.hpp"
#include "Utils/Logger.hpp"
-#include "Vision/Battle/BattleImageAnalyzer.h"
#include "Vision/Battle/BattleSkillReadyImageAnalyzer.h"
+#include "Vision/Battle/BattlefieldMatcher.h"
#include "Vision/Miscellaneous/DepotImageAnalyzer.h"
#include "Vision/Miscellaneous/StageDropsImageAnalyzer.h"
@@ -15,8 +15,6 @@ asst::DebugTask::DebugTask(const AsstCallback& callback, Assistant* inst) : Inte
bool asst::DebugTask::run()
{
- test_battle_image();
-
return true;
}
@@ -66,7 +64,7 @@ void asst::DebugTask::test_battle_image()
cv::Mat image = asst::imread(utils::path("1.png"));
cv::Mat resized;
cv::resize(image, resized, cv::Size(1280, 720), 0, 0, cv::INTER_AREA);
- BattleImageAnalyzer analyzer(resized);
- analyzer.set_target(BattleImageAnalyzer::Target::Oper);
+ BattlefieldMatcher analyzer(resized);
+ analyzer.set_object_of_interest({ .deployment = true });
analyzer.analyze();
}
diff --git a/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp b/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp
index a6e69d5d87..7eb17d0cd9 100644
--- a/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp
+++ b/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp
@@ -16,8 +16,6 @@
#include "Utils/Algorithm.hpp"
#include "Utils/ImageIo.hpp"
#include "Utils/Logger.hpp"
-#include "Vision/Battle/BattleImageAnalyzer.h"
-#include "Vision/Battle/BattleSkillReadyImageAnalyzer.h"
#include "Vision/Matcher.h"
#include "Vision/RegionOCRer.h"
diff --git a/src/MaaCore/Task/Miscellaneous/BattleProcessTask.h b/src/MaaCore/Task/Miscellaneous/BattleProcessTask.h
index 1a41f0c337..cde77e8a95 100644
--- a/src/MaaCore/Task/Miscellaneous/BattleProcessTask.h
+++ b/src/MaaCore/Task/Miscellaneous/BattleProcessTask.h
@@ -5,7 +5,6 @@
#include "Common/AsstBattleDef.h"
#include "Common/AsstTypes.h"
#include "Config/Miscellaneous/TilePack.h"
-#include "Vision/Battle/BattleImageAnalyzer.h"
namespace asst
{
diff --git a/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.cpp b/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.cpp
index 864a71e9c2..5024b5076b 100644
--- a/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.cpp
+++ b/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.cpp
@@ -17,8 +17,6 @@
#include "Task/ProcessTask.h"
#include "Utils/ImageIo.hpp"
#include "Utils/Logger.hpp"
-#include "Vision/Battle/BattleImageAnalyzer.h"
-#include "Vision/Battle/BattleSkillReadyImageAnalyzer.h"
#include "Vision/Matcher.h"
#include "Vision/RegionOCRer.h"
diff --git a/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.h b/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.h
index e08b17516e..4dbf88f451 100644
--- a/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.h
+++ b/src/MaaCore/Task/Roguelike/RoguelikeBattleTaskPlugin.h
@@ -8,7 +8,6 @@
#include "Config/Miscellaneous/TilePack.h"
#include "Task/AbstractTaskPlugin.h"
#include "Task/BattleHelper.h"
-#include "Vision/Battle/BattleImageAnalyzer.h"
namespace asst
{
diff --git a/src/MaaCore/Vision/Battle/BattleImageAnalyzer.cpp b/src/MaaCore/Vision/Battle/BattleImageAnalyzer.cpp
deleted file mode 100644
index 6b87a9c455..0000000000
--- a/src/MaaCore/Vision/Battle/BattleImageAnalyzer.cpp
+++ /dev/null
@@ -1,489 +0,0 @@
-#include "BattleImageAnalyzer.h"
-
-#include "Utils/Ranges.hpp"
-#include
-
-#include "Utils/NoWarningCV.h"
-
-#include "Config/TaskData.h"
-#include "Config/TemplResource.h"
-#include "Utils/Logger.hpp"
-#include "Vision/BestMatcher.h"
-#include "Vision/Matcher.h"
-#include "Vision/MultiMatcher.h"
-#include "Vision/TemplDetOCRer.h"
-#include "Vision/RegionOCRer.h"
-
-bool asst::BattleImageAnalyzer::set_target(int target)
-{
- m_target = target;
- return true;
-}
-
-void asst::BattleImageAnalyzer::set_pre_total_kills(int pre_total_kills)
-{
- m_pre_total_kills = pre_total_kills;
-}
-
-bool asst::BattleImageAnalyzer::analyze()
-{
- clear();
-
- // flag 无论如何都识别。表明当前画面是在战斗场景的
- bool ret = flag_analyze();
- if (!ret) {
- return false;
- }
-
- //
- // if (m_target & Target::Home) {
- // ret |= home_analyze();
- //}
-
- // 可能没有干员(全上场了),所以干员识别结果不影响返回值
- if (m_target & Target::Oper) {
- opers_analyze();
- }
-
- if (m_target & Target::DetailPage) {
- detail_page_analyze();
- }
-
- if (m_target & Target::Kills) {
- ret &= kills_analyze();
- }
-
- if (m_target & Target::Cost) {
- ret &= cost_analyze();
- }
-
- // if (m_target & Target::Vacancies) {
- // ret &= vacancies_analyze();
- // }
-
- return ret;
-}
-
-const std::vector& asst::BattleImageAnalyzer::get_opers() const noexcept
-{
- return m_opers;
-}
-
-const std::vector& asst::BattleImageAnalyzer::get_homes() const noexcept
-{
- return m_homes;
-}
-
-int asst::BattleImageAnalyzer::get_hp() const noexcept
-{
- return m_hp;
-}
-
-int asst::BattleImageAnalyzer::get_kills() const noexcept
-{
- return m_kills;
-}
-
-int asst::BattleImageAnalyzer::get_total_kills() const noexcept
-{
- return m_total_kills;
-}
-
-int asst::BattleImageAnalyzer::get_cost() const noexcept
-{
- return m_cost;
-}
-
-bool asst::BattleImageAnalyzer::get_in_detail_page() const noexcept
-{
- return m_in_detail_page;
-}
-
-bool asst::BattleImageAnalyzer::get_pause_button() const noexcept
-{
- return m_pause_button;
-}
-
-void asst::BattleImageAnalyzer::clear() noexcept
-{
- m_opers.clear();
- m_homes.clear();
- m_ready_skills.clear();
- m_hp = 0;
- m_kills = 0;
- m_cost = 0;
-}
-
-void asst::BattleImageAnalyzer::sort_opers_by_cost()
-{
- // 本来游戏就是按费用排的,这里倒序一下就行了
- ranges::reverse(m_opers);
-}
-
-bool asst::BattleImageAnalyzer::opers_analyze()
-{
- MultiMatcher flags_analyzer(m_image);
- const auto& flag_task_ptr = Task.get("BattleOpersFlag");
- flags_analyzer.set_task_info(flag_task_ptr);
-#ifndef ASST_DEBUG
- flags_analyzer.set_log_tracing(false);
-#endif
- if (!flags_analyzer.analyze()) {
- return false;
- }
- auto flags = flags_analyzer.get_result();
- if (flags.empty()) {
- return false;
- }
- sort_by_horizontal_(flags);
-
- const auto click_move = Task.get("BattleOperClickRange")->rect_move;
- const auto role_move = Task.get("BattleOperRoleRange")->rect_move;
- // const auto cost_move = Task.get("BattleOperCostRange")->rect_move;
- const auto avlb_move = Task.get("BattleOperAvailable")->rect_move;
- const auto cooling_move = Task.get("BattleOperCooling")->rect_move;
- const auto avatar_move = Task.get("BattleOperAvatar")->rect_move;
- // const int unselected_y = flag_task_ptr->roi.y;
-
- size_t index = 0;
- for (const auto& flag_mrect : flags) {
- battle::DeploymentOper oper;
- oper.rect = flag_mrect.rect.move(click_move);
-
- Rect role_rect = flag_mrect.rect.move(role_move);
- oper.role = oper_role_analyze(role_rect);
- if (oper.role == battle::Role::Unknown) {
- Log.warn("Unknown role");
- continue;
- }
-
- if (oper.rect.x + oper.rect.width >= m_image.cols) {
- oper.rect.width = m_image.cols - oper.rect.x;
- }
- Rect avatar_rect = oper.rect.move(avatar_move);
- oper.avatar = m_image(make_rect(avatar_rect));
-
- Rect available_rect = flag_mrect.rect.move(avlb_move);
- oper.available = oper_available_analyze(available_rect);
-
-#ifdef ASST_DEBUG
- if (oper.available) {
- cv::rectangle(m_image_draw, make_rect(oper.rect), cv::Scalar(0, 255, 0), 2);
- }
- else {
- cv::rectangle(m_image_draw, make_rect(oper.rect), cv::Scalar(0, 0, 255), 2);
- }
-#endif
-
- Rect cooling_rect = flag_mrect.rect.move(cooling_move);
- if (cooling_rect.x + cooling_rect.width >= m_image.cols) {
- cooling_rect.width = m_image.cols - cooling_rect.x;
- }
- if (cooling_rect.y + cooling_rect.height >= m_image.rows) {
- cooling_rect.height = m_image.rows - cooling_rect.y;
- }
- oper.cooling = oper_cooling_analyze(cooling_rect);
- if (oper.cooling && oper.available) {
- Log.error("oper is available, but with cooling");
- }
-
- // 干员费用识别的不太准,暂时也没用上,先注释掉,TODO:优化费用识别
- // Rect cost_rect = flag_mrect.rect.move(cost_move);
- // oper.cost = oper_cost_analyze(cost_rect);
- oper.index = index++;
- // oper.selected = flag_mrect.rect.y < unselected_y;
-
- m_opers.emplace_back(std::move(oper));
- }
-
- return true;
-}
-
-asst::battle::Role asst::BattleImageAnalyzer::oper_role_analyze(const Rect& roi)
-{
- static const std::unordered_map RoleMap = {
- { "Caster", battle::Role::Caster }, { "Medic", battle::Role::Medic }, { "Pioneer", battle::Role::Pioneer },
- { "Sniper", battle::Role::Sniper }, { "Special", battle::Role::Special }, { "Support", battle::Role::Support },
- { "Tank", battle::Role::Tank }, { "Warrior", battle::Role::Warrior }, { "Drone", battle::Role::Drone },
- };
-
- static const std::string TaskName = "BattleOperRole";
- static const std::string Ext = ".png";
- BestMatcher role_analyzer(m_image);
-#ifndef ASST_DEBUG
- role_analyzer.set_log_tracing(false);
-#endif // !ASST_DEBUG
- role_analyzer.set_task_info(TaskName);
- role_analyzer.set_roi(roi);
-
- for (const auto& role_name : RoleMap | views::keys) {
- role_analyzer.append_templ(TaskName + role_name + Ext);
- }
- if (!role_analyzer.analyze()) {
- Log.warn(__FUNCTION__, "unknown role");
- return battle::Role::Unknown;
- }
-
- const auto& templ_name = role_analyzer.get_result().templ_info.name;
-
- std::string role_name = templ_name.substr(TaskName.size(), templ_name.size() - TaskName.size() - Ext.size());
-
-#ifdef ASST_DEBUG
- cv::putText(m_image_draw, role_name, cv::Point(roi.x, roi.y - 5), 1, 1, cv::Scalar(0, 255, 255));
-#endif
-
- return RoleMap.at(role_name);
-}
-
-bool asst::BattleImageAnalyzer::oper_cooling_analyze(const Rect& roi)
-{
- const auto cooling_task_ptr = Task.get("BattleOperCooling");
-
- auto img_roi = m_image(make_rect(roi));
- cv::Mat hsv;
- cv::cvtColor(img_roi, hsv, cv::COLOR_BGR2HSV);
- int h_low = cooling_task_ptr->mask_range.first;
- int h_up = cooling_task_ptr->mask_range.second;
- int s_low = cooling_task_ptr->specific_rect.x;
- int s_up = cooling_task_ptr->specific_rect.y;
- int v_low = cooling_task_ptr->specific_rect.width;
- int v_up = cooling_task_ptr->specific_rect.height;
-
- cv::Mat bin;
- cv::inRange(hsv, cv::Scalar(h_low, s_low, v_low), cv::Scalar(h_up, s_up, v_up), bin);
-
- int count = 0;
- for (int i = 0; i != bin.rows; ++i) {
- for (int j = 0; j != bin.cols; ++j) {
- cv::uint8_t value = bin.at(i, j);
- if (value) {
- ++count;
- }
- }
- }
- // Log.trace("oper_cooling_analyze |", count);
- return count >= cooling_task_ptr->special_params.front();
-}
-
-int asst::BattleImageAnalyzer::oper_cost_analyze(const Rect& roi)
-{
- std::ignore = roi;
- return 0;
-}
-
-bool asst::BattleImageAnalyzer::oper_available_analyze(const Rect& roi)
-{
- cv::Mat hsv;
- cv::cvtColor(m_image(make_rect(roi)), hsv, cv::COLOR_BGR2HSV);
- cv::Scalar avg = cv::mean(hsv);
- // Log.trace("oper available, mean", avg[2]);
-
- const int thres = Task.get("BattleOperAvailable")->special_params.front();
- if (avg[2] < thres) {
- return false;
- }
- return true;
-}
-
-bool asst::BattleImageAnalyzer::home_analyze()
-{
- // 颜色转换
- cv::Mat hsv;
- cv::cvtColor(m_image, hsv, cv::COLOR_BGR2HSV);
- cv::Mat bin;
- cv::inRange(hsv, cv::Scalar(104, 160, 180), cv::Scalar(107, 220, 255), bin);
-
- // 开操作降噪
- cv::Mat morph_dst;
- cv::Mat open_kernel = cv::getStructuringElement(cv::MORPH_CROSS, cv::Size(1, 1));
- cv::morphologyEx(bin, morph_dst, cv::MORPH_OPEN, open_kernel);
-
- // 霍夫线
- std::vector lines;
- cv::HoughLinesP(morph_dst, lines, 1, 60 * CV_PI / 180, 10, 20, 10);
-
- int left = INT_MAX, right = 0, top = INT_MAX, bottom = 0;
- for (auto&& l : lines) {
- left = (std::min)({ left, l[0], l[2] });
- right = (std::max)({ right, l[0], l[2] });
- top = (std::min)({ top, l[1], l[3] });
- bottom = (std::max)({ bottom, l[1], l[3] });
-
-#ifdef ASST_DEBUG
- cv::line(m_image_draw, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), cv::Scalar(0, 0, 255), 3);
-#endif
- }
- if (right == 0) {
- Log.error("home recognition error");
- return false;
- }
- Rect home_rect(left, top, right - left, bottom - top);
- m_homes.emplace_back(home_rect);
-
-#ifdef ASST_DEBUG
- cv::rectangle(m_image_draw, make_rect(home_rect), cv::Scalar(0, 255, 0), 5);
-#endif
-
- return true;
-}
-
-bool asst::BattleImageAnalyzer::hp_flag_analyze()
-{
- // 识别 HP 的那个蓝白色图标
- auto flag_task_ptr = Task.get("BattleHpFlag");
- Matcher flag_analyzer(m_image);
- flag_analyzer.set_task_info(flag_task_ptr);
- if (!flag_analyzer.analyze()) {
- // 漏怪的时候,那个图标会变成红色的,所以多识别一次
- flag_task_ptr = Task.get("BattleHpFlag2");
- flag_analyzer.set_task_info(flag_task_ptr);
- if (!flag_analyzer.analyze()) {
- return false;
- }
- }
- return true;
-}
-
-bool asst::BattleImageAnalyzer::kills_flag_analyze()
-{
- Matcher flag_analyzer(m_image);
- flag_analyzer.set_task_info("BattleKillsFlag");
- return flag_analyzer.analyze().has_value();
-}
-
-bool asst::BattleImageAnalyzer::kills_analyze()
-{
- TemplDetOCRer kills_analyzer(m_image);
- kills_analyzer.set_task_info("BattleKillsFlag", "BattleKills");
- kills_analyzer.set_replace(Task.get("NumberOcrReplace")->replace_map);
-
- if (!kills_analyzer.analyze()) {
- return false;
- }
-
- std::string kills_text = kills_analyzer.get_result().front().text;
- if (kills_text.empty()) {
- return false;
- }
-
- size_t pos = kills_text.find('/');
- if (pos == std::string::npos) {
- Log.warn("cannot found flag /");
- // 这种时候绝大多数是把 "0/41" 中的 '/' 识别成了别的什么东西(其中又有绝大部分情况是识别成了 '1')
- // 所以这里依赖 m_pre_total_kills 转一下
- if (m_pre_total_kills <= 0) {
- // 第一次识别就识别错了,识别成了 "0141"
- if (kills_text.at(0) == '0') {
- pos = 1;
- }
- else {
- Log.error("m_pre_total_kills is zero");
- return false;
- }
- }
- else {
- size_t pre_pos = kills_text.find(std::to_string(m_pre_total_kills));
- if (pre_pos == std::string::npos || pre_pos == 0) {
- Log.error("can't get pre_pos");
- return false;
- }
- Log.trace("pre total kills pos:", pre_pos);
- pos = pre_pos - 1;
- }
- }
-
- // 例子中的"0"
- std::string kills_count = kills_text.substr(0, pos);
- if (kills_count.empty() || !ranges::all_of(kills_count, [](char c) -> bool { return std::isdigit(c); })) {
- return false;
- }
- int cur_kills = std::stoi(kills_count);
- m_kills = std::max(m_kills, cur_kills);
-
- // 例子中的"41"
- std::string total_kills = kills_text.substr(pos + 1, std::string::npos);
- int cur_total_kills = 0;
- if (total_kills.empty() || !ranges::all_of(total_kills, [](char c) -> bool { return std::isdigit(c); })) {
- Log.warn("total kills recognition failed, set to", m_pre_total_kills);
- cur_total_kills = m_pre_total_kills;
- }
- else {
- cur_total_kills = std::stoi(total_kills);
- }
- m_total_kills = std::max(cur_total_kills, m_pre_total_kills);
-
- Log.trace("Kills:", m_kills, "/", m_total_kills);
- return m_kills <= m_total_kills;
-}
-
-bool asst::BattleImageAnalyzer::cost_analyze()
-{
- RegionOCRer cost_analyzer(m_image);
- cost_analyzer.set_task_info("BattleCostData");
- cost_analyzer.set_replace(Task.get("NumberOcrReplace")->replace_map);
-
- if (!cost_analyzer.analyze()) {
- return false;
- }
-
- std::string cost_str = cost_analyzer.get_result().text;
-
- if (cost_str.empty() || !ranges::all_of(cost_str, [](const char& c) -> bool { return std::isdigit(c); })) {
- return false;
- }
- m_cost = std::stoi(cost_str);
-
- return true;
-}
-
-bool asst::BattleImageAnalyzer::vacancies_analyze()
-{
- return false;
-}
-
-bool asst::BattleImageAnalyzer::flag_analyze()
-{
- return pause_button_analyze() || hp_flag_analyze() || kills_flag_analyze();
-}
-
-bool asst::BattleImageAnalyzer::pause_button_analyze()
-{
- auto has_started_task_ptr = Task.get("BattleHasStarted");
- cv::Mat roi = m_image(make_rect(has_started_task_ptr->roi));
- cv::Mat roi_gray;
- cv::cvtColor(roi, roi_gray, cv::COLOR_BGR2GRAY);
- cv::Mat bin;
- const int value_threshold = has_started_task_ptr->special_params[0];
- cv::threshold(roi_gray, bin, value_threshold, 255, cv::THRESH_BINARY);
- int count = cv::countNonZero(bin);
- const int count_threshold = has_started_task_ptr->special_params[1];
- Log.trace(__FUNCTION__, "count", count, "threshold", count_threshold);
-
- m_pause_button = count > count_threshold;
- return m_pause_button;
-}
-
-bool asst::BattleImageAnalyzer::detail_page_analyze()
-{
- auto analyze = [&](const std::string& task_name) {
- auto task_ptr = Task.get(task_name);
- cv::Mat roi = m_image(make_rect(task_ptr->roi));
- cv::Mat roi_hsv;
- cv::cvtColor(roi, roi_hsv, cv::COLOR_BGR2HSV);
- cv::Mat bin1;
- cv::inRange(roi_hsv, cv::Scalar(99, 235, 235), cv::Scalar(105, 255, 255), bin1);
- int count1 = cv::countNonZero(bin1);
-
- cv::Mat bin2;
- cv::inRange(roi_hsv, cv::Scalar(99, 235, 135), cv::Scalar(105, 255, 155), bin2);
- int count2 = cv::countNonZero(bin2);
-
- const int threshold = task_ptr->special_params[0];
- Log.info("detail_page, count:", count1, count2, ", threshold:", threshold);
-
- return count1 > threshold || count2 > threshold;
- };
-
- m_in_detail_page = analyze("BattleOperDetailPageFlag") || analyze("BattleOperDetailPageOldFlag");
- return m_in_detail_page;
-}
diff --git a/src/MaaCore/Vision/Battle/BattleImageAnalyzer.h b/src/MaaCore/Vision/Battle/BattleImageAnalyzer.h
deleted file mode 100644
index 7bca839a43..0000000000
--- a/src/MaaCore/Vision/Battle/BattleImageAnalyzer.h
+++ /dev/null
@@ -1,73 +0,0 @@
-#pragma once
-#include "Vision/VisionHelper.h"
-
-#include "Common/AsstBattleDef.h"
-
-namespace asst
-{
- class BattleImageAnalyzer : public VisionHelper
- {
- public:
- enum Target // 需要识别的目标
- {
- None = 0,
- Oper = 1, // 下方的干员信息
- DetailPage = 2, // 是否点开了详情页
- Kills = 4, // 击杀数
- Cost = 8, // 费用
- };
-
- public:
- using VisionHelper::VisionHelper;
- virtual ~BattleImageAnalyzer() override = default;
-
- bool set_target(int target);
- void set_pre_total_kills(int pre_total_kills);
- bool analyze();
-
- virtual const std::vector& get_opers() const noexcept;
- virtual const std::vector& get_homes() const noexcept;
-
- int get_hp() const noexcept;
- int get_kills() const noexcept;
- int get_total_kills() const noexcept;
- int get_cost() const noexcept;
- bool get_in_detail_page() const noexcept;
- bool get_pause_button() const noexcept;
-
- void clear() noexcept;
- void sort_opers_by_cost(); // 高费在前,费用降序
-
- protected:
- bool opers_analyze(); // 识别干员
- bool detail_page_analyze(); // 识别是否在详情页
- battle::Role oper_role_analyze(const Rect& roi);
- bool oper_cooling_analyze(const Rect& roi);
- int oper_cost_analyze(const Rect& roi);
- bool oper_available_analyze(const Rect& roi);
-
- bool home_analyze(); // 识别蓝色的家门
- // bool skill_analyze(); // 识别技能是否可用
- bool kills_analyze(); // 识别击杀数
- bool cost_analyze(); // 识别费用
- bool vacancies_analyze(); // 识别剩余可部署人数
- bool flag_analyze();
- bool hp_flag_analyze();
- bool kills_flag_analyze();
- bool pause_button_analyze();
-
- int m_target = 0; // 待识别的目标
- int m_pre_total_kills = 0; // 之前的击杀总数,因为击杀数经常识别不准所以依赖外部传入作为参考
-
- /* 识别结果缓存 */
- std::vector m_opers; // 下方干员信息
- std::vector m_homes; // 蓝色的家门位置
- std::vector m_ready_skills; // 可以释放的技能(Rect实际是干员的位置)
- int m_hp = 0; // 剩余生命值
- int m_kills = 0; // 击杀数
- int m_total_kills = 0; // 击杀总数
- int m_cost = 0; // 部署费用
- bool m_in_detail_page = false;
- bool m_pause_button = false;
- };
-} // namespace asst
diff --git a/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp b/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp
new file mode 100644
index 0000000000..f8c6cd32ca
--- /dev/null
+++ b/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp
@@ -0,0 +1,360 @@
+#include "BattlefieldMatcher.h"
+
+#include "Utils/Ranges.hpp"
+#include
+
+#include "Utils/NoWarningCV.h"
+
+#include "Config/TaskData.h"
+#include "Config/TemplResource.h"
+#include "Utils/Logger.hpp"
+#include "Vision/BestMatcher.h"
+#include "Vision/Matcher.h"
+#include "Vision/MultiMatcher.h"
+#include "Vision/RegionOCRer.h"
+#include "Vision/TemplDetOCRer.h"
+
+using namespace asst;
+
+void BattlefieldMatcher::set_object_of_interest(ObjectOfInterest obj)
+{
+ m_object_of_interest = std::move(obj);
+}
+
+void BattlefieldMatcher::set_total_kills_prompt(int prompt)
+{
+ m_total_kills_prompt = prompt;
+}
+
+BattlefieldMatcher::ResultOpt BattlefieldMatcher::analyze() const
+{
+ Result result;
+
+ if (m_object_of_interest.flag) {
+ result.pause_button = pause_button_analyze();
+ if (!result.pause_button && !hp_flag_analyze() && !kills_flag_analyze()) {
+ // flag 表明当前画面是在战斗场景的,不在的就没必要识别了
+ return std::nullopt;
+ }
+ }
+
+ if (m_object_of_interest.deployment) {
+ result.deployment = deployment_analyze();
+ }
+
+ if (m_object_of_interest.kills) {
+ result.kills = kills_analyze();
+ if (!result.kills) {
+ return std::nullopt;
+ }
+ }
+
+ if (m_object_of_interest.costs) {
+ result.costs = costs_analyze();
+ if (!result.costs) {
+ return std::nullopt;
+ }
+ }
+
+ if (m_object_of_interest.in_detail) {
+ result.in_detail = in_detail_analyze();
+ }
+
+ return result;
+}
+
+std::vector BattlefieldMatcher::deployment_analyze() const
+{
+ MultiMatcher flags_analyzer(m_image);
+ const auto& flag_task_ptr = Task.get("BattleOpersFlag");
+ flags_analyzer.set_task_info(flag_task_ptr);
+
+#ifndef ASST_DEBUG
+ flags_analyzer.set_log_tracing(false);
+#endif
+
+ auto flag_opt = flags_analyzer.analyze();
+ if (!flag_opt) {
+ return {};
+ }
+ auto& flags = flag_opt.value();
+ sort_by_horizontal_(flags);
+
+ const Rect& click_move = Task.get("BattleOperClickRange")->rect_move;
+ const Rect& role_move = Task.get("BattleOperRoleRange")->rect_move;
+ const Rect& avlb_move = Task.get("BattleOperAvailable")->rect_move;
+ const Rect& cooling_move = Task.get("BattleOperCooling")->rect_move;
+ const Rect& avatar_move = Task.get("BattleOperAvatar")->rect_move;
+
+ std::vector oper_result;
+ size_t index = 0;
+ for (const auto& flag_res : flags) {
+ battle::DeploymentOper oper;
+ oper.rect = flag_res.rect.move(click_move);
+
+ Rect role_rect = flag_res.rect.move(role_move);
+ oper.role = oper_role_analyze(role_rect);
+ if (oper.role == battle::Role::Unknown) {
+ Log.warn("Unknown role");
+ continue;
+ }
+
+ if (oper.rect.x + oper.rect.width >= m_image.cols) {
+ oper.rect.width = m_image.cols - oper.rect.x;
+ }
+ Rect avatar_rect = oper.rect.move(avatar_move);
+ oper.avatar = m_image(make_rect(avatar_rect));
+
+ Rect available_rect = flag_res.rect.move(avlb_move);
+ oper.available = oper_available_analyze(available_rect);
+
+#ifdef ASST_DEBUG
+ if (oper.available) {
+ cv::rectangle(m_image_draw, make_rect(oper.rect), cv::Scalar(0, 255, 0), 2);
+ }
+ else {
+ cv::rectangle(m_image_draw, make_rect(oper.rect), cv::Scalar(0, 0, 255), 2);
+ }
+#endif
+
+ Rect cooling_rect = correct_rect(flag_res.rect.move(cooling_move), m_image);
+ oper.cooling = oper_cooling_analyze(cooling_rect);
+ if (oper.cooling && oper.available) {
+ Log.error("oper is available, but with cooling");
+ }
+
+ oper.index = index++;
+
+ oper_result.emplace_back(std::move(oper));
+ }
+
+ return oper_result;
+}
+
+battle::Role BattlefieldMatcher::oper_role_analyze(const Rect& roi) const
+{
+ static const std::unordered_map RoleMap = {
+ { "Caster", battle::Role::Caster }, { "Medic", battle::Role::Medic }, { "Pioneer", battle::Role::Pioneer },
+ { "Sniper", battle::Role::Sniper }, { "Special", battle::Role::Special }, { "Support", battle::Role::Support },
+ { "Tank", battle::Role::Tank }, { "Warrior", battle::Role::Warrior }, { "Drone", battle::Role::Drone },
+ };
+
+ static const std::string TaskName = "BattleOperRole";
+ static const std::string Ext = ".png";
+ BestMatcher role_analyzer(m_image);
+#ifndef ASST_DEBUG
+ role_analyzer.set_log_tracing(false);
+#endif // !ASST_DEBUG
+ role_analyzer.set_task_info(TaskName);
+ role_analyzer.set_roi(roi);
+
+ for (const auto& role_name : RoleMap | views::keys) {
+ role_analyzer.append_templ(TaskName + role_name + Ext);
+ }
+ auto role_opt = role_analyzer.analyze();
+ if (!role_opt) {
+ Log.warn(__FUNCTION__, "unknown role");
+ return battle::Role::Unknown;
+ }
+
+ const auto& templ_name = role_opt->templ_info.name;
+
+ std::string role_name = templ_name.substr(TaskName.size(), templ_name.size() - TaskName.size() - Ext.size());
+
+#ifdef ASST_DEBUG
+ cv::putText(m_image_draw, role_name, cv::Point(roi.x, roi.y - 5), 1, 1, cv::Scalar(0, 255, 255));
+#endif
+
+ return RoleMap.at(role_name);
+}
+
+bool BattlefieldMatcher::oper_cooling_analyze(const Rect& roi) const
+{
+ const auto cooling_task_ptr = Task.get("BattleOperCooling");
+
+ auto img_roi = m_image(make_rect(roi));
+ cv::Mat hsv;
+ cv::cvtColor(img_roi, hsv, cv::COLOR_BGR2HSV);
+ int h_low = cooling_task_ptr->mask_range.first;
+ int h_up = cooling_task_ptr->mask_range.second;
+ int s_low = cooling_task_ptr->specific_rect.x;
+ int s_up = cooling_task_ptr->specific_rect.y;
+ int v_low = cooling_task_ptr->specific_rect.width;
+ int v_up = cooling_task_ptr->specific_rect.height;
+
+ cv::Mat bin;
+ cv::inRange(hsv, cv::Scalar(h_low, s_low, v_low), cv::Scalar(h_up, s_up, v_up), bin);
+
+ int count = 0;
+ for (int i = 0; i != bin.rows; ++i) {
+ for (int j = 0; j != bin.cols; ++j) {
+ cv::uint8_t value = bin.at(i, j);
+ if (value) {
+ ++count;
+ }
+ }
+ }
+ // Log.trace("oper_cooling_analyze |", count);
+ return count >= cooling_task_ptr->special_params.front();
+}
+
+int BattlefieldMatcher::oper_cost_analyze(const Rect& roi) const
+{
+ std::ignore = roi;
+ return 0;
+}
+
+bool BattlefieldMatcher::oper_available_analyze(const Rect& roi) const
+{
+ cv::Mat hsv;
+ cv::cvtColor(m_image(make_rect(roi)), hsv, cv::COLOR_BGR2HSV);
+ cv::Scalar avg = cv::mean(hsv);
+ // Log.trace("oper available, mean", avg[2]);
+
+ const int thres = Task.get("BattleOperAvailable")->special_params.front();
+ if (avg[2] < thres) {
+ return false;
+ }
+ return true;
+}
+
+bool BattlefieldMatcher::hp_flag_analyze() const
+{
+ // 识别 HP 的那个蓝白色图标
+ Matcher flag_analyzer(m_image);
+ flag_analyzer.set_task_info("BattleHpFlag");
+ if (flag_analyzer.analyze()) {
+ return true;
+ }
+
+ // 漏怪的时候,那个图标会变成红色的,所以多识别一次
+ flag_analyzer.set_task_info("BattleHpFlag2");
+ return flag_analyzer.analyze().has_value();
+}
+
+bool BattlefieldMatcher::kills_flag_analyze() const
+{
+ Matcher flag_analyzer(m_image);
+ flag_analyzer.set_task_info("BattleKillsFlag");
+ return flag_analyzer.analyze().has_value();
+}
+
+std::optional> BattlefieldMatcher::kills_analyze() const
+{
+ TemplDetOCRer kills_analyzer(m_image);
+ kills_analyzer.set_task_info("BattleKillsFlag", "BattleKills");
+ kills_analyzer.set_replace(Task.get("NumberOcrReplace")->replace_map);
+
+ auto kills_opt = kills_analyzer.analyze();
+ if (!kills_opt) {
+ return std::nullopt;
+ }
+ const std::string& kills_text = kills_opt->front().text;
+
+ size_t pos = kills_text.find('/');
+ if (pos == std::string::npos) {
+ Log.warn("cannot found flag /");
+ // 这种时候绝大多数是把 "0/41" 中的 '/' 识别成了别的什么东西(其中又有绝大部分情况是识别成了 '1')
+ // 所以这里依赖 m_pre_total_kills 转一下
+ if (m_total_kills_prompt <= 0) {
+ // 第一次识别就识别错了,识别成了 "0141"
+ if (kills_text.at(0) != '0') {
+ Log.error("m_total_kills_prompt is zero");
+ return std::nullopt;
+ }
+ pos = 1;
+ }
+ else {
+ size_t pre_pos = kills_text.find(std::to_string(m_total_kills_prompt));
+ if (pre_pos == std::string::npos || pre_pos == 0) {
+ Log.error("can't get pre_pos");
+ return std::nullopt;
+ }
+ Log.trace("pre total kills pos:", pre_pos);
+ pos = pre_pos - 1;
+ }
+ }
+
+ // 例子中的"0"
+ std::string kills_count = kills_text.substr(0, pos);
+ if (kills_count.empty() || !ranges::all_of(kills_count, [](char c) -> bool { return std::isdigit(c); })) {
+ return std::nullopt;
+ }
+ int kills = std::stoi(kills_count);
+
+ // 例子中的"41"
+ std::string total_kills_text = kills_text.substr(pos + 1, std::string::npos);
+ int total_kills = 0;
+ if (total_kills_text.empty() || !ranges::all_of(total_kills_text, [](char c) -> bool { return std::isdigit(c); })) {
+ Log.warn("total kills recognition failed, set to", m_total_kills_prompt);
+ total_kills = m_total_kills_prompt;
+ }
+ else {
+ total_kills = std::stoi(total_kills_text);
+ }
+ total_kills = std::max(total_kills, m_total_kills_prompt);
+
+ Log.trace("Kills:", kills, "/", total_kills);
+ return std::make_pair(kills, total_kills);
+}
+
+std::optional BattlefieldMatcher::costs_analyze() const
+{
+ RegionOCRer cost_analyzer(m_image);
+ cost_analyzer.set_task_info("BattleCostData");
+ cost_analyzer.set_replace(Task.get("NumberOcrReplace")->replace_map);
+
+ auto cost_opt = cost_analyzer.analyze();
+ if (!cost_opt) {
+ return std::nullopt;
+ }
+ const std::string& cost_str = cost_opt->text;
+
+ if (cost_str.empty() || !ranges::all_of(cost_str, [](const char& c) -> bool { return std::isdigit(c); })) {
+ return std::nullopt;
+ }
+ return std::stoi(cost_str);
+}
+
+bool BattlefieldMatcher::pause_button_analyze() const
+{
+ auto has_started_task_ptr = Task.get("BattleHasStarted");
+ cv::Mat roi = m_image(make_rect(has_started_task_ptr->roi));
+ cv::Mat roi_gray;
+ cv::cvtColor(roi, roi_gray, cv::COLOR_BGR2GRAY);
+ cv::Mat bin;
+ const int value_threshold = has_started_task_ptr->special_params[0];
+ cv::threshold(roi_gray, bin, value_threshold, 255, cv::THRESH_BINARY);
+ int count = cv::countNonZero(bin);
+ const int count_threshold = has_started_task_ptr->special_params[1];
+ Log.trace(__FUNCTION__, "count", count, "threshold", count_threshold);
+
+ return count > count_threshold;
+}
+
+bool BattlefieldMatcher::in_detail_analyze() const
+{
+ auto analyze = [&](const std::string& task_name) {
+ auto task_ptr = Task.get(task_name);
+ cv::Mat roi = m_image(make_rect(task_ptr->roi));
+ cv::Mat roi_hsv;
+ cv::cvtColor(roi, roi_hsv, cv::COLOR_BGR2HSV);
+ cv::Mat bin1;
+ cv::inRange(roi_hsv, cv::Scalar(99, 235, 235), cv::Scalar(105, 255, 255), bin1);
+ int count1 = cv::countNonZero(bin1);
+
+ cv::Mat bin2;
+ cv::inRange(roi_hsv, cv::Scalar(99, 235, 135), cv::Scalar(105, 255, 155), bin2);
+ int count2 = cv::countNonZero(bin2);
+
+ const int threshold = task_ptr->special_params[0];
+ Log.info("in_detail, count:", count1, count2, ", threshold:", threshold);
+
+ return count1 > threshold || count2 > threshold;
+ };
+
+ if (!analyze("BattleOperDetailPageFlag") && !analyze("BattleOperDetailPageOldFlag")) {
+ return false;
+ }
+ return true;
+}
diff --git a/src/MaaCore/Vision/Battle/BattlefieldMatcher.h b/src/MaaCore/Vision/Battle/BattlefieldMatcher.h
new file mode 100644
index 0000000000..18a02866a1
--- /dev/null
+++ b/src/MaaCore/Vision/Battle/BattlefieldMatcher.h
@@ -0,0 +1,62 @@
+#pragma once
+#include "Vision/VisionHelper.h"
+
+#include "Common/AsstBattleDef.h"
+
+namespace asst
+{
+ class BattlefieldMatcher : public VisionHelper
+ {
+ public:
+ struct ObjectOfInterest
+ {
+ bool flag = true;
+ bool deployment = false;
+ bool kills = false;
+ bool costs = false;
+ bool in_detail = false;
+ };
+
+ struct Result
+ {
+ ObjectOfInterest object_of_interest;
+
+ std::vector deployment;
+ // kills / total_kills
+ std::optional> kills;
+ std::optional costs;
+
+ bool in_detail = false;
+ bool pause_button = false;
+ };
+
+ using ResultOpt = std::optional;
+
+ public:
+ using VisionHelper::VisionHelper;
+ virtual ~BattlefieldMatcher() override = default;
+
+ void set_object_of_interest(ObjectOfInterest obj);
+ void set_total_kills_prompt(int prompt);
+
+ ResultOpt analyze() const;
+
+ protected:
+ bool hp_flag_analyze() const;
+ bool kills_flag_analyze() const;
+ bool pause_button_analyze() const;
+
+ std::vector deployment_analyze() const; // 识别干员
+ battle::Role oper_role_analyze(const Rect& roi) const;
+ bool oper_cooling_analyze(const Rect& roi) const;
+ int oper_cost_analyze(const Rect& roi) const;
+ bool oper_available_analyze(const Rect& roi) const;
+
+ std::optional> kills_analyze() const; // 识别击杀数
+ std::optional costs_analyze() const; // 识别费用
+ bool in_detail_analyze() const; // 识别是否在详情页
+
+ ObjectOfInterest m_object_of_interest; // 待识别的目标
+ int m_total_kills_prompt = 0; // 之前的击杀总数,因为击杀数经常识别不准所以依赖外部传入作为参考
+ };
+} // namespace asst