Commit dbd3dfe1 authored by Radek Ošlejšek's avatar Radek Ošlejšek
Browse files

Resolve "Refactor batch processing GUI"

parent 988778d7
Loading
Loading
Loading
Loading
+19 −18
Original line number Diff line number Diff line
package cz.fidentis.analyst.gui.task;

import cz.fidentis.analyst.gui.task.batch.BatchAction;
import cz.fidentis.analyst.canvas.Canvas;
import cz.fidentis.analyst.toolbar.SceneToolboxFaceToFace;
import cz.fidentis.analyst.toolbar.SceneToolboxSingleFace;
import cz.fidentis.analyst.gui.task.batch.distance.BatchDistanceAction;
import cz.fidentis.analyst.gui.task.batch.registration.BatchRegistrationAction;
import cz.fidentis.analyst.gui.task.batch.FacesProxyDecorator;
import cz.fidentis.analyst.gui.task.curvature.CurvatureAction;
import cz.fidentis.analyst.gui.task.distance.DistanceAction;
import cz.fidentis.analyst.gui.task.faceinfo.FaceOverviewAction;
import cz.fidentis.analyst.gui.task.featurepoints.FeaturePointsAction;
import cz.fidentis.analyst.gui.task.interactivemask.InteractiveMaskAction;
import cz.fidentis.analyst.gui.task.registration.RegistrationAction;
import cz.fidentis.analyst.rendering.Camera;
import cz.fidentis.analyst.gui.task.symmetry.CuttingPlanesAction;
import cz.fidentis.analyst.gui.task.symmetry.SymmetryAction;
import cz.fidentis.analyst.gui.task.featurepoints.FeaturePointsAction;
import cz.fidentis.analyst.project.FacesProxy;
import cz.fidentis.analyst.gui.task.faceinfo.FaceOverviewAction;
import cz.fidentis.analyst.gui.task.interactivemask.InteractiveMaskAction;
import cz.fidentis.analyst.rendering.Camera;
import cz.fidentis.analyst.rendering.Scene;
import cz.fidentis.analyst.toolbar.SceneToolboxFaceToFace;
import cz.fidentis.analyst.toolbar.SceneToolboxSingleFace;
import org.openide.windows.TopComponent;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.Objects;
import javax.swing.GroupLayout;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.LayoutStyle;
import org.openide.windows.TopComponent;

/**
 * The non-singleton window/tab for the analysis of one, two or many to many faces
@@ -42,9 +42,9 @@ public class TaskWindow extends TopComponent {
    private final TaskControlPane controlPanel;
    private final JScrollPane scrollPane;
    
    private final FacesProxy faces;
    private final transient FacesProxy faces;
    
    private final ActionListener endTaskListener;
    private final transient ActionListener endTaskListener;
    
    private boolean askWhenClosing = true;
    
@@ -102,7 +102,6 @@ public class TaskWindow extends TopComponent {
        );
        
        if (answer == JOptionPane.YES_OPTION && super.canClose()) {
        //if (super.canClose()) {
            // notify project panel about closing me
            endTaskListener.actionPerformed(new ActionEvent(
                    this,
@@ -144,7 +143,7 @@ public class TaskWindow extends TopComponent {
    
    /**
     * Sets camera to canvas
     * @param camera 
     * @param camera Camera
     */
    public void setCamera(Camera camera) {
        this.canvas.setCamera(camera);
@@ -247,7 +246,9 @@ public class TaskWindow extends TopComponent {
                new InteractiveMaskAction(getCanvas(), faces, controlPanel);
                break;
            default: // batch mode
                new BatchAction(canvas, faces, controlPanel);
                FacesProxyDecorator batchFacesProxy = new FacesProxyDecorator(faces);
                new BatchRegistrationAction(canvas, batchFacesProxy, controlPanel);
                new BatchDistanceAction(canvas, batchFacesProxy, controlPanel);
                break;
        }
    }
+239 −0
Original line number Diff line number Diff line
package cz.fidentis.analyst.gui.task.batch;

import cz.fidentis.analyst.face.HumanFace;
import cz.fidentis.analyst.face.HumanFaceFactory;
import cz.fidentis.analyst.project.FacesProxy;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.List;
import java.util.Objects;

