Commit 4f41d093 authored by Markéta Schlemmerová's avatar Markéta Schlemmerová
Browse files

Split nose tip detection and nose tip candidates detection

parent b7b3b999
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -66,12 +66,12 @@ public interface LandmarkServices {
    }

    /**
     * Detects and returns the tip of the nose from mesh model.
     * Return tip of the nose from mesh model.
     *
     * @param meshModel Mesh model
     * @return Tip of the nose {@link MeshPoint}
     * @return Nose tip
     */
    static MeshPoint detectNoseTip(MeshModel meshModel) {
        return NoseTipDetectorImpl.detectNoseTipFromCurvature(meshModel);
        return NoseTipDetectorImpl.detectNoseTip(meshModel);
    }
}
+38 −62
Original line number Diff line number Diff line
@@ -2,119 +2,95 @@ package cz.fidentis.analyst.engines.landmarks.impl;

import cz.fidentis.analyst.data.mesh.MeshModel;
import cz.fidentis.analyst.data.mesh.MeshPoint;
import java.util.Collections;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Detects tip of the nose in mesh model based on curvature properties.
 * Detects tip of the nose from mesh model based on curvature properties.
 *
 * @author Marketa Schlemmerova
 */
public class NoseTipDetectorImpl {

    private static final double MAX_H_CURVATURE = 0;
    private static final double MIN_K_CURVATURE = 0.0001;
    private static final double MAX_H_CURVATURE = 0.08;
    private static final double MIN_K_CURVATURE = 0.008;

