This is an automated email from the ASF dual-hosted git repository. jsorel pushed a commit to branch feat/image2polygon in repository https://gitbox.apache.org/repos/asf/sis.git
commit fd950b7257076eafbd0546adce7033ee8fa2ca57 Author: jsorel <johann.so...@geomatys.com> AuthorDate: Tue Mar 25 16:57:26 2025 +0100 Rework ImageProcessor areas operation, use Predicate and DoubleToIntFunction to classify points --- .../main/org/apache/sis/image/ImageProcessor.java | 56 +++++++--- .../apache/sis/image/processing/polygon/Block.java | 6 +- .../sis/image/processing/polygon/Boundary.java | 8 +- .../sis/image/processing/polygon/Polygonize.java | 122 ++++++++------------- 4 files changed, 94 insertions(+), 98 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java index 9ab54631c9..cb3c52f313 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java @@ -34,6 +34,9 @@ import java.awt.image.RenderedImage; import java.awt.image.ImagingOpException; import java.awt.image.IndexColorModel; import java.awt.image.WritableRenderedImage; +import java.util.ArrayList; +import java.util.function.DoubleToIntFunction; +import java.util.function.Predicate; import javax.measure.Quantity; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform1D; @@ -1543,32 +1546,55 @@ public class ImageProcessor implements Cloneable { } /** - * Generates area polygons at the specified ranges computed from data provided by the given image. + * Generates area polygons by grouping samples matching given predicate computed from data provided by the given image. * Polygons will be computed for every bands in the given image. - * For each band, the result is given as a {@code Map} where keys are the specified {@code ranges} - * and values are the polygons at the associated range. - * If there are no polygons for a given level, there will be no corresponding entry in the map. - * The provided {@code ranges} must not overlap each other. + * For each band, the result is given as a {@code List} if polygon matching the predicate. * * @param data image providing source values. - * @param ranges value ranges for which to compute polygones. An array should be provided for each band. - * If there is more bands than {@code ranges.length}, the last array is reused for - * all remaining bands. + * @param predicates predicate to indicate if a value is to be included in the shape + * @param gridToCRS transform from pixel coordinates to geometry coordinates, or {@code null} if none. + * Integer source coordinates are located at pixel centers. + * @return the polygons of samples matching the predicate. The {@code List} size is the number of bands. + * List values are the polygons as a Java2D {@link Shape}. + * @throws ImagingOpException if an error occurred during calculation. + */ + public List<List<Shape>> areas(final RenderedImage data, Predicate<Double>[] predicates, final MathTransform gridToCRS) throws TransformException { + final DoubleToIntFunction[] array = new DoubleToIntFunction[predicates.length]; + for (int i = 0; i < predicates.length; i++) { + final Predicate<Double> predicate = predicates[i]; + array[i] = (double value) -> predicate.test(value) ? 1 : 0; + } + final List<Map<Integer, List<Shape>>> result = areas(data, array, gridToCRS); + final List<List<Shape>> results = new ArrayList<>(); + for (Map<Integer, List<Shape>> map : result) { + List<Shape> lst = map.get(1); + if (lst == null) lst = new ArrayList<>(); + results.add(lst); + } + return results; + } + + /** + * Generates area polygons by grouping samples in the same classification computed from data provided by the given image. + * Polygons will be computed for every bands in the given image. + * For each band, the result is given as a {@code Map} where keys are the classifiers returned values. + * + * @param data image providing source values. + * @param classifiers generate a classification key for a sample values, those values are used as key in the returned map * @param gridToCRS transform from pixel coordinates to geometry coordinates, or {@code null} if none. * Integer source coordinates are located at pixel centers. - * @return the polygons for specified ranges in each band. The {@code List} size is the number of bands. - * For each band, the {@code Map} size is equal or less than {@code ranges[band].length}. - * Map keys are the specified ranges, excluding those for which there are no polygons. + * @return the polygons for specified classification keys in each band. The {@code List} size is the number of bands. + * Map keys are the returned keys from the classifiers. * Map values are the polygons as a Java2D {@link Shape}. * @throws ImagingOpException if an error occurred during calculation. */ - public List<Map<NumberRange,List<Shape>>> areas(final RenderedImage data, NumberRange[][] ranges, final MathTransform gridToCRS) throws TransformException { - final Polygonize polygonizer = new Polygonize(data, ranges); - final List<Map<NumberRange, List<Shape>>> result = polygonizer.polygones(); + public List<Map<Integer,List<Shape>>> areas(final RenderedImage data, DoubleToIntFunction[] classifiers, final MathTransform gridToCRS) throws TransformException { + final Polygonize polygonizer = new Polygonize(data, classifiers); + final List<Map<Integer, List<Shape>>> result = polygonizer.polygones(); if (gridToCRS != null && !gridToCRS.isIdentity()) { final MathTransform2D trs2d = MathTransforms.bidimensional(gridToCRS); - for (Map<NumberRange, List<Shape>> m : result) { + for (Map<Integer, List<Shape>> m : result) { for (List<Shape> lst : m.values()) { for (int i = 0, n = lst.size(); i < n; i++) { lst.set(i, trs2d.createTransformedShape(lst.get(i))); diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Block.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Block.java index 45cbea909d..db67d850e7 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Block.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Block.java @@ -16,8 +16,6 @@ */ package org.apache.sis.image.processing.polygon; -import org.apache.sis.measure.NumberRange; - /** * Define a group of pixels with the same range. * @@ -25,14 +23,14 @@ import org.apache.sis.measure.NumberRange; */ final class Block { - public NumberRange range; + public int classe; public int startX; public int endX; public int y; public Boundary boundary; public void reset(){ - range = null; + classe = -1; startX = -1; endX = -1; y = -1; diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Boundary.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Boundary.java index 5a4ce953e9..b89179a594 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Boundary.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Boundary.java @@ -40,10 +40,10 @@ final class Boundary { //in construction geometries private final LinkedList<LinkedList<Point2D.Double>> floatings = new LinkedList<LinkedList<Point2D.Double>>(); - final NumberRange range; + final int classe; - public Boundary(final NumberRange range){ - this.range = range; + public Boundary(final int classe){ + this.classe = classe; } public void start(final int firstX, final int secondX, final int y){ @@ -329,7 +329,7 @@ final class Boundary { @Override public String toString() { final StringBuilder sb = new StringBuilder("Boundary : "); - sb.append(range.toString()); + sb.append(classe); for(LinkedList<Point2D.Double> coords : floatings){ sb.append(" \t{"); diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Polygonize.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Polygonize.java index 1782f30357..9c1a62f341 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Polygonize.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Polygonize.java @@ -24,7 +24,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.apache.sis.measure.NumberRange; +import java.util.function.DoubleToIntFunction; import org.apache.sis.image.PixelIterator; import org.opengis.coverage.grid.SequenceType; @@ -40,7 +40,7 @@ public final class Polygonize { private static final int CURRENT_LINE = 1; //last line cache boundary - private final List<Map<NumberRange, List<Shape>>> polygons = new ArrayList<>(); + private final List<Map<Integer, List<Shape>>> polygons = new ArrayList<>(); //buffer[band][LAST_LINE] holds last line buffer //buffer[band][CURRENT_LINE] holds current line buffer @@ -50,7 +50,7 @@ public final class Polygonize { private Block[] blocks; private final RenderedImage image; - private final NumberRange[][] ranges; + private final DoubleToIntFunction[] classifiers; /** * @@ -58,32 +58,25 @@ public final class Polygonize { * @param ranges data value ranges * @param band coverage band to process */ - public Polygonize(RenderedImage image, NumberRange[][] ranges){ + public Polygonize(RenderedImage image, DoubleToIntFunction[] classifiers){ this.image = image; - this.ranges = ranges; + this.classifiers = classifiers; } - public List<Map<NumberRange, List<Shape>>> polygones() { + public List<Map<Integer, List<Shape>>> polygones() { final PixelIterator iter = new PixelIterator.Builder().setIteratorOrder(SequenceType.LINEAR).create(image); final int nbBand = iter.getNumBands(); blocks = new Block[nbBand]; - final NumberRange NaNRange = new NaNRange(); + final DoubleToIntFunction[] classifiers = new DoubleToIntFunction[nbBand]; for (int band = 0; band < nbBand; band++) { - final Map<NumberRange, List<Shape>> bandState = new HashMap<>(); - //add a range for Nan values. - bandState.put(NaNRange, new ArrayList<>()); - polygons.add(bandState); - - for (final NumberRange range : ranges[Math.min(band, ranges.length-1)]) { - bandState.put(range, new ArrayList<>()); - } - + final Map<Integer, List<Shape>> bandState = new HashMap<>(); + polygons.add(bandState); blocks[band] = new Block(); + classifiers[band] = this.classifiers[Math.max(this.classifiers.length-1, band)]; } - /* This algorithm create polygons which follow the contour of each pixel. The 0,0 coordinate will match the pixel corner. @@ -102,7 +95,7 @@ public final class Polygonize { gridPosition.x = x; pixel = iter.getPixel(pixel); for (int band = 0; band < nbBand; band++) { - append(polygons.get(band), buffers[band], blocks[band], gridPosition, pixel[band]); + append(classifiers[band], polygons.get(band), buffers[band], blocks[band], gridPosition, pixel[band]); } iter.next(); } @@ -130,58 +123,51 @@ public final class Polygonize { new Point2D.Double(i+1, gridPosition.y) ); if (poly != null) { - polygons.get(band).get(buffers[band][LAST_LINE][i].range).add(poly); + final int classe = buffers[band][LAST_LINE][i].classe; + final Map<Integer, List<Shape>> map = polygons.get(band); + List<Shape> lst = map.get(classe); + if (lst == null) { + lst = new ArrayList<>(); + map.put(classe, lst); + } + lst.add(poly); } } } //avoid memory use buffers = null; - final List<Map<NumberRange, List<Shape>>> copy = new ArrayList<>(polygons); - //remove the NaNRange - for (Map m : copy) { - m.remove(NaNRange); - } + final List<Map<Integer, List<Shape>>> copy = new ArrayList<>(polygons); polygons.clear(); return copy; } - private static void append(Map<NumberRange, List<Shape>> results, final Boundary[][] buffers, final Block block, final Point point, Number value) { - - //special case for NaN or null - final NumberRange valueRange; - if (value == null || Double.isNaN(value.doubleValue())) { - valueRange = new NaNRange(); - } else { - valueRange = results.keySet().stream() - .filter(range -> range.containsAny(value)) - .findAny() - .orElseThrow(() -> new IllegalArgumentException("Value not in any range :" + value)); - } + private static void append(DoubleToIntFunction classifier, Map<Integer, List<Shape>> results, final Boundary[][] buffers, final Block block, final Point point, Number value) { - if (valueRange.equals(block.range)) { - //last pixel was in the same range + final int classe = classifier.applyAsInt(value.doubleValue()); + if (classe == block.classe) { + //last pixel was in the same class block.endX = point.x; return; - } else if (block.range != null) { + } else if (block.classe != -1) { //last pixel was in a different range, save it's geometry constructBlock(results, block, buffers); } //start a pixel serie - block.range = valueRange; + block.classe = classe; block.startX = point.x; block.endX = point.x; block.y = point.y; } - private static void constructBlock(Map<NumberRange, List<Shape>> results, final Block block, final Boundary[][] buffers) { + private static void constructBlock(Map<Integer, List<Shape>> results, final Block block, final Boundary[][] buffers) { //System.err.println("BLOCK ["+block.startX+","+block.endX+"]"); if(block.y == 0) { //first line, the buffer is empty, must fill it - final Boundary boundary = new Boundary(block.range); + final Boundary boundary = new Boundary(block.classe); boundary.start(block.startX, block.endX+1, block.y); for(int i=block.startX; i<=block.endX; i++) { @@ -196,7 +182,7 @@ public final class Polygonize { final int[] candidateExtent = findExtent(buffers, i); //do not treat same blockes here - if (candidate.range != block.range) { + if (candidate.classe != block.classe) { //System.err.println("A different block extent : "+ candidateExtent[0] + " " + candidateExtent[1]); //System.err.println("before :" + candidate.toString()); @@ -206,13 +192,27 @@ public final class Polygonize { new Point2D.Double(candidateExtent[0], block.y), new Point2D.Double(candidateExtent[1]+1, block.y) ); - if(poly != null) results.get(candidate.range).add(poly); + if (poly != null) { + List<Shape> lst = results.get(candidate.classe); + if (lst == null) { + lst = new ArrayList<>(); + results.put(candidate.classe, lst); + } + lst.add(poly); + } } else { final Shape poly = candidate.link( new Point2D.Double( (block.startX<candidateExtent[0]) ? candidateExtent[0]: block.startX, block.y), new Point2D.Double( (block.endX>candidateExtent[1]) ? candidateExtent[1]+1: block.endX+1, block.y) ); - if (poly != null) results.get(candidate.range).add(poly); + if (poly != null) { + List<Shape> lst = results.get(candidate.classe); + if (lst == null) { + lst = new ArrayList<>(); + results.put(candidate.classe, lst); + } + lst.add(poly); + } } //System.err.println("after :" + candidate.toString()); @@ -232,9 +232,7 @@ public final class Polygonize { final int[] candidateExtent = findExtent(buffers, i); //do not treat different blocks here - if (candidate.range == block.range) { - //System.err.println("A firnet block extent : "+ candidateExtent[0] + " " + candidateExtent[1]); -// //System.err.println("before :" + candidate.toString()); + if (candidate.classe == block.classe) { if (currentBoundary == null) { //set the current boundary, will expend this one @@ -250,7 +248,6 @@ public final class Polygonize { ); replaceInLastLigne(buffers, candidate, currentBoundary); - //System.out.println("Merging : " + currentBoundary.toString()); } if (candidateExtent[0] < firstAnchor) { @@ -262,14 +259,10 @@ public final class Polygonize { i = candidateExtent[1]+1; } - if (currentBoundary != null) { - //System.err.println("before :" + currentBoundary.toString()); - } - if (currentBoundary == null) { //no previous friendly boundary to link with //make a new one - currentBoundary = new Boundary(block.range); + currentBoundary = new Boundary(block.classe); currentBoundary.start(block.startX, block.endX+1, block.y); } else { if (firstAnchor < block.startX) { @@ -278,7 +271,6 @@ public final class Polygonize { } //add the coordinates - //System.err.println("> first anchor : " +firstAnchor + " lastAnchor : " +lastAnchor); if (firstAnchor == block.startX) { currentBoundary.add( new Point2D.Double(firstAnchor, block.y), @@ -302,17 +294,14 @@ public final class Polygonize { new Point2D.Double(block.endX+1, block.y+1) ); } else { - //System.err.println("0 add :" + currentBoundary.toString()); currentBoundary.add( new Point2D.Double(lastAnchor, block.y), new Point2D.Double(block.endX+1, block.y) ); - //System.err.println("1 add:" + currentBoundary.toString()); currentBoundary.add( new Point2D.Double(block.endX+1, block.y), new Point2D.Double(block.endX+1, block.y+1) ); - //System.err.println("after add:" + currentBoundary.toString()); } } else { currentBoundary.addFloating( @@ -320,13 +309,8 @@ public final class Polygonize { new Point2D.Double(block.endX+1, block.y+1) ); } - - //System.err.println(currentBoundary.toString()); - } - //System.err.println("after :" + currentBoundary.toString()); - //fill in the current line ----------------------------------------- for (int i = block.startX; i <= block.endX; i++) { @@ -369,16 +353,4 @@ public final class Polygonize { return extent; } - private static class NaNRange extends NumberRange{ - - public NaNRange() { - super(Double.class, 0d, true, 0d, true); - } - - @Override - public boolean contains(final Comparable number) throws IllegalArgumentException { - return Double.isNaN(((Number) number).doubleValue()); - } - } - }