nom.tam.fits.compression.algorithm.quant.Quantize Maven / Gradle / Ivy
package nom.tam.fits.compression.algorithm.quant;
/*
* #%L
* nom.tam FITS library
* %%
* Copyright (C) 1996 - 2024 nom-tam-fits
* %%
* This is free and unencumbered software released into the public domain.
*
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
* of the public at large and to the detriment of our heirs and
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
* #L%
*/
import java.util.Arrays;
/**
* (for internal use) Determines the optimal quantization to use for floating-point data. It estimates the noise
* level in the data to determine qhat quantization should be use to lose no information above the noise level.
*
* @deprecated (for internal use) This class sohuld have visibility reduced to the package level
*/
@SuppressWarnings("javadoc")
public class Quantize {
class DoubleArrayPointer {
private final double[] array;
private int startIndex;
DoubleArrayPointer(double[] arrayIn) {
array = arrayIn;
}
public DoubleArrayPointer copy(long l) {
DoubleArrayPointer result = new DoubleArrayPointer(array);
result.startIndex = (int) l;
return result;
}
public double get(int ii) {
return array[ii + startIndex];
}
}
private static final double DEFAULT_QUANT_LEVEL = 4.;
private static final double MAX_INT_AS_DOUBLE = Integer.MAX_VALUE;
private static final int MINIMUM_PIXEL_WIDTH = 9;
/**
* number of reserved values, starting with
*/
private static final long N_RESERVED_VALUES = 10;
private static final int N4 = 4;
private static final int N6 = 6;
private static final double NOISE_2_MULTIPLICATOR = 1.0483579;
private static final double NOISE_3_MULTIPLICATOR = 0.6052697;
private static final double NOISE_5_MULTIPLICATOR = 0.1772048;
private final QuantizeOption parameter;
/**
* maximum non-null value
*/
private double maxValue;
/**
* minimum non-null value
*/
private double minValue;
/**
* number of good, non-null pixels?
*/
private long ngood;
/**
* returned 2nd order MAD of all non-null pixels
*/
private double noise2;
/**
* returned 3rd order MAD of all non-null pixels
*/
private double noise3;
/* returned 5th order MAD of all non-null pixels */
private double noise5;
private double xmaxval;
private double xminval;
private double xnoise2;
private double xnoise3;
private double xnoise5;
public Quantize(QuantizeOption quantizeOption) {
parameter = quantizeOption;
}
/**
* Estimate the median and background noise in the input image using 2nd, 3rd and 5th order Median Absolute
* Differences. The noise in the background of the image is calculated using the MAD algorithms developed for
* deriving the signal to noise ratio in spectra (see issue #42 of the ST-ECF newsletter,
* http://www.stecf.org/documents/newsletter/) 3rd order: noise = 1.482602 / sqrt(6) * median (abs(2*flux(i) -
* flux(i-2) - flux(i+2))) The returned estimates are the median of the values that are computed for each row of the
* image.
*
* @param arrayIn 2 dimensional tiledImageOperation of image pixels
* @param nx number of pixels in each row of the image
* @param ny number of rows in the image
* @param nullcheck check for null values, if true
* @param nullvalue value of null pixels, if nullcheck is true
*/
private void calculateNoise(double[] arrayIn, int nx, int ny) {
DoubleArrayPointer array = new DoubleArrayPointer(arrayIn);
initializeNoise();
if (nx < MINIMUM_PIXEL_WIDTH) {
// treat entire tiledImageOperation as an image with a single row
nx = nx * ny;
ny = 1;
}
if (calculateNoiseShortRow(array, nx, ny)) {
return;
}
DoubleArrayPointer rowpix;
int nrows = 0, nrows2 = 0;
long ngoodpix = 0;
/* allocate arrays used to compute the median and noise estimates */
double[] differences2 = new double[nx];
double[] differences3 = new double[nx];
double[] differences5 = new double[nx];
double[] diffs2 = new double[ny];
double[] diffs3 = new double[ny];
double[] diffs5 = new double[ny];
/* loop over each row of the image */
for (int jj = 0; jj < ny; jj++) {
rowpix = array.copy(jj * nx); /* point to first pixel in the row */
int ii = 0;
ii = findNextValidPixelWithNullCheck(nx, rowpix, ii);
if (ii == nx) {
continue; /* hit end of row */
}
double v1 = getNextPixelAndCheckMinMax(rowpix, ii);
ngoodpix++;
ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii);
if (ii == nx) {
continue; /* hit end of row */
}
double v2 = getNextPixelAndCheckMinMax(rowpix, ii);
ngoodpix++;
ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii);
if (ii == nx) {
continue; /* hit end of row */
}
double v3 = getNextPixelAndCheckMinMax(rowpix, ii);
ngoodpix++;
ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii);
if (ii == nx) {
continue; /* hit end of row */
}
double v4 = getNextPixelAndCheckMinMax(rowpix, ii);
ngoodpix++;
ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii);
if (ii == nx) {
continue; /* hit end of row */
}
double v5 = getNextPixelAndCheckMinMax(rowpix, ii);
ngoodpix++;
ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii);
if (ii == nx) {
continue; /* hit end of row */
}
double v6 = getNextPixelAndCheckMinMax(rowpix, ii);
ngoodpix++;
ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii);
if (ii == nx) {
continue; /* hit end of row */
}
double v7 = getNextPixelAndCheckMinMax(rowpix, ii);
ngoodpix++;
ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii);
if (ii == nx) {
continue; /* hit end of row */
}
double v8 = getNextPixelAndCheckMinMax(rowpix, ii);
ngoodpix++;
// now populate the differences arrays for the remaining pixels in
// the row */
int nvals = 0;
int nvals2 = 0;
for (ii++; ii < nx; ii++) {
ii = findNextValidPixelWithNullCheck(nx, rowpix, ii);
if (ii == nx) {
continue; /* hit end of row */
}
double v9 = getNextPixelAndCheckMinMax(rowpix, ii);
/* construct tiledImageOperation of absolute differences */
if (!(v5 == v6 && v6 == v7)) {
differences2[nvals2] = Math.abs(v5 - v7);
nvals2++;
}
if (!(v3 == v4 && v4 == v5 && v5 == v6 && v6 == v7)) {
differences3[nvals] = Math.abs(2 * v5 - v3 - v7);
differences5[nvals] = Math.abs(N6 * v5 - N4 * v3 - N4 * v7 + v1 + v9);
nvals++;
} else {
/* ignore constant background regions */
ngoodpix++;
}
/* shift over 1 pixel */
v1 = v2;
v2 = v3;
v3 = v4;
v4 = v5;
v5 = v6;
v6 = v7;
v7 = v8;
v8 = v9;
} /* end of loop over pixels in the row */
// compute the median diffs Note that there are 8 more pixel values
// than there are diffs values.
ngoodpix += nvals;
if (nvals == 0) {
continue; /* cannot compute medians on this row */
}
if (nvals == 1) {
if (nvals2 == 1) {
diffs2[nrows2] = differences2[0];
nrows2++;
}
diffs3[nrows] = differences3[0];
diffs5[nrows] = differences5[0];
} else {
/* quick_select returns the median MUCH faster than using qsort */
if (nvals2 > 1) {
diffs2[nrows2] = quickSelect(differences2, nvals);
nrows2++;
}
diffs3[nrows] = quickSelect(differences3, nvals);
diffs5[nrows] = quickSelect(differences5, nvals);
}
nrows++;
} /* end of loop over rows */
computeMedianOfValuesEachRow(nrows, nrows2, diffs2, diffs3, diffs5);
setNoiseResult(ngoodpix);
}
private boolean calculateNoiseShortRow(DoubleArrayPointer array, int nx, int ny) {
/* rows must have at least 9 pixels */
if (nx < MINIMUM_PIXEL_WIDTH) {
int ngoodpix = 0;
for (int index = 0; index < nx; index++) {
if (isNull(array.get(index))) {
continue;
}
if (array.get(index) < xminval) {
xminval = array.get(index);
}
if (array.get(index) > xmaxval) {
xmaxval = array.get(index);
}
ngoodpix++;
}
setNoiseResult(ngoodpix);
return true;
}
return false;
}
protected void computeMedianOfValuesEachRow(int nrows, int nrows2, double[] diffs2, double[] diffs3, double[] diffs5) {
// compute median of the values for each row.
if (nrows == 0) {
xnoise3 = 0;
xnoise5 = 0;
} else if (nrows == 1) {
xnoise3 = diffs3[0];
xnoise5 = diffs5[0];
} else {
Arrays.sort(diffs3, 0, nrows);
Arrays.sort(diffs5, 0, nrows);
xnoise3 = (diffs3[(nrows - 1) / 2] + diffs3[nrows / 2]) / 2.;
xnoise5 = (diffs5[(nrows - 1) / 2] + diffs5[nrows / 2]) / 2.;
}
if (nrows2 == 0) {
xnoise2 = 0;
} else if (nrows2 == 1) {
xnoise2 = diffs2[0];
} else {
Arrays.sort(diffs2, 0, nrows2);
xnoise2 = (diffs2[(nrows2 - 1) / 2] + diffs2[nrows2 / 2]) / 2.;
}
}
protected int findNextValidPixelWithNullCheck(int nx, DoubleArrayPointer rowpix, int ii) {
return ii;
}
private double getNextPixelAndCheckMinMax(DoubleArrayPointer rowpix, int ii) {
double pixelValue = rowpix.get(ii); /* store the good pixel value */
if (pixelValue < xminval) {
xminval = pixelValue;
}
if (pixelValue > xmaxval) {
xmaxval = pixelValue;
}
return pixelValue;
}
protected double getNoise2() {
return noise2;
}
protected double getNoise3() {
return noise3;
}
protected double getNoise5() {
return noise5;
}
private void initializeNoise() {
xnoise2 = 0;
xnoise3 = 0;
xnoise5 = 0;
xminval = Double.MAX_VALUE;
xmaxval = Double.MIN_VALUE;
}
protected boolean isNull(double d) {
return false;
}
/**
* arguments: long row i: tile number = row number in the binary table double fdata[] i: tiledImageOperation of
* image pixels to be compressed long nxpix i: number of pixels in each row of fdata long nypix i: number of rows in
* fdata nullcheck i: check for nullvalues in fdata? double in_null_value i: value used to represent undefined
* pixels in fdata float qlevel i: quantization level int dither_method i; which dithering method to use int idata[]
* o: values of fdata after applying bzero and bscale double bscale o: scale factor double bzero o: zero offset int
* iminval o: minimum quantized value that is returned int imaxval o: maximum quantized value that is returned The
* function value will be one if the input fdata were copied to idata; in this case the parameters bscale and bzero
* can be used to convert back to nearly the original floating point values: fdata ~= idata * bscale + bzero. If the
* function value is zero, the data were not copied to idata.
*
* In earlier implementations of the compression code, we only used the noise3 value as the most reliable estimate
* of the background noise in an image. If it is not possible to compute a noise3 value, then this serves as a red
* flag to indicate that quantizing the image could cause a loss of significant information in the image.
*
*
* At some later date, we decided to take the more conservative approach of using the minimum of all three of the
* noise values (while still requiring that noise3 has a defined value) as the best estimate of the noise. Note that
* if an image contains pure Gaussian distributed noise, then noise2, noise3, and noise5 will have exactly the same
* value (within statistical measurement errors).
*
*
* @param fdata the data to quantinize
* @param nxpix the image width
* @param nypix the image hight
*
* @return true if the quantification was possible
*/
public boolean quantize(double[] fdata, int nxpix, int nypix) {
// MAD 2nd, 3rd, and 5th order noise values
double stdev;
double bScale; /* bscale, 1 in intdata = delta in fdata */
long nx = (long) nxpix * (long) nypix;
if (nx <= 1L) {
parameter.setBScale(1.);
parameter.setBZero(0.);
return false;
}
if (parameter.getQLevel() >= 0.) {
/* estimate background noise using MAD pixel differences */
calculateNoise(fdata, nxpix, nypix);
// special case of an image filled with Nulls
if (parameter.isCheckNull() && ngood == 0) {
/* set parameters to dummy values, which are not used */
minValue = 0.;
maxValue = 1.;
stdev = 1;
} else {
// use the minimum of noise2, noise3, and noise5 as the best
// noise value
stdev = noise3;
if (noise2 != 0. && noise2 < stdev) {
stdev = noise2;
}
if (noise5 != 0. && noise5 < stdev) {
stdev = noise5;
}
}
if (parameter.getQLevel() == 0.) {
bScale = stdev / DEFAULT_QUANT_LEVEL; /* default quantization */
} else {
bScale = stdev / parameter.getQLevel();
}
if (bScale == 0.) {
return false; /* don't quantize */
}
} else {
/* negative value represents the absolute quantization level */
bScale = -parameter.getQLevel();
/* only nned to calculate the min and max values */
calculateNoise(fdata, nxpix, nypix);
}
/* check that the range of quantized levels is not > range of int */
if ((maxValue - minValue) / bScale > 2. * MAX_INT_AS_DOUBLE - N_RESERVED_VALUES) {
return false; /* don't quantize */
}
parameter.setBScale(bScale);
parameter.setMinValue(minValue);
parameter.setMaxValue(maxValue);
parameter.setCheckNull(parameter.isCheckNull() && ngood != nx);
return true; /* yes, data have been quantized */
}
private double quickSelect(double[] arr, int n) {
int low, high;
int median;
int middle, ll, hh;
low = 0;
high = n - 1;
median = low + high >>> 1; // was (low + high) / 2;
for (;;) {
if (high <= low) {
return arr[median];
}
if (high == low + 1) { /* Two elements only */
if (arr[low] > arr[high]) {
swapElements(arr, low, high);
}
return arr[median];
}
/* Find median of low, middle and high items; swap into position low */
middle = low + high >>> 1; // was (low + high) / 2;
if (arr[middle] > arr[high]) {
swapElements(arr, middle, high);
}
if (arr[low] > arr[high]) {
swapElements(arr, low, high);
}
if (arr[middle] > arr[low]) {
swapElements(arr, middle, low);
}
/* Swap low item (now in position middle) into position (low+1) */
swapElements(arr, middle, low + 1);
/* Nibble from each end towards middle, swapping items when stuck */
ll = low + 1;
hh = high;
for (;;) {
do {
ll++;
} while (arr[low] > arr[ll]);
do {
hh--;
} while (arr[hh] > arr[low]);
if (hh < ll) {
break;
}
swapElements(arr, ll, hh);
}
/* Swap middle item (in position low) back into correct position */
swapElements(arr, low, hh);
/* Re-set active partition */
if (hh <= median) {
low = ll;
}
if (hh >= median) {
high = hh - 1;
}
}
}
private void setNoiseResult(long ngoodpix) {
minValue = xminval;
maxValue = xmaxval;
ngood = ngoodpix;
noise2 = NOISE_2_MULTIPLICATOR * xnoise2;
noise3 = NOISE_3_MULTIPLICATOR * xnoise3;
noise5 = NOISE_5_MULTIPLICATOR * xnoise5;
}
private void swapElements(double[] array, int one, int second) {
double value = array[one];
array[one] = array[second];
array[second] = value;
}
}