All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy