perf: 优化 masked TM_CCOEFF_NORMED 匹配性能 (#16593)

* 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)
This commit is contained in:
Aliothmoon
2026-05-15 19:22:29 +08:00
committed by GitHub
parent 210b1aef22
commit 416ecca979
7 changed files with 516 additions and 14 deletions

View File

@@ -99,6 +99,8 @@ bool asst::TemplResource::load(const std::filesystem::path& path)
return false;
}
#endif
m_templs.clear();
++m_revision;
return true;
}

View File

@@ -2,6 +2,7 @@
#include "AbstractResource.h"
#include <atomic>
#include <unordered_map>
#include <unordered_set>
@@ -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<std::string> m_load_required;
std::unordered_map<std::string, cv::Mat> m_templs;
std::unordered_map<std::string, std::filesystem::path> m_templ_paths;
std::atomic<uint64_t> m_revision { 0 };
};
}

View File

@@ -0,0 +1,371 @@
#include "MaskedCcoeffMatcher.h"
#include "MaaUtils/NoWarningCV.hpp"
#include <array>
#include <vector>
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<cv::Mat, 3> T_prime; // M*(T_c - μT_c)per-channel
double sigma_T_sq = 0.0;
double mask_area = 0.0;
std::vector<SparseEntry> sparse_entries; // 非零 mask 位置列表
int K = 0; // sparse_entries.size()
};
struct MaskedCcoeffMatcher::DftPlan
{
std::array<cv::Mat, 3> 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<const uint8_t*>(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<size_t>(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<const MaskedCcoeffMatcher::TemplatePlan> 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<TemplatePlan>();
plan->M = mask_f32;
plan->mask_area = mask_pixels;
if (plan->mask_area < 1.0) {
return {};
}
std::vector<cv::Mat> 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<float>(v, u) > 0.5f) {
SparseEntry e {};
e.dx = static_cast<int16_t>(u);
e.dy = static_cast<int16_t>(v);
for (int c = 0; c < 3; ++c) {
e.T_prime[c] = plan->T_prime[c].at<float>(v, u);
}
plan->sparse_entries.push_back(e);
}
}
}
plan->K = static_cast<int>(plan->sparse_entries.size());
std::lock_guard lk(m_cache_mtx);
auto [it, inserted] = m_template_plan_cache.emplace(cache_key, plan);
static_cast<void>(inserted);
return it->second;
}
std::shared_ptr<const MaskedCcoeffMatcher::DftPlan> 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<DftPlan>();
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<void>(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×105K 超稀疏阈值FFT 在小 DFT size 反而不如 OpenCV
// - 中等 result 配中等 KWindows 上 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<long long>(mask_pixels) * result_positions < 8'000'000LL) {
return true;
}
#else
if (result_positions < 12000 && mask_pixels >= 500) {
return true;
}
if (static_cast<long long>(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<cv::Mat> 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<long long>(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<float>(y + dy) + dx;
const float* Ig = I_ch[1].ptr<float>(y + dy) + dx;
const float* Ib = I_ch[2].ptr<float>(y + dy) + dx;
auto* num_p = numerator.ptr<float>(y);
auto* smir_p = sum_MI_r.ptr<float>(y);
auto* smig_p = sum_MI_g.ptr<float>(y);
auto* smib_p = sum_MI_b.ptr<float>(y);
auto* smi2_p = sum_MI2.ptr<float>(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<float>(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<float>(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;
}
}

View File

@@ -0,0 +1,53 @@
#pragma once
#include "MaaUtils/NoWarningCVMat.hpp"
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
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<const TemplatePlan> 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<const DftPlan> 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<std::string, std::shared_ptr<const TemplatePlan>> m_template_plan_cache;
std::unordered_map<std::string, std::shared_ptr<const DftPlan>> m_dft_plan_cache;
std::atomic<uint64_t> m_cache_revision { 0 };
};
}

View File

@@ -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::RawResult> 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::RawResult> 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<cv::Mat> {
@@ -218,7 +227,53 @@ std::vector<Matcher::RawResult> 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 keytempl_name + mask_ranges
// 资源模板绑定 revisioncv::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<MatchTaskInfo::GrayRange>(r)) {
const auto& g = std::get<MatchTaskInfo::GrayRange>(r);
fft_key += ":G" + std::to_string(g.first) + '_' + std::to_string(g.second);
}
else if (std::holds_alternative<MatchTaskInfo::ColorRange>(r)) {
const auto& col = std::get<MatchTaskInfo::ColorRange>(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::RawResult> 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::RawResult> 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;
}

View File

@@ -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<RawResult> preproc_and_match(const cv::Mat& image, const MatcherConfig::Params& params);

View File

@@ -21,7 +21,7 @@ MultiMatcher::ResultsVecOpt MultiMatcher::analyze() const
std::vector<Result> 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;
}