mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-15 17:30:27 +08:00
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:
@@ -99,6 +99,8 @@ bool asst::TemplResource::load(const std::filesystem::path& path)
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
m_templs.clear();
|
||||
++m_revision;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
}
|
||||
|
||||
371
src/MaaCore/Vision/MaskedCcoeffMatcher.cpp
Normal file
371
src/MaaCore/Vision/MaskedCcoeffMatcher.cpp
Normal 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×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<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;
|
||||
}
|
||||
}
|
||||
53
src/MaaCore/Vision/MaskedCcoeffMatcher.h
Normal file
53
src/MaaCore/Vision/MaskedCcoeffMatcher.h
Normal 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 };
|
||||
};
|
||||
}
|
||||
@@ -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 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<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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user