From ff5495ae6099e41dcbde38f4ba2bc8fe3d1a3a11 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Radek=20O=C5=A1lej=C5=A1ek?= <oslejsek@fi.muni.cz>
Date: Tue, 18 Jan 2022 13:05:18 +0100
Subject: [PATCH] Resolve "Fix landmarks initialization from file"

---
 .../cz/fidentis/analyst/face/HumanFace.java   | 66 +++++++++++++++----
 .../cz/fidentis/analyst/batch/IcpTask.java    |  1 -
 .../fidentis/analyst/core/ProjectTopComp.java | 14 +---
 .../services/FeaturePointCsvLoader.java       | 40 +++++++----
 4 files changed, 84 insertions(+), 37 deletions(-)

diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java
index 318bec54..d4489739 100644
--- a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java
+++ b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java
@@ -2,6 +2,7 @@ package cz.fidentis.analyst.face;
 
 import cz.fidentis.analyst.face.events.HumanFaceListener;
 import com.google.common.eventbus.EventBus;
+import cz.fidentis.analyst.Logger;
 import cz.fidentis.analyst.face.events.HumanFaceEvent;
 import cz.fidentis.analyst.feature.FeaturePoint;
 import cz.fidentis.analyst.feature.services.FeaturePointImportService;
@@ -75,9 +76,12 @@ public class HumanFace implements Serializable {
      * Use {@link restoreFromFile} to restore the human face from a dump file.
      * 
      * @param file OBJ file
+     * @param loadLandmarks If {@code true}, then the constructor aims to load 
+     * landmarks along with the mesh. Use {@link #getFeaturePoints()} to check 
+     * whether the landmarks (feature points) has been loaded.
      * @throws IOException on I/O failure
      */
-    public HumanFace(File file) throws IOException {
+    public HumanFace(File file, boolean loadLandmarks) throws IOException {
         meshModel = MeshObjLoader.read(new FileInputStream(file));
         meshModel.simplifyModel();
         this.id = file.getCanonicalPath();
@@ -87,6 +91,29 @@ public class HumanFace implements Serializable {
         bbox = visitor.getBoundingBox();
         
         eventBus = new EventBus();
+        
+        if (loadLandmarks) {
+            File landFile = this.findLandmarks();
+            if (landFile != null) {
+                try {
+                    loadFeaturePoints(landFile.getAbsoluteFile().getParent(), landFile.getName());
+                } catch(IOException ex) {
+                    Logger.print(ex.toString());
+                }
+            }
+        }
+    }
+    
+    /**
+     * Reads a 3D human face from the given OBJ file. 
+     * Also loads landmarks (feature points) if appropriate file is found.
+     * Use {@link restoreFromFile} to restore the human face from a dump file.
+     * 
+     * @param file OBJ file
+     * @throws IOException on I/O failure
+     */
+    public HumanFace(File file) throws IOException {
+        this(file, true);
     }
     
     /**
@@ -243,21 +270,11 @@ public class HumanFace implements Serializable {
     }
     
     /**
-     * Returns short name of the face distilled from the file name. May not be unique.
-     * @return short name of the face distilled from the file name
-     */
-    public String getName() {
-        String name = id.substring(0, id.lastIndexOf('.')); // remove extention
-        name = name.substring(id.lastIndexOf('/')+1, name.length());
-        return name;
-    }
-    
-    /**
-     * Returns short name of the face without its path in the name.
+     * Returns short name of the face without its path in the name. May not be unique.
      * @return short name of the face without its path in the name
      */
     public String getShortName() {
-        String name = this.getName();
+        String name = id.substring(0, id.lastIndexOf('.')); // remove extention
         name = name.substring(name.lastIndexOf(File.separatorChar) + 1, name.length());
         return name;
     }
@@ -308,6 +325,29 @@ public class HumanFace implements Serializable {
         return ret;
     }
     
+    /**
+     * Tries to find a file with landmarks definition based on the name of the face's OBJ file.
+     * @return The file with landmarks or {@code null}
+     */
+    public final File findLandmarks() {
+        String filename = getId().split(".obj")[0] + "_landmarks.csv";
+        if ((new File(filename)).exists()) {
+            return new File(filename);
+        }
+        
+        filename = getId().split("_ECA")[0] + "_landmarks.csv";
+        if ((new File(filename)).exists()) {
+            return new File(filename);
+        }
+        
+        filename = getId().split("_CA")[0] + "_landmarks.csv";
+        if ((new File(filename)).exists()) {
+            return new File(filename);
+        }
+        
+        return null;
+    }
+    
     /**
      * Creates serialized dump of the human face. Event buses are not stored.
      * Therefore, listeners have to re-register again after recovery.
diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java b/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java
index 3d9d3f50..093e56ac 100644
--- a/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java
+++ b/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java
@@ -9,7 +9,6 @@ import cz.fidentis.analyst.face.HumanFaceFactory;
 import cz.fidentis.analyst.icp.IcpTransformer;
 import cz.fidentis.analyst.icp.NoUndersampling;
 import cz.fidentis.analyst.icp.RandomStrategy;
-import cz.fidentis.analyst.icp.UndersamplingStrategy;
 import cz.fidentis.analyst.mesh.core.MeshModel;
 import cz.fidentis.analyst.scene.DrawableFace;
 import cz.fidentis.analyst.visitors.kdtree.AvgFaceConstructor;
diff --git a/GUI/src/main/java/cz/fidentis/analyst/core/ProjectTopComp.java b/GUI/src/main/java/cz/fidentis/analyst/core/ProjectTopComp.java
index da9809f5..d60fc747 100644
--- a/GUI/src/main/java/cz/fidentis/analyst/core/ProjectTopComp.java
+++ b/GUI/src/main/java/cz/fidentis/analyst/core/ProjectTopComp.java
@@ -15,8 +15,6 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -555,18 +553,10 @@ public final class ProjectTopComp extends TopComponent {
             for (File file : files) {
                 HumanFace face = null;
                 try {
-                    face = new HumanFace(file);
+                    face = new HumanFace(file, true);
                     out.printDuration("Loaded model " + face.getShortName() +" with " + face.getMeshModel().getNumVertices() + " vertices");
-                    // simple hack:
-                    Path path = Paths.get(file.getAbsolutePath());
-                    Path folder = path.getParent();
-                    Path filename = path.getFileName();
-                    String filestr = filename.toString();
-                    filestr = filestr.split("_ECA.obj")[0];
-                    filestr = filestr + "_landmarks.csv";
-                    face.loadFeaturePoints(folder.toString(), filestr);
                 } catch (IOException ex) {
-                    ex.printStackTrace();
+                    Logger.print(ex.toString());
                 }
 
                 String name = face.getShortName();
diff --git a/MeshModel/src/main/java/cz/fidentis/analyst/feature/services/FeaturePointCsvLoader.java b/MeshModel/src/main/java/cz/fidentis/analyst/feature/services/FeaturePointCsvLoader.java
index 32633708..0e9a7a25 100644
--- a/MeshModel/src/main/java/cz/fidentis/analyst/feature/services/FeaturePointCsvLoader.java
+++ b/MeshModel/src/main/java/cz/fidentis/analyst/feature/services/FeaturePointCsvLoader.java
@@ -14,13 +14,14 @@ import java.util.List;
 import java.util.stream.Stream;
 
 /**
- * Class used to import feature points from file of csv format
+ * Class used to import feature points from file of CSV format
  * 
  * @author Jakub Kolman
  */
 public class FeaturePointCsvLoader {
     
-    private static final String COLUMN_DELIMETER = ",";
+    private static final String PRIMARY_COLUMN_DELIMETER = ";";
+    private static final String SECONDARY_COLUMN_DELIMETER = ",";
     private static final String CODE_PREFIX_DELIMETER = " ";
 
     /**
@@ -36,28 +37,45 @@ public class FeaturePointCsvLoader {
         try (InputStreamReader streamReader
                 = new InputStreamReader(app.getFileAsStream(path, fileName), StandardCharsets.UTF_8);
                 BufferedReader reader = new BufferedReader(streamReader)) {
+            
+            String headerLine = reader.readLine();
+            final String delimiter;
+            if (headerLine.contains(PRIMARY_COLUMN_DELIMETER)) {
+                delimiter = PRIMARY_COLUMN_DELIMETER;
+            } else if (headerLine.contains(SECONDARY_COLUMN_DELIMETER)) {
+                delimiter = SECONDARY_COLUMN_DELIMETER;
+            } else {
+                throw new IOException(String.format("Feature point import file '%s' has wrong format - unknown delimiter", fileName));
+            }
 
             Stream<String> lines = reader.lines();
             List<List<String>> linesList = new ArrayList<>();
-
-            lines
-                    .forEach(line -> {
-                        linesList.add(Arrays.asList(line.split(COLUMN_DELIMETER)));
-                    });
-
-            if (linesList.size() != 2
-                    || linesList.get(0).size() != linesList.get(1).size()) {
+            
+            linesList.add(Arrays.asList(headerLine.split(delimiter, -1)));
+            lines.forEach(line -> { 
+                linesList.add(Arrays.asList(line.split(delimiter, -1)));
+            });
+            
+            if (linesList.stream().anyMatch(list -> list.size() != linesList.get(0).size())) {
                 throw new IOException(String.format("Feature point import file '%s' has wrong format", fileName));
             }
 
+            // TODO: In real data sets, there can be multiple FP collections (lines) in a single file,
+            // e.g., FPs before and after the face warping. Currently, we ignore them.
             List<FeaturePoint> points = new ArrayList<>();
             for (int i = 1; i < linesList.get(0).size(); i += 3) {
+                if (linesList.get(1).get(i).isBlank() 
+                        || linesList.get(1).get(i+1).isBlank() 
+                        || linesList.get(1).get(i+2).isBlank()) { // skip missing points
+                    continue;
+                }
                 FeaturePoint point = new FeaturePoint(
                         Double.parseDouble(linesList.get(1).get(i)),
                         Double.parseDouble(linesList.get(1).get(i + 1)),
                         Double.parseDouble(linesList.get(1).get(i + 2)),
                         FeaturePointTypeProvider.getInstance().getFeaturePointTypeByCode(
-                                getCode(linesList.get(0).get(i)))
+                                getCode(linesList.get(0).get(i))
+                        )
                 );
                 points.add(point);
             }
-- 
GitLab