AutoX.js - openCV多分辨率找圖
一、起因
AutoXjs 中有兩個找圖相關(guān)的方法 findImage 和 matchTemplate,之前一直沒發(fā)現(xiàn)什么問題,但最近在一次測試找圖時,明明大圖和模板圖的輪廓都清晰,卻怎么也找不到圖,降低閾值參數(shù)后找到的結(jié)果亂七八糟,我仔細對比圖像后發(fā)現(xiàn)原因,竟是大圖相較于模板摳圖時的畫面等比縮小了1.2倍,僅僅是0.2倍的整體像素差異,致使搜圖失敗。
于是我就去翻了AutoX 的具體實現(xiàn)代碼,發(fā)現(xiàn)這部分代碼已是5年前繼承于 autojs 的部分代碼,具體代碼文件在:TemplateMatching.java
其實現(xiàn)原理是借助 opencv的 Imgproc.matchTemplate 方法,以下是部分代碼文檔:
/**
* 采用圖像金字塔算法快速找圖
*
* @param img 圖片
* @param template 模板圖片
* @param matchMethod 匹配算法
* @param weakThreshold 弱閾值。該值用于在每一輪模板匹配中檢驗是否繼續(xù)匹配。如果相似度小于該值,則不再繼續(xù)匹配。
* @param strictThreshold 強閾值。該值用于檢驗最終匹配結(jié)果,以及在每一輪匹配中如果相似度大于該值則直接返回匹配結(jié)果。
* @param maxLevel 圖像金字塔的層數(shù)
* @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,那么在圖像細節(jié)更豐富的大圖中點P1一定也符合閾值。將圖像先等比例縮小,表示圖像的矩陣尺寸也就變小,這樣執(zhí)行找圖時,整體的計算量減小,找圖效率就會大大提高。
這里采用的金字塔算法快速找圖,圖像尺寸變換就像金字塔,每提高一層圖像就變,寬高就縮小一半。方法中的level
參數(shù)就是控制起始找圖層級的,從 level
層開始從上往下搜圖,level=0
,即金字塔最底層,也就是原圖和模板一比一對比。例如:level=2
就代表從第3層開始往下找圖,先在第3層找到 >=threshold 閾值的匹配點就加到最終結(jié)果中,剩余 < threshold 且 >=weakThreshold 的疑似匹配點就在第二層中重點關(guān)注,以此類推,直到連 >=weakThreshold 的點位也沒有就結(jié)束找圖。
我本以為將level參數(shù)調(diào)高就能忽略掉大圖放大1.2倍的找圖問題,但是level過高會導致圖像擠成一團,再嘗試降低 threshold,雖得到了幾個結(jié)果,但都是不相干的位置,根本沒法用。仔細一想,level參數(shù)只是控制找圖效率的,這種快速找圖的方式根本不適用于大圖或模板圖分辨率發(fā)生變化后的找圖場景,而這種情形是很常見的,比如在你手機上開發(fā)的腳本放到平板或者別的型號手機上導致找圖失敗,再比如游戲場景中因雙指縮放導致的畫面變化。
為了適應(yīng)大圖分辨率可能發(fā)生變化的找圖場景,我參照 AutoX 中的找圖代碼,重新用js實現(xiàn)了一版。由于我只看了找圖這一部分的相關(guān)源碼,項目其他的代碼不熟悉,就暫時不打算通過提交 PR 為項目做貢獻了,有能力的朋友可以完善此部分功能。
二、具體實現(xiàn)
重點部分我已加了文檔說明和注釋,來不及看也沒關(guān)系,直接看 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 是一個兩個或四個元素的數(shù)組。
* (region[0], region[1])表示找色區(qū)域的左上角;region[2]*region[3]表示找色區(qū)域的寬高。如果只有region只有兩個元素,則找色區(qū)域為(region[0], region[1])到屏幕右下角。
* 如果不指定region選項,則找色區(qū)域為整張圖片。
* @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的浮點數(shù)。默認值為0.9
* @param {number[]} region 找圖區(qū)域
* @param {number[]} scaleFactors 大圖的寬高縮放因子,默認為 [1, 0.9, 1.1, 0.8, 1.2]
* @param {number} max 找圖結(jié)果最大數(shù)量,默認為5
* @param {boolean} grayTransform 是否進行灰度化預處理,默認為true。
* 通常情況下將圖像轉(zhuǎn)換為灰度圖可以簡化匹配過程并提高匹配的準確性,當然,如果你的匹配任務(wù)中顏色信息對匹配結(jié)果具有重要意義,
* 可以跳過灰度化步驟,直接在彩色圖像上進行模板匹配。
*/
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
);
// 校驗參數(shù)
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 參數(shù)見上方定義
* @returns {Match[]}
*/
function matchTemplate(img, template, options) {
if (img == null || template == null) {
throw new Error('ParamError');
}
options = MatchOptions.check(options);
console.log('參數(shù):', 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);
// 執(zhí)行模板匹配,標準化相關(guān)性系數(shù)匹配法
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, '可疑目標數(shù):', 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) {
// 每次取匹配結(jié)果中的最大值和位置,從而使結(jié)果按相似度指標從高到低排序
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)
);
// 屏蔽已匹配到的區(qū)域
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]);
// 保存匹配點的大致范圍,用于后續(xù)去重。設(shè)置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;
}
/**
* 根據(jù)搜圖結(jié)果在原圖上畫框
* @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);
// 將結(jié)果畫框展示
showMatchRectangle(result, largeImage.mat, template.mat);
template.recycle();
largeImage.recycle();
}
// 初始化openCV
runtime.getImages().initOpenCvIfNeeded();
main();
備注說明
-
參數(shù)
threhold
、region
、max
跟AutoX中的一樣。 -
grayTransform:是否將圖像進行灰度預處理,開啟可大幅提高找圖效率,默認為true。
對于模板匹配任務(wù),通常關(guān)注的是圖像的紋理和亮度變化,而不是顏色差異。因此,將圖像轉(zhuǎn)換為灰度圖可以降低計算復雜度,減少模板匹配過程中的噪聲干擾,并提高匹配的穩(wěn)定性和準確性。尤其是對于一些目標圖案周圍顏色不確定的搜圖場景,開啟灰度處理后后,不管目標周圍顏色如何變化,都會找到一個較高準確度的匹配點。如果你的模板小圖中紋理不明顯,或是一團顏色相近的色塊,就得關(guān)閉該功能。 -
scaleFactors:是對小圖模板的縮放因子,數(shù)組類型,默認為
[1, 0.9, 1.1, 0.8, 1.2]
。每一項可以是數(shù)字,表示寬高等比例縮放,也可以是長度為2的數(shù)組,表示寬、高對應(yīng)的縮放比,例如:[0.9, 1, [1.1, 1.2]]
這里重點強調(diào)一點,我沒有在 openCV 里找到可以直接用于忽略圖像比例差異的搜圖方法,只能手動指定可能的縮放范圍,依次對小圖模板進行縮放后再搜圖。理論上,只要你設(shè)置的(不重復)縮放因子足夠多,就一定能找到目標,除非圖中沒有??。
-
max參數(shù)的妙用:搜圖過程中,默認在找到前 max 個符合閾值的匹配點就退出,但是可能存在一種情況,例如先在縮放比為 1.1 的情況下找到了相似度為 0.8 的點 P1,此時若還沒有找夠 max 個匹配點,在后續(xù)比例為 1.2 的情況下,檢測到點 P1 處的相似度提高到0.9,就將原來 P1 點的信息更新為更準確的信息。理解了這一點,如果你將 max 設(shè)置的非常大,我的搜圖算法就會按照
scaleFactors
中設(shè)置的全部縮放因子都檢測一遍,不會提前結(jié)束,那么最終結(jié)果中所有的 Match 對象中保存的都是最佳匹配點的信息,你可以憑借最終結(jié)果中的 scaleX、scaleY 信息,動態(tài)調(diào)整scaleFactors
參數(shù),使其優(yōu)先匹配最佳縮放比,提高后續(xù)的找圖效率。
三、測試結(jié)果展示
以下是一個測試數(shù)據(jù),模板圖是一坨近乎白色的光團,在使用了5個縮放因子情況下,對比了 grayTransform
參數(shù)開啟和關(guān)閉的情況,執(zhí)行效率上相差了好幾倍,且找圖結(jié)果也不一樣。
希望大家在使用時,清楚每個參數(shù)改變所產(chǎn)生的效果。
-
模板小圖
-
5個縮放因子下的找圖結(jié)果
-
開啟灰度處理
-
未開啟灰度處理
文章來源:http://www.zghlxwxcb.cn/news/detail-739770.html
持續(xù)更新鏈接:Android自動化-AutoX多分辨率找圖文章來源地址http://www.zghlxwxcb.cn/news/detail-739770.html
到了這里,關(guān)于AutoX.js - openCV多分辨率找圖的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!