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 caf730f09f98393f17910072af98105bb5012909
Author: jsorel <[email protected]>
AuthorDate: Tue Mar 25 11:50:05 2025 +0100

    Add image to polygon area algorithm
---
 .../main/org/apache/sis/image/ImageProcessor.java  |  65 ++++
 .../apache/sis/image/processing/polygon/Block.java |  40 +++
 .../sis/image/processing/polygon/Boundary.java     | 349 ++++++++++++++++++++
 .../sis/image/processing/polygon/Polygonize.java   | 356 +++++++++++++++++++++
 .../org/apache/sis/image/ImageProcessorTest.java   |  49 +++
 5 files changed, 859 insertions(+)

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 92d59fa7a4..89335d066d 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.DoublePredicate;
+import java.util.function.DoubleToIntFunction;
 import javax.measure.Quantity;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform1D;
@@ -56,8 +59,10 @@ import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.system.Modules;
 import org.apache.sis.image.processing.isoline.Isolines;
 import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.image.processing.polygon.Polygonize;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
+import org.opengis.referencing.operation.MathTransform2D;
 
 
 /**
@@ -1540,6 +1545,66 @@ public class ImageProcessor implements Cloneable {
         }
     }
 
+    /**
+     * 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 List} if polygon 
matching the predicate.
+     *
+     * @param  data       image providing source values.
+     * @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, DoublePredicate[] 
predicates, final MathTransform gridToCRS) throws TransformException {
+        final DoubleToIntFunction[] array = new 
DoubleToIntFunction[predicates.length];
+        for (int i = 0; i < predicates.length; i++) {
+            final DoublePredicate 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 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<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<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)));
+                    }
+                }
+            }
+        }
+        return result;
+    }
+    
     /**
      * Returns {@code true} if the given object is an image processor
      * of the same class with the same configuration.
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
new file mode 100644
index 0000000000..db67d850e7
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Block.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.sis.image.processing.polygon;
+
+/**
+ * Define a group of pixels with the same range.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+final class Block {
+
+    public int classe;
+    public int startX;
+    public int endX;
+    public int y;
+    public Boundary boundary;
+
+    public void reset(){
+        classe = -1;
+        startX = -1;
+        endX = -1;
+        y = -1;
+        boundary = null;
+    }
+
+}
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
new file mode 100644
index 0000000000..b89179a594
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Boundary.java
@@ -0,0 +1,349 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.sis.image.processing.polygon;
+
+import java.awt.Shape;
+import java.awt.geom.Area;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import org.apache.sis.measure.NumberRange;
+
+/**
+ * Define an "in construction" geometries.
+ * This type of objects contain a list of floeatings coordinates sequences
+ * that are progressivly merged together to obtain a polygon which might 
contain holes.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+final class Boundary {
+
+    //finished geometries
+    private final List<GeneralPath> holes = new ArrayList<>();
+
+    //in construction geometries
+    private final LinkedList<LinkedList<Point2D.Double>> floatings = new 
LinkedList<LinkedList<Point2D.Double>>();
+    final int classe;
+
+    public Boundary(final int classe){
+        this.classe = classe;
+    }
+
+    public void start(final int firstX, final int secondX, final int y){
+        if(firstX == secondX) throw new IllegalArgumentException("bugging 
algorithm");
+        final LinkedList<Point2D.Double> exterior = new 
LinkedList<Point2D.Double>();
+        exterior.addFirst(new Point2D.Double(firstX, y));
+        exterior.addFirst(new Point2D.Double(firstX, y+1));
+        exterior.addLast(new Point2D.Double(secondX, y));
+        exterior.addLast(new Point2D.Double(secondX, y+1));
+        floatings.add(exterior);
+
+        //checkValidity();
+    }
+
+    public void addFloating(final Point2D.Double from, final Point2D.Double 
to){
+        if (from.equals(to)) {
+            throw new IllegalArgumentException("bugging algorithm");
+        }
+        //System.err.println("Add Floating From: " + from + " New : " + to );
+
+        final LinkedList<Point2D.Double> ll = new LinkedList<Point2D.Double>();
+        ll.addFirst(to);
+        ll.addLast(from);
+        floatings.add(ll);
+
+        //checkValidity();
+    }
+
+    public void add(final Point2D.Double from, final Point2D.Double to){
+        if(from.equals(to)){
+            throw new IllegalArgumentException("bugging algorithm");
+        }
+
+        //System.err.println("Add From: " + from + " New : " + to );
+
+
+        for(final LinkedList<Point2D.Double> ll : floatings){
+            final Point2D.Double first = ll.getFirst();
+            if(first.equals(from)){
+
+                if(from.x == to.x && ll.size() > 1){
+                    final Point2D.Double second = ll.get(1);
+                    if(second.x == from.x){
+                        //points are aligned, just move the first point
+                        first.y = to.y;
+                    }else{
+                        //points are not aligned, must create a new point
+                        ll.addFirst(to);
+                    }
+                }else if(from.y == to.y && ll.size() > 1){
+                    final Point2D.Double second = ll.get(1);
+                    if(second.y == from.y){
+                        //points are aligned, just move the first point
+                        first.x = to.x;
+                    }else{
+                        //points are not aligned, must create a new point
+                        ll.addFirst(to);
+                    }
+                }else{
+                    ll.addFirst(to);
+                }
+
+                //checkValidity();
+                return;
+            }
+
+            final Point2D.Double last = ll.getLast();
+            if(last.equals(from)){
+                if(from.x == to.x && ll.size() > 1){
+                    final Point2D.Double second = ll.get(ll.size()-2);
+                    if(second.x == from.x){
+                        //points are aligned, just move the first point
+                        last.y = to.y;
+                    }else{
+                        //points are not aligned, must create a new point
+                        ll.addLast(to);
+                    }
+                }else if(from.y == to.y && ll.size() > 1){
+                    final Point2D.Double second = ll.get(ll.size()-2);
+                    if(second.y == from.y){
+                        //points are aligned, just move the first point
+                        last.x = to.x;
+                    }else{
+                        //points are not aligned, must create a new point
+                        ll.addLast(to);
+                    }
+                }else{
+                    ll.addLast(to);
+                }
+
+                //checkValidity();
+                return;
+            }
+        }
+
+        //checkValidity();
+        throw new IllegalArgumentException("bugging algorithm");
+    }
+
+    public Shape link(final Point2D.Double from, final Point2D.Double to){
+        if(from.equals(to)){
+            throw new IllegalArgumentException("bugging algorithm : " + to);
+        }
+
+        //System.err.println("Link From: " + from + " to : " + to );
+
+
+        LinkedList<Point2D.Double> fromList = null;
+        boolean fromStart = false;
+        LinkedList<Point2D.Double> toList = null;
+        boolean toStart = false;
+
+        for(final LinkedList<Point2D.Double> ll : floatings){
+
+            if(fromList == null){
+                final Point2D.Double first = ll.getFirst();
+                final Point2D.Double last = ll.getLast();
+                if(first.equals(from)){
+                    fromStart = true;
+                    fromList = ll;
+                }else if(last.equals(from)){
+                    fromStart = false;
+                    fromList = ll;
+                }
+            }
+
+            if(toList == null){
+                final Point2D.Double first = ll.getFirst();
+                final Point2D.Double last = ll.getLast();
+                if(first.equals(to)){
+                    toStart = true;
+                    toList = ll;
+                }else if(last.equals(to)){
+                    toStart = false;
+                    toList = ll;
+                }
+            }
+
+            if(fromList != null && toList != null) break;
+        }
+
+
+        if(fromList != null && toList != null){
+            if(fromList == toList){
+                //same list finish it
+                //checkValidity();
+                return finish(fromList);
+            }else{
+                combine(fromList, fromStart, toList, toStart);
+                //checkValidity();
+                return null;
+            }
+
+        }else if(fromList != null ){
+            add(from, to);
+            //checkValidity();
+            return null;
+        }else if(toList != null){
+            add(to, from);
+            //checkValidity();
+            return null;
+        }
+
+        //checkValidity();
+        throw new IllegalArgumentException("bugging algorithm");
+    }
+
+    private void combine(final LinkedList<Point2D.Double> fromList, final 
boolean fromStart,
+                         final LinkedList<Point2D.Double> toList, final 
boolean toStart){
+
+        if(fromStart){
+            if(toStart){
+                while(!toList.isEmpty()){
+                    fromList.addFirst(toList.pollFirst());
+                }
+            }else{
+                while(!toList.isEmpty()){
+                    fromList.addFirst(toList.pollLast());
+                }
+            }
+
+        }else{
+            if(toStart){
+                while(!toList.isEmpty()){
+                    fromList.addLast(toList.pollFirst());
+                }
+            }else{
+                while(!toList.isEmpty()){
+                    fromList.addLast(toList.pollLast());
+                }
+            }
+        }
+
+        floatings.remove(toList);
+        //checkValidity();
+    }
+
+
+    public void checkValidity(){
+
+        //check for list with less than 2 elements
+        for(LinkedList<Point2D.Double> ll : floatings){
+            if(ll.size() < 2){
+                //System.err.println(">>>> ERROR : " + this.toString());
+                throw new IllegalArgumentException("What ? a list with less 
than 2 elements, not valid !");
+            }
+        }
+
+        //check for diagonal cases
+        for(LinkedList<Point2D.Double> ll : floatings){
+            Point2D.Double last = ll.get(0);
+            for(int i=1;i<ll.size();i++){
+                Point2D.Double current = ll.get(i);
+                if(last.x != current.x && last.y != current.y){
+                    //System.err.println(">>>> ERROR : " + this.toString());
+                    throw new IllegalArgumentException("What ? a diagonal, not 
valid !");
+                }
+                last = current;
+            }
+        }
+
+    }
+
+
+
+    private Shape finish(final LinkedList<Point2D.Double> coords){
+
+        if(floatings.size() == 1){
+            //closing the polygon enveloppe
+            Shape geom = toClosedShape(coords);
+            //ring.setUserData(range);
+            floatings.remove(coords);
+            
+            if (!holes.isEmpty()) {
+                Area area = new Area(geom);
+                for (Shape hole : holes) {
+                    area.subtract(new Area(hole));
+                }
+                geom = area;
+            }
+            
+            return geom;
+        }else{
+            //closing a hole in the geometry
+            GeneralPath ring = toClosedShape(coords);
+            holes.add(ring);
+            floatings.remove(coords);
+            return null;
+        }
+
+    }
+
+    private static void reverse(final Point2D.Double[] array){
+        for(int l=0, r=array.length-1; l<r; l++, r--) {
+            Point2D.Double temp = array[l];
+            array[l] = array[r];
+            array[r] = temp;
+        }
+    }
+    
+    private GeneralPath toClosedShape(List<Point2D.Double> points) {
+        final GeneralPath path = new GeneralPath();
+        Point2D.Double start = points.get(0);
+        path.moveTo(start.x, start.y);
+        for (int i = 1, n = points.size(); i < n; i++) {
+            Point2D.Double pt = points.get(i);
+            path.lineTo(pt.x, pt.y);
+        }
+        path.closePath();
+        
+        return path;
+    }
+
+    public void merge(final Boundary candidate){
+        //merge the floating sequences
+        this.floatings.addAll(candidate.floatings);
+        //merge the holes
+        this.holes.addAll(candidate.holes);
+        candidate.floatings.clear();
+        candidate.holes.clear();
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("Boundary : ");
+        sb.append(classe);
+
+        for(LinkedList<Point2D.Double> coords : floatings){
+            sb.append("  \t{");
+            for(Point2D.Double c : coords){
+                
sb.append('[').append((int)c.x).append(';').append((int)c.y).append(']');
+            }
+            sb.append('}');
+        }
+
+        return sb.toString();
+    }
+
+    public boolean isEmpty(){
+        return floatings.isEmpty();
+    }
+
+}
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
new file mode 100644
index 0000000000..9c1a62f341
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/processing/polygon/Polygonize.java
@@ -0,0 +1,356 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.sis.image.processing.polygon;
+
+import java.awt.Point;
+import java.awt.Shape;
+import java.awt.geom.Point2D;
+import java.awt.image.RenderedImage;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.DoubleToIntFunction;
+import org.apache.sis.image.PixelIterator;
+import org.opengis.coverage.grid.SequenceType;
+
+
+/**
+ * Process to extract Polygon from an image.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Polygonize {
+
+    private static final int LAST_LINE = 0;
+    private static final int CURRENT_LINE = 1;
+
+    //last line cache boundary
+    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
+    private Boundary[][][] buffers;
+
+    //current pixel block
+    private Block[] blocks;
+
+    private final RenderedImage image;
+    private final DoubleToIntFunction[] classifiers;
+    
+    /**
+     *
+     * @param coverage coverage to process
+     * @param ranges data value ranges
+     * @param band coverage band to process
+     */
+    public Polygonize(RenderedImage image, DoubleToIntFunction[] classifiers){
+        this.image = image;
+        this.classifiers = classifiers;
+    }
+    
+    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 DoubleToIntFunction[] classifiers = new 
DoubleToIntFunction[nbBand];
+        for (int band = 0; band < nbBand; band++) {            
+            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.
+        */
+        final Point gridPosition = new Point(0, 0);
+        double[] pixel = null;
+
+        final int width = image.getWidth();
+        final int height = image.getHeight();
+        buffers = new Boundary[nbBand][2][width];
+        
+        for (int y = 0; y < height; y++) {
+            iter.moveTo(0, y);
+            gridPosition.y = y;
+            for (int x = 0; x < width; x++) {
+                gridPosition.x = x;
+                pixel = iter.getPixel(pixel);
+                for (int band = 0; band < nbBand; band++) {
+                    append(classifiers[band], polygons.get(band), 
buffers[band], blocks[band], gridPosition, pixel[band]);
+                }
+                iter.next();
+            }
+                        
+            //insert last geometry
+            for (int band = 0; band < nbBand; band++) {
+                constructBlock(polygons.get(band), blocks[band], 
buffers[band]);
+                
+                //flip buffers, reuse old buffer line.
+                Boundary[] oldLine = buffers[band][LAST_LINE];
+                buffers[band][LAST_LINE] = buffers[band][CURRENT_LINE];
+                buffers[band][CURRENT_LINE] = oldLine;
+
+                blocks[band].reset();    
+            }
+        
+        }
+        
+        //we have finish, close all geometries
+        gridPosition.y += 1;
+        for (int band = 0; band < nbBand; band++) {
+            for(int i = 0; i < buffers[band][LAST_LINE].length;i++) {
+                Shape poly = buffers[band][LAST_LINE][i].link(
+                        new Point2D.Double(i, gridPosition.y),
+                        new Point2D.Double(i+1, gridPosition.y)
+                        );
+                if (poly != null) {
+                    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<Integer, List<Shape>>> copy = new ArrayList<>(polygons);
+        polygons.clear();        
+        return copy;
+    }
+
+    private static void append(DoubleToIntFunction classifier, Map<Integer, 
List<Shape>> results, final Boundary[][] buffers, final Block block, final 
Point point, Number value) {
+
+        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.classe != -1) {
+            //last pixel was in a different range, save it's geometry
+            constructBlock(results, block, buffers);
+        }
+
+        //start a pixel serie
+        block.classe = classe;
+        block.startX = point.x;
+        block.endX = point.x;
+        block.y = point.y;
+    }
+
+    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.classe);
+            boundary.start(block.startX, block.endX+1, block.y);
+
+            for(int i=block.startX; i<=block.endX; i++) {
+                buffers[CURRENT_LINE][i] = boundary;
+            }
+        }else{
+            Boundary currentBoundary = null;
+
+            //first pass to close unfriendly blocks 
----------------------------
+            for (int i=block.startX; i<=block.endX;) {
+                final Boundary candidate = buffers[LAST_LINE][i];
+                final int[] candidateExtent = findExtent(buffers, i);
+
+                //do not treat same blockes here
+                if (candidate.classe != block.classe) {
+                    //System.err.println("A different block extent : "+ 
candidateExtent[0] + " " + candidateExtent[1]);
+                    //System.err.println("before :" + candidate.toString());
+
+                    if (candidateExtent[0] >= block.startX && 
candidateExtent[1] <= block.endX) {
+                        //block overlaps completly candidate
+                        final Shape poly = candidate.link(
+                                new Point2D.Double(candidateExtent[0], 
block.y),
+                                new Point2D.Double(candidateExtent[1]+1, 
block.y)
+                                );
+                        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) {
+                            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());
+                }
+
+                i = candidateExtent[1]+1;
+            }
+
+            //second pass to fuse with friendly blocks 
-------------------------
+
+            //we first merge the last line boundary if needed
+            int firstAnchor = Integer.MAX_VALUE;
+            int lastAnchor = Integer.MIN_VALUE;
+
+            for (int i = block.startX; i <= block.endX; ) {
+                final Boundary candidate = buffers[LAST_LINE][i];
+                final int[] candidateExtent = findExtent(buffers, i);
+
+                //do not treat different blocks here
+                if (candidate.classe == block.classe) {
+
+                    if (currentBoundary == null) {
+                        //set the current boundary, will expend this one
+                        currentBoundary = candidate;
+                    } else if(currentBoundary != null) {
+                        if(currentBoundary != candidate) {
+                            //those two blocks doesnt belong to the same 
boundaries, we must merge them
+                            currentBoundary.merge(candidate);
+                        }
+                        currentBoundary.link(
+                            new Point2D.Double(lastAnchor, block.y),
+                            new Point2D.Double(candidateExtent[0], block.y)
+                            );
+
+                        replaceInLastLigne(buffers, candidate, 
currentBoundary);
+                    }
+
+                    if (candidateExtent[0] < firstAnchor) {
+                        firstAnchor = candidateExtent[0];
+                    }
+                    lastAnchor = candidateExtent[1]+1;
+                }
+
+                i = candidateExtent[1]+1;
+            }
+
+            if (currentBoundary == null) {
+                //no previous friendly boundary to link with
+                //make a new one
+                currentBoundary = new Boundary(block.classe);
+                currentBoundary.start(block.startX, block.endX+1, block.y);
+            } else {
+                if (firstAnchor < block.startX) {
+                    //the previous block has created a floating sequence to 
this end
+                    firstAnchor = block.startX;
+                }
+
+                //add the coordinates
+                if (firstAnchor == block.startX) {
+                    currentBoundary.add(
+                        new Point2D.Double(firstAnchor, block.y),
+                        new Point2D.Double(block.startX, block.y+1)
+                        );
+                } else {
+                    currentBoundary.add(
+                        new Point2D.Double(firstAnchor, block.y),
+                        new Point2D.Double(block.startX, block.y)
+                        );
+                    currentBoundary.add(
+                        new Point2D.Double(block.startX, block.y),
+                        new Point2D.Double(block.startX, block.y+1)
+                        );
+                }
+
+                if (block.endX+1 >= lastAnchor) {
+                    if (lastAnchor == block.endX+1) {
+                        currentBoundary.add(
+                            new Point2D.Double(lastAnchor, block.y),
+                            new Point2D.Double(block.endX+1, block.y+1)
+                            );
+                    } else {
+                        currentBoundary.add(
+                            new Point2D.Double(lastAnchor, block.y),
+                            new Point2D.Double(block.endX+1, block.y)
+                            );
+                        currentBoundary.add(
+                            new Point2D.Double(block.endX+1, block.y),
+                            new Point2D.Double(block.endX+1, block.y+1)
+                            );
+                    }
+                } else {
+                    currentBoundary.addFloating(
+                            new Point2D.Double(block.endX+1, block.y),
+                            new Point2D.Double(block.endX+1, block.y+1)
+                            );
+                }
+            }
+
+            //fill in the current line 
-----------------------------------------
+
+            for (int i = block.startX; i <= block.endX; i++) {
+                if (currentBoundary.isEmpty()) {
+                    throw new IllegalArgumentException("An empty boundary 
inserted ? not possible.");
+                }
+
+                buffers[CURRENT_LINE][i] = currentBoundary;
+            }
+
+        }
+
+    }
+
+    private static void replaceInLastLigne(final Boundary[][] buffers, final 
Boundary old, final Boundary newone) {
+        for (int i = 0, n = buffers[LAST_LINE].length; i < n; i++) {
+            if (buffers[LAST_LINE][i] == old) {
+                buffers[LAST_LINE][i] = newone;
+            }
+
+            if (buffers[CURRENT_LINE][i] == old) {
+                buffers[CURRENT_LINE][i] = newone;
+            }
+        }
+    }
+
+
+    private static int[] findExtent(final Boundary[][] buffers, final int 
index) {
+        final int[] extent = new int[]{index,index};
+        final Boundary bnd = buffers[LAST_LINE][index];
+
+        while (extent[0] > 0 && buffers[LAST_LINE][ extent[0]-1 ] == bnd) {
+            extent[0]--;
+        }
+
+        while (extent[1] < buffers[LAST_LINE].length-1 && buffers[LAST_LINE][ 
extent[1]+1 ] == bnd) {
+            extent[1]++;
+        }
+
+        return extent;
+    }
+
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageProcessorTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageProcessorTest.java
index 7803a1f37c..e27a8dd08d 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageProcessorTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageProcessorTest.java
@@ -20,9 +20,12 @@ import java.util.Map;
 import java.util.stream.IntStream;
 import java.awt.Shape;
 import java.awt.Rectangle;
