AutoX.js - openCV多分辨率找图

news2024/11/19 6:30:58

AutoX.js - openCV多分辨率找图

一、起因

AutoXjs 中有两个找图相关的方法 findImage 和 matchTemplate,之前一直没发现什么问题,但最近在一次测试找图时,明明大图和模板图的轮廓都清晰,却怎么也找不到图,降低阈值参数后找到的结果乱七八糟,我仔细对比图像后发现原因,竟是大图相较于模板抠图时的画面等比缩小了1.2倍,仅仅是0.2倍的整体像素差异,致使搜图失败。

于是我就去翻了AutoX 的具体实现代码,发现这部分代码已是5年前继承于 autojs 的部分代码,具体代码文件在:TemplateMatching.java

其实现原理是借助 opencv的 Imgproc.matchTemplate 方法,以下是部分代码文档:

/**
 * 采用图像金字塔算法快速找图
 *
 * @param img             图片
 * @param template        模板图片
 * @param matchMethod     匹配算法
 * @param weakThreshold   弱阈值。该值用于在每一轮模板匹配中检验是否继续匹配。如果相似度小于该值,则不再继续匹配。
 * @param strictThreshold 强阈值。该值用于检验最终匹配结果,以及在每一轮匹配中如果相似度大于该值则直接返回匹配结果。
 * @param maxLevel        图像金字塔的层数
 * @return
 */
