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

org.integratedmodelling.engine.geospace.contextualizers.FeatureExtractor Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 * Copyright (C) 2007, 2015:
 * 
 * - Ferdinando Villa  - integratedmodelling.org - any
 * other authors listed in @author annotations
 *
 * All rights reserved. This file is part of the k.LAB software suite, meant to enable
 * modular, collaborative, integrated development of interoperable data and model
 * components. For details, see http://integratedmodelling.org.
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms
 * of the Affero General Public License Version 3 or any later version.
 *
 * This program 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the Affero General Public License along with this
 * program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite
 * 330, Boston, MA 02111-1307, USA. The license is also available at:
 * https://www.gnu.org/licenses/agpl.html
 *******************************************************************************/
package org.integratedmodelling.engine.geospace.contextualizers;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.integratedmodelling.api.knowledge.IConcept;
import org.integratedmodelling.api.knowledge.IExpression;
import org.integratedmodelling.api.knowledge.IObservation;
import org.integratedmodelling.api.modelling.IActiveSubject;
import org.integratedmodelling.api.modelling.IExtent;
import org.integratedmodelling.api.modelling.IModel;
import org.integratedmodelling.api.modelling.INumericObserver;
import org.integratedmodelling.api.modelling.IObservableSemantics;
import org.integratedmodelling.api.modelling.IScale;
import org.integratedmodelling.api.modelling.IState;
import org.integratedmodelling.api.modelling.ISubject;
import org.integratedmodelling.api.modelling.IValueResolver;
import org.integratedmodelling.api.modelling.contextualization.ISubjectInstantiator;
import org.integratedmodelling.api.modelling.resolution.IResolutionScope;
import org.integratedmodelling.api.modelling.scheduling.ITransition;
import org.integratedmodelling.api.monitoring.IMonitor;
import org.integratedmodelling.api.monitoring.Messages;
import org.integratedmodelling.api.project.IProject;
import org.integratedmodelling.api.services.annotations.Prototype;
import org.integratedmodelling.api.space.IGrid;
import org.integratedmodelling.api.space.IGridMask;
import org.integratedmodelling.api.space.ISpatialExtent;
import org.integratedmodelling.common.configuration.KLAB;
import org.integratedmodelling.common.kim.expr.GroovyExpression;
import org.integratedmodelling.common.space.IGeometricShape;
import org.integratedmodelling.common.states.States;
import org.integratedmodelling.common.utils.CamelCase;
import org.integratedmodelling.common.visualization.VisualizationFactory;
import org.integratedmodelling.common.vocabulary.NS;
import org.integratedmodelling.common.vocabulary.ObservableSemantics;
import org.integratedmodelling.engine.geospace.Geospace;
import org.integratedmodelling.engine.geospace.literals.ShapeValue;
import org.integratedmodelling.engine.modelling.runtime.Scale;
import org.integratedmodelling.exceptions.KlabException;
import org.integratedmodelling.exceptions.KlabUnsupportedOperationException;
import org.integratedmodelling.exceptions.KlabValidationException;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.impl.CoordinateArraySequence;

import ij.IJ;
import ij.ImagePlus;
import ij.blob.Blob;
import ij.blob.ManyBlobs;
import ij.process.ImageProcessor;

@Prototype(
        id = "gis.extract-features",
        published = true,
        args = {
                "# select",
                Prototype.EXPRESSION,
                "# ignore-holes",
                Prototype.BOOLEAN,
                "# use-convex-hull",
                Prototype.BOOLEAN,
                "# select-top-fraction",
                Prototype.FLOAT,
                "# select-bottom-fraction",
                Prototype.FLOAT,
                "# source-state",
                Prototype.TEXT,
                "# create-point-features",
                Prototype.BOOLEAN },
        returnTypes = { NS.SUBJECT_INSTANTIATOR },
        argDescriptions = {
                "boolean expression to compute whether each point belongs to a feature",
                "do not create holes in extracted polygons (default false)",
                "compute the convex hull of each polygon (default false)",
                "select the specified top fraction of the source-state input (required)",
                "select the specified bottom fraction of the source-state input (required)",
                "specify the source numeric input for fraction selection",
                "create point features when only spanning four cells or less (default false)" })
