From da56c78116536566691fe1e4ae655cf044c75638 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Radek=20O=C5=A1lej=C5=A1ek?= <oslejsek@fi.muni.cz>
Date: Mon, 3 Jan 2022 15:47:35 +0100
Subject: [PATCH] Resolve "Ground truth similarity stats"

---
 .../visitors/mesh/HausdorffDistance.java      |   2 +-
 .../tests/BatchSimilarityApproxHausdorff.java | 145 ++++++++++++++++++
 .../tests/BatchSimilarityGroundTruth.java     |  92 +++++++++++
 .../cz/fidentis/analyst/tests/BatchTests.java |  96 ------------
 .../analyst/mesh/core/MeshFacetImpl.java      |   3 +
 .../analyst/mesh/core/MeshTriangle.java       |   2 +-
 6 files changed, 242 insertions(+), 98 deletions(-)
 create mode 100644 GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityApproxHausdorff.java
 create mode 100644 GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruth.java
 delete mode 100644 GUI/src/main/java/cz/fidentis/analyst/tests/BatchTests.java

diff --git a/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/HausdorffDistance.java b/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/HausdorffDistance.java
index 02813452..283eff93 100644
--- a/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/HausdorffDistance.java
+++ b/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/HausdorffDistance.java
@@ -70,7 +70,7 @@ import javax.vecmath.Point3d;
  * @author Daniel Schramm
  * @author Radek Oslejsek
  */
