mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-16 17:57:01 +08:00
feat: 支持多模板匹配,任一达到阈值即视为命中
MultiMatcher 暂不支持,仅使用第一个模板
This commit is contained in:
@@ -397,9 +397,9 @@ namespace asst
|
||||
MatchTaskInfo(MatchTaskInfo&&) noexcept = default;
|
||||
MatchTaskInfo& operator=(const MatchTaskInfo&) = default;
|
||||
MatchTaskInfo& operator=(MatchTaskInfo&&) noexcept = default;
|
||||
std::string templ_name; // 匹配模板图片文件名
|
||||
double templ_threshold = 0; // 模板匹配阈值
|
||||
std::pair<int, int> mask_range; // 掩码的二值化范围
|
||||
std::vector<std::string> templ_names; // 匹配模板图片文件名
|
||||
std::vector<double> templ_thresholds; // 模板匹配阈值
|
||||
std::pair<int, int> mask_range; // 掩码的二值化范围
|
||||
};
|
||||
|
||||
// hash 计算任务的信息
|
||||
|
||||
@@ -724,17 +724,46 @@ asst::TaskData::taskptr_t asst::TaskData::generate_match_task_info(std::string_v
|
||||
default_ptr = default_match_task_info_ptr;
|
||||
}
|
||||
auto match_task_info_ptr = std::make_shared<MatchTaskInfo>();
|
||||
#ifdef ASST_DEBUG
|
||||
if (task_json.get("template", "") == std::string(name) + ".png") {
|
||||
Log.warn("template name of task", name, "could be omitted.");
|
||||
auto templ_opt = task_json.find("template");
|
||||
if (!templ_opt) {
|
||||
match_task_info_ptr->templ_names = { std::string(name) + ".png" };
|
||||
}
|
||||
#endif
|
||||
// template 留空时不从模板任务继承
|
||||
match_task_info_ptr->templ_name = task_json.get("template", std::string(name) + ".png");
|
||||
m_templ_required.emplace(match_task_info_ptr->templ_name);
|
||||
else if (templ_opt->is_string()) {
|
||||
match_task_info_ptr->templ_names = { templ_opt->as_string() + ".png" };
|
||||
}
|
||||
else if (templ_opt->is_array()) {
|
||||
match_task_info_ptr->templ_names = to_string_list(templ_opt->as_array());
|
||||
}
|
||||
else {
|
||||
Log.error("Invalid template type in task", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_templ_required.insert(match_task_info_ptr->templ_names.begin(), match_task_info_ptr->templ_names.end());
|
||||
|
||||
// 其余若留空则继承模板任务
|
||||
match_task_info_ptr->templ_threshold = task_json.get("templThreshold", default_ptr->templ_threshold);
|
||||
|
||||
auto threshold_opt = task_json.find("threshold");
|
||||
if (!threshold_opt) {
|
||||
match_task_info_ptr->templ_thresholds = default_ptr->templ_thresholds;
|
||||
}
|
||||
else if (threshold_opt->is_number()) {
|
||||
match_task_info_ptr->templ_thresholds = { threshold_opt->as_double() };
|
||||
}
|
||||
else if (threshold_opt->is_array()) {
|
||||
ranges::copy(threshold_opt->as_array() | views::transform(&ranges::range_value_t<json::array>::as_double),
|
||||
std::back_inserter(match_task_info_ptr->templ_thresholds));
|
||||
}
|
||||
else {
|
||||
Log.error("Invalid threshold type in task", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (match_task_info_ptr->templ_names.size() != match_task_info_ptr->templ_thresholds.size()) {
|
||||
Log.error("Template count and threshold count not match in task", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (auto opt = task_json.find<json::array>("maskRange")) {
|
||||
auto& mask_range = *opt;
|
||||
match_task_info_ptr->mask_range =
|
||||
@@ -892,8 +921,8 @@ bool asst::TaskData::append_base_task_info(taskptr_t task_info_ptr, std::string_
|
||||
std::shared_ptr<asst::MatchTaskInfo> asst::TaskData::_default_match_task_info()
|
||||
{
|
||||
auto match_task_info_ptr = std::make_shared<MatchTaskInfo>();
|
||||
match_task_info_ptr->templ_name = "__INVALID__";
|
||||
match_task_info_ptr->templ_threshold = TemplThresholdDefault;
|
||||
match_task_info_ptr->templ_names = { "__INVALID__" };
|
||||
match_task_info_ptr->templ_thresholds = { TemplThresholdDefault };
|
||||
|
||||
return match_task_info_ptr;
|
||||
}
|
||||
|
||||
@@ -132,14 +132,16 @@ bool asst::BattleHelper::update_deployment(bool init, const cv::Mat& reusable)
|
||||
BestMatcher avatar_analyzer(oper.avatar);
|
||||
if (oper.cooling) {
|
||||
Log.trace("start matching cooling", oper.index);
|
||||
static const double cooling_threshold = Task.get<MatchTaskInfo>("BattleAvatarCoolingData")->templ_threshold;
|
||||
static const double cooling_threshold =
|
||||
Task.get<MatchTaskInfo>("BattleAvatarCoolingData")->templ_thresholds.front();
|
||||
static const auto cooling_mask_range = Task.get<MatchTaskInfo>("BattleAvatarCoolingData")->mask_range;
|
||||
avatar_analyzer.set_threshold(cooling_threshold);
|
||||
avatar_analyzer.set_mask_range(cooling_mask_range.first, cooling_mask_range.second, true, true);
|
||||
}
|
||||
else {
|
||||
static const double threshold = Task.get<MatchTaskInfo>("BattleAvatarData")->templ_threshold;
|
||||
static const double drone_threshold = Task.get<MatchTaskInfo>("BattleDroneAvatarData")->templ_threshold;
|
||||
static const double threshold = Task.get<MatchTaskInfo>("BattleAvatarData")->templ_thresholds.front();
|
||||
static const double drone_threshold =
|
||||
Task.get<MatchTaskInfo>("BattleDroneAvatarData")->templ_thresholds.front();
|
||||
avatar_analyzer.set_threshold(oper.role == Role::Drone ? drone_threshold : threshold);
|
||||
}
|
||||
|
||||
|
||||
@@ -781,7 +781,7 @@ void asst::CombatRecordRecognitionTask::ananlyze_deployment_names(ClipInfo& clip
|
||||
continue;
|
||||
}
|
||||
BestMatcher avatar_analyzer(oper.avatar);
|
||||
static const double threshold = Task.get<MatchTaskInfo>("BattleAvatarDataForVideo")->templ_threshold;
|
||||
static const double threshold = Task.get<MatchTaskInfo>("BattleAvatarDataForVideo")->templ_thresholds.front();
|
||||
avatar_analyzer.set_threshold(threshold);
|
||||
// static const double drone_threshold = Task.get<MatchTaskInfo>("BattleDroneAvatarData")->templ_threshold;
|
||||
// avatar_analyzer.set_threshold(oper.role == battle::Role::Drone ? drone_threshold : threshold);
|
||||
|
||||
@@ -21,12 +21,22 @@ void MatcherConfig::set_task_info(const std::string& task_name)
|
||||
|
||||
void MatcherConfig::set_templ(std::variant<std::string, cv::Mat> templ)
|
||||
{
|
||||
m_params.templ = std::move(templ);
|
||||
m_params.templs = { std::move(templ) };
|
||||
}
|
||||
|
||||
// void asst::MatcherConfig::set_templ(std::vector<std::variant<std::string, cv::Mat>> templs)
|
||||
//{
|
||||
// m_params.templs = std::move(templs);
|
||||
// }
|
||||
|
||||
void MatcherConfig::set_threshold(double templ_thres) noexcept
|
||||
{
|
||||
m_params.templ_thres = templ_thres;
|
||||
m_params.templ_thres = { templ_thres };
|
||||
}
|
||||
|
||||
void asst::MatcherConfig::set_threshold(std::vector<double> templ_thres) noexcept
|
||||
{
|
||||
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)
|
||||
@@ -38,8 +48,8 @@ void MatcherConfig::set_mask_range(int lower, int upper, bool mask_with_src, boo
|
||||
|
||||
void MatcherConfig::_set_task_info(MatchTaskInfo task_info)
|
||||
{
|
||||
m_params.templ = std::move(task_info.templ_name);
|
||||
m_params.templ_thres = task_info.templ_threshold;
|
||||
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);
|
||||
|
||||
_set_roi(task_info.roi);
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace asst
|
||||
public:
|
||||
struct Params
|
||||
{
|
||||
std::variant<std::string, cv::Mat> templ;
|
||||
double templ_thres = 0.0;
|
||||
std::vector<std::variant<std::string, cv::Mat>> templs;
|
||||
std::vector<double> templ_thres;
|
||||
std::pair<int, int> mask_range;
|
||||
bool mask_with_src = false;
|
||||
bool mask_with_close = false;
|
||||
@@ -29,7 +29,9 @@ namespace asst
|
||||
void set_task_info(const std::string& task_name);
|
||||
|
||||
void set_templ(std::variant<std::string, cv::Mat> templ);
|
||||
// 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);
|
||||
|
||||
protected:
|
||||
|
||||
@@ -132,7 +132,7 @@ void asst::InfrastOperImageAnalyzer::mood_analyze()
|
||||
LogTraceFunction;
|
||||
|
||||
const auto prg_task_ptr = Task.get<MatchTaskInfo>("InfrastOperMoodProgressBar");
|
||||
uint8_t prg_lower_limit = static_cast<uint8_t>(prg_task_ptr->templ_threshold);
|
||||
uint8_t prg_lower_limit = static_cast<uint8_t>(prg_task_ptr->templ_thresholds.front());
|
||||
int prg_diff_thres = prg_task_ptr->special_params.front();
|
||||
Rect rect_move = prg_task_ptr->rect_move;
|
||||
|
||||
@@ -229,7 +229,7 @@ void asst::InfrastOperImageAnalyzer::skill_analyze()
|
||||
Matcher skill_analyzer(m_image);
|
||||
|
||||
skill_analyzer.set_mask_range(task_ptr->mask_range.first, task_ptr->mask_range.second);
|
||||
skill_analyzer.set_threshold(task_ptr->templ_threshold);
|
||||
skill_analyzer.set_threshold(task_ptr->templ_thresholds.front());
|
||||
|
||||
for (auto&& oper : m_result) {
|
||||
Rect roi = task_ptr->rect_move;
|
||||
@@ -371,7 +371,7 @@ void asst::InfrastOperImageAnalyzer::selected_analyze()
|
||||
}
|
||||
}
|
||||
Log.trace("selected_analyze |", count);
|
||||
oper.selected = count >= selected_task_ptr->templ_threshold;
|
||||
oper.selected = count >= selected_task_ptr->templ_thresholds.front();
|
||||
oper.rect = selected_rect; // 先凑合用(
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,79 +11,89 @@ using namespace asst;
|
||||
|
||||
Matcher::ResultOpt Matcher::analyze() const
|
||||
{
|
||||
const auto& [matched, templ, templ_name] = preproc_and_match(make_roi(m_image, m_roi), m_params);
|
||||
const auto match_results = preproc_and_match(make_roi(m_image, m_roi), m_params);
|
||||
|
||||
if (matched.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
double min_val = 0.0, max_val = 0.0;
|
||||
cv::Point min_loc, max_loc;
|
||||
cv::minMaxLoc(matched, &min_val, &max_val, &min_loc, &max_loc);
|
||||
|
||||
Rect rect(max_loc.x + m_roi.x, max_loc.y + m_roi.y, templ.cols, templ.rows);
|
||||
if (std::isnan(max_val) || std::isinf(max_val)) {
|
||||
max_val = 0;
|
||||
}
|
||||
if (m_log_tracing && max_val > m_params.templ_thres * 0.7) { // 得分太低的肯定不对,没必要打印
|
||||
Log.trace("match_templ |", templ_name, "score:", max_val, "rect:", rect, "roi:", m_roi);
|
||||
}
|
||||
|
||||
if (max_val < m_params.templ_thres) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// FIXME: 老接口太难重构了,先弄个这玩意兼容下,后续慢慢全删掉
|
||||
m_result.rect = rect;
|
||||
m_result.score = max_val;
|
||||
return m_result;
|
||||
}
|
||||
|
||||
Matcher::RawResult Matcher::preproc_and_match(const cv::Mat& image, const MatcherConfig::Params& params)
|
||||
{
|
||||
cv::Mat templ;
|
||||
std::string templ_name;
|
||||
|
||||
if (std::holds_alternative<std::string>(params.templ)) {
|
||||
templ_name = std::get<std::string>(params.templ);
|
||||
templ = TemplResource::get_instance().get_templ(templ_name);
|
||||
}
|
||||
else if (std::holds_alternative<cv::Mat>(params.templ)) {
|
||||
templ = std::get<cv::Mat>(params.templ);
|
||||
}
|
||||
else {
|
||||
Log.error("templ is none");
|
||||
}
|
||||
|
||||
if (templ.empty()) {
|
||||
Log.error("templ is empty!", templ_name);
|
||||
#ifdef ASST_DEBUG
|
||||
throw std::runtime_error("templ is empty: " + templ_name);
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
if (templ.cols > image.cols || templ.rows > image.rows) {
|
||||
Log.error("templ size is too large", templ_name, "image size:", image.cols, image.rows,
|
||||
"templ size:", templ.cols, templ.rows);
|
||||
return {};
|
||||
}
|
||||
|
||||
cv::Mat matched;
|
||||
if (params.mask_range.first == 0 && params.mask_range.second == 0) {
|
||||
cv::matchTemplate(image, templ, matched, cv::TM_CCOEFF_NORMED);
|
||||
}
|
||||
else {
|
||||
cv::Mat mask;
|
||||
cv::cvtColor(params.mask_with_src ? image : templ, mask, cv::COLOR_BGR2GRAY);
|
||||
cv::inRange(mask, params.mask_range.first, params.mask_range.second, mask);
|
||||
if (params.mask_with_close) {
|
||||
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
|
||||
cv::morphologyEx(mask, mask, cv::MORPH_CLOSE, kernel);
|
||||
for (size_t i = 0; i < match_results.size(); ++i) {
|
||||
const auto& [matched, templ, templ_name] = match_results[i];
|
||||
if (matched.empty()) {
|
||||
continue;
|
||||
}
|
||||
cv::matchTemplate(image, templ, matched, cv::TM_CCOEFF_NORMED, mask);
|
||||
|
||||
double min_val = 0.0, max_val = 0.0;
|
||||
cv::Point min_loc, max_loc;
|
||||
cv::minMaxLoc(matched, &min_val, &max_val, &min_loc, &max_loc);
|
||||
|
||||
Rect rect(max_loc.x + m_roi.x, max_loc.y + m_roi.y, templ.cols, templ.rows);
|
||||
if (std::isnan(max_val) || std::isinf(max_val)) {
|
||||
max_val = 0;
|
||||
}
|
||||
if (m_log_tracing && max_val > 0.5) { // 得分太低的肯定不对,没必要打印
|
||||
Log.trace("match_templ |", templ_name, "score:", max_val, "rect:", rect, "roi:", m_roi);
|
||||
}
|
||||
|
||||
double threshold = m_params.templ_thres[i];
|
||||
if (max_val < threshold) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// FIXME: 老接口太难重构了,先弄个这玩意兼容下,后续慢慢全删掉
|
||||
m_result.rect = rect;
|
||||
m_result.score = max_val;
|
||||
return m_result;
|
||||
}
|
||||
|
||||
return RawResult { .matched = matched, .templ = templ, .templ_name = templ_name };
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
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) {
|
||||
cv::Mat templ;
|
||||
std::string templ_name;
|
||||
|
||||
if (std::holds_alternative<std::string>(ptempl)) {
|
||||
templ_name = std::get<std::string>(ptempl);
|
||||
templ = TemplResource::get_instance().get_templ(templ_name);
|
||||
}
|
||||
else if (std::holds_alternative<cv::Mat>(ptempl)) {
|
||||
templ = std::get<cv::Mat>(ptempl);
|
||||
}
|
||||
else {
|
||||
Log.error("templ is none");
|
||||
}
|
||||
|
||||
if (templ.empty()) {
|
||||
Log.error("templ is empty!", templ_name);
|
||||
#ifdef ASST_DEBUG
|
||||
throw std::runtime_error("templ is empty: " + templ_name);
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
if (templ.cols > image.cols || templ.rows > image.rows) {
|
||||
Log.error("templ size is too large", templ_name, "image size:", image.cols, image.rows,
|
||||
"templ size:", templ.cols, templ.rows);
|
||||
return {};
|
||||
}
|
||||
|
||||
cv::Mat matched;
|
||||
if (params.mask_range.first == 0 && params.mask_range.second == 0) {
|
||||
cv::matchTemplate(image, templ, matched, cv::TM_CCOEFF_NORMED);
|
||||
}
|
||||
else {
|
||||
cv::Mat mask;
|
||||
cv::cvtColor(params.mask_with_src ? image : templ, mask, cv::COLOR_BGR2GRAY);
|
||||
cv::inRange(mask, params.mask_range.first, params.mask_range.second, mask);
|
||||
if (params.mask_with_close) {
|
||||
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);
|
||||
}
|
||||
|
||||
results.emplace_back(RawResult { .matched = matched, .templ = templ, .templ_name = templ_name });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace asst
|
||||
cv::Mat templ;
|
||||
std::string templ_name;
|
||||
};
|
||||
static RawResult preproc_and_match(const cv::Mat& image, const MatcherConfig::Params& params);
|
||||
static std::vector<RawResult> preproc_and_match(const cv::Mat& image, const MatcherConfig::Params& params);
|
||||
|
||||
protected:
|
||||
virtual void _set_roi(const Rect& roi) override { set_roi(roi); }
|
||||
|
||||
@@ -53,8 +53,8 @@ Matcher::ResultOpt PipelineAnalyzer::match(const std::shared_ptr<TaskInfo>& task
|
||||
Matcher match_analyzer(m_image, m_roi);
|
||||
|
||||
const auto match_task_ptr = std::dynamic_pointer_cast<MatchTaskInfo>(task_ptr);
|
||||
if (match_task_ptr->templ_threshold > 1.0) {
|
||||
Log.info(match_task_ptr->name, "'s threshold is", match_task_ptr->templ_threshold, ", just skip");
|
||||
if (ranges::all_of(match_task_ptr->templ_thresholds, [](double t) { return t > 1.0; })) {
|
||||
Log.info(match_task_ptr->name, "'s threshold is", match_task_ptr->templ_thresholds, ", just skip");
|
||||
return std::nullopt;
|
||||
}
|
||||
match_analyzer.set_task_info(match_task_ptr);
|
||||
|
||||
@@ -337,7 +337,7 @@ bool asst::StageDropsImageAnalyzer::analyze_baseline()
|
||||
int y_offset = task_ptr->roi.y + bounding_rect.y;
|
||||
|
||||
const int min_width = task_ptr->special_params.front();
|
||||
const int max_spacing = static_cast<int>(task_ptr->templ_threshold);
|
||||
const int max_spacing = static_cast<int>(task_ptr->templ_thresholds.front());
|
||||
|
||||
int i_start = 0, i_end = bounding.cols - 1;
|
||||
bool in = true; // 是否正处在白线中
|
||||
@@ -542,7 +542,7 @@ std::optional<asst::TextRect> asst::StageDropsImageAnalyzer::match_quantity_stri
|
||||
cv::inRange(gray, task_ptr->mask_range.first, task_ptr->mask_range.second, bin);
|
||||
|
||||
// split
|
||||
const int max_spacing = static_cast<int>(task_ptr->templ_threshold);
|
||||
const int max_spacing = static_cast<int>(task_ptr->templ_thresholds.front());
|
||||
std::vector<cv::Range> contours;
|
||||
int i_right = bin.cols - 1, i_left = 0;
|
||||
bool in = false;
|
||||
|
||||
@@ -14,19 +14,21 @@ using namespace asst;
|
||||
|
||||
MultiMatcher::ResultsVecOpt MultiMatcher::analyze() const
|
||||
{
|
||||
const auto& [matched, templ, templ_name] = Matcher::preproc_and_match(make_roi(m_image, m_roi), m_params);
|
||||
auto match_results = Matcher::preproc_and_match(make_roi(m_image, m_roi), m_params);
|
||||
const auto& [matched, templ, templ_name] = match_results.front();
|
||||
|
||||
if (matched.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<Result> results;
|
||||
double threshold = m_params.templ_thres.front();
|
||||
|
||||
int min_distance = (std::min)(templ.cols, templ.rows) / 2;
|
||||
for (int i = 0; i != matched.rows; ++i) {
|
||||
for (int j = 0; j != matched.cols; ++j) {
|
||||
auto value = matched.at<float>(i, j);
|
||||
if (value < m_params.templ_thres || std::isnan(value) || std::isinf(value)) {
|
||||
if (value < threshold || std::isnan(value) || std::isinf(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ int asst::RoguelikeRecruitSupportAnalyzer::match_elite(const Rect& roi, const in
|
||||
auto task_ptr = Task.get(task_name);
|
||||
analyzer.set_task_info(task_ptr);
|
||||
analyzer.set_roi(roi);
|
||||
analyzer.set_threshold(Task.get<MatchTaskInfo>(task_name)->templ_threshold);
|
||||
analyzer.set_threshold(Task.get<MatchTaskInfo>(task_name)->templ_thresholds.front());
|
||||
|
||||
if (!analyzer.analyze()) {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user