From bbfe9bdf66d2ae28502930274e900be3f6f5388e Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Thu, 29 Nov 2018 16:15:08 +0100 Subject: [PATCH] Backport ClassBreaks operation from GeoTools #222 --- jt-classbreaks/pom.xml | 33 +++ .../classbreaks/ClassBreaksDescriptor.java | 177 +++++++++++++ .../classbreaks/ClassBreaksOpImage.java | 241 ++++++++++++++++++ .../jaiext/classbreaks/ClassBreaksRIF.java | 139 ++++++++++ .../jaiext/classbreaks/Classification.java | 96 +++++++ .../classbreaks/ClassificationMethod.java | 48 ++++ .../EqualIntervalBreaksOpImage.java | 112 ++++++++ .../classbreaks/HistogramClassification.java | 196 ++++++++++++++ .../NaturalBreaksHistogramOpImage.java | 170 ++++++++++++ .../classbreaks/NaturalBreaksOpImage.java | 163 ++++++++++++ .../classbreaks/NaturalClassification.java | 58 +++++ .../QuantileBreaksHistogramOpImage.java | 109 ++++++++ .../classbreaks/QuantileBreaksOpImage.java | 121 +++++++++ .../classbreaks/QuantileClassification.java | 84 ++++++ .../resources/META-INF/registryFile.jaiext | 9 + .../classbreaks/ClassBreaksOpImageTest.java | 215 ++++++++++++++++ pom.xml | 3 + 17 files changed, 1974 insertions(+) create mode 100644 jt-classbreaks/pom.xml create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksDescriptor.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksOpImage.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksRIF.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/Classification.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassificationMethod.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/EqualIntervalBreaksOpImage.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/HistogramClassification.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalBreaksHistogramOpImage.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalBreaksOpImage.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalClassification.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileBreaksHistogramOpImage.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileBreaksOpImage.java create mode 100644 jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileClassification.java create mode 100644 jt-classbreaks/src/main/resources/META-INF/registryFile.jaiext create mode 100644 jt-classbreaks/src/test/java/it/geosolutions/jaiext/classbreaks/ClassBreaksOpImageTest.java diff --git a/jt-classbreaks/pom.xml b/jt-classbreaks/pom.xml new file mode 100644 index 00000000..6a312bfd --- /dev/null +++ b/jt-classbreaks/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + it.geosolutions.jaiext + jaiext + 1.1-SNAPSHOT + + it.geosolutions.jaiext.classbreaks + jt-classbreaks + 1.1-SNAPSHOT + jt-classbreaks + http://maven.apache.org + + UTF-8 + + + + it.geosolutions.jaiext.utilities + jt-utilities + ${project.version} + + + it.geosolutions.jaiext.utilities + jt-utilities + ${project.version} + test-jar + test + + + diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksDescriptor.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksDescriptor.java new file mode 100644 index 00000000..09e4e578 --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksDescriptor.java @@ -0,0 +1,177 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ + +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2016, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import java.awt.image.RenderedImage; + +import javax.media.jai.OperationDescriptorImpl; +import javax.media.jai.ROI; +import javax.media.jai.registry.RenderedRegistryMode; + +/** Operation descriptor for the ClassBreaks operation. */ +public class ClassBreaksDescriptor extends OperationDescriptorImpl { + + public static final String CLASSIFICATION_PROPERTY = "Classification"; + + public static final String NAME = "ClassBreaks"; + + static final int NUM_CLASSES_ARG = 0; + static final int METHOD_ARG = 1; + static final int EXTREMA_ARG = 2; + static final int ROI_ARG = 3; + static final int BAND_ARG = 4; + static final int X_PERIOD_ARG = 5; + static final int Y_PERIOD_ARG = 6; + static final int NODATA_ARG = 7; + static final int HISTOGRAM_ARG = 8; + static final int HISTOGRAM_BINS = 9; + + static String[] paramNames = + new String[] { + "numClasses", + "method", + "extrema", + "roi", + "band", + "xPeriod", + "yPeriod", + "noData", + "histogram", + "histogramBins" + }; + + static final Class[] paramClasses = { + Integer.class, + ClassificationMethod.class, + Double[][].class, + ROI.class, + Integer[].class, + Integer.class, + Integer.class, + Double.class, + Boolean.class, + Integer.class + }; + + static final Object[] paramDefaults = { + 10, + ClassificationMethod.EQUAL_INTERVAL, + null, + (ROI) null, + new Integer[] {Integer.valueOf(0)}, + 1, + 1, + null, + false, + 256 + }; + + public ClassBreaksDescriptor() { + super( + new String[][] { + {"GlobalName", NAME}, + {"LocalName", NAME}, + {"Vendor", "it.geosolutions.jaiext"}, + { + "Description", + "Classifies image values using equal interval method and calculates " + + "statistics for each class" + }, + {"DocURL", ""}, + {"Version", "1.0"}, + { + String.format("arg%dDesc", NUM_CLASSES_ARG), + String.format("%s - number of classes or bins", paramNames[NUM_CLASSES_ARG]) + }, + { + String.format("arg%dDesc", METHOD_ARG), + String.format("%s - classification method", paramNames[METHOD_ARG]) + }, + { + String.format("arg%dDesc", EXTREMA_ARG), + String.format("%s - range of values to include", paramNames[EXTREMA_ARG]) + }, + { + String.format("arg%dDesc", ROI_ARG), + String.format( + "%s (default %s) - region-of-interest constrainting the values to be counted", + paramNames[ROI_ARG], paramDefaults[ROI_ARG]) + }, + { + String.format("arg%dDesc", BAND_ARG), + String.format( + "%s (default %s) - bands of the image to process", + paramNames[BAND_ARG], paramDefaults[BAND_ARG]) + }, + { + String.format("arg%dDesc", X_PERIOD_ARG), + String.format( + "%s (default %s) - horizontal sampling rate", + paramNames[X_PERIOD_ARG], paramDefaults[X_PERIOD_ARG]) + }, + { + String.format("arg%dDesc", Y_PERIOD_ARG), + String.format( + "%s (default %s) - vertical sampling rate", + paramNames[Y_PERIOD_ARG], paramDefaults[Y_PERIOD_ARG]) + }, + { + String.format("arg%dDesc", NODATA_ARG), + String.format( + "%s (default %s) - value to treat as NODATA", + paramNames[NODATA_ARG], paramDefaults[NODATA_ARG]) + }, + { + String.format("arg%dDesc", HISTOGRAM_ARG), + String.format( + "%s (default %s) - if true, a histogram based computation will be used", + paramNames[HISTOGRAM_ARG], paramDefaults[HISTOGRAM_ARG]) + }, + { + String.format("arg%dDesc", HISTOGRAM_BINS), + String.format( + "%s (default %s) - if true, number of histogram bins to be used", + paramNames[HISTOGRAM_BINS], paramDefaults[HISTOGRAM_BINS]) + }, + }, + new String[] {RenderedRegistryMode.MODE_NAME}, + new String[] {"source0"}, + new Class[][] {{RenderedImage.class}}, + paramNames, + paramClasses, + paramDefaults, + null // valid values (none defined) + ); + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksOpImage.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksOpImage.java new file mode 100644 index 00000000..41572b08 --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksOpImage.java @@ -0,0 +1,241 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2016, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import java.awt.*; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.util.LinkedList; +import java.util.ListIterator; + +import javax.media.jai.PixelAccessor; +import javax.media.jai.ROI; +import javax.media.jai.StatisticsOpImage; +import javax.media.jai.UnpackedImageData; + +/** Abstract base class for various operations corresponding to classification method. */ +public abstract class ClassBreaksOpImage extends StatisticsOpImage { + + /* number of classes */ + protected Integer numClasses; + + /* range of values to calculate per band */ + protected Double[][] extrema; + + /* bands to process */ + protected Integer[] bands; + + /* no data value */ + protected Double noData; + + public ClassBreaksOpImage( + RenderedImage image, + Integer numClasses, + Double[][] extrema, + ROI roi, + Integer[] bands, + Integer xStart, + Integer yStart, + Integer xPeriod, + Integer yPeriod, + Double noData) { + + super(image, roi, xStart, yStart, xPeriod, yPeriod); + + this.numClasses = numClasses; + this.extrema = extrema; + this.bands = bands; + this.noData = noData; + } + + @Override + protected String[] getStatisticsNames() { + return new String[] {ClassBreaksDescriptor.CLASSIFICATION_PROPERTY}; + } + + @Override + public Object getProperty(String name) { + Object obj = properties.getProperty(ClassBreaksDescriptor.CLASSIFICATION_PROPERTY); + if (obj == Image.UndefinedProperty) { + // not calculated yet, give subclass a chance to optimize in cases where enough + // parameters are specified that the image does not have to be scanned + Classification c = preCalculate(); + if (c != null) { + properties.setProperty(ClassBreaksDescriptor.CLASSIFICATION_PROPERTY, c); + } + } + + return super.getProperty(name); + } + + @Override + public void setProperty(String name, Object value) { + if (value instanceof Classification) { + // calculation over, calculate the breaks + Classification c = (Classification) value; + for (int b = 0; b < bands.length; b++) { + postCalculate(c, b); + } + } + + super.setProperty(name, value); + } + + @Override + protected Object createStatistics(String name) { + if (ClassBreaksDescriptor.CLASSIFICATION_PROPERTY.equals(name)) { + return createClassification(); + } + return Image.UndefinedProperty; + } + + @Override + protected void accumulateStatistics(String name, Raster raster, Object obj) { + if (!ClassBreaksDescriptor.CLASSIFICATION_PROPERTY.equals(name)) { + return; + } + + Classification c = (Classification) obj; + + // ClassifiedStats2 stats = (ClassifiedStats2) obj; + SampleModel sampleModel = raster.getSampleModel(); + + Rectangle bounds = raster.getBounds(); + + LinkedList rectList; + if (roi == null) { // ROI is the whole Raster + rectList = new LinkedList(); + rectList.addLast(bounds); + } else { + rectList = + roi.getAsRectangleList( + bounds.x, bounds.y, + bounds.width, bounds.height); + if (rectList == null) { + return; // ROI does not intersect with Raster boundary. + } + } + + PixelAccessor accessor = new PixelAccessor(sampleModel, null); + + ListIterator iterator = rectList.listIterator(0); + + while (iterator.hasNext()) { + Rectangle r = (Rectangle) iterator.next(); + int tx = r.x; + int ty = r.y; + + // Find the actual ROI based on start and period. + r.x = startPosition(tx, xStart, xPeriod); + r.y = startPosition(ty, yStart, yPeriod); + r.width = tx + r.width - r.x; + r.height = ty + r.height - r.y; + + if (r.width <= 0 || r.height <= 0) { + continue; // no pixel to count in this rectangle + } + + switch (accessor.sampleType) { + case PixelAccessor.TYPE_BIT: + case DataBuffer.TYPE_BYTE: + case DataBuffer.TYPE_USHORT: + case DataBuffer.TYPE_SHORT: + case DataBuffer.TYPE_INT: + // countPixelsInt(accessor, raster, r, xPeriod, yPeriod, breaks); + // break; + case DataBuffer.TYPE_FLOAT: + case DataBuffer.TYPE_DOUBLE: + default: + calculate(accessor, raster, r, xPeriod, yPeriod, c); + break; + } + } + } + + void calculate( + PixelAccessor accessor, + Raster raster, + Rectangle rect, + int xPeriod, + int yPeriod, + Classification c) { + UnpackedImageData uid = accessor.getPixels(raster, rect, DataBuffer.TYPE_DOUBLE, false); + + double[][] doubleData = uid.getDoubleData(); + int pixelStride = uid.pixelStride * xPeriod; + int lineStride = uid.lineStride * yPeriod; + int[] offsets = uid.bandOffsets; + + for (int i = 0; i < bands.length; i++) { + int b = bands[i]; + + double[] data = doubleData[b]; + int lineOffset = offsets[b]; // line offset + + for (int h = 0; h < rect.height; h += yPeriod) { + int pixelOffset = lineOffset; // pixel offset + lineOffset += lineStride; + + for (int w = 0; w < rect.width; w += xPeriod) { + double d = data[pixelOffset]; + pixelOffset += pixelStride; + + // skip no data + if (noData != null && noData.equals(d)) { + continue; + } + + handleValue(d, c, i); + } + } + } + } + + protected abstract void handleValue(double d, Classification c, int band); + + protected abstract Classification createClassification(); + + protected Classification preCalculate() { + return null; + } + + protected abstract void postCalculate(Classification c, int band); + + /** Finds the first pixel at or after pos to be counted. */ + private int startPosition(int pos, int start, int Period) { + int t = (pos - start) % Period; + return t == 0 ? pos : pos + (Period - t); + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksRIF.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksRIF.java new file mode 100644 index 00000000..b653579f --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassBreaksRIF.java @@ -0,0 +1,139 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2016, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import static it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor.BAND_ARG; +import static it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor.EXTREMA_ARG; +import static it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor.HISTOGRAM_ARG; +import static it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor.HISTOGRAM_BINS; +import static it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor.METHOD_ARG; +import static it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor.NODATA_ARG; +import static it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor.NUM_CLASSES_ARG; +import static it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor.ROI_ARG; +import static it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor.X_PERIOD_ARG; +import static it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor.Y_PERIOD_ARG; + +import java.awt.*; +import java.awt.image.RenderedImage; +import java.awt.image.renderable.ParameterBlock; + +import javax.media.jai.CRIFImpl; +import javax.media.jai.ROI; + +/** + * RIF for the ClassBreaks operation. + * + *

