From 416ecca979d8608e41455d8a31977ecdfd366bc1 Mon Sep 17 00:00:00 2001 From: Aliothmoon <107878625+Aliothmoon@users.noreply.github.com> Date: Fri, 15 May 2026 19:22:29 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20masked=20TM=5FCCOE?= =?UTF-8?q?FF=5FNORMED=20=E5=8C=B9=E9=85=8D=E6=80=A7=E8=83=BD=20(#16593)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: FFT/sparse fast path for masked TM_CCOEFF_NORMED * perf: optimize masked ccoeff matching * perf: tune masked ccoeff dispatcher and remove fixed overhead - sync_cache_revision: atomic acquire/release double-check avoids taking the mutex on every preproc_and_match call. No-mask hot path no longer pays for the singleton at all (init hoisted into the FFT/sparse branch). - should_fallback_to_opencv: pick whichever path is empirically faster instead of only catching dense-mask small-result cases: * result < 1000 && K < 2000: keep sparse (OpenCV setup dominates) * result < 12000 && K >= 500: fallback (covers GameStart 200x120/ 58x58 and 138x130/105x105 dead zones) * K*result < 25M: fallback (covers SmileyOnWork 880x80/20x21 and similar long-thin / small-template FFT-loses cases) Windows MSVC x64 (min-of-2, n=803/1402): coverage: regressions 198 -> 50, mean 10.5ms -> 7.4ms (1.40x) perf: regressions 86 -> 3, mean 854us -> 296us (2.90x) Android arm64-v8a NEON (single run, n=803/1402): coverage: regressions 0, mean 125ms -> 80ms (1.72x) perf: regressions 1 (1.20x edge), mean 6.4ms -> 2.8ms (10.95x) * perf(masked-matcher): merge 3-channel I² into a single FFT in σ_I² compute σ_I²(x,y) = Σ_c [(M ⋆ I_c²) - (M ⋆ I_c)² / N] = Σ_c (M ⋆ I_c²) - (1/N) Σ_c (M ⋆ I_c)² ↑ this term is linear in I_c², so by convolution linearity Σ_c (M ⋆ I_c²) = M ⋆ Σ_c I_c². Precompute I_sq_sum = Σ_c I_c² in spatial domain and do a single FFT + single IFFT instead of three. The second term Σ_c (M ⋆ I_c)² still requires per-channel computation (squaring is non-linear). Net: -2 FFTs and -2 IFFTs per match() call (15 → 11 transforms). Validated min-of-3 runs on the 10 largest FFT-path cases per platform: windows: 8/10 cases -22%, 2 cases (66x41 templ) unchanged android: 8/10 cases -22%, 2 cases (66x41 templ) unchanged The two unchanged cases have very small templates where FFT is not the bottleneck (cvtColor / convertTo / split dominate). Correctness preserved across 1800 iterations × 10 cases on both platforms. * fix: 测试代码忘记删除了 * fix: make TemplResource::m_revision atomic * chore: 调一下匹配参数 * feat: 在日志中标记匹配路径(优化实现 vs cv::matchTemplate) --- src/MaaCore/Config/TemplResource.cpp | 2 + src/MaaCore/Config/TemplResource.h | 3 + src/MaaCore/Vision/MaskedCcoeffMatcher.cpp | 371 +++++++++++++++++++++ src/MaaCore/Vision/MaskedCcoeffMatcher.h | 53 +++ src/MaaCore/Vision/Matcher.cpp | 92 ++++- src/MaaCore/Vision/Matcher.h | 7 + src/MaaCore/Vision/MultiMatcher.cpp | 2 +- 7 files changed, 516 insertions(+), 14 deletions(-) create mode 100644 src/MaaCore/Vision/MaskedCcoeffMatcher.cpp create mode 100644 src/MaaCore/Vision/MaskedCcoeffMatcher.h diff --git a/src/MaaCore/Config/TemplResource.cpp b/src/MaaCore/Config/TemplResource.cpp index 875cdd747c..7ff318e836 100644 --- a/src/MaaCore/Config/TemplResource.cpp +++ b/src/MaaCore/Config/TemplResource.cpp @@ -99,6 +99,8 @@ bool asst::TemplResource::load(const std::filesystem::path& path) return false; } #endif + m_templs.clear(); + ++m_revision; return true; } diff --git a/src/MaaCore/Config/TemplResource.h b/src/MaaCore/Config/TemplResource.h index 36188065ff..7e5063c868 100644 --- a/src/MaaCore/Config/TemplResource.h +++ b/src/MaaCore/Config/TemplResource.h @@ -2,6 +2,7 @@ #include "AbstractResource.h" +#include #include #include @@ -19,10 +20,12 @@ public: virtual bool load(const std::filesystem::path& path) override; const cv::Mat& get_templ(const std::string& name); + uint64_t revision() const noexcept { return m_revision.load(std::memory_order_acquire); } private: std::unordered_set m_load_required; std::unordered_map m_templs; std::unordered_map m_templ_paths; + std::atomic m_revision { 0 }; }; } diff --git a/src/MaaCore/Vision/MaskedCcoeffMatcher.cpp b/src/MaaCore/Vision/MaskedCcoeffMatcher.cpp new file mode 100644 index 0000000000..469110a749 --- /dev/null +++ b/src/MaaCore/Vision/MaskedCcoeffMatcher.cpp @@ -0,0 +1,371 @@ +#include "MaskedCcoeffMatcher.h" + +#include "MaaUtils/NoWarningCV.hpp" + +#include +#include + +namespace asst +{ +namespace +{ +// 稀疏路径使用的每个有效 mask 像素的条目 +struct SparseEntry +{ + int16_t dx, dy; // 相对模板左上角的偏移 + float T_prime[3]; // M*(T_c - μT_c),per-channel +}; +} + +struct MaskedCcoeffMatcher::TemplatePlan +{ + cv::Mat M; // CV_32F mask, 0 or 1 + std::array T_prime; // M*(T_c - μT_c),per-channel + double sigma_T_sq = 0.0; + double mask_area = 0.0; + std::vector sparse_entries; // 非零 mask 位置列表 + int K = 0; // sparse_entries.size() +}; + +struct MaskedCcoeffMatcher::DftPlan +{ + std::array T_prime_dft; // FFT(M*(T_c - μT_c)),每通道一个 + cv::Mat M_dft; // FFT(M) +}; + +MaskedCcoeffMatcher& MaskedCcoeffMatcher::get_instance() +{ + static MaskedCcoeffMatcher instance; + return instance; +} + +void MaskedCcoeffMatcher::sync_cache_revision(const uint64_t revision) +{ + if (m_cache_revision.load(std::memory_order_acquire) == revision) { + return; + } + std::lock_guard lk(m_cache_mtx); + if (m_cache_revision.load(std::memory_order_relaxed) == revision) { + return; + } + // 清一下缓存 + m_template_plan_cache.clear(); + m_dft_plan_cache.clear(); + m_cache_revision.store(revision, std::memory_order_release); +} + +void MaskedCcoeffMatcher::fnv1a_update(uint64_t& h, const void* data, size_t size) +{ + const auto* ptr = static_cast(data); + for (size_t i = 0; i < size; ++i) { + h ^= ptr[i]; + h *= 1099511628211ULL; + } +} + +std::string MaskedCcoeffMatcher::make_mat_cache_key(const cv::Mat& mat) +{ + uint64_t h = 14695981039346656037ULL; + const int meta[] = { mat.rows, mat.cols, mat.type() }; + fnv1a_update(h, meta, sizeof(meta)); + + const size_t row_bytes = static_cast(mat.cols) * mat.elemSize(); + for (int y = 0; y < mat.rows; ++y) { + fnv1a_update(h, mat.ptr(y), row_bytes); + } + // 捏个hash key + return "mat:" + std::to_string(mat.rows) + "x" + std::to_string(mat.cols) + + ":" + std::to_string(mat.type()) + ":" + std::to_string(h); +} + +std::shared_ptr MaskedCcoeffMatcher::get_or_build_template_plan( + const std::string& cache_key, + const cv::Mat& templ_f32, + const cv::Mat& mask_f32, + int mask_pixels) +{ + { + std::lock_guard lk(m_cache_mtx); + if (const auto it = m_template_plan_cache.find(cache_key); it != m_template_plan_cache.end()) { + return it->second; + } + } + + auto plan = std::make_shared(); + plan->M = mask_f32; + plan->mask_area = mask_pixels; + if (plan->mask_area < 1.0) { + return {}; + } + + std::vector T_ch(3); + cv::split(templ_f32, T_ch); + + for (int c = 0; c < 3; ++c) { + const double mu_T = cv::sum(plan->M.mul(T_ch[c]))[0] / plan->mask_area; + plan->T_prime[c] = plan->M.mul(T_ch[c] - mu_T); + plan->sigma_T_sq += cv::sum(plan->T_prime[c].mul(plan->T_prime[c]))[0]; + } + + for (int v = 0; v < templ_f32.rows; ++v) { + for (int u = 0; u < templ_f32.cols; ++u) { + if (plan->M.at(v, u) > 0.5f) { + SparseEntry e {}; + e.dx = static_cast(u); + e.dy = static_cast(v); + for (int c = 0; c < 3; ++c) { + e.T_prime[c] = plan->T_prime[c].at(v, u); + } + plan->sparse_entries.push_back(e); + } + } + } + plan->K = static_cast(plan->sparse_entries.size()); + + std::lock_guard lk(m_cache_mtx); + auto [it, inserted] = m_template_plan_cache.emplace(cache_key, plan); + static_cast(inserted); + return it->second; +} + +std::shared_ptr MaskedCcoeffMatcher::get_or_build_dft_plan( + const std::string& cache_key, + const TemplatePlan& template_plan, + int dft_rows, + int dft_cols) +{ + const std::string dft_key = cache_key + ":dft:" + std::to_string(dft_rows) + "x" + std::to_string(dft_cols); + { + std::lock_guard lk(m_cache_mtx); + if (const auto it = m_dft_plan_cache.find(dft_key); it != m_dft_plan_cache.end()) { + return it->second; + } + } + + auto dft_plan = std::make_shared(); + cv::Mat padded = cv::Mat::zeros(dft_rows, dft_cols, CV_32F); + auto compute_into = [&](const cv::Mat& src, cv::Mat& out) { + padded.setTo(0.0f); + src.copyTo(padded(cv::Rect(0, 0, src.cols, src.rows))); + cv::dft(padded, out, cv::DFT_COMPLEX_OUTPUT); + }; + + compute_into(template_plan.M, dft_plan->M_dft); + for (int c = 0; c < 3; ++c) { + compute_into(template_plan.T_prime[c], dft_plan->T_prime_dft[c]); + } + + std::lock_guard lk(m_cache_mtx); + auto [it, inserted] = m_dft_plan_cache.emplace(dft_key, dft_plan); + static_cast(inserted); + return it->second; +} + +bool MaskedCcoeffMatcher::should_fallback_to_opencv(int mask_pixels, int result_positions) +{ + // 神秘调参值 + // - 极小 result + 低 K:稀疏路径整体工作量极小,留给 FFT/sparse + // - 极小 result + 高 K(如 138×130/105×105):K 超稀疏阈值,FFT 在小 DFT size 反而不如 OpenCV + // - 中等 result 配中等 K:Windows 上 OpenCV 紧凑 SIMD 快;Android 上 OpenCV 慢约 300x,阈值大幅收紧 + + if (result_positions < 1000 && mask_pixels < 2000) { + return false; + } + +#ifdef __ANDROID__ + if (result_positions < 3000 && mask_pixels >= 500) { + return true; + } + if (static_cast(mask_pixels) * result_positions < 8'000'000LL) { + return true; + } +#else + if (result_positions < 12000 && mask_pixels >= 500) { + return true; + } + if (static_cast(mask_pixels) * result_positions < 25'000'000LL) { + return true; + } +#endif + + return false; +} + +// 用 cv::dft 直接实现,消除冗余 FFT +// +// 当前 9 次 matchTemplate 的冗余: +// FFT(I_c) 每通道算两次(分别用于 xcorr(T'_c, I_c) 和 xcorr(M, I_c)) +// FFT(M) 每通道算两次(分别用于 xcorr(M, I_c) 和 xcorr(M, I_c²)) +// +// 优化后: +// FFT(I_c) 和 FFT(I_c²) 每通道各算一次并复用 +// FFT(T'_c) 和 FFT(M) 通过缓存跨调用复用 +// +// 等价于 cv::matchTemplate(image, templ, result, TM_CCOEFF_NORMED, mask) +cv::Mat MaskedCcoeffMatcher::match( + const cv::Mat& image_rgb, // CV_8UC3 + const cv::Mat& templ_rgb, // CV_8UC3 + const cv::Mat& mask_u8, // CV_8UC1, 0 or 255 + const std::string& cache_key, // 模板侧 FFT 缓存键 + int mask_pixels) +{ + const int rh = image_rgb.rows - templ_rgb.rows + 1; + const int rw = image_rgb.cols - templ_rgb.cols + 1; + if (rh <= 0 || rw <= 0) return {}; + + if (mask_pixels <= 0 || should_fallback_to_opencv(mask_pixels, rh * rw)) { + return {}; + } + + cv::Mat I, T, M; + image_rgb.convertTo(I, CV_32F); + templ_rgb.convertTo(T, CV_32F); + mask_u8.convertTo(M, CV_32F, 1.0 / 255.0); + + const auto template_plan = get_or_build_template_plan(cache_key, T, M, mask_pixels); + if (!template_plan) return {}; + + const double mask_area = template_plan->mask_area; + const double sigma_T_sq = template_plan->sigma_T_sq; + + // 图像通道拆分:稀疏和 FFT 两条路径都需要 + std::vector I_ch(3); + cv::split(I, I_ch); + + // 稀疏直接相关(小模板快路径,比如基建任务中那种就很合适) + // 双重条件:K < SPARSE_K_LIMIT 且总工作量 K×result_positions < SPARSE_WORK_LIMIT + // 仅满足 K 小但结果矩阵极大时(如 49×28 模板/690×434 图)仍走 FFT 路径 + static constexpr int SPARSE_K_LIMIT = 2000; + static constexpr long long SPARSE_WORK_LIMIT = 30'000'000LL; + if (template_plan->K > 0 && template_plan->K < SPARSE_K_LIMIT && + static_cast(template_plan->K) * rh * rw < SPARSE_WORK_LIMIT) { + cv::Mat numerator = cv::Mat::zeros(rh, rw, CV_32F); + cv::Mat sum_MI_r = cv::Mat::zeros(rh, rw, CV_32F); + cv::Mat sum_MI_g = cv::Mat::zeros(rh, rw, CV_32F); + cv::Mat sum_MI_b = cv::Mat::zeros(rh, rw, CV_32F); + cv::Mat sum_MI2 = cv::Mat::zeros(rh, rw, CV_32F); // Σ_c I_c² + + for (const auto& [dx, dy, T_prime] : template_plan->sparse_entries) { + for (int y = 0; y < rh; ++y) { + const float* Ir = I_ch[0].ptr(y + dy) + dx; + const float* Ig = I_ch[1].ptr(y + dy) + dx; + const float* Ib = I_ch[2].ptr(y + dy) + dx; + auto* num_p = numerator.ptr(y); + auto* smir_p = sum_MI_r.ptr(y); + auto* smig_p = sum_MI_g.ptr(y); + auto* smib_p = sum_MI_b.ptr(y); + auto* smi2_p = sum_MI2.ptr(y); + + // 编译器会自动向量化的 + for (int x = 0; x < rw; ++x) { + const float r = Ir[x], g = Ig[x], b = Ib[x]; + num_p[x] += T_prime[0] * r + T_prime[1] * g + T_prime[2] * b; + smir_p[x] += r; + smig_p[x] += g; + smib_p[x] += b; + smi2_p[x] += r * r + g * g + b * b; + } + } + } + + // sigma_I² = sum_MI2 - (sum_MI_r² + sum_MI_g² + sum_MI_b²) / mask_area + cv::Mat sq_sum, sq_g, sq_b; + cv::multiply(sum_MI_r, sum_MI_r, sq_sum); + cv::multiply(sum_MI_g, sum_MI_g, sq_g); + cv::multiply(sum_MI_b, sum_MI_b, sq_b); + cv::add(sq_sum, sq_g, sq_sum); + cv::add(sq_sum, sq_b, sq_sum); + cv::Mat sigma_I_sq; + cv::subtract(sum_MI2, sq_sum * (1.0 / mask_area), sigma_I_sq); + cv::max(sigma_I_sq, 0.0, sigma_I_sq); + + cv::Mat denom; + cv::sqrt(sigma_I_sq * sigma_T_sq, denom); + cv::Mat result; + cv::divide(numerator, denom, result); + cv::patchNaNs(result, 0.0); + const auto sigma_T_norm = static_cast(std::sqrt(sigma_T_sq)); + result.setTo(0.0f, denom < sigma_T_norm * 1e-5f); + cv::min(result, 1.0f, result); + cv::max(result, -1.0f, result); + return result; + } + + // DFT 的填充尺寸:仅在确认走 FFT 路径后才需要 + const int dft_rows = cv::getOptimalDFTSize(I.rows + T.rows - 1); + const int dft_cols = cv::getOptimalDFTSize(I.cols + T.cols - 1); + const auto dft_plan = get_or_build_dft_plan(cache_key, *template_plan, dft_rows, dft_cols); + + cv::Mat padded(dft_rows, dft_cols, CV_32F, cv::Scalar(0)); + cv::Mat I_dft(dft_rows, dft_cols, CV_32FC2); + cv::Mat spectrum(dft_rows, dft_cols, CV_32FC2); + cv::Mat result_buf(dft_rows, dft_cols, CV_32F); + cv::Mat sum_MI_buf(rh, rw, CV_32F); + cv::Mat sum_MI2_buf(rh, rw, CV_32F); + + auto make_dft_into = [&](const cv::Mat& src, cv::Mat& out) { + padded.setTo(0.0f); + src.copyTo(padded(cv::Rect(0, 0, src.cols, src.rows))); + cv::dft(padded, out, cv::DFT_COMPLEX_OUTPUT); + }; + auto xcorr_into = [&](const cv::Mat& dft_A, const cv::Mat& dft_B, cv::Mat& out) { + cv::mulSpectrums(dft_A, dft_B, spectrum, 0, true); + cv::dft(spectrum, result_buf, cv::DFT_INVERSE | cv::DFT_REAL_OUTPUT | cv::DFT_SCALE); + result_buf(cv::Rect(0, 0, rw, rh)).copyTo(out); + }; + auto xcorr_add = [&](const cv::Mat& dft_A, const cv::Mat& dft_B, cv::Mat& accum) { + cv::mulSpectrums(dft_A, dft_B, spectrum, 0, true); + cv::dft(spectrum, result_buf, cv::DFT_INVERSE | cv::DFT_REAL_OUTPUT | cv::DFT_SCALE); + cv::add(accum, result_buf(cv::Rect(0, 0, rw, rh)), accum); + }; + + cv::Mat numerator = cv::Mat::zeros(rh, rw, CV_32F); + cv::Mat sigma_I_sq_d = cv::Mat::zeros(rh, rw, CV_64F); // float64 避免 sum_MI2-sum_MI² 灾难性精度损失 + + // σ_I²(x,y) = Σ_c σ_I_c² = Σ_c [(M ⋆ I_c²) - (M ⋆ I_c)² / N] + // = Σ_c (M ⋆ I_c²) - (1/N) Σ_c (M ⋆ I_c)² + // ↑ 把这一项的三通道求和提到卷积外面 + // + // 利用卷积对加法线性:Σ_c (M ⋆ I_c²) = M ⋆ (Σ_c I_c²) + // 在空域里先把三通道平方加起来再做一次卷积,比每通道各做一次再相加少 2 次 FFT + 2 次 IFFT + // 第二项 Σ_c (M ⋆ I_c)² 因为有平方,不能这样合并(平方对加法非线性),仍逐通道算 + // 实测 Windows + Android 真 FFT 路径 case 平均 -22% + cv::Mat I_sq_sum = I_ch[0].mul(I_ch[0]) + I_ch[1].mul(I_ch[1]) + I_ch[2].mul(I_ch[2]); + cv::Mat I_sq_sum_dft(dft_rows, dft_cols, CV_32FC2); + make_dft_into(I_sq_sum, I_sq_sum_dft); + xcorr_into(I_sq_sum_dft, dft_plan->M_dft, sum_MI2_buf); + cv::Mat sum_MI2_d; + sum_MI2_buf.convertTo(sum_MI2_d, CV_64F); + cv::add(sigma_I_sq_d, sum_MI2_d, sigma_I_sq_d); + + for (int c = 0; c < 3; ++c) { + make_dft_into(I_ch[c], I_dft); + + // numerator += xcorr(T'_c, I_c) + xcorr_add(I_dft, dft_plan->T_prime_dft[c], numerator); + + // sigma_I² 第二项:-Σ_c (sum_MI_c)² / mask_area,逐通道累加 + xcorr_into(I_dft, dft_plan->M_dft, sum_MI_buf); + cv::Mat sum_MI_d, var_d; + sum_MI_buf.convertTo(sum_MI_d, CV_64F); + cv::multiply(sum_MI_d, sum_MI_d, var_d, -1.0 / mask_area); + cv::add(sigma_I_sq_d, var_d, sigma_I_sq_d); + } + + cv::Mat sigma_I_sq; + sigma_I_sq_d.convertTo(sigma_I_sq, CV_32F); + cv::max(sigma_I_sq, 0.0, sigma_I_sq); + + cv::Mat denom; + cv::sqrt(sigma_I_sq * sigma_T_sq, denom); + + cv::Mat result; + cv::divide(numerator, denom, result); + cv::patchNaNs(result, 0.0); + const auto sigma_T_norm = static_cast(std::sqrt(sigma_T_sq)); + result.setTo(0.0f, denom < sigma_T_norm * 1e-5f); + cv::min(result, 1.0f, result); + cv::max(result, -1.0f, result); + return result; +} +} diff --git a/src/MaaCore/Vision/MaskedCcoeffMatcher.h b/src/MaaCore/Vision/MaskedCcoeffMatcher.h new file mode 100644 index 0000000000..81bb0da36a --- /dev/null +++ b/src/MaaCore/Vision/MaskedCcoeffMatcher.h @@ -0,0 +1,53 @@ +#pragma once + +#include "MaaUtils/NoWarningCVMat.hpp" + +#include +#include +#include +#include +#include + +namespace asst +{ +class MaskedCcoeffMatcher +{ +public: + static MaskedCcoeffMatcher& get_instance(); + + void sync_cache_revision(uint64_t revision); + static std::string make_mat_cache_key(const cv::Mat& mat); + + cv::Mat match( + const cv::Mat& image_rgb, + const cv::Mat& templ_rgb, + const cv::Mat& mask_u8, + const std::string& cache_key, + int mask_pixels); + + static bool should_fallback_to_opencv(int mask_pixels, int result_positions); + +private: + struct TemplatePlan; + struct DftPlan; + + static void fnv1a_update(uint64_t& h, const void* data, size_t size); + + std::shared_ptr get_or_build_template_plan( + const std::string& cache_key, + const cv::Mat& templ_f32, + const cv::Mat& mask_f32, + int mask_pixels); + + std::shared_ptr get_or_build_dft_plan( + const std::string& cache_key, + const TemplatePlan& template_plan, + int dft_rows, + int dft_cols); + + std::mutex m_cache_mtx; + std::unordered_map> m_template_plan_cache; + std::unordered_map> m_dft_plan_cache; + std::atomic m_cache_revision { 0 }; +}; +} diff --git a/src/MaaCore/Vision/Matcher.cpp b/src/MaaCore/Vision/Matcher.cpp index 807190fdd6..188effbda0 100644 --- a/src/MaaCore/Vision/Matcher.cpp +++ b/src/MaaCore/Vision/Matcher.cpp @@ -5,6 +5,7 @@ #include "Config/TaskData.h" #include "Config/TemplResource.h" #include "MaaUtils/ImageIo.h" +#include "MaskedCcoeffMatcher.h" #include "Utils/DebugImageHelper.hpp" #include "Utils/Logger.hpp" #include "Utils/StringMisc.hpp" @@ -19,7 +20,7 @@ Matcher::ResultOpt Matcher::analyze() const const auto match_results = preproc_and_match(make_roi(m_image, m_roi), m_params); for (size_t i = 0; i < match_results.size(); ++i) { - const auto& [matched, templ, templ_name] = match_results[i]; + const auto& [matched, templ, templ_name, path] = match_results[i]; if (matched.empty()) { continue; } @@ -33,8 +34,15 @@ Matcher::ResultOpt Matcher::analyze() const Rect rect(max_loc.x + m_roi.x, max_loc.y + m_roi.y, templ.cols, templ.rows); double threshold = m_params.templ_thres[i]; + const char* path_tag = path == MatchPath::Optimized ? "optimized" : "opencv"; + const auto& method_i = m_params.methods.size() > i ? m_params.methods[i] : MatchMethod::Ccoeff; + std::string tag = "["; + tag += path_tag; + if (method_i == MatchMethod::HSVCount) tag += "|hsv"; + else if (method_i == MatchMethod::RGBCount) tag += "|rgb"; + tag += "]"; if (m_log_tracing && max_val > 0.5 && max_val > threshold - 0.2) { // 得分太低的肯定不对,没必要打印 - Log.trace("match_templ |", templ_name, "score:", max_val, "rect:", rect, "roi:", m_roi); + Log.trace("match_templ |", templ_name, tag, "score:", max_val, "rect:", rect, "roi:", m_roi); #ifdef ASST_DEBUG if (!m_params.methods.empty() && m_params.methods[0] == MatchMethod::HSVCount) { const cv::Rect expanded_roi( @@ -66,7 +74,7 @@ Matcher::ResultOpt Matcher::analyze() const #endif } else { - Log.debug("match_templ |", templ_name, "score:", max_val, "rect:", rect, "roi:", m_roi); + Log.debug("match_templ |", templ_name, tag, "score:", max_val, "rect:", rect, "roi:", m_roi); } if (max_val < threshold) { continue; @@ -152,6 +160,7 @@ std::vector Matcher::preproc_and_match(const cv::Mat& image, } cv::Mat matched; + auto match_path = MatchPath::OpenCV; cv::Mat templ_match, templ_count, templ_gray; cv::cvtColor(templ, templ_match, cv::COLOR_BGR2RGB); if (!image_gray.empty()) { @@ -175,7 +184,7 @@ std::vector Matcher::preproc_and_match(const cv::Mat& image, int match_algorithm = cv::TM_CCOEFF_NORMED; auto calc_mask = [&templ_name]( - const MatchTaskInfo::Ranges mask_ranges, + const MatchTaskInfo::Ranges& mask_ranges, const cv::Mat& templ, const cv::Mat& templ_gray, bool with_close) -> std::optional { @@ -218,7 +227,53 @@ std::vector Matcher::preproc_and_match(const cv::Mat& image, if (!mask_opt) { return {}; } - cv::matchTemplate(image_match, templ_match, matched, match_algorithm, mask_opt.value()); + // mask_src=false 时 mask 完全由模板决定,用 FFT 路径替代标量滑窗 + if (!params.mask_src) { + const int mask_pixels = cv::countNonZero(mask_opt.value()); + if (mask_pixels == mask_opt.value().rows * mask_opt.value().cols) { + cv::matchTemplate(image_match, templ_match, matched, match_algorithm); + } + else if (MaskedCcoeffMatcher::should_fallback_to_opencv( + mask_pixels, + (image_match.rows - templ_match.rows + 1) * (image_match.cols - templ_match.cols + 1))) { + // matched 保持 empty,统一落到下面的 OpenCV masked matchTemplate + } + else { + auto& masked_ccoeff_matcher = MaskedCcoeffMatcher::get_instance(); + const uint64_t templ_revision = TemplResource::get_instance().revision(); + masked_ccoeff_matcher.sync_cache_revision(templ_revision); + + // cache key:templ_name + mask_ranges + // 资源模板绑定 revision;cv::Mat 模板使用 row-wise 内容 hash + std::string fft_key = templ_name.empty() + ? MaskedCcoeffMatcher::make_mat_cache_key(templ) + : "res:" + std::to_string(templ_revision) + ":" + templ_name; + for (const auto& r : params.mask_ranges) { + if (std::holds_alternative(r)) { + const auto& g = std::get(r); + fft_key += ":G" + std::to_string(g.first) + '_' + std::to_string(g.second); + } + else if (std::holds_alternative(r)) { + const auto& col = std::get(r); + fft_key += ":C"; + for (auto v : col.first) fft_key += std::to_string(v) + ','; + fft_key += '_'; + for (auto v : col.second) fft_key += std::to_string(v) + ','; + } + } + fft_key += params.mask_close ? ":1" : ":0"; + + matched = masked_ccoeff_matcher.match( + image_match, templ_match, mask_opt.value(), fft_key, mask_pixels); + if (!matched.empty()) { + match_path = MatchPath::Optimized; + } + } + } + if (matched.empty()) { + cv::matchTemplate(image_match, templ_match, matched, match_algorithm, mask_opt.value()); + match_path = MatchPath::OpenCV; + } } if (method == MatchMethod::RGBCount || method == MatchMethod::HSVCount) { @@ -232,17 +287,28 @@ std::vector Matcher::preproc_and_match(const cv::Mat& image, cv::threshold(templ_active, templ_active, 1, 1, cv::THRESH_BINARY); cv::threshold(image_active, image_active, 1, 1, cv::THRESH_BINARY); - // 把 CCORR 当 count 用,计算 image_active 在 templ_active 形状内的像素数量 - cv::Mat tp, fp; + // tp = image_active 与 templ_active 的共激活像素数(TM_CCORR 当 count 用) + cv::Mat tp; int tp_fn = cv::countNonZero(templ_active); cv::matchTemplate(image_active, templ_active, tp, cv::TM_CCORR); tp.convertTo(tp, CV_32S); - cv::Mat templ_inactive = 1 - templ_active; - // TODO: 这里 TP+FP 是 image_active 的 count,可以消掉一个 matchtemplate - cv::matchTemplate(image_active, templ_inactive, fp, cv::TM_CCORR); - fp.convertTo(fp, CV_32S); + // sum_active = 每个窗口内 image_active 的总激活数 + // 由于 tp + fp = sum_active,用积分图代替第二次 matchTemplate + cv::Mat image_active_f; + image_active.convertTo(image_active_f, CV_32F); + cv::Mat integ; + cv::integral(image_active_f, integ, CV_32F); + const int kh = templ_active.rows, kw = templ_active.cols; + // sum_active[y,x] = integ[y+kh,x+kw] - integ[y,x+kw] - integ[y+kh,x] + integ[y,x] + cv::Mat sum_active = + integ(cv::Rect(kw, kh, tp.cols, tp.rows)) + - integ(cv::Rect(0, kh, tp.cols, tp.rows)) + - integ(cv::Rect(kw, 0, tp.cols, tp.rows)) + + integ(cv::Rect(0, 0, tp.cols, tp.rows)); + cv::Mat sum_active_i; + sum_active.convertTo(sum_active_i, CV_32S); cv::Mat count_result; - cv::divide(2 * tp, tp + fp + tp_fn, count_result, 1, CV_32F); // 数色结果为 f1_score + cv::divide(2 * tp, sum_active_i + tp_fn, count_result, 1, CV_32F); // 数色结果为 f1_score if (params.pure_color) { matched = 1.0f; @@ -250,7 +316,7 @@ std::vector Matcher::preproc_and_match(const cv::Mat& image, cv::multiply(matched, count_result, matched); // 最终结果是数色和模板匹配的点积 } - results.emplace_back(RawResult { .matched = matched, .templ = templ, .templ_name = templ_name }); + results.emplace_back(RawResult { .matched = matched, .templ = templ, .templ_name = templ_name, .path = match_path }); } return results; } diff --git a/src/MaaCore/Vision/Matcher.h b/src/MaaCore/Vision/Matcher.h index 6c9e5861c7..77b642eed6 100644 --- a/src/MaaCore/Vision/Matcher.h +++ b/src/MaaCore/Vision/Matcher.h @@ -21,11 +21,18 @@ public: const auto& get_result() const noexcept { return m_result; } public: + enum class MatchPath + { + OpenCV, // cv::matchTemplate + Optimized, // 优化实现 + }; + struct RawResult { cv::Mat matched; cv::Mat templ; std::string templ_name; + MatchPath path; }; static std::vector preproc_and_match(const cv::Mat& image, const MatcherConfig::Params& params); diff --git a/src/MaaCore/Vision/MultiMatcher.cpp b/src/MaaCore/Vision/MultiMatcher.cpp index 35606fe132..156245f4a3 100644 --- a/src/MaaCore/Vision/MultiMatcher.cpp +++ b/src/MaaCore/Vision/MultiMatcher.cpp @@ -21,7 +21,7 @@ MultiMatcher::ResultsVecOpt MultiMatcher::analyze() const std::vector results; for (size_t index = 0; index < match_results.size(); ++index) { - const auto& [matched, templ, templ_name] = match_results[index]; + const auto& [matched, templ, templ_name, _] = match_results[index]; if (matched.empty()) { continue; }