Compare commits

...

9 Commits

Author SHA1 Message Date
status102
210eceb856 fix: 漏啦 2025-06-07 20:46:17 +08:00
status102
cd606c2ed0 perf: 拆分 2025-06-07 20:42:21 +08:00
pre-commit-ci[bot]
47aca4d711 chore: Auto update by pre-commit hooks [skip changelog] 2025-06-07 10:33:07 +00:00
status102
2bc4c99a34 chore: test code 2025-06-07 18:31:57 +08:00
pre-commit-ci[bot]
51ecf9323c chore: Auto update by pre-commit hooks [skip changelog] 2025-06-07 10:11:39 +00:00
status102
931a403b7c perf: 减少一次HSV转换 2025-06-07 18:10:15 +08:00
status102
3d375d24a7 perf: 小c单独识别 2025-06-07 18:10:14 +08:00
status102
c7ef04f2ce perf: 合并检测 2025-06-07 18:10:14 +08:00
status102
43c1e4bece feat: 干员缓存 2025-06-07 18:10:03 +08:00
10 changed files with 210 additions and 35 deletions

View File

@@ -3905,6 +3905,16 @@
"将干员滑动到场上的 最小滑动 duration"
]
},
"BattleOperCache": {
"template": "empty.png",
"rectMove": [-38, 7, 55, 23],
"specialParams_doc": [
"前后帧干员role+cost区域RGB像素变化阈值",
"变化像素数量阈值",
"干员头像区域模板匹配分数阈值"
],
"specialParams": [10, 10, 98]
},
"BattleOpersFlag": {
"templThreshold": 0.65,
"roi": [0, 588, 1280, 18],

View File

@@ -110,6 +110,8 @@ public:
private:
void append_callback(AsstMsg msg, const json::value& detail);
public:
static void append_callback_for_inst(AsstMsg msg, const json::value& detail, Assistant* inst);
private:

View File

@@ -3,6 +3,7 @@
#include <future>
#include <thread>
#include "Assistant.h"
#include "Config/GeneralConfig.h"
#include "Config/Miscellaneous/AvatarCacheManager.h"
#include "Config/Miscellaneous/BattleDataConfig.h"
@@ -96,16 +97,13 @@ bool asst::BattleHelper::abandon()
return ProcessTask(this_task(), { "RoguelikeBattleExitBegin" }).run();
}
bool asst::BattleHelper::update_deployment_(
bool asst::BattleHelper::analyze_deployment(
std::vector<battle::DeploymentOper>& cur_opers,
const std::vector<battle::DeploymentOper>& old_deployment_opers,
const bool analyze_unknown)
{
LogTraceFunction;
// ————————————————————————————————————————
// 准备 lambda 函数
// ————————————————————————————————————————
// 设置干员名与部署位类型(近战/远程)
auto set_oper_name = [&](DeploymentOper& oper, const std::string& name) {
oper.name = name;
@@ -113,16 +111,12 @@ bool asst::BattleHelper::update_deployment_(
oper.is_usual_location = battle::get_role_usual_location(oper.role) == oper.location_type;
};
// ————————————————————————————————————————
// 初始化变量
// ————————————————————————————————————————
m_cur_deployment_opers = std::vector<battle::DeploymentOper>();
m_cur_deployment_opers.reserve(cur_opers.size());
std::vector<DeploymentOper> unknown_opers;
// ————————————————————————————————————————
// 匹配已知干员
// ————————————————————————————————————————
for (auto& oper : cur_opers) {
bool is_analyzed = false;
if (!old_deployment_opers.empty()) {
@@ -132,7 +126,7 @@ bool asst::BattleHelper::update_deployment_(
views::transform([&](const battle::DeploymentOper& temp_oper) {
return std::make_pair(temp_oper.name, temp_oper.avatar);
});
const auto& result = analyze_oper_with_cache(oper, avatar);
const auto& result = match_oper_with_avatar(oper, avatar);
if (result) {
set_oper_name(oper, result->templ_info.name);
remove_cooling_from_battlefield(oper);
@@ -141,7 +135,7 @@ bool asst::BattleHelper::update_deployment_(
}
if (!is_analyzed) {
// 之前的干员都没匹配上,那就把所有的干员都加进去
const auto& result2 = analyze_oper_with_cache(oper, AvatarCache.get_avatars(oper.role));
const auto& result2 = match_oper_with_avatar(oper, AvatarCache.get_avatars(oper.role));
if (result2) {
set_oper_name(oper, result2->templ_info.name);
remove_cooling_from_battlefield(oper);
@@ -163,9 +157,7 @@ bool asst::BattleHelper::update_deployment_(
}
}
// ————————————————————————————————————————
// 匹配未知非冷却干员
// ————————————————————————————————————————
if (ranges::count_if(unknown_opers, [](const DeploymentOper& it) { return !it.cooling; }) > 0) {
// 一个都没匹配上的,挨个点开来看一下
LogTraceScope("rec unknown opers");
@@ -231,6 +223,7 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable, b
}
}
static cv::Mat image_prev; // 缓存的上次图像
cv::Mat image = init || reusable.empty() ? m_inst_helper.ctrler()->get_image() : reusable;
if (init) {
auto draw_future = std::async(std::launch::async, [&]() { save_map(image); });
@@ -245,14 +238,38 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable, b
else {
oper_analyzer.set_object_of_interest({ .deployment = true });
}
if (init) {
image_prev = cv::Mat(); // 重新开始时清空上次图像
}
else {
oper_analyzer.set_image_prev(image_prev);
}
auto oper_result_opt = oper_analyzer.analyze();
if (!oper_result_opt) {
image_prev = image.clone(); // 记录上次图像
if (!oper_result_opt || oper_result_opt->deployment.status == BattlefieldMatcher::MatchStatus::Invalid) {
check_in_battle(image);
return false;
}
const auto old_deployment_opers = std::move(m_cur_deployment_opers);
if (!update_deployment_(oper_result_opt->deployment, old_deployment_opers, false)) {
if (oper_result_opt->deployment.status == BattlefieldMatcher::MatchStatus::HitCache) {
m_cur_deployment_opers = old_deployment_opers;
for (const auto& oper : m_cur_deployment_opers) {
Log.info(__FUNCTION__, "hit cache, oper count:", m_cur_deployment_opers.size(), "name:", oper.name);
}
if (m_cur_deployment_opers.empty()) {
Log.info(__FUNCTION__, "hit cache, no oper");
}
Assistant::append_callback_for_inst(
AsstMsg::SubTaskExtraInfo,
json::value { { "what", "BattleCacheTest" },
{ "details",
{
{ "hit", true },
{ "score", oper_analyzer.get_score() },
} } },
m_inst_helper.inst());
}
else if (!analyze_deployment(oper_result_opt->deployment.value, old_deployment_opers, false)) {
// 发现未知干员,暂停游戏后再重新识别干员
do {
pause();
@@ -285,10 +302,32 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable, b
check_in_battle(image);
return false;
}
update_deployment_(oper_result_opt->deployment, old_deployment_opers, true);
analyze_deployment(oper_result_opt->deployment.value, old_deployment_opers, true);
pause();
cancel_oper_selection();
image = m_inst_helper.ctrler()->get_image();
std::vector<std::string> names;
for (const auto& oper : oper_result_opt->deployment.value) {
names.emplace_back(oper.name);
}
Log.info("miss cache with unknown, oper count:", m_cur_deployment_opers.size(), "name:", names);
}
else {
std::vector<std::string> names;
for (const auto& oper : oper_result_opt->deployment.value) {
names.emplace_back(oper.name);
}
Log.info("miss cache, oper count:", m_cur_deployment_opers.size(), "name:", names);
Assistant::append_callback_for_inst(
AsstMsg::SubTaskExtraInfo,
json::value { { "what", "BattleCacheTest" },
{ "details",
{
{ "hit", false },
{ "score", oper_analyzer.get_score() },
} } },
m_inst_helper.inst());
}
if (init) {
@@ -1002,7 +1041,7 @@ std::optional<asst::Rect> asst::BattleHelper::get_oper_rect_on_deployment(const
template <typename T>
requires asst::ranges::range<T> && asst::OperAvatarPair<asst::ranges::range_value_t<T>>
std::optional<asst::BestMatcher::Result>
asst::BattleHelper::analyze_oper_with_cache(const asst::battle::DeploymentOper& oper, T&& avatar_cache)
asst::BattleHelper::match_oper_with_avatar(const asst::battle::DeploymentOper& oper, T&& avatar_cache)
{
BestMatcher avatar_analyzer(oper.avatar);
avatar_analyzer.set_method(MatchMethod::Ccoeff);

View File

@@ -47,9 +47,10 @@ protected:
bool speed_up();
bool abandon();
// 获取并分析部署区的干员
bool update_deployment(bool init = false, const cv::Mat& reusable = cv::Mat(), bool need_oper_cost = false);
// 更新部署区的干员仅当存在未识别干员且不处于冷却中return false
bool update_deployment_(
// 分析部署区的干员, 拿到名字, 仅当存在未识别干员且不处于冷却中return false
bool analyze_deployment(
std::vector<battle::DeploymentOper>& cur_opers,
const std::vector<battle::DeploymentOper>& old_deployment_opers,
bool stop_on_unknown);
@@ -96,10 +97,11 @@ protected:
std::string analyze_detail_page_oper_name(const cv::Mat& image);
std::optional<Rect> get_oper_rect_on_deployment(const std::string& name) const;
// 从干员头像list中, 匹配干员
template <typename T>
requires asst::ranges::range<T> && OperAvatarPair<asst::ranges::range_value_t<T>>
std::optional<asst::BestMatcher::Result>
analyze_oper_with_cache(const battle::DeploymentOper& oper, T&& avatar_cache);
match_oper_with_avatar(const battle::DeploymentOper& oper, T&& avatar_cache);
// 从场上干员和已占用格子中移除冷却中的干员
void remove_cooling_from_battlefield(const battle::DeploymentOper& oper);
@@ -116,6 +118,7 @@ protected:
std::unordered_map<std::string, std::chrono::steady_clock::time_point> m_last_use_skill_time;
int m_camera_count = 0;
std::pair<double, double> m_camera_shift = { 0., 0. };
cv::Mat m_image_prev_for_deploy; // 缓存图像, 用于判断部署区干员是否变化, 仅用于更新部署区
/* 实时更新的数据 */
bool m_in_battle = false;

View File

@@ -259,7 +259,7 @@ bool asst::CombatRecordRecognitionTask::analyze_deployment()
show_img(oper_analyzer);
if (analyzed) {
m_battle_start_frame = i;
deployment = std::move(oper_result_opt->deployment);
deployment = std::move(oper_result_opt->deployment.value);
break;
}
}
@@ -389,8 +389,8 @@ bool asst::CombatRecordRecognitionTask::slice_video()
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) {
if (iter == cur_opers.begin()) {
for (auto iter = cur_opers.value.begin(); iter != cur_opers.value.end(); ++iter) {
if (iter == cur_opers.value.begin()) {
continue;
}
int distance = std::abs(iter->rect.x - (iter - 1)->rect.x);
@@ -402,7 +402,7 @@ bool asst::CombatRecordRecognitionTask::slice_video()
bool oper_is_clicked = !result_opt->speed_button || !result_opt->pause_button;
bool oper_auto_retreat =
in_segment && continuity && !m_clips.empty() && cur_opers.size() != m_clips.back().deployment.size();
in_segment && continuity && !m_clips.empty() && cur_opers.value.size() != m_clips.back().deployment.size();
if (oper_is_clicked || oper_auto_retreat) {
if (m_clips.empty()) {
@@ -429,7 +429,7 @@ bool asst::CombatRecordRecognitionTask::slice_video()
ClipInfo info;
info.start_frame_index = i; // 后处理会加个 offset
info.end_frame_index = i;
info.deployment = cur_opers;
info.deployment = cur_opers.value;
info.start_frame = frame;
m_clips.emplace_back(std::move(info));

View File

@@ -23,6 +23,31 @@ asst::DebugTask::DebugTask(const AsstCallback& callback, Assistant* inst) :
bool asst::DebugTask::run()
{
auto image = imread(utils::path("C:/users/status102/desktop/fight-.png"));
auto start_time = std::chrono::steady_clock::now();
for (int i = 0; i < 100; i++) {
BattlefieldMatcher match(image);
match.set_object_of_interest({ .deployment = true });
auto result = match.analyze();
if (!result) {
Log.error("BattlefieldMatcher analyze failed");
return false;
}
}
auto cost = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start_time);
Log.info(__FUNCTION__, "cost:", cost.count(), "ms");
auto cd1 = imread(utils::path("C:\\Users\\status102\\desktop/deploy_cost0.png"));
auto cd2 = imread(utils::path("C:\\Users\\status102\\desktop/deploy_cost1.png"));
BattlefieldMatcher match(cd2);
match.set_image_prev(cd1);
match.set_object_of_interest({ .deployment = true });
if (match.analyze()) {
Log.info("active");
}
else {
Log.info("inactive");
}
callback(AsstMsg::SubTaskCompleted, json::value { { "subtask", "DebugTask" } });
return true;
}

View File

@@ -66,7 +66,7 @@ bool asst::SSSBattleProcessTask::update_deployment_with_skip(const cv::Mat& reus
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_same_time).count() > 30000) {
// 30s 能回 60 费,基本上已经到了挂机的时候,放缓检查的速度
Log.trace("30s is unchanged and the waiting time is extended to 1s");
interval_time = 1000;
// interval_time = 1000;
}
}
else {

View File

@@ -39,7 +39,7 @@ BattlefieldMatcher::ResultOpt BattlefieldMatcher::analyze() const
result.pause_button = pause_button_analyze();
if (!result.pause_button && !hp_flag_analyze() && !kills_flag_analyze() && !cost_symbol_analyze()) {
// flag 表明当前画面是在战斗场景的,不在的就没必要识别了
return std::nullopt;
return std::nullopt; // 测试时注释
}
}
@@ -73,11 +73,10 @@ BattlefieldMatcher::ResultOpt BattlefieldMatcher::analyze() const
return result;
}
std::vector<battle::DeploymentOper> BattlefieldMatcher::deployment_analyze() const
BattlefieldMatcher::MatchResult<std::vector<battle::DeploymentOper>> BattlefieldMatcher::deployment_analyze() const
{
const auto& flag_task_ptr = Task.get("BattleOpersFlag");
MultiMatcher flags_analyzer(m_image);
flags_analyzer.set_task_info(flag_task_ptr);
flags_analyzer.set_task_info("BattleOpersFlag");
#ifndef ASST_DEBUG
flags_analyzer.set_log_tracing(false);
@@ -89,6 +88,10 @@ std::vector<battle::DeploymentOper> BattlefieldMatcher::deployment_analyze() con
}
auto& flags = flag_opt.value();
sort_by_horizontal_(flags);
if (hit_deployment_cache(flags)) {
Log.info("Hit deployment cache, skip analyze");
return { .status = MatchStatus::HitCache };
}
const Rect& click_move = Task.get("BattleOperClickRange")->rect_move;
const Rect& role_move = Task.get("BattleOperRoleRange")->rect_move;
@@ -156,7 +159,86 @@ std::vector<battle::DeploymentOper> BattlefieldMatcher::deployment_analyze() con
oper_result.emplace_back(std::move(oper));
}
return oper_result;
return { .value = std::move(oper_result), .status = MatchStatus::Success };
}
bool asst::BattlefieldMatcher::hit_deployment_cache(const asst::MultiMatcher::ResultsVec& flags) const
{
if (m_image_prev.empty() || m_image.cols != m_image_prev.cols || m_image.rows != m_image_prev.rows) {
return false;
}
const auto& cache_task = Task.get("BattleOperCache");
const Rect& click_move = Task.get("BattleOperClickRange")->rect_move;
const Rect& avatar_move = Task.get("BattleOperAvatar")->rect_move;
const auto& dist = [](const asst::Rect& a, const asst::Rect& b) {
return std::sqrt(std::pow(a.x - b.x, 2) + std::pow(a.y - b.y, 2));
};
MultiMatcher flags_prev_ana(m_image_prev);
flags_prev_ana.set_task_info("BattleOpersFlag");
flags_prev_ana.set_log_tracing(false);
auto flags_prev_opt = flags_prev_ana.analyze();
if (!flags_prev_opt) {
return false; // 新图有干员, 但上一帧没有
}
auto& flags_prev = flags_prev_opt.value();
if (flags.size() != flags_prev.size()) {
return false; // 新图干员数量和上一帧不一致
}
sort_by_horizontal_(flags_prev);
bool is_same = true;
cv::Mat mask = cv::Mat(m_image.rows, m_image.cols, CV_8UC1, cv::Scalar(0));
cv::Mat avai_image, avai_image_prev, cache_det;
cv::Scalar avg, avg_prev;
for (size_t i = 0; i < flags.size(); ++i) {
if (dist(flags[i].rect, flags_prev[i].rect) > 3) {
is_same = false;
break; // 新图干员位置和上一帧不一致
}
const auto& flag_res = flags[i];
const auto& avatar_rect = flag_res.rect.move(click_move).move(avatar_move);
mask(make_rect<cv::Rect>(avatar_rect)).setTo(cv::Scalar(255));
cv::absdiff(
m_image(make_rect<cv::Rect>(flag_res.rect.move(cache_task->rect_move))),
m_image_prev(make_rect<cv::Rect>(flag_res.rect.move(cache_task->rect_move))),
cache_det);
cv::inRange(cache_det, cv::Scalar::all(cache_task->special_params[0]), cv::Scalar::all(255), cache_det);
int count = cv::countNonZero(cache_det);
if (count > cache_task->special_params[1]) { // 用于区分干员可用状态变化
Log.debug(__FUNCTION__, "oper cache changed, count:", count);
is_same = false;
break; // 新图干员缓存和上一帧不一致
}
}
if (!is_same) {
return false;
}
cv::Mat match;
/*
const auto& deploy_rect = Rect::bounding_box(
flags.front().rect, // 首干员c标
flags.front().rect.move(click_move).move(avatar_move), // 首干员头像
flags.back().rect.move(click_move).move(avatar_move)); // 尾干员头像
mask(make_rect<cv::Rect>(deploy_rect)).setTo(cv::Scalar(255));
mask.rowRange(60, 84).setTo(cv::Scalar(0)); // 剔除cd时间的区域
*/
cv::Rect cut_rect = cv::boundingRect(mask);
cv::matchTemplate(m_image(cut_rect), m_image_prev(cut_rect), match, cv::TM_SQDIFF_NORMED, mask(cut_rect));
double score;
cv::minMaxLoc(match, nullptr, &score);
double threshold = static_cast<double>(cache_task->special_params[2]) / 100;
if ((1 - score) > threshold) {
LogInfo << __FUNCTION__ << "hit cache, score:" << score;
return true;
}
else {
LogInfo << __FUNCTION__ << "miss cache, score:" << score;
}
return false;
}
battle::Role BattlefieldMatcher::oper_role_analyze(const Rect& roi) const

View File

@@ -2,6 +2,7 @@
#include "Vision/VisionHelper.h"
#include "Common/AsstBattleDef.h"
#include "Vision/MultiMatcher.h"
namespace asst
{
@@ -36,7 +37,8 @@ public:
struct Result
{
ObjectOfInterest object_of_interest;
std::vector<battle::DeploymentOper> deployment;
BattlefieldMatcher::MatchResult<std::vector<battle::DeploymentOper>> deployment;
MatchResult<std::pair<int, int>> kills; // kills / total_kills
MatchResult<int> costs;
@@ -54,14 +56,19 @@ public:
void set_object_of_interest(ObjectOfInterest obj);
void set_total_kills_prompt(int prompt);
void set_image_prev(const cv::Mat& image);
std::vector<double> get_score() const { return m_score; }
ResultOpt analyze() const;
protected:
bool hp_flag_analyze() const;
bool kills_flag_analyze() const;
bool pause_button_analyze() const;
std::vector<battle::DeploymentOper> deployment_analyze() const; // 识别干员
// 识别干员
MatchResult<std::vector<battle::DeploymentOper>> deployment_analyze() const;
// 识别干员是否命中缓存
bool hit_deployment_cache(const asst::MultiMatcher::ResultsVec& flags) 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;
@@ -79,6 +86,7 @@ protected:
ObjectOfInterest m_object_of_interest; // 待识别的目标
int m_total_kills_prompt = 0; // 之前的击杀总数,因为击杀数经常识别不准所以依赖外部传入作为参考
cv::Mat m_image_prev; // 缓存图像, 用于判断费用, 击杀数是否变化. 无变化则不重新识别
cv::Mat m_image_prev; // 缓存图像, 用于判断费用, 击杀数, 待部署区是否变化. 无变化则不重新识别
mutable std::vector<double> m_score; // 识别分数, 测试代码
};
} // namespace asst

View File

@@ -1353,6 +1353,12 @@ namespace MaaWpfGui.Main
string what = details["what"]?.ToString() ?? string.Empty;
switch (what)
{
case "BattleCacheTest":
{
Instances.CopilotViewModel.AddLog($"hit cache: {subTaskDetails["hit"]}, score: [{string.Join(",", subTaskDetails["score"].Select(i => i.ToObject<double>().ToString("0.######")))}]", UiLogColor.Info);
break;
}
case "StageDrops":
{
string allDrops = string.Empty;