/* JAI-Ext - OpenSource Java Advanced Image Extensions Library * http://www.geo-solutions.it/ * Copyright 2014 GeoSolutions * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package it.geosolutions.jaiext.errordiffusion; import it.geosolutions.jaiext.iterators.RandomIterFactory; import it.geosolutions.jaiext.range.Range; import it.geosolutions.jaiext.range.RangeFactory; import java.awt.Rectangle; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.awt.image.MultiPixelPackedSampleModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.util.Map; import javax.media.jai.ColorCube; import javax.media.jai.ImageLayout; import javax.media.jai.KernelJAI; import javax.media.jai.LookupTableJAI; import javax.media.jai.PlanarImage; import javax.media.jai.ROI; import javax.media.jai.ROIShape; import javax.media.jai.RasterAccessor; import javax.media.jai.RasterFactory; import javax.media.jai.RasterFormatTag; import javax.media.jai.UntiledOpImage; import javax.media.jai.iterator.RandomIter; import com.sun.media.jai.util.ImageUtil; import com.sun.media.jai.util.JDKWorkarounds; /** * An <code>OpImage</code> implementing the error diffusion operation as described in <code>ErrorDiffusionDescriptor</code>. * * <p> * This <code>OpImage</code> performs dithering of its source image into a single band image using a specified color map and error filter. For each * pixel in the source image the nearest entry in the color map is found and the index of this entry is assigned to the <code>OpImage</code> at that * location. The color quantization error is calculated by mapping the index back through the color map. The error in each band is then "diffused" to * other neighboring pixels in the source image according to the specified error filter. * * <p> * Optionally users may define a ROI and a NoData Range in order to reduce computation area or mask invalid pixel values.S */ public class ErrorDiffusionOpImage extends UntiledOpImage { /** * Constant indicating that the inner random iterators must pre-calculate an array of the image positions */ public static final boolean ARRAY_CALC = true; /** * Constant indicating that the inner random iterators must cache the current tile position */ public static final boolean TILE_CACHED = true; /** * Smallest float value which when added to unity will yield something other than unity. */ private static final float FLOAT_EPSILON = 1.192092896E-07F; /** * Variables used in the optimized case of 3-band byte to 1-band byte with a ColorCube color map and a Floyd-Steinberg kernel. */ private static final int NBANDS = 3; private static final int NGRAYS = 256; private static final int OVERSHOOT = 256; private static final int UNDERSHOOT = 256; private static final int TOTALGRAYS = (NGRAYS + UNDERSHOOT + OVERSHOOT); private static final int ERR_SHIFT = 8; /** * The color map which maps the <code>ErrorDiffusionOpImage</code> to its source. */ protected LookupTableJAI colorMap; /** * The kernel associated with the selected error filter. */ protected KernelJAI errorKernel; /** * The number of bands in the source image. */ private int numBandsSource; /** * Flag indicating whether this is an optimized case. */ private boolean isOptimizedCase = false; /** * Minimum valid pixel value */ private float minPixelValue; /** * Maximum valid pixel value */ private float maxPixelValue; /** Boolean indicating if ROI is present */ private final boolean hasROI; /** Boolean indicating if NoData Range is present */ private final boolean hasNodata; /** NoData Range used for checking the input NoData */ private Range nodata; /** Rectangle containing the bounds for the input ROI */ private Rectangle roiBounds; /** Input ROI used for reducing calculation area */ private ROI roi; /** Integer used as output NoData value */ private int destNoData; /** Boolean indicating if No Data and ROI are not used */ private boolean caseA; /** Boolean indicating if only the ROI is used */ private boolean caseB; /** Boolean indicating if only the No Data are used */ private boolean caseC; /** {@link PlanarImage} containing ROI data */ private PlanarImage roiImage; /** LookupTable used for having a quick check if a pixel is NoData or not */ private boolean[] lookupTable; /** * Determines whether a kernel is the Floyd-Steinberg kernel. * * @param kernel The <code>KernelJAI</code> to examine. * @return Whether the kernel argument is the Floyd-Steinberg kernel. */ private static boolean isFloydSteinbergKernel(KernelJAI kernel) { int ky = kernel.getYOrigin(); return (kernel.getWidth() == 3 && kernel.getXOrigin() == 1 && kernel.getHeight() - ky == 2 && Math.abs(kernel.getElement(2, ky) - 7.0F / 16.0F) < FLOAT_EPSILON && Math.abs(kernel.getElement(0, ky + 1) - 3.0F / 16.0F) < FLOAT_EPSILON && Math.abs(kernel.getElement(1, ky + 1) - 5.0F / 16.0F) < FLOAT_EPSILON && Math .abs(kernel.getElement(2, ky + 1) - 1.0F / 16.0F) < FLOAT_EPSILON); } /** * Create the dither table for the 3-band to 1-band byte optimized case. * * @param colorCube The color cube to be used in dithering. * @return The dither table of the optimized algorithm. */ private static int[] initFloydSteinberg24To8(ColorCube colorCube) { // Allocate memory for the dither table. int[] ditherTable = new int[NBANDS * TOTALGRAYS]; float[] thresh = new float[NGRAYS]; // // Get the colorcube parameters // int[] multipliers = colorCube.getMultipliers(); int[] dimsLessOne = colorCube.getDimsLessOne(); int offset = colorCube.getAdjustedOffset(); // // Construct tables for each band // for (int band = 0; band < NBANDS; band++) { int pTab = band * TOTALGRAYS; // // Calculate the binwidth for this band, i.e. the gray level step // from one quantization level to the next. Do this in scaled // integer to maintain precision. // float binWidth = 255.0F / dimsLessOne[band]; // // Pre-calculate the thresholds, so we don't have to do // it in the inner loops. The threshold is always the // midpoint of each bin, since, in error diffusion, the dithering // is done by the error distribution process, not by varying // the dither threshold as in ordered dither. // for (int i = 0; i < dimsLessOne[band]; i++) { thresh[i] = (i + 0.5F) * binWidth; } thresh[dimsLessOne[band]] = 256.0F; // // Populate the range below gray level zero with the same entry // as that for zero. The error distribution can cause undershoots // of as much as 255. // int tableInc = 1 << ERR_SHIFT; int tableValue = (-UNDERSHOOT) << ERR_SHIFT; for (int gray = -UNDERSHOOT; gray < 0; gray++) { ditherTable[pTab++] = tableValue; tableValue += tableInc; } // // Populate the main range of 0...255. // int indexContrib = 0; float frepValue = 0.0F; int repValue; int binNum = 0; float threshold = thresh[0]; int gray = 0; while (gray < 256) { // // Populate all the table values up to the next threshold. // Since the only thing which changes is the error, // and it changes by one scaled gray level, we can // just add the increment at each iteration. // int tableBase = indexContrib; repValue = (int) (frepValue + 0.5F); while ((float) gray < threshold) { ditherTable[pTab++] = ((gray - repValue) << ERR_SHIFT) + tableBase; gray++; } // // Once the gray level crosses a threshold, // move to the next bin threshold. Also update // the color contribution index step and the // representative value, needed to compute the error. // threshold = thresh[++binNum]; indexContrib += multipliers[band]; frepValue += binWidth; } // // Populate the range above gray level 255 with the same entry // as that for 255. As in the under-range case, the error // distribution can cause overshoots as high as 255 over max. // indexContrib -= multipliers[band]; repValue = 255; tableValue = ((256 - repValue) << ERR_SHIFT) | indexContrib; for (gray = 256; gray < (256 + OVERSHOOT); gray++) { ditherTable[pTab++] = tableValue; tableValue += tableInc; } } // End band loop // // Add in the colormap offset value to the index contribution // for the first band. This eliminates the need to add it in // when we do the error diffusion. // int pTab = 0; for (int count = TOTALGRAYS; count != 0; count--) { ditherTable[pTab] += offset; pTab++; } return ditherTable; } /** * Force the destination image to be single-banded. */ private static ImageLayout layoutHelper(ImageLayout layout, RenderedImage source, LookupTableJAI colorMap) { // Create or clone the layout. ImageLayout il = layout == null ? new ImageLayout() : (ImageLayout) layout.clone(); // Force the destination and source origins and dimensions to coincide. il.setMinX(source.getMinX()); il.setMinY(source.getMinY()); il.setWidth(source.getWidth()); il.setHeight(source.getHeight()); // Get the SampleModel. SampleModel sm = il.getSampleModel(source); // Ensure an appropriate SampleModel. if (colorMap.getNumBands() == 1 && colorMap.getNumEntries() == 2 && !ImageUtil.isBinary(il.getSampleModel(source))) { sm = new MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE, il.getTileWidth(source), il.getTileHeight(source), 1); il.setSampleModel(sm); } // Make sure that this OpImage is single-banded. if (sm.getNumBands() != 1) { sm = RasterFactory.createComponentSampleModel(sm, sm.getTransferType(), sm.getWidth(), sm.getHeight(), 1); il.setSampleModel(sm); // Clear the ColorModel mask if needed. ColorModel cm = il.getColorModel(null); if (cm != null && !JDKWorkarounds.areCompatibleDataModels(sm, cm)) { // Clear the mask bit if incompatible. il.unsetValid(ImageLayout.COLOR_MODEL_MASK); } } // Determine whether a larger bit depth is needed. int numColorMapBands = colorMap.getNumBands(); int maxIndex = 0; for (int i = 0; i < numColorMapBands; i++) { maxIndex = Math.max(colorMap.getOffset(i) + colorMap.getNumEntries() - 1, maxIndex); } // Create a deeper SampleModel if needed. if ((maxIndex > 255 && sm.getDataType() == DataBuffer.TYPE_BYTE) || (maxIndex > 65535 && sm.getDataType() != DataBuffer.TYPE_INT)) { int dataType = maxIndex > 65535 ? DataBuffer.TYPE_INT : DataBuffer.TYPE_USHORT; sm = RasterFactory.createComponentSampleModel(sm, dataType, sm.getWidth(), sm.getHeight(), 1); il.setSampleModel(sm); // Clear the ColorModel mask if needed. ColorModel cm = il.getColorModel(null); if (cm != null && !JDKWorkarounds.areCompatibleDataModels(sm, cm)) { // Clear the mask bit if incompatible. il.unsetValid(ImageLayout.COLOR_MODEL_MASK); } } // Set an IndexColorModel on the image if: // a. none is provided in the layout; // b. source and colormap have byte data type; // c. the colormap has 3 bands; // d. destination has byte or ushort data type. if ((layout == null || !il.isValid(ImageLayout.COLOR_MODEL_MASK)) && source.getSampleModel().getDataType() == DataBuffer.TYPE_BYTE && (sm.getDataType() == DataBuffer.TYPE_BYTE || sm.getDataType() == DataBuffer.TYPE_USHORT) && colorMap.getDataType() == DataBuffer.TYPE_BYTE && colorMap.getNumBands() == 3) { ColorModel cm = source.getColorModel(); if (cm == null || (cm != null && cm.getColorSpace().isCS_sRGB())) { int size = colorMap.getNumEntries(); byte[][] cmap = new byte[3][maxIndex + 1]; for (int i = 0; i < 3; i++) { byte[] band = cmap[i]; byte[] data = colorMap.getByteData(i); int offset = colorMap.getOffset(i); int end = offset + size; for (int j = offset; j < end; j++) { band[j] = data[j - offset]; } } int numBits = sm.getDataType() == DataBuffer.TYPE_BYTE ? 8 : 16; il.setColorModel(new IndexColorModel(numBits, maxIndex + 1, cmap[0], cmap[1], cmap[2])); } } return il; } /** * Constructs an ErrorDiffusionOpImage object. * * <p> * The image dimensions are derived from the source image. The tile grid layout, SampleModel, and ColorModel may optionally be specified by an * ImageLayout object. The calculation assumes that the entire color quantization error is distributed to the right and below the current pixel * and the filter kernel values are handled appropriately. * * @param source A RenderedImage. * @param layout An ImageLayout optionally containing the tile grid layout, SampleModel, and ColorModel, or null. * @param colorMap The color map to use which must have a number of bands equal to the number of bands in the source image. The offset of this * <code>LookupTableJAI</code> must be the same for all bands. * @param errorKernel The error filter kernel. This must have values between 0.0 and 1.0. Only the entries to the right of and on the same row as * the key entry, and those entries below of the row of the key entry are used; all other values are ignored. The values used must sum to * 1.0. Note that if a 1-by-1 error filter kernel is supplied, the value of the unique kernel element is irrelevant and the output of the * algorithm will simply be the index in the supplied color map of the nearest matching color to the source pixel at the same position. */ public ErrorDiffusionOpImage(RenderedImage source, Map config, ImageLayout layout, LookupTableJAI colorMap, KernelJAI errorKernel, ROI roi, Range nodata, int destNoData) { super(source, config, layoutHelper(layout, source, colorMap)); // Get the source sample model. SampleModel srcSampleModel = source.getSampleModel(); // Cache the number of bands in the source. numBandsSource = srcSampleModel.getNumBands(); // Set a reference to the LookupTableJAI. this.colorMap = colorMap; // Set a reference to the KernelJAI. this.errorKernel = errorKernel; // Checking ROI hasROI = roi != null; if (hasROI) { this.roi = roi; this.roiBounds = roi.getBounds(); } // Checking NoData hasNodata = nodata != null; if (hasNodata) { this.nodata = RangeFactory.convertToFloatRange(nodata); } // Definition of the possible cases that can be found // caseA = no ROI nor No Data // caseB = ROI present but No Data not present // caseC = No Data present but ROI not present // Last case not defined = both ROI and No Data are present caseA = !hasROI && !hasNodata; caseB = hasROI && !hasNodata; caseC = !hasROI && hasNodata; // Check if the nodata is defined in the ColorMap before setting it if (colorMap.getNumEntries() <= destNoData || destNoData < 0) { throw new IllegalArgumentException("Wrong index defined"); } else { this.destNoData = destNoData; } // Determine whether this is an (read "the") optimized case. isOptimizedCase = (sampleModel.getTransferType() == DataBuffer.TYPE_BYTE && srcSampleModel.getTransferType() == DataBuffer.TYPE_BYTE && numBandsSource == 3 && colorMap instanceof ColorCube && isFloydSteinbergKernel(errorKernel)); // Determine minumum and maximum valid pixel values switch (colorMap.getDataType()) { case DataBuffer.TYPE_BYTE: // Treat byte types as unsigned bytes minPixelValue = 0; maxPixelValue = -Byte.MIN_VALUE + Byte.MAX_VALUE; break; case DataBuffer.TYPE_SHORT: minPixelValue = Short.MIN_VALUE; maxPixelValue = Short.MAX_VALUE; break; case DataBuffer.TYPE_USHORT: minPixelValue = 0; maxPixelValue = -Short.MIN_VALUE + Short.MAX_VALUE; break; case DataBuffer.TYPE_INT: minPixelValue = Integer.MIN_VALUE; maxPixelValue = Integer.MAX_VALUE; break; case DataBuffer.TYPE_FLOAT: case DataBuffer.TYPE_DOUBLE: minPixelValue = 0; maxPixelValue = Float.MAX_VALUE; break; default: throw new RuntimeException(JaiI18N.getString("ErrorDiffusionOpImage0")); } // If we use the optimized case and NoData are present, we init the LookupTable for NoData check if (isOptimizedCase && hasNodata) { initLookupTable(nodata); } } private void initLookupTable(Range nodata) { // Convert the Range to Byte Range Range nd = RangeFactory.convertToByteRange(nodata); // Init the Boolean LookupTable lookupTable = new boolean[256]; // Init the lookuptable containing for (int i = 0; i < lookupTable.length; i++) { byte b = (byte) i; lookupTable[i] = !nd.contains(b); } } /** * Performs error diffusion on a specified rectangle. The sources are cobbled. As error diffusion must be calculated on a line-by-line basis * starting at the upper left corner of the image, all image lines through and including the last line of the tile containing the requested * <code>Rectangle</code> are calculated. * * @param sources The source image Raster. * @param dest A WritableRaster tile containing the area to be computed. * @param destRect The rectangle within dest to be processed. */ protected void computeImage(Raster[] sources, WritableRaster dest, Rectangle destRect) { Raster source = sources[0]; // ROI check ROI roiTile = null; RandomIter roiIter = null; boolean roiContainsTile = false; boolean roiDisjointTile = false; // If a ROI is present, then only the part contained inside the current // tile bounds is taken. if (hasROI) { Rectangle srcRectExpanded = mapDestRect(destRect, 0); // The tile dimension is extended for avoiding border errors srcRectExpanded.setRect(srcRectExpanded.getMinX() - 1, srcRectExpanded.getMinY() - 1, srcRectExpanded.getWidth() + 2, srcRectExpanded.getHeight() + 2); roiTile = roi.intersect(new ROIShape(srcRectExpanded)); if (!roiBounds.intersects(srcRectExpanded)) { roiDisjointTile = true; } else { roiContainsTile = roiTile.contains(srcRectExpanded); if (!roiContainsTile) { if (!roiTile.intersects(srcRectExpanded)) { roiDisjointTile = true; } else { PlanarImage roiIMG = getImage(); roiIter = RandomIterFactory.create(roiIMG, null, TILE_CACHED, ARRAY_CALC); } } } } // Image completely outside ROI, fill with the background value if (roiDisjointTile) { ImageUtil.fillBackground(dest, destRect, new double[] { destNoData }); return; } if (isOptimizedCase) { computeImageOptimized(source, dest, destRect, roiIter, roiContainsTile); } else { computeImageDefault(source, dest, destRect, roiIter, roiContainsTile); } } protected void computeImageDefault(Raster source, WritableRaster dest, Rectangle destRect, RandomIter roiIter, boolean roiContainsTile) { // Set X-coordinate range. int startX = minX; int endX = startX + width - 1; // Set Y-coordinate range. int startY = minY; int endY = startY + height - 1; // Set the number of lines in the calculation buffer. int numLinesBuffer = errorKernel.getHeight() - errorKernel.getYOrigin(); // Allocate memory for the calculation buffer. float[][] bufMem = new float[numLinesBuffer][width * numBandsSource]; float[][] bufNoData = new float[numLinesBuffer][width * numBandsSource]; // Allocate memory for the buffer index array. int[] bufIdx = new int[numLinesBuffer]; // Initialize the buffer index array and the rolling buffer. for (int idx = 0; idx < numLinesBuffer; idx++) { bufIdx[idx] = idx; source.getPixels(startX, startY + idx, width, 1, bufMem[idx]); source.getPixels(startX, startY + idx, width, 1, bufNoData[idx]); } // Set variable to indicate index of last rolling buffer line. int lastLineBuffer = numLinesBuffer - 1; // Initialize some kernel-dependent constants. int kernelWidth = errorKernel.getWidth(); float[] kernelData = errorKernel.getKernelData(); int diffuseRight = kernelWidth - errorKernel.getXOrigin() - 1; int diffuseBelow = errorKernel.getHeight() - errorKernel.getYOrigin() - 1; int kernelOffsetRight = errorKernel.getYOrigin() * kernelWidth + errorKernel.getXOrigin() + 1; int kernelOffsetBelow = (errorKernel.getYOrigin() + 1) * kernelWidth; // Set up some arrays for looping. float[] currentPixel = new float[numBandsSource]; float[] currentPixelReal = new float[numBandsSource]; float[] qError = new float[numBandsSource]; // Loop over lines. int[] dstData = new int[width]; if (caseA || (caseB && roiContainsTile)) { for (int y = startY; y <= endY; y++) { int currentIndex = bufIdx[0]; float[] currentLine = bufMem[currentIndex]; // Loop over pixels. int dstOffset = 0; for (int x = startX, z = 0; x <= endX; x++) { // Copy all samples of the current pixel. for (int b = 0; b < numBandsSource; b++) { currentPixel[b] = currentLine[z++]; // Clamp the current sample to the valid range if (currentPixel[b] < minPixelValue || currentPixel[b] > maxPixelValue) { currentPixel[b] = java.lang.Math.max(currentPixel[b], minPixelValue); currentPixel[b] = java.lang.Math.min(currentPixel[b], maxPixelValue); } } // Find the index of the nearest color in the map. int nearestIndex = colorMap.findNearestEntry(currentPixel); // Save the index in the output data buffer. dstData[dstOffset++] = nearestIndex; // Calculate the error between the nearest and actual // colors. boolean isQuantizationError = false; for (int b = 0; b < numBandsSource; b++) { qError[b] = currentPixel[b] - colorMap.lookupFloat(b, nearestIndex); if (qError[b] != 0.0F) { isQuantizationError = true; } } // If there was error in at least one band, distribute it. if (isQuantizationError) { // Distribute error to the right of key entry. int rightCount = Math.min(diffuseRight, endX - x); int kernelOffset = kernelOffsetRight; int sampleOffset = z; for (int u = 1; u <= rightCount; u++) { for (int b = 0; b < numBandsSource; b++) { currentLine[sampleOffset++] += qError[b] * kernelData[kernelOffset]; } kernelOffset++; } // Distribute error below key entry. int offsetLeft = Math.min(x - startX, diffuseRight); int count = Math.min(x + diffuseRight, endX) - Math.max(x - diffuseRight, startX) + 1; for (int v = 1; v <= diffuseBelow; v++) { float[] line = bufMem[bufIdx[v]]; kernelOffset = kernelOffsetBelow; sampleOffset = z - (offsetLeft + 1) * numBandsSource; for (int u = 1; u <= count; u++) { for (int b = 0; b < numBandsSource; b++) { line[sampleOffset++] += qError[b] * kernelData[kernelOffset]; } kernelOffset++; } } } } // // Save data for the current destination line. // dest.setSamples(startX, y, destRect.width, 1, 0, dstData); // Rotate the buffer indexes. for (int k = 0; k < lastLineBuffer; k++) { bufIdx[k] = bufIdx[k + 1]; } bufIdx[lastLineBuffer] = currentIndex; // If available, load next image line into the last buffer line. if (y + numLinesBuffer < getMaxY()) { source.getPixels(startX, y + numLinesBuffer, width, 1, bufMem[bufIdx[lastLineBuffer]]); } } } else if (caseB) { for (int y = startY; y <= endY; y++) { int currentIndex = bufIdx[0]; float[] currentLine = bufMem[currentIndex]; // Loop over pixels. int dstOffset = 0; for (int x = startX, z = 0; x <= endX; x++) { // Copy all samples of the current pixel. for (int b = 0; b < numBandsSource; b++) { currentPixel[b] = currentLine[z++]; // Clamp the current sample to the valid range if (currentPixel[b] < minPixelValue || currentPixel[b] > maxPixelValue) { currentPixel[b] = java.lang.Math.max(currentPixel[b], minPixelValue); currentPixel[b] = java.lang.Math.min(currentPixel[b], maxPixelValue); } } // Find the index of the nearest color in the map. int nearestIndex = colorMap.findNearestEntry(currentPixel); // Check against ROI boolean inROI = inROI(roiIter, y, x); // Save the index in the output data buffer. int finalIndex = inROI ? nearestIndex : destNoData; dstData[dstOffset++] = finalIndex; // Calculate the error between the nearest and actual // colors. boolean isQuantizationError = false; for (int b = 0; b < numBandsSource; b++) { qError[b] = currentPixel[b] - colorMap.lookupFloat(b, nearestIndex); if (qError[b] != 0.0F) { isQuantizationError = true; } } // If there was error in at least one band, distribute it. if (isQuantizationError && inROI) { // Distribute error to the right of key entry. int rightCount = Math.min(diffuseRight, endX - x); int kernelOffset = kernelOffsetRight; int sampleOffset = z; for (int u = 1; u <= rightCount; u++) { for (int b = 0; b < numBandsSource; b++) { currentLine[sampleOffset++] += qError[b] * kernelData[kernelOffset]; } kernelOffset++; } // Distribute error below key entry. int offsetLeft = Math.min(x - startX, diffuseRight); int count = Math.min(x + diffuseRight, endX) - Math.max(x - diffuseRight, startX) + 1; for (int v = 1; v <= diffuseBelow; v++) { float[] line = bufMem[bufIdx[v]]; kernelOffset = kernelOffsetBelow; sampleOffset = z - (offsetLeft + 1) * numBandsSource; for (int u = 1; u <= count; u++) { for (int b = 0; b < numBandsSource; b++) { line[sampleOffset++] += qError[b] * kernelData[kernelOffset]; } kernelOffset++; } } } } // // Save data for the current destination line. // dest.setSamples(startX, y, destRect.width, 1, 0, dstData); // Rotate the buffer indexes. for (int k = 0; k < lastLineBuffer; k++) { bufIdx[k] = bufIdx[k + 1]; } bufIdx[lastLineBuffer] = currentIndex; // If available, load next image line into the last buffer line. if (y + numLinesBuffer < getMaxY()) { source.getPixels(startX, y + numLinesBuffer, width, 1, bufMem[bufIdx[lastLineBuffer]]); } } } else if (caseC || (hasROI && hasNodata && roiContainsTile)) { for (int y = startY; y <= endY; y++) { int currentIndex = bufIdx[0]; float[] currentLine = bufMem[currentIndex]; float[] currentLineNoData = bufNoData[currentIndex]; // Loop over pixels. int dstOffset = 0; for (int x = startX, z = 0; x <= endX; x++) { // Boolean used for checking NoData boolean isNodata = false; // Copy all samples of the current pixel. for (int b = 0; b < numBandsSource; b++) { int zP = z++; currentPixel[b] = currentLine[zP]; currentPixelReal[b] = currentLineNoData[zP]; // Clamp the current sample to the valid range if (currentPixel[b] < minPixelValue || currentPixel[b] > maxPixelValue) { currentPixel[b] = java.lang.Math.max(currentPixel[b], minPixelValue); currentPixel[b] = java.lang.Math.min(currentPixel[b], maxPixelValue); } // Clamp the current sample to the valid range if (currentPixelReal[b] < minPixelValue || currentPixelReal[b] > maxPixelValue) { currentPixelReal[b] = java.lang.Math.max(currentPixelReal[b], minPixelValue); currentPixelReal[b] = java.lang.Math.min(currentPixelReal[b], maxPixelValue); } // NoData Check isNodata |= nodata.contains(currentPixelReal[b]); } // Find the index of the nearest color in the map. int nearestIndex = colorMap.findNearestEntry(currentPixel); // Save the index in the output data buffer. dstData[dstOffset++] = isNodata ? destNoData : nearestIndex; // Calculate the error between the nearest and actual // colors. boolean isQuantizationError = false; for (int b = 0; b < numBandsSource; b++) { qError[b] = currentPixel[b] - colorMap.lookupFloat(b, nearestIndex); if (qError[b] != 0.0F) { isQuantizationError = true; } } // If there was error in at least one band, distribute it. if (isQuantizationError && !isNodata) { // Distribute error to the right of key entry. int rightCount = Math.min(diffuseRight, endX - x); int kernelOffset = kernelOffsetRight; int sampleOffset = z; for (int u = 1; u <= rightCount; u++) { for (int b = 0; b < numBandsSource; b++) { currentLine[sampleOffset++] += qError[b] * kernelData[kernelOffset]; } kernelOffset++; } // Distribute error below key entry. int offsetLeft = Math.min(x - startX, diffuseRight); int count = Math.min(x + diffuseRight, endX) - Math.max(x - diffuseRight, startX) + 1; for (int v = 1; v <= diffuseBelow; v++) { float[] line = bufMem[bufIdx[v]]; kernelOffset = kernelOffsetBelow; sampleOffset = z - (offsetLeft + 1) * numBandsSource; for (int u = 1; u <= count; u++) { for (int b = 0; b < numBandsSource; b++) { line[sampleOffset++] += qError[b] * kernelData[kernelOffset]; } kernelOffset++; } } } } // // Save data for the current destination line. // dest.setSamples(startX, y, destRect.width, 1, 0, dstData); // Rotate the buffer indexes. for (int k = 0; k < lastLineBuffer; k++) { bufIdx[k] = bufIdx[k + 1]; } bufIdx[lastLineBuffer] = currentIndex; // If available, load next image line into the last buffer line. if (y + numLinesBuffer < getMaxY()) { source.getPixels(startX, y + numLinesBuffer, width, 1, bufMem[bufIdx[lastLineBuffer]]); source.getPixels(startX, y + numLinesBuffer, width, 1, bufNoData[bufIdx[lastLineBuffer]]); } } } else { for (int y = startY; y <= endY; y++) { int currentIndex = bufIdx[0]; float[] currentLine = bufMem[currentIndex]; float[] currentLineNoData = bufNoData[currentIndex]; // Loop over pixels. int dstOffset = 0; for (int x = startX, z = 0; x <= endX; x++) { // Boolean used for checking NoData boolean isNodata = false; // Copy all samples of the current pixel. for (int b = 0; b < numBandsSource; b++) { int zP = z++; currentPixel[b] = currentLine[zP]; currentPixelReal[b] = currentLineNoData[zP]; // Clamp the current sample to the valid range if (currentPixel[b] < minPixelValue || currentPixel[b] > maxPixelValue) { currentPixel[b] = java.lang.Math.max(currentPixel[b], minPixelValue); currentPixel[b] = java.lang.Math.min(currentPixel[b], maxPixelValue); } // Clamp the current sample to the valid range if (currentPixelReal[b] < minPixelValue || currentPixelReal[b] > maxPixelValue) { currentPixelReal[b] = java.lang.Math.max(currentPixelReal[b], minPixelValue); currentPixelReal[b] = java.lang.Math.min(currentPixelReal[b], maxPixelValue); } // NoData Check isNodata |= nodata.contains(currentPixelReal[b]); } // Find the index of the nearest color in the map. int nearestIndex = colorMap.findNearestEntry(currentPixel); // Check against ROI boolean inROI = inROI(roiIter, y, x); // Save the index in the output data buffer. dstData[dstOffset++] = inROI && !isNodata ? nearestIndex : destNoData; // Calculate the error between the nearest and actual // colors. boolean isQuantizationError = false; for (int b = 0; b < numBandsSource; b++) { qError[b] = currentPixel[b] - colorMap.lookupFloat(b, nearestIndex); if (qError[b] != 0.0F) { isQuantizationError = true; } } // If there was error in at least one band, distribute it. if (isQuantizationError && !isNodata && inROI) { // Distribute error to the right of key entry. int rightCount = Math.min(diffuseRight, endX - x); int kernelOffset = kernelOffsetRight; int sampleOffset = z; for (int u = 1; u <= rightCount; u++) { for (int b = 0; b < numBandsSource; b++) { currentLine[sampleOffset++] += qError[b] * kernelData[kernelOffset]; } kernelOffset++; } // Distribute error below key entry. int offsetLeft = Math.min(x - startX, diffuseRight); int count = Math.min(x + diffuseRight, endX) - Math.max(x - diffuseRight, startX) + 1; for (int v = 1; v <= diffuseBelow; v++) { float[] line = bufMem[bufIdx[v]]; kernelOffset = kernelOffsetBelow; sampleOffset = z - (offsetLeft + 1) * numBandsSource; for (int u = 1; u <= count; u++) { for (int b = 0; b < numBandsSource; b++) { line[sampleOffset++] += qError[b] * kernelData[kernelOffset]; } kernelOffset++; } } } } // // Save data for the current destination line. // dest.setSamples(startX, y, destRect.width, 1, 0, dstData); // Rotate the buffer indexes. for (int k = 0; k < lastLineBuffer; k++) { bufIdx[k] = bufIdx[k + 1]; } bufIdx[lastLineBuffer] = currentIndex; // If available, load next image line into the last buffer line. if (y + numLinesBuffer < getMaxY()) { source.getPixels(startX, y + numLinesBuffer, width, 1, bufMem[bufIdx[lastLineBuffer]]); source.getPixels(startX, y + numLinesBuffer, width, 1, bufNoData[bufIdx[lastLineBuffer]]); } } } } protected void computeImageOptimized(Raster source, WritableRaster dest, Rectangle destRect, RandomIter roiIter, boolean roiContainsTile) { // Set X-coordinate range. int startX = minX; int endX = startX + width - 1; // Set Y-coordinate range. int startY = minY; int endY = startY + height - 1; // Initialize the dither table. int[] ditherTable = initFloydSteinberg24To8((ColorCube) colorMap); // Initialize the padded source width. int sourceWidthPadded = source.getWidth() + 2; // Allocate memory for the error buffer. int[] errBuf = new int[sourceWidthPadded * NBANDS]; // Retrieve format tags. RasterFormatTag[] formatTags = getFormatTags(); RasterAccessor srcAccessor = new RasterAccessor(source, new Rectangle(startX, startY, source.getWidth(), source.getHeight()), formatTags[0], getSourceImage(0) .getColorModel()); RasterAccessor dstAccessor = new RasterAccessor(dest, destRect, formatTags[1], getColorModel()); // Set pixel and line strides. int srcPixelStride = srcAccessor.getPixelStride(); int srcScanlineStride = srcAccessor.getScanlineStride(); int dstPixelStride = dstAccessor.getPixelStride(); int dstScanlineStride = dstAccessor.getScanlineStride(); // Set data arrays. byte[] srcData0 = srcAccessor.getByteDataArray(0); byte[] srcData1 = srcAccessor.getByteDataArray(1); byte[] srcData2 = srcAccessor.getByteDataArray(2); byte[] dstData = dstAccessor.getByteDataArray(0); // Initialize line offset in each band. int srcLine0 = srcAccessor.getBandOffset(0); int srcLine1 = srcAccessor.getBandOffset(1); int srcLine2 = srcAccessor.getBandOffset(2); int dstLine = dstAccessor.getBandOffset(0); if (caseA || (caseB && roiContainsTile)) { // // For each line, calculate and distribute the error into // a 3 line error buffer (one line for each band). // Also accumulate the contributions of the 3 bands // into the same line of the temporary output buffer. // // The error buffer starts out with all zeroes as the // amount of error to propagate forward. // for (int y = startY; y <= endY; y++) { // Initialize pixel offset in each line in each band. int srcPixel0 = srcLine0; int srcPixel1 = srcLine1; int srcPixel2 = srcLine2; int dstPixel = dstLine; // // Determine the error and index contribution for // the each band. Keep the transitory errors // (errA, errC and errD) in local variables // (hopefully registers). The calculated value // of errB gets put into the error buffer, to be used // on the next line. // // This is the logic here. Floyd-Steinberg dithering // distributes errors to four neighboring pixels, // as shown below. X is the pixel being operated on. // // 7/16 of the error goes to pixel A // 3/16 of the error goes to pixel B // 5/16 of the error goes to pixel C // 1/16 of the error goes to pixel D // // X A // B C D // // The error distributed to pixel A is reused immediately // in the calculation of the next pixel on the same line. // The errors distributed to B, C and D will be used on the // following line. As we move from left to right, the // new error distributed to B gets added to the error // at the previous C. Likewise, the new C error gets added // to the previous D error. So only the errors propagating // to position B survive in the saved error buffer. The // only exception is at the line end, where error C must be // saved. The scheme is shown below. // // XA // BCD // BCD // BCD // BCD // // Treat the error buffer as pixel sequential. // This lets us use a single pointer with offsets // for the entries for all three bands. // // // Zero the error holders for all bands // The bands are called Red, Grn and Blu, but are // really just the first, second and third bands. // int errRedA = 0; int errRedC = 0; int errRedD = 0; int errGrnA = 0; int errGrnC = 0; int errGrnD = 0; int errBluA = 0; int errBluC = 0; int errBluD = 0; int pErr = 0; for (int x = startX; x <= endX; x++) { // // First band (Red) // The color index is initialized here. // Set the table pointer to the "Red" band // int pTab = UNDERSHOOT; int adjVal = ((errRedA + errBuf[pErr + 3] + 8) >> 4) + (int) (srcData0[srcPixel0] & 0xff); srcPixel0 += srcPixelStride; int tabval = ditherTable[pTab + adjVal]; int err = tabval >> 8; int err1 = err; int index = (tabval & 0xff); int err2 = err + err; errBuf[pErr] = errRedC + (err += err2); // 3/16 (B) errRedC = errRedD + (err += err2); // 5/16 (C) errRedD = err1; // 1/16 (D) errRedA = (err += err2); // 7/16 (A) // // Second band (Green) // Set the table pointer to the "Green" band // The color index is incremented here. // pTab += TOTALGRAYS; adjVal = ((errGrnA + errBuf[pErr + 4] + 8) >> 4) + (int) (srcData1[srcPixel1] & 0xff); srcPixel1 += srcPixelStride; tabval = ditherTable[pTab + adjVal]; err = tabval >> 8; err1 = err; index += (tabval & 0xff); err2 = err + err; errBuf[pErr + 1] = errGrnC + (err += err2); errGrnC = errGrnD + (err += err2); errGrnD = err1; errGrnA = (err += err2); pTab += TOTALGRAYS; // // Third band (Blue) // Set the table pointer to the "Blue" band // The color index is incremented here. // adjVal = ((errBluA + errBuf[pErr + 5] + 8) >> 4) + (int) (srcData2[srcPixel2] & 0xff); srcPixel2 += srcPixelStride; tabval = ditherTable[pTab + adjVal]; err = tabval >> 8; err1 = err; index += (tabval & 0xff); err2 = err + err; errBuf[pErr + 2] = errBluC + (err += err2); errBluC = errBluD + (err += err2); errBluD = err1; errBluA = (err += err2); // Save the result in the output data buffer. dstData[dstPixel] = (byte) (index & 0xff); dstPixel += dstPixelStride; pErr += 3; } // End pixel loop // // Save last error in line // int last = 3 * (sourceWidthPadded - 2); errBuf[last] = errRedC; errBuf[last + 1] = errGrnC; errBuf[last + 2] = errBluC; // Increment offset in each band to next line. srcLine0 += srcScanlineStride; srcLine1 += srcScanlineStride; srcLine2 += srcScanlineStride; dstLine += dstScanlineStride; } // End scanline loop } else if (caseB) { for (int y = startY; y <= endY; y++) { // Initialize pixel offset in each line in each band. int srcPixel0 = srcLine0; int srcPixel1 = srcLine1; int srcPixel2 = srcLine2; int dstPixel = dstLine; // // Zero the error holders for all bands // The bands are called Red, Grn and Blu, but are // really just the first, second and third bands. // int errRedA = 0; int errRedC = 0; int errRedD = 0; int errGrnA = 0; int errGrnC = 0; int errGrnD = 0; int errBluA = 0; int errBluC = 0; int errBluD = 0; int pErr = 0; for (int x = startX; x <= endX; x++) { int value0 = (int) (srcData0[srcPixel0] & 0xff); int value1 = (int) (srcData1[srcPixel1] & 0xff); int value2 = (int) (srcData2[srcPixel2] & 0xff); int index; // ROI Check if ((roiBounds.contains(x, y) && roiIter.getSample(x, y, 0) > 0)) { // // First band (Red) // The color index is initialized here. // Set the table pointer to the "Red" band // int pTab = UNDERSHOOT; int adjVal = ((errRedA + errBuf[pErr + 3] + 8) >> 4) + value0; int tabval = ditherTable[pTab + adjVal]; int err = tabval >> 8; int err1 = err; index = (tabval & 0xff); int err2 = err + err; errBuf[pErr] = errRedC + (err += err2); // 3/16 (B) errRedC = errRedD + (err += err2); // 5/16 (C) errRedD = err1; // 1/16 (D) errRedA = (err += err2); // 7/16 (A) // // Second band (Green) // Set the table pointer to the "Green" band // The color index is incremented here. // pTab += TOTALGRAYS; adjVal = ((errGrnA + errBuf[pErr + 4] + 8) >> 4) + value1; tabval = ditherTable[pTab + adjVal]; err = tabval >> 8; err1 = err; index += (tabval & 0xff); err2 = err + err; errBuf[pErr + 1] = errGrnC + (err += err2); errGrnC = errGrnD + (err += err2); errGrnD = err1; errGrnA = (err += err2); pTab += TOTALGRAYS; // // Third band (Blue) // Set the table pointer to the "Blue" band // The color index is incremented here. // adjVal = ((errBluA + errBuf[pErr + 5] + 8) >> 4) + value2; tabval = ditherTable[pTab + adjVal]; err = tabval >> 8; err1 = err; index += (tabval & 0xff); err2 = err + err; errBuf[pErr + 2] = errBluC + (err += err2); errBluC = errBluD + (err += err2); errBluD = err1; errBluA = (err += err2); } else { index = destNoData; } // Save the result in the output data buffer. dstData[dstPixel] = (byte) (index & 0xff); dstPixel += dstPixelStride; pErr += 3; srcPixel0 += srcPixelStride; srcPixel1 += srcPixelStride; srcPixel2 += srcPixelStride; } // End pixel loop // // Save last error in line // int last = 3 * (sourceWidthPadded - 2); errBuf[last] = errRedC; errBuf[last + 1] = errGrnC; errBuf[last + 2] = errBluC; // Increment offset in each band to next line. srcLine0 += srcScanlineStride; srcLine1 += srcScanlineStride; srcLine2 += srcScanlineStride; dstLine += dstScanlineStride; } // End scanline loop } else if (caseC || (hasROI && hasNodata && roiContainsTile)) { for (int y = startY; y <= endY; y++) { // Initialize pixel offset in each line in each band. int srcPixel0 = srcLine0; int srcPixel1 = srcLine1; int srcPixel2 = srcLine2; int dstPixel = dstLine; int errRedA = 0; int errRedC = 0; int errRedD = 0; int errGrnA = 0; int errGrnC = 0; int errGrnD = 0; int errBluA = 0; int errBluC = 0; int errBluD = 0; int pErr = 0; for (int x = startX; x <= endX; x++) { int value0 = (int) (srcData0[srcPixel0] & 0xff); int value1 = (int) (srcData1[srcPixel1] & 0xff); int value2 = (int) (srcData2[srcPixel2] & 0xff); // NoData Check boolean valid = lookupTable[value0] && lookupTable[value1] && lookupTable[value2]; int index; if (valid) { // // First band (Red) // The color index is initialized here. // Set the table pointer to the "Red" band // int pTab = UNDERSHOOT; int adjVal = ((errRedA + errBuf[pErr + 3] + 8) >> 4) + value0; int tabval = ditherTable[pTab + adjVal]; int err = tabval >> 8; int err1 = err; index = (tabval & 0xff); int err2 = err + err; errBuf[pErr] = errRedC + (err += err2); // 3/16 (B) errRedC = errRedD + (err += err2); // 5/16 (C) errRedD = err1; // 1/16 (D) errRedA = (err += err2); // 7/16 (A) // // Second band (Green) // Set the table pointer to the "Green" band // The color index is incremented here. // pTab += TOTALGRAYS; adjVal = ((errGrnA + errBuf[pErr + 4] + 8) >> 4) + value1; tabval = ditherTable[pTab + adjVal]; err = tabval >> 8; err1 = err; index += (tabval & 0xff); err2 = err + err; errBuf[pErr + 1] = errGrnC + (err += err2); errGrnC = errGrnD + (err += err2); errGrnD = err1; errGrnA = (err += err2); pTab += TOTALGRAYS; // // Third band (Blue) // Set the table pointer to the "Blue" band // The color index is incremented here. // adjVal = ((errBluA + errBuf[pErr + 5] + 8) >> 4) + value2; tabval = ditherTable[pTab + adjVal]; err = tabval >> 8; err1 = err; index += (tabval & 0xff); err2 = err + err; errBuf[pErr + 2] = errBluC + (err += err2); errBluC = errBluD + (err += err2); errBluD = err1; errBluA = (err += err2); } else { index = destNoData; } // Save the result in the output data buffer. dstData[dstPixel] = (byte) (index & 0xff); dstPixel += dstPixelStride; srcPixel0 += srcPixelStride; srcPixel1 += srcPixelStride; srcPixel2 += srcPixelStride; pErr += 3; } // End pixel loop // // Save last error in line // int last = 3 * (sourceWidthPadded - 2); errBuf[last] = errRedC; errBuf[last + 1] = errGrnC; errBuf[last + 2] = errBluC; // Increment offset in each band to next line. srcLine0 += srcScanlineStride; srcLine1 += srcScanlineStride; srcLine2 += srcScanlineStride; dstLine += dstScanlineStride; } // End scanline loop } else { for (int y = startY; y <= endY; y++) { // Initialize pixel offset in each line in each band. int srcPixel0 = srcLine0; int srcPixel1 = srcLine1; int srcPixel2 = srcLine2; int dstPixel = dstLine; // // Zero the error holders for all bands // The bands are called Red, Grn and Blu, but are // really just the first, second and third bands. // int errRedA = 0; int errRedC = 0; int errRedD = 0; int errGrnA = 0; int errGrnC = 0; int errGrnD = 0; int errBluA = 0; int errBluC = 0; int errBluD = 0; int pErr = 0; for (int x = startX; x <= endX; x++) { int value0 = (int) (srcData0[srcPixel0] & 0xff); int value1 = (int) (srcData1[srcPixel1] & 0xff); int value2 = (int) (srcData2[srcPixel2] & 0xff); boolean valid = lookupTable[value0] && lookupTable[value1] && lookupTable[value2]; int index; // NoData Check and ROI Check if (valid && (roiBounds.contains(x, y) && roiIter.getSample(x, y, 0) > 0)) { // // First band (Red) // The color index is initialized here. // Set the table pointer to the "Red" band // int pTab = UNDERSHOOT; int adjVal = ((errRedA + errBuf[pErr + 3] + 8) >> 4) + value0; int tabval = ditherTable[pTab + adjVal]; int err = tabval >> 8; int err1 = err; index = (tabval & 0xff); int err2 = err + err; errBuf[pErr] = errRedC + (err += err2); // 3/16 (B) errRedC = errRedD + (err += err2); // 5/16 (C) errRedD = err1; // 1/16 (D) errRedA = (err += err2); // 7/16 (A) // // Second band (Green) // Set the table pointer to the "Green" band // The color index is incremented here. // pTab += TOTALGRAYS; adjVal = ((errGrnA + errBuf[pErr + 4] + 8) >> 4) + value1; tabval = ditherTable[pTab + adjVal]; err = tabval >> 8; err1 = err; index += (tabval & 0xff); err2 = err + err; errBuf[pErr + 1] = errGrnC + (err += err2); errGrnC = errGrnD + (err += err2); errGrnD = err1; errGrnA = (err += err2); pTab += TOTALGRAYS; // // Third band (Blue) // Set the table pointer to the "Blue" band // The color index is incremented here. // adjVal = ((errBluA + errBuf[pErr + 5] + 8) >> 4) + value2; tabval = ditherTable[pTab + adjVal]; err = tabval >> 8; err1 = err; index += (tabval & 0xff); err2 = err + err; errBuf[pErr + 2] = errBluC + (err += err2); errBluC = errBluD + (err += err2); errBluD = err1; errBluA = (err += err2); } else { index = destNoData; } // Save the result in the output data buffer. dstData[dstPixel] = (byte) (index & 0xff); dstPixel += dstPixelStride; pErr += 3; srcPixel0 += srcPixelStride; srcPixel1 += srcPixelStride; srcPixel2 += srcPixelStride; } // End pixel loop // // Save last error in line // int last = 3 * (sourceWidthPadded - 2); errBuf[last] = errRedC; errBuf[last + 1] = errGrnC; errBuf[last + 2] = errBluC; // Increment offset in each band to next line. srcLine0 += srcScanlineStride; srcLine1 += srcScanlineStride; srcLine2 += srcScanlineStride; dstLine += dstScanlineStride; } // End scanline loop } // Make sure that the output data is copied to the destination. dstAccessor.copyDataToRaster(); } /** * This method provides a lazy initialization of the image associated to the ROI. The method uses the Double-checked locking in order to maintain * thread-safety * * @return */ private PlanarImage getImage() { PlanarImage img = roiImage; if (img == null) { synchronized (this) { img = roiImage; if (img == null) { roiImage = img = roi.getAsImage(); } } } return img; } /** * Private method for checking if a pixel in position (x,y) is inside the ROI * * @param roiIter * @param y * @param x * @return true if the pixel is inside ROI */ private boolean inROI(RandomIter roiIter, int y, int x) { return (roiBounds.contains(x, y) && roiIter.getSample(x, y, 0) > 0); } }