feat!: 拆分 maskRange 与 colorScales;增加 colorWithClose 开关数色时闭运算

This commit is contained in:
zzyyyl
2024-08-16 18:29:10 +08:00
parent 4937464195
commit 92f24ec988
11 changed files with 304 additions and 218 deletions

View File

@@ -15,7 +15,7 @@ icon: material-symbols:task
```json
{
"TaskName" : { // 任务名,带 @ 时可能为特殊任务,字段默认值会有不同,详见下方特殊任务类型
"TaskName": { // 任务名,带 @ 时可能为特殊任务,字段默认值会有不同,详见下方特殊任务类型
"baseTask": "xxx", // 以 xxx 任务为模板生成任务,详细见下方特殊任务类型中的 Base Task
@@ -33,7 +33,7 @@ icon: material-symbols:task
// - Stop: 停止当前任务
// - Swipe: 滑动,对应 specificRect 与 rectMove 字段
"sub": [ "SubTaskName1", "SubTaskName2" ],
"sub": ["SubTaskName1", "SubTaskName2"],
// 可选项,子任务,不推荐使用。会在执行完当前任务后,依次执行每一个子任务
// 可以套娃,子任务再套子任务。但要注意不要写出了死循环
@@ -42,7 +42,7 @@ icon: material-symbols:task
// 为 false 时,若某个子任务出错,则不继续执行后续任务(相当于本任务出错了)
// 为 true 时,子任务是否出错没有影响
"next": [ "OtherTaskName1", "OtherTaskName2" ],
"next": ["OtherTaskName1", "OtherTaskName2"],
// 可选项,表示执行完当前任务和 sub 任务后,下一个要执行的任务
// 会从前往后依次去识别,去执行第一个匹配上的
// 不填写默认执行完当前任务直接停止
@@ -54,16 +54,16 @@ icon: material-symbols:task
// 不填写时默认无穷大
// 达到最大次数后,若存在 exceededNext 字段,则执行 exceededNext否则直接任务停止
"exceededNext": [ "OtherTaskName1", "OtherTaskName2" ],
"exceededNext": ["OtherTaskName1", "OtherTaskName2"],
// 可选项,表示达到了最大执行次数后要执行的任务
// 不填写时,达到了最大执行次数则停止;填写后就执行这里的,而不是 next 里的
"onErrorNext": [ "OtherTaskName1", "OtherTaskName2" ],
"onErrorNext": ["OtherTaskName1", "OtherTaskName2"],
// 可选项,表示执行出错时,后续要执行的任务
"preDelay": 1000, // 可选项,表示识别到后延迟多久才执行 action单位毫秒不填写时默认 0
"postDelay": 1000, // 可选项,表示 action 执行完后延迟多久才去识别 next, 单位毫秒;不填写时默认 0
"roi": [ 0, 0, 1280, 720 ], // 可选项,表示识别的范围,格式为 [ x, y, width, height ]
"roi": [0, 0, 1280, 720], // 可选项,表示识别的范围,格式为 [ x, y, width, height ]
// 以 1280 * 720 为基准自动缩放;不填写时默认 [ 0, 0, 1280, 720 ]
// 尽量填写,减小识别范围可以减少性能消耗,加快识别速度
@@ -71,21 +71,21 @@ icon: material-symbols:task
// 第一次识别到后,以后永远只在第一次识别到的位置进行识别,开启可大幅节省性能
// 但仅适用于待识别目标位置完全不会变的任务,若待识别目标位置会变请设为 false
"rectMove": [ 0, 0, 0, 0 ], // 可选项,识别后的目标移动,不建议使用该选项。以 1280 * 720 为基准自动缩放
"rectMove": [0, 0, 0, 0], // 可选项,识别后的目标移动,不建议使用该选项。以 1280 * 720 为基准自动缩放
// 例如识别到了 A ,但实际要点击的是 A 下方 10 像素 5 * 2 区域的某个位置,
// 则可填写[ 0, 10, 5, 2 ],可以的话尽量直接识别要点击的位置,不建议使用该选项
// 额外的,当 action 为 Swipe 时有效且必选,表示滑动终点。
"reduceOtherTimes": [ "OtherTaskName1", "OtherTaskName2" ],
"reduceOtherTimes": ["OtherTaskName1", "OtherTaskName2"],
// 可选项,执行后减少其他任务的执行计数。
// 例如执行了吃理智药,则说明上一次点击蓝色开始行动按钮没生效,所以蓝色开始行动要-1
"specificRect": [ 100, 100, 50, 50 ],
"specificRect": [100, 100, 50, 50],
// 当 action 为 ClickRect 时有效且必选,表示指定的点击位置(范围内随机一点)。
// 当 action 为 Swipe 时有效且必选,表示滑动起点。
// 以 1280 * 720 为基准自动缩放
"specialParams": [ int, ... ], // 某些特殊识别器需要的参数
"specialParams": [int, ...], // 某些特殊识别器需要的参数
// 额外的,当 action 为 Swipe 时可选,[0] 表示 duration[1] 表示 是否启用额外滑动
/* algorithm MatchTemplate */
@@ -96,22 +96,29 @@ icon: material-symbols:task
"templThreshold": 0.8, // 可选项,图片模板匹配得分的阈值,超过阈值才认为识别到了,可以是数字或数字列表
// 默认 0.8, 可根据日志查看实际得分是多少
"maskRange": [ 1, 255 ], // 可选项,掩码范围。 array<int, 2> | list<array<array<int, 3>, 2>>
// 当为 array<int, 2> 时 是灰度掩码范围
// 例如将图片不需要识别的部分涂成黑色(灰度值为 0
// 然后设置"maskRange"的范围为 [ 1, 255 ], 匹配的时候即忽略涂黑的部分
// 当为 list<array<array<int, 3>, 2>> 时
// 最内层代表一个颜色,由 method 决定它是 RGB 或 HSV
// 中间一层是颜色范围的下限和上限;
// 最外层代表允许多个范围,最后取并集作为待识别区域。
"maskRange": [1, 255], // 可选项,匹配时的灰度掩码范围。 array<int, 2>
// 例如将图片不需要识别的部分涂成黑色(灰度值为 0
// 然后设置为 [ 1, 255 ], 匹配的时候即忽略涂黑的部分
"colorScales": [ // 当 method 为 HSVCount 或 RGBCount 时有效且必选,数色掩码范围。
[ // list<array<array<int, 3>, 2> | array<int, 2>>
[23, 150, 40], // 结构为 [[lower1, upper1], [lower2, upper2], ...]
[25, 230, 150] // 内层为 int 时是灰度,
], //   为 array<int, 3> 时是三通道颜色method 决定其是 RGB 或 HSV
... // 中间一层的 array<*, 2> 是颜色(或灰度)下限与上限:
], // 最外层代表不同的颜色范围,待识别区域为它们对应在模板图片上掩码的并集。
"colorWithClose": true, // 可选项,当 method 为 HSVCount 或 RGBCount 时有效,默认为 true
// 数色时是否先用闭运算处理掩码范围。
// 闭运算可以填补小黑点,一般会提高数色匹配效果,但若图片中包含文字建议设为 false
"method": "Ccoeff", // 可选项,模板匹配算法,可以是列表
// 不填写时默认为 Ccoeff
// - Ccoeff: 对应 cv::TM_CCOEFF_NORMED
// - RGBCount: 先将待匹配区域和模板图片依据 maskRange 二值化
// - Ccoeff: 对颜色不敏感的模板匹配算法,对应 cv::TM_CCOEFF_NORMED
// - RGBCount: 对颜色敏感的模板匹配算法
// 先将待匹配区域和模板图片依据 maskRange 二值化,
// 以 F1-score 为指标计算 RGB 颜色空间内的相似度,
// 再将结果与 Ccoeff 的结果点积
// 总之是一个颜色敏感的模板匹配算法
// - HSVCount: 类似 RGBCount颜色空间换为 HSV
/* algorithm OcrDetect */

View File

@@ -3838,16 +3838,21 @@
"postDelay": 1500
},
"InfrastOperSelected": {
"Doc": "参考 InfrastOperImageAnalyzer",
"template": "empty.png",
"templThreshold": 800,
"templThreshold_Doc": "这个任务中作为符合range的像素点数量阈值",
"maskRange": [
"specialParams_Doc": "这个任务中作为数色个数阈值",
"specialParams": [800],
"specificRect_Doc": "selected_move",
"specificRect": [-18, -220, 10, 160],
"rectMove_Doc": "oper_move",
"rectMove": [18, 0, 10, 160],
"colorScales_Doc": "HSV的范围",
"colorScales": [
[
[95, 0, 0],
[105, 255, 255]
]
],
"rectMove": [-18, -220, 10, 160]
]
},
"InfrastOperOnShift": {
"template": "OnShift.png",
@@ -4682,11 +4687,11 @@
},
"BattleOperCooling": {
"template": "empty.png",
"specialParams_Doc": "这个任务中作为数色个数阈值",
"specialParams": [300],
"specialParams_Doc": "这个任务中作为符合range的像素点数量阈值",
"rectMove": [-68, 124, 114, 4],
"maskRange_Doc": "HSV的范围",
"maskRange": [
"colorScales_Doc": "HSV的范围",
"colorScales": [
[
[0, 100, 0],
[20, 255, 150]
@@ -4779,7 +4784,7 @@
"templThreshold_Doc": "这里用来作为基线间距阈值",
"specialParams": [100],
"specialParams_Doc": "基线长度阈值不小于100",
"maskRange": [45, 255],
"colorScales": [[45, 255]],
"rectMove": [181, 40, 25, 0],
"rectMove_Doc": "作为hsv阈值使用"
},
@@ -4822,7 +4827,7 @@
"templThreshold": 3,
"templThreshold_Doc": "这里用来作为字间距阈值",
"roi": [62, 90, 43, 22],
"maskRange": [110, 255]
"colorScales": [[110, 255]]
},
"StageDrops-StageCF-ItemMatch": {
"template": "empty.png",
@@ -9550,7 +9555,7 @@
"Doc": "思维边界",
"baseTask": "Sarkaz@Roguelike@Stage",
"method": "HSVCount",
"maskRange": [
"colorScales": [
[
[90, 2, 121],
[134, 142, 230]
@@ -9561,7 +9566,7 @@
"Sarkaz@Roguelike@StageBoskyPassageEnter": {
"baseTask": "Sarkaz@Roguelike@StageEnter",
"method": "HSVCount",
"maskRange": [
"colorScales": [
[
[137, 150, 40],
[138, 180, 180]
@@ -9607,7 +9612,7 @@
},
"Sarkaz@Roguelike@StageCombatDps": {
"method": "HSVCount",
"maskRange": [
"colorScales": [
[
[127, 80, 50],
[136, 200, 180]
@@ -9623,7 +9628,7 @@
},
"Sarkaz@Roguelike@StageCombatDpsEnter": {
"method": "HSVCount",
"maskRange": [
"colorScales": [
[
[130, 130, 40],
[131, 180, 180]
@@ -9669,7 +9674,7 @@
"Doc": "先行一步拒绝可以获取2希望",
"baseTask": "Sarkaz@Roguelike@Stage",
"method": "HSVCount",
"maskRange": [
"colorScales": [
[
[15, 1, 93],
[93, 129, 141]
@@ -9754,7 +9759,7 @@
"Sarkaz@Roguelike@StageFilterTruthEnter": {
"baseTask": "Sarkaz@Roguelike@StageEnter",
"method": "HSVCount",
"maskRange": [
"colorScales": [
[
[23, 150, 40],
[25, 230, 150]
@@ -9815,7 +9820,7 @@
"Doc": "命运所指",
"baseTask": "Sarkaz@Roguelike@Stage",
"method": "HSVCount",
"maskRange": [
"colorScales": [
[
[18, 1, 94],
[93, 173, 109]
@@ -9827,7 +9832,7 @@
"action": "ClickSelf",
"method": "RGBCount",
"postDelay": 500,
"maskRange": [0, 200],
"colorScales": [[0, 200]],
"roi": [790, 130, 60, 144],
"next": ["Sarkaz@Roguelike@StageRefreshConfirm", "Sarkaz@Roguelike@StageRefreshUpperLimit"]
},
@@ -9874,7 +9879,7 @@
},
"Sarkaz@Roguelike@StageSafeHouseEnter": {
"method": "HSVCount",
"maskRange": [
"colorScales": [
[
[106, 100, 50],
[107, 160, 180]
@@ -9913,7 +9918,7 @@
},
"Sarkaz@Roguelike@StageTraderEnter": {
"method": "HSVCount",
"maskRange": [
"colorScales": [
[
[71, 150, 40],
[73, 180, 150]
@@ -9944,7 +9949,7 @@
"Doc": "失与得",
"baseTask": "Sarkaz@Roguelike@Stage",
"method": "HSVCount",
"maskRange": [
"colorScales": [
[
[18, 130, 155],
[20, 180, 180]

View File

@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <climits>
#include <cmath>
#include <functional>
@@ -7,6 +8,7 @@
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <variant>
#include <vector>
#include "Utils/StringMisc.hpp"
@@ -491,11 +493,16 @@ namespace asst
constexpr MatchTaskInfo(MatchTaskInfo&&) noexcept = default;
constexpr MatchTaskInfo& operator=(const MatchTaskInfo&) = default;
constexpr MatchTaskInfo& operator=(MatchTaskInfo&&) noexcept = default;
using Range = std::pair<std::vector<int>, std::vector<int>>;
using GrayRange = std::pair<int, int>;
using ColorRange = std::pair<std::array<int, 3>, std::array<int, 3>>;
using Range = std::variant<GrayRange, ColorRange>;
using Ranges = std::vector<Range>;
std::vector<std::string> templ_names; // 匹配模板图片文件名
std::vector<double> templ_thresholds; // 模板匹配阈值
std::vector<Range> mask_range; // 掩码的二值化范围
std::vector<MatchMethod> methods; // 匹配方法
Ranges mask_ranges; // 匹配掩码范围TaskData 仅允许 array<int, 2>,但保留彩色掩码支持
Ranges color_scales; // 数色掩码范围
bool color_close = true; // 数色时是否使用闭运算处理
};
using MatchTaskPtr = std::shared_ptr<MatchTaskInfo>;
using MatchTaskConstPtr = std::shared_ptr<const MatchTaskInfo>;

View File

@@ -171,6 +171,17 @@ bool asst::TaskData::lazy_parse(const json::value& json)
check_tasklist(task->exceeded_next, "exceeded_next", enable_justreturn_check);
check_tasklist(task->on_error_next, "on_error_next", enable_justreturn_check);
check_tasklist(task->reduce_other_times, "reduce_other_times");
static const std::unordered_set count_methods { MatchMethod::RGBCount, MatchMethod::HSVCount };
if (auto match_task = std::dynamic_pointer_cast<MatchTaskInfo>(task);
task->algorithm == AlgorithmType::MatchTemplate &&
ranges::find_if(match_task->methods, [&](MatchMethod m) { return count_methods.contains(m); }) !=
match_task->methods.cend() &&
match_task->color_scales.empty()) {
// RGBCount 和 HSVCount 必须有 color_scales
Log.error("Task", name, "with Count method has empty color_scales");
validity = false;
}
}
if (checking_task_set.size() > MAX_CHECKING_SIZE) {
// 生成超出上限一般是出现了会导致无限隐式生成的任务。比如 "#self@LoadingText". 这里给个警告.
@@ -523,71 +534,108 @@ asst::TaskPtr asst::TaskData::generate_match_task_info(std::string_view name, co
return nullptr;
}
auto mask_opt = task_json.find("maskRange");
if (!mask_opt) {
match_task_info_ptr->mask_range = default_ptr->mask_range;
if (auto mask_opt = task_json.find("maskRange"); !mask_opt) {
match_task_info_ptr->mask_ranges = default_ptr->mask_ranges;
}
else if (!mask_opt->is_array()) {
Log.error("Invalid mask type in task", name);
Log.error("Invalid mask_range type in task", name, ", should be `array<int, 2>`");
return nullptr;
}
else if (auto mask_opt_array = mask_opt->as_array(); mask_opt_array.size() == 2
&& mask_opt_array[0].is_number()
&& mask_opt_array[1].is_number()) {
match_task_info_ptr->mask_range.emplace_back(
std::vector { mask_opt_array[0].as_integer() },
std::vector { mask_opt_array[1].as_integer() });
else if (auto mask_array = mask_opt->as_array();
mask_array.size() == 2 && mask_array[0].is_number() && mask_array[1].is_number()) {
match_task_info_ptr->mask_ranges.emplace_back(
MatchTaskInfo::GrayRange { mask_array[0].as_integer(), mask_array[1].as_integer() });
}
else {
Log.error("Invalid mask_range in task", name);
return nullptr;
}
if (auto color_opt = task_json.find("colorScales"); !color_opt) {
match_task_info_ptr->color_scales = default_ptr->color_scales;
}
else if (!color_opt->is_array()) {
Log.error("Invalid color_scales type in task", name);
return nullptr;
}
else if (auto color_array = color_opt->as_array();
color_array.size() == 2 && color_array[0].is_number() && color_array[1].is_number()) {
// gray scale, color_array is array<int, 2>
Log.debug("Deprecated GrayRange color_scales in task", name, ", should be `list<pair<int, int>>`");
match_task_info_ptr->color_scales.emplace_back(
MatchTaskInfo::GrayRange { color_array[0].as_integer(), color_array[1].as_integer() });
}
else {
/* [
[[0, 0, 0], [0, 0, 255]],
[[0, 0, 0], [0, 255, 0]],
[[0, 0, 0], [255, 0, 0]]
[1, 255]
]
*/
match_task_info_ptr->mask_range.clear();
for (const auto& mask : mask_opt_array) {
if (!mask.is_array()) {
Log.error("Invalid mask in task", name);
match_task_info_ptr->color_scales.clear();
for (const auto& color_array_item : color_array) {
if (!color_array_item.is_array()) {
Log.error("Invalid color_range in task", name);
return nullptr;
}
const auto& mask_range = mask.as_array();
if (mask_range.size() != 2) {
Log.error("Invalid mask in task", name);
const auto& color_range = color_array_item.as_array();
if (color_range.size() != 2) { // lower & upper, 2 elements
Log.error("Invalid color_range in task", name, ", should have 2 elements (lower & upper) in each array");
return nullptr;
}
if (!mask_range[0].is_array() || !mask_range[1].is_array()) {
Log.error("Invalid mask in task", name);
return nullptr;
}
const auto& lower = mask_range[0].as_array();
const auto& upper = mask_range[1].as_array();
if (!ranges::all_of(lower, [](const json::value& v) { return v.is_number(); }) ||
!ranges::all_of(upper, [](const json::value& v) { return v.is_number(); })) {
Log.error("Invalid mask in task", name);
return nullptr;
}
if (lower.size() == 1 && upper.size() == 1) {
match_task_info_ptr->mask_range.emplace_back(
std::vector { lower[0].as_integer() },
std::vector { upper[0].as_integer() });
const auto& lower_item = color_range[0];
const auto& upper_item = color_range[1];
// gray scale, color_array_item is array<int, 2> (recommended)
if (lower_item.is_number() && upper_item.is_number()) {
match_task_info_ptr->color_scales.emplace_back(
MatchTaskInfo::GrayRange { lower_item.as_integer(), upper_item.as_integer() });
continue;
}
if (lower.size() == 3 && upper.size() == 3) {
match_task_info_ptr->mask_range.emplace_back(
std::vector { lower[0].as_integer(),
lower[1].as_integer(),
lower[2].as_integer() },
std::vector { upper[0].as_integer(),
upper[1].as_integer(),
upper[2].as_integer() });
if (!lower_item.is_array() || !upper_item.is_array()) {
Log.error("Invalid color_range in task", name);
return nullptr;
}
// color, color_array_item is array<array<int, 3>, 2>
const auto& lower = lower_item.as_array();
const auto& upper = upper_item.as_array();
if (!ranges::all_of(lower, &json::value::is_number) || !ranges::all_of(upper, &json::value::is_number)) {
Log.error("Invalid color_range in task", name);
return nullptr;
}
auto lower_number = lower | views::transform(&json::value::as_integer);
auto upper_number = upper | views::transform(&json::value::as_integer);
if (lower_number.size() == 1 && upper_number.size() == 1) {
// gray scale "[..., [[0], [255]], ...]"
Log.debug("Not recommended GrayRange color_scales in task", name, ", should be `list<pair<int, int>>`");
match_task_info_ptr->color_scales.emplace_back(
MatchTaskInfo::GrayRange { lower_number[0], upper_number[0] });
continue;
}
Log.error("Invalid mask in task", name);
if (lower_number.size() == 3 && upper_number.size() == 3) {
// color scale "[..., [[0, 0, 0], [0, 0, 255]], ...]"
match_task_info_ptr->color_scales.emplace_back(
MatchTaskInfo::ColorRange { std::array { lower_number[0], lower_number[1], lower_number[2] },
std::array { upper_number[0], upper_number[1], upper_number[2] } });
continue;
}
Log.error("Invalid color_range in task", name);
return nullptr;
}
}
utils::get_and_check_value_or(
name,
task_json,
"colorWithClose",
match_task_info_ptr->color_close,
default_ptr->color_close);
return match_task_info_ptr;
}
@@ -763,6 +811,9 @@ asst::MatchTaskConstPtr asst::TaskData::_default_match_task_info()
match_task_info_ptr->templ_names = { "__INVALID__" };
match_task_info_ptr->templ_thresholds = { TemplThresholdDefault };
match_task_info_ptr->methods = { MatchMethod::Ccoeff };
match_task_info_ptr->mask_ranges = {};
match_task_info_ptr->color_scales = {};
match_task_info_ptr->color_close = true;
return match_task_info_ptr;
}
@@ -800,36 +851,40 @@ asst::TaskConstPtr asst::TaskData::_default_task_info()
// 主要是处理是否包含未知键值的问题
bool asst::TaskData::syntax_check(std::string_view task_name, const json::value& task_json)
{
// clang-format off
// 以下按字典序排序
// clang-format off
static const std::unordered_map<AlgorithmType, std::unordered_set<std::string>> allowed_key_under_algorithm = {
{ AlgorithmType::Invalid,
{
"action", "algorithm", "baseTask", "cache", "exceededNext", "fullMatch",
"isAscii", "maskRange", "maxTimes", "method", "next", "ocrReplace",
"onErrorNext", "postDelay", "preDelay", "rectMove", "reduceOtherTimes", "replaceFull",
"roi", "specialParams", "sub", "subErrorIgnored", "templThreshold", "template",
"text", "withoutDet",
} },
{ AlgorithmType::Invalid, {} },
{ AlgorithmType::MatchTemplate,
{
"action", "algorithm", "baseTask", "cache", "exceededNext", "maskRange",
"maxTimes", "method", "next", "onErrorNext", "postDelay", "preDelay",
"rectMove", "reduceOtherTimes", "roi", "sub", "subErrorIgnored", "templThreshold",
"template", "specialParams",
// common
"action", "algorithm", "baseTask", "exceededNext", "maxTimes",
"next", "onErrorNext", "postDelay", "preDelay", "reduceOtherTimes",
"specialParams", "sub", "subErrorIgnored",
// specific
"cache", "colorScales", "colorWithClose", "maskRange", "method",
"rectMove", "roi", "specialParams", "templThreshold", "template",
} },
{ AlgorithmType::OcrDetect,
{
"action", "algorithm", "baseTask", "cache", "exceededNext", "fullMatch",
"isAscii", "maxTimes", "next", "ocrReplace", "onErrorNext", "postDelay",
"preDelay", "rectMove", "reduceOtherTimes", "replaceFull", "roi", "sub",
"subErrorIgnored", "text", "withoutDet", "specialParams",
// common
"action", "algorithm", "baseTask", "exceededNext", "maxTimes",
"next", "onErrorNext", "postDelay", "preDelay", "reduceOtherTimes",
"specialParams", "sub", "subErrorIgnored",
// specific
"cache", "fullMatch", "isAscii", "ocrReplace", "rectMove",
"replaceFull", "roi", "text", "withoutDet",
} },
{ AlgorithmType::JustReturn,
{
"action", "algorithm", "baseTask", "exceededNext", "maxTimes", "next",
"onErrorNext", "postDelay", "preDelay", "reduceOtherTimes", "specialParams", "sub",
"subErrorIgnored",
// common
"action", "algorithm", "baseTask", "exceededNext", "maxTimes",
"next", "onErrorNext", "postDelay", "preDelay", "reduceOtherTimes",
"specialParams", "sub", "subErrorIgnored",
// specific
} },
};
// clang-format on

View File

@@ -158,9 +158,9 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable, b
static const auto cooling_threshold =
Task.get<MatchTaskInfo>("BattleAvatarCoolingData")->templ_thresholds.front();
static const auto cooling_mask_range =
Task.get<MatchTaskInfo>("BattleAvatarCoolingData")->mask_range;
Task.get<MatchTaskInfo>("BattleAvatarCoolingData")->mask_ranges;
avatar_analyzer.set_threshold(cooling_threshold);
avatar_analyzer.set_mask_range(cooling_mask_range, true, true);
avatar_analyzer.set_mask_ranges(cooling_mask_range, true, true);
}
else {
static const auto threshold =

View File

@@ -190,23 +190,20 @@ bool BattlefieldMatcher::oper_cooling_analyze(const Rect& roi) const
{
const auto cooling_task_ptr = Task.get<MatchTaskInfo>("BattleOperCooling");
auto img_roi = m_image(make_rect<cv::Rect>(roi));
cv::Mat hsv;
cv::cvtColor(img_roi, hsv, cv::COLOR_BGR2HSV);
const auto& mask_range = cooling_task_ptr->mask_range[0];
cv::Mat bin;
cv::inRange(hsv, mask_range.first, mask_range.second, bin);
int count = 0;
for (int i = 0; i != bin.rows; ++i) {
for (int j = 0; j != bin.cols; ++j) {
cv::uint8_t value = bin.at<cv::uint8_t>(i, j);
if (value) {
++count;
}
}
if (cooling_task_ptr->color_scales.size() != 1 ||
!std::holds_alternative<MatchTaskInfo::ColorRange>(cooling_task_ptr->color_scales.front())) {
Log.error(__FUNCTION__, "| color_scales in `BattleOperCooling` is not a ColorRange");
return false;
}
const auto& color_scale = std::get<MatchTaskInfo::ColorRange>(cooling_task_ptr->color_scales.front());
auto img_roi = m_image(make_rect<cv::Rect>(roi));
cv::Mat hsv, bin;
cv::cvtColor(img_roi, hsv, cv::COLOR_BGR2HSV);
cv::inRange(hsv, color_scale.first, color_scale.second, bin);
int count = cv::countNonZero(bin);
// Log.trace("oper_cooling_analyze |", count);
return count >= cooling_task_ptr->special_params.front();
}

View File

@@ -39,21 +39,24 @@ void asst::MatcherConfig::set_threshold(std::vector<double> templ_thres) noexcep
m_params.templ_thres = std::move(templ_thres);
}
void MatcherConfig::set_mask_range(int lower, int upper, bool mask_with_src, bool mask_with_close)
void MatcherConfig::set_mask_range(int lower, int upper, bool mask_src, bool mask_close)
{
m_params.mask_range = { { { lower }, { upper } } };
m_params.mask_with_src = mask_with_src;
m_params.mask_with_close = mask_with_close;
m_params.mask_ranges = { MatchTaskInfo::GrayRange { lower, upper } };
m_params.mask_src = mask_src;
m_params.mask_close = mask_close;
}
void MatcherConfig::set_mask_range(
std::vector<MatchTaskInfo::Range> mask_range,
bool mask_with_src,
bool mask_with_close)
void MatcherConfig::set_mask_ranges(MatchTaskInfo::Ranges mask_ranges, bool mask_src, bool mask_close)
{
m_params.mask_range = std::move(mask_range);
m_params.mask_with_src = mask_with_src;
m_params.mask_with_close = mask_with_close;
m_params.mask_ranges = std::move(mask_ranges);
m_params.mask_src = mask_src;
m_params.mask_close = mask_close;
}
void MatcherConfig::set_color_scales(MatchTaskInfo::Ranges color_scales, bool color_close)
{
m_params.color_scales = std::move(color_scales);
m_params.color_close = color_close;
}
void MatcherConfig::set_method(MatchMethod method) noexcept
@@ -66,7 +69,9 @@ 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.mask_ranges = std::move(task_info.mask_ranges);
m_params.color_scales = std::move(task_info.color_scales);
m_params.color_close = task_info.color_close;
m_params.methods = std::move(task_info.methods);
_set_roi(task_info.roi);

View File

@@ -14,10 +14,12 @@ namespace asst
{
std::vector<std::variant<std::string, cv::Mat>> templs;
std::vector<double> templ_thres;
std::vector<MatchTaskInfo::Range> mask_range;
std::vector<MatchMethod> methods;
bool mask_with_src = false;
bool mask_with_close = false;
std::vector<MatchMethod> methods; // 匹配方法
MatchTaskInfo::Ranges mask_ranges; // 匹配时的颜色掩码范围
bool mask_src = false; // 匹配时是否使用原图掩码(默认使用模板掩码)
bool mask_close = false; // 匹配时是否使用闭运算处理
MatchTaskInfo::Ranges color_scales; // 数色时的颜色掩码范围
bool color_close = true; // 数色时是否使用闭运算处理
};
public:
@@ -33,11 +35,9 @@ namespace asst
// void set_templ(std::vector<std::variant<std::string, cv::Mat>> templs);
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_mask_range(
std::vector<MatchTaskInfo::Range> mask_range,
bool mask_with_src = false,
bool mask_with_close = false);
void set_mask_range(int lower, int upper, bool mask_src = false, bool mask_close = false);
void set_mask_ranges(MatchTaskInfo::Ranges mask_ranges, bool mask_src = false, bool mask_close = false);
void set_color_scales(MatchTaskInfo::Ranges color_scales, bool color_close = true);
void set_method(MatchMethod method) noexcept;
protected:

View File

@@ -228,7 +228,7 @@ void asst::InfrastOperImageAnalyzer::skill_analyze()
Matcher skill_analyzer(m_image);
skill_analyzer.set_mask_range(task_ptr->mask_range);
skill_analyzer.set_mask_ranges(task_ptr->mask_ranges);
skill_analyzer.set_threshold(task_ptr->templ_thresholds.front());
skill_analyzer.set_method(task_ptr->methods.front());
@@ -346,34 +346,28 @@ void asst::InfrastOperImageAnalyzer::selected_analyze()
LogTraceFunction;
const auto selected_task_ptr = Task.get<MatchTaskInfo>("InfrastOperSelected");
Rect rect_move = selected_task_ptr->rect_move;
const Rect selected_move = selected_task_ptr->specific_rect;
const Rect oper_move = selected_task_ptr->rect_move;
if (selected_task_ptr->color_scales.size() != 1 ||
!std::holds_alternative<MatchTaskInfo::ColorRange>(selected_task_ptr->color_scales.front())) {
Log.error(__FUNCTION__, "| color_scales in `InfrastOperSelected` is not a ColorRange");
return;
}
const auto& color_scale = std::get<MatchTaskInfo::ColorRange>(selected_task_ptr->color_scales.front());
for (auto&& oper : m_result) {
Rect selected_rect = rect_move;
selected_rect.x += oper.smiley.rect.x;
selected_rect.y += oper.smiley.rect.y;
Rect selected_rect = oper.smiley.rect.move(selected_move);
cv::Mat roi = m_image(make_rect<cv::Rect>(selected_rect));
cv::Mat hsv, bin;
cv::cvtColor(roi, hsv, cv::COLOR_BGR2HSV);
std::vector<cv::Mat> channels;
cv::split(hsv, channels);
int mask_lowb = selected_task_ptr->mask_range[0].first[0];
int mask_uppb = selected_task_ptr->mask_range[0].second[0];
cv::inRange(hsv, color_scale.first, color_scale.second, bin);
int count = cv::countNonZero(bin);
int count = 0;
auto& h_channel = channels.at(0);
for (int i = 0; i != h_channel.rows; ++i) {
for (int j = 0; j != h_channel.cols; ++j) {
cv::uint8_t value = h_channel.at<cv::uint8_t>(i, j);
if (mask_lowb < value && value < mask_uppb) {
++count;
}
}
}
Log.trace("selected_analyze |", count);
oper.selected = count >= selected_task_ptr->templ_thresholds.front();
oper.rect = selected_rect.move({ 18, 0, 10, 160 }); // 先凑合用(
oper.selected = count >= selected_task_ptr->special_params.front();
oper.rect = selected_rect.move(oper_move);
}
}

View File

@@ -32,7 +32,11 @@ Matcher::ResultOpt Matcher::analyze() const
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);
}
#ifdef ASST_DEBUG
else {
Log.debug("match_templ |", templ_name, "score:", max_val, "rect:", rect, "roi:", m_roi);
}
#endif
if (max_val < threshold) {
continue;
}
@@ -95,40 +99,44 @@ std::vector<Matcher::RawResult> Matcher::preproc_and_match(const cv::Mat& image,
}
cv::Mat matched;
cv::Mat image_for_match;
cv::Mat templ_for_match;
cv::Mat image_for_count;
cv::Mat templ_for_count;
cv::cvtColor(image, image_for_match, cv::COLOR_BGR2RGB);
cv::cvtColor(templ, templ_for_match, cv::COLOR_BGR2RGB);
cv::Mat image_match, image_count, image_gray;
cv::Mat templ_match, templ_count, templ_gray;
cv::cvtColor(image, image_match, cv::COLOR_BGR2RGB);
cv::cvtColor(templ, templ_match, cv::COLOR_BGR2RGB);
cv::cvtColor(image, image_gray, cv::COLOR_BGR2GRAY);
cv::cvtColor(templ, templ_gray, cv::COLOR_BGR2GRAY);
if (method == MatchMethod::HSVCount) {
cv::cvtColor(image, image_for_count, cv::COLOR_BGR2HSV);
cv::cvtColor(templ, templ_for_count, cv::COLOR_BGR2HSV);
cv::cvtColor(image, image_count, cv::COLOR_BGR2HSV);
cv::cvtColor(templ, templ_count, cv::COLOR_BGR2HSV);
}
else if (method == MatchMethod::RGBCount) {
image_for_count = image_for_match;
templ_for_count = templ_for_match;
image_count = image_match;
templ_count = templ_match;
}
// 目前所有的匹配都是用 TM_CCOEFF_NORMED
int match_algorithm = cv::TM_CCOEFF_NORMED;
auto calc_mask = [&](const cv::Mat& templ_for_mask, bool with_close)->std::optional<cv::Mat> {
cv::Mat templ_for_gray_mask;
cv::cvtColor(templ_for_mask, templ_for_gray_mask, cv::COLOR_BGR2GRAY);
auto calc_mask = [&templ_name](
const MatchTaskInfo::Ranges mask_ranges,
const cv::Mat& templ,
const cv::Mat& templ_gray,
bool with_close)
-> std::optional<cv::Mat> {
// Union all masks, not intersection
cv::Mat mask = cv::Mat::zeros(templ_for_gray_mask.size(), CV_8UC1);
for (const auto& range : params.mask_range) {
cv::Mat mask = cv::Mat::zeros(templ_gray.size(), CV_8UC1);
for (const auto& range : mask_ranges) {
cv::Mat current_mask;
if (range.first.size() == 1 && range.second.size() == 1) {
cv::inRange(templ_for_gray_mask, range.first[0], range.second[0], current_mask);
if (std::holds_alternative<MatchTaskInfo::GrayRange>(range)) {
const auto& gray_range = std::get<MatchTaskInfo::GrayRange>(range);
cv::inRange(templ_gray, gray_range.first, gray_range.second, current_mask);
}
else if (range.first.size() == 3 && range.second.size() == 3) {
cv::inRange(templ_for_mask, range.first, range.second, current_mask);
else if (std::holds_alternative<MatchTaskInfo::ColorRange>(range)) {
const auto& color_range = std::get<MatchTaskInfo::ColorRange>(range);
cv::inRange(templ, color_range.first, color_range.second, current_mask);
}
else {
Log.error("Invalid mask range");
Log.error("The task with template", templ_name, "holds invalid mask range");
return std::nullopt;
}
cv::bitwise_or(mask, current_mask, mask);
@@ -141,35 +149,31 @@ std::vector<Matcher::RawResult> Matcher::preproc_and_match(const cv::Mat& image,
return mask;
};
if (params.mask_range.empty() || method == MatchMethod::RGBCount || method == MatchMethod::HSVCount) {
// workaround: 数色时的模板匹配忽略 maskRange
// TODO: 区分 maskRange 和 colorRange
cv::matchTemplate(image_for_match, templ_for_match, matched, match_algorithm);
if (params.mask_ranges.empty()) {
cv::matchTemplate(image_match, templ_match, matched, match_algorithm);
}
else {
// match 时使用的 mask_range 当作 RGB 的
auto mask_opt = calc_mask(
params.mask_with_src ? image_for_match : templ_for_match,
params.mask_with_close);
params.mask_ranges,
params.mask_src ? image_match : templ_match,
params.mask_src ? image_gray : templ_gray,
params.mask_close);
if (!mask_opt) {
return {};
}
cv::matchTemplate(image_for_match, templ_for_match, matched, match_algorithm, mask_opt.value());
cv::matchTemplate(image_match, templ_match, matched, match_algorithm, mask_opt.value());
}
if (method == MatchMethod::RGBCount || method == MatchMethod::HSVCount) {
auto templ_active_opt = calc_mask(templ_for_count, false);
auto image_active_opt = calc_mask(image_for_count, false);
auto templ_active_opt = calc_mask(params.color_scales, templ_count, templ_gray, params.color_close);
auto image_active_opt = calc_mask(params.color_scales, image_count, image_gray, params.color_close);
if (!image_active_opt || !templ_active_opt) [[unlikely]] {
return {};
}
cv::Mat templ_active = std::move(templ_active_opt).value();
cv::Mat image_active = std::move(image_active_opt).value();
// 闭运算填充小空洞,避免数色的得分过低 TODO: 或许可以做成可选的
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::morphologyEx(templ_active, templ_active, cv::MORPH_CLOSE, kernel);
cv::morphologyEx(image_active, image_active, cv::MORPH_CLOSE, kernel);
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 形状内的像素数量
@@ -181,10 +185,9 @@ std::vector<Matcher::RawResult> Matcher::preproc_and_match(const cv::Mat& image,
// TODO: 这里 TP+FP 是 image_active 的 count可以消掉一个 matchtemplate
cv::matchTemplate(image_active, templ_inactive, fp, cv::TM_CCORR);
fp.convertTo(fp, CV_32S);
cv::Mat count_result; // 数色结果为 f1_score
cv::divide(2 * tp, tp + fp + tp_fn, count_result, 1, CV_32F);
// 返回的是数色和模板匹配的点积
cv::multiply(matched, count_result, matched);
cv::Mat count_result;
cv::divide(2 * tp, tp + fp + tp_fn, count_result, 1, CV_32F); // 数色结果为 f1_score
cv::multiply(matched, count_result, matched); // 最终结果是数色和模板匹配的点积
}
results.emplace_back(RawResult { .matched = matched, .templ = templ, .templ_name = templ_name });
}

View File

@@ -400,6 +400,12 @@ bool asst::StageDropsImageAnalyzer::analyze_baseline()
m_baseline.clear();
auto task_ptr = Task.get<MatchTaskInfo>("StageDrops-BaseLine");
if (task_ptr->color_scales.size() != 1 ||
!std::holds_alternative<MatchTaskInfo::GrayRange>(task_ptr->color_scales.front())) {
Log.error(__FUNCTION__, "| color_scales in `StageDrops-BaseLine` is not a GrayRange");
return false;
}
const auto& color_scale = std::get<MatchTaskInfo::GrayRange>(task_ptr->color_scales.front());
cv::Mat preprocessed_roi;
@@ -426,11 +432,7 @@ bool asst::StageDropsImageAnalyzer::analyze_baseline()
}
cv::Mat preprocessed_bin;
cv::inRange(
preprocessed_roi,
task_ptr->mask_range[0].first[0],
task_ptr->mask_range[0].second[0],
preprocessed_bin);
cv::inRange(preprocessed_roi, color_scale.first, color_scale.second, preprocessed_bin);
cv::Rect bounding_rect = cv::boundingRect(preprocessed_bin);
cv::Mat bounding = preprocessed_bin(bounding_rect);
@@ -594,7 +596,7 @@ std::string asst::StageDropsImageAnalyzer::match_item(const Rect& roi, StageDrop
auto match_item_with_templs = [&](const std::vector<std::string>& templs_list) -> std::string {
Matcher analyzer(m_image);
analyzer.set_mask_range(std::vector<MatchTaskInfo::Range> {}, false, true);
analyzer.set_mask_ranges({}, false, true);
analyzer.set_task_info("StageDrops-Item");
analyzer.set_roi(roi);
@@ -638,14 +640,19 @@ std::optional<asst::TextRect> asst::StageDropsImageAnalyzer::match_quantity_stri
bool use_word_model)
{
auto task_ptr = Task.get<MatchTaskInfo>("StageDrops-Quantity");
if (task_ptr->color_scales.size() != 1 ||
!std::holds_alternative<MatchTaskInfo::GrayRange>(task_ptr->color_scales.front())) {
Log.error(__FUNCTION__, "| color_scales in `StageDrops-Quantity` is not a GrayRange");
return std::nullopt;
}
const auto& color_scale = std::get<MatchTaskInfo::GrayRange>(task_ptr->color_scales.front());
Rect quantity_roi = roi.move(task_ptr->roi);
cv::Mat quantity_img = m_image(make_rect<cv::Rect>(quantity_roi));
cv::Mat gray;
cv::Mat gray, bin;
cv::cvtColor(quantity_img, gray, cv::COLOR_BGR2GRAY);
cv::Mat bin;
cv::inRange(gray, task_ptr->mask_range[0].first[0], task_ptr->mask_range[0].second[0], bin);
cv::inRange(gray, color_scale.first, color_scale.second, bin);
// split
const int max_spacing = static_cast<int>(task_ptr->templ_thresholds.front());
@@ -693,7 +700,7 @@ std::optional<asst::TextRect> asst::StageDropsImageAnalyzer::match_quantity_stri
RegionOCRer analyzer(m_image);
analyzer.set_task_info("NumberOcrReplace");
analyzer.set_roi(Rect(quantity_roi.x + far_left, quantity_roi.y, far_right - far_left, quantity_roi.height));
analyzer.set_bin_threshold(task_ptr->mask_range[0].first[0], task_ptr->mask_range[0].second[0]);
analyzer.set_bin_threshold(color_scale.first, color_scale.second);
analyzer.set_use_char_model(!use_word_model);
if (!analyzer.analyze()) {
@@ -708,6 +715,12 @@ std::optional<asst::TextRect> asst::StageDropsImageAnalyzer::match_quantity_stri
bool use_word_model)
{
auto task_ptr = Task.get<MatchTaskInfo>("StageDrops-Quantity");
if (task_ptr->color_scales.size() != 1 ||
!std::holds_alternative<MatchTaskInfo::GrayRange>(task_ptr->color_scales.front())) {
Log.error(__FUNCTION__, "| color_scales in `StageDrops-Quantity` is not a GrayRange");
return std::nullopt;
}
const auto& color_scale = std::get<MatchTaskInfo::GrayRange>(task_ptr->color_scales.front());
auto templ = TemplResource::get_instance().get_templ(item).clone();
if (templ.empty()) {
Log.error("templ is empty: ", item);
@@ -763,7 +776,7 @@ std::optional<asst::TextRect> asst::StageDropsImageAnalyzer::match_quantity_stri
Rect ocr_roi { new_roi.x + mask_rect.x, new_roi.y + mask_rect.y, mask_rect.width, mask_rect.height };
ocr.set_roi(ocr_roi);
ocr.set_use_char_model(!use_word_model);
ocr.set_bin_threshold(task_ptr->mask_range[0].first[0], task_ptr->mask_range[0].second[0]);
ocr.set_bin_threshold(color_scale.first, color_scale.second);
if (!ocr.analyze()) {
return std::nullopt;