package ij.process; import java.util.Arrays; /** A table for easier downsizing by convolution with a kernel. * Supports the interpolation methods of ImageProcessor: none, bilinear, bicubic * Convention used: The left edges of the first pixel are the same for source and destination. * E.g. when downsizing by a factor of 2, pixel 0 of the destination * takes the space of pixels 0 and 1 of the source. * * Example for use: Downsizing row 0 of 'pixels' from 'roi.width' to 'destinationWidth'. * The input range is given by the roi rectangle. * Output is written to row 0 of 'pixels2' (width: 'destinationWidth') <code> DownSizeTable dt = new DownSizeTable(width, roi.x, roi.width, destinationWidth, ImageProcessor.BICUBIC); int tablePointer = 0; for (int srcPoint=dt.srcStart, srcPoint<=dt.srcEnd; srcPoint++) { float v = pixels[srcPoint]; for (int i=0; i<dt.kernelSize; i++, tablePointer++) pixels2[dt.indices[tablePointer]] += v * dt.weights[tablePointer]; </code> */ public class DownsizeTable { /** Number of kernel points per source data point */ public final int kernelSize; /** index of the first point of the source data that should be accessed */ public final int srcStart; /** index of the last point of the source data that should be accessed */ public final int srcEnd; /** For each source point between srcStart and srcEnd, indices of destination * points where the data should be added. * Arranged in blocks of 'kernelSize' points. E.g. for kernelSize=2, array * elements 0,1 are for point srcStart, 2,3 for point srcStart+1, etc. */ public final int[] indices; /** For each source point, weights for adding it to the destination point * given in the corresponding element of 'indices' */ public final float[] weights; /** Kernel sizes corresponding to the interpolation methods NONE, BILINEAR, BICUBIC */ private final static int[] kernelSizes = new int[] {1, 2, 4}; private final int srcOrigin, srcLength; private final double scale; //source/destination pixel numbers private final int interpolationMethod; private final static int UNUSED=-1; //marks unused entries in 'indices' array /** Create a table for 1-dimensional downscaling interpolation. * Interpolation is done by * @param srcSize Size of source data, i.e., width or height of input image * @param srcOrigin Index of first pixel of source data that corresponds to an ouput pixel, * 0 or origin of source rectangle if only a roi is scaled * @param srcLength Number of pixels of source data that should correspond to output, i.e., * width or height of source roi * @param dstSize Number of destination pixels. * @param interpolationMethod One of the methods defined in ImageProcessor: NONE, BILINEAR, BICUBIC */ DownsizeTable(int srcSize, int srcOrigin, int srcLength, int dstSize, int interpolationMethod) { this.srcOrigin = srcOrigin; this.srcLength = srcLength; this.interpolationMethod = interpolationMethod; this.scale = srcLength / (double)dstSize; this.kernelSize = kernelSizes[interpolationMethod]; int srcStartUncorr = (int)(Math.ceil(1e-8+srcIndex(-0.5*kernelSize))); //may be <0 srcStart = srcStartUncorr < 0 ? 0 : srcStartUncorr; //corrected value, avoids pointing out of array int srcEndUncorr = (int)(Math.floor(1e-8+srcIndex(dstSize-1 + 0.5*kernelSize))); srcEnd = srcEndUncorr >=srcSize ? srcSize-1 : srcEndUncorr; int arraySize = (srcEnd - srcStart + 1) * kernelSize; indices = new int[arraySize]; weights = new float[arraySize]; Arrays.fill(indices, UNUSED); //IJ.log("src size="+srcSize+" range="+srcStart+"-"+srcEnd+" array:"+arraySize+" scale="+(float)scale); for (int dst=0; dst<dstSize; dst++) { double sum = 0; int lowestS = (int)(Math.ceil(1e-8+srcIndex(dst-0.5*kernelSize))); int highestS = (int)(Math.floor(-1e-8+srcIndex(dst+0.5*kernelSize))); for (int src=lowestS; src<=highestS; src++) { //out of bounds policy is 'use value of edge pixel'. //We therfore replace an out-of-bounds pixel by the edge pixel: int s = src < 0 ? 0 : (src >= srcSize ? srcSize-1 : src); int p = (s-srcStart)*kernelSize;// points to first value in 'indices' and 'weights' // arrays reserved for this source pixel while(indices[p]!=UNUSED && indices[p]!=dst) p++; //position used for other destination pixel, try the next one //if(p-(s-srcStart)*kernelSize>=kernelSize)IJ.log(srcSize+">"+dstSize+": too long: src="+src+" dst="+dst); indices[p] = dst; float weight = kernel(dst - dstIndex(src)); sum += weight; weights[p] += weight; //IJ.log("src="+src+"("+s+") to "+dst+" w="+weight+" p="+p); } //normalize: sum of weights contributing to this destination pixel should be 1 int iStart = (lowestS-srcStart)*kernelSize; if (iStart < 0) iStart = 0; int iStop = (highestS-srcStart)*kernelSize+(kernelSize-1); if (iStop>=indices.length) iStop = indices.length-1; //IJ.log("normalize "+iStart+"-"+iStop+" sum="+sum); for (int i=iStart; i<=iStop; i++) if (indices[i] == dst) weights[i] = (float)(weights[i]/sum); } for (int i=0; i<indices.length; i++) if (indices[i]==UNUSED) indices[i] = 0; //set unused entries to pixel 0 (weight is 0 anyhow), then they do no harm } // Converts a destination pixel coordinate (index) to the corresponding // source coordinate // All coordinates refer to the centers of the pixel // Also for fractional indices, e.g. dstIndex = i-0.5 for left edge of pixel. // The output is also fractional (not converted to int) // No check for source array bounds, may result in a value <0 or >= array size. private double srcIndex(double dstIndex) { return srcOrigin-0.5 + (dstIndex+0.5)*scale; } // Converts the coordinate (index) of a source pixel to the destination pixel private double dstIndex(int srcIndex) { return (srcIndex-srcOrigin+0.5)/scale - 0.5; } // Calculates the kernel value. Only valid within +/- 0.5*kernelSize protected float kernel(double x) { switch (interpolationMethod) { case ImageProcessor.NONE: return 1f; case ImageProcessor.BILINEAR: return 1f - (float)Math.abs(x); case ImageProcessor.BICUBIC: return (float)ImageProcessor.cubic(x); } return Float.NaN; } }