From c5d994f1d22fba70958dd2147629280a974861cb Mon Sep 17 00:00:00 2001
From: Jakub Kolman <kubokolman@gmail.com>
Date: Mon, 2 May 2022 16:49:40 +0200
Subject: [PATCH] [123] feat: procrustes analysis with subset of feature points

---
 .../procrustes/ProcrustesAnalysis.java        | 81 ++++++++++++++++---
 .../ProcrustesAnalysisFaceModel.java          | 78 ++++++++++++++++--
 .../procrustes/ProcrustesVisitor.java         |  3 +-
 .../analyst/tests/ProcrustesVisitorTest.java  |  4 +-
 .../fidentis/analyst/rotation_landmarks.csv   |  2 +
 5 files changed, 147 insertions(+), 21 deletions(-)
 create mode 100644 GUI/src/test/resources/cz/fidentis/analyst/rotation_landmarks.csv

diff --git a/Comparison/src/main/java/cz/fidentis/analyst/procrustes/ProcrustesAnalysis.java b/Comparison/src/main/java/cz/fidentis/analyst/procrustes/ProcrustesAnalysis.java
index f5c90f37..a200183c 100644
--- a/Comparison/src/main/java/cz/fidentis/analyst/procrustes/ProcrustesAnalysis.java
+++ b/Comparison/src/main/java/cz/fidentis/analyst/procrustes/ProcrustesAnalysis.java
@@ -1,5 +1,6 @@
 package cz.fidentis.analyst.procrustes;
 
+import cz.fidentis.analyst.Logger;
 import cz.fidentis.analyst.face.HumanFace;
 import cz.fidentis.analyst.feature.FeaturePoint;
 import cz.fidentis.analyst.feature.api.IPosition;
@@ -8,13 +9,19 @@ import org.ejml.simple.SimpleMatrix;
 import org.ejml.simple.SimpleSVD;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.zip.DataFormatException;
+import javax.swing.JFrame;
+import javax.swing.JOptionPane;
 import javax.vecmath.Point3d;
 import javax.vecmath.Vector3d;
 
+import cz.fidentis.analyst.procrustes.ProcrustesAnalysisFaceModel;
+import java.util.Comparator;
+
 
 /**
  * @author Jakub Kolman
@@ -40,16 +47,39 @@ public class ProcrustesAnalysis {
             HumanFace humanFace1,
             HumanFace humanFace2) throws DataFormatException {
 
-        ProcrustesAnalysisFaceModel model1 = new ProcrustesAnalysisFaceModel(humanFace1);
-        ProcrustesAnalysisFaceModel model2 = new ProcrustesAnalysisFaceModel(humanFace2);
-
-        if (model1.getFeaturePointsMap().values().size() != model2.getFeaturePointsMap().values().size()) {
-            throw new DataFormatException("Lists of feature points do not have the same size");
-        }
-
-        if (!checkFeaturePointsType(
-                model1.getFeaturePointValues(), model2.getFeaturePointValues())) {
-            throw new DataFormatException("Lists of feature points do not have the same feature point types");
+        ProcrustesAnalysisFaceModel model1;
+        ProcrustesAnalysisFaceModel model2;
+        
+        if (humanFace1.getFeaturePoints().size() != humanFace2.getFeaturePoints().size()
+                || !checkFeaturePointsType(
+                        sortListByFeaturePointType(humanFace1.getFeaturePoints()), 
+                        sortListByFeaturePointType(humanFace1.getFeaturePoints()))) {
+
+//            int n = 0;
+            Object[] options = {"Yes", "No"};
+            int n = JOptionPane.showOptionDialog(
+                new JFrame("Warning"),
+                "The sets of feature points do not correspond with each other. Do you want to continue with a subset of feature points?",
+                "Warning",
+                JOptionPane.YES_NO_OPTION,
+                JOptionPane.QUESTION_MESSAGE,
+                null,
+                options,
+                options[1]);
+            if (n == 0) {
+                ArrayList<Integer> viablePoints = getSubsetOfFeaturePoints(humanFace1.getFeaturePoints(), humanFace2.getFeaturePoints());
+                model1 = new ProcrustesAnalysisFaceModel(humanFace1, viablePoints);
+                model2 = new ProcrustesAnalysisFaceModel(humanFace2, viablePoints);
+                Logger.print("Yes");
+                Logger.print(Integer.valueOf(n).toString());
+            } else { 
+                JOptionPane.showMessageDialog(new JFrame("Procrustes cancelled"), 
+                        "Lists of feature points do not have the same size and work on subset was cancelled");
+                throw new DataFormatException("Lists of feature points do not have the same size and work on subset was cancelled");
+            }
+        } else {
+            model1 = new ProcrustesAnalysisFaceModel(humanFace1);
+            model2 = new ProcrustesAnalysisFaceModel(humanFace2);
         }
 
         this.faceModel1 = model1;
@@ -426,4 +456,35 @@ public class ProcrustesAnalysis {
         }
     }
 
+    /**
+     * Finds corresponding subset of feature points from both faces so the procrustes analysis can be
+     * applied on the subset.
+     * 
+     * @param featurePoints
+     * @param featurePoints0 
+     * 
+     * @return List of feature point types that are in both sets of feature points
+     */
+    private ArrayList<Integer> getSubsetOfFeaturePoints(List<FeaturePoint> featurePoints1, List<FeaturePoint> featurePoints2) {
+        HashMap<Integer, FeaturePoint> fpMap = new HashMap<>();
+        for (FeaturePoint fp : featurePoints1) {
+            fpMap.put(fp.getFeaturePointType().getType(), fp);
+        }
+        
+        ArrayList<Integer> vaiablePoints = new ArrayList<>();
+        featurePoints2.forEach(fp -> {
+            if (fpMap.get(fp.getFeaturePointType().getType()) != null) {
+                vaiablePoints.add(Integer.valueOf(fp.getFeaturePointType().getType()));
+            }
+        });
+        return vaiablePoints;
+    }
+
+    private List<FeaturePoint> sortListByFeaturePointType(List<FeaturePoint> featurePointList) {
+        Collections.sort(
+                featurePointList, Comparator.comparingInt(fp -> fp.getFeaturePointType().getType())
+        );
+        return featurePointList;
+    }
+    
 }
diff --git a/Comparison/src/main/java/cz/fidentis/analyst/procrustes/ProcrustesAnalysisFaceModel.java b/Comparison/src/main/java/cz/fidentis/analyst/procrustes/ProcrustesAnalysisFaceModel.java
index 35be152d..74f90f39 100644
--- a/Comparison/src/main/java/cz/fidentis/analyst/procrustes/ProcrustesAnalysisFaceModel.java
+++ b/Comparison/src/main/java/cz/fidentis/analyst/procrustes/ProcrustesAnalysisFaceModel.java
@@ -44,6 +44,31 @@ public class ProcrustesAnalysisFaceModel {
         this.featurePointTypeCorrespondence = createFeaturePointTypeCorrespondence(face.getFeaturePoints());
     }
 
+    /**
+     * Constructor for face model if there is a need for a subset selection of
+     * feature points before applying procrustes analysis.
+     *
+     * @param face
+     * @param viablePoints
+     */
+    public ProcrustesAnalysisFaceModel(HumanFace face, List<Integer> viablePoints) {
+        this.humanFace = face;
+        // getFeaturePoints() returns unmodifiable List. To sort it we need to make copy first
+        List<FeaturePoint> modifiableFeaturePointList = getSelectionOfFeaturePoints(face.getFeaturePoints(), viablePoints);
+
+        // Currently set feature points is not working as inteded because gui doesn't repaint them after changing values
+        face.setFeaturePoints(modifiableFeaturePointList);
+
+        // To be able to move vertices we have to make a copy of them because  getFacets() returns unmodifiable List
+        List<MeshPoint> modifiableVertices = new ArrayList<>(face.getMeshModel().getFacets().get(0).getVertices());
+
+        this.vertices = modifiableVertices;
+        this.meshModel = face.getMeshModel();
+        this.featurePointsMap = createFeaturePointMap(
+                sortListByFeaturePointType(modifiableFeaturePointList));
+        this.featurePointTypeCorrespondence = createFeaturePointTypeCorrespondence(face.getFeaturePoints());
+    }
+
     /**
      * sets feature points map and also sets feature point on human face for
      * visualisation
@@ -53,7 +78,7 @@ public class ProcrustesAnalysisFaceModel {
     public void setFeaturePointsMap(HashMap<Integer, FeaturePoint> featurePointsMap) {
         this.featurePointsMap = featurePointsMap;
         readjustFeaturePoints(featurePointsMap);
-        
+
 //        this.humanFace.setFeaturePoints(getFeaturePointValues());
     }
 
@@ -81,6 +106,16 @@ public class ProcrustesAnalysisFaceModel {
         return featurePointTypeCorrespondence;
     }
 
+    private List<FeaturePoint> getSelectionOfFeaturePoints(List<FeaturePoint> featurePoints, List<Integer> viablePoints) {
+        ArrayList<FeaturePoint> selection = new ArrayList<>();
+        featurePoints.forEach(fp -> {
+            if (viablePoints.contains(fp.getFeaturePointType().getType())) {
+                selection.add(fp);
+            }
+        });
+        return selection;
+    }
+
     /**
      * Creates corresponding key value pair of matrix row index and a feature
      * point type value. It is used so we can get back type of feature point
@@ -128,17 +163,44 @@ public class ProcrustesAnalysisFaceModel {
     }
 
     private void readjustFeaturePoints(HashMap<Integer, FeaturePoint> featurePointsMap) {
+        if (featurePointsMap.size() != this.humanFace.getFeaturePoints().size()) {
+            readjustSelection(featurePointsMap);
+        } else {
+            this.humanFace.getFeaturePoints().forEach(fp -> {
+                FeaturePoint movedFp = featurePointsMap.get(fp.getFeaturePointType().getType());
+                if (fp.getFeaturePointType().getType()
+                        != movedFp.getFeaturePointType().getType()) {
+                    throw new RuntimeException("Types do not correspond");
+                }
+                fp.getPosition().x = movedFp.getX();
+                fp.getPosition().y = movedFp.getY();
+                fp.getPosition().z = movedFp.getZ();
+            });
+        }
+    }
+
+    /**
+     * Moves subset of feature points to their new position. If point doesn't belong to subset it is moved to (0,0,0) instead.
+     * 
+     * @param featurePointsMap 
+     */
+    private void readjustSelection(HashMap<Integer, FeaturePoint> featurePointsMap) {
         this.humanFace.getFeaturePoints().forEach(fp -> {
             FeaturePoint movedFp = featurePointsMap.get(fp.getFeaturePointType().getType());
-            if (fp.getFeaturePointType().getType()
-                    != movedFp.getFeaturePointType().getType()) {
-                throw new RuntimeException("Types do not correspond");
+            if (movedFp != null) {
+                if (fp.getFeaturePointType().getType()
+                        != movedFp.getFeaturePointType().getType()) {
+                    throw new RuntimeException("Types do not correspond");
+                }
+                fp.getPosition().x = movedFp.getX();
+                fp.getPosition().y = movedFp.getY();
+                fp.getPosition().z = movedFp.getZ();
+            } else {
+                fp.getPosition().x = 0;
+                fp.getPosition().y = 0;
+                fp.getPosition().z = 0;
             }
-            fp.getPosition().x = movedFp.getX();
-            fp.getPosition().y = movedFp.getY();
-            fp.getPosition().z = movedFp.getZ();
         });
-//        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
     }
 
 }
