feat: 支持自定义 MatchTemplate 匹配方法 (#9785)

This commit is contained in:
zzyyyl
2024-07-20 12:57:03 +08:00
committed by GitHub
7 changed files with 125 additions and 14 deletions

View File

@@ -92,15 +92,20 @@ icon: material-symbols:task
/* algorithm MatchTemplate */
"template": "xxx.png", // 可选项,要匹配的图片文件名
"template": "xxx.png", // 可选项,要匹配的图片文件名,可以是字符串或字符串列表
// 默认 "任务名.png"
"templThreshold": 0.8, // 可选项,图片模板匹配得分的阈值,超过阈值才认为识别到了
"templThreshold": 0.8, // 可选项,图片模板匹配得分的阈值,超过阈值才认为识别到了,可以是数字或数字列表
// 默认 0.8, 可根据日志查看实际得分是多少
"maskRange": [ 1, 255 ], // 可选项,灰度掩码范围。例如将图片不需要识别的部分涂成黑色(灰度值为 0
// 然后设置"maskRange"的范围为 [ 1, 255 ], 匹配的时候即刻忽略涂黑的部分
"method": "Ccoeff", // 可选项,模板匹配算法,可以是列表
// 不填写时默认为 Ccoeff
// - Ccoeff: 对应 cv::TM_CCOEFF_NORMED
// - CcoeffHSV: 转换为 HSV 颜色空间后再进行 CCOEFF
/* algorithm OcrDetect */
"text": [ "接管作战", "代理指挥" ], // 必选项,要识别的文字内容,只要任一匹配上了即认为识别到了

View File

@@ -359,6 +359,38 @@ namespace asst
}
return "Invalid";
}
enum class MatchMethod
{
Invalid = -1,
Ccoeff = 0,
CcoeffHSV,
};
inline MatchMethod get_match_method(std::string method_str)
{
utils::tolowers(method_str);
static const std::unordered_map<std::string, MatchMethod> method_map = {
{ "ccoeff", MatchMethod::Ccoeff }, { "ccoeffhsv", MatchMethod::CcoeffHSV },
};
if (auto it = method_map.find(method_str); it != method_map.end()) {
return it->second;
}
return MatchMethod::Invalid;
}
inline std::string enum_to_string(MatchMethod method)
{
static const std::unordered_map<MatchMethod, std::string> method_map = {
{ MatchMethod::Invalid, "Invalid" },
{ MatchMethod::Ccoeff, "Ccoeff" },
{ MatchMethod::CcoeffHSV, "CcoeffHSV" },
};
if (auto it = method_map.find(method); it != method_map.end()) {
return it->second;
}
return "Invalid";
}
} // namespace asst
namespace asst
@@ -459,6 +491,7 @@ namespace asst
std::vector<std::string> templ_names; // 匹配模板图片文件名
std::vector<double> templ_thresholds; // 模板匹配阈值
std::pair<int, int> mask_range; // 掩码的二值化范围
std::vector<MatchMethod> methods; // 匹配方法
};
using MatchTaskPtr = std::shared_ptr<MatchTaskInfo>;
using MatchTaskConstPtr = std::shared_ptr<const MatchTaskInfo>;

View File

