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;
}
}