/**
  * 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) {
   original = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
   original.createGraphics().drawImage(image, 0, 0, null);
   mask = maskShape;
   getTemplate();
   // set up the Gaussian curve fitter
   dataset = new Dataset();
   fitter = new DatasetCurveFitter(dataset);
   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$
 }
  /**
   * 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 slope the slope 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) {
      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) {
      peakHeight = Double.NaN;
      peakWidth = Double.NaN;
      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);
    double xImage = xMatch + searchRect.x - left - trimLeft + dx;
    double yImage = yMatch + searchRect.y - top - trimTop + dy;
    return new TPoint(xImage, yImage);
  }
  /**
   * Gets the template location at which the best match occurs in a rectangle. May return null.
   *
   * @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, dx);
          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;
      }
    }
    double xImage = xMatch + searchRect.x - left - trimLeft + dx;
    double yImage = yMatch + searchRect.y - top - trimTop + dy;
    return new TPoint(xImage, yImage);
  }