This factory ends up creating on of the following operations based on the "method" parameter. + * + *

+ */ +public class ClassBreaksRIF extends CRIFImpl { + + public ClassBreaksRIF() { + super("ClassBreaks"); + } + + public RenderedImage create(ParameterBlock pb, RenderingHints hints) { + RenderedImage src = pb.getRenderedSource(0); + + int xStart = src.getMinX(); // default values + int yStart = src.getMinY(); + + Integer numBins = pb.getIntParameter(NUM_CLASSES_ARG); + ClassificationMethod method = (ClassificationMethod) pb.getObjectParameter(METHOD_ARG); + Double[][] extrema = (Double[][]) pb.getObjectParameter(EXTREMA_ARG); + ROI roi = (ROI) pb.getObjectParameter(ROI_ARG); + Integer[] bands = (Integer[]) pb.getObjectParameter(BAND_ARG); + Integer xPeriod = pb.getIntParameter(X_PERIOD_ARG); + Integer yPeriod = pb.getIntParameter(Y_PERIOD_ARG); + Double noData = (Double) pb.getObjectParameter(NODATA_ARG); + Boolean histogram = false; + if (pb.getNumParameters() >= 9) { + histogram = (Boolean) pb.getObjectParameter(HISTOGRAM_ARG); + } + Integer histogramBins = 256; + if (pb.getNumParameters() >= 10) + histogramBins = (Integer) pb.getObjectParameter(HISTOGRAM_BINS); + + switch (method) { + case EQUAL_INTERVAL: + return new EqualIntervalBreaksOpImage( + src, numBins, extrema, roi, bands, xStart, yStart, xPeriod, yPeriod, + noData); + case QUANTILE: + if (histogram) { + return new QuantileBreaksHistogramOpImage( + src, + numBins, + extrema, + roi, + bands, + xStart, + yStart, + xPeriod, + yPeriod, + noData, + histogramBins); + } else { + return new QuantileBreaksOpImage( + src, numBins, extrema, roi, bands, xStart, yStart, xPeriod, yPeriod, + noData); + } + case NATURAL_BREAKS: + if (histogram) { + return new NaturalBreaksHistogramOpImage( + src, + numBins, + extrema, + roi, + bands, + xStart, + yStart, + xPeriod, + yPeriod, + noData, + histogramBins); + } else { + return new NaturalBreaksOpImage( + src, numBins, extrema, roi, bands, xStart, yStart, xPeriod, yPeriod, + noData); + } + default: + throw new IllegalArgumentException(method.name()); + } + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/Classification.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/Classification.java new file mode 100644 index 00000000..e320665c --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/Classification.java @@ -0,0 +1,96 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2016, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + + +import com.sun.javafx.binding.Logging; + +import java.util.logging.Logger; + +/** Helper class used for raster classification. */ +public class Classification { + + static final Logger LOGGER = Logger.getLogger(Classification.class.getName()); + + /** classification method */ + ClassificationMethod method; + + /** the breaks */ + Double[][] breaks; + + /** min/max */ + Double[] min, max; + + public Classification(ClassificationMethod method, int numBands) { + this.method = method; + this.breaks = new Double[numBands][]; + this.min = new Double[numBands]; + this.max = new Double[numBands]; + } + + public ClassificationMethod getMethod() { + return method; + } + + public Number[][] getBreaks() { + return breaks; + } + + public void setBreaks(int b, Double[] breaks) { + this.breaks[b] = breaks; + } + + public Double getMin(int b) { + return min[b]; + } + + public void setMin(int b, Double min) { + this.min[b] = min; + } + + public Double getMax(int b) { + return max[b]; + } + + public void setMax(int b, Double max) { + this.max[b] = max; + } + + public void print() { + for (int i = 0; i < breaks.length; i++) { + for (Double d : breaks[i]) { + LOGGER.info(String.valueOf(d)); + } + } + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassificationMethod.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassificationMethod.java new file mode 100644 index 00000000..46122e16 --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/ClassificationMethod.java @@ -0,0 +1,48 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2016, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +/** Enumeration for method of classifying numeric values into ranges (classes). */ +public enum ClassificationMethod { + /** Classifies data into equally sized ranges. */ + EQUAL_INTERVAL, + + /** + * Classifies data into ranges such that the number of values falling into each range is + * approximately the same. + */ + QUANTILE, + + /** Classifies data into ranges such that ranges correspond to "clusters" of values. */ + NATURAL_BREAKS; +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/EqualIntervalBreaksOpImage.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/EqualIntervalBreaksOpImage.java new file mode 100644 index 00000000..12d6424c --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/EqualIntervalBreaksOpImage.java @@ -0,0 +1,112 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2016, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import java.awt.image.RenderedImage; + +import javax.media.jai.ROI; + +/** Classification op for the equal interval method. */ +public class EqualIntervalBreaksOpImage extends ClassBreaksOpImage { + + public EqualIntervalBreaksOpImage( + RenderedImage image, + Integer numClasses, + Double[][] extrema, + ROI roi, + Integer[] bands, + Integer xStart, + Integer yStart, + Integer xPeriod, + Integer yPeriod, + Double noData) { + + super(image, numClasses, extrema, roi, bands, xStart, yStart, xPeriod, yPeriod, noData); + } + + @Override + protected Classification createClassification() { + return new Classification(ClassificationMethod.EQUAL_INTERVAL, bands.length); + } + + @Override + protected Classification preCalculate() { + if (extrema != null) { + Classification c = createClassification(); + + // calculate the bins + for (int b = 0; b < bands.length; b++) { + double min = extrema[0][b]; + double max = extrema[1][b]; + + c.setMin(b, min); + c.setMax(b, max); + + calculateBreaks(c, b); + } + + return c; + } + return null; + } + + @Override + protected void handleValue(double d, Classification c, int band) { + c.setMin(band, c.getMin(band) == null ? d : Math.min(c.getMin(band), d)); + c.setMax(band, c.getMax(band) == null ? d : Math.max(c.getMax(band), d)); + } + + @Override + protected void postCalculate(Classification c, int band) { + calculateBreaks(c, band); + } + + void calculateBreaks(Classification c, int band) { + Double[] breaks = new Double[numClasses + 1]; + + // calculate the breaks + double min = c.getMin(band); + double max = c.getMax(band); + + double delta = (max - min) / (double) numClasses; + double start = min; + for (int j = 0; j < numClasses; j++) { + breaks[j] = start; + start += delta; + } + + // last value + breaks[numClasses] = max; + c.setBreaks(band, breaks); + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/HistogramClassification.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/HistogramClassification.java new file mode 100644 index 00000000..90d6c04c --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/HistogramClassification.java @@ -0,0 +1,196 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2018, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Classification that collects an histogram of the data instead of single values. Better suited for + * large datasets where collecting each single value would require too much memory. + */ +public class HistogramClassification extends Classification { + + public static final class Bucket { + int count; + double average; + double min; + double max; + + public Bucket(int count, double singleValue) { + this.count = count; + this.average = this.min = this.max = singleValue; + } + + public Bucket(int count, double average, double min, double max) { + this.count = count; + this.average = average; + this.min = min; + this.max = max; + } + + public int getCount() { + return count; + } + + public double getAverage() { + return average; + } + + public double getMin() { + return min; + } + + public double getMax() { + return max; + } + + @Override + public String toString() { + return "Bucket{" + + "count=" + + count + + ", average=" + + average + + ", min=" + + min + + ", max=" + + max + + '}'; + } + } + + private final double[] maximums; + private final int[][] bucketCounts; + private final double[][] bucketAverages; + private final boolean[][] bucketSingleValue; + private final double[] minimums; + private final double[] bucketSize; + + public HistogramClassification(int numBands, Double[][] extrema, int numBins) { + super(null, numBands); + if (extrema == null) { + throw new IllegalArgumentException( + "Histogram based classification methods need to be provided with the extrema parameter"); + } + checkExtremaArray(numBands, extrema, 0, "min"); + checkExtremaArray(numBands, extrema, 1, "max"); + for (int b = 0; b < numBands; b++) { + setMin(b, extrema[0][b]); + setMax(b, extrema[1][b]); + } + this.minimums = Arrays.stream(extrema[0]).mapToDouble(d -> d).toArray(); + this.maximums = Arrays.stream(extrema[1]).mapToDouble(d -> d).toArray(); + this.bucketSize = new double[numBands]; + for (int b = 0; b < numBands; b++) { + bucketSize[b] = (extrema[1][b] - extrema[0][b]) / numBins; + } + bucketCounts = new int[numBands][numBins]; + bucketAverages = new double[numBands][numBins]; + bucketSingleValue = new boolean[numBands][numBins]; + for (int b = 0; b < numBands; b++) { + Arrays.fill(bucketSingleValue[b], true); + } + } + + private void checkExtremaArray( + int numBands, Double[][] extrema, int minMax, String minMaxName) { + if (extrema[minMax].length < numBands) { + throw new IllegalArgumentException( + "Illegal extrema array, should have " + + minMaxName + + " array of " + + numBands + + " elements but only has " + + extrema[minMax].length + + " instead"); + } + } + + public void count(double value, int band) { + // throw away all elements outside of the desired range + double minimum = minimums[band]; + double maximum = maximums[band]; + if (value < minimum || value > maximum) { + return; + } + + // compute bucket involved + int idx = (int) ((value - minimum) / bucketSize[band]); + // update bucket count and bucket average + int[] bucketCount = bucketCounts[band]; + // on the max value the result might be the n+1 bucket + if (idx == bucketCount.length) { + idx--; + } + // increment count + bucketCount[idx]++; + double[] bucketsAverage = bucketAverages[band]; + // iterative mean here + double average = bucketsAverage[idx]; + if (bucketCount[idx] > 1) { + bucketSingleValue[band][idx] &= (average == value); + } + bucketsAverage[idx] = average + (value - average) / bucketCount[idx]; + } + + /** + * Returns a list of all non empty buckets + * + * @return + */ + public List getBuckets(int band) { + List buckets = new ArrayList<>(); + int[] histogram = bucketCounts[band]; + double[] bucketAverage = bucketAverages[band]; + double minimum = minimums[band]; + double size = bucketSize[band]; + for (int i = 0; i < histogram.length; i++) { + if (histogram[i] > 0) { + if (bucketSingleValue[band][i]) { + buckets.add(new Bucket(histogram[i], bucketAverage[i])); + } else { + buckets.add( + new Bucket( + histogram[i], + bucketAverage[i], + minimum + i * size, + minimum + (i + 1) * size)); + } + } + } + + return buckets; + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalBreaksHistogramOpImage.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalBreaksHistogramOpImage.java new file mode 100644 index 00000000..3f96f6a3 --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalBreaksHistogramOpImage.java @@ -0,0 +1,170 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2018, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import java.awt.image.RenderedImage; +import java.util.List; + +import javax.media.jai.ROI; + +import it.geosolutions.jaiext.classbreaks.HistogramClassification.Bucket; + +/** Classification op for the natural breaks method. */ +public class NaturalBreaksHistogramOpImage extends ClassBreaksOpImage { + + int numBins; + + public NaturalBreaksHistogramOpImage( + RenderedImage image, + Integer numClasses, + Double[][] extrema, + ROI roi, + Integer[] bands, + Integer xStart, + Integer yStart, + Integer xPeriod, + Integer yPeriod, + Double noData, + int numBins) { + super(image, numClasses, extrema, roi, bands, xStart, yStart, xPeriod, yPeriod, noData); + this.numBins = numBins; + } + + @Override + protected Classification createClassification() { + return new HistogramClassification(bands.length, extrema, numBins); + } + + @Override + protected void handleValue(double d, Classification c, int band) { + ((HistogramClassification) c).count(d, band); + } + + @Override + protected void postCalculate(Classification c, int band) { + HistogramClassification hc = (HistogramClassification) c; + List buckets = hc.getBuckets(band); + + final int k = numClasses; + final int m = buckets.size(); + + if (k >= m) { + // just return all the values + Double[] breaks = new Double[m + 1]; + for (int i = 0; i < m; i++) { + breaks[i] = buckets.get(i).getMin(); + } + breaks[m] = buckets.get(m - 1).getMax(); + c.setBreaks(band, breaks); + return; + } + + int[][] iwork = new int[m + 1][k + 1]; + double[][] work = new double[m + 1][k + 1]; + + for (int j = 1; j <= k; j++) { + // the first item is always in the first class! + iwork[0][j] = 1; + iwork[1][j] = 1; + // initialize work matirix + work[1][j] = 0; + for (int i = 2; i <= m; i++) { + work[i][j] = Double.MAX_VALUE; + } + } + + // calculate the class for each data item + for (int i = 1; i <= m; i++) { + // sum of data values + double s1 = 0; + // sum of squares of data values + double s2 = 0; + + double var = 0.0; + int totalCount = 0; + // consider all the previous values + for (int ii = 1; ii <= i; ii++) { + // index in to sorted data array + int i3 = i - ii + 1; + // remember to allow for 0 index + Bucket bucket = buckets.get(i3 - 1); + double average = bucket.getAverage(); + int count = bucket.getCount(); + // double squaredSum = bucket.getSquaredSum(); + // update running totals + // ... adding the sum of all squares contained in the bucket + s2 = s2 + (average * average * count); + // ... adding the sum of all values contained in the bucket + s1 += average * count; + totalCount += count; + double s0 = totalCount; + // calculate (square of) the variance + // (http://secure.wikimedia.org/wikipedia/en/wiki/Standard_deviation#Rapid_calculation_methods) + var = s2 - ((s1 * s1) / s0); + // System.out.println(s0+" "+s1+" "+s2); + // System.out.println(i+","+ii+" var "+var); + int ik = i3 - 1; + if (ik != 0) { + // not the last value + for (int j = 2; j <= k; j++) { + // for each class compare current value to var + previous value + // System.out.println("\tis "+work[i][j]+" >= "+(var + work[ik][j - 1])); + if (work[i][j] >= (var + work[ik][j - 1])) { + // if it is greater or equal update classification + iwork[i][j] = i3 - 1; + // System.out.println("\t\tiwork["+i+"]["+j+"] = "+i3); + work[i][j] = var + work[ik][j - 1]; + } + } + } + } + // store the latest variance! + iwork[i][1] = 1; + work[i][1] = var; + } + + Double[] breaks = new Double[k + 1]; + + // go through matrix and extract class breaks + int ik = m - 1; + breaks[k] = buckets.get(ik).getMax(); + for (int j = k; j >= 2; j--) { + int id = + (int) iwork[ik][j] - 1; // subtract one as we want inclusive breaks on the left? + breaks[j - 1] = buckets.get(id).getAverage(); + ik = (int) iwork[ik][j] - 1; + } + breaks[0] = buckets.get(0).getMin(); + hc.setBreaks(band, breaks); + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalBreaksOpImage.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalBreaksOpImage.java new file mode 100644 index 00000000..ea2c323d --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalBreaksOpImage.java @@ -0,0 +1,163 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2016, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import java.awt.image.RenderedImage; +import java.util.Collections; +import java.util.List; + +import javax.media.jai.ROI; + +/** Classification op for the natural breaks method. */ +public class NaturalBreaksOpImage extends ClassBreaksOpImage { + + public NaturalBreaksOpImage( + RenderedImage image, + Integer numClasses, + Double[][] extrema, + ROI roi, + Integer[] bands, + Integer xStart, + Integer yStart, + Integer xPeriod, + Integer yPeriod, + Double noData) { + super(image, numClasses, extrema, roi, bands, xStart, yStart, xPeriod, yPeriod, noData); + } + + @Override + protected Classification createClassification() { + return new NaturalClassification(bands.length); + } + + @Override + protected void handleValue(double d, Classification c, int band) { + if (extrema != null) { + double min = extrema[0][band]; + double max = extrema[1][band]; + + if (d < min || d > max) { + return; + } + } + ((NaturalClassification) c).count(d, band); + } + + @Override + protected void postCalculate(Classification c, int band) { + NaturalClassification nc = (NaturalClassification) c; + + List data = nc.getValues(band); + Collections.sort(data); + + final int k = numClasses; + final int m = data.size(); + + if (k >= m) { + // just return all the values + c.setBreaks(band, data.toArray(new Double[data.size()])); + return; + } + + int[][] iwork = new int[m + 1][k + 1]; + double[][] work = new double[m + 1][k + 1]; + + for (int j = 1; j <= k; j++) { + // the first item is always in the first class! + iwork[0][j] = 1; + iwork[1][j] = 1; + // initialize work matirix + work[1][j] = 0; + for (int i = 2; i <= m; i++) { + work[i][j] = Double.MAX_VALUE; + } + } + + // calculate the class for each data item + for (int i = 1; i <= m; i++) { + // sum of data values + double s1 = 0; + // sum of squares of data values + double s2 = 0; + + double var = 0.0; + // consider all the previous values + for (int ii = 1; ii <= i; ii++) { + // index in to sorted data array + int i3 = i - ii + 1; + // remember to allow for 0 index + double val = data.get(i3 - 1); + // update running totals + s2 = s2 + (val * val); + s1 += val; + double s0 = (double) ii; + // calculate (square of) the variance + // (http://secure.wikimedia.org/wikipedia/en/wiki/Standard_deviation#Rapid_calculation_methods) + var = s2 - ((s1 * s1) / s0); + // System.out.println(s0+" "+s1+" "+s2); + // System.out.println(i+","+ii+" var "+var); + int ik = i3 - 1; + if (ik != 0) { + // not the last value + for (int j = 2; j <= k; j++) { + // for each class compare current value to var + previous value + // System.out.println("\tis "+work[i][j]+" >= "+(var + work[ik][j - 1])); + if (work[i][j] >= (var + work[ik][j - 1])) { + // if it is greater or equal update classification + iwork[i][j] = i3 - 1; + // System.out.println("\t\tiwork["+i+"]["+j+"] = "+i3); + work[i][j] = var + work[ik][j - 1]; + } + } + } + } + // store the latest variance! + iwork[i][1] = 1; + work[i][1] = var; + } + + Double[] breaks = new Double[k + 1]; + + // go through matrix and extract class breaks + int ik = m - 1; + breaks[k] = data.get(ik); + for (int j = k; j >= 2; j--) { + int id = (int) iwork[ik][j] - 1; // subtract one as we want inclusive breaks on the + // left? + breaks[j - 1] = data.get(id); + ik = (int) iwork[ik][j] - 1; + } + breaks[0] = data.get(0); + nc.setBreaks(band, breaks); + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalClassification.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalClassification.java new file mode 100644 index 00000000..72b547cc --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/NaturalClassification.java @@ -0,0 +1,58 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2016, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import java.util.ArrayList; +import java.util.List; + +/** Helper class used for raster natural breaks classification. */ +public class NaturalClassification extends Classification { + + List[] values; + + public NaturalClassification(int numBands) { + super(ClassificationMethod.NATURAL_BREAKS, numBands); + values = new List[numBands]; + for (int i = 0; i < values.length; i++) { + values[i] = new ArrayList<>(); + } + } + + public void count(double value, int band) { + values[band].add(value); + } + + public List getValues(int band) { + return values[band]; + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileBreaksHistogramOpImage.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileBreaksHistogramOpImage.java new file mode 100644 index 00000000..e78ba17e --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileBreaksHistogramOpImage.java @@ -0,0 +1,109 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2018, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + + +import java.awt.image.RenderedImage; +import java.util.Iterator; +import java.util.List; +import java.util.TreeSet; + +import javax.media.jai.ROI; + +import it.geosolutions.jaiext.classbreaks.HistogramClassification.Bucket; + +/** + * Classification op for the quantile method, using histograms instead of a fully developed list of + * values + */ +public class QuantileBreaksHistogramOpImage extends ClassBreaksOpImage { + + int numBins; + + public QuantileBreaksHistogramOpImage( + RenderedImage image, + Integer numClasses, + Double[][] extrema, + ROI roi, + Integer[] bands, + Integer xStart, + Integer yStart, + Integer xPeriod, + Integer yPeriod, + Double noData, + int numBins) { + super(image, numClasses, extrema, roi, bands, xStart, yStart, xPeriod, yPeriod, noData); + this.numBins = numBins; + } + + @Override + protected Classification createClassification() { + return new HistogramClassification(bands.length, extrema, numBins); + } + + @Override + protected void handleValue(double d, Classification c, int band) { + ((HistogramClassification) c).count(d, band); + } + + @Override + protected void postCalculate(Classification c, int band) { + HistogramClassification hc = (HistogramClassification) c; + List buckets = hc.getBuckets(band); + + // calculate the number of values per class + int nvalues = buckets.stream().mapToInt(b -> b.getCount()).sum(); + int size = (int) Math.ceil(nvalues / (double) numClasses); + + // grab the key iterator + Iterator it = buckets.iterator(); + + TreeSet set = new TreeSet(); + Bucket e = it.next(); + + int classIdx = 1; + int count = 0; + set.add(e.getMin()); + while (classIdx < numClasses && it.hasNext()) { + count += e.getCount(); + e = it.next(); + + if (count >= (size * classIdx)) { + classIdx++; + set.add(e.getMin()); + } + } + set.add(buckets.get(buckets.size() - 1).getMax()); + hc.setBreaks(band, set.toArray(new Double[set.size()])); + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileBreaksOpImage.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileBreaksOpImage.java new file mode 100644 index 00000000..679db4c2 --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileBreaksOpImage.java @@ -0,0 +1,121 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2016, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import java.awt.image.RenderedImage; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeSet; + +import javax.media.jai.ROI; + +/** Classification op for the quantile method. */ +public class QuantileBreaksOpImage extends ClassBreaksOpImage { + + public QuantileBreaksOpImage( + RenderedImage image, + Integer numClasses, + Double[][] extrema, + ROI roi, + Integer[] bands, + Integer xStart, + Integer yStart, + Integer xPeriod, + Integer yPeriod, + Double noData) { + super(image, numClasses, extrema, roi, bands, xStart, yStart, xPeriod, yPeriod, noData); + } + + @Override + protected Classification createClassification() { + return new QuantileClassification(bands.length); + } + + @Override + protected void handleValue(double d, Classification c, int band) { + QuantileClassification qc = (QuantileClassification) c; + if (extrema != null) { + double min = extrema[0][band]; + double max = extrema[1][band]; + + if (d < min || d > max) { + return; + } + } + + qc.count(d, band); + } + + @Override + protected void postCalculate(Classification c, int band) { + QuantileClassification qc = (QuantileClassification) c; + + // get the total number of values + int nvalues = qc.getCount(band); + + // calculate the number of values per class + int size = (int) Math.ceil(nvalues / (double) numClasses); + + // grab the key iterator + Iterator> it = qc.getTable(band).entrySet().iterator(); + + TreeSet set = new TreeSet(); + Map.Entry e = it.next(); + + while (nvalues > 0) { + // add the next break + set.add(e.getKey()); + + for (int i = 0; i < size && nvalues > 0; i++) { + // consume the next value + int count = e.getValue(); + e.setValue(--count); + nvalues--; + + if (count == 0) { + // number of occurences of this entry exhausted, move to next + if (!it.hasNext()) { + break; + } + e = it.next(); + } + } + + if (nvalues == 0) { + // add the last value + set.add(e.getKey()); + } + } + qc.setBreaks(band, set.toArray(new Double[set.size()])); + } +} diff --git a/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileClassification.java b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileClassification.java new file mode 100644 index 00000000..cba4db61 --- /dev/null +++ b/jt-classbreaks/src/main/java/it/geosolutions/jaiext/classbreaks/QuantileClassification.java @@ -0,0 +1,84 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library + * http://www.geo-solutions.it/ + * Copyright 2018 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. + */ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2016, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.logging.Logger; + +/** Helper class used for raster quantile classification. */ +public class QuantileClassification extends Classification { + + static final Logger LOGGER = Logger.getLogger(Classification.class.getName()); + + int[] counts; + SortedMap[] tables; + + public QuantileClassification(int numBands) { + super(ClassificationMethod.QUANTILE, numBands); + counts = new int[numBands]; + tables = new SortedMap[numBands]; + } + + public void count(double value, int band) { + counts[band]++; + + SortedMap table = getTable(band); + + Integer count = table.get(value); + table.put(value, count != null ? new Integer(count + 1) : new Integer(1)); + } + + public SortedMap getTable(int band) { + SortedMap table = tables[band]; + if (table == null) { + table = new TreeMap(); + tables[band] = table; + } + return table; + } + + public int getCount(int band) { + return counts[band]; + } + + void printTable() { + for (int i = 0; i < tables.length; i++) { + SortedMap table = getTable(i); + for (Entry e : table.entrySet()) { + LOGGER.info(String.format("%f: %d", e.getKey(), e.getValue())); + } + } + } +} diff --git a/jt-classbreaks/src/main/resources/META-INF/registryFile.jaiext b/jt-classbreaks/src/main/resources/META-INF/registryFile.jaiext new file mode 100644 index 00000000..edad44da --- /dev/null +++ b/jt-classbreaks/src/main/resources/META-INF/registryFile.jaiext @@ -0,0 +1,9 @@ +# +# Image descriptors +# +descriptor it.geosolutions.jaiext.classbreaks.ClassBreaksDescriptor + +# +# RenderedImageFactories +# +rendered it.geosolutions.jaiext.classbreaks.ClassBreaksRIF it.geosolutions.jaiext ClassBreaks ClassBreaks diff --git a/jt-classbreaks/src/test/java/it/geosolutions/jaiext/classbreaks/ClassBreaksOpImageTest.java b/jt-classbreaks/src/test/java/it/geosolutions/jaiext/classbreaks/ClassBreaksOpImageTest.java new file mode 100644 index 00000000..afdffdb1 --- /dev/null +++ b/jt-classbreaks/src/test/java/it/geosolutions/jaiext/classbreaks/ClassBreaksOpImageTest.java @@ -0,0 +1,215 @@ +/* JAI-Ext - OpenSource Java Advanced Image Extensions Library +* http://www.geo-solutions.it/ +* Copyright 2018 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. +*/ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2018, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package it.geosolutions.jaiext.classbreaks; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +import java.awt.*; +import java.awt.image.RenderedImage; +import java.util.Arrays; + +import javax.media.jai.JAI; +import javax.media.jai.ParameterBlockJAI; +import javax.media.jai.RenderedOp; +import javax.media.jai.operator.ExtremaDescriptor; + +import it.geosolutions.jaiext.JAIExt; +import it.geosolutions.jaiext.testclasses.TestBase; +import it.geosolutions.jaiext.utilities.ImageUtilities; + +public class ClassBreaksOpImageTest extends TestBase { + + static final double EPS = 1e-3; + + static RenderedImage createImage() { + return ImageUtilities.createImageFromArray( + new Number[] {1, 1, 2, 3, 3, 8, 8, 9, 11, 14, 16, 24, 26, 26, 45, 53}, 4, 4); + } + + @Test + public void getMissingProperty() { + RenderedImage image = createImage(); + + ParameterBlockJAI pb = new ParameterBlockJAI(new ClassBreaksDescriptor()); + pb.addSource(image); + pb.setParameter("method", ClassificationMethod.QUANTILE); + pb.setParameter("numClasses", 5); + // raw creation like in CoverageClassStats, otherwise the issue gets masked by JAI wrappers + RenderedImage op = new ClassBreaksRIF().create(pb, null); + + // used to NPE here + Object roi = op.getProperty("ROI"); + assertEquals(Image.UndefinedProperty, roi); + } + + @Test + public void testEqualInterval() throws Exception { + RenderedImage image = createImage(); + + ParameterBlockJAI pb = new ParameterBlockJAI(new ClassBreaksDescriptor()); + pb.addSource(image); + pb.setParameter("method", ClassificationMethod.EQUAL_INTERVAL); + pb.setParameter("numClasses", 4); + RenderedImage op = JAI.create("ClassBreaks", pb, null); + Classification classification = + (Classification) op.getProperty(ClassBreaksDescriptor.CLASSIFICATION_PROPERTY); + assertNotNull(classification); + Number[] breaks = classification.getBreaks()[0]; + + assertEquals(5, breaks.length); + assertEquals(1, breaks[0].doubleValue(), EPS); + assertEquals(14, breaks[1].doubleValue(), EPS); + assertEquals(27, breaks[2].doubleValue(), EPS); + assertEquals(40, breaks[3].doubleValue(), EPS); + assertEquals(53, breaks[4].doubleValue(), EPS); + } + + @Test + public void testQuantileBreaks() throws Exception { + RenderedImage image = createImage(); + + ParameterBlockJAI pb = new ParameterBlockJAI(new ClassBreaksDescriptor()); + pb.addSource(image); + pb.setParameter("method", ClassificationMethod.QUANTILE); + pb.setParameter("numClasses", 4); + RenderedImage op = JAI.create("ClassBreaks", pb, null); + Classification classification = + (Classification) op.getProperty(ClassBreaksDescriptor.CLASSIFICATION_PROPERTY); + assertNotNull(classification); + Number[] breaks = classification.getBreaks()[0]; + + // 4 classes, 5 breaks + // 1, 1, 2, + // 3, 3, 8, 8, 9, + // 11, 14, 16, 24, + // 26, 26, 45, 53 + assertEquals(5, breaks.length); + assertEquals(1, breaks[0].doubleValue(), EPS); + assertEquals(3, breaks[1].doubleValue(), EPS); + assertEquals(11, breaks[2].doubleValue(), EPS); + assertEquals(26, breaks[3].doubleValue(), EPS); + assertEquals(53, breaks[4].doubleValue(), EPS); + } + + @Test + public void testQuantileBreaksHistogram() throws Exception { + RenderedImage image = createImage(); + + ParameterBlockJAI pb = new ParameterBlockJAI(new ClassBreaksDescriptor()); + pb.addSource(image); + pb.setParameter("method", ClassificationMethod.QUANTILE); + pb.setParameter("numClasses", 4); + pb.setParameter("extrema", getExtrema(image)); + pb.setParameter("histogram", true); + pb.setParameter("histogramBins", 100); + RenderedImage op = JAI.create("ClassBreaks", pb, null); + Classification classification = + (Classification) op.getProperty(ClassBreaksDescriptor.CLASSIFICATION_PROPERTY); + assertNotNull(classification); + Number[] breaks = classification.getBreaks()[0]; + + // 4 classes, 5 breaks (not the same as the exact count, slightly different approach, + // but still correct) + // 1, 1, 2, 3, 3, + // 8, 8, 9, + // 11, 14, 16, 24, + // 26, 26, 45, 53 + assertEquals(5, breaks.length); + assertEquals(1, breaks[0].doubleValue(), EPS); + assertEquals(8, breaks[1].doubleValue(), EPS); + assertEquals(11, breaks[2].doubleValue(), EPS); + assertEquals(26, breaks[3].doubleValue(), EPS); + assertEquals(53, breaks[4].doubleValue(), EPS); + } + + @Test + public void testNaturalBreaks() throws Exception { + RenderedImage image = createImage(); + + ParameterBlockJAI pb = new ParameterBlockJAI(new ClassBreaksDescriptor()); + pb.addSource(image); + pb.setParameter("method", ClassificationMethod.NATURAL_BREAKS); + pb.setParameter("numClasses", 4); + RenderedImage op = JAI.create("ClassBreaks", pb, null); + Classification classification = + (Classification) op.getProperty(ClassBreaksDescriptor.CLASSIFICATION_PROPERTY); + assertNotNull(classification); + Number[] breaks = classification.getBreaks()[0]; + + // 4 classes, 5 breaks + assertEquals(5, breaks.length); + assertEquals(1, breaks[0].doubleValue(), EPS); + assertEquals(3, breaks[1].doubleValue(), EPS); + assertEquals(16, breaks[2].doubleValue(), EPS); + assertEquals(26, breaks[3].doubleValue(), EPS); + assertEquals(53, breaks[4].doubleValue(), EPS); + } + + @Test + public void testNaturalBreaksHistogram() throws Exception { + RenderedImage image = createImage(); + + ParameterBlockJAI pb = new ParameterBlockJAI(new ClassBreaksDescriptor()); + pb.addSource(image); + pb.setParameter("method", ClassificationMethod.NATURAL_BREAKS); + pb.setParameter("numClasses", 4); + pb.setParameter("extrema", getExtrema(image)); + pb.setParameter("histogram", true); + pb.setParameter("histogramBins", 100); + RenderedImage op = JAI.create("ClassBreaks", pb, null); + Classification classification = + (Classification) op.getProperty(ClassBreaksDescriptor.CLASSIFICATION_PROPERTY); + assertNotNull(classification); + Number[] breaks = classification.getBreaks()[0]; + + // 4 classes, 5 breaks + assertEquals(5, breaks.length); + assertEquals(1, breaks[0].doubleValue(), EPS); + assertEquals(3, breaks[1].doubleValue(), EPS); + assertEquals(16, breaks[2].doubleValue(), EPS); + assertEquals(26, breaks[3].doubleValue(), EPS); + assertEquals(53, breaks[4].doubleValue(), EPS); + } + + private Double[][] getExtrema(RenderedImage image) { + RenderedOp extremaOp = ExtremaDescriptor.create(image, null, 1, 1, false, 1, null); + double[][] extrema= (double[][]) extremaOp.getProperty("extrema"); + Double[][] result = new Double[2][]; + result[0] = new Double[] {extrema[0][0]}; + result[1] = new Double[] {extrema[1][0]}; + return result; + } +} diff --git a/pom.xml b/pom.xml index 4db58620..44129cad 100644 --- a/pom.xml +++ b/pom.xml @@ -156,6 +156,7 @@ it.geosolutions.jaiext.threshold it.geosolutions.jaiext.clamp it.geosolutions.jaiext.shadedrelief + it.geosolutions.jaiext.classbreaks jt-concurrent-tile-cache, @@ -193,6 +194,7 @@ jt-threshold jt-clamp jt-shadedrelief + jt-classbreaks @@ -730,5 +732,6 @@ jt-scale2 jt-shadedrelief jt-jiffle + jt-classbreaks