ColorScience.YST yst = new ColorScience.YST();
double[][] rgb2yst = yst.fromRGB(back.getSampleModel().getDataType());
double[][] yst2rgb = yst.toRGB(back.getSampleModel().getDataType());
ParameterBlock pb = new ParameterBlock();
pb.addSource( back );
pb.add( rgb2yst );
RenderedOp ystImage = JAI.create("BandCombine", pb, JAIContext.noCacheHint);
pb = new ParameterBlock();
pb.addSource(ystImage);
pb.add(new int[]{0});
RenderedOp y = JAI.create("bandselect", pb, JAIContext.noCacheHint);
pb = new ParameterBlock();
pb.addSource(ystImage);
pb.add(new int[]{1, 2});
// NOTE: we cache this because the median filter is an area op that gets its input multiple times
RenderedOp cc = JAI.create("bandselect", pb, null);
RenderingHints mfHints = new RenderingHints(JAI.KEY_BORDER_EXTENDER, BorderExtender.createInstance(BorderExtender.BORDER_COPY));
mfHints.add(JAIContext.noCacheHint);
pb = new ParameterBlock();
pb.addSource(cc);
pb.add(MedianFilterDescriptor.MEDIAN_MASK_SQUARE); // X Shape seems to give the least artifacts
pb.add(new Integer(Math.max(2 * (int) (denoiseLevel * scale) + 1, 3)));
denoiser = JAI.create("MedianFilter", pb, mfHints);
RenderingHints layoutHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, Functions.getImageLayout(ystImage));
pb = new ParameterBlock();
pb.addSource(y);
pb.addSource(denoiser);
layoutHints.add(JAIContext.noCacheHint);
RenderedOp denoisedyst = JAI.create("BandMerge", pb, layoutHints);
pb = new ParameterBlock();
pb.addSource( denoisedyst );
pb.add( yst2rgb );
return JAI.create("BandCombine", pb, JAIContext.noCacheHint);
}