/**
 * In contrast to the {@code FacesProxy}, which deals with faces that are the subject of batch processing,
 * this class add handling of the (optionally computed) average face.
 * While the faces can be temporarily dumped, the average face is always in the memory.
 * It is assumed that the average face is added as the first item of a menu (combo box), whenever is computed.
 * This class provides methods for such menu (combo box) management and synchronization.
 *
 * @author Radek Oslejsek
 */
public class FacesProxyDecorator extends FacesProxy {
    private HumanFace avgFace;

    private final FacesProxy facesProxy;

    /**
     * Constructor.
     *
     * @param facesProxy original faces proxy dealing with faces to be processed
     */
    public FacesProxyDecorator(FacesProxy facesProxy) {
        Objects.requireNonNull(facesProxy);
        this.facesProxy = facesProxy;
    }

    public FacesProxy getFacesProxy() {
        return facesProxy;
    }

    /**
     * Returns the average human face or {@code null}
     * @return the average human face or {@code null}
     */
    public HumanFace getAvgFace() {
        return avgFace;
    }

    /**
     * Fill the menu (combo box) with names of faces, including the average face, if provided as input parameter.
     * The average face is stored.
     * An action event is triggered automatically by the {@code cbox} with with the originally selected item or
     * the newly added average face.
     *
     * @param avgFace The average face, can be {@code null}
     * @param selectNewAvgFace If {@code true} and a new average is added, then the average face is automatically selected.
     *                         If the {@code avgFace} is {@code null}, then this parameter has no effect.
     * @param cbox A combo box (menu)
     */
    public void syncSelectionMenu(HumanFace avgFace, boolean selectNewAvgFace, JComboBox<String> cbox) {
        this.avgFace = avgFace;
        syncSelectionMenu(selectNewAvgFace, cbox);
    }

    /**
     * Fill the menu (combo box) with names of faces stored in the face proxy, plus the optional average face.
     * An action event is triggered automatically by the {@code cbox} with the originally selected item or
     * the newly added average face.
     *
     * @param selectNewAvgFace If {@code true} and a new average is added, then the average face is automatically selected.
     * @param cbox A combo box (menu)
     */
    public void syncSelectionMenu(boolean selectNewAvgFace, JComboBox<String> cbox) {
        int expectedNumItems = haveAvgFace() ? facesProxy.getNumFaces() + 1 : facesProxy.getNumFaces();
        int realNumItems = cbox.getItemCount();
        int selectedItem = cbox.getSelectedIndex();

        cbox.removeAllItems();
        if (haveAvgFace()) {
            cbox.addItem("Average face");
        }
        facesProxy.getFaceFiles().forEach(f -> {
            String name = f.toString();
            name = name.substring(name.lastIndexOf(File.separatorChar) + 1);
            cbox.addItem(name);
        });

        // re-select proper item:
        if (realNumItems < expectedNumItems) { // adding a new average face
            if (selectNewAvgFace) {
                cbox.setSelectedIndex(0);
            } else if (selectedItem >= 0) {
                cbox.setSelectedIndex(selectedItem+1);
            }
        } else if (realNumItems > expectedNumItems) { // removing the average face
            cbox.setSelectedIndex((--selectedItem >=0) ? selectedItem : 0);
        } else { // nothing has changed
            if (selectedItem >= 0) {
                cbox.setSelectedIndex(selectedItem);
            }
        }
    }

    /**
     * Returns the face selected in a menu.
     *
     * @param cbox A combo box (menu)
     * @return The face selected in a menu. This is either some face from the dataset, or the average face.
     */
    public HumanFace getSelectedFace(JComboBox<String> cbox) {
        if (cbox.getItemCount() == 0) {
            return null;
        }
        if (isAvgFaceSelected(cbox)) {
            return avgFace;
        }
        int index = haveAvgFace() ? cbox.getSelectedIndex()-1 : cbox.getSelectedIndex();
        String id = facesProxy.getFactory().loadFace(facesProxy.getFaceFiles().get(index));
        return facesProxy.getFactory().getFace(id);
    }

    /**
     * Determines whether the average face has been already computed.
     *
     * @return {@code true} if the average face has been already computed
     */
    public boolean haveAvgFace() {
        return avgFace != null;
    }
    
    /**
     * Returns {@code true} if the item selected in the combo box is the average face
     *
     * @param cbox A combo box (menu)
     * @return {@code true} if the item selected in the combo box is the average face
     */
    public boolean isAvgFaceSelected(JComboBox<String> cbox) {
        return haveAvgFace() && cbox.getSelectedIndex() == 0;
    }


