/* * Copyright (c) 2010-2016, Sikuli.org, sikulix.com * Released under the MIT License. * */ package org.sikuli.script; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import org.opencv.core.Core; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.MatOfDouble; import org.opencv.core.Rect; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; import org.sikuli.basics.Debug; import org.sikuli.basics.Settings; /** * EXPERIMENTAL --- INTERNAL USE ONLY<br> * is not official API --- will not be in version 2 */ public class ImageFind implements Iterator<Match>{ private static String me = "ImageFind: "; private static int lvl = 3; private static void log(int level, String message, Object... args) { Debug.logx(level, me + message, args); } private ImageFinder owner = null; private boolean isValid = false; private boolean isInnerFind = false; private Image pImage = null; private Mat probe = new Mat(); private boolean isPlainColor = false; private boolean isBlack = false; private double similarity = Settings.MinSimilarity; private double waitingTime = Settings.AutoWaitTimeout; private boolean shouldCheckLastSeen = Settings.CheckLastSeen; private Object[] findArgs = null; private int resizeMinDownSample = 12; private double resizeFactor; private float[] resizeLevels = new float[] {1f, 0.4f}; private int resizeMaxLevel = resizeLevels.length - 1; private double resizeMinSim = 0.9; private double resizeMinFactor = 1.5; private Core.MinMaxLocResult findDownRes = null; private int sorted; public static final int AS_ROWS = 0; public static final int AS_COLUMNS = 1; public static final int BEST_FIRST = 2; private int finding = -1; public static final int FINDING_ANY = 0; public static final int FINDING_SOME = 1; public static final int FINDING_ALL = 2; private int count = 0; public static int SOME_COUNT = 5; public static int ALL_MAX = 100; private int allMax = 0; private List<Match> matches = Collections.synchronizedList(new ArrayList<Match>()); private boolean repeating; private long lastFindTime = 0; private long lastSearchTime = 0; public ImageFind() { matches.add(null); } public boolean isValid() { return true; } public void setIsInnerFind() { isInnerFind = true; } void setSimilarity(double sim) { similarity = sim; } public void setFindTimeout(double t) { waitingTime = t; } public void setFinding(int ftyp) { finding = ftyp; } public void setSorted(int styp) { sorted = styp; } public void setCount(int c) { count = c; } public List<Match> getMatches() { return matches; } protected boolean checkFind(ImageFinder owner, Object pprobe, Object... args) { if (owner.isValid()) { this.owner = owner; } else { return false; } isValid = false; shouldCheckLastSeen = Settings.CheckLastSeen; if (pprobe instanceof String) { pImage = Image.create((String) pprobe); if (pImage.isValid()) { isValid = true; } } else if (pprobe instanceof Image) { if (((Image) pprobe).isValid()) { isValid = true; pImage = (Image) pprobe; } } else if (pprobe instanceof Pattern) { if (((Pattern) pprobe).getImage().isValid()) { isValid = true; pImage = ((Pattern) pprobe).getImage(); similarity = ((Pattern) pprobe).getSimilar(); } } else if (pprobe instanceof Mat) { isValid = true; probe = (Mat) pprobe; waitingTime = 0.0; shouldCheckLastSeen = false; } else { log(-1, "find(... some, any, all): probe invalid (not Pattern, String nor valid Image)"); return false; } if (probe.empty()) { probe = Image.createMat(pImage.get()); } checkProbe(); if (!owner.isImage()) { if (args.length > 0) { if (args[0] instanceof Integer) { waitingTime = 0.0 + (Integer) args[0]; } else if (args[0] instanceof Double) { waitingTime = (Double) args[0]; } } if (args.length > 1) { findArgs = Arrays.copyOfRange(args, 1, args.length); } else { findArgs = null; } } return isValid; } private void checkProbe() { MatOfDouble pMean = new MatOfDouble(); MatOfDouble pStdDev = new MatOfDouble(); Core.meanStdDev(probe, pMean, pStdDev); double min = 0.00001; isPlainColor = false; double sum = 0.0; double arr[] = pStdDev.toArray(); for (int i = 0; i < arr.length; i++) { sum += arr[i]; } if (sum < min) { isPlainColor = true; } sum = 0.0; arr = pMean.toArray(); for (int i = 0; i < arr.length; i++) { sum += arr[i]; } if (sum < min && isPlainColor) { isBlack = true; } resizeFactor = Math.min(((double) probe.width())/resizeMinDownSample, ((double) probe.height())/resizeMinDownSample); resizeFactor = Math.max(1.0, resizeFactor); } protected ImageFind doFind() { Debug.enter(me + ": doFind"); Core.MinMaxLocResult fres = null; repeating = false; long begin = (new Date()).getTime(); long lap; while (true) { lastFindTime = (new Date()).getTime(); if (shouldCheckLastSeen && !repeating && !owner.isImage && pImage.getLastSeen() != null) { log(3, "checkLastSeen: trying ..."); ImageFinder f = new ImageFinder(new Region(pImage.getLastSeen())); if (null != f.findInner(probe, pImage.getLastSeenScore() - 0.01)) { log(lvl, "checkLastSeen: success"); set(f.next()); if (pImage != null) { pImage.setLastSeen(get().getRect(), get().getScore()); } break; } log(lvl, "checkLastSeen: not found"); } if (!owner.isMultiFinder || owner.base.empty()) { if (owner.isRegion) { owner.setBase(owner.region.getScreen().capture(owner.region).getImage()); } else if (owner.isScreen) { owner.setBase(owner.screen.capture().getImage()); } } if (!isInnerFind && resizeFactor > resizeMinFactor) { log(3, "downsampling: trying ..."); doFindDown(0, resizeFactor); fres = findDownRes; } if (fres == null) { if (!isInnerFind) { log(3, "downsampling: not found with (%f) - trying original size", resizeFactor); } fres = doFindDown(0, 0.0); if(fres != null && fres.maxVal > similarity - 0.01) { set(new Match((int) fres.maxLoc.x + owner.offX, (int) fres.maxLoc.y + owner.offY, probe.width(), probe.height(), fres.maxVal, null, null)); } } else { log(lvl, "downsampling: success: adjusting match"); set(checkFound(fres)); } lastFindTime = (new Date()).getTime() - lastFindTime; if (hasNext()) { get().setTimes(lastFindTime, lastSearchTime); if (pImage != null) { pImage.setLastSeen(get().getRect(), get().getScore()); } break; } else { if (isInnerFind || owner.isImage()) { break; } else { if (waitingTime < 0.001 || (lap = (new Date()).getTime() - begin) > waitingTime * 1000) { break; } if (owner.MaxTimePerScan > lap) { try { Thread.sleep(owner.MaxTimePerScan - lap); } catch (Exception ex) { } } repeating = true; } } } return this; } private Match checkFound(Core.MinMaxLocResult res) { Match match = null; ImageFinder f; Rect r = null; if (owner.isImage()) { int off = ((int) resizeFactor) + 1; r = getSubMatRect(owner.base, (int) res.maxLoc.x, (int) res.maxLoc.y, probe.width(), probe.height(), off); f = new ImageFinder(owner.base.submat(r)); } else { f = new ImageFinder((new Region((int) res.maxLoc.x + owner.offX, (int) res.maxLoc.y + owner.offY, probe.width(), probe.height())).grow(((int) resizeFactor) + 1)); } if (null != f.findInner(probe, similarity)) { log(lvl, "check after downsampling: success"); match = f.next(); if (owner.isImage()) { match.x += r.x; match.y += r.y; } } return match; } private static Rect getSubMatRect(Mat mat, int x, int y, int w, int h, int margin) { x = Math.max(0, x - margin); y = Math.max(0, y - margin); w = Math.min(w + 2 * margin, mat.width() - x); h = Math.min(h + 2 * margin, mat.height()- y); return new Rect(x, y, w, h); } private Core.MinMaxLocResult doFindDown(int level, double factor) { Debug.enter(me + ": doFindDown (%d - 1/%.2f)", level, factor * resizeLevels[level]); Debug timer = Debug.startTimer("doFindDown"); Mat b = new Mat(); Mat p = new Mat(); Core.MinMaxLocResult dres = null; double rfactor; if (factor > 0.0) { rfactor = factor * resizeLevels[level]; if (rfactor < resizeMinFactor) return null; Size sb = new Size(owner.base.cols()/rfactor, owner.base.rows()/factor); Size sp = new Size(probe.cols()/rfactor, probe.rows()/factor); Imgproc.resize(owner.base, b, sb, 0, 0, Imgproc.INTER_AREA); Imgproc.resize(probe, p, sp, 0, 0, Imgproc.INTER_AREA); dres = doFindMatch(b, p); log(lvl, "doFindDown: score: %.2f at (%d, %d)", dres.maxVal, (int) (dres.maxLoc.x * rfactor), (int) (dres.maxLoc.y * rfactor)); } else { dres = doFindMatch(owner.base, probe); timer.end(); return dres; } if (dres.maxVal < resizeMinSim) { if (level == resizeMaxLevel) { timer.end(); return null; } if (level == 0) { findDownRes = null; } level++; doFindDown(level, factor); } else { dres.maxLoc.x *= rfactor; dres.maxLoc.y *= rfactor; findDownRes = dres; } timer.end(); return null; } private Core.MinMaxLocResult doFindMatch(Mat base, Mat probe) { Mat res = new Mat(); Mat bi = new Mat(); Mat pi = new Mat(); if (!isPlainColor) { Imgproc.matchTemplate(base, probe, res, Imgproc.TM_CCOEFF_NORMED); } else { if (isBlack) { Core.bitwise_not(base, bi); Core.bitwise_not(probe, pi); } else { bi = base; pi = probe; } Imgproc.matchTemplate(bi, pi, res, Imgproc.TM_SQDIFF_NORMED); Core.subtract(Mat.ones(res.size(), CvType.CV_32F), res, res); } return Core.minMaxLoc(res); } @Override public boolean hasNext() { if (matches.size() > 0) { return matches.get(0) != null; } return false; } @Override public Match next() { Match m = null; if (matches.size() > 0) { m = matches.get(0); remove(); } return m; } @Override public void remove() { if (matches.size() > 0) { matches.remove(0); } } public Match get() { return get(0); } public Match get(int n) { if (n < matches.size()) { return matches.get(n); } return null; } private Match add(Match m) { if (matches.add(m)) { return m; } return null; } private Match set(Match m) { if (matches.size() > 0) { matches.set(0, m); } else { matches.add(m); } return m; } public int getSize() { return matches.size(); } }