diff --git a/resource/tasks/tasks.json b/resource/tasks/tasks.json index c514f62045..496141e8f9 100644 --- a/resource/tasks/tasks.json +++ b/resource/tasks/tasks.json @@ -3864,7 +3864,9 @@ "algorithm": "OcrDetect", "isAscii": true, "text": [], - "roi": [50, 0, 100, 40] + "roi": [50, 0, 100, 40], + "specialParams_doc": "Kill区域变化阈值, 模板匹配, 100=1.0; 正常在 0.997-1 之间波动, 少有0.995; _5->_6 的分数最高, 可达0.94", + "specialParams": [98] }, "BattleCostData": { "baseTask": "NumberOcrReplace", diff --git a/src/MaaCore/Common/AsstTypes.h b/src/MaaCore/Common/AsstTypes.h index bad4bc49b6..b44f90322b 100644 --- a/src/MaaCore/Common/AsstTypes.h +++ b/src/MaaCore/Common/AsstTypes.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -215,6 +216,37 @@ struct Rect Rect move(Rect move) const { return { x + move.x, y + move.y, move.width, move.height }; } + // 创建一个包含所有传入Rect的最小包围盒 + static Rect bounding_box(const std::vector& rects) + { + if (rects.empty()) { + return {}; + } + + int min_x = INT_MAX; + int min_y = INT_MAX; + int max_x = INT_MIN; + int max_y = INT_MIN; + + for (const auto& rect : rects) { + min_x = std::min(min_x, rect.x); + min_y = std::min(min_y, rect.y); + max_x = std::max(max_x, rect.x + rect.width); + max_y = std::max(max_y, rect.y + rect.height); + } + + return { min_x, min_y, max_x - min_x, max_y - min_y }; + } + + // 创建一个包含所有传入Rect的最小包围盒 + template + requires(std::same_as, Rect> && ...) + static Rect bounding_box(const Rect& first, const Args&... args) + { + std::vector rects = { first, args... }; + return bounding_box(rects); + } + int x = 0; int y = 0; int width = 0; diff --git a/src/MaaCore/Task/BattleHelper.cpp b/src/MaaCore/Task/BattleHelper.cpp index 1178607ca0..f61a9a8bf4 100644 --- a/src/MaaCore/Task/BattleHelper.cpp +++ b/src/MaaCore/Task/BattleHelper.cpp @@ -330,19 +330,21 @@ cv::Mat asst::BattleHelper::get_top_view(const cv::Mat& cam_img, bool side) return result; } -bool asst::BattleHelper::update_kills(const cv::Mat& reusable) +bool asst::BattleHelper::update_kills(const cv::Mat& image, const cv::Mat& image_prev) { - cv::Mat image = reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable; BattlefieldMatcher analyzer(image); analyzer.set_object_of_interest({ .kills = true }); + analyzer.set_image_prev(image_prev); if (m_total_kills) { analyzer.set_total_kills_prompt(m_total_kills); } auto result_opt = analyzer.analyze(); - if (!result_opt || !result_opt->kills) { + if (!result_opt || result_opt->kills.status == BattlefieldMatcher::MatchStatus::Invalid) { return false; } - std::tie(m_kills, m_total_kills) = result_opt->kills.value(); + if (result_opt->kills.status == BattlefieldMatcher::MatchStatus::Success) { + std::tie(m_kills, m_total_kills) = result_opt->kills.value; + } return true; } @@ -924,7 +926,7 @@ bool asst::BattleHelper::move_camera(const std::pair& delta) LogTraceFunction; Log.info("move", delta.first, delta.second); - update_kills(); + update_kills(m_inst_helper.ctrler()->get_image()); // 还没转场的时候 if (m_kills != 0) { diff --git a/src/MaaCore/Task/BattleHelper.h b/src/MaaCore/Task/BattleHelper.h index 92dd304d5e..9d552db543 100644 --- a/src/MaaCore/Task/BattleHelper.h +++ b/src/MaaCore/Task/BattleHelper.h @@ -53,7 +53,7 @@ protected: std::vector& cur_opers, const std::vector& old_deployment_opers, bool stop_on_unknown); - bool update_kills(const cv::Mat& reusable = cv::Mat()); + bool update_kills(const cv::Mat& image, const cv::Mat& image_prev = cv::Mat()); bool update_cost(const cv::Mat& image, const cv::Mat& image_prev = cv::Mat()); cv::Mat get_top_view(const cv::Mat& cam_img, bool side = true); diff --git a/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp b/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp index b2ca711ce6..67b7ebbf78 100644 --- a/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp +++ b/src/MaaCore/Task/Experiment/CombatRecordRecognitionTask.cpp @@ -377,8 +377,8 @@ bool asst::CombatRecordRecognitionTask::slice_video() m_battle_end_frame = 0; not_in_battle_count = 0; - if (result_opt->kills) { - auto& [cur_kills, cur_total_kills] = *result_opt->kills; + if (result_opt->kills.status == BattlefieldMatcher::MatchStatus::Success) { + auto& [cur_kills, cur_total_kills] = result_opt->kills.value; if (cur_kills != latest_kills) { m_frame_kills.emplace_back(std::make_pair(i, cur_kills)); } diff --git a/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp b/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp index 2804580d31..fdf901a4b0 100644 --- a/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp +++ b/src/MaaCore/Task/Miscellaneous/BattleProcessTask.cpp @@ -340,7 +340,7 @@ bool asst::BattleProcessTask::wait_condition(const Action& action) if (m_kills < action.kills) { update_image_if_empty(); while (!need_exit() && m_kills < action.kills) { - update_kills(image); + update_kills(image, image_prev); if (m_kills >= action.kills) { break; } diff --git a/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp b/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp index 7e28517f6f..81dff19e32 100644 --- a/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp +++ b/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp @@ -49,7 +49,7 @@ BattlefieldMatcher::ResultOpt BattlefieldMatcher::analyze() const if (m_object_of_interest.kills) { result.kills = kills_analyze(); - if (!result.kills) { + if (result.kills.status == MatchStatus::Invalid) { return std::nullopt; } } @@ -271,15 +271,18 @@ bool BattlefieldMatcher::kills_flag_analyze() const return flag_analyzer.analyze().has_value(); } -std::optional> BattlefieldMatcher::kills_analyze() const +BattlefieldMatcher::MatchResult> BattlefieldMatcher::kills_analyze() const { + if (hit_kills_cache()) { + return { .status = MatchStatus::HitCache }; + } 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; + return {}; } const std::string& kills_text = kills_opt->front().text; @@ -292,7 +295,7 @@ std::optional> BattlefieldMatcher::kills_analyze() const // 第一次识别就识别错了,识别成了 "0141" if (kills_text.at(0) != '0') { Log.error("m_total_kills_prompt is zero"); - return std::nullopt; + return {}; } pos = 1; } @@ -300,7 +303,7 @@ std::optional> BattlefieldMatcher::kills_analyze() const 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; + return {}; } Log.trace("pre total kills pos:", pre_pos); pos = pre_pos - 1; @@ -310,7 +313,7 @@ std::optional> BattlefieldMatcher::kills_analyze() const // 例子中的"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; + return {}; } int kills = std::stoi(kills_count); @@ -327,7 +330,35 @@ std::optional> BattlefieldMatcher::kills_analyze() const total_kills = std::max(total_kills, m_total_kills_prompt); Log.trace("Kills:", kills, "/", total_kills); - return std::make_pair(kills, total_kills); + return { .value = std::make_pair(kills, total_kills), .status = MatchStatus::Success }; +} + +bool asst::BattlefieldMatcher::hit_kills_cache() const +{ + if (m_image_prev.empty() || m_image.cols != m_image_prev.cols || m_image.rows != m_image_prev.rows) { + return false; + } + Matcher flag_match(m_image); + flag_match.set_task_info("BattleKillsFlag"); + if (!flag_match.analyze()) { + return false; + } + const auto& flag_rect = flag_match.get_result().rect; + const auto& task = Task.get("BattleKills"); + const auto& roi = flag_rect.move(task->roi); + + cv::Mat kills_image_cache = make_roi(m_image_prev, roi); + cv::Mat kills_image = make_roi(m_image, roi); + cv::cvtColor(kills_image_cache, kills_image_cache, cv::COLOR_BGR2GRAY); + cv::cvtColor(kills_image, kills_image, cv::COLOR_BGR2GRAY); + cv::Mat match; + cv::matchTemplate(kills_image, kills_image_cache, match, cv::TM_CCOEFF_NORMED); + double mark; + cv::minMaxLoc(match, nullptr, &mark); + // 正常在 0.997-1 之间波动, 少有0.995 + // _5->_6 的分数最高, 可达0.94 + const double threshold = static_cast(task->special_params[0]) / 100; + return mark > threshold; } bool BattlefieldMatcher::cost_symbol_analyze() const diff --git a/src/MaaCore/Vision/Battle/BattlefieldMatcher.h b/src/MaaCore/Vision/Battle/BattlefieldMatcher.h index cb350cbf5a..9298803ca9 100644 --- a/src/MaaCore/Vision/Battle/BattlefieldMatcher.h +++ b/src/MaaCore/Vision/Battle/BattlefieldMatcher.h @@ -37,7 +37,7 @@ public: { ObjectOfInterest object_of_interest; std::vector deployment; - std::optional> kills; // kills / total_kills + MatchResult> kills; // kills / total_kills MatchResult costs; // bool in_detail = false; @@ -67,9 +67,11 @@ protected: int oper_cost_analyze(const Rect& roi) const; bool oper_available_analyze(const Rect& roi) const; - std::optional> kills_analyze() const; // 识别击杀数 - bool cost_symbol_analyze() const; // 识别费用左侧图标 - MatchResult costs_analyze() const; // 识别费用 + MatchResult> kills_analyze() const; // 识别击杀数 + // 识别是否持有费用是否命中缓存 + bool hit_kills_cache() const; + bool cost_symbol_analyze() const; // 识别费用左侧图标 + MatchResult costs_analyze() const; // 识别费用 // 识别是否持有费用是否命中缓存 bool hit_costs_cache() const; bool in_detail_analyze() const; // 识别是否在详情页