diff --git a/Comparison/src/main/java/cz/fidentis/analyst/visitors/procrustes/ProcrustesVisitor.java b/Comparison/src/main/java/cz/fidentis/analyst/visitors/procrustes/ProcrustesVisitor.java
index a86ec99f..063e7533 100644
--- a/Comparison/src/main/java/cz/fidentis/analyst/visitors/procrustes/ProcrustesVisitor.java
+++ b/Comparison/src/main/java/cz/fidentis/analyst/visitors/procrustes/ProcrustesVisitor.java
@@ -67,8 +67,7 @@ public class ProcrustesVisitor extends MeshVisitor {
     public void visitMeshFacet(MeshFacet transformedFacet) {
         try {
             ProcrustesAnalysis procrustes = new ProcrustesAnalysis(this.primaryFace, this.secondaryFace, scale);
-            this.transformation = procrustes.analyze();
-            
+            this.transformation = procrustes.analyze();            
 //            applyTransformations(this.transformation, transformedFacet);
         } catch (Exception e) {
             System.err.println(e.getMessage());
diff --git a/GUI/src/main/java/cz/fidentis/analyst/tests/ProcrustesVisitorTest.java b/GUI/src/main/java/cz/fidentis/analyst/tests/ProcrustesVisitorTest.java
index 158a6e07..25cacbe3 100644
--- a/GUI/src/main/java/cz/fidentis/analyst/tests/ProcrustesVisitorTest.java
+++ b/GUI/src/main/java/cz/fidentis/analyst/tests/ProcrustesVisitorTest.java
@@ -40,6 +40,8 @@ public class ProcrustesVisitorTest {
     private static final String FACE_2 = "0002_02_ECA.obj";
     private static final String FACE_FP_1 = "0002_01_landmarks.csv";
     private static final String FACE_FP_2 = "0002_02_landmarks.csv";
+    private static final String FACE_FP_ROTATION_1 = "rotation_180_moved_2_landmarks.csv";
+    private static final String FACE_FP_ROTATION_2 = "rotation_landmarks.csv";
     
     /**
      * Main method 
@@ -52,7 +54,7 @@ public class ProcrustesVisitorTest {
                 FeaturePointImportService featurePointImportService = new FeaturePointImportService();
 
         List<FeaturePoint> fpList1 = featurePointImportService.importFeaturePoints(
-                DATA_DIR.toString(), FACE_FP_1);
+                DATA_DIR.toString(), FACE_FP_ROTATION_1);
         List<FeaturePoint> fpList2= featurePointImportService.importFeaturePoints(
                 DATA_DIR.toString(), FACE_FP_2);
 
diff --git a/GUI/src/test/resources/cz/fidentis/analyst/rotation_landmarks.csv b/GUI/src/test/resources/cz/fidentis/analyst/rotation_landmarks.csv
new file mode 100644
index 00000000..95294f66
--- /dev/null
+++ b/GUI/src/test/resources/cz/fidentis/analyst/rotation_landmarks.csv
@@ -0,0 +1,2 @@
+Scan name,EX_R x,EX_R y,EX_R z,EX_L x,EX_L y,EX_L z,EN_R x,EN_R y,EN_R z,EN_L x,EN_L y,EN_L z,PAS_R x,PAS_R y,PAS_R z
+0002_01,0,20,0,0,-20,0,0,0,-20,0,0,50,20,0,0
-- 
GitLab