+import java.awt.geom.PathIterator;
 import java.awt.image.Raster;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
+import java.util.List;
+import java.util.function.DoublePredicate;
 import org.opengis.referencing.operation.MathTransform;
 
 // Test dependencies
@@ -31,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.image.processing.isoline.IsolinesTest;
 import org.apache.sis.test.TestCase;
 import static org.apache.sis.test.TestUtilities.getSingleton;
+import org.opengis.referencing.operation.TransformException;
 
 
 /**
@@ -109,4 +113,49 @@ public final class ImageProcessorTest extends TestCase {
             IsolinesTest.verifyIsolineFromMultiCells(getSingleton(r.values()));
         } while ((parallel = !parallel) == true);
     }
+    
+    /**
+     * Tests {@link ImageProcessor#areas(RenderedImage, DoublePredicate[], 
MathTransform) }.
+     */
+    @Test
+    public void testAreas() throws TransformException {
+        final BufferedImage image = new BufferedImage(3, 3, 
BufferedImage.TYPE_BYTE_BINARY);
+        image.getRaster().setSample(1, 1, 0, 1);
+        boolean parallel = false;
+        do {
+            processor.setExecutionMode(parallel ? 
ImageProcessor.Mode.SEQUENTIAL : ImageProcessor.Mode.PARALLEL);
+            final DoublePredicate predicate = (double v) -> v == 1.0;
+            final List<Shape> list = getSingleton(processor.areas(image, new 
DoublePredicate[]{predicate}, null));
+            assertEquals(1, list.size());
+            final Shape shape = list.get(0);
+            
+            /*
+                     1      4
+               (1,2) +------+ (2,2)
+                     |      |
+                     |      |
+               (1,1) +------+ (2,1)
+                     2      3            
+            */
+            
+            final PathIterator pathIterator = shape.getPathIterator(null);
+            final double[] coords = new double[2];
+            assertEquals(PathIterator.SEG_MOVETO, 
pathIterator.currentSegment(coords));
+            assertArrayEquals(new double[]{1,2}, coords);
+            pathIterator.next();
+            assertEquals(PathIterator.SEG_LINETO, 
pathIterator.currentSegment(coords));
+            assertArrayEquals(new double[]{1,1}, coords);
+            pathIterator.next();
+            assertEquals(PathIterator.SEG_LINETO, 
pathIterator.currentSegment(coords));
+            assertArrayEquals(new double[]{2,1}, coords);
+            pathIterator.next();
+            assertEquals(PathIterator.SEG_LINETO, 
pathIterator.currentSegment(coords));
+            assertArrayEquals(new double[]{2,2}, coords);
+            pathIterator.next();
+            assertEquals(PathIterator.SEG_CLOSE, 
pathIterator.currentSegment(coords));
+            pathIterator.next();
+            assertTrue(pathIterator.isDone());
+            
+        } while ((parallel = !parallel) == true);
+    }
 }


Reply via email to