/**
   * 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);
  }
  /**
   * 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);
  }
 /**
  * Rebuilds the template from an input image. The input image dimensions must match the original.
  * The input and original are overlaid onto the existing template, if any. Pixels that fall
  * outside the mask are ignored in the final template.
  *
  * @param image the input image
  * @param alphaInput the opacity with which the input image is overlaid
  * @param alphaOriginal the opacity with which the original image is overlaid
  */
 public void rebuildTemplate(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;
   // return if both alphas are zero
   if (alphaInput == 0 && alphaOriginal == 0) return;
   // draw image onto argb input
   BufferedImage 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];
   }
   // 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));
     input.getRaster().getDataElements(0, 0, w, h, pixels);
     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));
     original.getRaster().getDataElements(0, 0, w, h, pixels);
     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);
     pixels = new int[wTemplate * hTemplate];
     templateR = new int[wTemplate * hTemplate];
     templateG = new int[wTemplate * hTemplate];
     templateB = new int[wTemplate * hTemplate];
     isPixelTransparent = new boolean[wTemplate * hTemplate];
     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);
   }
   // set up rgb and transparency arrays for faster 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
   }
 }