/**
   * Returns the specified property.
   *
   * @param name Property name.
   * @param opNode Operation node.
   */
  public Object getProperty(String name, Object opNode) {
    validate(name, opNode);

    if (opNode instanceof RenderedOp && name.equalsIgnoreCase("roi")) {
      RenderedOp op = (RenderedOp) opNode;

      ParameterBlock pb = op.getParameterBlock();

      // Retrieve the rendered source image and its ROI.
      PlanarImage src = (PlanarImage) pb.getRenderedSource(0);
      Object property = src.getProperty("ROI");
      if (property == null
          || property.equals(java.awt.Image.UndefinedProperty)
          || !(property instanceof ROI)) {
        return java.awt.Image.UndefinedProperty;
      }

      // Return undefined also if source ROI is empty.
      ROI srcROI = (ROI) property;
      if (srcROI.getBounds().isEmpty()) {
        return java.awt.Image.UndefinedProperty;
      }

      /// This should really create a proper AffineTransform
      /// and transform the ROI with it to avoid forcing
      /// ROI.getAsImage to be called.

      // Retrieve the transpose type and create a nearest neighbor
      // Interpolation object.
      TransposeType transposeType = (TransposeType) pb.getObjectParameter(0);
      Interpolation interp = Interpolation.getInstance(Interpolation.INTERP_NEAREST);

      // Return the transposed ROI.
      return new ROI(JAI.create("transpose", srcROI.getAsImage(), transposeType));
    }

    return java.awt.Image.UndefinedProperty;
  }
  private synchronized void computeHistogram(Rectangle visibleRect, PlanarImage image) {
    int channels = image.getSampleModel().getNumBands();

    Histogram hist = new Histogram(256, 256, 512, channels);

    bins = hist.getBins();

    // Raster raster = image.getData(visibleRect);
    Rectangle bounds = visibleRect; // image.getBounds();
    int pixel[] = null;

    int maxPixels = 256;
    int incX = bounds.width >= 2 * maxPixels ? bounds.width / maxPixels : 1;
    int incY = bounds.height >= 2 * maxPixels ? bounds.height / maxPixels : 1;

    double log2 = Math.log(2);

    int minTileX = image.XToTileX(bounds.x);
    int maxTileX = image.XToTileX(bounds.x + bounds.width - 1);
    int minTileY = image.YToTileY(bounds.y);
    int maxTileY = image.YToTileY(bounds.y + bounds.height - 1);

    for (int tx = minTileX; tx <= maxTileX; tx++)
      for (int ty = minTileY; ty <= maxTileY; ty++) {
        Raster raster = image.getTile(tx, ty);

        int minX = Math.max(bounds.x, raster.getMinX());
        int maxX = Math.min(bounds.x + bounds.width, raster.getMinX() + raster.getWidth());
        int minY = Math.max(bounds.y, raster.getMinY());
        int maxY = Math.min(bounds.y + bounds.height, raster.getMinY() + raster.getHeight());

        for (int x = minX; x < maxX; x += incX)
          for (int y = minY; y < maxY; y += incY) {
            pixel = raster.getPixel(x, y, pixel);
            for (int c = 0; c < channels; c++) {
              int v = (int) (511 * logTable[pixel[c]] / (16 * log2));
              if (v > 255) bins[c][v - 256]++;
              else bins[c][0]++;
            }
          }
      }

    bins = hist.getBins();
  }
    public PlanarImage setFront() {
      if (chroma_domain == 0 && chroma_range == 0 && luma_domain == 0 && luma_range == 0)
        return back;

      PlanarImage front = back;

      ColorScience.LinearTransform transform = new ColorScience.YST();

      double[][] rgb2yst = transform.fromRGB(back.getSampleModel().getDataType());
      double[][] yst2rgb = transform.toRGB(back.getSampleModel().getDataType());

      ParameterBlock pb = new ParameterBlock();
      pb.addSource(back);
      pb.add(rgb2yst);
      RenderedOp ystImage = JAI.create("BandCombine", pb, null);

      RenderingHints mfHints =
          new RenderingHints(
              JAI.KEY_BORDER_EXTENDER, BorderExtender.createInstance(BorderExtender.BORDER_COPY));

      if (chroma_domain != 0 && chroma_range != 0) {
        pb = new ParameterBlock();
        pb.addSource(ystImage);
        pb.add(chroma_domain * scale);
        pb.add(0.02f + 0.001f * chroma_domain);
        // pb.add(0.1f);
        ystImage = JAI.create("BilateralFilter", pb, mfHints);
        ystImage.setProperty(JAIContext.PERSISTENT_CACHE_TAG, Boolean.TRUE);
      }

      if (luma_domain != 0 && luma_range != 0) {
        pb = new ParameterBlock();
        pb.addSource(ystImage);
        pb.add(new int[] {0});
        RenderedOp y = JAI.create("bandselect", pb, null);

        pb = new ParameterBlock();
        pb.addSource(ystImage);
        pb.add(new int[] {1, 2});
        RenderedOp cc = JAI.create("bandselect", pb, JAIContext.noCacheHint);

        pb = new ParameterBlock();
        pb.addSource(y);
        pb.add((2 + luma_domain / 10f) * scale);
        pb.add(0.005f * luma_domain);
        y = JAI.create("BilateralFilter", pb, mfHints);

        RenderingHints layoutHints =
            new RenderingHints(JAI.KEY_IMAGE_LAYOUT, Functions.getImageLayout(ystImage));
        pb = new ParameterBlock();
        pb.addSource(y);
        pb.addSource(cc);
        layoutHints.add(JAIContext.noCacheHint);
        ystImage = JAI.create("BandMerge", pb, layoutHints);
      }

      pb = new ParameterBlock();
      pb.addSource(ystImage);
      pb.add(yst2rgb);
      front = JAI.create("BandCombine", pb, null);
      front.setProperty(JAIContext.PERSISTENT_CACHE_TAG, Boolean.TRUE);

      return front;
    }