    // ADAPTED PUBLIC METHODS of the FacesProxy class:

    @Override
    public void serialize(ObjectOutputStream out) throws IOException {
        facesProxy.serialize(out);
    }

    @Override
    public List<File> getFaceFiles() {
        return facesProxy.getFaceFiles();
    }

    @Override
    public File getFaceFile(int index) {
        return facesProxy.getFaceFile(index);
    }

    @Override
    public File getFaceFile(String faceName) {
        return facesProxy.getFaceFile(faceName);
    }

    @Override
    public HumanFace getFace(int index) {
        return facesProxy.getFace(index);
    }

    @Override
    public int getFaceIndex(String faceName) {
        return facesProxy.getFaceIndex(faceName);
    }

    @Override
    public int addFace(File file, boolean unique) {
        return facesProxy.addFace(file, unique);
    }

    @Override
    public boolean removeFace(String faceName) {
        return facesProxy.removeFace(faceName);
    }

    @Override
    public boolean isInMemory(int index) {
        return facesProxy.isInMemory(index);
    }

    @Override
    public String getFaceName(int index) {
        return facesProxy.getFaceName(index);
    }

    @Override
    public String getFacePath(int index, FaceFileType type) {
        return facesProxy.getFacePath(index,type);
    }

    @Override
    public String getFaceDir(int index) {
        return facesProxy.getFaceDir(index);
    }

    @Override
    public HumanFace getPrimaryFace() {
        return facesProxy.getPrimaryFace();
    }

    @Override
    public HumanFace getSecondaryFace() {
        return facesProxy.getSecondaryFace();
    }

    @Override
    public int getNumFaces() {
        return facesProxy.getNumFaces();
    }

    @Override
    public String getTaskName() {
        return facesProxy.getTaskName();
    }

    @Override
    public boolean equals(Object obj) {
        return facesProxy.equals(obj);
    }

    @Override
    public int hashCode() {
        return facesProxy.hashCode();
    }

    @Override
    public HumanFaceFactory getFactory() {
        return facesProxy.getFactory();
    }
}
+188 −0
Original line number Diff line number Diff line
package cz.fidentis.analyst.gui.task.batch.distance;

import cz.fidentis.analyst.Logger;
import cz.fidentis.analyst.canvas.Canvas;
import cz.fidentis.analyst.events.HumanFaceEvent;
import cz.fidentis.analyst.events.HumanFaceListener;
import cz.fidentis.analyst.face.HumanFace;
import cz.fidentis.analyst.gui.elements.ProgressDialog;
import cz.fidentis.analyst.gui.project.ProjectWindow;
import cz.fidentis.analyst.gui.task.ControlPanelAction;
import cz.fidentis.analyst.gui.task.batch.FacesProxyDecorator;
import cz.fidentis.analyst.gui.task.batch.distance.NearestNeighborsDistanceTask.DistMeasurement;
import org.openide.filesystems.FileChooserBuilder;

import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
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;

/**
 * Action listener for batch registration phase.
 * 
 * @author Radek Oslejsek
 */
public class BatchDistanceAction extends ControlPanelAction<BatchDistancePanel> implements HumanFaceListener {
    
    private int faceSceneSlot = -1;
    
    private double[][] distances;
    private double[][] deviations;
    
    /**
     * Constructor.
     * A new {@code BatchDistancePanel} is instantiated and added to the {@code topControlPane}
     *
     * @param canvas OpenGL canvas
     * @param batchFacesProxy Shared faces across multiple batch tabs.
     *                        The Distance tab checks for changes in the average face stored in the decorator.
     * @param topControlPane A top component when a new control panel is placed
     */
    public BatchDistanceAction(Canvas canvas, FacesProxyDecorator batchFacesProxy, JTabbedPane topControlPane) {
        super(canvas, batchFacesProxy.getFacesProxy(), topControlPane);
        setControlPanel(new BatchDistancePanel(this, batchFacesProxy));
        setShowHideCode(
                () -> { // SHOW
                    // provide a code that runs when the panel is focused (selected)

                    // The average face might have changed. The showSelectedFace() is invoked automatically
                    getControlPanel().updateDatasetMenu();
                },
                () -> { // HIDE
                    // provide a code that runs when the panel is closed (hidden)
                    hideSelectedFace();
                }
        );
        showSelectedFace();
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        String action = ae.getActionCommand();
         switch (action) {
            case BatchDistancePanel.ACTION_COMMAND_COMPUTE_SIMILARITY:
                computeSimilarity();
                break;
            case BatchDistancePanel.ACTION_COMMAND_SHOW_SELECTED_FACE:
                showSelectedFace();
                break;
            case BatchDistancePanel.ACTION_COMMAND_EXPORT_SIMILARITY:
                exportDistanceResults();
                break;
            default:
         }
        renderScene();
    }

