mirror of
https://github.com/JHUAPL/Terrasaur.git
synced 2026-01-09 14:28:04 -05:00
1024 lines
41 KiB
Java
1024 lines
41 KiB
Java
/*
|
|
* The MIT License
|
|
* Copyright © 2025 Johns Hopkins University Applied Physics Laboratory
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
package terrasaur.apps;
|
|
|
|
import java.awt.geom.Path2D;
|
|
import java.awt.geom.Point2D;
|
|
import java.io.File;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.PrintWriter;
|
|
import java.nio.charset.Charset;
|
|
import java.util.AbstractMap;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
import java.util.NavigableMap;
|
|
import java.util.NavigableSet;
|
|
import java.util.Set;
|
|
import java.util.TreeMap;
|
|
import java.util.TreeSet;
|
|
import org.apache.commons.cli.CommandLine;
|
|
import org.apache.commons.cli.Option;
|
|
import org.apache.commons.cli.Options;
|
|
import org.apache.commons.io.FileUtils;
|
|
import org.apache.commons.io.FilenameUtils;
|
|
import org.apache.commons.math3.geometry.euclidean.threed.Rotation;
|
|
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
|
|
import org.apache.commons.math3.util.Pair;
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
import picante.math.vectorspace.RotationMatrixIJK;
|
|
import terrasaur.smallBodyModel.SBMTStructure;
|
|
import terrasaur.smallBodyModel.SmallBodyModel;
|
|
import terrasaur.templates.TerrasaurTool;
|
|
import terrasaur.utils.*;
|
|
import terrasaur.utils.math.MathConversions;
|
|
import vtk.vtkCellArray;
|
|
import vtk.vtkDoubleArray;
|
|
import vtk.vtkIdList;
|
|
import vtk.vtkPoints;
|
|
import vtk.vtkPolyData;
|
|
import vtk.vtkPolyDataWriter;
|
|
|
|
/**
|
|
* Given a reference surface (either from a shape model or a set of points), find a best fit
|
|
* reference plane. Find the height above this plane for the reference surface and an input shape
|
|
* model. Report the differential height and volume between these two surfaces on a uniform grid.
|
|
* <p>
|
|
* This class uses three coordinate systems:
|
|
* <ul>
|
|
* <li>The global coordinate system. This is the coordinate system of the input data.</li>
|
|
* <li>The local coordinate system. This has the X and Y axes in the best fit plane to the reference
|
|
* data. The origin is optionally set by the user.</li>
|
|
* <li>The native coordinate system. This is not seen by the user. The XY plane is the same as the
|
|
* local coordinate system, but the origin may be translated and there may be a rotation applied
|
|
* about the Z axis.</li>
|
|
* </ul>
|
|
*
|
|
* @author Hari.Nair@jhuapl.edu
|
|
*
|
|
*/
|
|
public class DifferentialVolumeEstimator implements TerrasaurTool {
|
|
|
|
private static final Logger logger = LogManager.getLogger();
|
|
|
|
private DifferentialVolumeEstimator() {}
|
|
|
|
@Override
|
|
public String shortDescription() {
|
|
return "Find volume difference between two shape models.";
|
|
}
|
|
|
|
// degree of polynomial used to fit surface
|
|
private final int POLYNOMIAL_DEGREE = 2;
|
|
|
|
@Override
|
|
public String fullDescription(Options options) {
|
|
String header = "";
|
|
String footer = "\nThis program finds the volume difference between a shape model and a reference surface. "
|
|
+ "The reference surface can either be another shape model or a degree " + POLYNOMIAL_DEGREE
|
|
+ " fit to a set of supplied points. "
|
|
+ "A local coordinate system is derived from the reference surface. The heights of the shape and reference at "
|
|
+ "each grid point are reported. ";
|
|
return TerrasaurTool.super.fullDescription(options, header, footer);
|
|
}
|
|
|
|
/** input shape model */
|
|
private vtkPolyData globalPolyData;
|
|
/** shape model in native coordinates */
|
|
private SmallBodyModel nativeSBM;
|
|
/** number of radial profiles */
|
|
private Integer numProfiles;
|
|
|
|
public void setNumProfiles(Integer numProfiles) {
|
|
this.numProfiles = numProfiles;
|
|
}
|
|
|
|
/** true if local +Z is aligned with the center radial direction */
|
|
private boolean radialUp;
|
|
|
|
public void setRadialUp(boolean radialUp) {
|
|
this.radialUp = radialUp;
|
|
}
|
|
|
|
/** reference points in global coordinates */
|
|
private List<Vector3D> referencePoints;
|
|
|
|
public void setReferencePoints(List<Vector3D> referencePoints) {
|
|
this.referencePoints = referencePoints;
|
|
}
|
|
|
|
/** reference model in global coordinates */
|
|
private vtkPolyData referencePolyData;
|
|
|
|
public void setReferencePolyData(vtkPolyData referencePolyData) {
|
|
this.referencePolyData = referencePolyData;
|
|
}
|
|
|
|
/** Reference shape in native coordinates */
|
|
private SmallBodyModel referenceSBM;
|
|
/** reference surface in native coordinates */
|
|
private FitSurface referenceSurface;
|
|
|
|
private double gridSpacing;
|
|
private double gridHalfExtent;
|
|
|
|
/** global coordinates of the highest point of the shape model */
|
|
private Vector3D highPoint;
|
|
/** global coordinates of the lowest point of the shape model */
|
|
private Vector3D lowPoint;
|
|
|
|
/** this plane converts coordinates from native to global and back */
|
|
private FitPlane plane;
|
|
|
|
/** Inner edge of the ROI */
|
|
private Path2D.Double roiInner;
|
|
/** Outer edge of the ROI */
|
|
private Path2D.Double roiOuter;
|
|
|
|
// local grid is in the same plane as native grid but is translated and rotated
|
|
private Entry<Rotation, Vector3D> nativeToLocal;
|
|
|
|
// the origin of the local coordinate system, in global coordinates
|
|
private enum ORIGIN {
|
|
MIN_HEIGHT,
|
|
MAX_HEIGHT,
|
|
CUSTOM,
|
|
DEFAULT
|
|
}
|
|
|
|
public Vector3D nativeToLocal(Vector3D nativeIJK) {
|
|
return nativeToLocal.getKey().applyTo(nativeIJK.subtract(nativeToLocal.getValue()));
|
|
}
|
|
|
|
public Vector3D localToNative(Vector3D local) {
|
|
return nativeToLocal.getKey().applyInverseTo(local).add(nativeToLocal.getValue());
|
|
}
|
|
|
|
/**
|
|
* Set the inner boundary of the ROI
|
|
*
|
|
* @param filename file containing points in global coordinates
|
|
*/
|
|
public void setInnerROI(String filename) {
|
|
List<Vector3D> points = readPointsFromFile(filename);
|
|
roiInner = createOutline(points);
|
|
}
|
|
|
|
/**
|
|
* Set the outer boundary of the ROI
|
|
*
|
|
* @param filename file containing points in global coordinates
|
|
*/
|
|
public void setOuterROI(String filename) {
|
|
List<Vector3D> points = readPointsFromFile(filename);
|
|
roiOuter = createOutline(points);
|
|
}
|
|
|
|
/**
|
|
* Construct an outline on the local grid from a list of points in global coordinates.
|
|
*
|
|
* @param points points in global coordinates
|
|
* @return outline on local grid
|
|
*/
|
|
private Path2D.Double createOutline(List<Vector3D> points) {
|
|
|
|
Path2D.Double outline = new Path2D.Double();
|
|
for (int i = 0; i < points.size(); i++) {
|
|
Vector3D nativeIJK = plane.globalToLocal(points.get(i));
|
|
Vector3D localIJK = nativeToLocal.getKey().applyTo(nativeIJK.subtract(nativeToLocal.getValue()));
|
|
if (i == 0) {
|
|
outline.moveTo(localIJK.getX(), localIJK.getY());
|
|
} else {
|
|
outline.lineTo(localIJK.getX(), localIJK.getY());
|
|
}
|
|
}
|
|
outline.closePath();
|
|
|
|
return outline;
|
|
}
|
|
|
|
public DifferentialVolumeEstimator(vtkPolyData polyData) {
|
|
this.globalPolyData = polyData;
|
|
this.referencePolyData = null;
|
|
this.referencePoints = null;
|
|
this.numProfiles = 0;
|
|
this.radialUp = false;
|
|
}
|
|
|
|
/**
|
|
* Get the height of the shape model above the reference plane
|
|
*
|
|
* @param x in native coordinates
|
|
* @param y in native coordinates
|
|
* @return height, or {@link Double#NaN} if no intersection found
|
|
*/
|
|
public double getHeight(double x, double y) {
|
|
|
|
double height = Double.NaN;
|
|
double[] origin = {x, y, 0};
|
|
double[] direction = {0, 0, 1};
|
|
double[] intersect = new double[3];
|
|
|
|
long cellID = nativeSBM.computeRayIntersection(origin, direction, intersect);
|
|
if (cellID < 0) {
|
|
direction[2] = -1;
|
|
cellID = nativeSBM.computeRayIntersection(origin, direction, intersect);
|
|
}
|
|
if (cellID >= 0) height = direction[2] * new Vector3D(origin).distance(new Vector3D(intersect));
|
|
|
|
if (Double.isNaN(height)) return Double.NaN;
|
|
|
|
return height;
|
|
}
|
|
|
|
/**
|
|
* Get the height of the reference surface above the reference plane.
|
|
*
|
|
* @param x in native coordinates
|
|
* @param y in native coordinates
|
|
* @return height of surface at (x,y) above reference plane
|
|
*/
|
|
public double getRefHeight(double x, double y) {
|
|
|
|
double[] origin = {x, y, 0};
|
|
double[] direction = {0, 0, 1};
|
|
double[] intersect;
|
|
|
|
double refHeight = Double.NaN;
|
|
if (referenceSBM != null) {
|
|
intersect = new double[3];
|
|
|
|
long cellID = referenceSBM.computeRayIntersection(origin, direction, intersect);
|
|
if (cellID < 0) {
|
|
direction[2] = -1;
|
|
cellID = referenceSBM.computeRayIntersection(origin, direction, intersect);
|
|
}
|
|
if (cellID >= 0) {
|
|
refHeight = direction[2] * new Vector3D(origin).distance(new Vector3D(intersect));
|
|
}
|
|
} else {
|
|
refHeight = referenceSurface.value(x, y);
|
|
}
|
|
|
|
if (Double.isNaN(refHeight)) return Double.NaN;
|
|
|
|
return refHeight;
|
|
}
|
|
|
|
/**
|
|
* Create an array of grid points with heights
|
|
*
|
|
* @param gridHalfExtent half-size of grid
|
|
* @param gridSpacing spacing between points
|
|
* @return grid points sorted by x coordinate, then y
|
|
*/
|
|
private NavigableSet<GridPoint> createGrid(double gridHalfExtent, double gridSpacing) {
|
|
Set<Vector3D> localGrid = new HashSet<>();
|
|
|
|
for (double x = 0; x <= gridHalfExtent; x += gridSpacing) {
|
|
for (double y = 0; y <= gridHalfExtent; y += gridSpacing) {
|
|
localGrid.add(new Vector3D(x, y, 0));
|
|
if (y != 0) localGrid.add(new Vector3D(x, -y, 0));
|
|
if (x != 0) localGrid.add(new Vector3D(-x, y, 0));
|
|
if (x != 0 && y != 0) localGrid.add(new Vector3D(-x, -y, 0));
|
|
}
|
|
}
|
|
this.gridSpacing = gridSpacing;
|
|
this.gridHalfExtent = gridHalfExtent;
|
|
|
|
GridPoint highGridPoint = null;
|
|
GridPoint lowGridPoint = null;
|
|
NavigableSet<GridPoint> gridPoints = new TreeSet<>();
|
|
for (Vector3D localPoint : localGrid) {
|
|
GridPoint gp = new GridPoint(localPoint);
|
|
gridPoints.add(gp);
|
|
if (Double.isFinite(gp.height)) {
|
|
if (highGridPoint == null || highGridPoint.height < gp.height) highGridPoint = gp;
|
|
if (lowGridPoint == null || lowGridPoint.height > gp.height) lowGridPoint = gp;
|
|
}
|
|
}
|
|
|
|
if (highGridPoint != null) highPoint = highGridPoint.globalIJK;
|
|
if (lowGridPoint != null) lowPoint = lowGridPoint.globalIJK;
|
|
|
|
return gridPoints;
|
|
}
|
|
|
|
/**
|
|
* Create the reference surface, either from a fit to a set of points or from an input shape
|
|
* model.
|
|
*
|
|
* @param localOriginInGlobalCoordinates local origin in global coordinates
|
|
*/
|
|
public void createReference(Vector3D localOriginInGlobalCoordinates) {
|
|
|
|
double[] pt = new double[3];
|
|
|
|
if (referencePolyData != null) {
|
|
if (referencePoints != null) {
|
|
logger.warn(
|
|
"Both -referenceList and -referenceShape were specified. Reference surface will be set to argument of -referenceShape.");
|
|
}
|
|
referencePoints = new ArrayList<>();
|
|
for (int i = 0; i < referencePolyData.GetNumberOfPoints(); i++) {
|
|
referencePolyData.GetPoint(i, pt);
|
|
referencePoints.add(new Vector3D(pt));
|
|
}
|
|
}
|
|
|
|
// this is the best fit plane to the reference points. It can convert points in input
|
|
// (global) coordinates to native coordinates and vice versa
|
|
plane = new FitPlane(referencePoints);
|
|
|
|
// set the +Z direction for the local plane
|
|
Vector3D referenceNormal = radialUp ? plane.getTransform().getValue() : Vector3D.PLUS_K;
|
|
|
|
// check if the plane normal is pointing in the same direction as the reference normal. If not,
|
|
// flip the plane
|
|
Pair<Rotation, Vector3D> transform = plane.getTransform();
|
|
Vector3D planeNormal = transform.getKey().applyInverseTo(referenceNormal);
|
|
if (planeNormal.dotProduct(referenceNormal) < 0) plane = plane.reverseNormal();
|
|
|
|
// create the SmallBodyModel for the shape to evaluate
|
|
vtkPolyData nativePolyData = new vtkPolyData();
|
|
nativePolyData.DeepCopy(globalPolyData);
|
|
vtkPoints points = nativePolyData.GetPoints();
|
|
for (int i = 0; i < points.GetNumberOfPoints(); i++) {
|
|
points.GetPoint(i, pt);
|
|
Vector3D nativePoint = plane.globalToLocal(new Vector3D(pt));
|
|
double[] data = nativePoint.toArray();
|
|
points.SetPoint(i, data);
|
|
}
|
|
nativeSBM = new SmallBodyModel(nativePolyData);
|
|
|
|
// now define the reference shape/surface
|
|
if (referencePolyData != null) {
|
|
// create the SmallBodyModel for the reference shape
|
|
nativePolyData = new vtkPolyData();
|
|
nativePolyData.DeepCopy(referencePolyData);
|
|
points = nativePolyData.GetPoints();
|
|
for (int i = 0; i < points.GetNumberOfPoints(); i++) {
|
|
points.GetPoint(i, pt);
|
|
Vector3D nativePoint = plane.globalToLocal(new Vector3D(pt));
|
|
double[] data = nativePoint.toArray();
|
|
points.SetPoint(i, data);
|
|
}
|
|
|
|
referenceSBM = new SmallBodyModel(nativePolyData);
|
|
} else {
|
|
// create the reference surface
|
|
List<Vector3D> nativePoints = new ArrayList<>();
|
|
for (Vector3D v : referencePoints) {
|
|
Vector3D nativePoint = plane.globalToLocal(v);
|
|
nativePoints.add(nativePoint);
|
|
}
|
|
|
|
referenceSurface = new FitSurface(nativePoints, POLYNOMIAL_DEGREE);
|
|
}
|
|
|
|
// create a rotation matrix to go from native to local (where the Z axis is the same for both
|
|
// and the X axis is aligned in the same direction as the global X axis)
|
|
Pair<Rotation, Vector3D> globalToLocalTransform = plane.getTransform();
|
|
Vector3D kRow = Vector3D.PLUS_K;
|
|
Vector3D iRow = globalToLocalTransform.getKey().applyTo(Vector3D.PLUS_I);
|
|
Vector3D jRow = Vector3D.crossProduct(kRow, iRow).normalize();
|
|
kRow = Vector3D.crossProduct(iRow, jRow).normalize();
|
|
iRow = iRow.normalize();
|
|
|
|
Vector3D translateNativeToLocal = Vector3D.ZERO;
|
|
if (localOriginInGlobalCoordinates.getNorm() > 0) {
|
|
// translation to go from native to local (where localOriginInGlobalCoordinates defines 0,0 in
|
|
// the local frame)
|
|
Vector3D nativeOriginInGlobalCoordinates = plane.localToGlobal(Vector3D.ZERO);
|
|
Vector3D translateNativeToLocalInGlobalCoordinates =
|
|
localOriginInGlobalCoordinates.subtract(nativeOriginInGlobalCoordinates);
|
|
// TODO: check that the Z component is zero (it should be?)
|
|
translateNativeToLocal = globalToLocalTransform.getKey().applyTo(translateNativeToLocalInGlobalCoordinates);
|
|
}
|
|
|
|
Rotation rotateNativeToLocal = MathConversions.toRotation(new RotationMatrixIJK(
|
|
iRow.getX(),
|
|
jRow.getX(),
|
|
kRow.getX(),
|
|
iRow.getY(),
|
|
jRow.getY(),
|
|
kRow.getY(),
|
|
iRow.getZ(),
|
|
jRow.getZ(),
|
|
kRow.getZ()));
|
|
|
|
this.nativeToLocal = new AbstractMap.SimpleEntry<>(rotateNativeToLocal, translateNativeToLocal);
|
|
}
|
|
|
|
/**
|
|
* The header for grid and profile CSV files. Each line begins with a #
|
|
*
|
|
* @param header string at beginning of header
|
|
* @return complete header
|
|
*/
|
|
public static String getHeader(String header) {
|
|
StringBuffer sb = new StringBuffer();
|
|
sb.append(header);
|
|
sb.append("# Local X and Y are grid coordinates in the local reference frame\n");
|
|
sb.append("# Angle is measured from the local X axis, in degrees\n");
|
|
sb.append("# ROI flag is 1 if point is in the region of interest, 0 if not\n");
|
|
sb.append("# Global X, Y, and Z are the local grid points in the global " + " (input) reference system\n");
|
|
sb.append("# Reference Height is the height of the reference model (or fit surface) above "
|
|
+ "the local grid plane\n");
|
|
sb.append("# Model Height is the height of the shape model above the local grid plane. "
|
|
+ "NaN means there is no model intersection at this grid point.\n");
|
|
sb.append("# Bin volume is the grid cell area times the model - reference height\n");
|
|
sb.append("#\n");
|
|
sb.append(String.format("%s, ", "Local X"));
|
|
sb.append(String.format("%s, ", "Local Y"));
|
|
sb.append(String.format("%s, ", "Angle"));
|
|
sb.append(String.format("%s, ", "ROI Flag"));
|
|
sb.append(String.format("%s, ", "Global X"));
|
|
sb.append(String.format("%s, ", "Global Y"));
|
|
sb.append(String.format("%s, ", "Global Z"));
|
|
sb.append(String.format("%s, ", "Reference Height"));
|
|
sb.append(String.format("%s, ", "Model Height"));
|
|
sb.append(String.format("%s, ", "Model - Reference"));
|
|
sb.append(String.format("%s", "Bin Volume"));
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* The header for the sector CSV file. Each line begins with a #
|
|
*
|
|
* @param header string at beginning of header
|
|
* @return complete header
|
|
*/
|
|
public static String getSectorHeader(String header) {
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append(header);
|
|
sb.append("# Angle is measured from the local X axis, in degrees\n");
|
|
sb.append(
|
|
"# Sector volume is the grid cell area times the model - reference height summed over all grid cells in the ROI.\n");
|
|
sb.append("#\n");
|
|
sb.append(String.format("%s, ", "Index"));
|
|
sb.append(String.format("%s, ", "Start angle (degrees)"));
|
|
sb.append(String.format("%s, ", "Stop angle (degrees)"));
|
|
sb.append(String.format("%s, ", "Sector Volume above reference surface"));
|
|
sb.append(String.format("%s, ", "Sector Volume below reference surface"));
|
|
sb.append(String.format("%s", "Total Sector Volume"));
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
private String toCSV(GridPoint gp) {
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
sb.append(String.format("%f, ", gp.localIJK.getX()));
|
|
sb.append(String.format("%f, ", gp.localIJK.getY()));
|
|
|
|
double angle = Math.toDegrees(Math.atan2(gp.localIJK.getY(), gp.localIJK.getX()));
|
|
if (angle < 0) angle += 360;
|
|
sb.append(String.format("%f, ", angle));
|
|
sb.append(String.format("%d, ", isInsideROI(gp.localIJK) ? 1 : 0));
|
|
|
|
sb.append(String.format("%f, ", gp.globalIJK.getX()));
|
|
sb.append(String.format("%f, ", gp.globalIJK.getY()));
|
|
sb.append(String.format("%f, ", gp.globalIJK.getZ()));
|
|
|
|
sb.append(String.format("%g, ", gp.referenceHeight));
|
|
sb.append(String.format("%g, ", gp.height));
|
|
sb.append(String.format("%g, ", gp.differentialHeight));
|
|
sb.append(String.format("%g", gridSpacing * gridSpacing * gp.differentialHeight));
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param localIJ Point on the local grid. The Z coordinate is ignored
|
|
* @return true if the point is inside the outer boundary and outside the inner boundary. If the
|
|
* outer boundary is null then all points are considered to be inside the outer boundary. If the
|
|
* inner boundary is null all points are considered to be outside the inner boundary.
|
|
*/
|
|
private boolean isInsideROI(Vector3D localIJ) {
|
|
Point2D thisPoint = new Point2D.Double(localIJ.getX(), localIJ.getY());
|
|
boolean insideROI = roiOuter == null || roiOuter.contains(thisPoint);
|
|
if (roiInner != null) {
|
|
if (insideROI && roiInner.contains(thisPoint)) insideROI = false;
|
|
}
|
|
return insideROI;
|
|
}
|
|
|
|
/**
|
|
* Write out a VTK file with the local grid points. Useful for a sanity check.
|
|
*
|
|
* @param gridPointsList grid points
|
|
* @param profilesMap grid points along each profile
|
|
* @param sectorsMap grid points within each sector
|
|
* @param vtkFile file to write
|
|
*/
|
|
private void writeReferenceVTK(
|
|
Collection<GridPoint> gridPointsList,
|
|
Map<Integer, Collection<GridPoint>> profilesMap,
|
|
Map<Integer, Collection<GridPoint>> sectorsMap,
|
|
String vtkFile) {
|
|
|
|
Map<Vector3D, Boolean> roiMap = new HashMap<>();
|
|
Map<Vector3D, Integer> profileMap = new HashMap<>();
|
|
Map<Vector3D, Integer> sectorMap = new HashMap<>();
|
|
|
|
for (GridPoint gp : gridPointsList) {
|
|
Vector3D localIJK = gp.localIJK;
|
|
Vector3D nativeIJK = nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue());
|
|
Vector3D globalIJK = plane.localToGlobal(nativeIJK);
|
|
roiMap.put(globalIJK, isInsideROI(gp.localIJK));
|
|
profileMap.put(globalIJK, 0);
|
|
sectorMap.put(globalIJK, 0);
|
|
}
|
|
|
|
for (int i : profilesMap.keySet()) {
|
|
for (GridPoint gp : profilesMap.get(i)) {
|
|
Vector3D localIJK = gp.localIJK;
|
|
Vector3D nativeIJK =
|
|
nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue());
|
|
Vector3D globalIJK = plane.localToGlobal(nativeIJK);
|
|
profileMap.put(globalIJK, i + 1);
|
|
}
|
|
}
|
|
|
|
for (int i : sectorsMap.keySet()) {
|
|
for (GridPoint gp : sectorsMap.get(i)) {
|
|
Vector3D localIJK = gp.localIJK;
|
|
Vector3D nativeIJK =
|
|
nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue());
|
|
Vector3D globalIJK = plane.localToGlobal(nativeIJK);
|
|
sectorMap.put(globalIJK, i + 1);
|
|
}
|
|
}
|
|
|
|
vtkDoubleArray insideROI = new vtkDoubleArray();
|
|
insideROI.SetName("Inside ROI");
|
|
|
|
vtkDoubleArray profiles = new vtkDoubleArray();
|
|
profiles.SetName("Profiles");
|
|
|
|
vtkDoubleArray sectors = new vtkDoubleArray();
|
|
sectors.SetName("Sectors");
|
|
|
|
vtkPoints pointsXYZ = new vtkPoints();
|
|
for (Vector3D point : roiMap.keySet()) {
|
|
double[] array = point.toArray();
|
|
pointsXYZ.InsertNextPoint(array);
|
|
insideROI.InsertNextValue(roiMap.get(point) ? 1 : 0);
|
|
profiles.InsertNextValue(profileMap.get(point));
|
|
sectors.InsertNextValue(sectorMap.get(point));
|
|
}
|
|
|
|
vtkPolyData polyData = new vtkPolyData();
|
|
polyData.SetPoints(pointsXYZ);
|
|
polyData.GetPointData().AddArray(insideROI);
|
|
polyData.GetPointData().AddArray(profiles);
|
|
polyData.GetPointData().AddArray(sectors);
|
|
|
|
vtkCellArray cells = new vtkCellArray();
|
|
polyData.SetPolys(cells);
|
|
|
|
for (int i = 0; i < pointsXYZ.GetNumberOfPoints(); i++) {
|
|
vtkIdList idList = new vtkIdList();
|
|
idList.InsertNextId(i);
|
|
cells.InsertNextCell(idList);
|
|
}
|
|
|
|
vtkPolyDataWriter writer = new vtkPolyDataWriter();
|
|
writer.SetInputData(polyData);
|
|
writer.SetFileName(vtkFile);
|
|
writer.SetFileTypeToBinary();
|
|
writer.Update();
|
|
}
|
|
|
|
/**
|
|
* Write the local grid out to a file
|
|
*
|
|
* @param gridPoints grid points
|
|
* @param header file header
|
|
* @param outputBasename CSV file to write
|
|
*/
|
|
private void writeGridCSV(Collection<GridPoint> gridPoints, String header, String outputBasename) {
|
|
String csvFile = outputBasename + "_grid.csv";
|
|
|
|
try (PrintWriter pw = new PrintWriter(csvFile)) {
|
|
pw.println(getHeader(header));
|
|
for (GridPoint gp : gridPoints) pw.println(toCSV(gp));
|
|
} catch (FileNotFoundException e) {
|
|
logger.warn("Can't write " + csvFile);
|
|
logger.warn(e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write profiles to file
|
|
*
|
|
* @param gridPoints grid points
|
|
* @param header file header
|
|
* @param outputBasename CSV file to write
|
|
*/
|
|
private Map<Integer, Collection<GridPoint>> writeProfileCSV(
|
|
Collection<GridPoint> gridPoints, String header, String outputBasename) {
|
|
Map<Integer, Collection<GridPoint>> profileMap = new HashMap<>();
|
|
|
|
if (numProfiles == 0) return profileMap;
|
|
|
|
// sort grid points into radial bins
|
|
NavigableMap<Integer, Set<GridPoint>> radialMap = new TreeMap<>();
|
|
for (GridPoint gp : gridPoints) {
|
|
double radius = gp.localIJK.getNorm() / gridSpacing;
|
|
int key = (int) radius;
|
|
Set<GridPoint> set = radialMap.computeIfAbsent(key, k -> new HashSet<>());
|
|
set.add(gp);
|
|
}
|
|
|
|
final double deltaAngle = 2 * Math.PI / numProfiles;
|
|
for (int i = 0; i < numProfiles; i++) {
|
|
|
|
Collection<GridPoint> profileGridPoints = new HashSet<>();
|
|
profileMap.put(i, profileGridPoints);
|
|
|
|
double angle = deltaAngle * i;
|
|
String csvFile =
|
|
String.format("%s_profile_%03d.csv", outputBasename, (int) Math.round(Math.toDegrees(angle)));
|
|
|
|
try (PrintWriter pw = new PrintWriter(csvFile)) {
|
|
pw.println(getHeader(header));
|
|
for (int bin : radialMap.keySet()) {
|
|
|
|
// stop profile at grid edge
|
|
double thisX = Math.abs(Math.cos(angle) * bin) * gridSpacing;
|
|
if (thisX > gridHalfExtent) continue;
|
|
double thisY = Math.abs(Math.sin(angle) * bin) * gridSpacing;
|
|
if (thisY > gridHalfExtent) continue;
|
|
|
|
// sort points in this radial bin by angular distance from profile angle
|
|
NavigableSet<GridPoint> sortedByAngle = new TreeSet<>((o1, o2) -> {
|
|
double angle1 = Math.atan2(o1.localIJK.getY(), o1.localIJK.getX());
|
|
if (angle1 < 0) angle1 += 2 * Math.PI;
|
|
double angle2 = Math.atan2(o2.localIJK.getY(), o2.localIJK.getX());
|
|
if (angle2 < 0) angle2 += 2 * Math.PI;
|
|
return Double.compare(Math.abs(angle1 - angle), Math.abs(angle2 - angle));
|
|
});
|
|
|
|
sortedByAngle.addAll(radialMap.get(bin));
|
|
|
|
pw.println(toCSV(sortedByAngle.first()));
|
|
GridPoint thisPoint = sortedByAngle.first();
|
|
if (Double.isFinite(thisPoint.differentialHeight)) profileGridPoints.add(thisPoint);
|
|
}
|
|
} catch (FileNotFoundException e) {
|
|
logger.warn("Can't write {}", csvFile);
|
|
logger.warn(e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
return profileMap;
|
|
}
|
|
|
|
/**
|
|
* Write sector volumes to a file
|
|
*
|
|
* @param gridPoints grid points
|
|
* @param header file header
|
|
* @param outputBasename CSV file to write
|
|
*/
|
|
private Map<Integer, Collection<GridPoint>> writeSectorCSV(
|
|
Collection<GridPoint> gridPoints, String header, String outputBasename) {
|
|
|
|
// grid points in each sector
|
|
Map<Integer, Collection<GridPoint>> sectorMap = new HashMap<>();
|
|
|
|
if (numProfiles == 0) return sectorMap;
|
|
|
|
String csvFile = outputBasename + "_sector.csv";
|
|
|
|
NavigableMap<Double, Double> aboveMap = new TreeMap<>();
|
|
NavigableMap<Double, Double> belowMap = new TreeMap<>();
|
|
final double deltaAngle = 2 * Math.PI / numProfiles;
|
|
for (int i = 0; i < numProfiles; i++) {
|
|
aboveMap.put(i * deltaAngle, 0.);
|
|
belowMap.put(i * deltaAngle, 0.);
|
|
}
|
|
|
|
// run through all the grid points and put them in the appropriate sector
|
|
double gridCellArea = gridSpacing * gridSpacing;
|
|
for (GridPoint gp : gridPoints) {
|
|
Vector3D localIJK = gp.localIJK;
|
|
double azimuth = Math.atan2(localIJK.getY(), localIJK.getX());
|
|
if (azimuth < 0) azimuth += 2 * Math.PI;
|
|
double key = aboveMap.floorKey(azimuth);
|
|
|
|
int sector = (int) (key / deltaAngle);
|
|
Collection<GridPoint> sectorGridPoints = sectorMap.computeIfAbsent(sector, k -> new HashSet<>());
|
|
|
|
if (isInsideROI(gp.localIJK)) {
|
|
double dv = gridCellArea * gp.differentialHeight;
|
|
if (Double.isFinite(dv)) {
|
|
if (dv > 0) {
|
|
aboveMap.compute(key, (k, value) -> value + dv);
|
|
} else {
|
|
belowMap.compute(key, (k, value) -> value + dv);
|
|
}
|
|
sectorGridPoints.add(gp);
|
|
}
|
|
}
|
|
}
|
|
|
|
try (PrintWriter pw = new PrintWriter(csvFile)) {
|
|
pw.println(getSectorHeader(header));
|
|
for (double azimuth : aboveMap.keySet()) {
|
|
StringBuffer sb = new StringBuffer();
|
|
sb.append(String.format("%d, ", (int) (azimuth / deltaAngle)));
|
|
sb.append(String.format("%.2f, ", Math.toDegrees(azimuth)));
|
|
sb.append(String.format("%.2f, ", Math.toDegrees(azimuth + deltaAngle)));
|
|
sb.append(String.format("%e, ", aboveMap.get(azimuth)));
|
|
sb.append(String.format("%e, ", belowMap.get(azimuth)));
|
|
sb.append(String.format("%e", aboveMap.get(azimuth) + belowMap.get(azimuth)));
|
|
pw.println(sb);
|
|
}
|
|
|
|
} catch (FileNotFoundException e) {
|
|
logger.warn("Can't write " + csvFile);
|
|
logger.warn(e.getLocalizedMessage());
|
|
}
|
|
|
|
return sectorMap;
|
|
}
|
|
|
|
private class GridPoint implements Comparable<GridPoint> {
|
|
Vector3D localIJK;
|
|
Vector3D globalIJK;
|
|
double referenceHeight;
|
|
double height;
|
|
double differentialHeight;
|
|
|
|
/**
|
|
* Create a grid point from an input location in local coordinates
|
|
*
|
|
* @param xy point in local coordinates. Z value is ignored.
|
|
*/
|
|
public GridPoint(Vector3D xy) {
|
|
this.localIJK = xy;
|
|
Vector3D nativeIJK = nativeToLocal.getKey().applyInverseTo(localIJK).add(nativeToLocal.getValue());
|
|
globalIJK = plane.localToGlobal(nativeIJK);
|
|
referenceHeight = getRefHeight(nativeIJK.getX(), nativeIJK.getY());
|
|
height = getHeight(nativeIJK.getX(), nativeIJK.getY());
|
|
differentialHeight = height - referenceHeight;
|
|
}
|
|
|
|
/**
|
|
* sort by the x coordinate on the local grid, then by the y coordinate.
|
|
*/
|
|
@Override
|
|
public int compareTo(GridPoint o) {
|
|
int compare = Double.compare(localIJK.getX(), o.localIJK.getX());
|
|
if (compare == 0) compare = Double.compare(localIJK.getY(), o.localIJK.getY());
|
|
return compare;
|
|
}
|
|
}
|
|
|
|
private static List<Vector3D> readPointsFromFile(String filename) {
|
|
List<Vector3D> points = new ArrayList<>();
|
|
|
|
try {
|
|
if (FilenameUtils.getExtension(filename).equalsIgnoreCase("vtk")) {
|
|
|
|
vtkPolyData polydata = PolyDataUtil.loadShapeModel(filename);
|
|
double[] pt = new double[3];
|
|
for (int i = 0; i < polydata.GetNumberOfPoints(); i++) {
|
|
polydata.GetPoint(i, pt);
|
|
points.add(new Vector3D(pt));
|
|
}
|
|
} else {
|
|
List<String> lines = FileUtils.readLines(new File(filename), Charset.defaultCharset());
|
|
for (String line : lines) {
|
|
if (line.trim().isEmpty() || line.trim().startsWith("#")) continue;
|
|
SBMTStructure structure = SBMTStructure.fromString(line);
|
|
points.add(structure.centerXYZ());
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
logger.warn(e.getLocalizedMessage());
|
|
}
|
|
return points;
|
|
}
|
|
|
|
private static Options defineOptions() {
|
|
Options options = TerrasaurTool.defineOptions();
|
|
options.addOption(Option.builder("gridExtent")
|
|
.required()
|
|
.hasArg()
|
|
.desc(
|
|
"Required. Size of local grid, in same units as shape model and reference surface. Grid is assumed to be square.")
|
|
.build());
|
|
options.addOption(Option.builder("gridSpacing")
|
|
.required()
|
|
.hasArg()
|
|
.desc("Required. Spacing of local grid, in same units as shape model and reference surface.")
|
|
.build());
|
|
options.addOption(Option.builder("logFile")
|
|
.hasArg()
|
|
.desc("If present, save screen output to log file.")
|
|
.build());
|
|
options.addOption(Option.builder("logLevel")
|
|
.hasArg()
|
|
.desc("If present, print messages above selected priority. Valid values are "
|
|
+ "ALL, OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, or FINEST. Default is INFO.")
|
|
.build());
|
|
options.addOption(Option.builder("numProfiles")
|
|
.hasArg()
|
|
.desc("Number of radial profiles to create. Profiles are evenly spaced in degrees and evaluated "
|
|
+ "at intervals of gridSpacing in the radial direction.")
|
|
.build());
|
|
options.addOption(Option.builder("origin")
|
|
.hasArg()
|
|
.desc(
|
|
"If present, set origin of local coordinate system. "
|
|
+ "Options are MAX_HEIGHT (set to maximum elevation of the shape model), "
|
|
+ "MIN_HEIGHT (set to minimum elevation of the shape model), "
|
|
+ "or a three element vector specifying the desired origin, comma separated, no spaces (e.g. 11.45,-45.34,0.932).")
|
|
.build());
|
|
options.addOption(Option.builder("output")
|
|
.hasArg()
|
|
.required()
|
|
.desc(
|
|
"Basename of output files. Files will be named ${output}_grid.csv for the grid, ${output}_sector.csv for the sectors, "
|
|
+ "and ${output}_profile_${degrees}.csv for profiles.")
|
|
.build());
|
|
options.addOption(Option.builder("radialUp")
|
|
.desc("Specify +Z direction of local coordinate system to be in the radial "
|
|
+ "direction. Default is to align local +Z along global +Z.")
|
|
.build());
|
|
options.addOption(Option.builder("referenceList")
|
|
.hasArg()
|
|
.desc("File containing reference points. If the file extension is .vtk it is read as a VTK file, "
|
|
+ "otherwise it is assumed to be an SBMT structure file.")
|
|
.build());
|
|
options.addOption(Option.builder("referenceShape")
|
|
.hasArg()
|
|
.desc("Reference shape.")
|
|
.build());
|
|
options.addOption(Option.builder("referenceVTK")
|
|
.hasArg()
|
|
.desc("If present, write out a VTK file with the reference surface at each grid point. "
|
|
+ "If an ROI is defined color points inside/outside the boundaries.")
|
|
.build());
|
|
options.addOption(Option.builder("roiInner")
|
|
.hasArg()
|
|
.desc(
|
|
"Flag points closer to the origin than this as outside the ROI. Supported formats are the same as referenceList.")
|
|
.build());
|
|
options.addOption(Option.builder("roiOuter")
|
|
.hasArg()
|
|
.desc(
|
|
"Flag points closer to the origin than this as outside the ROI. Supported formats are the same as referenceList.")
|
|
.build());
|
|
options.addOption(Option.builder("shapeModel")
|
|
.hasArg()
|
|
.required()
|
|
.desc("Shape model for volume computation.")
|
|
.build());
|
|
return options;
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
TerrasaurTool defaultOBJ = new DifferentialVolumeEstimator();
|
|
|
|
Options options = defineOptions();
|
|
|
|
CommandLine cl = defaultOBJ.parseArgs(args, options);
|
|
|
|
Map<MessageLabel, String> startupMessages = defaultOBJ.startupMessages(cl);
|
|
for (MessageLabel ml : startupMessages.keySet())
|
|
logger.info(String.format("%s %s", ml.label, startupMessages.get(ml)));
|
|
|
|
StringBuilder header = new StringBuilder();
|
|
header.append("# ").append(new Date()).append("\n");
|
|
header.append("# ")
|
|
.append(defaultOBJ.getClass().getSimpleName())
|
|
.append(" [")
|
|
.append(AppVersion.getVersionString())
|
|
.append("]\n");
|
|
header.append("# ").append(startupMessages.get(MessageLabel.ARGUMENTS)).append("\n");
|
|
|
|
NativeLibraryLoader.loadVtkLibraries();
|
|
|
|
double gridHalfExtent = Double.parseDouble(cl.getOptionValue("gridExtent")) / 2;
|
|
double gridSpacing = Double.parseDouble(cl.getOptionValue("gridSpacing"));
|
|
|
|
String outputBasename = cl.getOptionValue("output");
|
|
String dirName = FilenameUtils.getFullPath(outputBasename);
|
|
if (!dirName.trim().isEmpty()) {
|
|
File dir = new File(dirName);
|
|
if (!dir.exists()) dir.mkdirs();
|
|
}
|
|
|
|
vtkPolyData polyData = null;
|
|
try {
|
|
polyData = PolyDataUtil.loadShapeModel(cl.getOptionValue("shapeModel"));
|
|
} catch (Exception e) {
|
|
logger.error("Cannot load shape model!");
|
|
logger.error(e.getLocalizedMessage(), e);
|
|
}
|
|
|
|
ORIGIN originType = ORIGIN.DEFAULT;
|
|
Vector3D localOrigin = Vector3D.ZERO;
|
|
if (cl.hasOption("origin")) {
|
|
String originString = cl.getOptionValue("origin");
|
|
if (originString.contains(",")) {
|
|
String[] parts = originString.split(",");
|
|
if (parts.length == 3) {
|
|
localOrigin = new Vector3D(
|
|
Double.parseDouble(parts[0].trim()),
|
|
Double.parseDouble(parts[1].trim()),
|
|
Double.parseDouble(parts[2].trim()));
|
|
originType = ORIGIN.CUSTOM;
|
|
}
|
|
} else {
|
|
originType = ORIGIN.valueOf(originString.toUpperCase());
|
|
}
|
|
}
|
|
DifferentialVolumeEstimator app = new DifferentialVolumeEstimator(polyData);
|
|
|
|
if (cl.hasOption("numProfiles")) app.setNumProfiles(Integer.parseInt(cl.getOptionValue("numProfiles")));
|
|
|
|
if (cl.hasOption("radialUp")) app.setRadialUp(true);
|
|
|
|
if (cl.hasOption("referenceShape")) {
|
|
try {
|
|
app.setReferencePolyData(PolyDataUtil.loadShapeModel(cl.getOptionValue("referenceShape")));
|
|
} catch (Exception e) {
|
|
logger.error("Cannot load reference shape model!");
|
|
logger.error(e.getLocalizedMessage(), e);
|
|
}
|
|
}
|
|
if (cl.hasOption("referenceList"))
|
|
app.setReferencePoints(readPointsFromFile(cl.getOptionValue("referenceList")));
|
|
|
|
app.createReference(localOrigin);
|
|
|
|
// Shift the origin if needed
|
|
switch (originType) {
|
|
case CUSTOM:
|
|
case DEFAULT:
|
|
break;
|
|
case MAX_HEIGHT:
|
|
app.createGrid(gridHalfExtent, 0.1 * gridSpacing);
|
|
localOrigin = app.highPoint;
|
|
app.createReference(localOrigin);
|
|
break;
|
|
case MIN_HEIGHT:
|
|
app.createGrid(gridHalfExtent, 0.1 * gridSpacing);
|
|
localOrigin = app.lowPoint;
|
|
app.createReference(localOrigin);
|
|
break;
|
|
}
|
|
|
|
if (cl.hasOption("roiInner")) app.setInnerROI(cl.getOptionValue("roiInner"));
|
|
|
|
if (cl.hasOption("roiOuter")) app.setOuterROI(cl.getOptionValue("roiOuter"));
|
|
|
|
NavigableSet<GridPoint> gridPoints = app.createGrid(gridHalfExtent, gridSpacing);
|
|
|
|
app.writeGridCSV(gridPoints, header.toString(), outputBasename);
|
|
Map<Integer, Collection<GridPoint>> profileMap =
|
|
app.writeProfileCSV(gridPoints, header.toString(), outputBasename);
|
|
Map<Integer, Collection<GridPoint>> sectorMap =
|
|
app.writeSectorCSV(gridPoints, header.toString(), outputBasename);
|
|
|
|
if (cl.hasOption("referenceVTK")) {
|
|
app.writeReferenceVTK(gridPoints, profileMap, sectorMap, cl.getOptionValue("referenceVTK"));
|
|
}
|
|
|
|
logger.info("Finished.");
|
|
}
|
|
}
|