-public class HausdorffDistance extends MeshVisitor  {
+    public class HausdorffDistance extends MeshVisitor  {
     
     /**
      * Distance computation strategies.
diff --git a/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityApproxHausdorff.java b/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityApproxHausdorff.java
new file mode 100644
index 00000000..32b5bf5b
--- /dev/null
+++ b/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityApproxHausdorff.java
@@ -0,0 +1,145 @@
+package cz.fidentis.analyst.tests;
+
+import cz.fidentis.analyst.face.HumanFace;
+import cz.fidentis.analyst.icp.IcpTransformer;
+import cz.fidentis.analyst.icp.NoUndersampling;
+import cz.fidentis.analyst.visitors.mesh.HausdorffDistance;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.DoubleSummaryStatistics;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A class for testing the efficiency of batch processing algorithms.
+ * The goal of this tool is to measure time and precision of 
+ * approximated Hausdorff distance (HD). 
+ * 
+ * - First, HD of the first face (primary face) with other faces is computed
+ *   - Each secondary face is superimposed using ICP
+ *   - Then it is stored in k-d tree 
+ *   - Relative (!) HD from the primary to secondary face is computed and stored.
+ *     The order cannot be reverted because we need HD matrices of the same size.
+ *   - TODO: First face should be replaced with average face
+ * - Then, the mutual distances are approximated:
+ *   - For each pair of faces A and B, the distance is computed as the difference 
+ *     between distances A-P and B-P, where P is the primary face.
+ * 
+ * @author Radek Oslejsek
+ */
+public class BatchSimilarityApproxHausdorff {
+    private static final String DATA_DIR = "../../analyst-data-antropologie/_ECA";
+    private static final String OUTPUT_FILE = "../../SIMILARITY_APPROX_HAUSDORFF.csv";
+    
+    /**
+     * Main method 
+     * @param args Input arguments 
+     * @throws IOException on IO error
+     */
+    public static void main(String[] args) throws IOException, ClassNotFoundException, Exception {
+        List<Path> faces = Files.list(new File(DATA_DIR).toPath())
+                .filter(f -> f.toString().endsWith(".obj"))
+                .sorted()
+                .limit(100)
+                .collect(Collectors.toList());
+        
+        
+        Map<Integer, List<Double>> distances = new HashMap<>();
+        
+        for (int i = 1; i < faces.size(); i++) {
+            HumanFace priFace = new HumanFace(faces.get(0).toFile());
+            HumanFace secFace = new HumanFace(faces.get(i).toFile());
+            
+            secFace.computeKdTree(false);
+            
+            System.out.println(i + "/" + (faces.size()-1) + " (" + priFace.getShortName() + "/" + secFace.getShortName() + ")");
+            
+            IcpTransformer icp = new IcpTransformer(priFace.getMeshModel(), 50, true, 0.3, new NoUndersampling());
+            secFace.getMeshModel().compute(icp, true);
+            
+            HausdorffDistance hd = new HausdorffDistance(secFace.getKdTree(), HausdorffDistance.Strategy.POINT_TO_POINT, true, true);
+            priFace.getMeshModel().compute(hd, true);
+            
+            //System.out.println(hd.getDistances().get(priFace.getMeshModel().getFacets().get(0)).size());
+            distances.put(i, hd.getDistances().get(priFace.getMeshModel().getFacets().get(0)));
+        }
+        
+        BufferedWriter bfw = new BufferedWriter(new FileWriter(OUTPUT_FILE));
+        bfw.write("PRI FACE;SEC FACE;MIN;MAX;AVG");
+        bfw.newLine();
+        
+        for (int i = 0; i < faces.size(); i++) {
+            for (int j = 0; j < faces.size(); j++) {
+                if (i == j) {
+                    continue;
+                }
+                
+                DoubleSummaryStatistics stats;
+                if (i == 0 || j == 0) {
+                    stats = getStats(distances.get((i == 0) ? j : i));
+                } else {
+                    stats = getStats(distances.get(i), distances.get(j));
+                }
+                
+                bfw.write(getShortName(faces.get(i).toFile())+";");
+                bfw.write(getShortName(faces.get(j).toFile())+";");
+                if (stats != null) {
+                    bfw.write(String.format("%.20f", stats.getMin())+";");
+                    bfw.write(String.format("%.20f", stats.getMax())+";");
+                    bfw.write(String.format("%.20f", stats.getAverage())+"");
+                    bfw.newLine();
+                } else {
+                    bfw.write(String.format("%.20f", Double.NaN)+";");
+                    bfw.write(String.format("%.20f", Double.NaN)+";");
+                    bfw.write(String.format("%.20f", Double.NaN)+"");
+                    bfw.newLine();
+                }
+                bfw.flush();
+            }
+        }
+        
+        bfw.close(); 
+        
+    }    
+
+    private static DoubleSummaryStatistics getStats(List<Double> d1, List<Double> d2) {
+        if (d1.size() != d2.size()) {
+            return null;
+        }
+        
+        List<Double> ret = new ArrayList<>(d1.size());
+        for (int i = 0; i < d1.size(); i++) {
+            ret.add(Math.abs(d2.get(i) - d1.get(i)));
+        }
+        
+        return ret.stream()
+                .mapToDouble(Double::doubleValue)
+                .summaryStatistics();
+    }
+    
+    private static DoubleSummaryStatistics getStats(List<Double> d1) {
+        List<Double> ret = new ArrayList<>(d1.size());
+        for (int i = 0; i < d1.size(); i++) {
+            ret.add(Math.abs(d1.get(i)));
+        }
+        
+        return d1.stream()
+                .mapToDouble(v -> Math.abs(v))
+                .summaryStatistics();
+    }
+    
+    private static String getShortName(File file) throws IOException {
+        String id = file.getCanonicalPath();
+        String name = id.substring(0, id.lastIndexOf('.')); // remove extention
+        name = name.substring(id.lastIndexOf('/')+1, name.length());
+        name = name.substring(name.lastIndexOf(File.separatorChar) + 1, name.length());
+        return name;
+    }
+}
diff --git a/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruth.java b/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruth.java
new file mode 100644
index 00000000..85852531
--- /dev/null
+++ b/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruth.java
@@ -0,0 +1,92 @@
+package cz.fidentis.analyst.tests;
+
+import cz.fidentis.analyst.face.HumanFace;
+import cz.fidentis.analyst.icp.IcpTransformer;
+import cz.fidentis.analyst.icp.NoUndersampling;
+import cz.fidentis.analyst.visitors.mesh.HausdorffDistance;
+import cz.fidentis.analyst.visitors.mesh.HausdorffDistance.Strategy;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.DoubleSummaryStatistics;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * A class for testing the efficiency of batch processing algorithms.
+ * The goal of this tool is to create a "ground truth" measurements for other optimization techniques
+ * 
+ * - All pairs are take one by one from the collection
+ * - First (primary) face is stored in k-d tree
+ * - Secondary face is superimposed using ICP
+ * - Hausdorff distance from the secondary to the primary face is computed using POINT_TO_POINT strategy and absolute distances.
+ * 
+ * @author Radek Oslejsek
+ */
+public class BatchSimilarityGroundTruth {
+    
+    private static final String DATA_DIR = "../../analyst-data-antropologie/_ECA";
+    private static final String OUTPUT_FILE = "../../SIMILARITY_GROUND_TRUTH.csv";
+    private static final int MAX_SAMPLES = 100;
+    
+    /**
+     * Main method 
+     * @param args Input arguments 
+     * @throws IOException on IO error
+     */
+    public static void main(String[] args) throws IOException, ClassNotFoundException, Exception {
+        List<Path> faces = Files.list(new File(DATA_DIR).toPath())
+                .filter(f -> f.toString().endsWith(".obj"))
+                .sorted()
+                .limit(MAX_SAMPLES)
+                .collect(Collectors.toList());
+        
+        BufferedWriter bfw = new BufferedWriter(new FileWriter(OUTPUT_FILE));
+        bfw.write("SEC FACE;PRI FACE;MIN;MAX;AVG (from SEC to PRI);IPC time (mm:ss,mil);HD time (mm:ss,mil)");
+        bfw.newLine();
+        
+        int counter = 1;
+        for (int i = 0; i < faces.size(); i++) {
+            HumanFace priFace = new HumanFace(faces.get(i).toFile());
+            priFace.computeKdTree(false);
+            
+            for (int j = 0; j < faces.size(); j++) {
+                if (i == j) {
+                    continue;
+                }
+                
+                HumanFace secFace = new HumanFace(faces.get(j).toFile());
+                
+                System.out.println(counter++ + "/" + (faces.size()*faces.size()-faces.size()) + " (" + priFace.getShortName() + "/" + secFace.getShortName() + ")");
+                
+                long icpTime = System.currentTimeMillis();
+                IcpTransformer icp = new IcpTransformer(priFace.getMeshModel(), 50, true, 0.3, new NoUndersampling());
+                secFace.getMeshModel().compute(icp, true);
+                Duration icpDuration = Duration.ofMillis(System.currentTimeMillis() - icpTime);
+                
+                long hdTime = System.currentTimeMillis();
+                HausdorffDistance hd = new HausdorffDistance(priFace.getKdTree(), Strategy.POINT_TO_POINT, false, true);
+                secFace.getMeshModel().compute(hd, true);
+                DoubleSummaryStatistics stats = hd.getStats();
+                Duration hdDuration = Duration.ofMillis(System.currentTimeMillis() - hdTime);
+                
+                bfw.write(secFace.getShortName()+";");
+                bfw.write(priFace.getShortName()+";");
+                bfw.write(String.format("%.20f", stats.getMin())+";");
+                bfw.write(String.format("%.20f", stats.getMax())+";");
+                bfw.write(String.format("%.20f", stats.getAverage())+";");
+                bfw.write(String.format("%02d:%02d,%03d", icpDuration.toMinutesPart(), icpDuration.toSecondsPart(), icpDuration.toMillisPart())+";");
+                bfw.write(String.format("%02d:%02d,%03d", hdDuration.toMinutesPart(), hdDuration.toSecondsPart(), hdDuration.toMillisPart())+"");
+                bfw.newLine();
+                bfw.flush();
+            }
+        }
+        
+        bfw.close();        
+    }
+
+}
diff --git a/GUI/src/main/java/cz/fidentis/analyst/tests/BatchTests.java b/GUI/src/main/java/cz/fidentis/analyst/tests/BatchTests.java
deleted file mode 100644
index 8b875288..00000000
--- a/GUI/src/main/java/cz/fidentis/analyst/tests/BatchTests.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package cz.fidentis.analyst.tests;
-
-import cz.fidentis.analyst.BatchProcessor;
-import cz.fidentis.analyst.face.HumanFace;
-import cz.fidentis.analyst.face.HumanFaceFactory;
-import cz.fidentis.analyst.icp.NoUndersampling;
-import cz.fidentis.analyst.icp.UndersamplingStrategy;
-import cz.fidentis.analyst.kdtree.KdTree;
-import cz.fidentis.analyst.mesh.io.MeshObjExporter;
-import java.io.File;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * A class for testing the efficiency of batch processing algorithms.
- * 
- * @author Radek Oslejsek
- */
-public class BatchTests {
-    
-    private static final String DATA_DIR = "../../analyst-data/multi-scan-models-anonymized-fixed/";
-    private static final String PRIMARY_FACE_PATH = DATA_DIR + "/average-boy-17-20/average_boy_17-20.obj";
-    //private static final String PRIMARY_FACE_PATH = dataDir + "/average-girl-17-20/average_girl_17-20.obj";
-    //private static final String PRIMARY_FACE_PATH = "../../analyst-data/basic-models/02.obj";
-    //private static final String PRIMARY_FACE_PATH = dataDir + "/07/00007_01_ECA.obj";
-    private static final String TEMPLATE_FACE_PATH = DATA_DIR + "template.obj";
-    
-    /**
-     * Main method 
-     * @param args Input arguments 
-     * @throws IOException on IO error
-     */
-    public static void main(String[] args) throws IOException, ClassNotFoundException, Exception {
-        BatchProcessor batch = new BatchProcessor(200); // the best is 500?
-        long startTime, endTime;
-        
-        List<String> faceIDs;
-        String primaryFaceId = HumanFaceFactory.instance().loadFace(new File(PRIMARY_FACE_PATH));
-        HumanFace primaryFace = HumanFaceFactory.instance().getFace(primaryFaceId);
-        KdTree primaryFacekdTree = primaryFace.computeKdTree(false);
-        
-        //////////////////////////////////////////////
-        System.out.println();
-        System.out.println("Loading files:");
-        startTime = System.currentTimeMillis();
-        faceIDs = batch.readFaces(DATA_DIR);
-        endTime = System.currentTimeMillis();
-        printTimes(startTime, endTime, faceIDs.size());
-        System.out.println(faceIDs.size() + "\t\t Number of files");
-        System.out.println(HumanFaceFactory.instance());
-        
-        //////////////////////////////////////////////
-        System.out.println();
-        System.out.println("Computation of symmetry planes:");
-        startTime = System.currentTimeMillis();
-        batch.computeSymmetryPlanes(faceIDs);
-        batch.computeSymmetryPlanes(Collections.singletonList(primaryFaceId));
-        endTime = System.currentTimeMillis();
-        printTimes(startTime, endTime, faceIDs.size());
-        
-        //////////////////////////////////////////////
-        UndersamplingStrategy strategy = new NoUndersampling();
-        //UndersamplingStrategy strategy = new RandomStrategy(0.5);
-        System.out.println();
-        System.out.println("ICP registration with " + strategy + ":");
-        startTime = System.currentTimeMillis();
-        batch.superimposeOnPrimaryFace(primaryFaceId, faceIDs, 10, 0.05, strategy);
-        endTime = System.currentTimeMillis();
-        printTimes(startTime, endTime, faceIDs.size());
-        System.out.printf("%.2f \t\t Avg number of iterations", batch.getAvgNumIcpIterations());
-        System.out.println();
-        
-        //////////////////////////////////////////////
-        System.out.println();
-        System.out.println("Computation of averaged template face:");
-        startTime = System.currentTimeMillis();
-        batch.computeTemplateFace(primaryFaceId, faceIDs);
-        endTime = System.currentTimeMillis();
-        printTimes(startTime, endTime, faceIDs.size());
-        System.out.println("Writing average template to " + TEMPLATE_FACE_PATH);
-        MeshObjExporter exp = new MeshObjExporter(batch.getAvgTemplateFace().getMeshModel());
-        exp.exportModelToObj(new File(TEMPLATE_FACE_PATH));
-    }
-    
-    private static void printTimes(long startTime, long endTime, int numFaces) {
-        double timeDist = endTime - startTime;
-        double avgTime = (timeDist/numFaces)/1000.0;
-        System.out.printf("%.2f sec \t Overall time", timeDist/1000.0);
-        System.out.println();
-        System.out.printf("%.2f sec \t Average time", avgTime);
-        System.out.println();
-        System.out.printf("%.2f min \t Estimated time for 500 faces", (avgTime*500)/60);
-        System.out.println();
-    }
-}
diff --git a/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshFacetImpl.java b/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshFacetImpl.java
index 6af74718..7d701dc0 100644
--- a/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshFacetImpl.java
+++ b/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshFacetImpl.java
@@ -209,6 +209,9 @@ public class MeshFacetImpl implements MeshFacet {
         
         for (MeshTriangle tri: this.getAdjacentTriangles(vertexIndex)) {
             Point3d projection = tri.getClosestPoint(point);
+            if (projection == null) {
+                continue;
+            }
             Vector3d aux = new Vector3d(projection);
             aux.sub(point);
             double d = aux.length();
diff --git a/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java b/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java
index 803940f1..e2fedd37 100644
--- a/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java
+++ b/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java
@@ -118,7 +118,7 @@ public class MeshTriangle implements Iterable<MeshPoint> {
      * the triangle inside the triangle boundaries.
      * 
      * @param point 3D point
-     * @return the closest point or {@code null} if the input parametr is missing
+     * @return the closest point or {@code null} if the input parameter is missing
      */
     public Point3d getClosestPoint(Point3d point) {
         if (point == null) {
-- 
GitLab