    /**
     * Detects nose tip from mesh model. Filters candidate points based on curvature properties
     * and selects point with the highest Gaussian curvature.
     * Detects nose tip from mesh model using curvature properties. Selects point with the highest Gaussian curvature.
     *
     * @param meshModel Mesh model
     * @throws IllegalArgumentException If mesh model is null
     * @return Tip of the nose {@link MeshPoint}
     */
    public static MeshPoint detectNoseTipFromCurvature(MeshModel meshModel) {
        if (meshModel == null) {
            throw new IllegalArgumentException("mesh model cannot be null");
    public static MeshPoint detectNoseTip(MeshModel meshModel) {
        validateMeshModel(meshModel);

        return detectNoseTipCandidatesFromCurvature(meshModel).getFirst();
    }

        List<MeshPoint> candidatePoints = filterCandidatesFromCurvature(meshModel);
        return getMaxGaussianCurvaturePoint(candidatePoints);
    /**
     * Filters nose tip candidates sorted by Gaussian curvature.
     *
     * @param meshModel Mesh model
     * @throws IllegalArgumentException If mesh model is null
     * @return Tip of the nose candidates
     */
    public static List<MeshPoint> detectNoseTipCandidatesFromCurvature(MeshModel meshModel) {
        validateMeshModel(meshModel);

        List<MeshPoint> candidatePoints = filterPointsByCurvature(meshModel, MAX_H_CURVATURE, MIN_K_CURVATURE);
        return sortByGaussianCurvatureDesc(candidatePoints);
    }

    /**
     * Filters nose tip candidates based on HK curvature classification, using Mean and Gaussian curvature.
     *
     * @param meshModel Mesh model
     * @throws IllegalArgumentException If mesh model is null
     * @return Candidates for nose tip
     */
    private static List<MeshPoint> filterCandidatesFromCurvature(MeshModel meshModel) {
        if (meshModel == null) {
            throw new IllegalArgumentException("mesh model cannot be null");
        }
    private static List<MeshPoint> filterPointsByCurvature(MeshModel meshModel,
                                                           double maxMeanCurvature, double minGaussianCurvature) {
        validateMeshModel(meshModel);

        return meshModel.getFacets().parallelStream()
                .flatMap(facet -> facet.getVertices().stream())
                .filter(meshPoint -> {
                    var curvature = meshPoint.getCurvature();
                    return curvature != null &&
                            meshPoint.getCurvature().mean() < MAX_H_CURVATURE &&
                            meshPoint.getCurvature().gaussian() > MIN_K_CURVATURE;
                            meshPoint.getCurvature().mean() < maxMeanCurvature &&
                            meshPoint.getCurvature().gaussian() > minGaussianCurvature;
                })
                .collect(Collectors.toList());
    }

    /**
     * Returns mesh point with the highest Gaussian curvature from a list.
     * If the list is null or empty, returns null.
     * Sorts a list of mesh points by Gaussian curvature in descending order.
     *
     * @param candidatePoints Candidates for nose tip
     * @return Point with the highest Gaussian curvature {@link MeshPoint}
     * @param candidatePoints List of points to sort
     * @return Sorted list of points (highest Gaussian curvature first)
     */
    private static MeshPoint getMaxGaussianCurvaturePoint(List<MeshPoint> candidatePoints) {
    private static List<MeshPoint> sortByGaussianCurvatureDesc(List<MeshPoint> candidatePoints) {
        if (candidatePoints == null || candidatePoints.isEmpty()) {
            return null;
            return Collections.emptyList();
        }

        return candidatePoints.stream()
                .max(Comparator.comparingDouble(point -> point.getCurvature().gaussian()))
                .orElse(null);
    }

    /**
     * Filters mesh points where Mean curvature < MAX_H_CURVATURE for test purposes.
     *
     * @param meshModel Mesh model
     * @throws IllegalArgumentException If mesh model is null
     * @return Mesh points with Mean curvature below the threshold
     */
    public static List<MeshPoint> filterPointsByMeanCurvature(MeshModel meshModel) {
        if (meshModel == null) {
            throw new IllegalArgumentException("mesh model cannot be null");
        }

        return meshModel.getFacets().parallelStream()
                .flatMap(facet -> facet.getVertices().stream())
                .filter(meshPoint -> {
                    var curvature = meshPoint.getCurvature();
                    return curvature != null &&
                            meshPoint.getCurvature().mean() < MAX_H_CURVATURE;
                })
                .sorted(Comparator.comparingDouble(point -> -point.getCurvature().gaussian()))
                .collect(Collectors.toList());
    }

    /**
     * Filters mesh points where Gaussian curvature > MIN_K_CURVATURE for test purposes.
     * Validates that the provided mesh model is not null.
     *
     * @param meshModel Mesh model
     * @throws IllegalArgumentException If mesh model is null
     * @return Mesh points with Gaussian curvature above the threshold
     * @param meshModel Mesh model to validate
     * @throws IllegalArgumentException If the mesh model is null
     */
    public static List<MeshPoint> filterPointsByGaussianCurvature(MeshModel meshModel) {
    private static void validateMeshModel(MeshModel meshModel) {
        if (meshModel == null) {
            throw new IllegalArgumentException("mesh model cannot be null");
            throw new IllegalArgumentException("Mesh model cannot be null");
        }

        return meshModel.getFacets().parallelStream()
                .flatMap(facet -> facet.getVertices().stream())
                .filter(meshPoint -> {
                    var curvature = meshPoint.getCurvature();
                    return curvature != null &&
                            meshPoint.getCurvature().gaussian() > MIN_K_CURVATURE;
                })
                .collect(Collectors.toList());
    }
}
+43 −41
Original line number Diff line number Diff line
@@ -11,15 +11,15 @@ 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.List;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Test nose tip detection on Analyst Data Public.
 * Tests the functionality of the {@link NoseTipDetectorImpl} class on Analyst Data Public.
 *
 * @author Marketa Schlemmerova
 */
@@ -28,93 +28,95 @@ public class NoseTipDetectorImplTest {
    private static final Path TEST_FILE_DIRECTORY = Paths.get("src", "test", "resources",
            "cz", "fidentis", "analyst", "engines", "landmarks");

    private void detectNoseTip(String fileName) throws IOException {
        MeshPoint noseTip = getMeshModelFromObjFile(fileName);
        assertNotNull(noseTip, "nose tip not found");
        System.out.printf("Nose Tip for %s: %.5f %.5f %.5f%n", fileName, noseTip.getX(), noseTip.getY(), noseTip.getZ());
    private void noseCandidatesDetectionTest(String fileName) throws IOException {
        MeshModel meshModel = loadMeshModel(fileName);
        List<MeshPoint> noseCandidates = NoseTipDetectorImpl.detectNoseTipCandidatesFromCurvature(meshModel);

        assertNotNull(noseCandidates, "Candidate list is null.");
        assertFalse(noseCandidates.isEmpty(), "No nose tip candidates detected in: " + fileName);

        printNoseCandidatesDetails(noseCandidates);
    }

    private MeshPoint getMeshModelFromObjFile(String fileName) throws IOException {
        File scanFile = new File(TEST_FILE_DIRECTORY.toFile(), fileName);
        assertNotNull(scanFile);
    private MeshModel loadMeshModel(String fileName) throws IOException {
        File file = TEST_FILE_DIRECTORY.resolve(fileName).toFile();
        assertTrue(file.exists(), "File not found: " + fileName);

        MeshModel meshModel = MeshObjLoader.readFromObj(scanFile);
        assertNotNull(meshModel);
        MeshModel meshModel = MeshObjLoader.readFromObj(file);
        assertNotNull(meshModel, "Failed to load mesh model from file: " + fileName);

        CurvatureServices.computeAndSet(meshModel, new CurvatureConfig());
        assertTrue(meshModel.hasCurvature());
        assertTrue(meshModel.hasCurvature(), "Curvature not computed for file: " + fileName);

        printCurvatureCandidates(meshModel);
        return NoseTipDetectorImpl.detectNoseTipFromCurvature(meshModel);
        return meshModel;
    }

    private void printCurvatureCandidates(MeshModel meshModel){
        List<MeshPoint> meanCurvatureCandidates = new ArrayList<>(NoseTipDetectorImpl.filterPointsByMeanCurvature(meshModel));
        System.out.println("Number of candidates with Mean curvature < 0:          " + meanCurvatureCandidates.size());

        List<MeshPoint> gaussianCurvatureCandidates = new ArrayList<>(NoseTipDetectorImpl.filterPointsByGaussianCurvature(meshModel));
        System.out.println("Number of candidates with Gaussian curvature > 0.0001: " + gaussianCurvatureCandidates.size());
    private void printNoseCandidatesDetails(List<MeshPoint> candidates) {
        System.out.printf("Nose tip candidates: %d\n\n", candidates.size());
        for (MeshPoint candidate : candidates) {
            System.out.printf("%.5f, %.5f, %.5f\n", candidate.getX(), candidate.getY(), candidate.getZ());
        }
    }

    @Test
    void nullMeshModelTest() {
        Exception ex = assertThrows(IllegalArgumentException.class,
                () -> NoseTipDetectorImpl.detectNoseTipFromCurvature(null));
        assertTrue(ex.getMessage().contains("mesh model cannot be null"));
                () -> NoseTipDetectorImpl.detectNoseTipCandidatesFromCurvature(null));
        assertTrue(ex.getMessage().contains("Mesh model cannot be null"));
    }

    @Test
    void nose_detection_scan_average_boy() throws IOException {
        noseCandidatesDetectionTest("average_boy_17-20.obj");
    }

    @Test
    void nose_detection_scan_average_girl() throws IOException {
        noseCandidatesDetectionTest("average_girl_17-20.obj");
    }

    @Test
    void nose_detection_01() throws IOException {
        detectNoseTip("01.obj");
        noseCandidatesDetectionTest("01.obj");
    }

    @Test
    void nose_detection_02() throws IOException {
        detectNoseTip("02.obj");
        noseCandidatesDetectionTest("02.obj");
    }

    @Test
    void nose_detection_03() throws IOException {
        detectNoseTip("03.obj");
        noseCandidatesDetectionTest("03.obj");
    }

    @Test
    void nose_detection_04() throws IOException {
        detectNoseTip("04.obj");
        noseCandidatesDetectionTest("04.obj");
    }

    @Test
    void nose_detection_scan02() throws IOException {
        detectNoseTip("00002_01_ECA.obj");
        noseCandidatesDetectionTest("00002_01_ECA.obj");
    }

    @Test
    void nose_detection_scan04() throws IOException {
        detectNoseTip("00004_01_ECA.obj");
        noseCandidatesDetectionTest("00004_01_ECA.obj");
    }

    @Test
    void nose_detection_scan06() throws IOException {
        detectNoseTip("00006_01_ECA.obj");
        noseCandidatesDetectionTest("00006_01_ECA.obj");
    }

    @Test
    void nose_detection_scan07() throws IOException {
        detectNoseTip("00007_01_ECA.obj");
        noseCandidatesDetectionTest("00007_01_ECA.obj");
    }

    @Test
    void nose_detection_scan10() throws IOException {
        detectNoseTip("00010_01_ECA.obj");
    }

    @Test
    void nose_detection_scan_average_boy() throws IOException {
        detectNoseTip("average_boy_17-20.obj");
    }

    @Test
    void nose_detection_scan_average_girl() throws IOException {
        detectNoseTip("average_girl_17-20.obj");
        noseCandidatesDetectionTest("00010_01_ECA.obj");
    }
}