/* * The org.opensourcephysics.media.core package defines the Open Source Physics * media framework for working with video and other media. * * Copyright (c) 2014 Douglas Brown and Wolfgang Christian. * * This is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA * or view the license online at http://www.gnu.org/copyleft/gpl.html * * For additional information and documentation on Open Source Physics, * please see <http://www.opensourcephysics.org/>. */ package org.opensourcephysics.media.core; import java.awt.*; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.image.*; import java.util.ArrayList; import java.util.HashMap; import java.util.TreeMap; import org.opensourcephysics.display.Dataset; import org.opensourcephysics.tools.DatasetCurveFitter; import org.opensourcephysics.tools.FitBuilder; import org.opensourcephysics.tools.UserFunction; /** * A class to find the best match of a template image in a target image. * The match location is estimated to sub-pixel accuracy by assuming the * distribution of match scores near a peak is Gaussian. * * @author Douglas Brown * @version 1.0 */ public class TemplateMatcher { // static constants private static final double LARGE_NUMBER = 1.0E10; // instance fields private BufferedImage original, template, working, match; private Shape mask; private int[] pixels, templateR, templateG, templateB; private boolean[] isPixelTransparent; private int[] targetPixels, matchPixels; private int wTemplate, hTemplate; // width and height of the template image private int wTarget, hTarget; // width and height of the target image private int wTest, hTest; // width and height of the tested image (in search rect) private TPoint p = new TPoint(); // for general use in methods private double largeNumber = 1.0E20; // bigger than any expected difference private DatasetCurveFitter fitter; // used for Gaussian fit private Dataset dataset; // used for Gaussian fit private UserFunction f; // used for Gaussian fit private double[] pixelOffsets = {-1, 0, 1}; // used for Gaussian fit private double[] xValues = new double[3]; // used for Gaussian fit private double[] yValues = new double[3]; // used for Gaussian fit private double peakHeight, peakWidth; // peak height and width of most recent match private int trimLeft, trimTop; private int[] alphas = new int[2]; // most recent alphas {input, original} private int index; // for AutoTracker--not used internally /** * Constructs a TemplateMatcher object. If a mask shape is specified, then * only pixels that are entirely inside the mask are included in the template. * * @param image the image to match * @param maskShape a shape to define inside pixels (may be null) */ public TemplateMatcher(BufferedImage image, Shape maskShape) { mask = maskShape; setTemplate(image); // set up the Gaussian curve fitter dataset = new Dataset(); fitter = new DatasetCurveFitter(dataset, new FitBuilder(null)); fitter.setActive(true); fitter.setAutofit(true); f = new UserFunction("gaussian"); //$NON-NLS-1$ f.setParameters(new String[] {"a", "b", "c"}, //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ new double[] {1, 0, 1}); f.setExpression("a*exp(-(x-b)^2/c)", new String[] {"x"}); //$NON-NLS-1$ //$NON-NLS-2$ } /** * Sets the template to be used for the next search. * The new template dimensions must match those of the previous template. * * @param image the template image */ public void setTemplate(BufferedImage image) { if (template!=null && image.getType()==BufferedImage.TYPE_INT_ARGB && wTemplate==image.getWidth() && hTemplate==image.getHeight()) { template = image; template.getRaster().getDataElements(0, 0, wTemplate, hTemplate, pixels); // set up rgb and transparency arrays for fast matching for (int i = 0; i < pixels.length; i++) { int val = pixels[i]; templateR[i] = getRed(val); // red templateG[i] = getGreen(val); // green templateB[i] = getBlue(val); // blue isPixelTransparent[i] = getAlpha(val)==0; // alpha } } else { if (image.getType()!=BufferedImage.TYPE_INT_ARGB) { original = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); original.createGraphics().drawImage(image, 0, 0, null); } else original = image; template = buildTemplate(original, 255, 0); // builds from scratch setTemplate(template); } } /** * Gets the template. Includes only pixels inside the mask. * * @return the template */ public BufferedImage getTemplate() { if (template == null) { template = buildTemplate(original, 255, 0); // builds from scratch setTemplate(template); } return template; } /** * Builds the template from an input image. * The input image dimensions must match the original. * The input and original are overlaid onto the working image, from which * the template is generated. Pixels that fall outside the mask are ignored * in the f template. * * @param image the input image * @param alphaInput the opacity with which the input image is overlaid (0-255) * @param alphaOriginal the opacity with which the original image is overlaid (0-255) * @return the template */ public BufferedImage buildTemplate(BufferedImage image, int alphaInput, int alphaOriginal) { int w = image.getWidth(); int h = image.getHeight(); // return if image dimensions do not match original image if (original.getWidth()!=w || original.getHeight()!=h) return null; // return existing if both alphas are zero if (alphaInput==0 && alphaOriginal==0) return template!=null? template: original; // save alphas alphas[0] = alphaInput; alphas[1] = alphaOriginal; // set up argb input image BufferedImage input; if (image.getType()==BufferedImage.TYPE_INT_ARGB) input = image; else { input = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); input.createGraphics().drawImage(image, 0, 0, null); } // create working image if needed if (working==null) { working = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); } // reset template dimensions and create new template if needed if (template==null || w!=wTemplate || h!=hTemplate) { wTemplate = w; hTemplate = h; int len = w*h; template = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); pixels = new int[len]; templateR = new int[len]; templateG = new int[len]; templateB = new int[len]; isPixelTransparent = new boolean[len]; matchPixels = new int[len]; } // set alpha of input and draw onto working Graphics2D gWorking = working.createGraphics(); alphaInput = Math.max(0, Math.min(255, alphaInput)); if (alphaInput>0) { // overlay only if not transparent gWorking.setComposite(getComposite(alphaInput)); gWorking.drawImage(input, 0, 0, null); } // set alpha of original and draw onto working alphaOriginal = Math.max(0, Math.min(255, alphaOriginal)); if (alphaOriginal>0) { // overlay only if not transparent gWorking.setComposite(getComposite(alphaOriginal)); gWorking.drawImage(original, 0, 0, null); } // read pixels from working raster working.getRaster().getDataElements(0, 0, wTemplate, hTemplate, pixels); if (mask != null) { // set pixels outside mask to transparent for (int i = 0; i < pixels.length; i++) { boolean inside = true; // pixel is inside only if all corners are inside int x = i%wTemplate, y = i/wTemplate; for (int j = 0; j < 2; j++) { for (int k = 0; k < 2; k++) { p.setLocation(x+j, y+k); inside = inside && mask.contains(p); } } if (!inside) pixels[i] = pixels[i] & (0 << 24); // set alpha to zero (transparent) } } // write pixels to template raster template.getRaster().setDataElements(0, 0, wTemplate, hTemplate, pixels); // trim transparent edges from template int trimRight=0, trimBottom=0; trimLeft = trimTop = 0; // left edge boolean transparentEdge = true; while (transparentEdge && trimLeft < wTemplate) { for (int line = 0; line < hTemplate; line++) { int i = line*wTemplate+trimLeft; transparentEdge = transparentEdge && getAlpha(pixels[i])==0; } if (transparentEdge) trimLeft++; } // right edge transparentEdge = true; while (transparentEdge && (trimLeft+trimRight) < wTemplate) { for (int line = 0; line < hTemplate; line++) { int i = (line+1)*wTemplate-1-trimRight; transparentEdge = transparentEdge && getAlpha(pixels[i])==0; } if (transparentEdge) trimRight++; } // top edge transparentEdge = true; while (transparentEdge && trimTop < hTemplate) { for (int col = 0; col < wTemplate; col++) { int i = trimTop*wTemplate+col; transparentEdge = transparentEdge && getAlpha(pixels[i])==0; } if (transparentEdge) trimTop++; } // bottom edge transparentEdge = true; while (transparentEdge && (trimTop+trimBottom) < hTemplate) { for (int col = 0; col < wTemplate; col++) { int i = (hTemplate-1-trimBottom)*wTemplate+col; transparentEdge = transparentEdge && getAlpha(pixels[i])==0; } if (transparentEdge) trimBottom++; } // reduce size of template if needed if (trimLeft+trimRight+trimTop+trimBottom > 0) { wTemplate -= (trimLeft+trimRight); hTemplate -= (trimTop+trimBottom); wTemplate = Math.max(wTemplate, 1); hTemplate = Math.max(hTemplate, 1); int len = wTemplate*hTemplate; pixels = new int[len]; templateR = new int[len]; templateG = new int[len]; templateB = new int[len]; isPixelTransparent = new boolean[len]; matchPixels = new int[len]; BufferedImage bi = new BufferedImage(wTemplate, hTemplate, BufferedImage.TYPE_INT_ARGB); bi.createGraphics().drawImage(template, -trimLeft, -trimTop, null); template = bi; template.getRaster().getDataElements(0, 0, wTemplate, hTemplate, pixels); } return template; } /** * Gets the alphas used to build the most recent template. * * @return int[] {alphaInput, alphaOriginal} */ public int[] getAlphas() { return alphas; } /** * Sets the index. * * @param index the index */ public void setIndex(int index) { this.index = index; } /** * Gets the index. * Note: index is not set internally, but only with calls to setIndex(). * * @return the index */ public int getIndex() { return index; } /** * Gets the working image pixels used to generate the template. * * @param pixels int[] of pixels. If null, it will be created * @return the filled pixels array */ public int[] getWorkingPixels(int[] pixels) { if (pixels==null || pixels.length!=wTemplate*hTemplate) { pixels = new int[wTemplate*hTemplate]; } working.getRaster().getDataElements(0, 0, wTemplate, hTemplate, pixels); return pixels; } /** * Sets the working image pixels used to generate the template. * * @param pixels int[] of pixels */ public void setWorkingPixels(int[] pixels) { if (pixels!=null && pixels.length==wTemplate*hTemplate) working.getRaster().setDataElements(0, 0, wTemplate, hTemplate, pixels); } /** * Gets the template location at which the best match occurs in a rectangle. * May return null. * * Template matching process: * 1. At each test position in the search area, find the RGB square deviation ("RGBSqD": sum * of squares of rgb differences of all pixels) between the template and video image. * Note that the RGBSqD is zero for a perfect match and larger for poorer matches. * 2. Determine the average RGBSqD for all test positions. * 3. Define the position for which the RGBSqD is minimum as the "working" best match. * Define the peak height ("PH") of this match to be PH = (avgRGBSqD/matchRGBSqD)-1. * Note that the PH may vary from zero to infinity. * 4. If the PH exceeds the "Automark" setting, the match is deemed to be a good one * (i.e., significantly better than average). * 5. For sub-pixel accuracy, fit a Gaussian curve to the PHs of the working best match * and its immediate vertical and horizontal neighbors. Note that the 3-point Gaussian * fits should be exact. * 6. The final best match (sub-pixel) is the position of the peak of the Gaussian fit. * 7. Note that the width of the Gaussian fit is probably correlated with the uncertainty of * the match position, but it is not used to explicitly estimate this uncertainty. * * @param target the image to search * @param searchRect the rectangle to search within the target image * @return the optimized template location at which the best match, if any, is found */ public TPoint getMatchLocation(BufferedImage target, Rectangle searchRect) { wTarget = target.getWidth(); hTarget = target.getHeight(); // determine insets needed to accommodate template int left = wTemplate/2, right = left; if (wTemplate%2>0) right++; int top = hTemplate/2, bottom = top; if (hTemplate%2>0) bottom++; // trim search rectangle if necessary searchRect.x = Math.max(left, Math.min(wTarget-right, searchRect.x)); searchRect.y = Math.max(top, Math.min(hTarget-bottom, searchRect.y)); searchRect.width = Math.min(wTarget-searchRect.x-right, searchRect.width); searchRect.height = Math.min(hTarget-searchRect.y-bottom, searchRect.height); if (searchRect.width <= 0 || searchRect.height <= 0) { peakHeight = Double.NaN; peakWidth = Double.NaN; return null; } // set up test pixels to search (rectangle plus template) int xMin = Math.max(0, searchRect.x-left); int xMax = Math.min(wTarget, searchRect.x+searchRect.width+right); int yMin = Math.max(0, searchRect.y-top); int yMax = Math.min(hTarget, searchRect.y+searchRect.height+bottom); wTest = xMax-xMin; hTest = yMax-yMin; if (target.getType() != BufferedImage.TYPE_INT_RGB) { BufferedImage image = new BufferedImage(wTarget, hTarget, BufferedImage.TYPE_INT_RGB); image.createGraphics().drawImage(target, 0, 0, null); target = image; } targetPixels = new int[wTest * hTest]; target.getRaster().getDataElements(xMin, yMin, wTest, hTest, targetPixels); // find the rectangle point with the minimum difference double matchDiff = largeNumber; // larger than typical differences int xMatch=0, yMatch=0; double avgDiff = 0; for (int x = 0; x <= searchRect.width; x++) { for (int y = 0; y <= searchRect.height; y++) { double diff = getDifferenceAtTestPoint(x, y); avgDiff += diff; if (diff < matchDiff) { matchDiff = diff; xMatch = x; yMatch = y; } } } avgDiff /= (searchRect.width*searchRect.height); peakHeight = avgDiff/matchDiff-1; peakWidth = Double.NaN; double dx = 0, dy = 0; // if match is not exact, fit a Gaussian and find peak if (!Double.isInfinite(peakHeight)) { // fill data arrays xValues[1] = yValues[1] = peakHeight; for (int i = -1; i < 2; i++) { if (i == 0) continue; double diff = getDifferenceAtTestPoint(xMatch+i, yMatch); xValues[i+1] = avgDiff/diff-1; diff = getDifferenceAtTestPoint(xMatch, yMatch+i); yValues[i+1] = avgDiff/diff-1; } // estimate peakHeight = peak of gaussian // estimate offset dx of gaussian double pull = 1/(xValues[1]-xValues[0]); double push = 1/(xValues[1]-xValues[2]); if (Double.isNaN(pull)) pull=LARGE_NUMBER; if (Double.isNaN(push)) push=LARGE_NUMBER; dx = 0.6*(push-pull)/(push+pull); // estimate width wx of gaussian double ratio = dx>0? peakHeight/xValues[0]: peakHeight/xValues[2]; double wx = dx>0? dx+1: dx-1; wx = wx*wx/Math.log(ratio); // estimate offset dy of gaussian pull = 1/(yValues[1]-yValues[0]); push = 1/(yValues[1]-yValues[2]); if (Double.isNaN(pull)) pull=LARGE_NUMBER; if (Double.isNaN(push)) push=LARGE_NUMBER; dy = 0.6*(push-pull)/(push+pull); // estimate width wy of gaussian ratio = dy>0? peakHeight/yValues[0]: peakHeight/yValues[2]; double wy = dy>0? dy+1: dy-1; wy = wy*wy/Math.log(ratio); // set x parameters and fit to x data dataset.clear(); dataset.append(pixelOffsets, xValues); double rmsDev = 1; for (int k = 0; k < 3; k++) { double c = k==0? wx: k==1? wx/3: wx*3; f.setParameterValue(0, peakHeight); f.setParameterValue(1, dx); f.setParameterValue(2, c); rmsDev = fitter.fit(f); if (rmsDev < 0.01) { // fitter succeeded (3-point fit should be exact) dx = f.getParameterValue(1); peakWidth = f.getParameterValue(2); break; } } if (!Double.isNaN(peakWidth)) { // set y parameters and fit to y data dataset.clear(); dataset.append(pixelOffsets, yValues); for (int k = 0; k < 3; k++) { double c = k==0? wy: k==1? wy/3: wy*3; f.setParameterValue(0, peakHeight); f.setParameterValue(1, dy); f.setParameterValue(2, c); rmsDev = fitter.fit(f); if (rmsDev < 0.01) { // fitter succeeded (3-point fit should be exact) dy = f.getParameterValue(1); peakWidth = (peakWidth+f.getParameterValue(2))/2; break; } } if (rmsDev > 0.01) peakWidth = Double.NaN; } } xMatch = xMatch+searchRect.x-left-trimLeft; yMatch = yMatch+searchRect.y-top-trimTop; refreshMatchImage(target, xMatch, yMatch); return new TPoint(xMatch+dx, yMatch+dy); } /** * Refreshes the match image. * * @param target the matched image * @param x the match x-position * @param y the match y-position */ private void refreshMatchImage(BufferedImage target, int x, int y) { target.getRaster().getDataElements(x+1, y+1, wTemplate, hTemplate, matchPixels); for (int i = 0; i< matchPixels.length; i++) { matchPixels[i] = getValue(isPixelTransparent[i]? 0: 255, matchPixels[i]); // if (!isPixelTransparent[i]) // matchPixels[i] = getValue(255, matchPixels[i]); // else { // matchPixels[i] = getValue(0, matchPixels[i]); // } } if (match==null || match.getWidth()!=wTemplate || match.getHeight()!=hTemplate) { match = new BufferedImage(wTemplate, hTemplate, BufferedImage.TYPE_INT_ARGB); } match.getRaster().setDataElements(0, 0, wTemplate, hTemplate, matchPixels); } /** * Gets the template location at which the best match occurs in a * rectangle and along a line. May return null. * * @param target the image to search * @param searchRect the rectangle to search within the target image * @param x0 the x-component of a point on the line * @param y0 the y-component of a point on the line * @param theta the angle of the line * @param spread the spread of the line (line width = 1+2*spread) * @return the optimized template location of the best match, if any */ public TPoint getMatchLocation(BufferedImage target, Rectangle searchRect, double x0, double y0, double theta, int spread) { wTarget = target.getWidth(); hTarget = target.getHeight(); // determine insets needed to accommodate template int left = wTemplate/2, right = left; if (wTemplate%2>0) right++; int top = hTemplate/2, bottom = top; if (hTemplate%2>0) bottom++; // trim search rectangle if necessary searchRect.x = Math.max(left, Math.min(wTarget-right, searchRect.x)); searchRect.y = Math.max(top, Math.min(hTarget-bottom, searchRect.y)); searchRect.width = Math.min(wTarget-searchRect.x-right, searchRect.width); searchRect.height = Math.min(hTarget-searchRect.y-bottom, searchRect.height); if (searchRect.width <= 0 || searchRect.height <= 0) { // not able to search peakHeight = Double.NaN; peakWidth = Double.NaN; return null; } // set up test pixels to search (rectangle plus template) int xMin = Math.max(0, searchRect.x-left); int xMax = Math.min(wTarget, searchRect.x+searchRect.width+right); int yMin = Math.max(0, searchRect.y-top); int yMax = Math.min(hTarget, searchRect.y+searchRect.height+bottom); wTest = xMax-xMin; hTest = yMax-yMin; if (target.getType() != BufferedImage.TYPE_INT_RGB) { BufferedImage image = new BufferedImage(wTarget, hTarget, BufferedImage.TYPE_INT_RGB); image.createGraphics().drawImage(target, 0, 0, null); target = image; } targetPixels = new int[wTest * hTest]; target.getRaster().getDataElements(xMin, yMin, wTest, hTest, targetPixels); // get the points to search along the line ArrayList<Point2D> searchPts = getSearchPoints(searchRect, x0, y0, theta); if (searchPts==null) { // not able to search peakHeight = Double.NaN; peakWidth = -1; return null; } // collect differences in a map as they are measured HashMap<Point2D, Double> diffs = new HashMap<Point2D, Double>(); // find the point with the minimum difference from template double matchDiff = largeNumber; // larger than typical differences int xMatch=0, yMatch=0; double avgDiff = 0; Point2D matchPt = null; for (Point2D pt: searchPts) { int x = (int)pt.getX(); int y = (int)pt.getY(); double diff = getDifferenceAtTestPoint(x, y); diffs.put(pt, diff); avgDiff += diff; if (diff < matchDiff) { matchDiff = diff; xMatch = x; yMatch = y; matchPt = pt; } } avgDiff /= searchPts.size(); peakHeight = avgDiff/matchDiff-1; peakWidth = Double.NaN; double dl = 0; int matchIndex = searchPts.indexOf(matchPt); // if match is not exact, fit a Gaussian and find peak if (!Double.isInfinite(peakHeight) && matchIndex>0 && matchIndex<searchPts.size()-1) { // fill data arrays Point2D pt = searchPts.get(matchIndex-1); double diff = diffs.get(pt); xValues[0] = -pt.distance(matchPt); yValues[0] = avgDiff/diff-1; xValues[1] = 0; yValues[1] = peakHeight; pt = searchPts.get(matchIndex+1); diff = diffs.get(pt); xValues[2] = pt.distance(matchPt); yValues[2] = avgDiff/diff-1; // determine approximate offset (dl) and width (w) values double pull = -xValues[0]/(yValues[1]-yValues[0]); double push = xValues[2]/(yValues[1]-yValues[2]); if (Double.isNaN(pull)) pull=LARGE_NUMBER; if (Double.isNaN(push)) push=LARGE_NUMBER; dl = 0.3*(xValues[2]-xValues[0])*(push-pull)/(push+pull); double ratio = dl>0? peakHeight/yValues[0]: peakHeight/yValues[2]; double w = dl>0? dl-xValues[0]: dl-xValues[2]; w = w*w/Math.log(ratio); // set parameters and fit to x data dataset.clear(); dataset.append(xValues, yValues); double rmsDev = 1; for (int k = 0; k < 3; k++) { double c = k==0? w: k==1? w/3: w*3; f.setParameterValue(0, peakHeight); f.setParameterValue(1, dl); f.setParameterValue(2, c); rmsDev = fitter.fit(f); if (rmsDev < 0.01) { // fitter succeeded (3-point fit should be exact) dl = f.getParameterValue(1); peakWidth = f.getParameterValue(2); break; } } } double dx = dl*Math.cos(theta); double dy = dl*Math.sin(theta); xMatch = xMatch+searchRect.x-left-trimLeft; yMatch = yMatch+searchRect.y-top-trimTop; refreshMatchImage(target, xMatch, yMatch); return new TPoint(xMatch+dx, yMatch+dy); } /** * Gets the most recent match image. * * @return the best match image */ public BufferedImage getMatchImage() { return match; } /** * Returns the width and height of the peak for the most recent match. * The peak height is the ratio meanSqPixelDiff/matchSqPixelDiff. * The peak width is the mean of the vertical and horizontal Gaussian fit widths. * This data can be used to determine whether a match is acceptable. * A peak height greater than 5 is a reasonable standard for acceptability. * * Special cases: * 1. If the match is perfect, then the height is infinite and the width NaN. * 2. If the searchRect fell outside the target image, then no match was * possible and both the width and height are NaN. * 3. If there were no points to search along the 1D x-axis path, then * the height is NaN and the width is negative. * 4. If the Gaussian fit optimization was not successful (either horizontally * or vertically) then the height is finite and the width is NaN. * * @return double[2] {mean Gaussian width, height} */ public double[] getMatchWidthAndHeight() { return new double[] {peakWidth, peakHeight}; } /** * Method to get the color value * * @param a 0-255 alpha * @param argb current color value * @return the integer value */ public static int getValue(int a, int argb) { int r = getRed(argb); int g = getGreen(argb); int b = getBlue(argb); return getValue(a, r, g, b); } /** * Method to get the color value * * @param a 0-255 alpha * @param r 0-255 red * @param g 0-255 green * @param b 0-255 blue * @return the integer value */ public static int getValue(int a, int r, int g, int b) { int value = (a << 24) + (r << 16) + (g << 8) + b; return value; } /** * Method to get the alpha component from a color value * * @param value the color value * @return 0-255 alpha component */ public static int getAlpha(int value) { int alpha = (value >> 24) & 0xff; return alpha; } /** * Method to get the red component from a color value * * @param value the color value * @return 0-255 red component */ public static int getRed(int value) { int red = (value >> 16) & 0xff; return red; } /** * Method to get the green component from a color value * * @param value the color value * @return 0-255 green component */ public static int getGreen(int value) { int green = (value >> 8) & 0xff; return green; } /** * Method to get the blue component from a color value * * @param value the color value * @return 0-255 blue component */ public static int getBlue(int value) { int blue = value & 0xff; return blue; } //_____________________________ private methods _______________________ /** * Gets a list of Point2D objects that lie within pixels in a rectangle * and along a line. * * @param searchRect the rectangle * @param x0 the x-component of a point on the line * @param y0 the y-component of a point on the line * @param theta the angle of the line * @return a list of Point2D */ public ArrayList<Point2D> getSearchPoints(Rectangle searchRect, double x0, double y0, double theta) { double slope = -Math.tan(theta); // create line to search along Line2D line = new Line2D.Double(); if (Math.abs(slope)>LARGE_NUMBER) { // vertical axis line.setLine(x0, y0, x0, y0+1); } else if (Math.abs(slope)<1/LARGE_NUMBER) { // horizontal axis line.setLine(x0, y0, x0+1, y0); } else { line.setLine(x0, y0, x0+1, y0+slope); } // create intersection points (to set line ends) Point2D p1 = new Point2D.Double(); Point2D p2 = new Point2D.Double(Double.NaN, Double.NaN); Point2D p = p1; boolean foundBoth = false; double d = searchRect.x; Object[] data = getDistanceAndPointAtX(line, d); if (data!=null) { p.setLocation((Point2D)data[1]); if (p.getY()>=searchRect.y && p.getY()<=searchRect.y+searchRect.height) { // line end is left edge p = p2; } } d += searchRect.width; data = getDistanceAndPointAtX(line, d); if (data!=null) { p.setLocation((Point2D)data[1]); if (p.getY()>=searchRect.y && p.getY()<=searchRect.y+searchRect.height) { // line end is right edge if (p==p1) p = p2; else foundBoth = true; } } if (!foundBoth) { d = searchRect.y; data = getDistanceAndPointAtY(line, d); if (data!=null) { p.setLocation((Point2D)data[1]); if (p.getX()>=searchRect.x && p.getX()<=searchRect.x+searchRect.width) { // line end is top edge if (p==p1) p = p2; else if (!p1.equals(p2)) foundBoth = true; } } } if (!foundBoth) { d += searchRect.height; data = getDistanceAndPointAtY(line, d); if (data!=null) { p.setLocation((Point2D)data[1]); if (p.getX()>=searchRect.x && p.getX()<=searchRect.x+searchRect.width) { // line end is bottom edge if (p==p2 && !p1.equals(p2)) foundBoth = true; } } } // if both line ends have been found, use line to find pixels to search if (foundBoth) { // set line ends to intersections line.setLine(p1, p2); if (p1.getX()>p2.getX()) { line.setLine(p2, p1); } // find pixel intersections that fall along the line int xMin = (int)Math.ceil(Math.min(p1.getX(), p2.getX())); int xMax = (int)Math.floor(Math.max(p1.getX(), p2.getX())); int yMin = (int)Math.ceil(Math.min(p1.getY(), p2.getY())); int yMax = (int)Math.floor(Math.max(p1.getY(), p2.getY())); // collect intersections in TreeMap sorted by position along line TreeMap<Double, Point2D> intersections = new TreeMap<Double, Point2D>(); for (int x = xMin; x <= xMax; x++) { Object[] next = getDistanceAndPointAtX(line, x); if (next==null) continue; intersections.put((Double)next[0], (Point2D)next[1]); } for (int y = yMin; y <= yMax; y++) { Object[] next = getDistanceAndPointAtY(line, y); if (next==null) continue; intersections.put((Double)next[0], (Point2D)next[1]); } p = null; // create array of search points that are midway between intersections ArrayList<Point2D> searchPts = new ArrayList<Point2D>(); for (Double key: intersections.keySet()) { Point2D next = intersections.get(key); if (p!=null) { double x = (p.getX()+next.getX())/2 - searchRect.x; double y = (p.getY()+next.getY())/2 - searchRect.y; p.setLocation(x, y); searchPts.add(p); } p = next; } return searchPts; } return null; } /** * Gets the distance and point along a Line2D at a specified x. * If the Line2D is vertical this returns null. * * Based on a simplification of algorithm described by Paul Burke * at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/ (April 1986) * * @param line the line * @param x the value of x * @return Object[] {fractional distance from line end, Point2D} */ private Object[] getDistanceAndPointAtX(Line2D line, double x) { double dx = line.getX2()-line.getX1(); // if line is vertical, return null if (dx==0) return null; // parametric eqn of line: P = P1 + u(P2 - P1) double u = (x-line.getX1())/dx; double y = line.getY1() + u*(line.getY2()-line.getY1()); return new Object[] {u, new Point2D.Double(x, y)}; } /** * Gets the distance and point along a Line2D at a specified y. * If the Line2D is horizontal this returns null. * * Based on a simplification of algorithm described by Paul Burke * at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/ (April 1986) * * @param line the line * @param y the value of y * @return Object[] {fractional distance from line end, Point2D} */ private Object[] getDistanceAndPointAtY(Line2D line, double y) { double dy = line.getY2()-line.getY1(); // if line is horizontal, return null if (dy==0) return null; // parametric eqn of line: P = P1 + u(P2 - P1) double u = (y-line.getY1())/dy; double x = line.getX1() + u*(line.getX2()-line.getX1()); return new Object[] {u, new Point2D.Double(x, y)}; } /** * Gets the total difference between the template and test pixels * at a specified test point. The test point is the point on the test * image where the top left corner of the template is located. * * @param x the test point x-component * @param y the test point y-component */ private double getDifferenceAtTestPoint(int x, int y) { // for each pixel in template, get difference from corresponding test pixel // return sum of these differences double diff = 0; for (int i = 0; i < wTemplate; i++) { for (int j = 0; j < hTemplate; j++) { int templateIndex = j*wTemplate+i; int testIndex = (y+j)*wTest+x+i; if (testIndex < 0 || testIndex >= targetPixels.length) return Double.NaN; // may occur when doing Gaussian fit if (!isPixelTransparent[templateIndex]) { // include only non-transparent pixels int pixel = targetPixels[testIndex]; diff += getRGBDifference(pixel, templateR[templateIndex], templateG[templateIndex], templateB[templateIndex]); } } } return diff; } /** * Gets the difference between a pixel and a comparison set of rgb components. */ private double getRGBDifference(int pixel, int r, int g, int b) { int rPix = (pixel >> 16) & 0xff; // red int gPix = (pixel >> 8) & 0xff; // green int bPix = (pixel ) & 0xff; // blue int dr = r-rPix; int dg = g-gPix; int db = b-bPix; return dr*dr + dg*dg + db*db; // sum of squares of rgb differences } /** * Gets an AlphaComposite object with a specified alpha value. */ private AlphaComposite getComposite(int alpha) { float a = 1.0f*alpha/255; return AlphaComposite.getInstance(AlphaComposite.SRC_OVER, a); } }