/* * Copyright (C) 2015 by Array Systems Computing Inc. http://www.array.ca * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * 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 GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package org.esa.s1tbx.fex.gpf.oceantools; import com.bc.ceres.core.ProgressMonitor; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.Point; import org.esa.snap.core.datamodel.Band; import org.esa.snap.core.datamodel.GeoPos; import org.esa.snap.core.datamodel.MetadataElement; import org.esa.snap.core.datamodel.PixelPos; import org.esa.snap.core.datamodel.PlainFeatureFactory; import org.esa.snap.core.datamodel.Product; import org.esa.snap.core.datamodel.ProductData; import org.esa.snap.core.datamodel.VectorDataNode; import org.esa.snap.core.datamodel.VirtualBand; import org.esa.snap.core.dataop.downloadable.XMLSupport; import org.esa.snap.core.gpf.Operator; import org.esa.snap.core.gpf.OperatorException; import org.esa.snap.core.gpf.OperatorSpi; import org.esa.snap.core.gpf.Tile; import org.esa.snap.core.gpf.annotations.OperatorMetadata; import org.esa.snap.core.gpf.annotations.Parameter; import org.esa.snap.core.gpf.annotations.SourceProduct; import org.esa.snap.core.gpf.annotations.TargetProduct; import org.esa.snap.core.util.ProductUtils; import org.esa.snap.core.util.StringUtils; import org.esa.snap.core.util.SystemUtils; import org.esa.snap.engine_utilities.datamodel.AbstractMetadata; import org.esa.snap.engine_utilities.gpf.OperatorUtils; import org.esa.snap.engine_utilities.gpf.TileIndex; import org.esa.snap.engine_utilities.util.ResourceUtils; import org.esa.snap.engine_utilities.util.VectorUtils; import org.geotools.feature.FeatureCollection; import org.jdom2.Document; import org.jdom2.Element; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import java.awt.*; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * The ship detection discrimination operator. False ship detections are eliminated based on simple target * measurements. The operator first clusters contiguous detected pixels into a single cluster and then * extracts the width and length information from the target. Based on these measurements and user input * discrimination criteria, targets that are too big or too small are eliminated. * <p/> * [1] D. J. Crisp, "The State-of-the-Art in Ship Detection in Synthetic Aperture Radar Imagery." DSTO-RR-0272, 2004-05. */ @OperatorMetadata(alias = "Object-Discrimination", category = "Radar/SAR Applications/Ocean Applications/Object Detection", authors = "Jun Lu, Luis Veci", version = "1.0", copyright = "Copyright (C) 2015 by Array Systems Computing Inc.", description = "Remove false alarms from the detected objects.") public class ObjectDiscriminationOp extends Operator { @SourceProduct(alias = "source") private Product sourceProduct; @TargetProduct private Product targetProduct = null; @Parameter(description = "Minimum target size", defaultValue = "50.0", label = "Minimum Target Size (m)") private double minTargetSizeInMeter = 50.0; @Parameter(description = "Maximum target size", defaultValue = "600.0", label = "Maximum Target Size (m)") private double maxTargetSizeInMeter = 600.0; private boolean clusteringPerformed = false; private int sourceImageWidth = 0; private int sourceImageHeight = 0; private double rangeSpacing = 0; private double azimuthSpacing = 0; private final Map<String, List<ShipRecord>> bandClusterLists = new HashMap<>(); private File targetReportFile = null; private SimpleFeatureType shipFeatureType; public static final String VECTOR_NODE_NAME = "ShipDetections"; private static final String STYLE_FORMAT = "fill:#ff0000; fill-opacity:0.2; stroke:#ff0000; stroke-opacity:1.0; stroke-width:1.0; symbol:circle"; public static final String ATTRIB_DETECTED_X = "Detected_x"; public static final String ATTRIB_DETECTED_Y = "Detected_y"; public static final String ATTRIB_DETECTED_LAT = "Detected_lat"; public static final String ATTRIB_DETECTED_LON = "Detected_lon"; public static final String ATTRIB_DETECTED_WIDTH = "Detected_width"; public static final String ATTRIB_DETECTED_LENGTH = "Detected_length"; public static final String ATTRIB_CORR_SHIP_LAT = "Corr_ship_lat"; public static final String ATTRIB_CORR_SHIP_LON = "Corr_ship_lon"; public static final String ATTRIB_AIS_MMSI = "AIS_MMSI"; public static final String ATTRIB_AIS_SHIP_NAME= "AIS_shipname"; private static final String PRODUCT_SUFFIX = "_SHP"; @Override public void initialize() throws OperatorException { try { final MetadataElement absRoot = AbstractMetadata.getAbstractedMetadata(sourceProduct); rangeSpacing = AbstractMetadata.getAttributeDouble(absRoot, AbstractMetadata.range_spacing); azimuthSpacing = AbstractMetadata.getAttributeDouble(absRoot, AbstractMetadata.azimuth_spacing); sourceImageWidth = sourceProduct.getSceneRasterWidth(); sourceImageHeight = sourceProduct.getSceneRasterHeight(); setTargetReportFilePath(); createTargetProduct(); shipFeatureType = createFeatureType(); } catch (Throwable e) { OperatorUtils.catchOperatorException(getId(), e); } } /** * Set absolute path for outputing target report file. */ private void setTargetReportFilePath() { final String fileName = sourceProduct.getName() + "_object_detection_report.xml"; targetReportFile = new File(ResourceUtils.getReportFolder(), fileName); } /** * Create target product. */ private void createTargetProduct() { targetProduct = new Product(sourceProduct.getName() + PRODUCT_SUFFIX, sourceProduct.getProductType(), sourceImageWidth, sourceImageHeight); ProductUtils.copyProductNodes(sourceProduct, targetProduct); addSelectedBands(); updateTargetProductMetadata(); } /** * Add the user selected bands to target product. * * @throws OperatorException The exceptions. */ private void addSelectedBands() throws OperatorException { final Band[] bands = sourceProduct.getBands(); for (Band srcBand : bands) { final String srcBandName = srcBand.getName(); final boolean copySourceImage = !srcBandName.contains(AdaptiveThresholdingOp.SHIPMASK_NAME); // copy all bands if (srcBand instanceof VirtualBand) { ProductUtils.copyVirtualBand(targetProduct, (VirtualBand) srcBand, srcBand.getName()); } else { ProductUtils.copyBand(srcBand.getName(), sourceProduct, targetProduct, copySourceImage); } } } /** * Save target report file path in the metadata. */ private void updateTargetProductMetadata() { final MetadataElement absTgt = AbstractMetadata.getAbstractedMetadata(targetProduct); absTgt.setAttributeString(AbstractMetadata.target_report_file, targetReportFile.getAbsolutePath()); } /** * Called by the framework in order to compute a tile for the given target band. * <p>The default implementation throws a runtime exception with the message "not implemented".</p> * * @param targetBand The target band. * @param targetTile The current tile associated with the target band to be computed. * @param pm A progress monitor which should be used to determine computation cancelation requests. * @throws OperatorException If an error occurs during computation of the target raster. */ @Override public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) throws OperatorException { try { final Rectangle targetTileRectangle = targetTile.getRectangle(); final int tx0 = targetTileRectangle.x; final int ty0 = targetTileRectangle.y; final int tw = targetTileRectangle.width; final int th = targetTileRectangle.height; //System.out.println("tx0 = " + tx0 + ", ty0 = " + ty0 + ", tw = " + tw + ", th = " + th); final int x0 = Math.max(tx0 - 10, 0); final int y0 = Math.max(ty0 - 10, 0); final int w = Math.min(tw + 20, sourceImageWidth); final int h = Math.min(th + 20, sourceImageHeight); final Rectangle sourceTileRectangle = new Rectangle(x0, y0, w, h); //System.out.println("x0 = " + x0 + ", y0 = " + y0 + ", w = " + w + ", h = " + h); final Band sourceBand = sourceProduct.getBand(targetBand.getName()); final int[][] pixelsScanned = new int[h][w]; final List<ShipRecord> clusterList = new ArrayList<>(); final Tile bitMaskTile = getSourceTile(sourceBand, sourceTileRectangle); final ProductData bitMaskData = bitMaskTile.getDataBuffer(); final TileIndex srcIndex = new TileIndex(bitMaskTile); // src and trg tile are different size final int maxy = ty0 + th; final int maxx = tx0 + tw; for (int ty = ty0; ty < maxy; ty++) { srcIndex.calculateStride(ty); for (int tx = tx0; tx < maxx; tx++) { if (pixelsScanned[ty - y0][tx - x0] == 0) { if (bitMaskData.getElemIntAt(srcIndex.getIndex(tx)) == 1) { final List<PixelPos> clusterPixels = new ArrayList<>(); clustering(tx, ty, x0, y0, w, h, bitMaskData, bitMaskTile, pixelsScanned, clusterPixels); final ShipRecord record = generateRecord(x0, y0, w, h, clusterPixels); final double size = Math.sqrt(record.length * record.length + record.width * record.width); if (size >= minTargetSizeInMeter && size <= maxTargetSizeInMeter) { clusterList.add(record); } } } } } if (!clusterList.isEmpty()) { AddShipRecordsAsVectors(clusterList); } List<ShipRecord> shipRecordList = bandClusterLists.get(targetBand.getName()); if (shipRecordList == null) { shipRecordList = new ArrayList<>(); bandClusterLists.put(targetBand.getName(), shipRecordList); } shipRecordList.addAll(clusterList); targetTile.setRawSamples(getSourceTile(sourceBand, targetTileRectangle).getRawSamples()); clusteringPerformed = true; } catch (Throwable e) { OperatorUtils.catchOperatorException(getId(), e); } } /** * Find pixels detected as target in a 3x3 window centered at a given point. * * @param xc The x coordinate of the given point. * @param yc The y coordinate of the given point. * @param x0 The x coordinate for the upper left corner point of the source rectangle. * @param y0 The y coordinate for the upper left corner point of the source rectangle. * @param w The width of the source rectangle. * @param h The height of the source rectangle. * @param bitMaskData The bit maks band data. * @param bitMaskTile The bit mask band tile. * @param pixelsScanned The binary array indicating which pixel in the tile has been scaned. * @param clusterPixels The list of pixels in the cluster. */ private static void clustering(final int xc, final int yc, final int x0, final int y0, final int w, final int h, final ProductData bitMaskData, final Tile bitMaskTile, final int[][] pixelsScanned, List<PixelPos> clusterPixels) { pixelsScanned[yc - y0][xc - x0] = 1; clusterPixels.add(new PixelPos(xc, yc)); final int[] x = {xc - 1, xc, xc + 1, xc - 1, xc + 1, xc - 1, xc, xc + 1}; final int[] y = {yc - 1, yc - 1, yc - 1, yc, yc, yc + 1, yc + 1, yc + 1}; for (int i = 0; i < 8; i++) { if (x[i] >= x0 && x[i] < x0 + w && y[i] >= y0 && y[i] < y0 + h && pixelsScanned[y[i] - y0][x[i] - x0] == 0 && bitMaskData.getElemIntAt(bitMaskTile.getDataBufferIndex(x[i], y[i])) == 1) { clustering(x[i], y[i], x0, y0, w, h, bitMaskData, bitMaskTile, pixelsScanned, clusterPixels); } } } /** * Generate a ship record for the detected cluster. * * @param x0 The x coordinate for the upper left corner point of the source rectangle. * @param y0 The y coordinate for the upper left corner point of the source rectangle. * @param w The width of the source rectangle. * @param h The height of the source rectangle. * @param clusterPixels The list of pixels in the cluster. * @return ShipRecord */ private ShipRecord generateRecord(final int x0, final int y0, final int w, final int h, List<PixelPos> clusterPixels) { int xMin = x0 + w - 1; int xMax = x0; int yMin = y0 + h - 1; int yMax = y0; for (PixelPos pixel : clusterPixels) { if (pixel.x < xMin) { xMin = (int) pixel.x; } if (pixel.x > xMax) { xMax = (int) pixel.x; } if (pixel.y < yMin) { yMin = (int) pixel.y; } if (pixel.y > yMax) { yMax = (int) pixel.y; } } final double xMid = (xMin + xMax) / 2.0; final double yMid = (yMin + yMax) / 2.0; final GeoPos geoPos = targetProduct.getSceneGeoCoding().getGeoPos(new PixelPos(xMid, yMid), null); final double width = (xMax - xMin + 1) * rangeSpacing; final double length = (yMax - yMin + 1) * azimuthSpacing; return new ShipRecord((int) xMid, (int) yMid, geoPos.lat, geoPos.lon, width, length); } /** * Output cluster information to file. */ @Override public void dispose() { if (!clusteringPerformed) { return; } writeBandClusterListsToFile(); } /** * Output cluster information to file. * * @throws OperatorException when can't save metadata */ private void writeBandClusterListsToFile() throws OperatorException { try { final Element root = new Element("Detection"); final Document doc = new Document(root); for (String bandName : bandClusterLists.keySet()) { final Element elem = new Element("targetsDetected"); elem.setAttribute("bandName", bandName); final List<ShipRecord> clusterList = bandClusterLists.get(bandName); for (ShipRecord rec : clusterList) { final Element subElem = new Element("target"); subElem.setAttribute(ATTRIB_DETECTED_X, String.valueOf(rec.x)); subElem.setAttribute(ATTRIB_DETECTED_Y, String.valueOf(rec.y)); subElem.setAttribute(ATTRIB_DETECTED_LAT, String.valueOf(rec.lat)); subElem.setAttribute(ATTRIB_DETECTED_LON, String.valueOf(rec.lon)); subElem.setAttribute(ATTRIB_DETECTED_WIDTH, String.valueOf(rec.width)); subElem.setAttribute(ATTRIB_DETECTED_LENGTH, String.valueOf(rec.length)); elem.addContent(subElem); } root.addContent(elem); } XMLSupport.SaveXML(doc, targetReportFile.getAbsolutePath()); } catch (IOException e) { SystemUtils.LOG.warning("Unable to save target report " + e.getMessage()); } } private SimpleFeatureType createFeatureType() { final List<AttributeDescriptor> attributeDescriptors = new ArrayList<>(); attributeDescriptors.add(VectorUtils.createAttribute(ATTRIB_DETECTED_X, Integer.class)); attributeDescriptors.add(VectorUtils.createAttribute(ATTRIB_DETECTED_Y, Integer.class)); attributeDescriptors.add(VectorUtils.createAttribute(ATTRIB_DETECTED_LAT, Double.class)); attributeDescriptors.add(VectorUtils.createAttribute(ATTRIB_DETECTED_LON, Double.class)); attributeDescriptors.add(VectorUtils.createAttribute(ATTRIB_DETECTED_WIDTH, Double.class)); attributeDescriptors.add(VectorUtils.createAttribute(ATTRIB_DETECTED_LENGTH, Double.class)); return VectorUtils.createFeatureType(targetProduct, VECTOR_NODE_NAME, attributeDescriptors); } private synchronized void AddShipRecordsAsVectors(final List<ShipRecord> clusterList) { VectorDataNode vectorDataNode = targetProduct.getVectorDataGroup().get(VECTOR_NODE_NAME); if (vectorDataNode == null) { vectorDataNode = new VectorDataNode(VECTOR_NODE_NAME, shipFeatureType); targetProduct.getVectorDataGroup().add(vectorDataNode); } final FeatureCollection<SimpleFeatureType, SimpleFeature> collection = vectorDataNode.getFeatureCollection(); final GeometryFactory geometryFactory = new GeometryFactory(); int c = collection.size(); for (ShipRecord rec : clusterList) { final String name = "target_" + StringUtils.padNum(c, 3, '0'); Point p = geometryFactory.createPoint(new Coordinate(rec.x, rec.y)); final SimpleFeature feature = PlainFeatureFactory.createPlainFeature(shipFeatureType, name, p, STYLE_FORMAT); feature.setAttribute(ATTRIB_DETECTED_X, rec.x); feature.setAttribute(ATTRIB_DETECTED_Y, rec.y); feature.setAttribute(ATTRIB_DETECTED_LAT, rec.lat); feature.setAttribute(ATTRIB_DETECTED_LON, rec.lon); feature.setAttribute(ATTRIB_DETECTED_WIDTH, rec.width); feature.setAttribute(ATTRIB_DETECTED_LENGTH, rec.length); collection.add(feature); c++; } } public static class ShipRecord { public final int x; public final int y; public final double lat; public final double lon; public final double width; public final double length; public double corr_lat; public double corr_lon; public int mmsi; public String shipName; public ShipRecord(final int x, final int y, final double lat, final double lon, final double width, final double length) { this.x = x; this.y = y; this.lat = lat; this.lon = lon; this.width = width; this.length = length; this.corr_lat = 0.0; this.corr_lon = 0.0; } } public static class AttributeInfo { public final String attributeName; public final Class attributeClass; public AttributeInfo(final String attributeName, final Class attributeClass) { this.attributeName = attributeName; this.attributeClass = attributeClass; } } /** * Operator SPI. */ public static class Spi extends OperatorSpi { public Spi() { super(ObjectDiscriminationOp.class); } } }