public class FeatureExtractor implements ISubjectInstantiator, IValueResolver {

    IExpression            selector;
    IProject               project;
    IScale                 scale;
    IGrid                  grid;
    private IConcept       type;
    GeometryFactory        gfact               = new GeometryFactory();
    private IActiveSubject context;

    boolean                computeConvexHull   = false;
    boolean                ignoreHoles         = false;
    boolean                createPointFeatures = false;
    private IMonitor       monitor;

    /*
     * these are only set when "select-*-fraction" and "source-state" are given
     */
    double                 selectFraction      = Double.NaN;
    boolean                topFraction         = false;
    String                 sourceState         = null;

    public FeatureExtractor() {
    }

    public FeatureExtractor(IScale scale, IMonitor monitor, boolean computeConvexHull, boolean ignoreHoles,
            boolean createPointFeatures) {
        this.scale = scale;
        this.monitor = monitor;
        this.computeConvexHull = computeConvexHull;
        this.ignoreHoles = ignoreHoles;
        this.createPointFeatures = createPointFeatures;
    }

    @Override
    public boolean canDispose() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public void setContext(Map parameters, IModel model, IProject project) {

        if (parameters.containsKey("select")) {
            this.selector = new GroovyExpression(parameters.get("select").toString(), model);
        }
        
        if (parameters.containsKey("ignore-holes")) {
            this.ignoreHoles = (Boolean) parameters.get("ignore-holes");
        }
        if (parameters.containsKey("use-convex-hull")) {
            this.computeConvexHull = (Boolean) parameters.get("use-convex-hull");
        }
        if (parameters.containsKey("create-point-features")) {
            this.createPointFeatures = (Boolean) parameters.get("create-point-features");
        }

        if (parameters.containsKey("select-top-fraction")
                || parameters.containsKey("select-bottom-fraction")) {
            selectFraction = parameters.containsKey("select-top-fraction")
                    ? ((Number) parameters.get("select-top-fraction")).doubleValue()
                    : ((Number) parameters.get("select-bottom-fraction")).doubleValue();
            if (parameters.containsKey("source-state")) {
                this.sourceState = parameters.get("source-state").toString();
            }
            this.topFraction = parameters.containsKey("select-top-fraction");
        }

        this.project = project;
    }

    @Override
    public void initialize(IActiveSubject context, IResolutionScope resolutionContext, IModel model, Map expectedInputs, Map expectedOutputs, IMonitor monitor)
            throws KlabException {

        this.scale = context.getScale();
        if (!(scale.isSpatiallyDistributed() && scale.getSpace().getGrid() != null && ((scale
                .isTemporallyDistributed() && scale.getExtentCount() == 2) || (!scale
                        .isTemporallyDistributed() && scale.getExtentCount() == 1)))) {
            throw new KlabUnsupportedOperationException("feature extraction only works on purely spatial[/temporal] extents");
        }

        this.grid = this.scale.getSpace().getGrid();
        this.type = (IConcept) model.getObservable().getType();
        this.context = context;
        this.monitor = monitor;

    }

    @Override
    public Map createSubjects(IActiveSubject context, ITransition transition, Map inputs)
            throws KlabException {
        if (transition != null) {
            // TODO - only run if anything has changed, which should mean
            // inputs.size()
            // > 0, but
            // currently means always, so avoid.
            return null;
        }
        return createBlobs(inputs, transition);
    }

