mirror of
https://github.com/JHUAPL/Terrasaur.git
synced 2026-01-09 14:28:04 -05:00
278 lines
12 KiB
Java
278 lines
12 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.io.File;
|
|
import java.nio.charset.Charset;
|
|
import java.util.*;
|
|
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.math3.geometry.euclidean.threed.Vector3D;
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
import spice.basic.Plane;
|
|
import spice.basic.Vector3;
|
|
import terrasaur.smallBodyModel.BoundingBox;
|
|
import terrasaur.smallBodyModel.SmallBodyModel;
|
|
import terrasaur.templates.TerrasaurTool;
|
|
import terrasaur.utils.Log4j2Configurator;
|
|
import terrasaur.utils.NativeLibraryLoader;
|
|
import terrasaur.utils.PolyDataUtil;
|
|
import vtk.vtkGenericCell;
|
|
import vtk.vtkPoints;
|
|
import vtk.vtkPolyData;
|
|
import vtk.vtksbCellLocator;
|
|
|
|
/**
|
|
* AdjustShapeModelToOtherShapeModel program. See the usage string for more information about this
|
|
* program.
|
|
*
|
|
* @author Eli Kahn
|
|
* @version 1.0
|
|
*/
|
|
public class AdjustShapeModelToOtherShapeModel implements TerrasaurTool {
|
|
|
|
private static final Logger logger = LogManager.getLogger();
|
|
|
|
@Override
|
|
public String shortDescription() {
|
|
return "Adjust vertices of one shape model to lie on the surface of another.";
|
|
}
|
|
|
|
@Override
|
|
public String fullDescription(Options options) {
|
|
|
|
String header =
|
|
"""
|
|
\n
|
|
This program takes 2 shape models in OBJ format and tries to adjust
|
|
to vertices of the first shape model so they lie on the surface of the
|
|
second shape model. It does this by shooting a ray starting from the origin
|
|
in the direction of each point of the first model into the second model and
|
|
then changes the point of the first model to the intersection point.""";
|
|
return TerrasaurTool.super.fullDescription(options, header, "");
|
|
}
|
|
|
|
private static Options defineOptions() {
|
|
Options options = TerrasaurTool.defineOptions();
|
|
options.addOption(Option.builder("from")
|
|
.hasArg()
|
|
.desc("path to first shape model in OBJ format which will get shifted to the second shape model")
|
|
.build());
|
|
options.addOption(Option.builder("to")
|
|
.hasArg()
|
|
.desc("path to second shape model in OBJ format which the first shape model will try to match to")
|
|
.build());
|
|
options.addOption(Option.builder("output")
|
|
.hasArg()
|
|
.desc(
|
|
"path to adjusted shape model in OBJ format generated by this program by shifting first to second")
|
|
.build());
|
|
options.addOption(Option.builder("filelist")
|
|
.desc(
|
|
"""
|
|
If specified then the second required argument to this program,
|
|
"to" is a file containing a list of OBJ files to match to.
|
|
In this situation the ray is shot into each of the shape models in this
|
|
list and any intersection points are averaged together to produce the
|
|
final intersection point. Note that any individual shape model in this
|
|
list may be only a piece of the the complete shape model (e.g. a mapola).
|
|
However, the global shape model formed when all these pieces are
|
|
combined together, may not have any holes or gaps.""")
|
|
.build());
|
|
options.addOption(Option.builder("fit-plane-radius")
|
|
.hasArg()
|
|
.desc(
|
|
"""
|
|
If present, find a local normal at each point in the first shape
|
|
model by fitting a plane to all points within the specified radius.
|
|
Use this normal to adjust the point to the second shape model rather
|
|
than the radial vector.""")
|
|
.build());
|
|
options.addOption(Option.builder("local")
|
|
.desc(
|
|
"""
|
|
Use when adjusting a local OBJ file to another. The best fit plane to the
|
|
first shape model is used to adjust the vertices rather than the radial vector
|
|
for each point.
|
|
""")
|
|
.build());
|
|
return options;
|
|
}
|
|
|
|
static Vector3D computeMeanPoint(List<Vector3D> points) {
|
|
Vector3D meanPoint = new Vector3D(0., 0., 0.);
|
|
for (Vector3D point : points) meanPoint = meanPoint.add(point);
|
|
meanPoint = meanPoint.scalarMultiply(1. / points.size());
|
|
return meanPoint;
|
|
}
|
|
|
|
public static void adjustShapeModelToOtherShapeModel(
|
|
vtkPolyData frompolydata, ArrayList<vtkPolyData> topolydata, double planeRadius, boolean localModel)
|
|
throws Exception {
|
|
vtkPoints points = frompolydata.GetPoints();
|
|
long numberPoints = frompolydata.GetNumberOfPoints();
|
|
|
|
boolean fitPlane = (planeRadius > 0);
|
|
SmallBodyModel sbModel = new SmallBodyModel(frompolydata);
|
|
double diagonalLength = new BoundingBox(frompolydata.GetBounds()).getDiagonalLength();
|
|
|
|
ArrayList<vtksbCellLocator> cellLocators = new ArrayList<>();
|
|
for (vtkPolyData polydata : topolydata) {
|
|
vtksbCellLocator cellLocator = new vtksbCellLocator();
|
|
cellLocator.SetDataSet(polydata);
|
|
cellLocator.CacheCellBoundsOn();
|
|
cellLocator.AutomaticOn();
|
|
cellLocator.BuildLocator();
|
|
cellLocators.add(cellLocator);
|
|
}
|
|
|
|
vtkGenericCell cell = new vtkGenericCell();
|
|
double tol = 1e-6;
|
|
double[] t = new double[1];
|
|
double[] pcoords = new double[3];
|
|
int[] subId = new int[1];
|
|
long[] cell_id = new long[1];
|
|
|
|
double[] localNormal = null;
|
|
if (localModel) {
|
|
// fit a plane to the local model and check that the normal points outward
|
|
Plane localPlane = PolyDataUtil.fitPlaneToPolyData(frompolydata);
|
|
Vector3 localNormalVector = localPlane.getNormal();
|
|
if (localNormalVector.dot(localPlane.getPoint()) < 0) localNormalVector = localNormalVector.negate();
|
|
localNormal = localNormalVector.toArray();
|
|
}
|
|
|
|
double[] p = new double[3];
|
|
Vector3D origin = new Vector3D(0., 0., 0.);
|
|
for (int i = 0; i < numberPoints; ++i) {
|
|
points.GetPoint(i, p);
|
|
Vector3D thisPoint = new Vector3D(p);
|
|
|
|
Vector3D lookDir;
|
|
|
|
if (fitPlane) {
|
|
// fit a plane to the local area
|
|
System.arraycopy(p, 0, origin.toArray(), 0, 3);
|
|
lookDir = new Vector3D(sbModel.getNormalAtPoint(p, planeRadius)).normalize();
|
|
} else if (localModel) {
|
|
System.arraycopy(p, 0, origin.toArray(), 0, 3);
|
|
lookDir = new Vector3D(localNormal).normalize();
|
|
} else {
|
|
// use radial vector
|
|
lookDir = new Vector3D(p).normalize();
|
|
}
|
|
|
|
Vector3D lookPt = lookDir.scalarMultiply(diagonalLength);
|
|
lookPt = lookPt.add(thisPoint);
|
|
|
|
List<Vector3D> intersections = new ArrayList<>();
|
|
for (vtksbCellLocator cellLocator : cellLocators) {
|
|
double[] intersectPoint = new double[3];
|
|
|
|
// trace ray from thisPoint to the lookPt - Assume cell intersection is the closest one if
|
|
// there are multiple?
|
|
// NOTE: result should return 1 in case of intersection but doesn't sometimes.
|
|
// Use the norm of intersection point to test for intersection instead.
|
|
int result = cellLocator.IntersectWithLine(
|
|
thisPoint.toArray(), lookPt.toArray(), tol, t, intersectPoint, pcoords, subId, cell_id, cell);
|
|
Vector3D intersectVector = new Vector3D(intersectPoint);
|
|
|
|
NavigableMap<Double, Vector3D> pointsMap = new TreeMap<>();
|
|
if (intersectVector.getNorm() > 0) {
|
|
pointsMap.put(thisPoint.subtract(intersectVector).getNorm(), intersectVector);
|
|
}
|
|
|
|
// look in the other direction
|
|
lookPt = lookDir.scalarMultiply(-diagonalLength);
|
|
lookPt = lookPt.add(thisPoint);
|
|
result = cellLocator.IntersectWithLine(
|
|
thisPoint.toArray(), lookPt.toArray(), tol, t, intersectPoint, pcoords, subId, cell_id, cell);
|
|
|
|
intersectVector = new Vector3D(intersectPoint);
|
|
if (intersectVector.getNorm() > 0) {
|
|
pointsMap.put(thisPoint.subtract(intersectVector).getNorm(), intersectVector);
|
|
}
|
|
|
|
if (!pointsMap.isEmpty()) intersections.add(pointsMap.get(pointsMap.firstKey()));
|
|
}
|
|
|
|
if (intersections.isEmpty()) throw new Exception("Error: no intersections at all");
|
|
|
|
Vector3D meanIntersectionPoint = computeMeanPoint(intersections);
|
|
points.SetPoint(i, meanIntersectionPoint.toArray());
|
|
}
|
|
}
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
TerrasaurTool defaultOBJ = new AdjustShapeModelToOtherShapeModel();
|
|
|
|
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)));
|
|
|
|
boolean loadListFromFile = cl.hasOption("filelist");
|
|
double planeRadius = Double.parseDouble(cl.getOptionValue("fit-plane-radius", "-1"));
|
|
boolean localModel = cl.hasOption("local");
|
|
|
|
NativeLibraryLoader.loadVtkLibraries();
|
|
|
|
String fromfile = cl.getOptionValue("from");
|
|
String tofile = cl.getOptionValue("to");
|
|
String outfile = cl.getOptionValue("output");
|
|
|
|
Log4j2Configurator.getInstance();
|
|
logger.info("loading <from-obj-file>: {}", fromfile);
|
|
vtkPolyData frompolydata = PolyDataUtil.loadShapeModelAndComputeNormals(fromfile);
|
|
|
|
ArrayList<vtkPolyData> topolydata = new ArrayList<>();
|
|
if (loadListFromFile) {
|
|
List<String> lines = FileUtils.readLines(new File(tofile), Charset.defaultCharset());
|
|
for (String file : lines) {
|
|
|
|
// checking length prevents trying to load an empty line, such as the
|
|
// last line of the file.
|
|
if (file.length() > 1) {
|
|
logger.info("loading <to-obj-file>: {}", file);
|
|
topolydata.add(PolyDataUtil.loadShapeModelAndComputeNormals(file));
|
|
}
|
|
}
|
|
} else {
|
|
logger.info("loading <to-obj-file>: {}", tofile);
|
|
topolydata.add(PolyDataUtil.loadShapeModelAndComputeNormals(tofile));
|
|
}
|
|
|
|
adjustShapeModelToOtherShapeModel(frompolydata, topolydata, planeRadius, localModel);
|
|
|
|
PolyDataUtil.saveShapeModelAsOBJ(frompolydata, outfile);
|
|
|
|
logger.info("wrote {}", outfile);
|
|
}
|
|
}
|