@@ -479,7 +479,45 @@ asst::TaskPtr asst::TaskData::generate_match_task_info(std::string_view name, co
return nullptr;
}
auto method_opt = task_json.find("method");
if (!method_opt) {
match_task_info_ptr->methods = default_ptr->methods;
match_task_info_ptr->methods.resize(match_task_info_ptr->templ_names.size(),
default_ptr->methods.back());
}
else if (method_opt->is_string()) {
// 单个数值时,所有模板都使用这个阈值
match_task_info_ptr->methods.resize(match_task_info_ptr->templ_names.size(),
get_match_method(method_opt->as_string()));
}
else if (method_opt->is_array()) {
ranges::copy(method_opt->as_array() |
views::transform(&json::value::as_string) |
views::transform(&get_match_method),
std::back_inserter(match_task_info_ptr->methods));
}
else {
Log.error("Invalid method type in task", name);
return nullptr;
}
if (ranges::find(match_task_info_ptr->methods, MatchMethod::Invalid) != match_task_info_ptr->methods.end()) {
Log.error("Invalid method in task", name);
return nullptr;
}
if (match_task_info_ptr->templ_names.size() != match_task_info_ptr->methods.size()) {
Log.error("Template count and method count not match in task", name);
return nullptr;
}
if (match_task_info_ptr->templ_names.size() == 0 || match_task_info_ptr->methods.size() == 0) {
Log.error("Template or method is empty in task", name);
return nullptr;
}
utils::get_and_check_value_or(name, task_json, "maskRange", match_task_info_ptr->mask_range, default_ptr->mask_range);
return match_task_info_ptr;
}
@@ -675,6 +713,7 @@ asst::MatchTaskConstPtr asst::TaskData::_default_match_task_info()
auto match_task_info_ptr = std::make_shared<MatchTaskInfo>();
match_task_info_ptr->templ_names = { "__INVALID__" };
match_task_info_ptr->templ_thresholds = { TemplThresholdDefault };
match_task_info_ptr->methods = { MatchMethod::Ccoeff };
return match_task_info_ptr;
}
@@ -726,18 +765,18 @@ bool asst::TaskData::syntax_check(std::string_view task_name, const json::value&
static const std::unordered_map<AlgorithmType, std::unordered_set<std::string>> allowed_key_under_algorithm = {
{ AlgorithmType::Invalid,
{
"action", "algorithm", "baseTask", "cache", "exceededNext", "fullMatch",
"hash", "isAscii", "maskRange", "maxTimes", "next", "ocrReplace",
"onErrorNext", "postDelay", "preDelay", "rectMove", "reduceOtherTimes", "replaceFull",
"roi", "specialParams", "sub", "subErrorIgnored", "templThreshold", "template",
"text", "threshold", "withoutDet",
"action", "algorithm", "baseTask", "cache", "exceededNext", "fullMatch",
"hash", "isAscii", "maskRange", "maxTimes", "method", "next",
"ocrReplace", "onErrorNext", "postDelay", "preDelay", "rectMove", "reduceOtherTimes",
"replaceFull", "roi", "specialParams", "sub", "subErrorIgnored", "templThreshold",
"template", "text", "threshold", "withoutDet",
} },
{ AlgorithmType::MatchTemplate,
{
"action", "algorithm", "baseTask", "cache", "exceededNext", "maskRange",
"maxTimes", "next", "onErrorNext", "postDelay", "preDelay", "rectMove",
"reduceOtherTimes", "roi", "sub", "subErrorIgnored", "templThreshold", "template",
"specialParams",
"action", "algorithm", "baseTask", "cache", "exceededNext", "maskRange",
"maxTimes", "method", "next", "onErrorNext", "postDelay", "preDelay",
"rectMove", "reduceOtherTimes", "roi", "sub", "subErrorIgnored", "templThreshold",
"template", "specialParams",
} },
{ AlgorithmType::OcrDetect,
{

View File

@@ -39,6 +39,15 @@ namespace asst::utils
return false;
}
bool parse_json_as(const json::value& input, MatchMethod& output)
{
if (input.is_string()) {
output = get_match_method(input.as_string());
return output != MatchMethod::Invalid;
}
return false;
}
// std::pair<FirstT, SecondT> <- [first, second]
template <typename FirstT, typename SecondT>
requires(requires(const json::value& input, FirstT x, SecondT y) {

View File

@@ -46,12 +46,18 @@ void MatcherConfig::set_mask_range(int lower, int upper, bool mask_with_src, boo
m_params.mask_with_close = mask_with_close;
}
void MatcherConfig::set_method(std::vector<MatchMethod> methods) noexcept
{
m_params.methods = std::move(methods);
}
void MatcherConfig::_set_task_info(MatchTaskInfo task_info)
{
m_params.templs.clear();
ranges::copy(task_info.templ_names, std::back_inserter(m_params.templs));
m_params.templ_thres = std::move(task_info.templ_thresholds);
m_params.mask_range = std::move(task_info.mask_range);
m_params.methods = std::move(task_info.methods);
_set_roi(task_info.roi);
}

View File

@@ -15,6 +15,7 @@ namespace asst
std::vector<std::variant<std::string, cv::Mat>> templs;
std::vector<double> templ_thres;
std::pair<int, int> mask_range;
std::vector<MatchMethod> methods;
bool mask_with_src = false;
bool mask_with_close = false;
};
@@ -33,6 +34,7 @@ namespace asst
void set_threshold(double templ_thres) noexcept;
void set_threshold(std::vector<double> templ_thres) noexcept;
void set_mask_range(int lower, int upper, bool mask_with_src = false, bool mask_with_close = false);
void set_method(std::vector<MatchMethod> methods) noexcept;
protected:
virtual void _set_roi(const Rect& roi) = 0;

View File

@@ -49,7 +49,15 @@ Matcher::ResultOpt Matcher::analyze() const
std::vector<Matcher::RawResult> Matcher::preproc_and_match(const cv::Mat& image, const MatcherConfig::Params& params)
{
std::vector<Matcher::RawResult> results;
for (auto& ptempl : params.templs) {
for (size_t i = 0; i != params.templs.size(); ++i) {
const auto& ptempl = params.templs[i];
const auto& method = params.methods[i];
if (method == MatchMethod::Invalid) {
Log.error(__FUNCTION__, "| invalid method");
return {};
}
cv::Mat templ;
std::string templ_name;
@@ -80,8 +88,17 @@ std::vector<Matcher::RawResult> Matcher::preproc_and_match(const cv::Mat& image,
}
cv::Mat matched;
if (method == MatchMethod::CcoeffHSV) {
cv::cvtColor(image, image, cv::COLOR_BGR2HSV);
cv::cvtColor(templ, templ, cv::COLOR_BGR2HSV);
}
int match_algorithm = cv::TM_CCOEFF_NORMED;
if (method == MatchMethod::Ccoeff || method == MatchMethod::CcoeffHSV) {
match_algorithm = cv::TM_CCOEFF_NORMED;
}
if (params.mask_range.first == 0 && params.mask_range.second == 0) {
cv::matchTemplate(image, templ, matched, cv::TM_CCOEFF_NORMED);
cv::matchTemplate(image, templ, matched, match_algorithm);
}
else {
cv::Mat mask;
@@ -91,7 +108,7 @@ std::vector<Matcher::RawResult> Matcher::preproc_and_match(const cv::Mat& image,
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::morphologyEx(mask, mask, cv::MORPH_CLOSE, kernel);
}
cv::matchTemplate(image, templ, matched, cv::TM_CCOEFF_NORMED, mask);
cv::matchTemplate(image, templ, matched, match_algorithm, mask);
}
results.emplace_back(RawResult { .matched = matched, .templ = templ, .templ_name = templ_name });