    private Map createBlobs(Map inputs, ITransition transition)
            throws KlabException {

        Map ret = new HashMap<>();

        /*
         * TODO we may want to keep the image around when it's reused.
         */
        ImagePlus image = IJ.createImage("blobs", "8-bit black", grid.getXCells(), grid.getYCells(), 1);
        ImageProcessor imp = image.getProcessor();
        boolean warned = false;
        
        IState fractionState = null;
        double[] limits = null;
        if (sourceState != null && !Double.isNaN(selectFraction)) {
            fractionState = inputs.get(sourceState);
            if (fractionState == null) {
                throw new KlabValidationException("state " + sourceState + " not found in inputs");
            }
            if (!(fractionState.getObserver() instanceof INumericObserver)) {
                throw new KlabValidationException("state " + sourceState + " must be numeric");
            }
            limits = VisualizationFactory.getBoundaries(fractionState, scale.getIndex(transition), false);
        }

        /*
         * apply to input over space to obtain boolean selector
         */
        Map parameters = new HashMap<>();
        for (int n : scale.getIndex(transition)) {

            Object o = null;

            if (fractionState != null) {

                o = Boolean.FALSE;
                double d = States.getDouble(fractionState, n);
                if (!Double.isNaN(d)) {
                    
                    double perc = 0;
                    if (topFraction) {
                        perc = (limits[1] - d)/(limits[1] - limits[0]);
                    } else {
                        perc = (d - limits[0])/(limits[1] - limits[0]);
                    }
                    o = perc <= selectFraction;
                }

            } else if (selector != null) {

                parameters.clear();
                for (String s : inputs.keySet()) {
                    o = States.get(inputs.get(s), n);
                    if (o instanceof Number && Double.isNaN(((Number) o).doubleValue())) {
                        o = null;
                    }
                    parameters.put(s, o);
                }

                o = selector.eval(parameters, monitor);
                if (!(o instanceof Boolean)) {
                    throw new KlabValidationException("feature extraction selector must return true/false");
                }
            } else if (!warned) {
                monitor.warn("no input for feature extractor: specify either select or select fraction");
                warned = true;
            }

            int spaceOffset = scale.getExtentOffset(scale.getSpace(), n);
            int[] xy = grid.getXYOffsets(spaceOffset);

            imp.set(xy[0], xy[1], ((Boolean) o) ? 0 : 255);
        }

        String baseName = CamelCase.toLowerCase(type.getLocalName(), '-');
        ManyBlobs blobs = new ManyBlobs(image);
        blobs.findConnectedComponents();
        int i = 1;
        int skipped = 0;
        for (Blob blob : blobs) {
            ISubject subject = createSubject(blob, baseName + "-" + i);
            if (subject != null) {
                ret.put(baseName + "-" + i, subject);
                i++;
            } else {
                skipped++;
            }
        }

        if (skipped > 0) {
            monitor.info("skipped " + skipped
                    + " features not meeting requirements", Messages.INFOCLASS_MODEL);
        }

        return ret;
    }

    private ISubject createSubject(Blob blob, String id) throws KlabException {

        /*
         * TODO apply filters, if any, and cull unsuitable candidates.
         */

        Geometry polygon = null;
        if (blob.getOuterContour().npoints < 4) {
            if (createPointFeatures) {
                polygon = getPoint(blob.getCenterOfGravity());
            }
        } else {

            /*
             * create spatial context
             */
            LinearRing shell = getLinearRing(blob.getOuterContour());
            if (shell == null) {
                return null;
            }

            /*
             * safest strategy - allows holes that overlap the perimeter
             */
            polygon = new Polygon(shell, null, gfact);
            polygon = polygon.buffer(0);
            if (computeConvexHull) {
                polygon = polygon.convexHull();
            }

            if (!ignoreHoles) {
                for (LinearRing hole : getLinearRings(blob.getInnerContours())) {
                    Geometry h = new Polygon(hole, null, gfact);
                    h = h.buffer(0);
                    polygon = polygon.difference(h);
                }
            }
        }

        /*
         * clip to context shape
         */
        if (polygon != null) {
            polygon = polygon.intersection(((IGeometricShape) scale.getSpace()).getGeometry());
        }

        if (polygon == null || polygon.isEmpty()) {
            return null;
        }

        ShapeValue shape = new ShapeValue(polygon, Geospace.getCRSFromID(scale.getSpace().getCRSCode()));

        /*
         * create subject
         */
        ISubject ret = context
                .newSubject(new ObservableSemantics(type), getScale(shape.asExtent(), context), id, KLAB
                        .p(NS.PART_OF));

        /*
         * TODO add states if requested
         */

        return ret;
    }

