Java tutorial
/* * Copyright 2013 The British Library/SCAPE Project Consortium * Author: William Palmer (William.Palmer@bl.uk) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package uk.bl.dpt.qa.gui; import java.awt.Color; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.net.URL; import java.text.DecimalFormat; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.ResourceBundle; import org.apache.commons.imaging.ImageReadException; import org.apache.commons.imaging.Imaging; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import uk.bl.dpt.qa.DissimilarV2; import uk.bl.dpt.qa.gui.CheckResult.ManualCheck; import javafx.animation.FadeTransition; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Task; import javafx.embed.swing.SwingFXUtils; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.geometry.Bounds; import javafx.scene.Group; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ScrollPane; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.ScrollEvent; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.FileChooser.ExtensionFilter; import javafx.stage.Stage; import javafx.stage.Window; import javafx.util.Duration; /** * Class for DissimilarGUI * @author wpalmer * */ public class DissimilarGUIThread { private static Logger gLogger = LogManager.getLogger(); /** * Initialise class */ public DissimilarGUIThread() { gLogger.trace("Initialising DissimilarGUIThread"); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // generated code (see below for handlers) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @FXML // ResourceBundle that was given to the FXMLLoader private ResourceBundle resources; @FXML // URL location of the FXML file that was given to the FXMLLoader private URL location; @FXML // fx:id="buttonFail" private Button buttonFail; // Value injected by FXMLLoader @FXML // fx:id="buttonNext" private Button buttonNext; // Value injected by FXMLLoader @FXML // fx:id="buttonOK" private Button buttonOK; // Value injected by FXMLLoader @FXML // fx:id="buttonPrevious" private Button buttonPrevious; // Value injected by FXMLLoader @FXML // fx:id="buttonToggleSSIMHeatmap" private Button buttonToggleSSIMHeatmap; // Value injected by FXMLLoader @FXML // fx:id="buttonZoomToFit" private Button buttonZoomToFit; // Value injected by FXMLLoader @FXML // fx:id="checkboxAdvance" private CheckBox checkboxAdvance; // Value injected by FXMLLoader @FXML // fx:id="checkboxGenerateOnLoad" private CheckBox checkboxGenerateOnLoad; // Value injected by FXMLLoader @FXML // fx:id="checkboxPrecacheNext" private CheckBox checkboxPrecacheNext; // Value injected by FXMLLoader @FXML // fx:id="groupLeft" private Group groupLeft; // Value injected by FXMLLoader @FXML // fx:id="groupRight" private Group groupRight; // Value injected by FXMLLoader @FXML // fx:id="imageLeft" private ImageView imageLeft; // Value injected by FXMLLoader @FXML // fx:id="imageLeftGrid" private GridPane imageLeftGrid; // Value injected by FXMLLoader @FXML // fx:id="imageLeftLabel" private Label imageLeftLabel; // Value injected by FXMLLoader @FXML // fx:id="imageRight" private ImageView imageRight; // Value injected by FXMLLoader @FXML // fx:id="imageRightGrid" private GridPane imageRightGrid; // Value injected by FXMLLoader @FXML // fx:id="imageRightLabel" private Label imageRightLabel; // Value injected by FXMLLoader @FXML // fx:id="labelCheckStatus" private Label labelCheckStatus; // Value injected by FXMLLoader @FXML // fx:id="labelImageNumber" private Label labelImageNumber; // Value injected by FXMLLoader @FXML // fx:id="mainPane" private GridPane mainPane; // Value injected by FXMLLoader @FXML // fx:id="menuBar" private MenuBar menuBar; // Value injected by FXMLLoader @FXML // fx:id="menuEdit" private Menu menuEdit; // Value injected by FXMLLoader @FXML // fx:id="menuEditDelete" private MenuItem menuEditDelete; // Value injected by FXMLLoader @FXML // fx:id="menuFile" private Menu menuFile; // Value injected by FXMLLoader @FXML // fx:id="menuFileClose" private MenuItem menuFileClose; // Value injected by FXMLLoader @FXML // fx:id="menuHelp" private Menu menuHelp; // Value injected by FXMLLoader @FXML // fx:id="menuHelpAbout" private MenuItem menuHelpAbout; // Value injected by FXMLLoader @FXML // fx:id="menuOpen" private MenuItem menuOpen; // Value injected by FXMLLoader @FXML // fx:id="menuSave" private MenuItem menuSave; // Value injected by FXMLLoader @FXML // fx:id="progressIndicator" private ProgressIndicator progressIndicator; // Value injected by FXMLLoader @FXML // fx:id="psnrLabel" private Label psnrLabel; // Value injected by FXMLLoader @FXML // fx:id="psnrLabelValue" private Label psnrLabelValue; // Value injected by FXMLLoader @FXML // fx:id="psnrPane" private VBox psnrPane; // Value injected by FXMLLoader @FXML // fx:id="scrollPaneLeft" private ScrollPane scrollPaneLeft; // Value injected by FXMLLoader @FXML // fx:id="scrollPaneRight" private ScrollPane scrollPaneRight; // Value injected by FXMLLoader @FXML // fx:id="ssimLabel" private Label ssimLabel; // Value injected by FXMLLoader @FXML // fx:id="ssimLabelValue" private Label ssimLabelValue; // Value injected by FXMLLoader @FXML // fx:id="ssimMinLabel" private Label ssimMinLabel; // Value injected by FXMLLoader @FXML // fx:id="ssimMinPane" private VBox ssimMinPane; // Value injected by FXMLLoader @FXML // fx:id="ssimMinValue" private Label ssimMinValue; // Value injected by FXMLLoader @FXML // fx:id="ssimPane" private VBox ssimPane; // Value injected by FXMLLoader @FXML // fx:id="ssimVarianceLabel" private Label ssimVarianceLabel; // Value injected by FXMLLoader @FXML // fx:id="ssimVariancePane" private VBox ssimVariancePane; // Value injected by FXMLLoader @FXML // fx:id="ssimVarianceValue" private Label ssimVarianceValue; // Value injected by FXMLLoader @FXML // This method is called by the FXMLLoader when initialization is complete void initialize() { assert buttonFail != null : "fx:id=\"buttonFail\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert buttonNext != null : "fx:id=\"buttonNext\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert buttonOK != null : "fx:id=\"buttonOK\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert buttonPrevious != null : "fx:id=\"buttonPrevious\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert buttonToggleSSIMHeatmap != null : "fx:id=\"buttonToggleSSIMHeatmap\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert buttonZoomToFit != null : "fx:id=\"buttonZoomToFit\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert checkboxAdvance != null : "fx:id=\"checkboxAdvance\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert checkboxGenerateOnLoad != null : "fx:id=\"checkboxGenerateOnLoad\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert checkboxPrecacheNext != null : "fx:id=\"checkboxPrecacheNext\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert groupLeft != null : "fx:id=\"groupLeft\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert groupRight != null : "fx:id=\"groupRight\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert imageLeft != null : "fx:id=\"imageLeft\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert imageLeftGrid != null : "fx:id=\"imageLeftGrid\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert imageLeftLabel != null : "fx:id=\"imageLeftLabel\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert imageRight != null : "fx:id=\"imageRight\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert imageRightGrid != null : "fx:id=\"imageRightGrid\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert imageRightLabel != null : "fx:id=\"imageRightLabel\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert labelCheckStatus != null : "fx:id=\"labelCheckStatus\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert labelImageNumber != null : "fx:id=\"labelImageNumber\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert mainPane != null : "fx:id=\"mainPane\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert menuBar != null : "fx:id=\"menuBar\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert menuEdit != null : "fx:id=\"menuEdit\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert menuEditDelete != null : "fx:id=\"menuEditDelete\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert menuFile != null : "fx:id=\"menuFile\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert menuFileClose != null : "fx:id=\"menuFileClose\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert menuHelp != null : "fx:id=\"menuHelp\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert menuHelpAbout != null : "fx:id=\"menuHelpAbout\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert menuOpen != null : "fx:id=\"menuOpen\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert menuSave != null : "fx:id=\"menuSave\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert progressIndicator != null : "fx:id=\"progressIndicator\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert psnrLabel != null : "fx:id=\"psnrLabel\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert psnrLabelValue != null : "fx:id=\"psnrLabelValue\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert psnrPane != null : "fx:id=\"psnrPane\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert scrollPaneLeft != null : "fx:id=\"scrollPaneLeft\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert scrollPaneRight != null : "fx:id=\"scrollPaneRight\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert ssimLabel != null : "fx:id=\"ssimLabel\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert ssimLabelValue != null : "fx:id=\"ssimLabelValue\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert ssimMinLabel != null : "fx:id=\"ssimMinLabel\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert ssimMinPane != null : "fx:id=\"ssimMinPane\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert ssimMinValue != null : "fx:id=\"ssimMinValue\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert ssimPane != null : "fx:id=\"ssimPane\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert ssimVarianceLabel != null : "fx:id=\"ssimVarianceLabel\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert ssimVariancePane != null : "fx:id=\"ssimVariancePane\" was not injected: check your FXML file 'dissimilar3.fxml'."; assert ssimVarianceValue != null : "fx:id=\"ssimVarianceValue\" was not injected: check your FXML file 'dissimilar3.fxml'."; // Initialize your logic here: all @FXML variables will have been injected //set the max widths of the images here scrollPaneLeft.prefWidthProperty().bind(imageLeftGrid.widthProperty()); scrollPaneLeft.prefHeightProperty().bind(imageLeftGrid.heightProperty()); scrollPaneLeft.maxWidthProperty().bind(imageLeftGrid.widthProperty()); scrollPaneLeft.maxHeightProperty().bind(imageLeftGrid.heightProperty()); scrollPaneRight.prefWidthProperty().bind(imageRightGrid.widthProperty()); scrollPaneRight.prefHeightProperty().bind(imageRightGrid.heightProperty()); scrollPaneRight.maxWidthProperty().bind(imageRightGrid.widthProperty()); scrollPaneRight.maxHeightProperty().bind(imageRightGrid.heightProperty()); scrollPaneLeft.vvalueProperty().bindBidirectional(scrollPaneRight.vvalueProperty()); scrollPaneLeft.hvalueProperty().bindBidirectional(scrollPaneRight.hvalueProperty()); scrollPaneLeft.setPannable(true); scrollPaneRight.setPannable(scrollPaneLeft.isPannable()); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // global variables/constants ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// private boolean gHeatmapEnabled = false; private BufferedImage gHeatmap = null; private Image gHeatmapImage = null; private Image gRightImageSave = null; private boolean gHeatmapGenerated = false; private List<CheckResult> gResults = null; private int gCurrentRecord = 0; private int gPrecached = -1; private boolean gArePrecachingNow = false; private Image gPrecachedLeft = null; private Image gPrecachedRight = null; private boolean gResizeCodeHooked = false; private final int gFadeDelayMS = 500; private final int gThreadSleepMS = 200; /** * Minimum height for window */ public static final int gMinHeight = 700; /** * Minimum width for window */ public static final int gMinWidth = 800; /** * Re-initialise variables */ void init() { gHeatmapEnabled = false; gHeatmap = null; gHeatmapImage = null; gRightImageSave = null; gHeatmapGenerated = false; gResults = null; gCurrentRecord = 0; gPrecached = -1; gArePrecachingNow = false; gPrecachedLeft = null; gPrecachedRight = null; imageLeft.setScaleX(1); imageLeft.setScaleY(1); imageRight.setScaleX(1); imageRight.setScaleY(1); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // handlers ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Handler for MenuItem[fx:id="menuFileClose"] onAction @FXML void close(ActionEvent event) { // handle the event here System.exit(0); } // Handler for Button[fx:id="buttonToggleSSIMHeatmap"] onAction @FXML void toggleSSIMHeatmap(ActionEvent event) { // handle the event here internalToggleHeatmap(!gHeatmapEnabled, true); } // Handler for Button[fx:id="buttonNext"] onAction @FXML void nextPressed(ActionEvent event) { internalDisplayPair(++gCurrentRecord); } // Handler for Button[fx:id="buttonOK"] onAction @FXML void okPressed(ActionEvent event) { gResults.get(gCurrentRecord).setManualCheck(ManualCheck.OK); //System.out.println(CheckResult.csvHeader); gLogger.trace(gResults.get(gCurrentRecord).toCSV()); internalUpdateManualCheckLabel(gCurrentRecord); if (checkboxAdvance.selectedProperty().get()) { internalDisplayPair(++gCurrentRecord); } } // Handler for Button[fx:id="buttonFail"] onAction @FXML void failPressed(ActionEvent event) { gResults.get(gCurrentRecord).setManualCheck(ManualCheck.FAIL); //System.out.println(CheckResult.csvHeader); gLogger.trace(gResults.get(gCurrentRecord).toCSV()); internalUpdateManualCheckLabel(gCurrentRecord); if (checkboxAdvance.selectedProperty().get()) { internalDisplayPair(++gCurrentRecord); } } // Handler for Button[fx:id="buttonPrevious"] onAction @FXML void previousPressed(ActionEvent event) { internalDisplayPair(--gCurrentRecord); } // Handler for MenuItem[fx:id="menuOpen"] onAction @FXML void openFile(ActionEvent event) { FileChooser chooser = new FileChooser(); chooser.setTitle("Choose an input CSV file..."); chooser.getExtensionFilters().add(new ExtensionFilter("CSV file with input pairs", "*.csv")); chooser.setInitialDirectory(new File("f:/eclipse-workspace/dissimilar/")); Window window = mainPane.getScene().getWindow(); File inputFile = chooser.showOpenDialog(window); if (inputFile != null && inputFile.exists()) { internalOpenFile(inputFile); } } // Handler for MenuItem[fx:id="menuSave"] onAction @FXML void saveResults(ActionEvent event) { FileChooser chooser = new FileChooser(); chooser.setTitle("Choose an output CSV file..."); chooser.getExtensionFilters().add(new ExtensionFilter("CSV file", "*.csv")); Window window = mainPane.getScene().getWindow(); File outputFile = chooser.showOpenDialog(window); if (outputFile != null) { //internalSaveFile(outputFile);//overwrite? gLogger.trace("FIXME: file saving"); } else { internalSaveFile(outputFile); } } @FXML void mouseScroll(ScrollEvent event) { event.consume(); final double diff = 0.1; //scroll pane locations final double h = scrollPaneLeft.getHvalue(); final double v = scrollPaneLeft.getVvalue(); final double min = 400; if (event.getDeltaY() > 0 && ((imageLeft.getBoundsInParent().getWidth() < min) || imageLeft.getBoundsInParent().getHeight() < min)) { return; } if (event.getDeltaY() < 0) { imageLeft.setScaleX(imageLeft.getScaleX() + diff); imageLeft.setScaleY(imageLeft.getScaleY() + diff); } else { imageLeft.setScaleX(imageLeft.getScaleX() - diff); imageLeft.setScaleY(imageLeft.getScaleY() - diff); } imageRight.setScaleX(imageLeft.getScaleX()); imageRight.setScaleY(imageLeft.getScaleY()); //just set the same ratio of scroll - this should work out mostly ok scrollPaneLeft.setHvalue(h); scrollPaneLeft.setVvalue(v); } // Handler for Button[fx:id="buttonZoomToFit"] onAction @FXML void zoomToFit(ActionEvent event) { internalImageZoomToFit(); //gLogger.trace("Fullscreen: "+((Stage)mainPane.getScene().getWindow()).isFullScreen()); //hook in to resize code to enable call to zoom to fit if (!gResizeCodeHooked) { internalHookResize(); } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // mostly solely GUI methods ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Reset ImageView size so all of an image is shown */ private void internalImageZoomToFit() { final double scaleX = scrollPaneLeft.getViewportBounds().getWidth() / imageLeft.getImage().getWidth(); final double scaleY = scrollPaneLeft.getViewportBounds().getHeight() / imageLeft.getImage().getHeight(); //System.out.println(scrollPaneLeft.getViewportBounds().getWidth()+" "+imageLeft.getImage().getWidth()); //System.out.println(scaleX+" "+scaleY); if (scaleX < scaleY) { imageLeft.setScaleX(scaleX); imageLeft.setScaleY(scaleX); imageRight.setScaleX(scaleX); imageRight.setScaleY(scaleX); } else { imageLeft.setScaleX(scaleY); imageLeft.setScaleY(scaleY); imageRight.setScaleX(scaleY); imageRight.setScaleY(scaleY); } } /** * Hook the resize of the panel */ private void internalHookResize() { gLogger.trace("Hooking maximise (this code does not work): " + mainPane.getScene().getWindow().getClass().getSimpleName()); //urgh if (mainPane.getScene().getWindow() instanceof Stage) { Stage stage = ((Stage) mainPane.getScene().getWindow()); stage.fullScreenProperty().addListener(new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> pValue, final Boolean pOld, final Boolean pNew) { Platform.runLater(new Runnable() { //@Override public void run() { gLogger.trace("Maximised: " + pOld + " -> " + pNew); internalImageZoomToFit(); } }); } }); gLogger.trace("Hooking resize (this code does not work)"); ChangeListener<Number> listener = new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> arg0, Number arg1, Number arg2) { internalImageZoomToFit(); System.out.println(arg1.intValue() + " -> " + arg2.intValue()); } }; stage.widthProperty().addListener(listener); stage.heightProperty().addListener(listener); //mainPane.widthProperty().addListener(listener); //mainPane.heightProperty().addListener(listener); } gResizeCodeHooked = true; } /** * Convenience method for disabling/enabling GUI - user controls should be added here * @param pDisable true = disable interface, false = enable interface */ private void internalDisableInterface(boolean pDisable) { buttonNext.setDisable(pDisable); buttonPrevious.setDisable(pDisable); buttonOK.setDisable(pDisable); buttonFail.setDisable(pDisable); buttonToggleSSIMHeatmap.setDisable(pDisable); checkboxAdvance.setDisable(pDisable); checkboxGenerateOnLoad.setDisable(pDisable); checkboxPrecacheNext.setDisable(pDisable); imageLeft.setDisable(pDisable); imageRight.setDisable(pDisable); scrollPaneLeft.setDisable(pDisable); scrollPaneRight.setDisable(pDisable); buttonZoomToFit.setDisable(pDisable); } /** * Convenience method to run before a GUI update thread */ private void internalBeforeGUIThread() { internalDisableInterface(true); progressIndicator.setVisible(true); progressIndicator.setOpacity(1); } /** * Convenience method to run after a GUI update thread (i.e. fade progress indicator) */ private void internalAfterGUIThread() { internalDisableInterface(false); Platform.runLater(new Runnable() { //@Override public void run() { FadeTransition fade = new FadeTransition(Duration.millis(gFadeDelayMS), progressIndicator); fade.setFromValue(1); fade.setToValue(0); fade.play(); } }); } /** * Toggle heatmap visibility * @param pEnabled whether heatmap should be enabled * @param pUpdateImage whether or not to update the heatmap image */ private void internalToggleHeatmap(boolean pEnabled, boolean pUpdateImage) { String enabled = "-fx-color: green;"; String disabled = ""; gHeatmapEnabled = pEnabled; if (gHeatmapEnabled) { disabled = buttonToggleSSIMHeatmap.getStyle(); buttonToggleSSIMHeatmap.setStyle(enabled); if (pUpdateImage) internalOverlayHeatmap(); } else { buttonToggleSSIMHeatmap.setStyle(disabled); if (pUpdateImage) { imageRight.setImage(gRightImageSave); } } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // internal methods (trying to keep javafx out of here if poss) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Save the results to pFile * @param pFile file to save results to */ private void internalSaveFile(File pFile) { //print to console anyway for the moment System.out.println(CheckResult.csvHeader); for (CheckResult result : gResults) { System.out.println(result.toCSV()); } if (null != pFile) { //save to file } } /** * Open input csv file * @param pFile file to load from */ private void internalOpenFile(File pFile) { init(); gResults = Collections.synchronizedList(new LinkedList<CheckResult>()); try { BufferedReader buf = new BufferedReader(new FileReader(pFile)); while (buf.ready()) { CheckResult result = new CheckResult(); String[] line = buf.readLine().split(","); if (line == null || line.length != 2) { continue; } result.setFileOne(new File(line[0])); result.setFileTwo(new File(line[1])); gResults.add(result); } buf.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } //initialise the results array to hold the correct number of results if (checkboxGenerateOnLoad.selectedProperty().get()) { //start a background thread to calculate all the values internalStartBackgroundLoadThread(); } //load the first image internalDisplayPair(gCurrentRecord); } /** * Start a background thread to pre-process all input pairs */ private void internalStartBackgroundLoadThread() { Task<Integer> task = new ProcessingLoadTask(gResults, false, false, false); Thread loader = new Thread(task); loader.setDaemon(true); loader.start(); } /** * Main display method - calculates next record to display, calculates it if necessary and * displays it * @param pRecord record to display */ private void internalDisplayPair(final int pRecord) { if (pRecord >= gResults.size()) { gCurrentRecord = gResults.size() - 1; gLogger.trace("No next!"); return; } if (pRecord < 0) { gCurrentRecord = 0; gLogger.trace("No prev!"); return; } //set the image number labelImageNumber.setText("Pair " + (pRecord + 1 + " of " + gResults.size())); internalUpdateManualCheckLabel(pRecord); gHeatmap = null; gHeatmapGenerated = false; gRightImageSave = null; gHeatmapImage = null; internalToggleHeatmap(false, false); boolean exit = false; if (pRecord == gPrecached) { gLogger.trace("Displaying precached pair"); internalDisplayPrecachedPair(pRecord); exit = true; } if (checkboxPrecacheNext.selectedProperty().get()) { gLogger.trace("Generating next precached pair"); internalPrecachePair(pRecord + 1); } if (exit) { // internalImageZoomToFit(); return; } if (gResults.get(pRecord).processed) { //i.e. we have already calculated this! gLogger.trace("Reloading: " + gResults.get(pRecord).toCSV()); internalReloadRecord(pRecord); } else { if (gResults.get(pRecord).isProcessing) { gLogger.trace("Waiting for processing... " + gResults.get(pRecord).toCSV()); internalBeforeGUIThread(); progressIndicator.progressProperty().unbind(); progressIndicator.setProgress(0); try { while (!gResults.get(pRecord).processed) { gLogger.trace("Waiting for: " + gResults.get(pRecord).toCSV()); Thread.sleep(gThreadSleepMS); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } internalAfterGUIThread(); internalReloadRecord(pRecord); } if (gResults.get(pRecord).claimForProcessing()) { gLogger.trace("Processing: " + gResults.get(pRecord).toCSV()); internalCalcAndDisplayRecord(pRecord); } } // internalImageZoomToFit(); } /** * Pre-cache the next set of images and calculate if necessary * @param pRecord */ private void internalPrecachePair(final int pRecord) { if (pRecord >= gResults.size()) { gLogger.trace("Precache: No next!"); return; } if (pRecord < 0) { gLogger.trace("Precache: No prev!"); return; } gLogger.trace("Precaching record: " + pRecord); gArePrecachingNow = true; boolean justLoad = false; if (!gResults.get(pRecord).processed) { if (gResults.get(pRecord).claimForProcessing()) { List<CheckResult> single = new LinkedList<CheckResult>(); single.add(gResults.get(pRecord)); Task<Integer> task = new ProcessingLoadTask(single, false, true, true); Thread loader = new Thread(task); loader.setDaemon(true); //FIXME: hack here to set precached id gPrecached = pRecord; loader.start(); return; } else { //enter busy-wait loop? - just load images and presume calcs will be done(?) justLoad = true; } } if (gResults.get(pRecord).processed | justLoad) { Task<Integer> task = new Task<Integer>() { @Override protected Integer call() throws Exception { gLogger.trace("just loading images for precache (no calcs)"); final Image left = SwingFXUtils.toFXImage(internalLoadImage(gResults.get(pRecord).getFileOne()), null); final Image right = SwingFXUtils .toFXImage(internalLoadImage(gResults.get(pRecord).getFileTwo()), null); Platform.runLater(new Runnable() { //@Override public void run() { gPrecachedLeft = left; gPrecachedRight = right; gPrecached = pRecord; gArePrecachingNow = false; } }); gLogger.trace("Precaching done for record " + pRecord); return null; } }; Thread loader = new Thread(task); loader.setDaemon(true); loader.start(); return; } gLogger.trace("Precaching incomplete for record " + pRecord); gArePrecachingNow = false; gPrecached = -1; } /** * Pre-cache the next set of images and calculate if necessary * @param pRecord */ private void internalDisplayPrecachedPair(int pRecord) { if (pRecord >= gResults.size()) { gCurrentRecord = gResults.size() - 1; gLogger.trace("No next!"); return; } if (pRecord < 0) { gCurrentRecord = 0; gLogger.trace("No prev!"); return; } if (pRecord == gPrecached) { if (gArePrecachingNow) { //wait until we aren't precaching try { while (!gArePrecachingNow) { gLogger.trace( "Waiting for preacache operation to finish: " + gResults.get(pRecord).toCSV()); Thread.sleep(gThreadSleepMS); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } gLogger.trace("Displaying precached record: " + pRecord); if (gPrecachedLeft == null) gLogger.trace("gPrecachedLeft == null"); if (gPrecachedRight == null) gLogger.trace("gPrecachedRight == null"); internalDisableInterface(true); imageLeft.setImage(gPrecachedLeft); imageRight.setImage(gPrecachedRight); imageLeftLabel.setText(gResults.get(pRecord).getFileOne().getName()); imageRightLabel.setText(gResults.get(pRecord).getFileTwo().getName()); psnrLabelValue.setText(gResults.get(pRecord).getPsnr() + ""); ssimLabelValue.setText(gResults.get(pRecord).getSsimMean() + ""); internalSetSSIMVariance(gResults.get(pRecord).getSsimVariance()); ssimMinValue.setText(gResults.get(pRecord).getSsimMin() + ""); internalUpdateManualCheckLabel(pRecord); internalDisableInterface(false); internalImageZoomToFit(); } } private void internalSetSSIMVariance(double pVar) { ssimVarianceValue.setText(new DecimalFormat("0.0000000").format(pVar) + ""); } /** * Calculate and display a record * @param pRecord record to calculate and display */ private void internalCalcAndDisplayRecord(final int pRecord) { List<CheckResult> single = new LinkedList<CheckResult>(); single.add(gResults.get(pRecord)); //this is a hack - create a list with only one entry for processing //third param is true to ignore isProcessing checks as the record //should be claimed for us by this point Task<Integer> task = new ProcessingLoadTask(single, true, true, false); progressIndicator.progressProperty().bind(task.progressProperty()); imageLeftLabel.setText(gResults.get(pRecord).getFileOne().getName()); imageRightLabel.setText(gResults.get(pRecord).getFileTwo().getName()); Thread loader = new Thread(task); loader.setDaemon(true); loader.start(); } /** * Update GUI from manual check entry * @param pRecord record to use */ private void internalUpdateManualCheckLabel(int pRecord) { if (!gResults.get(pRecord).processed) { //set an empty text labelCheckStatus.setStyle(""); labelCheckStatus.setText(""); return; } if (gResults.get(pRecord).getManualCheck() == ManualCheck.OK) { labelCheckStatus.setStyle(buttonOK.getStyle()); labelCheckStatus.setText("OK"); return; } if (gResults.get(pRecord).getManualCheck() == ManualCheck.FAIL) { labelCheckStatus.setStyle(buttonFail.getStyle()); labelCheckStatus.setText("FAIL"); return; } //set an empty text labelCheckStatus.setStyle(""); labelCheckStatus.setText(""); } /** * Reload a record (no calculations) * @param pRecord */ private void internalReloadRecord(final int pRecord) { gLogger.trace("Reloading record: " + pRecord); Task<Integer> task = new Task<Integer>() { @Override protected Integer call() throws Exception { int max = 2; int count = 0; Platform.runLater(new Runnable() { //@Override public void run() { internalBeforeGUIThread(); imageLeft.setImage(null); imageRight.setImage(null); imageLeftLabel.setText(gResults.get(pRecord).getFileOne().getName()); imageRightLabel.setText(gResults.get(pRecord).getFileTwo().getName()); psnrLabelValue.setText(gResults.get(pRecord).getPsnr() + ""); ssimLabelValue.setText(gResults.get(pRecord).getSsimMean() + ""); internalSetSSIMVariance(gResults.get(pRecord).getSsimVariance()); ssimMinValue.setText(gResults.get(pRecord).getSsimMin() + ""); internalUpdateManualCheckLabel(pRecord); } }); final BufferedImage one = internalLoadImage(gResults.get(pRecord).getFileOne()); Platform.runLater(new Runnable() { //@Override public void run() { imageLeft.setImage(SwingFXUtils.toFXImage(one, null)); } }); updateProgress(++count, max); final BufferedImage two = internalLoadImage(gResults.get(pRecord).getFileTwo()); Platform.runLater(new Runnable() { //@Override public void run() { imageRight.setImage(SwingFXUtils.toFXImage(two, null)); internalImageZoomToFit(); } }); updateProgress(++count, max); internalAfterGUIThread(); return null; } }; progressIndicator.progressProperty().bind(task.progressProperty()); Thread loader = new Thread(task); loader.setDaemon(true); loader.start(); internalDisableInterface(false); } /** * Load an image * @param pImage image to load * @return loaded image */ private BufferedImage internalLoadImage(File pImage) { try { if (pImage.getName().toLowerCase().endsWith(".jp2")) { //load a jp2? //FIXME: use openjpeg? //this uses jai (jj2000) //BufferedImage image = ImageIO.read(pImage); BufferedImage image = null;//OpenJPEGLoader.loadJP2(pImage); return image; } return Imaging.getBufferedImage(pImage); } catch (IOException e) { return null; } catch (NullPointerException e) { return null; } catch (ImageReadException e) { return null; } } /** * overlay heatmap on image */ private void internalOverlayHeatmap() { gRightImageSave = imageRight.getImage(); if (!gHeatmapGenerated) { internalBeforeGUIThread(); //threaded load so GUI doesn't hang Task<Integer> task = new Task<Integer>() { @Override protected Integer call() throws Exception { BufferedImage image = SwingFXUtils.fromFXImage(imageRight.getImage(), null); if (gHeatmap == null) { //re-generate heatmap (must be on a re-load) try { gHeatmap = Imaging.getBufferedImage(gResults.get(gCurrentRecord).getHeatmapTemp()); } catch (IOException | ImageReadException e) { // TODO Auto-generated catch block e.printStackTrace(); } } for (int y = 0; y < image.getHeight(); y++) { for (int x = 0; x < image.getWidth(); x++) { int rgb = image.getRGB(x, y); Color heatmapColor = new Color(gHeatmap.getRGB((x / DissimilarV2.SSIMWINDOWSIZE), (y / DissimilarV2.SSIMWINDOWSIZE))); int heatmapPixel = heatmapColor.getGreen();//&maxPixelValue; if (heatmapColor.getGreen() != heatmapColor.getBlue() && heatmapColor.getBlue() != heatmapColor.getRed()) { gLogger.error("Heatmap error (should not happen)"); } double factor = 1 - (((double) heatmapPixel / 255)); Color col = new Color(rgb); int red = (int) (factor * col.getRed()); int green = (int) (factor * col.getGreen()); int blue = (int) (factor * col.getBlue()); if (red < 0) red = 0; if (green < 0) green = 0; if (blue < 0) blue = 0; col = new Color(red, green, blue); image.setRGB(x, y, col.getRGB()); } } gHeatmapImage = SwingFXUtils.toFXImage(image, null); gHeatmapGenerated = true; Platform.runLater(new Runnable() { //@Override public void run() { imageRight.setImage(gHeatmapImage); } }); internalAfterGUIThread(); return 1; } }; progressIndicator.progressProperty().bind(task.progressProperty()); Thread loader = new Thread(task); loader.setDaemon(true); loader.start(); } else { imageRight.setImage(gHeatmapImage); } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // internal classes ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Processes an input pair, updating values in referenced list * NOTE: list should be synchronized for multi-threading * i.e. gResults = Collections.synchronizedList(new LinkedList<CheckResult>()); * @author wpalmer * */ private class ProcessingLoadTask extends Task<Integer> { private List<CheckResult> taskResults = null; private boolean updateGUI = false; private boolean ignoreClaims = false; private boolean populatePrecache = false; /** * Create a new ProcessingLoadTask * @param pResults list of objects to process * @param pUpdateGUI whether or not to update the GUI * @param pIgnoreClaims whether or not to ignore CheckResult.isProcessing (be careful) * @param pPopulatePrecache whether or not to populate the global precache images */ public ProcessingLoadTask(List<CheckResult> pResults, boolean pUpdateGUI, boolean pIgnoreClaims, boolean pPopulatePrecache) { taskResults = pResults; updateGUI = pUpdateGUI; ignoreClaims = pIgnoreClaims; populatePrecache = pPopulatePrecache; } @Override protected Integer call() throws Exception { gLogger.trace("LoadTask calculating: " + taskResults.size() + " entries"); for (CheckResult result : taskResults) { //some other process had claimed this record for processing if (!ignoreClaims & !result.claimForProcessing()) { gLogger.trace("LoadTask Skipping: " + result.toCSV()); continue; } else { gLogger.trace("LoadTask Processing: " + result.toCSV()); } result.isProcessing = true; if (updateGUI) { Platform.runLater(new Runnable() { //@Override public void run() { internalBeforeGUIThread(); //filenames are set in calling method due to issues accessing members here imageLeft.setImage(null); imageRight.setImage(null); String calculating = "Working"; psnrLabelValue.setText(calculating); ssimLabelValue.setText(calculating); ssimVarianceValue.setText(calculating); ssimMinValue.setText(calculating); } }); } //if any more updateProgress() calls are added then increase this value!!! int maxProgress = 8; int progress = 0; final BufferedImage imageOne = internalLoadImage(result.getFileOne()); updateProgress(++progress, maxProgress); if (updateGUI) { Platform.runLater(new Runnable() { //@Override public void run() { if (imageOne != null) imageLeft.setImage(SwingFXUtils.toFXImage(imageOne, null)); } }); } if (populatePrecache) { Platform.runLater(new Runnable() { //@Override public void run() { gPrecachedLeft = SwingFXUtils.toFXImage(imageOne, null); } }); } final BufferedImage imageTwo = internalLoadImage(result.getFileTwo()); updateProgress(++progress, maxProgress); if (imageOne != null & imageTwo != null) { // System.out.println("Loaded ok"); } else { gLogger.error("Loading error: " + result.toCSV()); //re-enable GUI here if (updateGUI) { internalAfterGUIThread(); } return null; } final boolean greyscale = (imageOne.getType() == BufferedImage.TYPE_BYTE_GRAY || imageOne.getType() == BufferedImage.TYPE_USHORT_GRAY); if (updateGUI) { Platform.runLater(new Runnable() { //@Override public void run() { imageRight.setImage(SwingFXUtils.toFXImage(imageTwo, null)); } }); } if (populatePrecache) { Platform.runLater(new Runnable() { //@Override public void run() { gPrecachedRight = SwingFXUtils.toFXImage(imageTwo, null); } }); } int[] oneA = imageOne.getRGB(0, 0, imageOne.getWidth(), imageOne.getHeight(), null, 0, imageOne.getWidth()); updateProgress(++progress, maxProgress); int[] twoA = imageTwo.getRGB(0, 0, imageTwo.getWidth(), imageTwo.getHeight(), null, 0, imageTwo.getWidth()); updateProgress(++progress, maxProgress); final int width = imageOne.getWidth(); final int height = imageOne.getHeight(); //calculate psnr and ssim final double psnr = DissimilarV2.calcPSNR(oneA, twoA, greyscale); result.setPsnr(psnr); if (updateGUI) { Platform.runLater(new Runnable() { //@Override public void run() { psnrLabelValue.setText(psnr + ""); } }); } updateProgress(++progress, maxProgress); List<Double> ssimMinimum = new LinkedList<Double>(); List<Double> ssimVariance = new LinkedList<Double>(); final File tempHeatmap = File.createTempFile( result.getFileOne().getName() + "-" + result.getFileTwo().getName() + "-", ".png"); final double ssim = DissimilarV2.calcSSIM(oneA, twoA, width, height, greyscale, tempHeatmap.getAbsolutePath(), ssimMinimum, ssimVariance); tempHeatmap.deleteOnExit(); result.setHeatmapTemp(tempHeatmap); result.setSsimMean(ssim); final double ssimVar = ssimVariance.get(0); result.setSsimVariance(ssimVar); final double ssimMin = ssimMinimum.get(0); result.setSsimMin(ssimMin); updateProgress(++progress, maxProgress); if (updateGUI) { Platform.runLater(new Runnable() { //@Override public void run() { try { gHeatmap = Imaging.getBufferedImage(tempHeatmap); } catch (IOException | ImageReadException e) { // TODO Auto-generated catch block e.printStackTrace(); } ssimLabelValue.setText(ssim + ""); internalSetSSIMVariance(ssimVar); ssimMinValue.setText(ssimMin + ""); internalDisableInterface(false); } }); } updateProgress(++progress, maxProgress); //try and free some memory oneA = null; twoA = null; System.gc(); updateProgress(++progress, maxProgress); if (updateGUI) { internalAfterGUIThread(); internalImageZoomToFit(); } if (populatePrecache) { Platform.runLater(new Runnable() { //@Override public void run() { gArePrecachingNow = false; gLogger.trace("Precaching done (incl calcs) for record " + gPrecached); } }); } result.processed = true; result.isProcessing = false; //System.out.println(result.toCSV()); } return taskResults.size(); } } }