public static List<Match> fastTemplateMatching(Mat img, Mat template, int matchMethod, float weakThreshold, float strictThreshold, int maxLevel, int limit) {
    TimingLogger logger = new TimingLogger(LOG_TAG, "fast_tm");

从中我们可以得知,其采用了“图像金字塔算法快速找图”的处理方式,大体流程是这样的,先将大图和模板图都进行等比缩放,比如宽高都缩小为原来的1/2,如果此时能找到满足阈值的点P1,那么在图像细节更丰富的大图中点P1一定也符合阈值。将图像先等比例缩小,表示图像的矩阵尺寸也就变小,这样执行找图时,整体的计算量减小,找图效率就会大大提高。

这里采用的金字塔算法快速找图,图像尺寸变换就像金字塔,每提高一层图像就变,宽高就缩小一半。方法中的level 参数就是控制起始找图层级的,从 level 层开始从上往下搜图,level=0,即金字塔最底层,也就是原图和模板一比一对比。例如:level=2 就代表从第3层开始往下找图,先在第3层找到 >=threshold 阈值的匹配点就加到最终结果中,剩余 < threshold 且 >=weakThreshold 的疑似匹配点就在第二层中重点关注,以此类推,直到连 >=weakThreshold 的点位也没有就结束找图。

我本以为将level参数调高就能忽略掉大图放大1.2倍的找图问题,但是level过高会导致图像挤成一团,再尝试降低 threshold,虽得到了几个结果,但都是不相干的位置,根本没法用。仔细一想,level参数只是控制找图效率的,这种快速找图的方式根本不适用于大图或模板图分辨率发生变化后的找图场景,而这种情形是很常见的,比如在你手机上开发的脚本放到平板或者别的型号手机上导致找图失败,再比如游戏场景中因双指缩放导致的画面变化。

为了适应大图分辨率可能发生变化的找图场景,我参照 AutoX 中的找图代码,重新用js实现了一版。由于我只看了找图这一部分的相关源码,项目其他的代码不熟悉,就暂时不打算通过提交 PR 为项目做贡献了,有能力的朋友可以完善此部分功能。

二、具体实现

重点部分我已加了文档说明和注释,来不及看也没关系,直接看 main() 方法中的示例,开箱即用。

importClass(org.opencv.imgproc.Imgproc);
importClass(org.opencv.core.Core);
importClass(org.opencv.core.Rect);
importClass(org.opencv.core.Mat);
importClass(org.opencv.core.Point);
importClass(org.opencv.core.Size);
importClass(org.opencv.core.CvType);
importClass(org.opencv.core.Scalar);
importClass(org.opencv.imgcodecs.Imgcodecs);

/**
 * @param {number[]} region 是一个两个或四个元素的数组。
 * (region[0], region[1])表示找色区域的左上角;region[2]*region[3]表示找色区域的宽高。如果只有region只有两个元素,则找色区域为(region[0], region[1])到屏幕右下角。
 * 如果不指定region选项,则找色区域为整张图片。
 * @param {*} img
 * @returns {org.opencv.core.Rect}
 */
function buildRegion(region, img) {
  if (region == undefined) {
    region = [];
  }
  let x = region[0] === undefined ? 0 : region[0];
  let y = region[1] === undefined ? 0 : region[1];
  let width = region[2] === undefined ? img.getWidth() - x : region[2];
  let height = region[3] === undefined ? img.getHeight() - y : region[3];
  if (x < 0 || y < 0 || x + width > img.width || y + height > img.height) {
    throw new Error(
      'out of region: region = [' + [x, y, width, height] + '], image.size = [' + [img.width, img.height] + ']'
    );
  }
  return new Rect(x, y, width, height);
}

/**
 * @param {number} threshold 图片相似度。取值范围为0~1的浮点数。默认值为0.9
 * @param {number[]} region 找图区域
 * @param {number[]} scaleFactors 大图的宽高缩放因子,默认为 [1, 0.9, 1.1, 0.8, 1.2]
 * @param {number} max 找图结果最大数量,默认为5
 * @param {boolean} grayTransform 是否进行灰度化预处理,默认为true。
 * 通常情况下将图像转换为灰度图可以简化匹配过程并提高匹配的准确性,当然,如果你的匹配任务中颜色信息对匹配结果具有重要意义,
 * 可以跳过灰度化步骤,直接在彩色图像上进行模板匹配。
 */
function MatchOptions(threshold, region, scaleFactors, max, grayTransform) {
  this.threshold = threshold;
  this.region = region;
  this.scaleFactors = scaleFactors;
  this.max = max;
  this.grayTransform = grayTransform;
}

const defaultMatchOptions = new MatchOptions(
  0.9,
  undefined,
  [
    [1, 1],
    [0.9, 0.9],
    [1.1, 1.1],
    [0.8, 0.8],
    [1.2, 1.2]
  ],
  5,
  true
);
// 校验参数
MatchOptions.check = function (options) {
  if (options == undefined) {
    return defaultMatchOptions;
  }
  // deep copy
  let newOptions = JSON.parse(JSON.stringify(options));
  if (newOptions.threshold == undefined) {
    newOptions.threshold = defaultMatchOptions.threshold;
  }
  if (newOptions.region && !Array.isArray(newOptions.region)) {
    throw new TypeError('region type is number[]');
  }
  if (newOptions.max == undefined) {
    newOptions.max = defaultMatchOptions.max;
  }
  if (newOptions.scaleFactors == undefined) {
    newOptions.scaleFactors = defaultMatchOptions.scaleFactors;
  } else if (!Array.isArray(newOptions.scaleFactors)) {
    throw new TypeError('scaleFactors');
  }
  for (let index = 0; index < newOptions.scaleFactors.length; index++) {
    let factor = newOptions.scaleFactors[index];
    if (Array.isArray(factor) && factor[0] > 0 && factor[1] > 0) {
      // nothing
    } else if (typeof factor === 'number') {
      newOptions.scaleFactors[index] = [factor, factor];
    } else {
      throw new TypeError('scaleFactors');
    }
  }
  if (newOptions.grayTransform === undefined) {
    newOptions.grayTransform = defaultMatchOptions.grayTransform;
  }

  return newOptions;
};

function Match(point, similarity, scaleX, scaleY) {
  this.point = point;
  this.similarity = similarity;
  this.scaleX = scaleX;
  this.scaleY = scaleY;
}

/**
 * 找图,在图中找出所有匹配的位置
 * @param {Image} img
 * @param {Image} template
 * @param {MatchOptions} options 参数见上方定义
 * @returns {Match[]}
 */
function matchTemplate(img, template, options) {
  if (img == null || template == null) {
    throw new Error('ParamError');
  }
  options = MatchOptions.check(options);
  console.log('参数:', options);

  let largeMat = img.mat;
  let templateMat = template.mat;
  let largeGrayMat;
  let templateGrayMat;
  if (options.region) {
    options.region = buildRegion(options.region, img);
    largeMat = new Mat(largeMat, options.region);
  }
  // 灰度处理
  if (options.grayTransform) {
    largeGrayMat = new Mat();
    Imgproc.cvtColor(largeMat, largeGrayMat, Imgproc.COLOR_BGR2GRAY);
    templateGrayMat = new Mat();
    Imgproc.cvtColor(templateMat, templateGrayMat, Imgproc.COLOR_BGR2GRAY);
  }
  // =================================================
  let finalMatches = [];
  for (let factor of options.scaleFactors) {
    let [fx, fy] = factor;
    let resizedTemplate = new Mat();
    Imgproc.resize(templateGrayMat || templateMat, resizedTemplate, new Size(), fx, fy, Imgproc.INTER_LINEAR);
    // 执行模板匹配,标准化相关性系数匹配法
    let matchMat = new Mat();
    Imgproc.matchTemplate(largeGrayMat || largeMat, resizedTemplate, matchMat, Imgproc.TM_CCOEFF_NORMED);

    let currentMatches = _getAllMatch(matchMat, resizedTemplate, options.threshold, factor, options.region);
    console.log('缩放比:', factor, '可疑目标数:', currentMatches.length);
    for (let match of currentMatches) {
      if (finalMatches.length === 0) {
        finalMatches = currentMatches.slice(0, options.max);
        break;
      }
      if (!isOverlapping(finalMatches, match)) {
        finalMatches.push(match);
      }
      if (finalMatches.length >= options.max) {
        break;
      }
    }
    resizedTemplate.release();
    matchMat.release();
    if (finalMatches.length >= options.max) {
      break;
    }
  }
  largeMat !== img.mat && largeMat.release();
  largeGrayMat && largeGrayMat.release();
  templateGrayMat && templateGrayMat.release();

  return finalMatches;
}

function _getAllMatch(tmResult, templateMat, threshold, factor, rect) {
  let currentMatches = [];
  let mmr = Core.minMaxLoc(tmResult);

  while (mmr.maxVal >= threshold) {
    // 每次取匹配结果中的最大值和位置,从而使结果按相似度指标从高到低排序
    let pos = mmr.maxLoc; // Point
    let value = mmr.maxVal;

    let start = new Point(Math.max(0, pos.x - templateMat.width() / 2), Math.max(0, pos.y - templateMat.height() / 2));
    let end = new Point(
      Math.min(tmResult.width() - 1, pos.x + templateMat.width() / 2),
      Math.min(tmResult.height() - 1, pos.y + templateMat.height() / 2)
    );
    // 屏蔽已匹配到的区域
    Imgproc.rectangle(tmResult, start, end, new Scalar(0), -1);
    mmr = Core.minMaxLoc(tmResult);

    if (rect) {
      pos.x += rect.x;
      pos.y += rect.y;
      start.x += rect.x;
      start.y += rect.y;
      end.x += rect.x;
      end.y += rect.y;
    }
    let match = new Match(pos, value, factor[0], factor[1]);
    // 保存匹配点的大致范围,用于后续去重。设置enumerable为false相当于声明其为私有属性
    Object.defineProperty(match, 'matchAroundRect', { value: new Rect(start, end), writable: true, enumerable: false });
    currentMatches.push(match);
  }

  return currentMatches;
}

/**
 * 判断新检测到的点位是否与之前的某个点位重合。
 * @param {Match[]} matches
 * @param {Match} newMatch
 * @returns {boolean}
 */
function isOverlapping(matches, newMatch) {
  for (let existingMatch of matches) {
    // 也可判断两点间的距离,但是平方、开方运算不如比较范围简单高效
    if (existingMatch.matchAroundRect.contains(newMatch.point)) {
      if (newMatch.similarity > existingMatch.similarity) {
        existingMatch.point = newMatch.point;
        existingMatch.similarity = newMatch.similarity;
        existingMatch.scaleX = newMatch.scaleX;
        existingMatch.scaleY = newMatch.scaleY;
        existingMatch.matchAroundRect = newMatch.matchAroundRect;
      }
      return true;
    }
  }
  return false;
}
/**
 * 根据搜图结果在原图上画框
 * @param {Match[]} matches
 * @param {*} srcMat
 * @param {*} templateMat
 */
function showMatchRectangle(matches, srcMat, templateMat) {
  for (let match of matches) {
    let start = match.point;
    let end = new Point(
      match.point.x + templateMat.width() * match.scaleX,
      match.point.y + templateMat.height() * match.scaleY
    );
    Imgproc.rectangle(srcMat, start, end, new Scalar(0, 0, 255), 3);
  }

  const saveName = '/sdcard/Download/temp.jpg';
  let img2 = images.matToImage(srcMat);
  images.save(img2, saveName);
  app.viewFile(saveName);
  img2.recycle();
}

function main() {
  let largeImage = images.read('/sdcard/Download/large.jpg');
  let template = images.read('/sdcard/Download/template.jpg');

  console.log('大图尺寸:', [largeImage.getWidth(), largeImage.getHeight()]);
  console.log('模板尺寸:', [template.getWidth(), template.getHeight()]);

  let startTs = Date.now();
  let result = matchTemplate(largeImage, template, {
    threshold: 0.85,
    region: [100, 100],
    grayTransform: false,
    scaleFactors: [1, 0.9, 1.1, 0.8, 1.2],
    max: 6
  });
  console.log('找图耗时:', (Date.now() - startTs) / 1000);
  console.log(result);
  // 将结果画框展示
  showMatchRectangle(result, largeImage.mat, template.mat);
  template.recycle();
  largeImage.recycle();
}

// 初始化openCV
runtime.getImages().initOpenCvIfNeeded();
main();

备注说明

  • 参数 threholdregionmax 跟AutoX中的一样。

  • grayTransform:是否将图像进行灰度预处理,开启可大幅提高找图效率,默认为true。

    对于模板匹配任务,通常关注的是图像的纹理和亮度变化,而不是颜色差异。因此,将图像转换为灰度图可以降低计算复杂度,减少模板匹配过程中的噪声干扰,并提高匹配的稳定性和准确性。如果你的模板小图中纹理不明显,或是一团颜色相近的色块,就得关闭该功能。

  • scaleFactors:是对小图模板的缩放因子,数组类型,默认为[1, 0.9, 1.1, 0.8, 1.2]。每一项可以是数字,表示宽高等比例缩放,也可以是长度为2的数组,表示宽、高对应的缩放比,例如:[0.9, 1, [1.1, 1.2]]

    这里重点强调一点,我没有在 openCV 里找到可以直接用于忽略图像比例差异的搜图方法,只能手动指定可能的缩放范围,依次对小图模板进行缩放后再搜图。理论上,只要你设置的(不重复)缩放因子足够多,就一定能找到目标,除非图中没有😁。

  • max参数的妙用:搜图过程中,默认在找到前 max 个符合阈值的匹配点就退出,但是可能存在一种情况,例如先在缩放比为 1.1 的情况下找到了相似度为 0.8 的点 P1,此时若还没有找够 max 个匹配点,在后续比例为 1.2 的情况下,检测到点 P1 处的相似度提高到0.9,就将原来 P1 点的信息更新为更准确的信息。理解了这一点,如果你将 max 设置的非常大,我的搜图算法就会按照 scaleFactors 中设置的全部缩放因子都检测一遍,不会提前结束,那么最终结果中所有的 Match 对象中保存的都是最佳匹配点的信息,你可以凭借最终结果中的 scaleX、scaleY 信息,动态调整 scaleFactors 参数,使其优先匹配最佳缩放比,提高后续的找图效率。

三、测试结果展示

以下是一个测试数据,模板图是一坨近乎白色的光团,对比了 grayTransform 参数开启和关闭的情况,虽然效率相差好几倍,但是此时还是关闭灰度预处理的结果更准确。

希望大家在使用时,清楚每个参数改变所产生的效果。

  • 模板小图
    在这里插入图片描述

  • 5个缩放因子下的找图结果
    在这里插入图片描述

  • 开启灰度处理
    在这里插入图片描述

  • 未开启灰度处理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1155303.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】顺序表实例探究

&#x1f497;个人主页&#x1f497; ⭐个人专栏——数据结构学习⭐ &#x1f4ab;点击关注&#x1f929;一起学习C语言&#x1f4af;&#x1f4ab; 目录 导读&#xff1a;1. 顺序表的基本内容1.1 概念及结构1.2 时间和空间复杂度1.3 基本操作1.4 顺序表的优缺点 2. 静态顺序表…

CVPR 2023 | 主干网络FasterNet 核心解读 代码分析

本文分享来自CVPR 2023的论文&#xff0c;提出了一种快速的主干网络&#xff0c;名为FasterNet。 论文提出了一种新的卷积算子&#xff0c;partial convolution&#xff0c;部分卷积(PConv)&#xff0c;通过减少冗余计算和内存访问来更有效地提取空间特征。 创新在于部分卷积…

xhadmin多应用SaaS框架怎么更新?

xhadmin是什么&#xff1f; xhadmin 是一套基于最新技术的研发的多应用 Saas 框架&#xff0c;支持在线升级和安装模块及模板&#xff0c;拥有良好的开发框架、成熟稳定的技术解决方案、提供丰富的扩展功能。为开发者赋能&#xff0c;助力企业发展、国家富强&#xff0c;致力于…

【设计模式】第13节:结构型模式之“享元模式”

一、简介 所谓“享元”&#xff0c;顾名思义就是被共享的单元。享元模式的意图是复用对象&#xff0c;节省内存&#xff0c;前提是享元对象是不可变对象。 实现&#xff1a;通过工厂模式&#xff0c;在工厂类中&#xff0c;通过一个Map或者List来缓存已经创建好的享元对象&am…

这样的软件测试报告模板你绝对没见过!!!

测试报告如此重要&#xff0c;那么我们应该如何撰写呢&#xff1f;为了让大家彻底掌握测试模板的撰写&#xff0c;所以本文结构如下&#xff1a; 1、测试报告写给谁看&#xff1f; 2、测试报告的基本骨架&#xff08;通过|不通过&#xff09;&#xff1f; 3、测试报告如何才能达…

超级搜索技术,普通人变强的唯一外挂

搜索效率&#xff1a;Google >微信公众号 >短视频 >百度 1、信息咨询搜索 在Google搜索栏前面加上 “” 限定关键词 intitle 限定标题 allintitle 限定标题多个关键词 intext 限定内容关键词 inurl 限定网址关键词 site 限定网址来源 imagesize 限定图片尺寸 filet…

[LeetCode]-27. 移除元素-26.删除有序数组中的重复项-88.合并两个有序数组

目录 27.移除元素 题目 思路 代码 26. 删除有序数组中的重复项 题目 思路 代码 88.合并两个有序数组 题目 思路 代码 总结 27.移除元素 27. 移除元素 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/remove-element/description/ 题目 给你一…

【快报】正在把教学视频搬运到B站和油管

hello 大家好&#xff0c;我是老戴。 熟悉我的同学知道&#xff0c;我从14年开始录制GIS相关的教学视频&#xff0c;之前是放到优酷上给大家下载&#xff0c;后期发现很多人把视频弄下来淘宝上卖&#xff0c;然后我就把视频整体放到了我自己的网站上。 随着视频录制的数量越来…

C++归并排序算法的应用:计算右侧小于当前元素的个数

题目 给你一个整数数组 nums &#xff0c;按要求返回一个新数组 counts 。数组 counts 有该性质&#xff1a; counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。 示例 1&#xff1a; 输入&#xff1a;nums [5,2,6,1] 输出&#xff1a;[2,1,1,0] 解释&#xff1a; 5 …

2024年湖北武汉建筑企业三类人员安全员ABC怎么报考

2024年湖北武汉建筑企业三类人员安全员ABC怎么报考 武汉建筑企业报考三类人员&#xff0c;建筑单位归属地在武汉&#xff0c;且有建筑相关的一些资Z&#xff0c;才可以申报一定数量的三类人员、安全员ABC、建筑安全员ABC、专职安全员C证、建设厅安全员ABC证。 建筑企业-报考建…

在线开发平台是什么?有哪些优势?

目录 一、什么是在线开发平台&#xff1f; 二、企业为什么选择在线开发平台&#xff1f; &#xff08;1&#xff09;风险低&#xff0c;回报高 &#xff08;2&#xff09;可视化操作更形象 &#xff08;3&#xff09;易维护 三、在线开发平台功能展示 技术介绍 随着互联网和信息…

Jetpack:024-Jetpack中的滚动事件

文章目录 1. 概念介绍2. 使用方法2.1 高级事件2.2 低级事件 3. 示例代码4. 内容总结 我们在上一章回中介绍了Jetpack中事件相关的内容&#xff0c;本章回中主要 介绍事件中的滚动事件。闲话休提&#xff0c;让我们一起Talk Android Jetpack吧&#xff01; 1. 概念介绍 我们在…

三相马达的电机故障维护

目录 电机故障维护​编辑 更换电机操作 三相电路 热继电器 今天继续小编的工作经验的分享&#xff0c;今天就说说遇到的问题吧&#xff0c;今天组立熔接机出现故障&#xff0c;后面部分出现了“咕噜噜”的杂声&#xff0c;走到后面一听是电机发出的声音。没有办法了就开始拆…

Py之transformers_stream_generator:transformers_stream_generator的简介、安装、使用方法之详细攻略

Py之transformers_stream_generator&#xff1a;transformers_stream_generator的简介、安装、使用方法之详细攻略 目录 transformers_stream_generator的简介 1、Web Demo T1、original T2、stream transformers_stream_generator的安装 transformers_stream_generator的…

【Linux虚拟机】 JDK、Tomcat、MySQL安装配置讲解

目录 一、上传安装包到服务器 二、JDK与Tomcat安装 2.1 解压安装包 2.2 配置JDK环境变量 2.3 配置Tomcat环境 三、MySQL安装配置 3.1 删除默认数据库 3.2 安装mysql安装包 3.3 mysql初始化操作 四、后端接口部署 4.1 导入项目.war 4.2 新建数据库 4.3 运行服务器项目…

白票某度自媒体混剪剪辑视频素材/爬虫软件说明文档

大家好&#xff0c;我是淘小白~ 软件&#xff1a;某度自媒体混剪素材爬虫软件 语言&#xff1a;Python 说明文档&#xff1a; 1、自定义关键词采集 2、采集百度aigc视频素材&#xff0c;经过测试&#xff0c;使用剪映的文字成片某度视频素材&#xff0c;可过头条的原创检测…

SPSS单样本t检验

前言&#xff1a; 本专栏参考教材为《SPSS22.0从入门到精通》&#xff0c;由于软件版本原因&#xff0c;部分内容有所改变&#xff0c;为适应软件版本的变化&#xff0c;特此创作此专栏便于大家学习。本专栏使用软件为&#xff1a;SPSS25.0 本专栏所有的数据文件请点击此链接下…

OSFP基础实验

目录 题目&#xff1a;拓扑如下 实验步骤&#xff1a; 第一步&#xff1a;设计思路 第二步&#xff1a;搭建拓扑 第三步&#xff1a;配置命令 1&#xff09;IP地址配置 2&#xff09;OSPF配置 3&#xff09;R3部分接口做静默接口 4&#xff09;缺省路由 5&#xff09…

数据结构之“初窥门径”

目录 前言&#xff1a; 一&#xff0c;数据结构起源 二&#xff0c;基本概念和术语 2.1数据 2.2数据元素 2.3数据项 2.4数据对象 2.5数据结构 三&#xff0c;逻辑结构与物理结构 3.1逻辑结构 3.1.1集合结构 3.1.2线性结构 3.1.3树形结构 3.1.4图形结构 3.2物理结…

unity中meta文件GUID异常问题

错误信息&#xff1a; The .meta file Assets/Scripts/Editor/ConvertConfigToBinary/TxtConverter.cs.meta does not have a valid GUID and its corresponding Asset file will be ignored. If this file is not malformed, please add a GUID, or delete the .meta file and…