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 4402d79bc6c8b6a203563fcb75206e924390ac1e..1d2ccb68250f1d628c034be348ed6529a64d7855 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 @@ -29,23 +29,25 @@ import javax.vecmath.Point3d; * points separately</li> * <li>Priorities of the faces' vertices with respect to all the given types * of feature points merged together</li> - * <li>Average weighted Hausdorff distance with respect to the given types of feature - * points for each of the faces' mesh facets</li> + * <li>Weighted average of Hausdorff distance of all vertices within the priority sphere + * of feature points of the given types for each of the faces' mesh facets</li> * </ul> * This visitor is instantiated with a single k-d tree (either given as the input * parameter, or automatically created from the triangular mesh). * When applied to other human faces, it computes the Hausdorff distance from their mesh facets * to the instantiated k-d tree. + * * <p> - * The Hausdorff distance is computed either as absolute or relative. Absolute - * represents Euclidean distance (all numbers are positive). On the contrary, - * relative distance considers orientation of the visited face's facets (determined by its normal vectors) - * and produces positive or negative distances depending on whether the primary - * mesh is "in front of" or "behind" the given vertex. + * The Hausdorff distance is computed either as absolute or relative. Absolute + * represents Euclidean distance (all numbers are positive). On the contrary, + * relative distance considers orientation of the visited face's facets (determined by its normal vectors) + * and produces positive or negative distances depending on whether the primary + * mesh is "in front of" or "behind" the given vertex. * </p> + * * <p> - * This visitor is thread-safe, i.e., a single instance of the visitor can be used - * to inspect multiple human faces simultaneously. + * This visitor is thread-safe, i.e., a single instance of the visitor can be used + * to inspect multiple human faces simultaneously. * </p> * * @author Daniel Schramm @@ -187,12 +189,14 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { /** * Returns Hausdorff distance of the visited faces' mesh facets to the source mesh facets. * + * <p> * Keys in the map contain mesh facets that were measured with the - * source facets. + * source facets.<br> * For each facet of the visited human face, a list of distances to the source * facets is stored. The order of distances corresponds to the order of vertices * in the measured facet, i.e., the i-th value is the distance of the i-th vertex * of the visited face's facet. + * </p> * * @return Hausdorff distance for all points of all the visited human faces' facets */ @@ -203,12 +207,14 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { /** * Returns the nearest points of the visited faces' mesh facets to the source mesh facets. * + * <p> * Keys in the map contain mesh facets that were measured with the - * source facets. + * source facets.<br> * For each facet of the visited human face, a list of the nearest points to the source * facets is stored. The order of points corresponds to the order of vertices * in the measured facet, i.e., the i-th point is the nearest point of the i-th vertex * of the visited face's facet. + * </p> * * @return The nearest points for all points of all the visited human faces' facets */ @@ -221,15 +227,17 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * to the given types of feature points. * Priorities are calculated for each type of feature point separately. * + * <p> * Keys in the map contain human faces whose mesh facets were measured with the - * source facets. - * For each human face, there is a map of examined types of feature points. + * source facets.<br> + * For each human face, there is a map of examined types of feature points.<br> * Each feature point type then stores a map of priorities of vertices * of the face's mesh facets computed with respect to the face's feature point of * the corresponding type. * The order of priorities in the innermost map's value (list) corresponds to the order of vertices * in its corresponding key (facet), i.e., the i-th value in the list is the priority of * the i-th vertex of the facet. + * </p> * * @return Priorities of vertices with respect to the given types of feature points */ @@ -238,24 +246,26 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { } /** - * For all visited faces' mesh facets, the method returns the average weighted Hausdorff distance - * with respect to the given types of feature points (i.e. the sum of weighted distances - * of the mesh facet's vertices to the source facets divided by the number of vertices - * located within the feature point's priority sphere). - * The average weighted distances are calculated for each type of feature point separately. + * For all visited faces' mesh facets, the method returns the weighted average of Hausdorff distance + * of all vertices within the priority sphere of feature points of the given types + * (i.e. the sum of weighted distances of the mesh facet's vertices to the source facets + * divided by the sum of their priorities). + * The weighted average is calculated for each type of feature point separately. * + * <p> * Keys in the map contain human faces whose mesh facets were measured with the - * source facets. - * For each human face, there is a map of examined types of feature points. + * source facets.<br> + * For each human face, there is a map of examined types of feature points.<br> * Each feature point type then stores a map of the face's mesh facets together with - * the average weighted distances of their vertices to the source facets. - * The average weighted distances are calculated with respect to the face's feature point - * of the corresponding type. + * the weighted average of distance of their vertices to the source facets. + * The weighted average of distance is calculated with respect to the face's feature point + * of the corresponding type.<br> * <i>If there is no vertex within the priority sphere of a feature point, - * the value of the average weighted Hausdorff distance is {@link Double#NaN}</i> + * the value of the average weighted Hausdorff distance is {@link Double#NaN}</i> + * </p> * - * @return Average weighted Hausdorff distances with respect to the given types - * of feature points + * @return Weighted average of Hausdorff distance of all vertices within the priority sphere + * of feature points of the given types */ public Map<HumanFace, Map<FeaturePointType, Map<MeshFacet, Double>>> getFeaturePointWeights() { return Collections.unmodifiableMap(featurePointWeights); @@ -265,14 +275,16 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { * Returns priorities of vertices of the visited faces' mesh facets with respect * to all the given types of feature points merged together. * + * <p> * Keys in the map contain human faces whose mesh facets were measured with the - * source facets. + * source facets.<br> * For each human face, there is a map of priorities of vertices of the visited * face's mesh facets. The priorities are computed with respect to all the face's * feature points of given types together. * The order of priorities in the inner map's value (list) corresponds to the order of vertices * in its corresponding key (facet), i.e., the i-th value in the list is the priority of * the i-th vertex of the facet. + * </p> * * @return Priorities of vertices with respect to all given types of feature * points merged together @@ -370,19 +382,24 @@ public class HausdorffDistancePrioritized extends HumanFaceVisitor { synchronized (this) { /* - * Calculate the average weighted Hausdorff distance for the feature point. + * Calculate the weighted average of Hausdorff distance + * of all vertices within the feature point's priority sphere. */ final List<Double> facetDistances = hausdorffDistances.get(facet); featurePointWeights.computeIfAbsent(humanFace, face -> 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)) - .average() - .orElse(Double.NaN)); // If there is no vertex in the priority sphere + .boxed() + .collect(WeightedAverageCollector.toWeightedAverage( + facetDistances::get, + facetPriorities::get + )) + ); /* - * Merge priorities computed for the current feature point type with the priorities of already computed feature point types. + * Merge priorities computed for the current feature point type + * with the priorities of already computed feature point types. */ final List<Double> storedFacetPriorities = mergedPriorities .computeIfAbsent(humanFace, face -> new HashMap<>()) diff --git a/Comparison/src/main/java/cz/fidentis/analyst/visitors/face/WeightedAverageCollector.java b/Comparison/src/main/java/cz/fidentis/analyst/visitors/face/WeightedAverageCollector.java new file mode 100644 index 0000000000000000000000000000000000000000..f42241899438a6bec7f06a3148fa2c39c5e2035b --- /dev/null +++ b/Comparison/src/main/java/cz/fidentis/analyst/visitors/face/WeightedAverageCollector.java @@ -0,0 +1,135 @@ +package cz.fidentis.analyst.visitors.face; + +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; +import java.util.stream.Collector; + +/** + * A collector for calculation of the weighted average, suitable for use in Java 8 streams. + * + * <p> + * A mutable reduction operation that accumulates input elements into a mutable + * result container, transforming the accumulated result into a final + * representation after all input elements have been processed. + * </p> + * + * @author Daniel Schramm + * @param <T> Data type of the stream elements + */ +public class WeightedAverageCollector<T> implements Collector<T, IntemediateResults, Double> { + + private final ToDoubleFunction<? super T> valueFunction, weightFunction; + + /** + * Constructor. + * + * @param valueFunction Function returning the value for a given stream element + * @param weightFunction Function returning the weight for a given stream element + */ + public WeightedAverageCollector( + ToDoubleFunction<? super T> valueFunction, + ToDoubleFunction<? super T> weightFunction) { + this.valueFunction = valueFunction; + this.weightFunction = weightFunction; + } + + /** + * Returns a {@link Collector} interface object used to compute the weighted average. + * + * @param <T> Data type of the stream elements + * @param valueFunction Function returning the value for a given stream element + * @param weightFunction Function returning the weight for a given stream element + * @return A Collector interface object used to compute the weighted average + */ + public static <T> Collector<T, ?, Double> toWeightedAverage( + ToDoubleFunction<? super T> valueFunction, + ToDoubleFunction<? super T> weightFunction) { + return new WeightedAverageCollector(valueFunction, weightFunction); + } + + /** + * A function that creates and returns a new mutable result container. + * + * @return A function which returns a new, mutable result container + */ + @Override + public Supplier<IntemediateResults> supplier() { + return IntemediateResults::new; + } + + /** + * A function that folds a value into a mutable result container. + * + * @return A function which folds a value into a mutable result container + */ + @Override + public BiConsumer<IntemediateResults, T> accumulator() { + return (iResult, streamElement) -> { + iResult.weightedValSum += valueFunction.applyAsDouble(streamElement) + * weightFunction.applyAsDouble(streamElement); + iResult.weightSum += weightFunction.applyAsDouble(streamElement); + }; + } + + /** + * A function that accepts two partial results and merges them. The + * combiner function may fold state from one argument into the other and + * return that, or may return a new result container. + * + * @return A function which combines two partial results into a combined + * result + */ + @Override + public BinaryOperator<IntemediateResults> combiner() { + return (iResult1, iResult2) -> { + iResult1.weightedValSum += iResult2.weightedValSum; + iResult1.weightSum += iResult2.weightSum; + + return iResult1; + }; + } + + /** + * Perform the final transformation from the intermediate accumulation type + * {@link IntemediateResults} to the final result type {@link Double}. + * + * <p> + * If the characteristic {@code IDENTITY_FINISH} is + * set, this function may be presumed to be an identity transform with an + * unchecked cast from {@link IntemediateResults} to {@link Double}. + * </p> + * + * @return A function which transforms the intermediate result to the final + * result + */ + @Override + public Function<IntemediateResults, Double> finisher() { + return iResult -> iResult.weightedValSum / iResult.weightSum; + } + + /** + * Returns a {@code Set} of {@code Collector.Characteristics} indicating + * the characteristics of this Collector. + * + * @return An immutable set of collector characteristics + */ + @Override + public Set<Characteristics> characteristics() { + return Set.of(Characteristics.UNORDERED); + } +} + +/** + * A helper class which stores intermediate results + * for the {@link WeightedAverageCollector} class. + * + * @author Daniel Schramm + */ +class IntemediateResults { + double weightedValSum = 0; + double weightSum = 0; +} 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 90bfef802aefa36c307f1dca9b2894d72e2f8803..9706a4224d06ac9bbda21e6f68e9c8f354b82f85 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 @@ -45,7 +45,9 @@ public class HausdorffDistancePrioritizedTest { private final List<Double> prioritiesListFP1 = List.of(1d, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0d, 0d); private final double weightedDistanceFP1 = 0 + 0.09 + 0.16 + 0.21 + 0.24 + 0.25 + 0.24 + 0.21 + 0.16 + 0.09 + 0 + 0; - private final double averageWeightedDistanceFP1 = weightedDistanceFP1 / 10; // There are 10 vertices inside the priority sphere + private final double averageWeightedDistanceFP1 = weightedDistanceFP1 / prioritiesListFP1.stream() + .mapToDouble(Double::doubleValue) + .sum(); private final Point3d featurePoint1 = new Point3d(0, 0, 0); private final FeaturePointType fpType1 = new FeaturePointType(0, null, null, null); @@ -282,7 +284,8 @@ public class HausdorffDistancePrioritizedTest { for (final MeshFacet facet: facets) { expectedPrioritiesMapFP2.put(facet, Collections.nCopies(VERTICES_NUM, 0d)); } - expectedPrioritiesMapFP2.put(facet1, List.of(0d, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1d, 0.9)); + final List<Double> prioritiesList = List.of(0d, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1d, 0.9); + expectedPrioritiesMapFP2.put(facet1, prioritiesList); final Map<FeaturePointType, Map<MeshFacet, List<Double>>> expectedPriorities = Map.of(fpType1, expectedPrioritiesMapFP1, fpType2, expectedPrioritiesMapFP2); @@ -290,7 +293,12 @@ public class HausdorffDistancePrioritizedTest { for (final MeshFacet facet: facets) { expectedWeightedDistancesMapFP2.put(facet, Double.NaN); } - expectedWeightedDistancesMapFP2.put(facet1, (0 + 0.01 + 0.04 + 0.09 + 0.16 + 0.25 + 0.36 + 0.49 + 0.64 + 0.81 + 1 + 0.99) / 11); + expectedWeightedDistancesMapFP2.put( + facet1, + (0 + 0.01 + 0.04 + 0.09 + 0.16 + 0.25 + 0.36 + 0.49 + 0.64 + 0.81 + 1 + 0.99) / prioritiesList.stream() + .mapToDouble(Double::doubleValue) + .sum() + ); final Map<FeaturePointType, Map<MeshFacet, Double>> expectedFeaturePointsWeights = Map.of(fpType1, expectedWeightedDistancesMapFP1, fpType2, expectedWeightedDistancesMapFP2); @@ -346,7 +354,9 @@ public class HausdorffDistancePrioritizedTest { expectedWeightedDistancesMapFP3.put(facet, Double.NaN); expectedWeightedDistancesMapFP4.put(facet, Double.NaN); } - final double weightedDistance = (0 + 0.01 + 0.04 + 0.09 + 0.16 + 0.25 + 0.36 + 0.49 + 0.64 + 0.81 + 1 + 0.99) / 11; + final double weightedDistance = (0 + 0.01 + 0.04 + 0.09 + 0.16 + 0.25 + 0.36 + 0.49 + 0.64 + 0.81 + 1 + 0.99) / prioritiesList.stream() + .mapToDouble(Double::doubleValue) + .sum(); expectedWeightedDistancesMapFP2.put(facet1, weightedDistance); expectedWeightedDistancesMapFP3.put(facet3, weightedDistance); expectedWeightedDistancesMapFP4.put(facet5, -weightedDistance); @@ -409,7 +419,9 @@ public class HausdorffDistancePrioritizedTest { expectedWeightedDistancesMapFP2.put(facet, Double.NaN); expectedWeightedDistancesMapFP3.put(facet, Double.NaN); } - final double weightedDistance = 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 * vertexPriority + 0; + final double weightedDistance = (0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 * vertexPriority + 0) / prioritiesList.stream() + .mapToDouble(Double::doubleValue) + .sum(); expectedWeightedDistancesMapFP2.put(facet1, weightedDistance); expectedWeightedDistancesMapFP2.put(facet3, weightedDistance); expectedWeightedDistancesMapFP3.put(facet1, weightedDistance); @@ -473,7 +485,9 @@ public class HausdorffDistancePrioritizedTest { expectedWeightedDistancesMapFP3.put(facet, Double.NaN); expectedWeightedDistancesMapFP4.put(facet, Double.NaN); } - final double weightedDistance = (0 + 0.01 + 0.04 + 0.09 + 0.16 + 0.25 + 0.36 + 0.49 + 0.64 + 0.81 + 1 + 0.99) / 11; + final double weightedDistance = (0 + 0.01 + 0.04 + 0.09 + 0.16 + 0.25 + 0.36 + 0.49 + 0.64 + 0.81 + 1 + 0.99) / prioritiesList.stream() + .mapToDouble(Double::doubleValue) + .sum(); expectedWeightedDistancesMapFP2.put(facet1, weightedDistance); expectedWeightedDistancesMapFP3.put(facet3, weightedDistance); expectedWeightedDistancesMapFP4.put(facet5, -weightedDistance); 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 56a92405bbe223515e824557a3c99b2f36935620..6227837d1def1f36f56a49d61b97f825818263f3 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/core/ControlPanelBuilder.java +++ b/GUI/src/main/java/cz/fidentis/analyst/core/ControlPanelBuilder.java @@ -7,6 +7,9 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; import java.math.RoundingMode; import java.text.DecimalFormat; import java.text.NumberFormat; @@ -280,7 +283,10 @@ public class ControlPanelBuilder { * @return This new GUI object */ public JButton addButton(String caption, ActionListener action) { - return addButtonCustom(caption, action, new GridBagConstraints()); + final GridBagConstraints c = new GridBagConstraints(); + c.anchor = GridBagConstraints.LINE_START; + + return addButtonCustom(caption, action, c); } /** @@ -462,6 +468,39 @@ public class ControlPanelBuilder { } inputField.postActionEvent(); // invoke textField action listener }); + slider.addMouseListener( + new MouseAdapter() { + @Override + public void mouseExited(MouseEvent e) { + e.setSource(inputField); + inputField.dispatchEvent(e); + } + + @Override + public void mouseEntered(MouseEvent e) { + e.setSource(inputField); + inputField.dispatchEvent(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + e.setSource(inputField); + inputField.dispatchEvent(e); + } + + @Override + public void mousePressed(MouseEvent e) { + e.setSource(inputField); + inputField.dispatchEvent(e); + } + + @Override + public void mouseClicked(MouseEvent e) { + e.setSource(inputField); + inputField.dispatchEvent(e); + } + } + ); inputField.addActionListener((ActionEvent ae) -> { if (max == -1) { // percents in [0,1] 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 d803a9530c3ca85acfa34e8b98510a5cff88ca7e..a4ec60f28e493f4d41aa070506c088fa4418c2bb 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/distance/DistanceAction.java +++ b/GUI/src/main/java/cz/fidentis/analyst/distance/DistanceAction.java @@ -4,6 +4,7 @@ import com.jogamp.opengl.GL2; import cz.fidentis.analyst.canvas.Canvas; import cz.fidentis.analyst.core.LoadedActionEvent; import cz.fidentis.analyst.core.ControlPanelAction; +import cz.fidentis.analyst.core.ControlPanelBuilder; import cz.fidentis.analyst.feature.FeaturePoint; import cz.fidentis.analyst.feature.FeaturePointType; import cz.fidentis.analyst.mesh.core.MeshFacet; @@ -38,8 +39,7 @@ public class DistanceAction extends ControlPanelAction { private final Map<FeaturePointType, Double> featurePointTypes; private String strategy = DistancePanel.STRATEGY_POINT_TO_POINT; private boolean relativeDist = false; - private boolean weightedDist = false; - private boolean heatmapRender = false; + private String heatmapDisplayed = DistancePanel.HEATMAP_HAUSDORFF_DISTANCE; private boolean weightedFPsShow = true; private Map<MeshFacet, List<Double>> weightedHausdorffDistance = null; @@ -79,7 +79,7 @@ public class DistanceAction extends ControlPanelAction { fpWeights.getValue() // ... compute average FP weight over all its facets .values() .stream() - .mapToDouble(weight -> weight) + .mapToDouble(Double::doubleValue) .average() .orElse(Double.NaN))) .filter(fpWeight -> !Double.isNaN(fpWeight.getValue())) // Filter out feature points with Double.NaN weight @@ -123,7 +123,7 @@ public class DistanceAction extends ControlPanelAction { } } - // If the positin of secondary face has been changed + // If the position of secondary face has been changed if (secondaryFaceMoved) { visitor = null; // recompute Hausdorff distance @@ -139,7 +139,7 @@ public class DistanceAction extends ControlPanelAction { } updateHausdorffDistanceInformation(); - getSecondaryDrawableFace().setRenderHeatmap(heatmapRender); + getSecondaryDrawableFace().setRenderHeatmap(isHeatmapDisplayed()); } else { weightedFeaturePoints.hide(); } @@ -152,12 +152,10 @@ public class DistanceAction extends ControlPanelAction { final String action = ae.getActionCommand(); switch (action) { - case DistancePanel.ACTION_COMMAND_SHOW_HIDE_HEATMAP: - heatmapRender = ((JToggleButton) ae.getSource()).isSelected(); - if (heatmapRender) { - updateHausdorffDistanceInformation(); - } - getSecondaryDrawableFace().setRenderHeatmap(heatmapRender); + case DistancePanel.ACTION_COMMAND_SET_DISPLAYED_HEATMAP: + heatmapDisplayed = (String) ((JComboBox) ae.getSource()).getSelectedItem(); + updateHausdorffDistanceInformation(); + getSecondaryDrawableFace().setRenderHeatmap(isHeatmapDisplayed()); break; case DistancePanel.ACTION_COMMAND_SHOW_HIDE_WEIGHTED_FPOINTS: weightedFPsShow = ((JToggleButton) ae.getSource()).isSelected(); @@ -169,16 +167,23 @@ public class DistanceAction extends ControlPanelAction { break; case DistancePanel.ACTION_COMMAND_SET_DISTANCE_STRATEGY: strategy = (String) ((JComboBox) ae.getSource()).getSelectedItem(); - this.visitor = null; // recompute - updateHausdorffDistanceInformation(); + recompute(); break; case DistancePanel.ACTION_COMMAND_RELATIVE_ABSOLUTE_DIST: this.relativeDist = ((JToggleButton) ae.getSource()).isSelected(); - this.visitor = null; // recompute - updateHausdorffDistanceInformation(); + recompute(); break; case DistancePanel.ACTION_COMMAND_FEATURE_POINT_HIGHLIGHT: highlightFeaturePoint((LoadedActionEvent) ae); + recompute(); + break; + case DistancePanel.ACTION_COMMAND_FEATURE_POINT_SELECT_ALL: + highlightAllFeaturePoints(true); + recompute(); + break; + case DistancePanel.ACTION_COMMAND_FEATURE_POINT_SELECT_NONE: + highlightAllFeaturePoints(false); + recompute(); break; case DistancePanel.ACTION_COMMAND_FEATURE_POINT_HOVER_IN: hoverFeaturePoint((LoadedActionEvent) ae, true); @@ -189,13 +194,8 @@ public class DistanceAction extends ControlPanelAction { case DistancePanel.ACTION_COMMAND_FEATURE_POINT_RESIZE: resizeFeaturePoint((LoadedActionEvent) ae); break; - case DistancePanel.ACTION_COMMAND_WEIGHTED_DISTANCE: - this.weightedDist = ((JToggleButton) ae.getSource()).isSelected(); - updateHausdorffDistanceInformation(); - break; case DistancePanel.ACTION_COMMAND_DISTANCE_RECOMPUTE: - this.visitor = null; // recompute - updateHausdorffDistanceInformation(); + recompute(); break; default: // to nothing @@ -203,6 +203,15 @@ public class DistanceAction extends ControlPanelAction { renderScene(); } + /** + * Recalculates the Hausdorff distance and updates all GUI elements + * of {@link DistancePanel}. + */ + private void recompute() { + this.visitor = null; + updateHausdorffDistanceInformation(); + } + /** * (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}. @@ -218,7 +227,20 @@ public class DistanceAction extends ControlPanelAction { setHausdorffDistanceStatistics(); } - getSecondaryDrawableFace().setHeatMap(weightedDist ? weightedHausdorffDistance : visitor.getDistances()); + final Map<MeshFacet, List<Double>> heatmap; + switch (heatmapDisplayed) { + case DistancePanel.HEATMAP_HAUSDORFF_DISTANCE: + heatmap = visitor.getDistances(); + break; + case DistancePanel.HEATMAP_WEIGHTED_HAUSDORFF_DISTANCE: + heatmap = weightedHausdorffDistance; + break; + case DistancePanel.HEATMAP_HIDE: + return; + default: + throw new UnsupportedOperationException(heatmapDisplayed); + } + getSecondaryDrawableFace().setHeatMap(heatmap); } /** @@ -292,7 +314,7 @@ public class DistanceAction extends ControlPanelAction { weights -> weights.getValue() // ... compute average FP weight over all its facets .values() .stream() - .mapToDouble(weight -> weight) + .mapToDouble(Double::doubleValue) .average() .orElse(Double.NaN)))); } @@ -307,17 +329,26 @@ public class DistanceAction extends ControlPanelAction { .values() .stream() .flatMap(List::stream) - .mapToDouble(distance -> distance) + .mapToDouble(Double::doubleValue) .summaryStatistics(), weightedHausdorffDistance .values() .stream() .flatMap(List::stream) - .mapToDouble(distance -> distance) + .mapToDouble(Double::doubleValue) .summaryStatistics() ); } + /** + * Returns {@code true} if the heatmap is displayed and {@code false} otherwise. + * + * @return {@code true} if the heatmap is displayed, {@code false} otherwise + */ + private boolean isHeatmapDisplayed() { + return !DistancePanel.HEATMAP_HIDE.equals(heatmapDisplayed); + } + /** * Changes the color of the secondary face's feature point at the given index * and of its weighted representation when the feature point is (de)selected @@ -339,6 +370,32 @@ public class DistanceAction extends ControlPanelAction { } } + /** + * Changes the color of all secondary face's feature points and of their + * weighted representations. + * All feature points are also (de)selected for the computation + * of the weighted Hausdorff distance. + * + * @param highlighted {@code true} if the feature points are to be highlighted, + * {@code false} otherwise + */ + private void highlightAllFeaturePoints(boolean highlighted) { + if (highlighted) { + final List<FeaturePoint> featurePoints = getSecondaryFeaturePoints().getFeaturePoints(); + for (int i = 0; i < featurePoints.size(); i++) { + colorSecondaryFaceFeaturePoint(i, FEATURE_POINT_HIGHLIGHT_COLOR); + featurePointTypes.put( + featurePoints.get(i).getFeaturePointType(), + weightedFeaturePoints.getSize(i) + ); + } + } else { + getSecondaryFeaturePoints().resetAllColorsToDefault(); + weightedFeaturePoints.resetAllColorsToDefault(); + featurePointTypes.clear(); + } + } + /** * Changes the color of the secondary face's feature point at the given index * and of its weighted representation when the cursor hovers over the feature point's name. @@ -392,7 +449,7 @@ public class DistanceAction extends ControlPanelAction { */ private void resizeFeaturePoint(LoadedActionEvent actionEvent) { final int index = (int) actionEvent.getData(); - final double size = Double.parseDouble(((JTextField) actionEvent.getSource()).getText()); + final double size = ControlPanelBuilder.parseLocaleDouble((JTextField) actionEvent.getSource()); weightedFeaturePoints.setSize(index, size); featurePointTypes.replace(getTypeOfFeaturePoint(index), size); 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 057526561553eb5625a0962da723ff3cf00472fc..a04b4273a17a57e1bc5cd7681eb9c48bef99dfc1 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/distance/DistancePanel.java +++ b/GUI/src/main/java/cz/fidentis/analyst/distance/DistancePanel.java @@ -9,20 +9,24 @@ import cz.fidentis.analyst.scene.DrawableFeaturePoints; import cz.fidentis.analyst.symmetry.SymmetryPanel; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.util.ArrayList; import java.util.DoubleSummaryStatistics; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.AbstractAction; +import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; +import javax.swing.KeyStroke; /** * Control panel for Hausdorff distance. @@ -41,13 +45,14 @@ public class DistancePanel extends ControlPanel { /* * External actions */ - public static final String ACTION_COMMAND_SHOW_HIDE_HEATMAP = "show-hide heatmap"; + public static final String ACTION_COMMAND_SET_DISPLAYED_HEATMAP = "set displayed 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_HOVER_IN = "highlight hovered FP"; public static final String ACTION_COMMAND_FEATURE_POINT_HOVER_OUT = "set default color of hovered FP"; public static final String ACTION_COMMAND_FEATURE_POINT_HIGHLIGHT = "highlight feature point with color"; + public static final String ACTION_COMMAND_FEATURE_POINT_SELECT_ALL = "select all feature points"; + public static final String ACTION_COMMAND_FEATURE_POINT_SELECT_NONE = "deselect all feature points"; public static final String ACTION_COMMAND_FEATURE_POINT_RESIZE = "set size of feature point"; public static final String ACTION_COMMAND_DISTANCE_RECOMPUTE = "recompute the Hausdorff distance"; public static final String ACTION_COMMAND_SHOW_HIDE_WEIGHTED_FPOINTS = "show-hide weighted feature points"; @@ -57,8 +62,12 @@ public class DistancePanel extends ControlPanel { */ 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 HEATMAP_HAUSDORFF_DISTANCE = "Hausdorff distance"; + public static final String HEATMAP_WEIGHTED_HAUSDORFF_DISTANCE = "Weighted Hausdorff distance"; + public static final String HEATMAP_HIDE = "None"; private final Map<FeaturePointType, JLabel> featurePointStats; + private final List<JCheckBox> featurePointCheckBoxes; private final JLabel avgHD, avgHDWeight, maxHD, maxHDWeight, minHD, minHDWeight; /** @@ -97,16 +106,16 @@ public class DistancePanel extends ControlPanel { final JPanel featurePointsPanel = new JPanel(); final ControlPanelBuilder fpBuilder = new ControlPanelBuilder(featurePointsPanel); featurePointStats = new HashMap<>(featurePoints.size()); + featurePointCheckBoxes = new ArrayList<>(featurePoints.size()); for (int i = 0; i < featurePoints.size(); i++) { final FeaturePoint featurePoint = featurePoints.get(i); final FeaturePointType featurePointType = featurePoint.getFeaturePointType(); final JCheckBox checkBox = fpBuilder.addCheckBox( selectedFPs.contains(featurePointType), - createListener(action, ACTION_COMMAND_DISTANCE_RECOMPUTE) + createListener(action, ACTION_COMMAND_FEATURE_POINT_HIGHLIGHT, i) ); checkBox.setText(featurePointType.getName()); - checkBox.addActionListener(createListener(action, ACTION_COMMAND_FEATURE_POINT_HIGHLIGHT, i)); final int index = i; checkBox.addMouseListener(new MouseAdapter() { @Override @@ -127,22 +136,58 @@ public class DistancePanel extends ControlPanel { index)); } }); + featurePointCheckBoxes.add(checkBox); final JTextField sliderInput = fpBuilder.addSliderOptionLine(null, null, 100, null); sliderInput.setText(ControlPanelBuilder.doubleToStringLocale(DrawableFeaturePoints.DEFAULT_SIZE)); sliderInput.postActionEvent(); // Set correct position of slider - sliderInput.addActionListener(new AbstractAction() { - private final ActionListener listener = createListener(action, ACTION_COMMAND_DISTANCE_RECOMPUTE); + sliderInput.addActionListener(createListener(action, ACTION_COMMAND_FEATURE_POINT_RESIZE, i)); + // Modify listener of the ENTER key press + final Object enterKeyAction = sliderInput.getInputMap() + .get(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)); + final Action originalEnterAction = sliderInput.getActionMap() + .get(enterKeyAction); + sliderInput.getActionMap().put( + enterKeyAction, + new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + originalEnterAction.actionPerformed(e); + if (!checkBox.isSelected()) { + return; // Recompute only if the feature point is selected + } + + action.actionPerformed(new ActionEvent( + e.getSource(), + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_DISTANCE_RECOMPUTE + )); + } + } + ); + // Add mouse listener of the slider + sliderInput.addMouseListener(new MouseAdapter() { + private double oldValue = DrawableFeaturePoints.DEFAULT_SIZE; + + @Override + public void mousePressed(MouseEvent e) { + oldValue = ControlPanelBuilder.parseLocaleDouble(sliderInput); + } @Override - public void actionPerformed(ActionEvent ae) { - if (!checkBox.isSelected()) { - return; + public void mouseReleased(MouseEvent e) { + if (!checkBox.isSelected() + || oldValue == ControlPanelBuilder.parseLocaleDouble(sliderInput)) { + return; // Recompute only if the feature point is selected and value changed } - listener.actionPerformed(ae); // Recompute only if the feature point is selected + + action.actionPerformed(new ActionEvent( + e.getSource(), + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_DISTANCE_RECOMPUTE) + ); } }); - sliderInput.addActionListener(createListener(action, ACTION_COMMAND_FEATURE_POINT_RESIZE, i)); fpBuilder.addGap(); featurePointStats.put(featurePointType, fpBuilder.addLabelLine(null)); @@ -154,6 +199,24 @@ public class DistancePanel extends ControlPanel { .setBorder(BorderFactory.createTitledBorder("Feature points")); builder.addLine(); + builder.addButton("Select all", (ActionEvent ae) -> { + selectAllFeaturePoints(true); + action.actionPerformed(new ActionEvent( + ae.getSource(), + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_FEATURE_POINT_SELECT_ALL) + ); + }); + builder.addButton("Deselect all", (ActionEvent ae) -> { + selectAllFeaturePoints(false); + action.actionPerformed(new ActionEvent( + ae.getSource(), + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_FEATURE_POINT_SELECT_NONE) + ); + }); + builder.addLine(); + builder.addCaptionLine("Visualization options:"); builder.addLine(); builder.addCheckBoxOptionLine( @@ -163,20 +226,17 @@ public class DistancePanel extends ControlPanel { createListener(action, ACTION_COMMAND_SHOW_HIDE_WEIGHTED_FPOINTS) ); builder.addLine(); - builder.addCheckBoxOptionLine( - null, - "Show Hausdorff distance", - false, - createListener(action, ACTION_COMMAND_SHOW_HIDE_HEATMAP) - ); - builder.addLine(); - builder.addCheckBoxOptionLine( - null, - "Weighted Hausdorff distance", - false, - createListener(action, ACTION_COMMAND_WEIGHTED_DISTANCE) + builder.addOptionText("Heatmap"); + builder.addComboBox( + List.of( + HEATMAP_HAUSDORFF_DISTANCE, + HEATMAP_WEIGHTED_HAUSDORFF_DISTANCE, + HEATMAP_HIDE + ), + createListener(action, ACTION_COMMAND_SET_DISPLAYED_HEATMAP) ); + builder.addGap(); builder.addLine(); builder.addCaptionLine("Hausdorff distance:"); @@ -223,6 +283,18 @@ public class DistancePanel extends ControlPanel { return new ImageIcon(SymmetryPanel.class.getClassLoader().getResource("/" + ICON)); } + /** + * (De)selects all feature points for the computation of the weighted Hausdorff distance. + * + * @param selected {@code true} if all feature point checkboxes are to be selected, + * {@code false} otherwise + */ + private void selectAllFeaturePoints(boolean selected) { + featurePointCheckBoxes.forEach(fpCheckBox -> + fpCheckBox.setSelected(selected) + ); + } + /** * Updates GUI elements that display statistical data about the calculated Hausdorff distance. *