package cz.fidentis.analyst.distance; import com.jogamp.opengl.GL2; import cz.fidentis.analyst.Logger; 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; 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.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; import javax.vecmath.Point3d; /** * Action listener for computation of the Hausdorff distance. * * @author Radek Oslejsek * @author Daniel Schramm */ public class DistanceAction extends ControlPanelAction { /* * Attributes handling the state */ private HausdorffDistancePrioritized visitor = null; private final Map<FeaturePointType, Double> featurePointTypes; private String strategy = DistancePanel.STRATEGY_POINT_TO_POINT; private boolean relativeDist = false; private String heatmapDisplayed = DistancePanel.HEATMAP_HAUSDORFF_DISTANCE; private boolean weightedFPsShow = true; private FeaturePointType hoveredFeaturePoint = null; private final DistancePanel controlPanel; private final DrawableFeaturePoints weightedFeaturePoints; private static final Color WEIGHTED_FEATURE_POINT_DEFAULT_COLOR = Color.WHITE; private static final Color FEATURE_POINT_HIGHLIGHT_COLOR = Color.MAGENTA; private static final Color FEATURE_POINT_HOVER_COLOR = Color.CYAN; /** * Constructor. * * @param canvas OpenGL canvas * @param topControlPanel Top component for placing control panels */ public DistanceAction(Canvas canvas, JTabbedPane topControlPanel) { super(canvas, topControlPanel); // Calculate weighted Hausdorff distance for all feature points of the secondary face calculateHausdorffDistance( getSecondaryFeaturePoints().getFeaturePoints() .stream() .collect(Collectors.toMap( FeaturePoint::getFeaturePointType, featurePoint -> DrawableFeaturePoints.DEFAULT_SIZE)) ); this.featurePointTypes = visitor.getFeaturePointWeights() .get(getSecondaryDrawableFace().getHumanFace()) // Get FP weights for the secondary face .entrySet() .stream() .map(fpWeights -> Map.entry( fpWeights.getKey(), // For each FP type at the secondary face... fpWeights.getValue() // ... compute average FP weight over all its facets .values() .stream() .mapToDouble(Double::doubleValue) .average() .orElse(Double.NaN))) .filter(fpWeight -> !Double.isNaN(fpWeight.getValue())) // Filter out feature points with Double.NaN weight .collect(Collectors.toMap( Map.Entry::getKey, fpType -> DrawableFeaturePoints.DEFAULT_SIZE)); this.controlPanel = new DistancePanel(this, getSecondaryFeaturePoints().getFeaturePoints(), featurePointTypes.keySet()); this.visitor = null; // Add weighted feature points to the scene this.weightedFeaturePoints = new DrawableFeaturePoints(getSecondaryFeaturePoints().getFeaturePoints()); weightedFeaturePoints.setRenderMode(GL2.GL_LINE); weightedFeaturePoints.setColor(WEIGHTED_FEATURE_POINT_DEFAULT_COLOR); getCanvas().getScene().addOtherDrawable(weightedFeaturePoints); // Place control panel to the topControlPanel topControlPanel.addTab(controlPanel.getName(), controlPanel.getIcon(), controlPanel); topControlPanel.addChangeListener(e -> { // If the distance panel is focused... if (((JTabbedPane) e.getSource()).getSelectedComponent() instanceof DistancePanel) { // ... display heatmap and feature points relevant to the Hausdorff distance getCanvas().getScene().setDefaultColors(); weightedFeaturePoints.resetAllColorsToDefault(); if (weightedFPsShow) { weightedFeaturePoints.show(); } boolean secondaryFaceMoved = false; final List<FeaturePoint> secondaryFPs = getSecondaryFeaturePoints().getFeaturePoints(); final List<FeaturePoint> weightedFPs = weightedFeaturePoints.getFeaturePoints(); for (int i = 0; i < secondaryFPs.size(); i++) { final FeaturePoint faceFP = secondaryFPs.get(i); final FeaturePoint weightedFP = weightedFPs.get(i); // Highlight feature point if selected if (featurePointTypes.containsKey(faceFP.getFeaturePointType())) { colorSecondaryFaceFeaturePoint(i, FEATURE_POINT_HIGHLIGHT_COLOR); } if (!faceFP.getPosition().equals(weightedFP.getPosition())) { secondaryFaceMoved = true; } } // If the position of secondary face has been changed if (secondaryFaceMoved) { visitor = null; // recompute Hausdorff distance // Relocate weighted feature points for (int i = 0; i < secondaryFPs.size(); i++) { final Point3d faceFpPos = secondaryFPs.get(i).getPosition(); final Point3d weightedFpPos = weightedFPs.get(i).getPosition(); weightedFpPos.x = faceFpPos.x; weightedFpPos.y = faceFpPos.y; weightedFpPos.z = faceFpPos.z; } } updateHausdorffDistanceInformation(); getSecondaryDrawableFace().setRenderHeatmap(isHeatmapDisplayed()); } else { getSecondaryDrawableFace().setRenderHeatmap(false); weightedFeaturePoints.hide(); getSecondaryDrawableFace().clearHeatMapSaturation(); } }); topControlPanel.setSelectedComponent(controlPanel); // Focus Hausdorff distance panel } @Override public void actionPerformed(ActionEvent ae) { final String action = ae.getActionCommand(); switch (action) { 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(); if (weightedFPsShow) { weightedFeaturePoints.show(); } else { weightedFeaturePoints.hide(); } break; case DistancePanel.ACTION_COMMAND_SET_DISTANCE_STRATEGY: strategy = (String) ((JComboBox) ae.getSource()).getSelectedItem(); recompute(); break; case DistancePanel.ACTION_COMMAND_RELATIVE_ABSOLUTE_DIST: this.relativeDist = ((JToggleButton) ae.getSource()).isSelected(); 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); break; case DistancePanel.ACTION_COMMAND_FEATURE_POINT_HOVER_OUT: hoverFeaturePoint((LoadedActionEvent) ae, false); break; case DistancePanel.ACTION_COMMAND_FEATURE_POINT_RESIZE: resizeFeaturePoint((LoadedActionEvent) ae); break; case DistancePanel.ACTION_COMMAND_DISTANCE_RECOMPUTE: recompute(); break; default: // to nothing } 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}. */ private void updateHausdorffDistanceInformation() { if (visitor == null) { calculateHausdorffDistance(featurePointTypes); // Update GUI elements that display the calculated Hausdorff distance metrics setFeaturePointWeigths(); setHausdorffDistanceStatistics(); } switch (heatmapDisplayed) { case DistancePanel.HEATMAP_HAUSDORFF_DISTANCE: getSecondaryDrawableFace().clearHeatMapSaturation(); break; case DistancePanel.HEATMAP_WEIGHTED_HAUSDORFF_DISTANCE: getSecondaryDrawableFace().setHeatMapSaturation( visitor.getMergedPriorities() .get(getSecondaryDrawableFace().getHumanFace()) ); break; case DistancePanel.HEATMAP_HIDE: return; default: throw new UnsupportedOperationException(heatmapDisplayed); } getSecondaryDrawableFace().setHeatMap(visitor.getDistances()); } /** * (Re)calculates the Hausdorff distance. * * @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}. */ private void calculateHausdorffDistance(Map<FeaturePointType, Double> featurePoints) { final Strategy useStrategy; switch (strategy) { case DistancePanel.STRATEGY_POINT_TO_POINT: useStrategy = Strategy.POINT_TO_POINT; break; case DistancePanel.STRATEGY_POINT_TO_TRIANGLE: useStrategy = Strategy.POINT_TO_TRIANGLE_APPROXIMATE; break; default: throw new UnsupportedOperationException(strategy); } Logger out = Logger.measureTime(); this.visitor = new HausdorffDistancePrioritized(getPrimaryDrawableFace().getModel(), featurePoints, useStrategy, relativeDist, true); getSecondaryDrawableFace().getHumanFace().accept(visitor); out.printDuration("Computation of Hausdorff distance for models with " + getPrimaryDrawableFace().getHumanFace().getMeshModel().getNumVertices() + "/" + getSecondaryDrawableFace().getHumanFace().getMeshModel().getNumVertices() + " vertices" ); } /** * Updates the GUI elements of {@link DistancePanel} that display * the weights of feature points used to calculate the weighted Hausdorff distance. */ private void setFeaturePointWeigths() { 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(Double::doubleValue) .average() .orElse(Double.NaN)))); } /** * Updates the GUI elements of {@link DistancePanel} elements that display * statistical data about the calculated Hausdorff distance. */ private void setHausdorffDistanceStatistics() { controlPanel.updateHausdorffDistanceStats( visitor.getDistances() .values() .stream() .flatMap(List::stream) .mapToDouble(Double::doubleValue) .summaryStatistics(), getWeightedDistance() .values() .stream() .flatMap(List::stream) .mapToDouble(Double::doubleValue) .summaryStatistics() ); } /** * Calculates weighted Hausdorff distance of the face. * * @return weighted Hausdorff distance */ private Map<MeshFacet, List<Double>> getWeightedDistance() { final Map<MeshFacet, List<Double>> weightedDistances = new HashMap<>(visitor.getDistances()); final Map<MeshFacet, List<Double>> mergedPriorities = visitor.getMergedPriorities() .get(getSecondaryDrawableFace().getHumanFace()); // Merge the map of distances with the map of priorities 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; } /** * 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 * for the computation of the weighted Hausdorff distance. * 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); if (((JToggleButton) actionEvent.getSource()).isSelected()) { colorSecondaryFaceFeaturePoint(index, FEATURE_POINT_HIGHLIGHT_COLOR); featurePointTypes.put(fpType, weightedFeaturePoints.getSize(index)); } else { resetSecondaryFaceFeaturePointColor(index); featurePointTypes.remove(fpType); } } /** * 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. * 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 * @param entered {@code true} if the cursor entered the feature point, * {@code false} if the cursor left the feature point */ private void hoverFeaturePoint(LoadedActionEvent actionEvent, boolean entered) { final int index = (int) actionEvent.getData(); if (entered) { // entering a feature point colorSecondaryFaceFeaturePoint(index, FEATURE_POINT_HOVER_COLOR); hoveredFeaturePoint = getTypeOfFeaturePoint(index); } else if (featurePointTypes.containsKey(hoveredFeaturePoint)) { // leaving highlighted FP colorSecondaryFaceFeaturePoint(index, FEATURE_POINT_HIGHLIGHT_COLOR); } else { // leaving ordinary FP resetSecondaryFaceFeaturePointColor(index); } } /** * Sets the color of the secondary face's feature point at the given index * and of its weighted representation. * * @param index Index of the feature point * @param color New color of the feature point */ private void colorSecondaryFaceFeaturePoint(int index, Color color) { getSecondaryFeaturePoints().setColor(index, color); weightedFeaturePoints.setColor(index, color); } /** * Resets to default the color of the secondary face's feature point at the given index * and of its weighted representation. * * @param index Index of the feature point */ private void resetSecondaryFaceFeaturePointColor(final int index) { getSecondaryFeaturePoints().resetColorToDefault(index); weightedFeaturePoints.resetColorToDefault(index); } /** * 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 = ControlPanelBuilder.parseLocaleDouble((JTextField) actionEvent.getSource()); weightedFeaturePoints.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; } return featurePoints.get(index) .getFeaturePointType(); } }