    private LinearRing[] getLinearRings(List rings) {
        ArrayList ret = new ArrayList<>();
        for (java.awt.Polygon p : rings) {
            LinearRing ring = getLinearRing(p);
            if (p != null) {
                ret.add(ring);
            }
        }
        return ret.toArray(new LinearRing[ret.size()]);
    }

    private Geometry getPoint(Point2D point2d) {

        int x = (int) point2d.getX();
        int y = (int) point2d.getY();
        double[] xy = grid.getCoordinates(grid.getOffset(x, y));
        return gfact.createPoint(new Coordinate(xy[0], xy[1]));
    }

    private LinearRing getLinearRing(java.awt.Polygon p) {

        if (p.npoints < 4) {
            return null;
        }

        ArrayList coords = new ArrayList<>();
        for (int i = 0; i < p.npoints; i++) {

            int x = p.xpoints[i];
            int y = p.ypoints[i];

            double[] xy = grid.getCoordinates(grid.getOffset(x, y));
            coords.add(new Coordinate(xy[0], xy[1]));
        }

        return new LinearRing(new CoordinateArraySequence(coords
                .toArray(new Coordinate[coords.size()])), gfact);
    }

    private IScale getScale(ISpatialExtent extent, ISubject context) throws KlabException {

        List exts = new ArrayList<>();
        for (IExtent e : context.getScale()) {
            if (e instanceof ISpatialExtent) {
                exts.add(extent);
            } else {
                exts.add(e);
            }
        }
        return new Scale(exts.toArray(new IExtent[exts.size()]));
    }

    public Collection extractShapes(IGridMask mask) {

        List ret = new ArrayList<>();

        grid = mask.getGrid();
        ImagePlus image = IJ.createImage("blobs", "8-bit black", grid.getXCells(), grid.getYCells(), 1);
        ImageProcessor imp = image.getProcessor();
        for (int x = 0; x < grid.getXCells(); x++) {
            for (int y = 0; y < grid.getYCells(); y++) {
                imp.set(x, y, mask.isActive(x, y) ? 0 : 255);
            }
        }

        ManyBlobs blobs = new ManyBlobs(image);
        blobs.findConnectedComponents();

        for (Blob blob : blobs) {

            Geometry polygon = null;
            if (blob.getOuterContour().npoints < 4) {
                if (createPointFeatures) {
                    polygon = getPoint(blob.getCenterOfGravity());
                }
            } else {
                /*
                 * create spatial context
                 */
                LinearRing shell = getLinearRing(blob.getOuterContour());
                if (shell == null) {
                    continue;
                }

                /*
                 * safest strategy - allows holes that overlap the perimeter
                 */
                polygon = new Polygon(shell, null, gfact);
                polygon = polygon.buffer(0);
                if (computeConvexHull) {
                    polygon = polygon.convexHull();
                }

                if (!ignoreHoles) {
                    for (LinearRing hole : getLinearRings(blob.getInnerContours())) {
                        Geometry h = new Polygon(hole, null, gfact);
                        h = h.buffer(0);
                        polygon = polygon.difference(h);
                    }
                }

                /*
                 * clip to context shape
                 */
                if (polygon != null) {
                    polygon = polygon.intersection(((IGeometricShape) scale.getSpace()).getGeometry());
                }

                if (polygon == null || polygon.isEmpty()) {
                    continue;
                }

                ShapeValue shape = new ShapeValue(polygon, Geospace
                        .getCRSFromID(scale.getSpace().getCRSCode()));
                ret.add(shape);
            }
        }

        return ret;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy