diff --git a/docs/zh-cn/protocol/task-schema.md b/docs/zh-cn/protocol/task-schema.md index 4cf78c575b..18477e8311 100644 --- a/docs/zh-cn/protocol/task-schema.md +++ b/docs/zh-cn/protocol/task-schema.md @@ -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 | list, 2>> - // 当为 array 时 是灰度掩码范围 - // 例如将图片不需要识别的部分涂成黑色(灰度值为 0) - // 然后设置"maskRange"的范围为 [ 1, 255 ], 匹配的时候即忽略涂黑的部分 - // 当为 list, 2>> 时 - // 最内层代表一个颜色,由 method 决定它是 RGB 或 HSV; - // 中间一层是颜色范围的下限和上限; - // 最外层代表允许多个范围,最后取并集作为待识别区域。 + "maskRange": [1, 255], // 可选项,匹配时的灰度掩码范围。 array + // 例如将图片不需要识别的部分涂成黑色(灰度值为 0) + // 然后设置为 [ 1, 255 ], 匹配的时候即忽略涂黑的部分 + + "colorScales": [ // 当 method 为 HSVCount 或 RGBCount 时有效且必选,数色掩码范围。 + [ // list, 2> | array> + [23, 150, 40], // 结构为 [[lower1, upper1], [lower2, upper2], ...] + [25, 230, 150] // 内层为 int 时是灰度, + ], //   为 array 时是三通道颜色,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 时有效 */ diff --git a/resource/tasks.json b/resource/tasks.json index 0a7260cdc4..676aecd3dd 100644 --- a/resource/tasks.json +++ b/resource/tasks.json @@ -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] diff --git a/src/MaaCore/Common/AsstTypes.h b/src/MaaCore/Common/AsstTypes.h index b2e884229f..fa42039547 100644 --- a/src/MaaCore/Common/AsstTypes.h +++ b/src/MaaCore/Common/AsstTypes.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #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>; + using GrayRange = std::pair; + using ColorRange = std::pair, std::array>; + using Range = std::variant; + using Ranges = std::vector; std::vector templ_names; // 匹配模板图片文件名 std::vector templ_thresholds; // 模板匹配阈值 - std::vector mask_range; // 掩码的二值化范围 std::vector methods; // 匹配方法 + Ranges mask_ranges; // 匹配掩码范围,TaskData 仅允许 array,但保留彩色掩码支持 + Ranges color_scales; // 数色掩码范围 + bool color_close = true; // 数色时是否使用闭运算处理 }; using MatchTaskPtr = std::shared_ptr; using MatchTaskConstPtr = std::shared_ptr; diff --git a/src/MaaCore/Config/TaskData.cpp b/src/MaaCore/Config/TaskData.cpp index daf31ef7bb..7abb663d64 100644 --- a/src/MaaCore/Config/TaskData.cpp +++ b/src/MaaCore/Config/TaskData.cpp @@ -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(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`"); 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 + Log.debug("Deprecated GrayRange color_scales in task", name, ", should be `list>`"); + 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 (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, 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>`"); + 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> 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 diff --git a/src/MaaCore/Task/BattleHelper.cpp b/src/MaaCore/Task/BattleHelper.cpp index cb695b083c..c5a62cf07e 100644 --- a/src/MaaCore/Task/BattleHelper.cpp +++ b/src/MaaCore/Task/BattleHelper.cpp @@ -158,9 +158,9 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable, b static const auto cooling_threshold = Task.get("BattleAvatarCoolingData")->templ_thresholds.front(); static const auto cooling_mask_range = - Task.get("BattleAvatarCoolingData")->mask_range; + Task.get("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 = diff --git a/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp b/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp index 82d5803670..1553ee6459 100644 --- a/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp +++ b/src/MaaCore/Vision/Battle/BattlefieldMatcher.cpp @@ -190,23 +190,20 @@ bool BattlefieldMatcher::oper_cooling_analyze(const Rect& roi) const { const auto cooling_task_ptr = Task.get("BattleOperCooling"); - auto img_roi = m_image(make_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(i, j); - if (value) { - ++count; - } - } + if (cooling_task_ptr->color_scales.size() != 1 || + !std::holds_alternative(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(cooling_task_ptr->color_scales.front()); + auto img_roi = m_image(make_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(); } diff --git a/src/MaaCore/Vision/Config/MatcherConfig.cpp b/src/MaaCore/Vision/Config/MatcherConfig.cpp index aa7c11828b..84817463df 100644 --- a/src/MaaCore/Vision/Config/MatcherConfig.cpp +++ b/src/MaaCore/Vision/Config/MatcherConfig.cpp @@ -39,21 +39,24 @@ void asst::MatcherConfig::set_threshold(std::vector 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 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); diff --git a/src/MaaCore/Vision/Config/MatcherConfig.h b/src/MaaCore/Vision/Config/MatcherConfig.h index 55cde50673..050d817e91 100644 --- a/src/MaaCore/Vision/Config/MatcherConfig.h +++ b/src/MaaCore/Vision/Config/MatcherConfig.h @@ -14,10 +14,12 @@ namespace asst { std::vector> templs; std::vector templ_thres; - std::vector mask_range; - std::vector methods; - bool mask_with_src = false; - bool mask_with_close = false; + std::vector 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> templs); void set_threshold(double templ_thres) noexcept; void set_threshold(std::vector 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 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: diff --git a/src/MaaCore/Vision/Infrast/InfrastOperImageAnalyzer.cpp b/src/MaaCore/Vision/Infrast/InfrastOperImageAnalyzer.cpp index 06ff3cc755..219d16cef6 100644 --- a/src/MaaCore/Vision/Infrast/InfrastOperImageAnalyzer.cpp +++ b/src/MaaCore/Vision/Infrast/InfrastOperImageAnalyzer.cpp @@ -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("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(selected_task_ptr->color_scales.front())) { + Log.error(__FUNCTION__, "| color_scales in `InfrastOperSelected` is not a ColorRange"); + return; + } + const auto& color_scale = std::get(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(selected_rect)); + cv::Mat hsv, bin; cv::cvtColor(roi, hsv, cv::COLOR_BGR2HSV); - std::vector 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(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); } } diff --git a/src/MaaCore/Vision/Matcher.cpp b/src/MaaCore/Vision/Matcher.cpp index 67b6a63746..b0592f1914 100644 --- a/src/MaaCore/Vision/Matcher.cpp +++ b/src/MaaCore/Vision/Matcher.cpp @@ -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::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 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 { // 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(range)) { + const auto& gray_range = std::get(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(range)) { + const auto& color_range = std::get(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::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::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 }); } diff --git a/src/MaaCore/Vision/Miscellaneous/StageDropsImageAnalyzer.cpp b/src/MaaCore/Vision/Miscellaneous/StageDropsImageAnalyzer.cpp index 9583fdc102..490e294bfa 100644 --- a/src/MaaCore/Vision/Miscellaneous/StageDropsImageAnalyzer.cpp +++ b/src/MaaCore/Vision/Miscellaneous/StageDropsImageAnalyzer.cpp @@ -400,6 +400,12 @@ bool asst::StageDropsImageAnalyzer::analyze_baseline() m_baseline.clear(); auto task_ptr = Task.get("StageDrops-BaseLine"); + if (task_ptr->color_scales.size() != 1 || + !std::holds_alternative(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(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& templs_list) -> std::string { Matcher analyzer(m_image); - analyzer.set_mask_range(std::vector {}, 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::StageDropsImageAnalyzer::match_quantity_stri bool use_word_model) { auto task_ptr = Task.get("StageDrops-Quantity"); + if (task_ptr->color_scales.size() != 1 || + !std::holds_alternative(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(task_ptr->color_scales.front()); Rect quantity_roi = roi.move(task_ptr->roi); cv::Mat quantity_img = m_image(make_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(task_ptr->templ_thresholds.front()); @@ -693,7 +700,7 @@ std::optional 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::StageDropsImageAnalyzer::match_quantity_stri bool use_word_model) { auto task_ptr = Task.get("StageDrops-Quantity"); + if (task_ptr->color_scales.size() != 1 || + !std::holds_alternative(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(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::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;