    @Override
    public void acceptEvent(HumanFaceEvent event) {
        // NOTHING TO DO
    }
    
    private void computeSimilarity() {
        ProgressDialog<Void, Integer> progressBar = new ProgressDialog<>(getControlPanel(), "Similarity computation");
        final BatchDistanceTask task;
        
        switch (getControlPanel().getSimilarityStrategy()) {
            case BatchDistancePanel.SIMILARITY_PAIRWISE:
                task = new PairwiseDistanceTask(progressBar, getControlPanel(),false);
                break;
            case BatchDistancePanel.SIMILARITY_PAIRWISE_CROP:
                task = new PairwiseDistanceTask(progressBar, getControlPanel(), true);
                break;
            case BatchDistancePanel.SIMILARITY_AVG_REL_DIST:
                task = new NearestNeighborsDistanceTask(progressBar, getControlPanel(), DistMeasurement.RELATIVE_DISTANCE);
                break;
            case BatchDistancePanel.SIMILARITY_AVG_VEC:
                task = new NearestNeighborsDistanceTask(progressBar, getControlPanel(), DistMeasurement.VECTOR_DISTANCE);
                break;
            case BatchDistancePanel.SIMILARITY_AVG_COMBO:
                task = new NearestNeighborsDistanceTask(progressBar, getControlPanel(), DistMeasurement.COMBINED);
                break;
            case BatchDistancePanel.SIMILARITY_AVG_RAY_CASTING:
                task = new RayCastingDistanceTask(progressBar, getControlPanel());
                break;
            default:
                return;
        }
        
        task.addPropertyChangeListener((PropertyChangeEvent evt) -> { // when done ...
            if ("state".equals(evt.getPropertyName()) && (SwingWorker.StateValue.DONE.equals(evt.getNewValue()))) {
                double[][] result = task.getDistSimilarities();
                if (result != null) {
                    this.distances = result;
                    this.deviations = task.getDistDeviations();
                }
                getControlPanel().enableSimilarityExport(this.distances != null);
           }
        });
        
        progressBar.runTask(task);
    } 

    private void showSelectedFace() {
        HumanFace face = getControlPanel().getSelectedFace();
        if (face != null) {
            if (faceSceneSlot == -1) {
                faceSceneSlot = getScene().getFreeSlotForFace();
            }
            getScene().setDrawableFace(faceSceneSlot, face); 
            getScene().setFaceAsPrimary(faceSceneSlot);
            getScene().showDrawableFace(faceSceneSlot, true);
        }
    }

    private void hideSelectedFace() {
        if (faceSceneSlot >= 0) {
            getScene().showDrawableFace(faceSceneSlot, false);
        }
    }

    private void exportDistanceResults() {
        if (this.distances == null) {
            return;
        }
        
        File file = new FileChooserBuilder(ProjectWindow.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;AVG dist PRI-SEC;Std. deviation PRI-SEC;AVG dist SEC-PRI;Std. deviation SEC-PRI");
                w.newLine();
                
                for (int i = 0; i < distances.length; i++) {
                    for (int j = i; j < distances.length; j++) {
                        if (i == j) {
                            continue;
                        }
                        w.write(getShortName(getControlPanel().getFacesProxyDecorator().getFaceFile(i)) + ";");
                        w.write(getShortName(getControlPanel().getFacesProxyDecorator().getFaceFile(j)) + ";");
                        w.write(String.format("%.8f", distances[i][j]) + ";");
                        w.write(String.format("%.8f", deviations[i][j]) + ";");
                        w.write(String.format("%.8f", distances[j][i]) + ";");
                        w.write(String.format("%.8f", deviations[j][i]) + ";");
                        w.newLine();
                    }
                }
            } catch (IOException ex) {
                Logger.print(ex.toString());
            }
        }
    }
    
    private static String getShortName(File file) {
        String name = file.toString();
        name = name.substring(name.lastIndexOf(File.separatorChar) + 1, name.length());
        return name;
    }

    
}
+238 −0

File added.

Preview size limit exceeded, changes collapsed.

+263 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading