From f08020113f243e14d18f25e63651d8ee66b13270 Mon Sep 17 00:00:00 2001 From: Ullallulloo Date: Fri, 20 Apr 2018 10:18:57 -0400 Subject: [PATCH] Initial commit --- .gitignore | 37 + .project | 11 + pom.xml | 64 ++ .../name/matthewminer/fingerprinter/App.java | 936 ++++++++++++++++++ src/main/resources/images/icons/icon_16.png | Bin 0 -> 671 bytes src/main/resources/images/icons/icon_256.png | Bin 0 -> 32641 bytes src/main/resources/images/icons/icon_48.png | Bin 0 -> 4033 bytes 7 files changed, 1048 insertions(+) create mode 100644 .gitignore create mode 100644 .project create mode 100644 pom.xml create mode 100644 src/main/java/name/matthewminer/fingerprinter/App.java create mode 100644 src/main/resources/images/icons/icon_16.png create mode 100644 src/main/resources/images/icons/icon_256.png create mode 100644 src/main/resources/images/icons/icon_48.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cacb43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# VSCode +.vscode + +# Test +src/test + +# Other stuff +.settings +cache +candidates +share +target +probe.* \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..3b9053c --- /dev/null +++ b/.project @@ -0,0 +1,11 @@ + + + fingerprinter + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f582472 --- /dev/null +++ b/pom.xml @@ -0,0 +1,64 @@ + + 4.0.0 + name.matthewminer.fingerprinter + fingerprinter + jar + 1.0 + Fingerprinter + https://fingerprinter.matthewminer.name + + + junit + junit + 3.8.1 + test + + + com.machinezoo.sourceafis + sourceafis + 3.1.1 + + + org.slf4j + slf4j-api + 1.7.5 + + + org.slf4j + slf4j-log4j12 + 1.7.5 + + + + + + maven-assembly-plugin + + + + name.matthewminer.fingerprinter.App + + + + jar-with-dependencies + + + + + + + ${basedir}/src/main/resources + + **/* + + + + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + diff --git a/src/main/java/name/matthewminer/fingerprinter/App.java b/src/main/java/name/matthewminer/fingerprinter/App.java new file mode 100644 index 0000000..42ece8f --- /dev/null +++ b/src/main/java/name/matthewminer/fingerprinter/App.java @@ -0,0 +1,936 @@ +package name.matthewminer.fingerprinter; + +import com.machinezoo.sourceafis.FingerprintMatcher; +import com.machinezoo.sourceafis.FingerprintTemplate; +import java.io.File; +import java.lang.Exception; +import java.lang.Math; +import java.lang.Runnable; +import java.lang.Thread; +import java.nio.charset.Charset; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Scanner; +import java.util.concurrent.LinkedBlockingDeque; +import javafx.animation.AnimationTimer; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ListProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; +import javafx.concurrent.Task; +import javafx.concurrent.WorkerStateEvent; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableRow; +import javafx.scene.control.TableView; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.RowConstraints; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.text.TextAlignment; +import javafx.stage.DirectoryChooser; +import javafx.stage.FileChooser; +import javafx.stage.FileChooser.ExtensionFilter; +import javafx.stage.Stage; +import javafx.util.Callback; +import name.matthewminer.fingerprinter.App.Progress; + +/** + * @author Matthew Miner mminer237@gmail.com + * @version 1.0 + * @since 1.0 + */ +public class App extends Application { + /** The threshold to consider two prints a "match" */ + private static final double THRESHOLD = 40; + /** Whether the program was opened in a GUI window */ + private static boolean inWindow = false; + /** The image of the probe print */ + private static ImageView probeImage; + /** + * The image of the candidate print currently being + * looked at + */ + private static ImageView candidateImage; + /** + * The two panes containing the pictures of the + * prints being viewed + */ + private static List imagePanes = new ArrayList(); + /** The checkbox of whether to cache images */ + private static CheckBox cacheCheck; + /** The checkbox of whether to use TV mode */ + private static final CheckBox tvCheck = new CheckBox("TV Mode (Epilepsy Warning)"); + /** The checkbox of whether to use TV mode slowly */ + private static final CheckBox slowCheck = new CheckBox("Less Fast TV"); + /** The checkbox of whether to use color in TV mode */ + private static CheckBox colorCheck; + /** The big progress bar */ + private static final ProgressBar progressBar = new ProgressBar(0F); + /** The GUI box where all processing is logged */ + private static TextArea logBox; + /** List of fingerprints that have been compared */ + private static final ObservableList resultData = FXCollections.observableArrayList(); + + /** + * Initialize the program when JavaFX is not properly started. + * @param args Command line arguments passed + */ + public static void main(String[] args) { + App.launch(args); + } + + /** + * Start the program. + * @param primaryStage The primary stage from JavaFX + */ + @Override + public void start(final Stage primaryStage) { + /** Command line arguments passed */ + List argList = getParameters().getRaw(); + /** Command line arguments passed */ + String[] args = new String[argList.size()]; + args = argList.toArray(args); + + if (args.length > 0) { + if (args[0].equals("--default") || args[0].equals("-default") || args[0].equals("-d")) { + run(new String[0]); + } + else if (args[0].equals("--help") || args[0].equals("-help") || args[0].equals("help") || args[0].equals("-h") || args[0].equals("?")) { + System.out.println("Usage: java -jar fingerprinter.jar [probe [candidatesFolder]] (--default | --help)"); + System.exit(0); + return; + } + else + run(args); + } + else { + inWindow = true; + primaryStage.setTitle("Fingerprinter"); + primaryStage.getIcons().addAll( + new Image(App.class.getResourceAsStream("/images/icons/icon_16.png")), + new Image(App.class.getResourceAsStream("/images/icons/icon_48.png")), + new Image(App.class.getResourceAsStream("/images/icons/icon_256.png")) + ); + + /** The main layout grid */ + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(10, 10, 10, 10)); + RowConstraints rowConstraints = new RowConstraints(); + rowConstraints.setVgrow(Priority.ALWAYS); + grid.getRowConstraints().addAll(new RowConstraints(), new RowConstraints(), rowConstraints); + + probeImage = new ImageView(); + probeImage.setFitHeight(300); + probeImage.setFitWidth(300); + probeImage.setPreserveRatio(true); + StackPane probeImagePane = new StackPane(); + probeImagePane.setMinSize(340, 340); + probeImagePane.setStyle("-fx-background-color:#AAAAAA"); + probeImagePane.getChildren().add(probeImage); + grid.add(probeImagePane, 0, 0); + imagePanes.add(probeImagePane); + candidateImage = new ImageView(); + candidateImage.setFitHeight(300); + candidateImage.setFitWidth(300); + candidateImage.setPreserveRatio(true); + StackPane candidateImagePane = new StackPane(); + candidateImagePane.setMinSize(340, 340); + candidateImagePane.setStyle("-fx-background-color:#AAAAAA"); + candidateImagePane.getChildren().add(candidateImage); + grid.add(candidateImagePane, 2, 0); + imagePanes.add(candidateImagePane); + + /** The table that shows results */ + TableView resultTable = new TableView(); + resultTable.setRowFactory(tv -> { + TableRow row = new TableRow() { + @Override + public void updateItem(Fingerprint item, boolean empty) { + super.updateItem(item, empty); + if (item == null || empty) { + setStyle(""); + } + else { + setStyle("-fx-background-color: hsb(" + String.valueOf(Math.min(120, getItem().getScore() * 120 / THRESHOLD)) + ",41%,94%);"); + } + }; + }; + return row; + }); + + /** The match score column in the table */ + TableColumn scoreCol = new TableColumn("Match Score"); + scoreCol.setPrefWidth(90); + scoreCol.setCellValueFactory( + cellData->cellData.getValue().scoreProperty().asObject() + ); + scoreCol.setSortType(TableColumn.SortType.DESCENDING); + /** The name column in the table */ + TableColumn nameCol = new TableColumn("Name"); + nameCol.setPrefWidth(175); + nameCol.setCellValueFactory( + cellData->cellData.getValue().nameProperty() + ); + /** List of fingerprints that have been compared and sorted */ + SortedList sortedResultData = new SortedList(resultData); + sortedResultData.comparatorProperty().bind(resultTable.comparatorProperty()); + resultTable.setItems(sortedResultData); + resultTable.getSortOrder().addAll(scoreCol); + resultTable.getColumns().addAll(scoreCol, nameCol); + resultTable.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() { + public void changed(ObservableValue observable, + Fingerprint oldValue, + Fingerprint newValue + ) { + if (newValue != null) + Fingerprint.setAsImage(newValue, candidateImage); + } + }); + grid.add(resultTable, 3, 0, 1, 4); + + /** The HBox containing the elements to select a probe */ + HBox probeChooserPane = new HBox(); + probeChooserPane.setPrefWidth(300.0); + grid.add(probeChooserPane, 0, 1); + /** The TextField containing the path of the probe */ + final TextField probeBox = new TextField(); + probeBox.setPrefWidth(220.0); + probeChooserPane.getChildren().add(probeBox); + /** The Button to select a probe */ + Button probeButton = new Button(); + probeButton.setPrefWidth(120.0); + probeButton.setText("Choose Probe"); + probeButton.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent event) { + FileChooser probeChooser = new FileChooser(); + probeChooser.setTitle("Select Probe Print Image"); + probeChooser.getExtensionFilters().addAll( + new ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg", "*.gif"), + new ExtensionFilter("All Files", "*.*") + ); + if ( + !probeBox.getText().isEmpty() && + Paths.get(probeBox.getText()).getParent() != null + ) + probeChooser.setInitialDirectory(Paths.get(probeBox.getText()).getParent().toFile()); + else + probeChooser.setInitialDirectory(Paths.get(".").toFile()); + try { + File selectedFile = probeChooser.showOpenDialog(primaryStage); + if (selectedFile != null) { + Path selectedPath = selectedFile.toPath(); + probeBox.setText(selectedPath.normalize().toString()); + } + } catch(Exception e) { + System.out.println(e.getMessage()); + e.printStackTrace(); + } + } + }); + probeChooserPane.getChildren().add(probeButton); + + /** The HBox containing the elements to select candidates */ + HBox candidatesChooserPane = new HBox(); + candidatesChooserPane.setPrefWidth(300.0); + grid.add(candidatesChooserPane, 2, 1); + /** The TextField containing the path of candidates */ + final TextField candidatesBox = new TextField(); + candidatesBox.setPrefWidth(220.0); + candidatesChooserPane.getChildren().add(candidatesBox); + /** The Button to select candidates */ + Button candidatesButton = new Button(); + candidatesButton.setPrefWidth(120.0); + candidatesButton.setText("Choose Candidates"); + candidatesButton.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent event) { + DirectoryChooser candidatesChooser = new DirectoryChooser(); + candidatesChooser.setTitle("Select Candidate Prints Folder"); + if ( + !candidatesBox.getText().isEmpty() && + Files.isDirectory(Paths.get(candidatesBox.getText())) + ) + candidatesChooser.setInitialDirectory(Paths.get(candidatesBox.getText()).toFile()); + else + candidatesChooser.setInitialDirectory(Paths.get(".").toFile()); + try { + File selectedFile = candidatesChooser.showDialog(primaryStage); + if (selectedFile != null) { + Path selectedPath = selectedFile.toPath(); + candidatesBox.setText(selectedPath.normalize().toString()); + } + } catch(Exception e) { + System.out.println(e.getMessage()); + e.printStackTrace(); + } + } + }); + candidatesChooserPane.getChildren().add(candidatesButton); + + /** Main button to run the program */ + Button runButton = new Button(); + runButton.setMinWidth(105.0); + runButton.setText("Run Comparison"); + runButton.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent event) { + try { + run(new String[] {probeBox.getText(), candidatesBox.getText()}); + } + catch(Exception e) { + System.out.println(e.getMessage()); + } + } + }); + grid.add(runButton, 1, 1); + + /** The HBox containing the program options */ + HBox options = new HBox(); + options.setSpacing(10); + grid.add(options, 0, 2, 3, 1); + cacheCheck = new CheckBox("Use Print Templates Cache"); + options.getChildren().add(cacheCheck); + options.getChildren().add(tvCheck); + options.getChildren().add(slowCheck); + colorCheck = new CheckBox("Colorful Failure"); + options.getChildren().add(colorCheck); + + logBox = new TextArea(); + logBox.setPrefHeight(20000.0); + logBox.setEditable(false); + logBox.setWrapText(true); + grid.add(logBox, 0, 3, 3, 1); + + progressBar.setPrefWidth(20000.0); + grid.add(progressBar, 0, 4, 3, 1); + + /** Link to author's website */ + Hyperlink credit = new Hyperlink("Made by Matthew Miner"); + credit.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent t) { + getHostServices().showDocument("https://matthewminer.name"); + } + }); + credit.prefWidthProperty().bind(grid.widthProperty()); + credit.setStyle("-fx-alignment: CENTER_RIGHT"); + grid.add(credit, 3, 4); + + Scene primaryScene = new Scene(grid, 1105, 640); + primaryStage.setScene(primaryScene); + primaryStage.show(); + } + } + + /** + * Run the comparison. + * @param args Command line arguments passed + */ + private void run(String[] args) { + /** Stream of images named "probe" if none is specified */ + DirectoryStream probesStream; + /** Path of the probe image file */ + Path probePath; + try { + progressBar.setProgress(-1F); + printMessage("Looking for probe..."); + if (args.length > 0) { + printMessage(String.format("Looking for \"%s\" as probe...", args[0])); + probePath = Paths.get(args[0]); + if (Files.exists(probePath) && !Files.isDirectory(probePath)) + printMessage(String.format("Using \"%s\" as probe...", args[0])); + else + throw new Exception(); + } + else { + printMessage("Looking for \"probe.(png|jpg|jpeg|gif)\" as probe..."); + probesStream = Files.newDirectoryStream(Paths.get("."), "probe.{png,jpg,jpeg,gif}"); + probePath = probesStream.iterator().next(); + if (Files.exists(probePath)) { + printMessage(String.format("Using \"%s\" as probe...", probePath.getFileName().toString())); + } + else + throw new Exception(); + } + } + catch(Exception e) { + printMessage("No probe found."); + if (!inWindow) + System.exit(0); + return; + } + + /** Path of the candidates folder */ + Path candidatesPath; + try { + printMessage("Looking for candidates folder..."); + if (args.length > 1) { + candidatesPath = Paths.get(args[1]); + if (Files.exists(candidatesPath)) + printMessage(String.format("Using \"%s\" as candidates folder...", args[1])); + else + throw new Exception(); + } + else { + candidatesPath = Paths.get("candidates"); + if (Files.exists(candidatesPath)) + printMessage("Using \"candidates\" as candidates folder..."); + else + throw new Exception(); + } + } + catch(Exception e) { + if (!inWindow) + System.exit(0); + printMessage("No candidates folder found."); + return; + } + + try { + printMessage("Loading probe..."); + /** JSON data of the data in the probe image */ + String json = null; + /** Fingerprint to try to load a cached fingerprint into */ + Fingerprint testProbe; + if (inWindow && cacheCheck.isSelected()) { + Path cachePath = Paths.get(probePath.getParent().toString() + "/cache/" + probePath.getFileName().toString() + ".json"); + if (Files.exists(cachePath)) { + try { + json = String.join("", Files.readAllLines( + cachePath, + Charset.forName("UTF-8") + )); + testProbe = new Fingerprint(probePath, json); + printMessage("Loaded probe template cache..."); + } catch(Exception e) { + printMessage(String.format("Failed to load template cache for \"%s\".", probePath)); + } + } + } + /** Fingerprint to search for */ + final Fingerprint probe = new Fingerprint(probePath, json); + printMessage("Loaded probe..."); + Fingerprint.setAsImage(probe, probeImage); + + printMessage("Loading candidates..."); + DirectoryStream candidatesStream = Files.newDirectoryStream(candidatesPath, "*.{png,jpg,jpeg,gif}"); + final Progress loadCandidatesProgress = new Progress(candidatesPath.toFile().list().length); + Task> loadCandidates = new Task>() { + @Override + protected List call() throws Exception { + /** List of fingerprints to compare the probe print against */ + List candidates = new ArrayList(); + /** Whether any cached prints were loaded successfully */ + boolean loadedOne = false; + /** Whether any cached prints failed to load */ + boolean failedOne = false; + for (Path candidatePath : candidatesStream) { + if (inWindow && cacheCheck.isSelected()) { + Path cachePath = Paths.get(candidatesPath.toString() + "/cache/" + candidatePath.getFileName().toString() + ".json"); + if (Files.exists(cachePath)) { + try { + /** JSON data of the data in the probe image */ + String json = String.join("", Files.readAllLines( + cachePath, + Charset.forName("UTF-8") + )); + candidates.add(new Fingerprint(candidatePath, json)); + loadedOne = true; + } + catch(Exception e) { + failedOne = true; + printMessage(String.format("Failed to load template cache for \"%s\".", candidatePath)); + } + } + else { + failedOne = true; + candidates.add(new Fingerprint(candidatePath)); + } + loadCandidatesProgress.increment(); + Platform.runLater(new Runnable() { + @Override + public void run() { + progressBar.setProgress(loadCandidatesProgress.getProgress()); + } + }); + } + else { + candidates.add(new Fingerprint(candidatePath)); + } + } + if (loadedOne) + if (failedOne) + printMessage("Loaded partial print template cache..."); + else { + printMessage("Loaded all candidates from print template cache..."); + } + return candidates; + } + }; + loadCandidates.setOnSucceeded(new EventHandler() { + @Override + public void handle(WorkerStateEvent t) { + printMessage("Comparing fingerprints..."); + /** List of successfully loaded candidates */ + List candidates = loadCandidates.getValue(); + /** Progress of the comparison's run */ + Progress runProgress = new Progress(candidates.size()); + /** Comparison model to run the comparison on */ + final Model model = new Model(probe.template, candidates, runProgress); + /** Timer to use in animating the images */ + final TVAnimation timer = new TVAnimation(model, runProgress); + timer.start(); + model.completedProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { + Platform.runLater(new Runnable() { + @Override + public void run() { + if (newValue) { + timer.stop(); + Fingerprint match = model.getMatch().getScore() >= THRESHOLD ? model.getMatch() : null; + if (match != null) { + printMessage("Match found!"); + printMessage(String.format("Match Name: %s", match.getName())); + printMessage(String.format("Match Score: %f", match.getScore())); + Fingerprint.setAsImage(match, candidateImage); + } + else { + printMessage(String.format("No match found that exceeded threshold of %.2f.", THRESHOLD)); + } + if (inWindow) + Fingerprint.listResults(candidates, resultData); + else { + System.out.print("Would you like to see all results? (Y/N): "); + if (Character.toLowerCase(new Scanner(System.in).nextLine().charAt(0)) == 'y') + Fingerprint.listResults(candidates); + System.exit(0); + } + } + } + }); + } + }); + model.start(); + } + }); + new Thread(loadCandidates).start(); + } catch(Exception e) { + System.out.println(e.getMessage()); + e.printStackTrace(); + if (!inWindow) + System.exit(0); + return; + } + } + + /** Stores and computes the percentage of prints processed */ + public class Progress { + /** Total number of items to process */ + private int total; + /** Number of items already processed */ + private int done; + + /** + * Constructor for Progress + * @param total Total number of items to process + */ + public Progress(int total) { + this(total, 0); + } + /** + * Constructor for Progress + * @param total Total number of items to process + * @param done Number of items already processed + */ + public Progress(int total, int done) { + this.total = total; + this.done = done; + } + + /** + * Get the total number of items + * @return The total number of items + */ + public int getTotal() { + return total; + } + /** + * Get the number of items processed + * @return The number of items processed + */ + public int getDone() { + return done; + } + /** + * Get the percentage of items processed + * @return The percentage of items processed + */ + public double getProgress() { + return (double)done / (double)total; + } + /** + * Increment the number of items processed + * @return The number of items processed + */ + public int increment() { + return ++done; + } + } + + /** Time the TV mode processing */ + private class TVAnimation extends AnimationTimer { + /** + * The processing model to get the results from + * and allow to advance + */ + final private Model model; + /** The Progress object to update */ + final private Progress progress; + /** Default number of frames to wait before switching images */ + final private int framesToSkipStart = 5; + /** Counter of frames to wait before switching images */ + private int framesToSkip = 0; + /** + * Constructor for TVAnimation + * @param model The processing model to get the results from + * and allow to advance + * @param progress The Progress object to update + */ + TVAnimation(Model model, Progress progress) { + this.model = model; + this.progress = progress; + } + + @Override + public void handle(long now) { + if (framesToSkip == 0 || !slowCheck.isSelected()) { + framesToSkip = framesToSkipStart; + Fingerprint result = model.pollResult(); + double p = progress.getProgress(); + progressBar.setProgress(p); + if (result != null) + Fingerprint.setAsImage(result, candidateImage); + } + else + framesToSkip--; + } + } + + /** + * Processing model that does the comparison + * in a separate thread + */ + private class Model extends Thread { + /** Whether the comparison has finished running */ + private BooleanProperty completedProperty = new SimpleBooleanProperty(false); + /** The fingerprint to use as the probe */ + private ObjectProperty probeProperty = new SimpleObjectProperty<>(); + /** The best-matched fingerprint */ + private ObjectProperty matchProperty = new SimpleObjectProperty<>(); + /** The list of candidate prints to compare against */ + private ListProperty candidatesProperty = new SimpleListProperty<>(); + /** + * Double-ended queue to hold the next candidate and + * limit the processing speed if TV mode is enabled + */ + private LinkedBlockingDeque deque = new LinkedBlockingDeque(5); + /** The Progress object to store results in */ + final private Progress progress; + + /** + * Constructor for Model + * @param probe The fingerprint to use as the probe + * @param candidates The list of candidate prints to compare against + * @param progress The Progress object to store results in + */ + public Model(FingerprintTemplate probe, final List candidates, Progress progress) { + probeProperty.set(probe); + candidatesProperty.set(FXCollections.observableArrayList(candidates)); + this.progress = progress; + setDaemon(true); + } + + /** + * Get the BooleanProperty of whether the comparison has finished + * @return The BooleanProperty of whether the comparison has finished + */ + public BooleanProperty completedProperty() { + return completedProperty; + } + + /** + * Get the best-matched fingerprint + * @return The best-matched fingerprint + */ + public Fingerprint getMatch() { + return matchProperty.get(); + } + + @Override + public void run() { + try { + /** The FingerprintMatcher to use for the comparison */ + final FingerprintMatcher matcher = new FingerprintMatcher() + .index(probeProperty.get()); + /** The best-matched fingerprint */ + Fingerprint match = null; + /** The highest score found */ + double high = 0; + + for (Fingerprint candidate : candidatesProperty) { + /** The score of the current matchup */ + double score = matcher.match(candidate.template); + candidate.setScore(score); + if (score > high) { + high = score; + match = candidate; + } + progress.increment(); + if (tvCheck.isSelected()) + try{ + deque.put(candidate); + } catch(Exception e) { + System.out.println(e.getMessage()); + e.printStackTrace(); + } + } + matchProperty.set(match); + } catch(Exception e) { + System.out.println(e.getMessage()); + e.printStackTrace(); + } + completedProperty.set(true); + progress.increment(); + if (tvCheck.isSelected()) + try{ + deque.put(getMatch()); + } catch(Exception e) { + System.out.println(e.getMessage()); + e.printStackTrace(); + } + else + Platform.runLater(new Runnable() { + @Override + public void run() { + progressBar.setProgress(progress.getProgress()); + } + }); + } + + /** + * Take one fingerprint from the result deque + * @return The oldest tested fingerprint in the deque + */ + Fingerprint pollResult() { + return deque.poll(); + } + } + + /** + * Print a message to the console and log box + * @param message The message to print out + */ + private static void printMessage(String message) { + System.out.println(message); + try { + logBox.appendText(message + "\n"); + } catch(Exception e) {} + } + + /** A fingerprint object */ + private static class Fingerprint { + /** The name of the fingerprint */ + private final StringProperty name; + /** The filepath of the fingerprint image */ + private final Path path; + /** The score of the fingerprint versus the probe */ + private DoubleProperty score = new SimpleDoubleProperty(); + /** The template for the fingerprint */ + FingerprintTemplate template; + + /** + * Constructor for Fingerprint without a JSON cache + * @param path The filepath of the fingerprint + */ + public Fingerprint(Path path) { + this(path, null); + } + public Fingerprint(Path path, String jsonTemplate) { + name = new SimpleStringProperty(path.getFileName().toString()); + this.path = path; + if (jsonTemplate != null) { + try { + template = new FingerprintTemplate().deserialize(jsonTemplate); + } + catch(Exception e) { + jsonTemplate = null; + printMessage(String.format("Had a problem reading the cached version of %s. Trying actual image...", getName())); + } + } + else { + try { + /** Binary data of fingerprint image */ + byte[] image = Files.readAllBytes(path); + template = new FingerprintTemplate() + .dpi(500) + .create(image); + } + catch(Exception e) { + printMessage(String.format("Failed to create template for \"%s\".", getName())); + System.out.println(e.getMessage()); + return; + } + } + if (inWindow && cacheCheck.isSelected()) { + try { + /** String of the cache directory path */ + String cache = path.getParent().toString() + "/cache/"; + Files.createDirectories(Paths.get(cache)); + Files.write( + Paths.get(cache + getName() + ".json"), + Arrays.asList(template.serialize()), + Charset.forName("UTF-8") + ); + } + catch(Exception e) { + printMessage(String.format("Failed to save template cache for \"%s\".", getName())); + } + } + } + + /** + * Get the name of the fingerprint + * @return The name of the fingerprint + */ + public String getName() { + return name.get(); + } + + /** + * Get the match score of the fingerprint + * @return The match score of the fingerprint + */ + public double getScore() { + return score.get(); + } + + /** + * Get the name property of the fingerprint + * @return The name property of the fingerprint + */ + public StringProperty nameProperty() { + return name; + } + + /** + * Get the match score property of the fingerprint + * @return The match score property of the fingerprint + */ + public DoubleProperty scoreProperty() { + return score; + } + + /** + * Set the match score of the fingerprint + * @param s Match score of the fingerprint + */ + public void setScore(double s) { + score.setValue(s); + } + + /** + * Print a list of fingerprint match results + * @param candidates The list of fingerprint match results to list + */ + public static void listResults(List candidates) { + Collections.sort(candidates, Comparator.comparingDouble(Fingerprint::getScore)); + Collections.reverse(candidates); + System.out.println("Match Score\tFingerprint Name\n"); + for (Fingerprint candidate : candidates) { + System.out.format("%f\t%s\n", candidate.getScore(), candidate.getName()); + } + } + + /** + * Print a list of fingerprint match results and put them in the result data list + * @param candidates The list of fingerprint match results to list + * @param resultData The ObservableList of result data to fill + */ + public static void listResults(List candidates, ObservableList resultData) { + listResults(candidates); + resultData.clear(); + resultData.addAll(candidates); + } + + /** + * Set a fingerprint as a displayed image + * @param print The fingerprint to display + * @param i The ImageView to set the fingerprint at + */ + public static void setAsImage(Fingerprint print, ImageView i) { + setImage(print.path.toString(), print.getScore(), i); + } + } + + /** + * Set a fingerprint as a displayed image + * @param path The filepath of the image to display + * @param score The match score of the fingerprint to display + * @param i The ImageView to set the fingerprint at + */ + public static void setImage(String path, double score, ImageView i) { + if (inWindow) { + i.setImage(new Image("file:" + path)); + if (colorCheck.isSelected()) + if (score == 0) + imagePanes.forEach(pane -> pane.setStyle("-fx-background-color:#AAAAAA")); + else + imagePanes.forEach(pane -> pane.setStyle("-fx-background-color: hsb(" + String.valueOf(Math.min(120, score * 120 / THRESHOLD)) + ",41%,94%);")); + else + if (score > THRESHOLD) + imagePanes.forEach(pane -> pane.setStyle("-fx-background-color:#8EF290")); + else + imagePanes.forEach(pane -> pane.setStyle("-fx-background-color:#AAAAAA")); + } + } +} \ No newline at end of file diff --git a/src/main/resources/images/icons/icon_16.png b/src/main/resources/images/icons/icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..bdb8308bb2d1c6855fb7b71374e6dee6c92666e2 GIT binary patch literal 671 zcmV;Q0$}}#P)BtiEMn&L`P1cHh!e%0>&e{Y8n)^+=M zPBSp`Ff$BMz`()89?RIiAHJI@^{l~{fAV(f@}vIf=;2VnzI4o7TF$R$e)6O~c@|XM zf$s?&A+#)UUht+2{*{ta+-#qUtrZoF932U+r*|i5dM3v>sCN`Uh`1|ZGv|fkU9%Tg z3n#X%U~MIQ2GqD5JIg9ouF_#aKsSLq1nPs??194OZ2}UFa<&0+ay=a$5!eRcwSWgS zuZ8CB_3eKZ`J(`2Er~R!AjG8r7g_XRQp22`0`aKgxApt7POCar(6g^zLA&E>Sa3pB&oCe*s{z@-@t@5BUH9002ovPDHLk FV1h#SAv^#8 literal 0 HcmV?d00001 diff --git a/src/main/resources/images/icons/icon_256.png b/src/main/resources/images/icons/icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..355058cdf9ebdb752330a3cff7eb8e2c19f0430c GIT binary patch literal 32641 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/images/icons/icon_48.png b/src/main/resources/images/icons/icon_48.png new file mode 100644 index 0000000000000000000000000000000000000000..8774b09f5d9c522c96e616ccf5a9bf2f6c6c78b8 GIT binary patch literal 4033 zcmV;y4?ggTP)2yLuc0x>m$Px+4pzJb; zC>zuV_?zxUj8?!CbObEAHuen|k#KqCOiKhghyzRvfF?|hcO z^PK{K<%P@-*0F>lu_=6N_PVasUHCo-InbnmHY^*C0u%rifXWK1X1eLV^l^sCrm(L+ z+K>K004;zqh5V%3jf}ZiPn&01sI=`3!K%Q3(;NWZQuE_umslQKnr+BGUF)ih3um&FU%3^^~cUA}AFA$dDruDSR>@`M2$IY}!%hugYXecJhjpRf|%% zjGUMpt*h$YJ9E2lqrihtdpzy=7cK0FI|&e^OLX41s^venU>H+vs0s{Di{vHdva%`q zIcs=QofDIzq9bOv`vkX#CK*Oasxnn1KjS9#q<tx{jAVj3OEy4!}?q-~gn|GcA2&n*Oe5WXP!S?Af!k z|KlTq6par2uFh2!rroNabJHk(YykzD2}{BD9nOzFI_W#=m~LDs?{aLe{)J)wlDVcO zEBu1jxz4`&+4gYLK>(cqLf7g%Ff;`y0Hy@iI%RQU*(#c3xC8Dl-`e5a^qwS(rvY39 za2Y_SQ|Rrj_gC6Z`Hr=wDY7&s#*#UjFD}jJi+$T1>$ilYfOTQwL(4Uk-cae;`_ggW zf$adg0fdL+{7?W007?LPOA{Y|*2<;lwuLX2f7tWpUs$mI$5K`9cyOIL;%W+T05a|{EO;PKRW!yP z>o~lt|C9aqCO-OTzG~c^ZlT}#f$h~zpE=eaazwlBI@&-PnZyh;n>=f>Zicx%eCcyr zw9QxTEALNaQ%obcObY-+rO>5Ap073oh+dELLjlkND41g+;(!o)tIr)Ep< zyRQJS0dN~wgC{J8T|LpxP_=h|SDGS+H#3%;Eam9ACMI$BnZSw5je%M(B~wI}YIM4W z(%&^sTUuM`*>m}NAg)wOPyqNeE-O<(sd$gjf7U0uxlF~VJOJp8bnONp7OQU?Ge6;1 zFKTH0ct9q-g8Ss2{w=%S?t1>~#ffE}673z6Z`MwIhX7snaqpUnuU(&nK@y?qhP&@D z)5#x~S)MC>*Z$nO;X;sCfzysuS#mpV)LVaSeE!i;-|gu9y;jWagYS5D%O_1dw0Tb0HEe;=jO^2zC%?cLFzO4 ztiMmu-Kod5RRu!=U;${&jMW4H9NGt&*=uSApmrSTenJ(z5uX)zw3*` zn|s#olpu+dw9_6QubJxE*0;X0F;H7i6SU=*=4EfNV4EaJjq7Z$?v2P{KToP3T9NX? z%@~FXSDQjg6|Lm~z#H>8@NLk`01R42uLA%PLJfds0QCTl*83~#4!XWPOn_wWHb3|X zLn+R0=zgudFKX{lkg5q!r~l<)0F55edFh~Q$1xBDm7++09r69c4FymrNfi$OZlSLa zfGkL&NP=VmC`p;506-E2A&t$puFCk}jk4sGsQ^yybbegj9c{m?B>CJYtbd*?K)A5M z{@OQ!919tlgcbKDJfa2AxUc`S+WuHyH%o9OzfF0u6xXQ2m8K8{fa3@S2LKMy?gS8v z$e{>95R`#7^C3AD0RWODQ&qHjii%R-o2|-!Jy%uG3*h|5o;S(`S%{{vnZM3c74_Jn zZRaaIyQ>J0%sB0o=K-`!klK#>4mJRQnN2FgHO9h~6d(XF1VNAhAc}GbK-e#Oya15S zW$S%{%MSoNqZ@fXcv5j8c&d&f=!9kF->v{~z9-Vze9B*Sk|IdeB6Hai0Oxi(K059e zoIOfXb=!m1-{%2nIq2SbCMHEgDoTB)8D{QA0+53>4hSIvAmA39ZU9JAWShDoExiDc z#H3^a=>4SUtz%&+okH48cSJcsjzD?Eo-Bku~C6u-+y^mUWb=7(laE zbT^l~zBo#NAjhgFy~u#+@QUuXhQP4~0t7oZVex$cE+2O7JRO#TJ|(HXJ%t$w08U$^ z#SQ=(Ryz^bu+_a1fCL~ClVc$OP!c);Kz~!Hp#uOC8Ed99*6Vi0>^6?%(ifN>&;mI7 zjcZ%2E!x(?5sHH4X{+V|xV)=>OPwf-Q3GvWoJd=}0t81}_+l#of}5MLqy&J+D|mbW z02yZ700m^k>-5fD{hw|+-7r#P7STkw6G(@bAkG102rewSq7j5 zK-1RV_YX^ugcNq3oMM;m{4#3eFi7Tn0^*I2df*grgrUa?J z%D@g8zz5*;8Gp^z4fZ!a1mH)!e(%2=zR(JQn5Lhfw%_%|89^4J8d|@2TEct^z`53N z(}jB9(K-^OW^v-fPXOqsbnmGLfJD|h3xLlrdi?-E6SNrst)~;T01%Ob&Y?PgB>(`3 zAYN%+0^mTrcJ~43I_^7g4gf~+V@tiF%YG(M(?Eh!PgT!*X5gK(Tl?Ox2#WrG9c`L3 zMLSD66F70K&R@B)(O>fk0Lj24r~$w$csu~~`Rd}E;zLJ9a1Djsl>p=cr^5gQ#A6}= zpz~bdWV0h?@77Sd>|3-mn9ut*R7a(7Ktt;ud@}Wg5&%sRDRk!ZzQ1j>N87hFhtJmo zsQ%FY=9{1Py>|qFI#)Sn6ach_n_5Va79FWK0zgyjbS19gpzB%>w&Ve5GfOIrxvIkK z6fQ%%*R}OXNDPXj`QkhcrJreK()OJSR9&=1+gd6;dq4M!9y z75Xo%>wfjq8ODVRa#V%60HihTE1w-0s3EB^8~_P`vMt=CoT#0atEWxb3PN>!pZhbL zo;I-~6j`~6T*`t%zNF)%_eg~R!9Vb>db(lZFBT;|`cjUvurw-#17A5euMSB*-QvWu z-!P`=xl-&@h+E(kU=xYsM=rOZaUeA~;N>Y`h zqSQGo$?04|pQFlCzI(lG^}|%q-KQTNaoeTrr9tGn*o4 zPL?H!BNZG^seJ@MnAv1=SPbr~@f|onRDfaaL%(AR#N%N+39{%O^Fy~z(oLU4f*e#< zmLSDQkb@T;kRbz*Wq?4r>x;9cx|vxDQl+p*J9{i_YLX+`{rpSqPre8sHWUZ0_l8D9 ze93dkkb`G~b^RxNhda!yRimek8j>Js5+ntHkRih-xC1AAhnf$2zHTq%Cs;-*audFG zeO#ByWtxmkq8G{uL6U{Abtpz0w5Q_NB~BTRO)k-g_(J(qKwh1 zV(Vb`4NHNDE!ytNRODMCQaHTXzNU78`QdST#;6iyF}SH`O-6(NXdi|!jcydcU|$>n z4FEL&9)JNr5`a_ymONEa)~))vWAl_nnG`{jf)o>JlA(N}+q=H|zmGp={nM=+sbIrW zsOEU!*e|#DymM8E8-2+qAb!s80pJACi};Gt3ZT;&>*=oZRkYXnD|&cJt+jILdV(Mc zj#O|tssiF@yA_x{8)pYHsvMI$@535@^;nvDqKGX@lyf( z5Ks*9{NVy%2higcUG6IHekV_=`4NgNvx?-k1?ma<*E|2XJ4aclGcx8J18u%%lxoa3 zHNFEsIGz4k0E0G|#Y2dL^vRHY=YsVn}`~dm^_&Oskv9|Cf?-+GSW~w5yfF$TsfA4v_(aNUNNnC0k zB~wzl`^!D}hf@3J0vI$2-v+(k5wnv)(I3lKjmynY<`spcz=6*l8>&SJ2kQNYKkSZn z_Tyg+0Qj~n@dJo;M4D-ipbqjm*S1 n$`}B~ffhtOe+2O(dEoy5MZcO+)d4?$00000NkvXXu0mjf?vkKv literal 0 HcmV?d00001