diff --git a/Comparison/src/main/java/cz/fidentis/analyst/visitors/face/HausdorffDistancePrioritized.java b/Comparison/src/main/java/cz/fidentis/analyst/visitors/face/HausdorffDistancePrioritized.java index 83ed7f1c7ce671694f97e4f66067e24cefe4d2b8..4402d79bc6c8b6a203563fcb75206e924390ac1e 100644 --- a/Comparison/src/main/java/cz/fidentis/analyst/visitors/face/HausdorffDistancePrioritized.java +++ b/Comparison/src/main/java/cz/fidentis/analyst/visitors/face/HausdorffDistancePrioritized.java @@ -54,7 +54,7 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { private final HausdorffDistance distanceVisitor; - private final Set<FeaturePointType> featurePointTypes; + private final Map<FeaturePointType, Double> featurePointTypes; private final Map<HumanFace, Map<FeaturePointType, Map<MeshFacet, List<Double>>>> priorities = new HashMap<>(); private final Map<HumanFace, Map<FeaturePointType, Map<MeshFacet, Double>>> featurePointWeights = new HashMap<>(); @@ -65,7 +65,8 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * * @param mainFacets Facets to which distance from the visited human face's facets is to be computed. * Must not be {@code null}. - * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized. + * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized + * together with the radii of priority spheres around the feature points of the corresponding type. * Must not be {@code null}. * @param strategy Strategy of the computation of distance * @param relativeDistance If {@code true}, then the visitor calculates the relative distances with respect @@ -74,11 +75,16 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * @param parallel If {@code true}, then the algorithm runs concurrently utilizing all CPU cores * @throws IllegalArgumentException if some parameter is wrong */ - public HausdorffDistancePrioritized(Set<MeshFacet> mainFacets, Set<FeaturePointType> featurePoints, Strategy strategy, boolean relativeDistance, boolean parallel) { + public HausdorffDistancePrioritized(Set<MeshFacet> mainFacets, Map<FeaturePointType, Double> featurePoints, Strategy strategy, boolean relativeDistance, boolean parallel) { distanceVisitor = new HausdorffDistance(mainFacets, strategy, relativeDistance, parallel); if (featurePoints == null) { throw new IllegalArgumentException("featurePoints"); } + for (final Double radius: featurePoints.values()) { + if (radius == null || radius < 0) { + throw new IllegalArgumentException("featurePoints"); + } + } featurePointTypes = featurePoints; } @@ -86,7 +92,8 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * Constructor. * * @param mainFacet Primary facet to which distance from the visited human face's facets is to be computed. Must not be {@code null}. - * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized. + * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized + * together with the radii of priority spheres around the feature points of the corresponding type. * Must not be {@code null}. * @param strategy Strategy of the computation of distance * @param relativeDistance If {@code true}, then the visitor calculates the relative distances with respect @@ -95,7 +102,7 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * @param parallel If {@code true}, then the algorithm runs concurrently utilizing all CPU cores * @throws IllegalArgumentException if some parameter is wrong */ - public HausdorffDistancePrioritized(MeshFacet mainFacet, Set<FeaturePointType> featurePoints, Strategy strategy, boolean relativeDistance, boolean parallel) { + public HausdorffDistancePrioritized(MeshFacet mainFacet, Map<FeaturePointType, Double> featurePoints, Strategy strategy, boolean relativeDistance, boolean parallel) { this(new HashSet<>(Collections.singleton(mainFacet)), featurePoints, strategy, relativeDistance, parallel); } @@ -104,7 +111,8 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * * @param mainModel The mesh model with primary facets to which distance from the visited human face's facets is to be computed. * Must not be {@code null} or empty. - * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized. + * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized + * together with the radii of priority spheres around the feature points of the corresponding type. * Must not be {@code null}. * @param strategy Strategy of the computation of distance * @param relativeDistance If {@code true}, then the visitor calculates the relative distances with respect @@ -113,7 +121,7 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * @param parallel If {@code true}, then the algorithm runs concurrently utilizing all CPU cores * @throws IllegalArgumentException if some parameter is wrong */ - public HausdorffDistancePrioritized(MeshModel mainModel, Set<FeaturePointType> featurePoints, Strategy strategy, boolean relativeDistance, boolean parallel) { + public HausdorffDistancePrioritized(MeshModel mainModel, Map<FeaturePointType, Double> featurePoints, Strategy strategy, boolean relativeDistance, boolean parallel) { this(new HashSet<>(mainModel.getFacets()), featurePoints, strategy, relativeDistance, parallel); } @@ -121,7 +129,8 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * Constructor. * * @param face Human face to which distance from other human faces is to be computed. Must not be {@code null}. - * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized. + * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized + * together with the radii of priority spheres around the feature points of the corresponding type. * Must not be {@code null}. * @param strategy Strategy of the computation of distance * @param relativeDistance If {@code true}, then the visitor calculates the relative distances with respect @@ -130,7 +139,7 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * @param parallel If {@code true}, then the algorithm runs concurrently utilizing all CPU cores * @throws IllegalArgumentException if some parameter is wrong */ - public HausdorffDistancePrioritized(HumanFace face, Set<FeaturePointType> featurePoints, Strategy strategy, boolean relativeDistance, boolean parallel) { + public HausdorffDistancePrioritized(HumanFace face, Map<FeaturePointType, Double> featurePoints, Strategy strategy, boolean relativeDistance, boolean parallel) { this(face.getMeshModel(), featurePoints, strategy, relativeDistance, parallel); } @@ -139,7 +148,8 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * * @param mainKdTree The KD tree to which distance from the visited human face's facets is to be computed. * Must not be {@code null}. - * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized. + * @param featurePoints Types of feature points according to which the Hausdorff distances will be prioritized + * together with the radii of priority spheres around the feature points of the corresponding type. * Must not be {@code null}. * @param strategy Strategy of the computation of distance * @param relativeDistance If {@code true}, then the visitor calculates the relative distances with respect @@ -148,23 +158,30 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * @param parallel If {@code true}, then the algorithm runs concurrently utilizing all CPU cores * @throws IllegalArgumentException if some parameter is wrong */ - public HausdorffDistancePrioritized(KdTree mainKdTree, Set<FeaturePointType> featurePoints, Strategy strategy, boolean relativeDistance, boolean parallel) { + public HausdorffDistancePrioritized(KdTree mainKdTree, Map<FeaturePointType, Double> featurePoints, Strategy strategy, boolean relativeDistance, boolean parallel) { distanceVisitor = new HausdorffDistance(mainKdTree, strategy, relativeDistance, parallel); if (featurePoints == null) { throw new IllegalArgumentException("featurePoints"); } + for (final Double radius: featurePoints.values()) { + if (radius == null || radius < 0) { + throw new IllegalArgumentException("featurePoints"); + } + } featurePointTypes = featurePoints; } /** * Returns types of feature points according to which the computation of Hausdorff - * distance is prioritized. + * distance is prioritized together with the radii of priority spheres around + * the feature points of the corresponding type. * * @return Types of feature points according to which the computation of Hausdorff - * distance is prioritized + * distance is prioritized together with the radii of priority spheres around + * the feature points of the corresponding type */ - public Set<FeaturePointType> getFeaturePointTypes() { - return Collections.unmodifiableSet(featurePointTypes); + public Map<FeaturePointType, Double> getFeaturePointTypes() { + return Collections.unmodifiableMap(featurePointTypes); } /** @@ -305,19 +322,45 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { public void visitHumanFace(HumanFace humanFace) { // Compute the Hasudorff distance using the 'distanceVisitor' humanFace.getMeshModel().compute(distanceVisitor, inParallel()); + + /* + * If there are no feature points to be computed + */ + if (featurePointTypes.isEmpty()) { + final List<MeshFacet> faceFacets = humanFace.getMeshModel().getFacets(); + final Map<MeshFacet, List<Double>> faceMergedPriorities = new HashMap<>(faceFacets.size()); + for (final MeshFacet facet: faceFacets) { + faceMergedPriorities.put(facet, Collections.nCopies(facet.getNumberOfVertices(), 0d)); + } + + synchronized (this) { + priorities.put(humanFace, Map.of()); + featurePointWeights.put(humanFace, Map.of()); + mergedPriorities.put(humanFace, faceMergedPriorities); + } + + return; + } + /* + * Compute priorities of humanFace's vertices for each of the given feature point types + */ final Map<MeshFacet, List<Double>> hausdorffDistances = distanceVisitor.getDistances(); - - // Compute priorities of humanFace's vertices for each of the given feature point types - for (final FeaturePointType fpType: featurePointTypes) { - final FeaturePoint featurePoint = humanFace.getFeaturePoints().get(fpType.getType()); // Get feature point of desired type + for (final Map.Entry<FeaturePointType, Double> fpType: featurePointTypes.entrySet()) { + final FeaturePointType featurePointType = fpType.getKey(); + final double featurePointRadius = fpType.getValue(); + + final FeaturePoint featurePoint = getFeaturePointByType(humanFace, featurePointType); + if (featurePoint == null) { + continue; + } - final PrioritySphere priorityVisitor = new PrioritySphere(featurePoint.getPosition(), computeSphereRadius(humanFace)); + final PrioritySphere priorityVisitor = new PrioritySphere(featurePoint.getPosition(), featurePointRadius); humanFace.getMeshModel().compute(priorityVisitor, inParallel()); synchronized (this) { priorities.computeIfAbsent(humanFace, face -> new HashMap<>()) - .computeIfAbsent(fpType, featurePointType -> new HashMap<>()) + .computeIfAbsent(featurePointType, fPointType -> new HashMap<>()) .putAll(priorityVisitor.getPriorities()); } @@ -331,7 +374,7 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { */ final List<Double> facetDistances = hausdorffDistances.get(facet); featurePointWeights.computeIfAbsent(humanFace, face -> new HashMap<>()) - .computeIfAbsent(fpType, featurePointType -> new HashMap<>()) + .computeIfAbsent(featurePointType, fPointType -> new HashMap<>()) .put(facet, IntStream.range(0, facetDistances.size()) .filter(i -> facetPriorities.get(i) > 0) // Filter out vertices that are outside of the priority sphere .mapToDouble(i -> facetDistances.get(i) * facetPriorities.get(i)) @@ -358,11 +401,20 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { } } - private double computeSphereRadius(HumanFace humanFace) { - // TODO TEMPORARY BEGIN - // Sphere radius needs to be computed dynamically. - // The best way to compute the right radius should be thought out in more depth. - return 1; - // TODO TEMPORARY END + /** + * Finds and returns a feature point of the desired type in the given human face. + * + * @param face Face containing the desired feature point + * @param type Type of the feature point to be obtained + * @return Feature point of the desired type + */ + protected FeaturePoint getFeaturePointByType(HumanFace face, FeaturePointType type) { + for (final FeaturePoint featurePoint: face.getFeaturePoints()) { + if (type.getType() == featurePoint.getFeaturePointType().getType()) { + return featurePoint; + } + } + + return null; } } diff --git a/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/PrioritySphere.java b/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/PrioritySphere.java index 81d0cfa0c6df54fc543fe82a5a0fa349a8a2682c..31c3b647c80261ce3c453b14a8ec3e7e9622493c 100644 --- a/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/PrioritySphere.java +++ b/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/PrioritySphere.java @@ -41,13 +41,16 @@ public class PrioritySphere extends MeshVisitor { * * @param sphereCenterPosition Position of the center of the sphere in which * the priorities will be calculated - * @param sphereRadius Radius of the sphere + * @param sphereRadius Radius of the sphere (must be greater than or equal to 0) */ public PrioritySphere(Point3d sphereCenterPosition, double sphereRadius) { if (sphereCenterPosition == null) { throw new IllegalArgumentException("sphereCenterPosition"); } this.sphereCenterPosition = sphereCenterPosition; + if (sphereRadius < 0) { + throw new IllegalArgumentException("sphereRadius"); + } this.sphereRadius = sphereRadius; } diff --git a/Comparison/src/test/java/cz/fidentis/analyst/visitors/face/HausdorffDistancePrioritizedTest.java b/Comparison/src/test/java/cz/fidentis/analyst/visitors/face/HausdorffDistancePrioritizedTest.java index a7315155e3a19555cbc843478ef336863fab7ffa..90bfef802aefa36c307f1dca9b2894d72e2f8803 100644 --- a/Comparison/src/test/java/cz/fidentis/analyst/visitors/face/HausdorffDistancePrioritizedTest.java +++ b/Comparison/src/test/java/cz/fidentis/analyst/visitors/face/HausdorffDistancePrioritizedTest.java @@ -14,11 +14,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.IntStream; import javax.vecmath.Point3d; import javax.vecmath.Vector3d; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -106,13 +106,20 @@ public class HausdorffDistancePrioritizedTest { return face; } + private static Map<FeaturePointType, Double> zipListsToMap(List<FeaturePointType> fpTypes, List<Double> fpRadii) { + assertEquals(fpTypes.size(), fpRadii.size()); + return IntStream.range(0, fpTypes.size()) + .boxed() + .collect(Collectors.toMap(fpTypes::get, fpRadii::get)); + } + protected void performTest(HumanFace face, Map<FeaturePointType, Map<MeshFacet, List<Double>>> expectedPriorities, Map<FeaturePointType, Map<MeshFacet, Double>> expectedFeaturePointWeights, Map<MeshFacet, List<Double>> expectedMergedPriorities, HausdorffDistancePrioritized visitor) { - final Set<FeaturePointType> featurePointTypes = visitor.getFeaturePointTypes(); + final Set<FeaturePointType> featurePointTypes = visitor.getFeaturePointTypes().keySet(); final List<FeaturePoint> faceFeaturePoints = face.getFeaturePoints(); featurePointTypes.forEach(fpType -> assertTrue(faceFeaturePoints.stream() .anyMatch(fPoint -> fPoint.getFeaturePointType().equals(fpType)))); @@ -203,9 +210,11 @@ public class HausdorffDistancePrioritizedTest { @Test public void singleFeaturePointTest() throws IOException { final List<Point3d> featurePoints = List.of(featurePoint1); - final List<FeaturePointType> featurePointTypes = List.of(fpType1); + final List<FeaturePointType> fpTypes = List.of(fpType1); + final Map<FeaturePointType, Double> featurePointTypes = zipListsToMap(fpTypes, List.of(1d)); + + final HumanFace face = getHumanFace(facets, featurePoints, fpTypes); - final HumanFace face = getHumanFace(facets, featurePoints, featurePointTypes); final Map<FeaturePointType, Map<MeshFacet, List<Double>>> expectedPriorities = Map.of(fpType1, expectedPrioritiesMapFP1); final Map<FeaturePointType, Map<MeshFacet, Double>> expectedFeaturePointsWeights = Map.of(fpType1, expectedWeightedDistancesMapFP1); @@ -215,7 +224,7 @@ public class HausdorffDistancePrioritizedTest { } final HausdorffDistancePrioritized visitor = new HausdorffDistancePrioritized(getHumanFace(facets2, List.of(), List.of()), - new HashSet<>(featurePointTypes), + featurePointTypes, POINT_TO_POINT, true, false); @@ -227,9 +236,10 @@ public class HausdorffDistancePrioritizedTest { public void twoFeaturePointsNoFacetIntersecTest() throws IOException { final List<Point3d> featurePoints = List.of(featurePoint1, new Point3d(3, 0, 0)); final FeaturePointType fpType2 = new FeaturePointType(1, null, null, null); - final List<FeaturePointType> featurePointTypes = List.of(fpType1, fpType2); + final List<FeaturePointType> fpTypes = List.of(fpType1, fpType2); + final Map<FeaturePointType, Double> featurePointTypes = zipListsToMap(fpTypes, List.of(1d, 1d)); - final HumanFace face = getHumanFace(facets, featurePoints, featurePointTypes); + final HumanFace face = getHumanFace(facets, featurePoints, fpTypes); final Map<MeshFacet, List<Double>> expectedPrioritiesMapFP2 = new HashMap<>(); for (final MeshFacet facet: facets) { @@ -251,7 +261,7 @@ public class HausdorffDistancePrioritizedTest { } final HausdorffDistancePrioritized visitor = new HausdorffDistancePrioritized(getHumanFace(facets2, List.of(), List.of()), - new HashSet<>(featurePointTypes), + featurePointTypes, POINT_TO_POINT, false, false); @@ -263,9 +273,10 @@ public class HausdorffDistancePrioritizedTest { public void twoFeaturePointsSingleFacetIntersecTest() throws IOException { final List<Point3d> featurePoints = List.of(featurePoint1, new Point3d(1, 0, 0)); final FeaturePointType fpType2 = new FeaturePointType(1, null, null, null); - final List<FeaturePointType> featurePointTypes = List.of(fpType1, fpType2); + final List<FeaturePointType> fpTypes = List.of(fpType1, fpType2); + final Map<FeaturePointType, Double> featurePointTypes = zipListsToMap(fpTypes, List.of(1d, 1d)); - final HumanFace face = getHumanFace(facets, featurePoints, featurePointTypes); + final HumanFace face = getHumanFace(facets, featurePoints, fpTypes); final Map<MeshFacet, List<Double>> expectedPrioritiesMapFP2 = new HashMap<>(); for (final MeshFacet facet: facets) { @@ -290,7 +301,7 @@ public class HausdorffDistancePrioritizedTest { expectedMergedPrioritiesMap.put(facet1, List.of(1d, 0.9, 0.8, 0.7, 0.6, 0.5, 0.6, 0.7, 0.8, 0.9, 1d, 0.9)); final HausdorffDistancePrioritized visitor = new HausdorffDistancePrioritized(getHumanFace(facets2, List.of(), List.of()), - new HashSet<>(featurePointTypes), + featurePointTypes, POINT_TO_POINT, true, false); @@ -304,9 +315,10 @@ public class HausdorffDistancePrioritizedTest { final FeaturePointType fpType2 = new FeaturePointType(1, null, null, null); final FeaturePointType fpType3 = new FeaturePointType(2, null, null, null); final FeaturePointType fpType4 = new FeaturePointType(3, null, null, null); - final List<FeaturePointType> featurePointTypes = List.of(fpType1, fpType2, fpType3, fpType4); + final List<FeaturePointType> fpTypes = List.of(fpType1, fpType2, fpType3, fpType4); + final Map<FeaturePointType, Double> featurePointTypes = zipListsToMap(fpTypes, List.of(1d, 1d, 1d, 1d)); - final HumanFace face = getHumanFace(facets, featurePoints, featurePointTypes); + final HumanFace face = getHumanFace(facets, featurePoints, fpTypes); final Map<MeshFacet, List<Double>> expectedPrioritiesMapFP2 = new HashMap<>(); final Map<MeshFacet, List<Double>> expectedPrioritiesMapFP3 = new HashMap<>(); @@ -353,7 +365,7 @@ public class HausdorffDistancePrioritizedTest { expectedMergedPrioritiesMap.put(facet5, mergedPriorities); final HausdorffDistancePrioritized visitor = new HausdorffDistancePrioritized(getHumanFace(facets2, List.of(), List.of()), - new HashSet<>(featurePointTypes), + featurePointTypes, POINT_TO_POINT, true, false); @@ -366,9 +378,10 @@ public class HausdorffDistancePrioritizedTest { final List<Point3d> featurePoints = List.of(featurePoint1, new Point3d(0.999, 0.999, 0), new Point3d(0.999, -0.999, 0)); final FeaturePointType fpType2 = new FeaturePointType(1, null, null, null); final FeaturePointType fpType3 = new FeaturePointType(2, null, null, null); - final List<FeaturePointType> featurePointTypes = List.of(fpType1, fpType2, fpType3); + final List<FeaturePointType> fpTypes = List.of(fpType1, fpType2, fpType3); + final Map<FeaturePointType, Double> featurePointTypes = zipListsToMap(fpTypes, List.of(1d, 1d, 1d)); - final HumanFace face = getHumanFace(facets, featurePoints, featurePointTypes); + final HumanFace face = getHumanFace(facets, featurePoints, fpTypes); final Map<MeshFacet, List<Double>> expectedPrioritiesMapFP2 = new HashMap<>(); final Map<MeshFacet, List<Double>> expectedPrioritiesMapFP3 = new HashMap<>(); @@ -415,7 +428,7 @@ public class HausdorffDistancePrioritizedTest { expectedMergedPrioritiesMap.put(facet4, mergedPriorities); final HausdorffDistancePrioritized visitor = new HausdorffDistancePrioritized(getHumanFace(facets2, List.of(), List.of()), - new HashSet<>(featurePointTypes), + featurePointTypes, POINT_TO_POINT, true, false); @@ -429,9 +442,10 @@ public class HausdorffDistancePrioritizedTest { final FeaturePointType fpType2 = new FeaturePointType(1, null, null, null); final FeaturePointType fpType3 = new FeaturePointType(2, null, null, null); final FeaturePointType fpType4 = new FeaturePointType(3, null, null, null); - final List<FeaturePointType> featurePointTypes = List.of(fpType1, fpType2, fpType3, fpType4); + final List<FeaturePointType> fpTypes = List.of(fpType1, fpType2, fpType3, fpType4); + final Map<FeaturePointType, Double> featurePointTypes = zipListsToMap(fpTypes, List.of(1d, 1d, 1d, 1d)); - final HumanFace face = getHumanFace(facets, featurePoints, featurePointTypes); + final HumanFace face = getHumanFace(facets, featurePoints, fpTypes); final Map<MeshFacet, List<Double>> expectedPrioritiesMapFP2 = new HashMap<>(); final Map<MeshFacet, List<Double>> expectedPrioritiesMapFP3 = new HashMap<>(); @@ -478,7 +492,7 @@ public class HausdorffDistancePrioritizedTest { expectedMergedPrioritiesMap.put(facet5, mergedPriorities); final HausdorffDistancePrioritized visitor = new HausdorffDistancePrioritized(getHumanFace(facets2, List.of(), List.of()), - new HashSet<>(featurePointTypes), + featurePointTypes, POINT_TO_POINT, true, true); diff --git a/GUI/src/main/java/cz/fidentis/analyst/core/ControlPanel.java b/GUI/src/main/java/cz/fidentis/analyst/core/ControlPanel.java index 02017a8501bff1c9ce5d0dc39c5a1f6dfc6aefd1..65eab763b3aafd2d5caa29bde27cd0b710aeaf65 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/core/ControlPanel.java +++ b/GUI/src/main/java/cz/fidentis/analyst/core/ControlPanel.java @@ -58,4 +58,29 @@ public abstract class ControlPanel extends JPanel { } }; } + + /** + * Creates and returns action listener that can be connected with a low-level + * GUI element (e.g., a button). Action event of the low-level element is then + * re-directed to the given {@code ControlPanelAction} as given command. + * The listener may also carry additional data as a payload. + * + * @param action An instance of the {@link ControlPanelAction} + * @param command Control panel command + * @param data Payload data of the action listener + * @return Action listener + */ + protected final ActionListener createListener(ActionListener action, String command, Object data) { + return new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + action.actionPerformed(new LoadedActionEvent( + e.getSource(), + ActionEvent.ACTION_PERFORMED, + command, + data) + ); + } + }; + } } diff --git a/GUI/src/main/java/cz/fidentis/analyst/core/ControlPanelBuilder.java b/GUI/src/main/java/cz/fidentis/analyst/core/ControlPanelBuilder.java index 5abef3e79f8d59530bc1c4902868257ab4309845..56a92405bbe223515e824557a3c99b2f36935620 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/core/ControlPanelBuilder.java +++ b/GUI/src/main/java/cz/fidentis/analyst/core/ControlPanelBuilder.java @@ -22,6 +22,7 @@ import javax.swing.JComboBox; import javax.swing.JFormattedTextField; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JTextField; import javax.swing.event.ChangeEvent; @@ -34,7 +35,7 @@ import javax.swing.text.NumberFormatter; */ public class ControlPanelBuilder { - public static final Font CAPTION_FONT= new Font("Arial", 1, 18); + public static final Font CAPTION_FONT = new Font("Arial", 1, 18); public static final Insets CAPTION_PADDING = new Insets(20, 0, 20, 0); // top, left, bottom, right public static final ImageIcon HELP_ICON = new ImageIcon(ControlPanelBuilder.class.getResource("/info.png")); public static final Font OPTION_TEXT_FONT = new Font("Arial", 1, 14); @@ -123,30 +124,57 @@ public class ControlPanelBuilder { * Adds a line with caption. * * @param caption Caption text. - * @return This new GUI object + * @return This new GUI object */ public JLabel addCaptionLine(String caption) { GridBagConstraints c = new GridBagConstraints(); c.insets = CAPTION_PADDING; - c.gridwidth = GridBagConstraints.REMAINDER; - c.gridx = col; - c.gridy = row; - c.anchor = GridBagConstraints.LINE_START; - c.fill = GridBagConstraints.NONE; - JLabel label = new JLabel(caption); + + JLabel label = addLabelLineCustom(caption, c); label.setFont(CAPTION_FONT); - controlPanel.add(label, c); + //addLine(); return label; } + /** + * Adds a line with a plain text label. + * + * @param text Text of the label + * @return This new GUI object + */ + public JLabel addLabelLine(String text) { + return addLabelLineCustom(text, new GridBagConstraints()); + } + + /** + * Adds a line with a plain text label whose appearance can be further + * customized by {@code constraints}. + * + * @param text Text of the label + * @param constraints {@code GridBagConstraints} to customize the label + * @return This new GUI object + */ + private JLabel addLabelLineCustom(String text, GridBagConstraints constraints) { + constraints.gridwidth = GridBagConstraints.REMAINDER; + constraints.gridx = col; + constraints.gridy = row; + constraints.anchor = GridBagConstraints.LINE_START; + constraints.fill = GridBagConstraints.NONE; + + JLabel label = new JLabel(text); + controlPanel.add(label, constraints); + + return label; + } + /** * Adds a line with slider option. * * @param helpAction Action listener invoked when the help icon is clicked. If {@code null}, then no help is shown. * @param text Option text. * @param sliderMax Max value of the slider (and the value field). If {@code -1}, then percentage slider is shown with 100 as the max. value. - * @param helpAction Action listener invoked when the input field is changed + * @param inputAction Action listener invoked when the input field is changed * @return Creates slider */ public JTextField addSliderOptionLine(ActionListener helpAction, String text, int sliderMax, ActionListener inputAction) { @@ -233,25 +261,51 @@ public class ControlPanelBuilder { GridBagConstraints c = new GridBagConstraints(); c.insets = new Insets(2, 0, 40, 0); // small external space on top and bottom c.weighty = 1.0; //request any extra vertical space - c.gridwidth = BUTTON_WIDTH; - c.gridy = row; c.anchor = GridBagConstraints.PAGE_END; - c.fill = GridBagConstraints.NONE; List<JButton> retButtons = new ArrayList<>(); for (int i = 0; i < buttons.size(); i++) { - JButton button = new JButton(); - button.setText(buttons.get(i)); - button.addActionListener(actions.get(i)); - c.gridx = col; - col += BUTTON_WIDTH; - controlPanel.add(button, c); + JButton button = addButtonCustom(buttons.get(i), actions.get(i), c); retButtons.add(button); } return retButtons; } + /** + * Adds a simple button. + * + * @param caption Label + * @param action Action listener invoked when the corresponding button is clicked + * @return This new GUI object + */ + public JButton addButton(String caption, ActionListener action) { + return addButtonCustom(caption, action, new GridBagConstraints()); + } + + /** + * Adds a simple button whose appearance can be further customized by {@code constraints}. + * + * @param caption Label + * @param action Action listener invoked when the corresponding button is clicked + * @param constraints {@code GridBagConstraints} to customize the button + * @return This new GUI object + */ + private JButton addButtonCustom(String caption, ActionListener action, GridBagConstraints constraints) { + JButton button = new JButton(); + button.setText(caption); + button.addActionListener(action); + + constraints.gridwidth = BUTTON_WIDTH; + constraints.gridy = row; + constraints.gridx = col; + col += BUTTON_WIDTH; + constraints.fill = GridBagConstraints.NONE; + controlPanel.add(button, constraints); + + return button; + } + /** * Adds a combo box * @param items Items @@ -307,7 +361,7 @@ public class ControlPanelBuilder { * Adds a check box. * * @param selected Initial state - * @param actions Action listener invoked when the checkbox is clicked. + * @param action Action listener invoked when the checkbox is clicked. * @return This builder */ public JCheckBox addCheckBox(boolean selected, ActionListener action) { @@ -330,7 +384,7 @@ public class ControlPanelBuilder { /** * Adds a help icon. * - * @param actions Action listener invoked when the icon is clicked. + * @param action Action listener invoked when the icon is clicked. * @return This builder */ public JButton addOptionHelpIcon(ActionListener action) { @@ -429,4 +483,25 @@ public class ControlPanelBuilder { return slider; } + /** + * Adds a scrollable pane that carries the given panel as its content. + * + * @param content Panel to be made scrollable + * @return This new GUI object + */ + public JScrollPane addScrollPane(JPanel content) { + JScrollPane scrollPane = new JScrollPane(content); + + GridBagConstraints c = new GridBagConstraints(); + c.weighty = 1.0; + c.gridwidth = GridBagConstraints.REMAINDER; + c.gridy = row; + c.gridx = col; + c.anchor = GridBagConstraints.CENTER; + c.fill = GridBagConstraints.BOTH; + controlPanel.add(scrollPane, c); + + return scrollPane; + } + } diff --git a/GUI/src/main/java/cz/fidentis/analyst/core/LoadedActionEvent.java b/GUI/src/main/java/cz/fidentis/analyst/core/LoadedActionEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..05b7c390a07fec6f137cc08b81beb01296b9dea8 --- /dev/null +++ b/GUI/src/main/java/cz/fidentis/analyst/core/LoadedActionEvent.java @@ -0,0 +1,81 @@ +package cz.fidentis.analyst.core; + +import java.awt.event.ActionEvent; + +/** + * A subclass of {@link ActionEvent} extended to carry additional data as a payload. + * + * @author Daniel Schramm + */ +public class LoadedActionEvent extends ActionEvent { + + private final Object data; + + /** + * Constructor. + * + * @param source The object that originated the event + * @param id An integer that identifies the event. + * For information on allowable values, see the class description + * for {@link ActionEvent} + * @param command A string that may specify a command (possibly one + * of several) associated with the event + * @param data Payload data of the event + * @throws IllegalArgumentException if {@code source} is null + */ + public LoadedActionEvent(Object source, int id, String command, Object data) { + super(source, id, command); + this.data = data; + } + + /** + * Constructor. + * + * @param source The object that originated the event + * @param id An integer that identifies the event. + * For information on allowable values, see the class description + * for {@link ActionEvent} + * @param command A string that may specify a command (possibly one + * of several) associated with the event + * @param modifiers The modifier keys down during event (shift, ctrl, alt, meta). + * Passing negative parameter is not recommended. + * Zero value means that no modifiers were passed + * @param data Payload data of the event + * @throws IllegalArgumentException if {@code source} is null + */ + public LoadedActionEvent(Object source, int id, String command, int modifiers, Object data) { + super(source, id, command, modifiers); + this.data = data; + } + + /** + * Constructor. + * + * @param source The object that originated the event + * @param id An integer that identifies the event. + * For information on allowable values, see the class description + * for {@link ActionEvent} + * @param command A string that may specify a command (possibly one + * of several) associated with the event + * @param modifiers The modifier keys down during event (shift, ctrl, alt, meta). + * Passing negative parameter is not recommended. + * Zero value means that no modifiers were passed + * @param when A long that gives the time the event occurred. + * Passing negative or zero value is not recommended + * @param data Payload data of the event + * @throws IllegalArgumentException if {@code source} is null + */ + public LoadedActionEvent(Object source, int id, String command, long when, int modifiers, Object data) { + super(source, id, command, when, modifiers); + this.data = data; + } + + /** + * Returns payload data of the action event. + * + * @return Payload data of the action event + */ + public Object getData() { + return data; + } +} diff --git a/GUI/src/main/java/cz/fidentis/analyst/distance/DistanceAction.java b/GUI/src/main/java/cz/fidentis/analyst/distance/DistanceAction.java index 8e4b6723b39d61d1742266ca7c00fe7b94e18ad0..d1d4d488e4bb06f67a1cb7e04cdf06a946521f45 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/distance/DistanceAction.java +++ b/GUI/src/main/java/cz/fidentis/analyst/distance/DistanceAction.java @@ -1,27 +1,45 @@ package cz.fidentis.analyst.distance; import cz.fidentis.analyst.canvas.Canvas; +import cz.fidentis.analyst.core.LoadedActionEvent; import cz.fidentis.analyst.core.ControlPanelAction; -import cz.fidentis.analyst.visitors.mesh.HausdorffDistance; +import cz.fidentis.analyst.feature.FeaturePoint; +import cz.fidentis.analyst.feature.FeaturePointType; +import cz.fidentis.analyst.mesh.core.MeshFacet; +import cz.fidentis.analyst.scene.DrawableFeaturePoints; +import cz.fidentis.analyst.visitors.face.HausdorffDistancePrioritized; import cz.fidentis.analyst.visitors.mesh.HausdorffDistance.Strategy; +import java.awt.Color; import java.awt.event.ActionEvent; +import java.util.DoubleSummaryStatistics; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import javax.swing.JComboBox; import javax.swing.JTabbedPane; +import javax.swing.JTextField; import javax.swing.JToggleButton; /** * Action listener for the curvature computation. * * @author Radek Oslejsek + * @author Daniel Schramm */ public class DistanceAction extends ControlPanelAction { /* * Attributes handling the state */ - private HausdorffDistance visitor = null; + private HausdorffDistancePrioritized visitor = null; + private final Map<FeaturePointType, Double> featurePointTypes = new HashMap<>(); private String strategy = DistancePanel.STRATEGY_POINT_TO_POINT; private boolean relativeDist = false; + private boolean weightedDist = false; + + private Map<MeshFacet, List<Double>> hausdorffDistance = null; private final DistancePanel controlPanel; @@ -33,12 +51,12 @@ public class DistanceAction extends ControlPanelAction { */ public DistanceAction(Canvas canvas, JTabbedPane topControlPanel) { super(canvas, topControlPanel); - this.controlPanel = new DistancePanel(this); + this.controlPanel = new DistancePanel(this, getSecondaryFeaturePoints().getFeaturePoints()); } @Override public void actionPerformed(ActionEvent ae) { - String action = ae.getActionCommand(); + final String action = ae.getActionCommand(); switch (action) { case DistancePanel.ACTION_COMMAND_SHOW_HIDE_PANEL: @@ -46,7 +64,7 @@ public class DistanceAction extends ControlPanelAction { break; case DistancePanel.ACTION_COMMAND_SHOW_HIDE_HEATMAP: if (((JToggleButton) ae.getSource()).isSelected()) { - setHeatmap(); + calculateHausdorffDistance(); getSecondaryDrawableFace().setRenderHeatmap(true); } else { getSecondaryDrawableFace().setRenderHeatmap(false); @@ -55,12 +73,27 @@ public class DistanceAction extends ControlPanelAction { case DistancePanel.ACTION_COMMAND_SET_DISTANCE_STRATEGY: strategy = (String) ((JComboBox) ae.getSource()).getSelectedItem(); this.visitor = null; // recompute - setHeatmap(); + calculateHausdorffDistance(); break; case DistancePanel.ACTION_COMMAND_RELATIVE_ABSOLUTE_DIST: this.relativeDist = ((JToggleButton) ae.getSource()).isSelected(); this.visitor = null; // recompute - setHeatmap(); + calculateHausdorffDistance(); + break; + case DistancePanel.ACTION_COMMAND_FEATURE_POINT_HIGHLIGHT: + highlightFeaturePoint((LoadedActionEvent) ae); + break; + case DistancePanel.ACTION_COMMAND_FEATURE_POINT_RESIZE: + resizeFeaturePoint((LoadedActionEvent) ae); + break; + case DistancePanel.ACTION_COMMAND_WEIGHTED_DISTANCE: + this.weightedDist = ((JToggleButton) ae.getSource()).isSelected(); + this.hausdorffDistance = null; // recompute only priorities + calculateHausdorffDistance(); + break; + case DistancePanel.ACTION_COMMAND_WEIGHTED_DIST_RECOMPUTE: + this.visitor = null; // recompute + calculateHausdorffDistance(); break; default: // to nothing @@ -68,8 +101,12 @@ public class DistanceAction extends ControlPanelAction { renderScene(); } - protected void setHeatmap() { - Strategy useStrategy; + /** + * (Re)calculates the Hausdorff distance and updates the heat map of the secondary face + * as well as values of all appropriate GUI elements of {@link DistancePanel}. + */ + protected void calculateHausdorffDistance() { + final Strategy useStrategy; switch (strategy) { case DistancePanel.STRATEGY_POINT_TO_POINT: useStrategy = Strategy.POINT_TO_POINT; @@ -81,11 +118,145 @@ public class DistanceAction extends ControlPanelAction { throw new UnsupportedOperationException(strategy); } - if (visitor == null) { - this.visitor = new HausdorffDistance(getPrimaryDrawableFace().getModel(), useStrategy, relativeDist, true); - getSecondaryDrawableFace().getModel().compute(visitor); + if (visitor == null) { + this.visitor = new HausdorffDistancePrioritized(getPrimaryDrawableFace().getModel(), + featurePointTypes, + useStrategy, + relativeDist, + true); + getSecondaryDrawableFace().getHumanFace().accept(visitor); + hausdorffDistance = null; + } + + // Update GUI elements that display the calculated Hausdorff distance + if (hausdorffDistance == null) { + hausdorffDistance = getWeightedDistance(); + setHausdorffDistanceStatistics(); + setFeaturePointWeigths(); + } + getSecondaryDrawableFace().setHeatMap(hausdorffDistance); + } + + /** + * Calculates weighted (or regular) Hausdorff distance of the face. + * + * @return weighted Hausdorff distance + */ + private Map<MeshFacet, List<Double>> getWeightedDistance() { + if (!weightedDist) { + return visitor.getDistances(); + } + + // Merge the map of distances with the map of priorities + final Map<MeshFacet, List<Double>> weightedDistances = new HashMap<>(visitor.getDistances()); + + final Map<MeshFacet, List<Double>> mergedPriorities = visitor.getMergedPriorities() + .get(getSecondaryDrawableFace().getHumanFace()); + for (final Map.Entry<MeshFacet, List<Double>> facetPriorities: mergedPriorities.entrySet()) { + weightedDistances.merge( + facetPriorities.getKey(), + facetPriorities.getValue(), + (distancesList, prioritiesList) -> + IntStream.range(0, distancesList.size()) + .mapToDouble(i -> distancesList.get(i) * prioritiesList.get(i)) + .boxed() + .collect(Collectors.toList())); + } + + return weightedDistances; + } + + /** + * Updates the GUI elements of {@link DistancePanel} elements that display + * statistical data about the calculated Hausdorff distance. + */ + private void setHausdorffDistanceStatistics() { + final DoubleSummaryStatistics distanceStats = hausdorffDistance.values() + .stream() + .flatMap(List::stream) + .mapToDouble(distance -> distance) + .summaryStatistics(); + + controlPanel.updateHausdorffDistanceStats( + distanceStats.getAverage(), + distanceStats.getMax(), + distanceStats.getMin() + ); + } + + /** + * Updates the GUI elements of {@link DistancePanel} that display + * the weights of feature points used to calculate the weighted Hausdorff distance. + */ + private void setFeaturePointWeigths() { + if (!weightedDist) { + controlPanel.updateFeaturePointWeights(Map.of()); + return; + } + + controlPanel.updateFeaturePointWeights( + visitor.getFeaturePointWeights() + .get(getSecondaryDrawableFace().getHumanFace()) // Get FP weights for the secondary face + .entrySet() + .stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, // For each FP type at the secondary face... + weights -> weights.getValue() // ... compute average FP weight over all its facets + .values() + .stream() + .mapToDouble(weight -> weight) + .average() + .orElse(Double.NaN)))); + } + + /** + * Changes the colour of the secondary face's feature point at the given index. + * The index is received as the data payload of {@code actionEvent}. + * + * @param actionEvent Action event with the index of the feature point as its payload data + */ + private void highlightFeaturePoint(LoadedActionEvent actionEvent) { + final int index = (int) actionEvent.getData(); + final FeaturePointType fpType = getTypeOfFeaturePoint(index); + final DrawableFeaturePoints secondaryFeaturePoints = getSecondaryFeaturePoints(); + + if (((JToggleButton) actionEvent.getSource()).isSelected()) { + secondaryFeaturePoints.setColor(index, Color.MAGENTA); + featurePointTypes.put(fpType, secondaryFeaturePoints.getSize(index)); + } else { + secondaryFeaturePoints.resetColorToDefault(index); + featurePointTypes.remove(fpType); + } + } + + /** + * Changes the size of the secondary face's feature point at the given index. + * The index is received as the data payload of {@code actionEvent}. + * + * @param actionEvent Action event with the index of the feature point as its payload data + */ + private void resizeFeaturePoint(LoadedActionEvent actionEvent) { + final int index = (int) actionEvent.getData(); + final double size = Double.parseDouble(((JTextField) actionEvent.getSource()).getText()); + + getSecondaryFeaturePoints().setSize(index, size); + featurePointTypes.replace(getTypeOfFeaturePoint(index), size); + } + + /** + * Returns type of the feature point at the given index in the secondary face. + * + * @param index Index of the feature point + * @return Type of the feature point or {@code null} + */ + private FeaturePointType getTypeOfFeaturePoint(int index) { + final List<FeaturePoint> featurePoints = getSecondaryFeaturePoints().getFeaturePoints(); + if (index < 0 || index >= featurePoints.size()) { + return null; } - getSecondaryDrawableFace().setHeatMap(visitor.getDistances()); + return featurePoints.get(index) + .getFeaturePointType(); } } diff --git a/GUI/src/main/java/cz/fidentis/analyst/distance/DistancePanel.java b/GUI/src/main/java/cz/fidentis/analyst/distance/DistancePanel.java index fe8f5da82d2b1fa30f5a146794d19fcdf1cc75ca..035a8274f3f007166f1eb7cd11ae3ce9ce1a5796 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/distance/DistancePanel.java +++ b/GUI/src/main/java/cz/fidentis/analyst/distance/DistancePanel.java @@ -2,15 +2,29 @@ package cz.fidentis.analyst.distance; import cz.fidentis.analyst.core.ControlPanel; import cz.fidentis.analyst.core.ControlPanelBuilder; +import cz.fidentis.analyst.feature.FeaturePoint; +import cz.fidentis.analyst.feature.FeaturePointType; +import cz.fidentis.analyst.scene.DrawableFeaturePoints; import cz.fidentis.analyst.symmetry.SymmetryPanel; +import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import javax.swing.BorderFactory; import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; /** * Control panel for Hausdorff distance. * * @author Radek Oslejsek + * @author Daniel Schramm */ public class DistancePanel extends ControlPanel { @@ -26,21 +40,28 @@ public class DistancePanel extends ControlPanel { public static final String ACTION_COMMAND_SHOW_HIDE_HEATMAP = "show-hide heatmap"; public static final String ACTION_COMMAND_SET_DISTANCE_STRATEGY = "set strategy"; public static final String ACTION_COMMAND_RELATIVE_ABSOLUTE_DIST = "switch abosulte-relative distance"; + public static final String ACTION_COMMAND_WEIGHTED_DISTANCE = "switch weighted distance on/off"; + public static final String ACTION_COMMAND_FEATURE_POINT_HIGHLIGHT = "highlight feature point with color"; + public static final String ACTION_COMMAND_FEATURE_POINT_RESIZE = "set size of feature point"; + public static final String ACTION_COMMAND_WEIGHTED_DIST_RECOMPUTE = "recompute weighted distance"; /* * Configuration of panel-specific GUI elements */ - public static final String STRATEGY_POINT_TO_POINT= "Point to point"; - public static final String STRATEGY_POINT_TO_TRIANGLE= "Point to triangle"; + public static final String STRATEGY_POINT_TO_POINT = "Point to point"; + public static final String STRATEGY_POINT_TO_TRIANGLE = "Point to triangle"; + + private final JPanel featurePointsStats; + private final JLabel avgHD, maxHD, minHD; /** * Constructor. * @param action Action listener */ - public DistancePanel(ActionListener action) { + public DistancePanel(ActionListener action, List<FeaturePoint> featurePoints) { this.setName(NAME); - ControlPanelBuilder builder = new ControlPanelBuilder(this); + final ControlPanelBuilder builder = new ControlPanelBuilder(this); builder.addCaptionLine("Computation options:"); builder.addLine(); @@ -61,6 +82,75 @@ public class DistancePanel extends ControlPanel { builder.addGap(); builder.addLine(); + final JCheckBox weightedDistChBx = builder.addCheckBoxOptionLine( + null, + "Weighted Hausdorff distance", + false, + createListener(action, ACTION_COMMAND_WEIGHTED_DISTANCE) + ); + builder.addLine(); + + final JPanel featurePointsPanel = new JPanel(); + final ControlPanelBuilder fpBuilder = new ControlPanelBuilder(featurePointsPanel); + final List<JCheckBox> fpCheckBoxes = new ArrayList<>(featurePoints.size()); + final List<JTextField> fpSliderInputs = new ArrayList<>(featurePoints.size()); + for (int i = 0; i < featurePoints.size(); i++) { + final FeaturePoint featurePoint = featurePoints.get(i); + + final JCheckBox checkBox = fpBuilder.addCheckBox( + false, + createListener(action, ACTION_COMMAND_FEATURE_POINT_HIGHLIGHT, i) + ); + checkBox.setText(featurePoint.getFeaturePointType().getName()); + fpCheckBoxes.add(checkBox); + + final JTextField sliderInput = fpBuilder.addSliderOptionLine( + null, + null, + 100, + createListener(action, ACTION_COMMAND_FEATURE_POINT_RESIZE, i) + ); + sliderInput.setText(ControlPanelBuilder.doubleToStringLocale(DrawableFeaturePoints.DEFAULT_SIZE)); + sliderInput.postActionEvent(); + fpSliderInputs.add(sliderInput); + + fpBuilder.addLine(); + } + builder.addScrollPane(featurePointsPanel) + .setBorder(BorderFactory.createTitledBorder("Feature points")); + builder.addLine(); + + final JButton recomputeButton = builder.addButton( + "Recompute", + createListener(action, ACTION_COMMAND_WEIGHTED_DIST_RECOMPUTE) + ); + recomputeButton.setEnabled(false); + recomputeButton.addActionListener((ActionEvent ae) -> + recomputeButton.setEnabled(false) + ); + weightedDistChBx.addActionListener((ActionEvent ae) -> { + if (!weightedDistChBx.isSelected()) { + recomputeButton.setEnabled(false); + } + }); + for (final JCheckBox checkBox: fpCheckBoxes) { + checkBox.addActionListener((ActionEvent ae) -> { + if (weightedDistChBx.isSelected()) { + recomputeButton.setEnabled(true); + } + }); + } + for (int i = 0; i < fpSliderInputs.size(); i++) { + final JTextField sliderInput = fpSliderInputs.get(i); + final JCheckBox sliderCheckBox = fpCheckBoxes.get(i); + sliderInput.addActionListener((ActionEvent ae) -> { + if (weightedDistChBx.isSelected() && sliderCheckBox.isSelected()) { + recomputeButton.setEnabled(true); + } + }); + } + builder.addLine(); + builder.addCaptionLine("Visualization options:"); builder.addLine(); builder.addCheckBoxOptionLine( @@ -70,6 +160,35 @@ public class DistancePanel extends ControlPanel { createListener(action, ACTION_COMMAND_SHOW_HIDE_HEATMAP) ); builder.addLine(); + + builder.addCaptionLine("Hausdorff distance:"); + builder.addLine(); + + builder.addOptionText("Average"); + builder.addGap(); + avgHD = builder.addLabelLine(""); + builder.addGap(); + builder.addLine(); + builder.addOptionText("Maximum"); + builder.addGap(); + maxHD = builder.addLabelLine(""); + builder.addGap(); + builder.addLine(); + builder.addOptionText("Minimum"); + builder.addGap(); + minHD = builder.addLabelLine(""); + builder.addGap(); + builder.addLine(); + + featurePointsStats = new JPanel(); + final JScrollPane fpStatsScrollPanel = builder.addScrollPane(featurePointsStats); + fpStatsScrollPanel.setBorder(BorderFactory.createTitledBorder("Feature point weights")); + fpStatsScrollPanel.setVisible(false); + weightedDistChBx.addActionListener((ActionEvent ae) -> { + fpStatsScrollPanel.setVisible(weightedDistChBx.isSelected()); + }); + builder.addLine(); + builder.addVerticalStrut(); } @@ -86,4 +205,38 @@ public class DistancePanel extends ControlPanel { public static ImageIcon getStaticIcon() { return new ImageIcon(SymmetryPanel.class.getClassLoader().getResource("/" + ICON)); } + + /** + * Updates GUI elements that display statistical data about the calculated Hausdorff distance. + * + * @param averageDist Average distance + * @param maxDist Maximum distance + * @param minDist Minimum distance + */ + public void updateHausdorffDistanceStats(double averageDist, double maxDist, double minDist) { + avgHD.setText(Double.toString(averageDist)); + maxHD.setText(Double.toString(maxDist)); + minHD.setText(Double.toString(minDist)); + } + + /** + * Updates GUI elements that display the weights of feature points + * used to calculate the weighted Hausdorff distance. + * + * @param featurePointWeights Map of feature point types and their weights + */ + public void updateFeaturePointWeights(Map<FeaturePointType, Double> featurePointWeights) { + featurePointsStats.removeAll(); + + final ControlPanelBuilder fpBuilder = new ControlPanelBuilder(featurePointsStats); + for (final Map.Entry<FeaturePointType, Double> fpStats: featurePointWeights.entrySet()) { + fpBuilder.addOptionText(fpStats.getKey().getName()); + fpBuilder.addGap(); + + fpBuilder.addLabelLine(Double.toString(fpStats.getValue())); + fpBuilder.addGap(); + fpBuilder.addLine(); + } + } + } diff --git a/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFace.java b/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFace.java index 9b7d6f1198e8aa0ab9e9f486856b76ab1e6bb46f..84bc48f766b69b2444ca4fbc73f00815ee5744e0 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFace.java +++ b/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFace.java @@ -1,8 +1,8 @@ package cz.fidentis.analyst.scene; import com.jogamp.opengl.GL2; +import cz.fidentis.analyst.face.HumanFace; import cz.fidentis.analyst.mesh.core.MeshFacet; -import cz.fidentis.analyst.mesh.core.MeshModel; import java.awt.Color; import java.util.Collections; import java.util.HashMap; @@ -19,10 +19,7 @@ public class DrawableFace extends DrawableMesh { public static final Color SKIN_COLOR_PRIMARY = new Color(224, 172, 105); public static final Color SKIN_COLOR_SECONDARY = new Color(236, 188, 180); - /* feature points */ - //private List<FeaturePoint> featurePoints = new ArrayList<>(); - //private List<Color> featurePointsColor = new ArrayList<>(); - //private boolean renderFeaturePoints = false; + private final HumanFace humanFace; private boolean renderHeatMap = false; /** @@ -31,13 +28,15 @@ public class DrawableFace extends DrawableMesh { private Map<MeshFacet, List<Double>> heatmap = new HashMap<>(); /** - * Constructor. + * Constructor. * - * @param model Drawable mesh model of the face - * @throws IllegalArgumentException if the model is {@code null} + * @param face Drawable human face + * @throws IllegalArgumentException if the mesh model + * of the human face is {@code null} */ - public DrawableFace(MeshModel model) { - super(model); + public DrawableFace(HumanFace face) { + super(face.getMeshModel()); + humanFace = face; setColor(SKIN_COLOR_PRIMARY); } @@ -50,12 +49,28 @@ public class DrawableFace extends DrawableMesh { this.heatmap = heatmap; } + /** + * Returns heatmap of the face + * (values at mesh vertices that are to be transferred to colors). + * + * @return Heatmap of the face + */ public Map<MeshFacet, List<Double>> getHeatMap() { return Collections.unmodifiableMap(heatmap); } + + /** + * Returns the human face. + * + * @return {@link HumanFace} + */ + public HumanFace getHumanFace() { + return humanFace; + } /** * Sets if the heatmap should be rendered or not. + * * @param render The switch */ public void setRenderHeatmap(boolean render) { @@ -64,6 +79,7 @@ public class DrawableFace extends DrawableMesh { /** * Determines whether the heatmap is set to be rendered. + * * @return id the heatmap will be rendered */ public boolean isHeatmapRendered() { diff --git a/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFeaturePoints.java b/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFeaturePoints.java index dceacfbde5e02c92a43aeeda63c041dbd2258f73..d17f2c3936988a3ecc6e1bcad3fac2076849be04 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFeaturePoints.java +++ b/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFeaturePoints.java @@ -14,10 +14,12 @@ import java.util.Map; * Drawable feature points. * * @author Radek Oslejsek + * @author Daniel Schramm */ public class DrawableFeaturePoints extends Drawable { public static final Color DEFAULT_COLOR = Color.LIGHT_GRAY; + public static final double DEFAULT_SIZE = 3f; private static final GLU GLU_CONTEXT = new GLU(); @@ -27,10 +29,16 @@ public class DrawableFeaturePoints extends Drawable { /** * feature points with color different from the default color */ - private Map<Integer, Color> specialColors = new HashMap<>(); + private final Map<Integer, Color> specialColors = new HashMap<>(); + + /** + * feature points with size different from the default size + */ + private final Map<Integer, Double> specialSizes = new HashMap<>(); /** * Constructor. + * * @param featurePoints Feature points */ public DrawableFeaturePoints(List<FeaturePoint> featurePoints) { @@ -39,9 +47,10 @@ public class DrawableFeaturePoints extends Drawable { } /** - * Returns color of given feature point + * Returns color of the feature point. + * * @param index Index of the feature point - * @return the color or {@code null} + * @return The color or {@code null} */ public Color getColor(int index) { if (index < 0 || index >= featurePoints.size()) { @@ -55,9 +64,10 @@ public class DrawableFeaturePoints extends Drawable { } /** - * Sets color of the feature point. + * Sets color of the feature point. + * * @param index Index of the feature point - * @param color Color + * @param color New color of the feature point */ public void setColor(int index, Color color) { if (index < 0 || index >= featurePoints.size() || color == null) { @@ -71,7 +81,9 @@ public class DrawableFeaturePoints extends Drawable { } /** - * Removes (possible) special color of given feature point. + * Removes (possible) special color of the feature point. + * + * @param index Index of the feature point */ public void resetColorToDefault(int index) { setColor(index, getColor()); @@ -83,6 +95,56 @@ public class DrawableFeaturePoints extends Drawable { public void resetAllColorsToDefault() { this.specialColors.clear(); } + + /** + * Returns size of the feature point. + * + * @param index Index of the feature point + * @return The size or {@code null} + */ + public Double getSize(int index) { + if (index < 0 || index >= featurePoints.size()) { + return null; + } + if (specialSizes.containsKey(index)) { + return specialSizes.get(index); + } else { + return DEFAULT_SIZE; + } + } + + /** + * Sets size of the feature point. + * + * @param index Index of the feature point + * @param size New size of the feature point + */ + public void setSize(int index, double size) { + if (index < 0 || index >= featurePoints.size()) { + return; + } + if (size == DEFAULT_SIZE) { + specialSizes.remove(index); + } else { + specialSizes.put(index, size); + } + } + + /** + * Removes (possible) special size of the feature point. + * + * @param index Index of the feature point + */ + public void resetSizeToDefault(int index) { + setSize(index, DEFAULT_SIZE); + } + + /** + * Removes all individual sizes of feature points. + */ + public void resetAllSizesToDefault() { + specialSizes.clear(); + } @Override protected void renderObject(GL2 gl) { @@ -108,7 +170,7 @@ public class DrawableFeaturePoints extends Drawable { GLU_CONTEXT.gluQuadricDrawStyle(center, GLU.GLU_FILL); GLU_CONTEXT.gluQuadricNormals(center, GLU.GLU_FLAT); GLU_CONTEXT.gluQuadricOrientation(center, GLU.GLU_OUTSIDE); - GLU_CONTEXT.gluSphere(center, 3f, 16, 16); + GLU_CONTEXT.gluSphere(center, specialSizes.getOrDefault(i, DEFAULT_SIZE), 16, 16); GLU_CONTEXT.gluDeleteQuadric(center); gl.glPopMatrix(); diff --git a/GUI/src/main/java/cz/fidentis/analyst/scene/Scene.java b/GUI/src/main/java/cz/fidentis/analyst/scene/Scene.java index 7ada8326754119e88649888babf15afc87991b6f..ab19624d5e94aab63bc1796d6c4d606ea7a8c86d 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/scene/Scene.java +++ b/GUI/src/main/java/cz/fidentis/analyst/scene/Scene.java @@ -29,7 +29,7 @@ public class Scene { } faces.add(face); - drawableFaces.add(new DrawableFace(face.getMeshModel())); + drawableFaces.add(new DrawableFace(face)); if (face.getFeaturePoints() != null) { drawableFeaturePoints.add(new DrawableFeaturePoints(face.getFeaturePoints())); } else { @@ -44,7 +44,7 @@ public class Scene { * Constructor for one-to-one analysis. * * @param primary Primary face to be analyzed - * @param primary Secondary face to be analyzed + * @param secondary Secondary face to be analyzed * @throws IllegalArgumentException if some face is {@code null} */ public Scene(HumanFace primary, HumanFace secondary) { @@ -58,8 +58,8 @@ public class Scene { faces.add(primary); faces.add(secondary); - drawableFaces.add(new DrawableFace(primary.getMeshModel())); - drawableFaces.add(new DrawableFace(secondary.getMeshModel())); + drawableFaces.add(new DrawableFace(primary)); + drawableFaces.add(new DrawableFace(secondary)); if (primary.getFeaturePoints() != null) { drawableFeaturePoints.add(new DrawableFeaturePoints(primary.getFeaturePoints()));