diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java index d44897396feb08994f8e7d8c8a20173baacea540..3d9275be94d881059ea81afca54b32e790972b09 100644 --- a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java +++ b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java @@ -116,6 +116,32 @@ public class HumanFace implements Serializable { this(file, true); } + /** + * Creates a human face from existing mesh model. The mesh model is + * stored directly (not copied). + * + * @param model Mesh model + * @param id Canonical path to the OBJ file + * @throws IllegalArgumentException if the {@code model} is {@code null} + */ + public HumanFace(MeshModel model, String id) { + if (model == null) { + throw new IllegalArgumentException("model"); + } + if (id == null || id.isBlank()) { + throw new IllegalArgumentException("id"); + } + this.meshModel = model; + + BoundingBox visitor = new BoundingBox(); + meshModel.compute(visitor); + bbox = visitor.getBoundingBox(); + + this.id = id; + + eventBus = new EventBus(); + } + /** * Returns the triangular mesh model of the human face. * diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceFactory.java b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceFactory.java index 451bffa739e1a18c9487dd6f283a557f5a48f870..176fd5da38171e4ce6c3d54a9e483433bfa7978c 100644 --- a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceFactory.java +++ b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceFactory.java @@ -105,11 +105,13 @@ public class HumanFaceFactory { } /** - * I set to {@code true}, then the dump file of a face is created only once - * and then reused for every recovery (it never overwritten). + * If set to {@code true}, then the dump file of a face is created only once + * and then reused for every recovery (it is never overwritten). * It accelerates the dumping face, but can be used only if the state of * faces never changes between the first dump a consequent recoveries. - * + * If set to {@code false}, then every dump of the face to the disk overwrites + * the file. + * @param use If {@code true}, then this optimization is turned on. */ public void setReuseDumpFile(boolean use) { diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTask.java b/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTask.java index 06ae0a528452f452c2c6b62e0bae6b8e61762c07..d4c063bfd22e0ad663d6e93b8d8ee47c225959a8 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTask.java +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTask.java @@ -22,7 +22,8 @@ public class ApproxHausdorffDistTask extends SimilarityTask { private final Stopwatch hdComputationTime = new Stopwatch("Hausdorff distance time:\t"); private final Stopwatch loadTime = new Stopwatch("Disk access time:\t"); - private HumanFace avgFace; + //private HumanFace avgFace; + private int templateFaceIndex; /** * Constructor. @@ -31,50 +32,64 @@ public class ApproxHausdorffDistTask extends SimilarityTask { * @param controlPanel A control panel with computation parameters. Must not be {@code null} * @param avgFace average face */ - public ApproxHausdorffDistTask(ProgressDialog progressDialog, BatchPanel controlPanel, HumanFace avgFace) { + public ApproxHausdorffDistTask(ProgressDialog progressDialog, BatchPanel controlPanel, int templateFaceIndex) { super(progressDialog, controlPanel); - this.avgFace = avgFace; + this.templateFaceIndex = templateFaceIndex; } @Override protected Void doInBackground() throws Exception { HumanFaceFactory factory = getControlPanel().getHumanFaceFactory(); - List<Path> faces = getControlPanel().getFaces(); - - getControlPanel().getHumanFaceFactory().setReuseDumpFile(true); - factory.setStrategy(HumanFaceFactory.Strategy.LRU); - - double[] dist = new double[faces.size()]; + List<Path> faces = getControlPanel().getFacePaths(); totalTime.start(); + factory.setReuseDumpFile(true); // it's safe because no changes are made to models + factory.setStrategy(HumanFaceFactory.Strategy.MRU); // keep first X faces in the memory + + // We don't need to reaload the initFace periodically for two reasons: + // - It is never dumped from memory to disk because we use MRU + // - Even if dumped, the face keeps in the mempry until we hold the pointer to it + loadTime.start(); + String templateFaceId = factory.loadFace(faces.get(templateFaceIndex).toFile()); + HumanFace templateFace = factory.getFace(templateFaceId); + loadTime.stop(); + + + double[] dist = new double[faces.size()]; + for (int i = 0; i < faces.size(); i++) { if (isCancelled()) { // the user canceled the process return null; } - loadTime.start(); - String faceId = factory.loadFace(faces.get(i).toFile()); - HumanFace face = factory.getFace(faceId); - loadTime.stop(); - - hdComputationTime.start(); - //avgFace.computeKdTree(true); - HausdorffDistance hd = new HausdorffDistance( - avgFace.getMeshModel(), - HausdorffDistance.Strategy.POINT_TO_POINT, - true, // relative - true, // parallel - false // crop - ); - face.getMeshModel().compute(hd, true); - dist[i] = hd.getStats().getAverage(); - hdComputationTime.stop(); + if (i != templateFaceIndex) { + + loadTime.start(); + String faceId = factory.loadFace(faces.get(i).toFile()); + HumanFace face = factory.getFace(faceId); + loadTime.stop(); + + hdComputationTime.start(); + templateFace.computeKdTree(false); + HausdorffDistance hd = new HausdorffDistance( + templateFace.getKdTree(), + HausdorffDistance.Strategy.POINT_TO_POINT, + true, // relative + true, // parallel + true // crop + ); + face.getMeshModel().compute(hd, true); + dist[i] = hd.getStats().getAverage(); + hdComputationTime.stop(); + } // update progress bar int progress = (int) Math.round(100.0 * (i+1) / faces.size()); getProgressDialog().setValue(progress); + + //Logger.print(factory.toString()); } hdComputationTime.start(); diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/BatchAction.java b/GUI/src/main/java/cz/fidentis/analyst/batch/BatchAction.java index 7b7b8fa4f8dba0599f514d4bd53ac9d7fb0fec7d..9d3e078687153bf62ed4615a0a7fbd2503f5f084 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/BatchAction.java +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/BatchAction.java @@ -1,5 +1,6 @@ package cz.fidentis.analyst.batch; +import com.jogamp.opengl.GL2; import cz.fidentis.analyst.Logger; import cz.fidentis.analyst.canvas.Canvas; import cz.fidentis.analyst.core.ControlPanelAction; @@ -7,12 +8,20 @@ import cz.fidentis.analyst.face.HumanFace; import cz.fidentis.analyst.face.events.HumanFaceEvent; import cz.fidentis.analyst.face.events.HumanFaceListener; import cz.fidentis.analyst.core.ProgressDialog; +import cz.fidentis.analyst.core.ProjectTopComp; +import cz.fidentis.analyst.scene.DrawableFace; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.util.Arrays; import javax.swing.JOptionPane; import javax.swing.JTabbedPane; import javax.swing.SwingWorker; +import javax.swing.filechooser.FileNameExtensionFilter; +import org.openide.filesystems.FileChooserBuilder; /** * Action listener for batch registration phase. @@ -22,8 +31,9 @@ import javax.swing.SwingWorker; public class BatchAction extends ControlPanelAction implements HumanFaceListener { private final BatchPanel controlPanel; - private HumanFace avgFace = null; - private int avgFaceIndex = -1; + private int faceSceneSlot = -1; + + private double[][] distances; /** * Constructor. @@ -58,8 +68,14 @@ public class BatchAction extends ControlPanelAction implements HumanFaceListener computeRegistrationAndTemplate(); break; case BatchPanel.ACTION_COMMAND_COMPUTE_SIMILARITY: - computeSimilarity(); - break; + computeSimilarity(); + break; + case BatchPanel.ACTION_COMMAND_SHOW_SELECTED_FACE: + showSelectedIcpFace(); + break; + case BatchPanel.ACTION_COMMAND_EXPORT_SIMILARITY: + exportDistanceResults(); + break; default: } renderScene(); @@ -71,35 +87,56 @@ public class BatchAction extends ControlPanelAction implements HumanFaceListener } private void computeRegistrationAndTemplate() { - ProgressDialog progressBar = new ProgressDialog(controlPanel, "ICP registration and average face computation"); - - if (controlPanel.showIcpPreview() && this.avgFaceIndex != -1) { - getScene().showDrawableFace(avgFaceIndex, false); // hide temporarily + if (controlPanel.getSelectedFaceIndex() == -1) { + JOptionPane.showMessageDialog( + controlPanel, + "No faces in the data set", + null, + JOptionPane.WARNING_MESSAGE); + return; } + ProgressDialog progressBar = new ProgressDialog(controlPanel, "ICP registration and average face computation"); + IcpTask task = new IcpTask( + controlPanel.getSelectedFaceIndex(), progressBar, controlPanel, - controlPanel.showIcpPreview() ? getCanvas() : null); + getCanvas()); - task.addPropertyChangeListener((PropertyChangeEvent evt) -> { // when done ... + // The following code will be executed when the task is done: + task.addPropertyChangeListener((PropertyChangeEvent evt) -> { if ("state".equals(evt.getPropertyName()) && (SwingWorker.StateValue.DONE.equals(evt.getNewValue()))) { - avgFace = task.getAverageFace(); - if (avgFaceIndex == -1) { // first successful computation - avgFaceIndex = getScene().getFreeIndex(); - } - getScene().setDrawableFace(avgFaceIndex, avgFace); // either shows or removes the face - getScene().setFaceAsPrimary(avgFaceIndex); - getScene().showDrawableFace(avgFaceIndex, true); - renderScene(); + HumanFace avgFace = task.getAverageFace(); + getScene().clearScene(); + faceSceneSlot = -1; + getCanvas().getCamera().initLocation(); + controlPanel.addAndSelectAvgFace(avgFace); // show selected face (either the average ot other) } }); + + // Prepare the scene - show the selected face towards which other faces are transformed: + HumanFace selectedFace = controlPanel.getSelectedFace(); + if (controlPanel.showIcpPreview() && selectedFace != null) { + getScene().clearScene(); // scene is recovered automatically - see the event handler above + faceSceneSlot = getScene().getFreeIndex(); + + // Add inital face to the scene: + getScene().setDrawableFace(faceSceneSlot, selectedFace); + getScene().getDrawableFace(faceSceneSlot).setTransparency(0.7f); + getScene().getDrawableFace(faceSceneSlot).setRenderMode(GL2.GL_POINT); + getScene().getDrawableFace(faceSceneSlot).setColor(DrawableFace.SKIN_COLOR_PRIMARY); + + // locate the camera to the best angle: + getCanvas().getCamera().initLocation(); + getCanvas().getCamera().rotate(10, -80); + getCanvas().getCamera().move(40, 20); + } progressBar.runTask(task); } private void computeSimilarity() { - ProgressDialog progressBar = new ProgressDialog(controlPanel, "Similarity computation"); final SimilarityTask task; @@ -108,15 +145,7 @@ public class BatchAction extends ControlPanelAction implements HumanFaceListener task = new CompleteHausdorffDistTask(progressBar, controlPanel); break; case BatchPanel.SIMILARITY_APPROX_HD: - if (avgFace == null) { - JOptionPane.showMessageDialog( - controlPanel, - "Compute average face first", - "No average face", - JOptionPane.WARNING_MESSAGE); - return; - } - task = new ApproxHausdorffDistTask(progressBar, controlPanel, avgFace); + task = new ApproxHausdorffDistTask(progressBar, controlPanel, controlPanel.getSelectedFaceIndex()); break; default: return; @@ -125,12 +154,77 @@ public class BatchAction extends ControlPanelAction implements HumanFaceListener task.addPropertyChangeListener((PropertyChangeEvent evt) -> { // when done ... if ("state".equals(evt.getPropertyName()) && (SwingWorker.StateValue.DONE.equals(evt.getNewValue()))) { double[][] result = task.getSimilarities(); - Logger.print(Arrays.deepToString(result)); - // TO DO ... - } + if (result != null) { + this.distances = result; + } + controlPanel.enableSimilatiryExport(this.distances != null); + } }); progressBar.runTask(task); } + + private void showSelectedIcpFace() { + HumanFace face = controlPanel.getSelectedFace(); + if (face != null) { + if (faceSceneSlot == -1) { + faceSceneSlot = getScene().getFreeIndex(); + } + getScene().setDrawableFace(faceSceneSlot, face); + getScene().setFaceAsPrimary(faceSceneSlot); + getScene().showDrawableFace(faceSceneSlot, true); + } + } + + private void exportDistanceResults() { + if (this.distances == null) { + return; + } + + File file = new FileChooserBuilder(ProjectTopComp.class) + .setTitle("Specify a file to save") + .setDefaultWorkingDirectory(new File(System.getProperty("user.home"))) + .setFileFilter(new FileNameExtensionFilter("CSV files (*.csv)", "csv")) + .showSaveDialog(); + + if (file != null) { + try { + BufferedWriter w = new BufferedWriter(new FileWriter(file)); + w.write("PRI FACE;SEC FACE;Dist from PRI to SEC;Dist from SEC to PRI;lower HD;higher HD"); + w.newLine(); + + for (int i = 0; i < distances.length; i++) { + for (int j = i; j < distances.length; j++) { + if (distances[i][j] != distances[j][i]) { + Logger.print("SIMILARITY ERROR for face " + i + " and " + j); + } + w.write(getShortName(controlPanel.getFacePaths().get(i).toFile()) + ";"); + w.write(getShortName(controlPanel.getFacePaths().get(j).toFile()) + ";"); + w.write(String.format("%.8f", distances[i][j]) + ";"); + w.write(String.format("%.8f", distances[j][i]) + ";"); + if (distances[i][j] > distances[j][i]) { + w.write(String.format("%.8f", distances[i][j]) + ";"); + w.write(String.format("%.8f", distances[j][i]) + ""); + } else { + w.write(String.format("%.8f", distances[j][i]) + ";"); + w.write(String.format("%.8f", distances[i][j]) + ""); + } + w.newLine(); + } + } + + w.close(); + } catch (IOException ex) { + Logger.print(ex.toString()); + } + } + } + + private static String getShortName(File file) throws IOException { + String name = file.toString(); + name = name.substring(name.lastIndexOf(File.separatorChar) + 1, name.length()); + return name; + } + } diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/BatchPanel.form b/GUI/src/main/java/cz/fidentis/analyst/batch/BatchPanel.form index 9c119401977ef0c2e2a2fdbe9b78815a5a0d3d1d..83d97f0b023f0ef2257175281d98ecab4046ef34 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/BatchPanel.form +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/BatchPanel.form @@ -41,7 +41,7 @@ <Component id="jPanel1" min="-2" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> <Component id="jPanel3" min="-2" max="-2" attributes="0"/> - <EmptySpace pref="194" max="32767" attributes="0"/> + <EmptySpace max="32767" attributes="0"/> </Group> </Group> </DimensionLayout> @@ -65,33 +65,24 @@ <Group type="102" attributes="0"> <EmptySpace max="-2" attributes="0"/> <Group type="103" groupAlignment="0" attributes="0"> - <Component id="jSeparator1" alignment="1" max="32767" attributes="0"/> + <Component id="jCheckBox3" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="jCheckBox1" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="jCheckBox2" alignment="0" min="-2" max="-2" attributes="0"/> <Group type="102" alignment="0" attributes="0"> - <Component id="jCheckBox3" min="-2" max="-2" attributes="0"/> - <EmptySpace max="32767" attributes="0"/> <Component id="jButton1" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="jButton5" min="-2" max="-2" attributes="0"/> </Group> <Group type="102" attributes="0"> <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" alignment="0" attributes="0"> - <Group type="103" groupAlignment="0" attributes="0"> - <Component id="jLabel2" min="-2" max="-2" attributes="0"/> - <Component id="jCheckBox4" alignment="0" min="-2" max="-2" attributes="0"/> - </Group> - <EmptySpace max="-2" attributes="0"/> - <Component id="comboSliderInteger1" min="-2" max="-2" attributes="0"/> - </Group> - <Component id="jCheckBox2" alignment="0" min="-2" max="-2" attributes="0"/> - <Group type="102" alignment="0" attributes="0"> - <Component id="jCheckBox1" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="jComboBox1" min="-2" max="-2" attributes="0"/> - </Group> + <Component id="jLabel2" min="-2" max="-2" attributes="0"/> + <Component id="jCheckBox4" alignment="0" min="-2" max="-2" attributes="0"/> </Group> - <EmptySpace min="0" pref="10" max="32767" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="comboSliderInteger1" min="-2" max="-2" attributes="0"/> </Group> </Group> - <EmptySpace max="-2" attributes="0"/> + <EmptySpace pref="22" max="32767" attributes="0"/> </Group> </Group> </DimensionLayout> @@ -107,19 +98,16 @@ </Group> <Component id="comboSliderInteger1" alignment="0" min="-2" max="-2" attributes="0"/> </Group> - <EmptySpace max="-2" attributes="0"/> - <Component id="jSeparator1" min="-2" pref="10" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> + <EmptySpace type="separate" max="-2" attributes="0"/> <Component id="jCheckBox2" min="-2" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> - <Group type="103" groupAlignment="3" attributes="0"> - <Component id="jCheckBox1" alignment="3" min="-2" max="-2" attributes="0"/> - <Component id="jComboBox1" alignment="3" min="-2" max="-2" attributes="0"/> - </Group> + <Component id="jCheckBox1" min="-2" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> + <Component id="jCheckBox3" min="-2" max="-2" attributes="0"/> + <EmptySpace type="separate" max="-2" attributes="0"/> <Group type="103" groupAlignment="3" attributes="0"> - <Component id="jCheckBox3" alignment="3" min="-2" max="-2" attributes="0"/> <Component id="jButton1" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="jButton5" alignment="3" min="-2" max="-2" attributes="0"/> </Group> <EmptySpace max="32767" attributes="0"/> </Group> @@ -127,16 +115,6 @@ </DimensionLayout> </Layout> <SubComponents> - <Component class="javax.swing.JComboBox" name="jComboBox1"> - <Properties> - <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> - <StringArray count="0"/> - </Property> - </Properties> - <AuxValues> - <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<String>"/> - </AuxValues> - </Component> <Component class="javax.swing.JButton" name="jButton1"> <Properties> <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> @@ -174,6 +152,9 @@ <ResourceString bundle="cz/fidentis/analyst/batch/Bundle.properties" key="BatchPanel.jCheckBox2.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> </Property> </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="jCheckBox2ActionPerformed"/> + </Events> </Component> <Component class="javax.swing.JCheckBox" name="jCheckBox3"> <Properties> @@ -190,11 +171,12 @@ </Property> </Properties> </Component> - <Component class="javax.swing.JSeparator" name="jSeparator1"> + <Component class="javax.swing.JButton" name="jButton5"> <Properties> - <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> - <Color blue="74" green="74" red="76" type="rgb"/> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="cz/fidentis/analyst/batch/Bundle.properties" key="BatchPanel.jButton5.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> </Property> + <Property name="enabled" type="boolean" value="false"/> </Properties> </Component> </SubComponents> @@ -219,6 +201,10 @@ <Component id="jButton2" min="-2" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> <Component id="jButton3" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="jLabel1" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="jComboBox1" min="-2" max="-2" attributes="0"/> <EmptySpace max="32767" attributes="0"/> </Group> </Group> @@ -227,11 +213,17 @@ <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" alignment="0" attributes="0"> <EmptySpace max="-2" attributes="0"/> - <Group type="103" groupAlignment="3" attributes="0"> - <Component id="jButton2" alignment="3" min="-2" max="-2" attributes="0"/> - <Component id="jButton3" alignment="3" min="-2" max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="103" alignment="0" groupAlignment="3" attributes="0"> + <Component id="jLabel1" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="jComboBox1" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="jButton2" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="jButton3" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> </Group> - <EmptySpace max="32767" attributes="0"/> + <EmptySpace pref="16" max="32767" attributes="0"/> </Group> </Group> </DimensionLayout> @@ -251,13 +243,33 @@ </Property> </Properties> </Component> + <Component class="javax.swing.JLabel" name="jLabel1"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Ubuntu" size="15" style="1"/> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="cz/fidentis/analyst/batch/Bundle.properties" key="BatchPanel.jLabel1.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + </Properties> + </Component> + <Component class="javax.swing.JComboBox" name="jComboBox1"> + <Properties> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="0"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<String>"/> + </AuxValues> + </Component> </SubComponents> </Container> <Container class="javax.swing.JPanel" name="jPanel3"> <Properties> <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> - <TitledBorder title="Similarity"> + <TitledBorder title="Similarity computation"> <ResourceString PropertyName="titleX" bundle="cz/fidentis/analyst/batch/Bundle.properties" key="BatchPanel.jPanel3.border.title_1" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> <Font PropertyName="font" name="Dialog" size="12" style="1"/> </TitledBorder> @@ -268,22 +280,36 @@ <Layout> <DimensionLayout dim="0"> <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" alignment="0" attributes="0"> - <EmptySpace max="-2" attributes="0"/> - <Component id="jComboBox2" min="-2" max="-2" attributes="0"/> - <EmptySpace max="32767" attributes="0"/> - <Component id="jButton4" min="-2" max="-2" attributes="0"/> + <Group type="102" attributes="0"> <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <Component id="jComboBox2" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="jButtonInfo1" min="-2" max="-2" attributes="0"/> + </Group> + <Group type="102" alignment="0" attributes="0"> + <Component id="jButton4" min="-2" max="-2" attributes="0"/> + <EmptySpace type="unrelated" max="-2" attributes="0"/> + <Component id="jButton6" min="-2" max="-2" attributes="0"/> + </Group> + </Group> + <EmptySpace pref="359" max="32767" attributes="0"/> </Group> </Group> </DimensionLayout> <DimensionLayout dim="1"> <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" alignment="0" attributes="0"> - <EmptySpace max="-2" attributes="0"/> + <EmptySpace min="-2" pref="14" max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" max="-2" attributes="0"> + <Component id="jButtonInfo1" max="32767" attributes="0"/> + <Component id="jComboBox2" max="32767" attributes="0"/> + </Group> + <EmptySpace min="-2" pref="8" max="-2" attributes="0"/> <Group type="103" groupAlignment="3" attributes="0"> - <Component id="jComboBox2" alignment="3" min="-2" max="-2" attributes="0"/> <Component id="jButton4" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="jButton6" alignment="3" min="-2" max="-2" attributes="0"/> </Group> <EmptySpace max="32767" attributes="0"/> </Group> @@ -308,6 +334,34 @@ </Property> </Properties> </Component> + <Component class="javax.swing.JButton" name="jButtonInfo1"> + <Properties> + <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> + <Image iconType="3" name="/info.png"/> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="cz/fidentis/analyst/batch/Bundle.properties" key="BatchPanel.jButtonInfo1.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.EmptyBorderInfo"> + <EmptyBorder/> + </Border> + </Property> + <Property name="borderPainted" type="boolean" value="false"/> + <Property name="focusPainted" type="boolean" value="false"/> + <Property name="focusable" type="boolean" value="false"/> + <Property name="requestFocusEnabled" type="boolean" value="false"/> + <Property name="rolloverEnabled" type="boolean" value="false"/> + </Properties> + </Component> + <Component class="javax.swing.JButton" name="jButton6"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="cz/fidentis/analyst/batch/Bundle.properties" key="BatchPanel.jButton6.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + </Component> </SubComponents> </Container> </SubComponents> diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/BatchPanel.java b/GUI/src/main/java/cz/fidentis/analyst/batch/BatchPanel.java index f7f7cc3fff9682985130eb9617a4e3c67a4aa6a8..0bf9da45bafcb9ada6f5acc4a0cc2e5d1fddf118 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/BatchPanel.java +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/BatchPanel.java @@ -1,17 +1,16 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ package cz.fidentis.analyst.batch; +import cz.fidentis.analyst.Logger; import cz.fidentis.analyst.core.ControlPanel; import cz.fidentis.analyst.core.ProjectTopComp; +import cz.fidentis.analyst.face.HumanFace; import cz.fidentis.analyst.face.HumanFaceFactory; +import cz.fidentis.analyst.mesh.io.MeshObjExporter; import static cz.fidentis.analyst.registration.RegistrationPanel.getStaticIcon; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -19,6 +18,7 @@ import java.util.Collections; import java.util.List; import javax.swing.AbstractAction; import javax.swing.ImageIcon; +import javax.swing.JOptionPane; import javax.swing.filechooser.FileNameExtensionFilter; import org.openide.filesystems.FileChooserBuilder; @@ -29,11 +29,13 @@ import org.openide.filesystems.FileChooserBuilder; */ public class BatchPanel extends ControlPanel { - public static final String SIMILARITY_COMPLETE_HD = "Two-way HD of all pairs"; - public static final String SIMILARITY_APPROX_HD = "Approximate HD via average face"; + public static final String SIMILARITY_COMPLETE_HD = "Two-way HD of all pairs (very slow)"; + public static final String SIMILARITY_APPROX_HD = "Approximate HD using selected face (fast)"; public static final String ACTION_COMMAND_COMPUTE_ICP = "Compute ICP and average face"; public static final String ACTION_COMMAND_COMPUTE_SIMILARITY = "Compute similarity"; + public static final String ACTION_COMMAND_SHOW_SELECTED_FACE = "Show selected face"; + public static final String ACTION_COMMAND_EXPORT_SIMILARITY = "Export similarity"; /* * Mandatory design elements @@ -41,13 +43,14 @@ public class BatchPanel extends ControlPanel { public static final String ICON = "registration28x28.png"; public static final String NAME = "Batch Processing"; - private List<Path> faces = new ArrayList<>(); + private List<Path> paths = new ArrayList<>(); private HumanFaceFactory factory = new HumanFaceFactory(); + private boolean haveAvgFace = false; /** * ICP undersampling. 100 = none */ - private int undersampling = 10; + private int undersampling = 100; /** @@ -60,7 +63,7 @@ public class BatchPanel extends ControlPanel { initComponents(); - jButton2.addActionListener(new AbstractAction() { + jButton2.addActionListener(new AbstractAction() { // [Add faces] @Override public void actionPerformed(ActionEvent e) { File[] files = new FileChooserBuilder(ProjectTopComp.class) @@ -72,25 +75,21 @@ public class BatchPanel extends ControlPanel { if (files != null) { for (File file : files) { - faces.add(Paths.get(file.getAbsolutePath())); + paths.add(Paths.get(file.getAbsolutePath())); } } - faces.stream().forEach(f -> { + paths.stream().forEach(f -> { String name = f.toString(); name = name.substring(name.lastIndexOf(File.separatorChar) + 1, name.length()); jComboBox1.addItem(name); - }); - jComboBox1.setSelectedIndex(0); + }); // Action event is triggered automatically by the jComboBox1 with the first item set as selected } }); - jButton3.addActionListener(new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - faces.clear(); - jComboBox1.removeAllItems(); - } + jButton3.addActionListener((ActionEvent e) -> { + paths.clear(); + jComboBox1.removeAllItems(); }); jButton1.addActionListener(createListener(action, ACTION_COMMAND_COMPUTE_ICP)); @@ -102,10 +101,22 @@ public class BatchPanel extends ControlPanel { this.undersampling = comboSliderInteger1.getValue(); }); - jComboBox2.addItem(SIMILARITY_COMPLETE_HD); jComboBox2.addItem(SIMILARITY_APPROX_HD); + jComboBox2.addItem(SIMILARITY_COMPLETE_HD); jButton4.addActionListener(createListener(action, ACTION_COMMAND_COMPUTE_SIMILARITY)); + + jButton5.addActionListener((ActionEvent e) -> { // [Export AVG face] + exportAvgFace(); + }); + + jButton6.addActionListener(createListener(action, ACTION_COMMAND_EXPORT_SIMILARITY)); // [Export results] + + jComboBox1.addActionListener(createListener(action, ACTION_COMMAND_SHOW_SELECTED_FACE)); + + jButtonInfo1.addActionListener((ActionEvent e) -> { + showSimilarityInfo(); + }); } @Override @@ -114,19 +125,87 @@ public class BatchPanel extends ControlPanel { } /** - * Returns index of a face that should be used for the average face computation - * @return index of a face that should be used for the average face computation + * Sets the similarity export button + * + * @param on set on or off + */ + public void enableSimilatiryExport(boolean on) { + jButton6.setEnabled(on); + } + + /** + * Returns index of a face that is selected or -1 + * @return index of a face that is selected or -1 + */ + public int getSelectedFaceIndex() { + return (jComboBox1.getItemCount() == 0) ? -1 : jComboBox1.getSelectedIndex(); + } + + /** + * Returns the face select in the combo box + * @return the face select in the combo box + */ + public HumanFace getSelectedFace() { + if (jComboBox1.getItemCount() == 0) { + return null; + } + + //Logger.print("AAA " + paths.get(jComboBox1.getSelectedIndex()).toString()); + String id = factory.loadFace(paths.get(jComboBox1.getSelectedIndex()).toFile()); + return factory.getFace(id); + } + + /** + * Method logic: + * <ul> + * <li>If {@code face} is not {@code null} and the previous AVG face exists, + * then the AVG face is replaced and selected.</li> + * <li>If {@code face} is not {@code null} and no previous AVG face exists, + * then the AVG face is added and selected.</li> + * <li>If {@code face} is {@code null} and the previous AVG face exists, + * then the previous AVG face is preserved and selected.</li> + * <li>If {@code face} is {@code null} and no previous AVG face exists, + * then nothing happens.</li> + * </ul> + * + * @param face new AVG face */ - public int getTemplateFaceIndex() { - return this.jComboBox1.getSelectedIndex(); + public void addAndSelectAvgFace(HumanFace face) { + //this.avgFace = face; + + // Copy existing ites of the combobox and clear it + List<String> items = new ArrayList<>(); + for (int i = 0; i < jComboBox1.getItemCount(); i++) { + items.add(jComboBox1.getItemAt(i)); + } + jComboBox1.removeAllItems(); + + // the logic: + if (face != null && haveAvgFace) { + paths.set(0, new File(face.getPath()).toPath()); // replace path + // do nothing with the combobox + } else if (face != null && !haveAvgFace) { + paths.add(0, new File(face.getPath()).toPath()); + items.add(0, "Average face"); + } else if (face == null && haveAvgFace) { + } else { + // do nothing at all + } + + // Copy items back to the combobox. + // Action event is triggered automatically by the jComboBox1 with the first item set as selected + items.stream().forEach(i -> jComboBox1.addItem(i)); + + haveAvgFace = (face != null || haveAvgFace); + jButton5.setEnabled(haveAvgFace); } /** * Returns paths to faces to be processed * @return paths to faces to be processed */ - public List<Path> getFaces() { - return Collections.unmodifiableList(faces); + public List<Path> getFacePaths() { + return Collections.unmodifiableList(paths); } /** @@ -185,6 +264,43 @@ public class BatchPanel extends ControlPanel { public String getSimilarityStrategy() { return jComboBox2.getSelectedItem().toString(); } + + private void showSimilarityInfo() { + JOptionPane.showMessageDialog( + this, + "<html>" + + "<strong>" + SIMILARITY_COMPLETE_HD + "</strong>: <br/>" + + "Hausdorff distance is computed for all paris in both directions.<br/>" + + "<br/>" + + "<strong>" + SIMILARITY_APPROX_HD + "</strong>: <br/>" + + "First, a relative Hausdorff distance is computed between all faces and the selected face T.<br/>" + + "Then, the distance of each pair A,B is estimated as the difference between A,T and B,T." + + "</html>", + "Distance computation strategies", + JOptionPane.INFORMATION_MESSAGE + ); + } + + private void exportAvgFace() { + if (!haveAvgFace) { + return; + } + + File file = new FileChooserBuilder(ProjectTopComp.class) + .setTitle("Specify a file to save") + .setDefaultWorkingDirectory(new File(System.getProperty("user.home"))) + .setFileFilter(new FileNameExtensionFilter("obj files (*.obj)", "obj")) + .showSaveDialog(); + + if (file != null) { + try { + String id = factory.loadFace(paths.get(0).toFile()); // first face is average + new MeshObjExporter(factory.getFace(id).getMeshModel()).exportModelToObj(file); + } catch (IOException ex) { + Logger.print(ex.toString()); + } + } + } /** * This method is called from within the constructor to initialize the form. @@ -196,7 +312,6 @@ public class BatchPanel extends ControlPanel { private void initComponents() { jPanel1 = new javax.swing.JPanel(); - jComboBox1 = new javax.swing.JComboBox<>(); jButton1 = new javax.swing.JButton(); jLabel2 = new javax.swing.JLabel(); comboSliderInteger1 = new cz.fidentis.analyst.core.ComboSliderInteger(); @@ -204,13 +319,17 @@ public class BatchPanel extends ControlPanel { jCheckBox2 = new javax.swing.JCheckBox(); jCheckBox3 = new javax.swing.JCheckBox(); jCheckBox4 = new javax.swing.JCheckBox(); - jSeparator1 = new javax.swing.JSeparator(); + jButton5 = new javax.swing.JButton(); jPanel2 = new javax.swing.JPanel(); jButton2 = new javax.swing.JButton(); jButton3 = new javax.swing.JButton(); + jLabel1 = new javax.swing.JLabel(); + jComboBox1 = new javax.swing.JComboBox<>(); jPanel3 = new javax.swing.JPanel(); jComboBox2 = new javax.swing.JComboBox<>(); jButton4 = new javax.swing.JButton(); + jButtonInfo1 = new javax.swing.JButton(); + jButton6 = new javax.swing.JButton(); setPreferredSize(new java.awt.Dimension(600, 600)); @@ -235,13 +354,19 @@ public class BatchPanel extends ControlPanel { jCheckBox2.setSelected(true); org.openide.awt.Mnemonics.setLocalizedText(jCheckBox2, org.openide.util.NbBundle.getMessage(BatchPanel.class, "BatchPanel.jCheckBox2.text")); // NOI18N + jCheckBox2.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jCheckBox2ActionPerformed(evt); + } + }); jCheckBox3.setSelected(true); org.openide.awt.Mnemonics.setLocalizedText(jCheckBox3, org.openide.util.NbBundle.getMessage(BatchPanel.class, "BatchPanel.jCheckBox3.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(jCheckBox4, org.openide.util.NbBundle.getMessage(BatchPanel.class, "BatchPanel.jCheckBox4.text")); // NOI18N - jSeparator1.setForeground(new java.awt.Color(118, 116, 116)); + org.openide.awt.Mnemonics.setLocalizedText(jButton5, org.openide.util.NbBundle.getMessage(BatchPanel.class, "BatchPanel.jButton5.text")); // NOI18N + jButton5.setEnabled(false); javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); jPanel1.setLayout(jPanel1Layout); @@ -250,26 +375,20 @@ public class BatchPanel extends ControlPanel { .addGroup(jPanel1Layout.createSequentialGroup() .addContainerGap() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jSeparator1, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(jCheckBox3) + .addComponent(jCheckBox1) + .addComponent(jCheckBox2) .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(jCheckBox3) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(jButton1)) + .addComponent(jButton1) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jButton5)) .addGroup(jPanel1Layout.createSequentialGroup() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jLabel2) - .addComponent(jCheckBox4)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(comboSliderInteger1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(jCheckBox2) - .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(jCheckBox1) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addGap(0, 10, Short.MAX_VALUE))) - .addContainerGap()) + .addComponent(jLabel2) + .addComponent(jCheckBox4)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(comboSliderInteger1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addContainerGap(22, Short.MAX_VALUE)) ); jPanel1Layout.setVerticalGroup( jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -281,18 +400,16 @@ public class BatchPanel extends ControlPanel { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(jCheckBox4)) .addComponent(comboSliderInteger1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGap(18, 18, 18) .addComponent(jCheckBox2) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jCheckBox1) - .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(jCheckBox1) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jCheckBox3) + .addGap(18, 18, 18) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jCheckBox3) - .addComponent(jButton1)) + .addComponent(jButton1) + .addComponent(jButton5)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); @@ -302,6 +419,9 @@ public class BatchPanel extends ControlPanel { org.openide.awt.Mnemonics.setLocalizedText(jButton3, org.openide.util.NbBundle.getMessage(BatchPanel.class, "BatchPanel.jButton3.text")); // NOI18N + jLabel1.setFont(new java.awt.Font("Ubuntu", 1, 15)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(BatchPanel.class, "BatchPanel.jLabel1.text")); // NOI18N + javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2); jPanel2.setLayout(jPanel2Layout); jPanel2Layout.setHorizontalGroup( @@ -311,40 +431,70 @@ public class BatchPanel extends ControlPanel { .addComponent(jButton2) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jButton3) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel1) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); jPanel2Layout.setVerticalGroup( jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel2Layout.createSequentialGroup() .addContainerGap() - .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jButton2) - .addComponent(jButton3)) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel1) + .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jButton2) + .addComponent(jButton3))) + .addContainerGap(16, Short.MAX_VALUE)) ); jPanel3.setBorder(javax.swing.BorderFactory.createTitledBorder(null, org.openide.util.NbBundle.getMessage(BatchPanel.class, "BatchPanel.jPanel3.border.title_1"), javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, new java.awt.Font("Dialog", 1, 12))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(jButton4, org.openide.util.NbBundle.getMessage(BatchPanel.class, "BatchPanel.jButton4.text")); // NOI18N + jButtonInfo1.setIcon(new javax.swing.ImageIcon(getClass().getResource("/info.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(jButtonInfo1, org.openide.util.NbBundle.getMessage(BatchPanel.class, "BatchPanel.jButtonInfo1.text")); // NOI18N + jButtonInfo1.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1)); + jButtonInfo1.setBorderPainted(false); + jButtonInfo1.setFocusPainted(false); + jButtonInfo1.setFocusable(false); + jButtonInfo1.setRequestFocusEnabled(false); + jButtonInfo1.setRolloverEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(jButton6, org.openide.util.NbBundle.getMessage(BatchPanel.class, "BatchPanel.jButton6.text")); // NOI18N + jButton6.setEnabled(false); + javax.swing.GroupLayout jPanel3Layout = new javax.swing.GroupLayout(jPanel3); jPanel3.setLayout(jPanel3Layout); jPanel3Layout.setHorizontalGroup( jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel3Layout.createSequentialGroup() .addContainerGap() - .addComponent(jComboBox2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(jButton4) - .addContainerGap()) + .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel3Layout.createSequentialGroup() + .addComponent(jComboBox2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jButtonInfo1)) + .addGroup(jPanel3Layout.createSequentialGroup() + .addComponent(jButton4) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jButton6))) + .addContainerGap(359, Short.MAX_VALUE)) ); jPanel3Layout.setVerticalGroup( jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel3Layout.createSequentialGroup() - .addContainerGap() + .addGap(14, 14, 14) + .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(jButtonInfo1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jComboBox2)) + .addGap(8, 8, 8) .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jComboBox2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(jButton4)) + .addComponent(jButton4) + .addComponent(jButton6)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); @@ -369,10 +519,14 @@ public class BatchPanel extends ControlPanel { .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jPanel3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(194, Short.MAX_VALUE)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); }// </editor-fold>//GEN-END:initComponents + private void jCheckBox2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jCheckBox2ActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_jCheckBox2ActionPerformed + private void jCheckBox1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jCheckBox1ActionPerformed // TODO add your handling code here: }//GEN-LAST:event_jCheckBox1ActionPerformed @@ -388,16 +542,20 @@ public class BatchPanel extends ControlPanel { private javax.swing.JButton jButton2; private javax.swing.JButton jButton3; private javax.swing.JButton jButton4; + private javax.swing.JButton jButton5; + private javax.swing.JButton jButton6; + private javax.swing.JButton jButtonInfo1; private javax.swing.JCheckBox jCheckBox1; private javax.swing.JCheckBox jCheckBox2; private javax.swing.JCheckBox jCheckBox3; private javax.swing.JCheckBox jCheckBox4; private javax.swing.JComboBox<String> jComboBox1; private javax.swing.JComboBox<String> jComboBox2; + private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel2; private javax.swing.JPanel jPanel1; private javax.swing.JPanel jPanel2; private javax.swing.JPanel jPanel3; - private javax.swing.JSeparator jSeparator1; // End of variables declaration//GEN-END:variables + } diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/CompleteHausdorffDistTask.java b/GUI/src/main/java/cz/fidentis/analyst/batch/CompleteHausdorffDistTask.java index 7ae9cb3c83a1ab9a61ff024555899a3dec3e4970..095c9e2a4d8426720f29563ee2dc5ae7543b04dd 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/CompleteHausdorffDistTask.java +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/CompleteHausdorffDistTask.java @@ -34,9 +34,9 @@ public class CompleteHausdorffDistTask extends SimilarityTask { @Override protected Void doInBackground() throws Exception { HumanFaceFactory factory = getControlPanel().getHumanFaceFactory(); - List<Path> faces = getControlPanel().getFaces(); + List<Path> faces = getControlPanel().getFacePaths(); - getControlPanel().getHumanFaceFactory().setReuseDumpFile(true); + factory.setReuseDumpFile(true); // it's safe because no changes are made to models factory.setStrategy(HumanFaceFactory.Strategy.MRU); totalTime.start(); diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java b/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java index 093e56acb176a89d97c41ef5d0fa38d3caefa472..59a6d71e5bce67a697ea53a2075fd04fde4e52ff 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java @@ -1,6 +1,5 @@ package cz.fidentis.analyst.batch; -import com.jogamp.opengl.GL2; import cz.fidentis.analyst.Logger; import cz.fidentis.analyst.canvas.Canvas; import cz.fidentis.analyst.core.ProgressDialog; @@ -10,8 +9,10 @@ import cz.fidentis.analyst.icp.IcpTransformer; import cz.fidentis.analyst.icp.NoUndersampling; import cz.fidentis.analyst.icp.RandomStrategy; import cz.fidentis.analyst.mesh.core.MeshModel; +import cz.fidentis.analyst.mesh.io.MeshObjExporter; import cz.fidentis.analyst.scene.DrawableFace; import cz.fidentis.analyst.visitors.kdtree.AvgFaceConstructor; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.List; @@ -30,28 +31,37 @@ public class IcpTask extends SwingWorker<MeshModel, HumanFace> { private final ProgressDialog progressDialog; private final BatchPanel controlPanel; private final Canvas canvas; - private int avgFaceSceneIndex = -1; - private int icpFaceSceneIndex = -1; + + private int faceSceneSlot = -1; // scene slot to show the transformed face + + private final int initialFaceIndex; // index of initial face in the list of available faces private final Stopwatch totalTime = new Stopwatch("Total computation time:\t"); private final Stopwatch avgFaceComputationTime = new Stopwatch("AVG face computation time:\t"); private final Stopwatch icpComputationTime = new Stopwatch("ICP registration time:\t"); - private final Stopwatch loadTime = new Stopwatch("File (re-)loading time:\t"); + private final Stopwatch loadTime = new Stopwatch("Disk access time:\t"); private final Stopwatch kdTreeConstructionTime = new Stopwatch("KD trees construction time:\t"); /** * Constructor. * + * @param initialFaceIndex Index to the {@code controlPanel.getFaces()} list of faces * @param progressDialog A window that show the progress of the computation. Must not be {@code null} * @param controlPanel A control panel with computation parameters. Must not be {@code null} - * @param canvas A canvas with 3D scene. If not {@code null}, then the transformed faces are rendered one by one. + * @param canvas A canvas with 3D scene. */ - public IcpTask(ProgressDialog progressDialog, BatchPanel controlPanel, Canvas canvas) { - try { - avgFace = new HumanFace(controlPanel.getFaces().get(controlPanel.getTemplateFaceIndex()).toFile()); - } catch (IOException ex) { - throw new IllegalArgumentException(ex); + public IcpTask(int initialFaceIndex, ProgressDialog progressDialog, BatchPanel controlPanel, Canvas canvas) { + if (initialFaceIndex < 0 || initialFaceIndex >= controlPanel.getFacePaths().size()) { + throw new IllegalArgumentException("initFace"); } + + this.initialFaceIndex = initialFaceIndex; + + //try { + // avgFace = new HumanFace(controlPanel.getFaces().get(controlPanel.getTemplateFaceIndex()).toFile()); + //} catch (IOException ex) { + // throw new IllegalArgumentException(ex); + //} this.progressDialog = progressDialog; this.controlPanel = controlPanel; @@ -61,26 +71,33 @@ public class IcpTask extends SwingWorker<MeshModel, HumanFace> { @Override protected MeshModel doInBackground() throws Exception { HumanFaceFactory factory = controlPanel.getHumanFaceFactory(); - List<Path> faces = controlPanel.getFaces(); + List<Path> faces = controlPanel.getFacePaths(); int undersampling = controlPanel.getIcpUndersampling(); boolean computeICP = controlPanel.computeICP(); boolean computeAvgFace = controlPanel.computeAvgFace(); - controlPanel.getHumanFaceFactory().setReuseDumpFile(true); - factory.setStrategy(HumanFaceFactory.Strategy.LRU); + factory.setReuseDumpFile(false); // we can't use this optimization because changes are made in models + factory.setStrategy(HumanFaceFactory.Strategy.MRU); // keep first X faces in the memory + + // We don't need to reaload the initFace periodically for two reasons: + // - It is never dumped from memory to disk because we use MRU + // - Even if dumped, the face keeps in the mempry until we hold the pointer to it + loadTime.start(); + String initFaceId = factory.loadFace(faces.get(initialFaceIndex).toFile()); + HumanFace initFace = factory.getFace(initFaceId); + loadTime.stop(); totalTime.start(); - - AvgFaceConstructor avgFaceConstructor = new AvgFaceConstructor(avgFace.getMeshModel()); - + AvgFaceConstructor avgFaceConstructor = null; + for (int i = 0; i < faces.size(); i++) { if (isCancelled()) { return null; } - + // Compute AVG template face. Use each tranfromed face only once. Skip the original face - if (i != controlPanel.getTemplateFaceIndex() && (computeICP || computeAvgFace)) { + if (i != initialFaceIndex && (computeICP || computeAvgFace)) { loadTime.start(); String faceId = factory.loadFace(faces.get(i).toFile()); @@ -91,40 +108,57 @@ public class IcpTask extends SwingWorker<MeshModel, HumanFace> { face.computeKdTree(false); kdTreeConstructionTime.stop(); - if (computeICP) {// ICP registration: + if (computeICP) { // ICP registration: icpComputationTime.start(); IcpTransformer icp = new IcpTransformer( - avgFace.getMeshModel(), + initFace.getMeshModel(), 100, controlPanel.scaleIcp(), 0.3, (undersampling == 100) ? new NoUndersampling() : new RandomStrategy(undersampling)); - face.getMeshModel().compute(icp, true); + face.getMeshModel().compute(icp, true); // superimpose face towards the initFace icpComputationTime.stop(); } if (computeAvgFace) { // AVG template face avgFaceComputationTime.start(); + if (avgFaceConstructor == null) { + avgFaceConstructor = new AvgFaceConstructor(initFace.getMeshModel()); + } face.getKdTree().accept(avgFaceConstructor); avgFaceComputationTime.stop(); } + + // Preserving k-d tree could accelerate the recomputation of ICP or average face, + // but slows down the approximative computation of distace where + // only k-d tree of the template face is necessary (or is not used at all) + face.removeKdTree(); - publish(face); + publish(face); // update progress bar and possibly render the transformed face } int progress = (int) Math.round(100.0 * (i + 1.0) / faces.size()); progressDialog.setValue(progress); + + //Logger.print(factory.toString()); } if (computeAvgFace) { - avgFace.setMeshModel(avgFaceConstructor.getAveragedMeshModel()); + File tempFile = File.createTempFile(this.getClass().getSimpleName(), ".obj"); + tempFile.deleteOnExit(); + avgFace = new HumanFace(avgFaceConstructor.getAveragedMeshModel(), tempFile.getCanonicalPath()); + try { + new MeshObjExporter(avgFace.getMeshModel()).exportModelToObj(tempFile); + } catch (IOException ex) { + Logger.print(ex.toString()); + } } totalTime.stop(); printTimeStats(); - return avgFace.getMeshModel(); + return (avgFace == null) ? null : avgFace.getMeshModel(); } @Override @@ -133,41 +167,21 @@ public class IcpTask extends SwingWorker<MeshModel, HumanFace> { if (isCancelled()) { avgFace = null; } - if (canvas != null && avgFaceSceneIndex >= 0) { - canvas.getScene().setDrawableFace(avgFaceSceneIndex, null); // remove from scene - avgFaceSceneIndex = -1; - } - if (canvas != null && icpFaceSceneIndex >= 0) { - canvas.getScene().setDrawableFace(icpFaceSceneIndex, null); // remove from scene - icpFaceSceneIndex = -1; - } - if (canvas != null) { - canvas.getCamera().initLocation(); - } } @Override - protected void process(List<HumanFace> chunks) { + protected void process(List<HumanFace> chunks) { chunks.stream().forEach(f -> { - if (canvas != null) { - if (icpFaceSceneIndex == -1) { // first rendering - avgFaceSceneIndex = canvas.getScene().getFreeIndex(); - - // locate the camera to the best angle: - canvas.getCamera().initLocation(); - canvas.getCamera().rotate(10, -80); - canvas.getCamera().move(40, 20); - - canvas.getScene().setDrawableFace(avgFaceSceneIndex, avgFace); - canvas.getScene().getDrawableFace(avgFaceSceneIndex).setRenderMode(GL2.GL_POINT); - canvas.getScene().getDrawableFace(avgFaceSceneIndex).setTransparency(0.7f); - - icpFaceSceneIndex = canvas.getScene().getFreeIndex(); + if (isCancelled()) { + return; + } + if (controlPanel.showIcpPreview()) { + if (faceSceneSlot == -1) { + faceSceneSlot = canvas.getScene().getFreeIndex(); } - - canvas.getScene().setDrawableFace(icpFaceSceneIndex, f); - canvas.getScene().getDrawableFace(icpFaceSceneIndex).setTransparency(0.5f); - canvas.getScene().getDrawableFace(icpFaceSceneIndex).setColor(DrawableFace.SKIN_COLOR_SECONDARY); + canvas.getScene().setDrawableFace(faceSceneSlot, f); + canvas.getScene().getDrawableFace(faceSceneSlot).setTransparency(0.5f); + canvas.getScene().getDrawableFace(faceSceneSlot).setColor(DrawableFace.SKIN_COLOR_SECONDARY); canvas.renderScene(); } }); diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/SimilarityTask.java b/GUI/src/main/java/cz/fidentis/analyst/batch/SimilarityTask.java index 5a570ccc03e50bd4456c9f0e93d2c7679344f235..d57085fd8aaa9f096fa6c54c519b7510014d601b 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/SimilarityTask.java +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/SimilarityTask.java @@ -24,7 +24,7 @@ public abstract class SimilarityTask extends SwingWorker<Void, Integer> { public SimilarityTask(ProgressDialog progressDialog, BatchPanel controlPanel) { this.progressDialog = progressDialog; this.controlPanel = controlPanel; - int nFaces = getControlPanel().getFaces().size(); + int nFaces = getControlPanel().getFacePaths().size(); similarities = new double[nFaces][nFaces]; } 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 acd044bd21a65e742580e2f395ccede079d9d419..0a319fc039f2d7e80176972c8b1225a3590dab42 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/scene/Scene.java +++ b/GUI/src/main/java/cz/fidentis/analyst/scene/Scene.java @@ -27,6 +27,18 @@ public class Scene { public static final int MAX_FACES_IN_SCENE = 20; + /** + * Removes all objects. + */ + public void clearScene() { + this.drawableFaces.clear(); + this.drawableFeaturePoints.clear(); + this.drawableSymmetryPlanes.clear(); + this.otherDrawables.clear(); + primaryFaceIndex = -1; + secondaryFaceIndex = -1; + } + /** * Finds and returns a first free index for human face and its drawables. * @return a first free index for human face and its drawables. @@ -391,5 +403,8 @@ public class Scene { return this.drawableFaces.size(); } - + @Override + public String toString() { + return this.drawableFaces.toString(); + } } diff --git a/GUI/src/main/resources/cz/fidentis/analyst/batch/Bundle.properties b/GUI/src/main/resources/cz/fidentis/analyst/batch/Bundle.properties index 554d822cff33b9bd9077f58acee30b1d1bb4f175..abf208f3762a1c1e5d01daaaa53791bb262b85b9 100644 --- a/GUI/src/main/resources/cz/fidentis/analyst/batch/Bundle.properties +++ b/GUI/src/main/resources/cz/fidentis/analyst/batch/Bundle.properties @@ -1,16 +1,19 @@ - -BatchPanel.jCheckBox1.text_1=compute average face from -BatchPanel.jLabel2.text=ICP undersampling (100% = none): -BatchPanel.jButton1.text=Compute -BatchPanel.jPanel1.border.title=Registration and average face computation -BatchPanel.jPanel3.border.title_1=Similarity +BatchPanel.jPanel3.border.title_1=Similarity computation BatchPanel.jButton3.text=Clear faces # To change this license header, choose License Headers in Project Properties. # To change this template file, choose Tools | Templates # and open the template in the editor. BatchPanel.jButton2.text=Add faces BatchPanel.jPanel2.border.title_1=Dataset -BatchPanel.jCheckBox2.text=compute ICP transformations BatchPanel.jButton4.text=Compute -BatchPanel.jCheckBox3.text=show ICP preview in 3D +BatchPanel.jPanel1.border.title=Registration and average face computation +BatchPanel.jLabel1.text=Selected face: +BatchPanel.jButton5.text=Export AVG face BatchPanel.jCheckBox4.text=ICP scaling +BatchPanel.jCheckBox3.text=Show ICP transformations in the 3D preview +BatchPanel.jCheckBox2.text=Transform faces towards the selected one using ICP +BatchPanel.jCheckBox1.text_1=Compute an average face from the selected one +BatchPanel.jLabel2.text=ICP undersampling (100% = none): +BatchPanel.jButton1.text=Compute +BatchPanel.jButtonInfo1.text= +BatchPanel.jButton6.text=Export results