diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/TemplateMatching.java b/autojs/src/main/java/com/stardust/autojs/core/image/TemplateMatching.java index 578ccd5b9..c25e58060 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/image/TemplateMatching.java +++ b/autojs/src/main/java/com/stardust/autojs/core/image/TemplateMatching.java @@ -32,6 +32,7 @@ public class TemplateMatching { public static class Match { public final Point point; public final double similarity; + public Integer templateIndex = 0; public Match(Point point, double similarity) { this.point = point; @@ -42,7 +43,8 @@ public Match(Point point, double similarity) { @Override public String toString() { return "Match{" + - "point=" + point + + "templateIndex=" + templateIndex + + ", point=" + point + ", similarity=" + similarity + '}'; } @@ -73,86 +75,79 @@ public static Point fastTemplateMatching(Mat img, Mat template, int matchMethod, * @return */ public static List fastTemplateMatching(Mat img, Mat template, int matchMethod, float weakThreshold, float strictThreshold, int maxLevel, int limit, Boolean transparentMask) { - TimingLogger logger = new TimingLogger(LOG_TAG, "fast_tm"); - // 创建资源回收列表 + List templates = Collections.singletonList(template); + return fastMultiTemplateMatching(img, templates, matchMethod, weakThreshold, + strictThreshold, maxLevel, limit, transparentMask); + } + + /** + * 多模板匹配(高效版) + * 传入一张主图和多个模板,内部共享主图金字塔,一次完成所有模板的匹配。 + * + * @param img 主图 + * @param templates 模板列表 + * @param matchMethod 匹配算法 + * @param weakThreshold 弱阈值 + * @param strictThreshold 强阈值 + * @param maxLevel 金字塔层数(设为 MAX_LEVEL_AUTO 自动计算) + * @param limit 每层最多返回的候选点数量(控制计算量) + * @param transparentMask 是否启用透明蒙版 + * @return 按模板顺序返回匹配结果列表;若某模板无匹配,则对应列表为空 + */ + public static List fastMultiTemplateMatching(Mat img, + List templates, + int matchMethod, + float weakThreshold, + float strictThreshold, + int maxLevel, + int limit, + Boolean transparentMask) { + TimingLogger logger = new TimingLogger(LOG_TAG, "fast_multi_tm"); List resourcesToRelease = new ArrayList<>(); try { + // 确定全局最大金字塔层数(取所有模板允许层数的最大值) + int globalMaxLevel; if (maxLevel == MAX_LEVEL_AUTO) { - maxLevel = selectPyramidLevel(img, template); - logger.addSplit("selectPyramidLevel:" + maxLevel); - } - List finalMatchResult = new ArrayList<>(); - List previousMatchResult = Collections.emptyList(); - boolean isFirstMatching = true; - for (int level = maxLevel; level >= 0; level--) { - List currentMatchResult = new ArrayList<>(); - Mat src = getPyramidDownAtLevel(img, level); - Mat currentTemplate = getPyramidDownAtLevel(template, level); - Mat currentMask = null; - - if (transparentMask) { - currentMask = TemplateMatchingKt.INSTANCE.processingAlphaChannel(currentTemplate); + globalMaxLevel = 0; + for (Mat template : templates) { + int level = selectPyramidLevel(img, template); + if (level > globalMaxLevel) globalMaxLevel = level; } + } else { + globalMaxLevel = maxLevel; + } - // +++ 添加到释放列表 +++ + // 构建主图金字塔(0 ~ globalMaxLevel) + List imgPyramid = new ArrayList<>(); + for (int level = 0; level <= globalMaxLevel; level++) { + Mat src = getPyramidDownAtLevel(img, level); + imgPyramid.add(src); if (src != img) { resourcesToRelease.add(src); } - if (currentTemplate != template) { - resourcesToRelease.add(currentTemplate); - } - if (currentMask != null) { - resourcesToRelease.add(currentMask); - } - - // 如果在上一轮中没有匹配到图片,则考虑是否退出匹配 - if (previousMatchResult.isEmpty()) { - // 如果不是第一次匹配,并且不满足shouldContinueMatching的条件,则直接退出匹配 - if (!isFirstMatching && !shouldContinueMatching(level, maxLevel)) { - break; - } - Mat matchResult = matchTemplate(src, currentTemplate, matchMethod, currentMask); - resourcesToRelease.add(matchResult); - getBestMatched(matchResult, currentTemplate, matchMethod, weakThreshold, currentMatchResult, limit, null); - } else { - for (Match match : previousMatchResult) { - Rect r = getROI(match.point, src, currentTemplate); - Mat m = new Mat(src, r); - Mat matchResult = matchTemplate(m, currentTemplate, matchMethod, currentMask); - - // +++ 添加到释放列表 +++ - resourcesToRelease.add(m); - resourcesToRelease.add(matchResult); - - getBestMatched(matchResult, currentTemplate, matchMethod, weakThreshold, currentMatchResult, limit, r); - } - } + } - logger.addSplit("level:" + level + ", result:" + previousMatchResult); - - // 把满足强阈值的点找出来,加到最终结果列表 - if (!currentMatchResult.isEmpty()) { - Iterator iterator = currentMatchResult.iterator(); - while (iterator.hasNext()) { - Match match = iterator.next(); - if (match.similarity >= strictThreshold) { - pyrUp(match.point, level); - finalMatchResult.add(match); - iterator.remove(); - } - } - // 如果所有结果都满足强阈值,则退出循环,返回最终结果 - if (currentMatchResult.isEmpty()) { - break; - } + List allResults = new ArrayList<>(); + for (int i = 0; i < templates.size(); i++) { + List result = matchTemplateWithPyramid(imgPyramid, + templates.get(i), + globalMaxLevel, + matchMethod, + weakThreshold, + strictThreshold, + limit, + transparentMask, + resourcesToRelease); + for (Match r : result) { + r.templateIndex = i; } - isFirstMatching = false; - previousMatchResult = currentMatchResult; + allResults.addAll(result); } - logger.addSplit("result:" + finalMatchResult); + + logger.addSplit("multi_templates: " + templates.size()); logger.dumpToLog(); - return finalMatchResult; + return allResults; } finally { for (Mat mat : resourcesToRelease) { try { @@ -164,6 +159,103 @@ public static List fastTemplateMatching(Mat img, Mat template, int matchM } } + /** + * 利用已有的主图金字塔对单个模板进行匹配 + * + * @param imgPyramid 主图金字塔(0 为原图) + * @param template 单个模板原图 + * @param globalMaxLevel 全局最大层数(用于限制模板自身层数) + * @param matchMethod 匹配算法 + * @param weakThreshold 弱阈值 + * @param strictThreshold 强阈值 + * @param limit 每层最大候选数 + * @param transparentMask 是否处理透明通道 + * @param resourcesToRelease 资源回收列表 + * @return 匹配结果(坐标已映射回原图) + */ + private static List matchTemplateWithPyramid(List imgPyramid, + Mat template, + int globalMaxLevel, + int matchMethod, + float weakThreshold, + float strictThreshold, + int limit, + Boolean transparentMask, + List resourcesToRelease) { + // 计算模板自身的最大可行层数 + int tMaxLevel = selectPyramidLevel(imgPyramid.get(0), template); + tMaxLevel = Math.min(tMaxLevel, globalMaxLevel); + + // 构建模板金字塔 + List templatePyramid = new ArrayList<>(); + for (int level = 0; level <= tMaxLevel; level++) { + Mat t = getPyramidDownAtLevel(template, level); + templatePyramid.add(t); + if (t != template) { + resourcesToRelease.add(t); + } + } + + List finalMatchResult = new ArrayList<>(); + List previousMatchResult = Collections.emptyList(); + boolean isFirstMatching = true; + + for (int level = tMaxLevel; level >= 0; level--) { + List currentMatchResult = new ArrayList<>(); + Mat src = imgPyramid.get(level); + Mat currentTemplate = templatePyramid.get(level); + Mat currentMask = null; + + if (transparentMask) { + currentMask = TemplateMatchingKt.INSTANCE.processingAlphaChannel(currentTemplate); + resourcesToRelease.add(currentMask); + } + + if (previousMatchResult.isEmpty()) { + // 决定是否继续匹配(根据弱阈值跳过部分层) + if (!isFirstMatching && !shouldContinueMatching(level, tMaxLevel)) { + break; + } + // 全图匹配 + Mat matchResult = matchTemplate(src, currentTemplate, matchMethod, currentMask); + resourcesToRelease.add(matchResult); + getBestMatched(matchResult, currentTemplate, matchMethod, weakThreshold, + currentMatchResult, limit, null); + } else { + // 在上一层的匹配点附近进行局部匹配 + for (Match match : previousMatchResult) { + Rect r = getROI(match.point, src, currentTemplate); + Mat m = new Mat(src, r); + Mat matchResult = matchTemplate(m, currentTemplate, matchMethod, currentMask); + resourcesToRelease.add(m); + resourcesToRelease.add(matchResult); + getBestMatched(matchResult, currentTemplate, matchMethod, weakThreshold, + currentMatchResult, limit, r); + } + } + + // 强阈值处理:达到 strictThreshold 的结果直接加入最终列表,并停止该模板后续匹配 + if (!currentMatchResult.isEmpty()) { + Iterator iterator = currentMatchResult.iterator(); + while (iterator.hasNext()) { + Match match = iterator.next(); + if (match.similarity >= strictThreshold) { + pyrUp(match.point, level); // 坐标还原至原图尺寸 + finalMatchResult.add(match); + iterator.remove(); + } + } + if (currentMatchResult.isEmpty()) { + break; // 所有候选都满足强阈值,提前终止 + } + } + + isFirstMatching = false; + previousMatchResult = currentMatchResult; + } + + return finalMatchResult; + } private static Mat getPyramidDownAtLevel(Mat m, int level) { if (level == 0) { @@ -273,7 +365,7 @@ private static Match getBestMatched(Mat tmResult, int matchMethod, float weakThr pos.y += rect.y; } logger.addSplit("value:" + value); - if(!Double.isFinite(value)){ + if (!Double.isFinite(value)) { return getBestMatched(replaceNoFinite(tmResult), matchMethod, weakThreshold, rect); } return new Match(pos, value); diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt index 1dca8da3c..a922b6d9f 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt +++ b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt @@ -35,6 +35,7 @@ class ScreenCaptureManager : ScreenCaptureRequester { return } + recycle() val result = run { val result = CompletableDeferred() TransparentActivity.requestNewActivity(context) { activity -> @@ -85,9 +86,6 @@ class ScreenCaptureManager : ScreenCaptureRequester { // 绑定后立即启动服务 context.startForegroundService(serviceIntent) - - delay(50) // 短暂等待服务启动 - serviceConnected.await() } diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt b/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt index dca27e6c1..a01c458af 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt +++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt @@ -241,11 +241,6 @@ class Images( fun releaseScreenCapturer() { disposables.forEach { it.dispose() } disposables.clear() - try { - mScreenCaptureRequester.recycle() - } catch (e: Exception) { - Log.e(Images::class.java.name, "Error recycling screen capture", e) - } } @JvmOverloads @@ -303,17 +298,41 @@ class Images( maxLevel: Int, limit: Int, transparentMask: Boolean = false + ): List { + val templates: List = listOf(template) + return matchMultiTemplates( + image, + templates, + weakThreshold, + threshold, + rect, + maxLevel, + limit, + transparentMask + ) + } + + @JvmOverloads + fun matchMultiTemplates( + image: ImageWrapper?, + templates: List, + weakThreshold: Float, + threshold: Float, + rect: Rect?, + maxLevel: Int, + limit: Int, + transparentMask: Boolean = false ): List { initOpenCvIfNeeded() if (image == null) throw NullPointerException("image = null") - if (template == null) throw NullPointerException("template = null") + if (templates.isEmpty() || templates.any { it == null }) throw NullPointerException("template = null") var src = image.mat if (rect != null) { src = Mat(src, rect) } - val result = TemplateMatching.fastTemplateMatching( - src, template.mat, Imgproc.TM_CCOEFF_NORMED, + val result = TemplateMatching.fastMultiTemplateMatching( + src, templates.map { it?.mat }, Imgproc.TM_CCOEFF_NORMED, weakThreshold, threshold, maxLevel, limit, transparentMask ) for (match in result) { diff --git a/autojs/src/main/js/v6-api/src/images/index.ts b/autojs/src/main/js/v6-api/src/images/index.ts index 38e68f077..f8288cae1 100644 --- a/autojs/src/main/js/v6-api/src/images/index.ts +++ b/autojs/src/main/js/v6-api/src/images/index.ts @@ -336,6 +336,11 @@ images.findImage = function (img: Image, template: Image, options?: FindImageOpt } images.matchTemplate = function (img: Image, template: Image, + options?: FindImageOptions & { max?: number }) { + return images.matchMultiTemplates(img, [template], options); +} + +images.matchMultiTemplates = function (img: Image, templates: Image[], options?: FindImageOptions & { max?: number }) { initIfNeeded(); options = options || {}; @@ -349,15 +354,13 @@ images.matchTemplate = function (img: Image, template: Image, var result; const transparentMask = !!options.transparentMask if (options.region) { - result = javaImages.matchTemplate(img, template, weakThreshold, threshold, buildRegion(options.region, img), maxLevel, max, transparentMask); + result = javaImages.matchMultiTemplates(img, templates, weakThreshold, threshold, buildRegion(options.region, img), maxLevel, max, transparentMask); } else { - result = javaImages.matchTemplate(img, template, weakThreshold, threshold, null, maxLevel, max, transparentMask); + result = javaImages.matchMultiTemplates(img, templates, weakThreshold, threshold, null, maxLevel, max, transparentMask); } return new MatchingResult(result); } - - images.findImageInRegion = function (img: Image, template: Image, x: number, y: number, width?: number, height?: number, threshold?: number) { return images.findImage(img, template, {