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