diff --git a/src/main/groovy/qupath/ext/qp_scope/QP_scope.groovy b/src/main/groovy/qupath/ext/qp_scope/QP_scope.groovy index ff8dd78..4f4d010 100644 --- a/src/main/groovy/qupath/ext/qp_scope/QP_scope.groovy +++ b/src/main/groovy/qupath/ext/qp_scope/QP_scope.groovy @@ -49,29 +49,43 @@ class QP_scope implements QuPathExtension { private void addMenuItem(QuPathGUI qupath) { def logger = LoggerFactory.getLogger(QuPathGUI.class) - //Check for dependencies and QuPath version + // Check for dependencies and QuPath version logger.info("QuPath Version") logger.info(getQuPathVersion().toString()) - //TODO how to check if version is supported? + // TODO: how to check if version is supported? + // Get or create the menu def menu = qupath.getMenu("Extensions>${name}", true) + + // First menu item def qpScope1 = new MenuItem("Start qp_scope") - // TODO tooltip + // TODO: tooltip qpScope1.setOnAction(e -> { - //TODO check preferences for all necessary entries + // TODO: check preferences for all necessary entries QP_scope_GUI.createGUI1() }) + // Second menu item def qpScope2 = new MenuItem("Second scan on existing annotations") - // TODO tooltip + // TODO: tooltip qpScope2.setOnAction(e -> { - //TODO check preferences for all necessary entries + // TODO: check preferences for all necessary entries QP_scope_GUI.createGUI2() }) + // Third menu item - "Use current image as macro view" + def qpScope3 = new MenuItem("Use current image as macro view") + // TODO: tooltip + qpScope3.setOnAction(e -> { + QP_scope_GUI.createGUI3() + }) + + // Add the menu items to the menu menu.getItems() << qpScope1 menu.getItems() << qpScope2 + menu.getItems() << qpScope3 } + } //@ActionMenu("Extensions") diff --git a/src/main/groovy/qupath/ext/qp_scope/functions/QP_scope_GUI.groovy b/src/main/groovy/qupath/ext/qp_scope/functions/QP_scope_GUI.groovy index 33d03a2..cc1acc4 100644 --- a/src/main/groovy/qupath/ext/qp_scope/functions/QP_scope_GUI.groovy +++ b/src/main/groovy/qupath/ext/qp_scope/functions/QP_scope_GUI.groovy @@ -7,6 +7,7 @@ import javafx.scene.control.Dialog import javafx.scene.control.Label import javafx.scene.control.TextField import javafx.scene.layout.GridPane +import javafx.scene.layout.HBox import javafx.stage.Modality import org.slf4j.LoggerFactory import qupath.ext.qp_scope.utilities.utilityFunctions @@ -20,9 +21,11 @@ import qupath.lib.gui.scripting.QPEx import qupath.ext.basicstitching.stitching.stitchingImplementations import java.awt.image.BufferedImage +import java.nio.charset.StandardCharsets import java.nio.file.Path import java.nio.file.Paths import java.nio.file.Files +import java.util.stream.Collectors import static qupath.lib.scripting.QP.project @@ -46,6 +49,11 @@ class QP_scope_GUI { static TextField pythonScriptField = new TextField(preferences.installation) static TextField projectsFolderField = new TextField(preferences.projects) static TextField sampleLabelField = new TextField("First_Test") // New field for sample label + // GUI3 + static CheckBox slideFlippedCheckBox = new CheckBox("Slide is flipped") + static TextField groovyScriptField = new TextField("C:/ImageAnalysis/python/DetectTissueSize.groovy") // Default empty + static TextField pixelSizeField = new TextField("0.25") // Default empty + static CheckBox nonIsotropicCheckBox = new CheckBox("Non-isotropic pixels") static void createGUI1() { // Create the dialog @@ -205,6 +213,12 @@ class QP_scope_GUI { pane.add(label, 0, rowIndex) pane.add(control, 1, rowIndex) } + // Overloaded addToGrid method for a single Node + // TODO fix hardcoding of 2 and 1 + private static void addToGrid(GridPane pane, Node node, int rowIndex) { + pane.add(node, 0, rowIndex, 2, 1); // The node spans 2 columns + } + static void createGUI2() { //TODO check if in a project? def logger = LoggerFactory.getLogger(QuPathGUI.class) @@ -300,6 +314,8 @@ class QP_scope_GUI { } } + + //Create the second interface window for performing higher resolution or alternate modality scans private static GridPane createContent2() { GridPane pane = new GridPane() @@ -317,6 +333,228 @@ class QP_scope_GUI { addToGrid(pane, new Label('Projects path:'), projectsFolderField, row++) // Listener for the checkbox + return pane + } + + static void createGUI3() { + // Create the dialog + def dlg = new Dialog() + dlg.initModality(Modality.APPLICATION_MODAL) + dlg.setTitle("Macro View Configuration") + dlg.setHeaderText("Configure settings for macro view.") + + // Set the content + dlg.getDialogPane().setContent(createContent3()) + + // Add Okay and Cancel buttons + dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL) + + // Define response validation + dlg.setResultConverter(dialogButton -> { + if (dialogButton == ButtonType.OK) { + if (!isValidInput(x1Field.getText()) || !isValidInput(y1Field.getText())) { + Dialogs.showWarningNotification("Invalid Input", "Please enter valid numeric values for coordinates.") + return null; // Prevent dialog from closing + } + } + return dialogButton; + }); + + // Show the dialog and capture the response + Optional result = dlg.showAndWait(); + def logger = LoggerFactory.getLogger(QuPathGUI.class) + // Handling the response + if (result.isPresent() && result.get() == ButtonType.OK) { + // Retrieve values from text fields and checkbox + String xCoordinate = x1Field.getText(); + String yCoordinate = y1Field.getText(); + String pixelSize = pixelSizeField.getText(); + boolean isSlideFlipped = slideFlippedCheckBox.isSelected(); + boolean arePixelsNonIsotropic = nonIsotropicCheckBox.isSelected(); + String groovyScriptPath = groovyScriptField.getText(); + def sampleLabel = sampleLabelField.getText() + def virtualEnvPath = virtualEnvField.getText() + def pythonScriptPath = pythonScriptField.getText() + def projectsFolderPath = projectsFolderField.getText() + + // Check if data is all present + if ([xCoordinate, yCoordinate, pixelSize, groovyScriptPath].any { it == null || it.isEmpty() }) { + Dialogs.showWarningNotification("Warning!", "Insufficient data to send command to microscope!") + return + } + String imageName = QP.getCurrentImageName(); + + // Determine the pixel size based on imageName + if (imageName.contains("3600")) { + pixelSize = "2.0"; + } else if (imageName.contains("7200")) { + pixelSize = "1.0"; + } + + // Expect the classifier file path to be in a specific location + // Extract the directory from pythonScriptPath + Path scriptDirectory = Paths.get(pythonScriptPath).getParent(); + + // Combine the directory with the new filename + Path jsonFilePath = scriptDirectory.resolve("Tissue-lowres.json"); + Path exportScriptPath = scriptDirectory.resolve("save4xMacroTiling.groovy") + // Convert Path back to String and fix slashes to not be escape chars + String jsonFilePathString = jsonFilePath.toString().replace("\\", "/"); + String exportScriptPathString = exportScriptPath.toString().replace("\\", "/"); + + //Create the QuPath project + Project currentQuPathProject= utilityFunctions.createProjectFolder(projectsFolderPath, sampleLabel, preferences.firstScanType) + def scanTypeWithIndex = utilityFunctions.getUniqueFolderName(projectsFolderPath + File.separator + sampleLabel + File.separator + preferences.firstScanType) + def tempTileDirectory = projectsFolderPath + File.separator + sampleLabel+File.separator+scanTypeWithIndex + + + //Get the current image open in QuPath and add it to the project + def serverPath = QP.getCurrentImageData().getServerPath() + + String macroImagePath = utilityFunctions.extractFilePath(serverPath); + + if (macroImagePath != null) { + logger.info("Extracted file path: " + macroImagePath); + } else { + logger.info("File path could not be extracted."); + } + + //open the newly created project + //https://qupath.github.io/javadoc/docs/qupath/lib/gui/QuPathGUI.html#setProject(qupath.lib.projects.Project) + def qupathGUI = QPEx.getQuPath() + + utilityFunctions.addImageToProject( new File(macroImagePath), currentQuPathProject) + qupathGUI.setProject(currentQuPathProject) + //Find the existing images - there should only be one since the project was just created + def matchingImage = currentQuPathProject.getImageList().find { image -> + (new File(image.getImageName()).name == new File(macroImagePath).name) + } + + //Open the first image + //https://qupath.github.io/javadoc/docs/qupath/lib/gui/QuPathGUI.html#openImageEntry(qupath.lib.projects.ProjectImageEntry) + qupathGUI.openImageEntry(matchingImage) + //TODO ADD MACRO IMAGE TO PROJECT and open SECOND image + + qupathGUI.refreshProject() + + String tissueDetectScript = utilityFunctions.modifyTissueDetectScript(groovyScriptPath, pixelSize, jsonFilePathString) + //logger.info(tissueDetectScript) + // Run the modified script + QuPathGUI.getInstance().runScript(null, tissueDetectScript); + //At this point the tissue should be outlined in an annotation + + //Create an export tile locations + String tilesCSVdirectory = projectsFolderPath + File.separator + sampleLabel + File.separator + "tiles_csv"; + String exportScript = utilityFunctions.modifyCSVExportScript(exportScriptPathString, pixelSize, tilesCSVdirectory) + logger.info(exportScript) + logger.info(tilesCSVdirectory) + logger.info(exportScriptPathString) + QuPathGUI.getInstance().runScript(null, exportScript); + + // Additional code for annotations + def annotations = QP.getAnnotationObjects(); + if (annotations.size() != 1) { + Dialogs.showWarningNotification("Error!", "Can only handle 1 annotation at the moment!"); + return; + } + + def x1 = annotations[0].getROI().getBoundsX() + def y1 = annotations[0].getROI().getBoundsY() + def x2 = annotations[0].getROI().getBoundsWidth() + def y2 = annotations[0].getROI().getBoundsHeight() + // TODO Check if any value is empty + + //Send the QuPath pixel coordinates for the bounding box along with the pixel size and upper left coordinates of the tissue + def boundingBox = utilityFunctions.transformBoundingBox(x1,y1,x2,y2,pixelSize, xCoordinate, yCoordinate, isSlideFlipped) + + + logger.info(tilesCSVdirectory) + + + // scanTypeWithIndex will be the name of the folder where the tiles will be saved to + + List args = [pythonScriptPath, + projectsFolderPath, + sampleLabel, + scanTypeWithIndex, + tilesCSVdirectory, //no annotation JSON file location + boundingBox ] + //TODO can we create non-blocking python code + utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, args) + + + String stitchedImageOutputFolder = projectsFolderPath + File.separator + sampleLabel + File.separator + "SlideImages" + //TODO Need to check if stitching is successful, provide error + //stitchingImplementations.stitchCore(stitchingType, folderPath, compressionType, pixelSize, downsample, matchingString) + //TODO add output folder to stitchCore + String stitchedImagePathStr = stitchingImplementations.stitchCore("Coordinates in TileCoordinates.txt file", projectsFolderPath + File.separator + sampleLabel, stitchedImageOutputFolder, "J2K_LOSSY", 0, 1, scanTypeWithIndex) + + + //utilityFunctions.showAlertDialog("Wait and complete stitching in other version of QuPath") + + //String stitchedImagePathStr = stitchedImageOutputFolder + File.separator + preferences.firstScanType + sampleLabel + ".ome.tif" + File stitchedImagePath = new File(stitchedImagePathStr) + utilityFunctions.addImageToProject(stitchedImagePath, currentQuPathProject) + + + + qupathGUI.setProject(currentQuPathProject) + //Find the existing images - there should only be one since the project was just created + matchingImage = currentQuPathProject.getImageList().find { image -> + (new File(image.getImageName()).name == new File(stitchedImagePathStr).name) + } + + //Open the first image + //https://qupath.github.io/javadoc/docs/qupath/lib/gui/QuPathGUI.html#openImageEntry(qupath.lib.projects.ProjectImageEntry) + qupathGUI.openImageEntry(matchingImage) + //TODO ADD MACRO IMAGE TO PROJECT and open SECOND image + + qupathGUI.refreshProject() + //Check if the tiles should be deleted from the collection folder + if (preferences.tileHandling == "Delete") + utilityFunctions.deleteTilesAndFolder(tempTileDirectory) + if (preferences.tileHandling == "Zip") { + utilityFunctions.zipTilesAndMove(tempTileDirectory) + utilityFunctions.deleteTilesAndFolder(tempTileDirectory) + } + + + } + } + + // Helper method to check if input is numeric + private static boolean isValidInput(String input) { + return input.matches("\\d*"); + } + + + private static GridPane createContent3() { + GridPane pane = new GridPane() + pane.setHgap(10) + pane.setVgap(10) + def row = 0 + + // Add new components for the checkbox and Groovy script path + addToGrid(pane, new Label('Sample Label:'), sampleLabelField, row++) + // Add components for Python environment and script path + addToGrid(pane, new Label('Python Virtual Env Location:'), virtualEnvField, row++) + addToGrid(pane, new Label('.py file path:'), pythonScriptField, row++) + addToGrid(pane, new Label('Projects path:'), projectsFolderField, row++) + + addToGrid(pane, new Label('Slide flipped:'), slideFlippedCheckBox, row++) + addToGrid(pane, new Label('.groovy file path:'), groovyScriptField, row++) + // Add new components for pixel size and non-isotropic pixels checkbox on the same line + HBox pixelSizeBox = new HBox(10); + pixelSizeBox.getChildren().addAll(new Label('Pixel Size XY:'), pixelSizeField, nonIsotropicCheckBox); + addToGrid(pane, pixelSizeBox, row++); + // Add new components for "Upper left XY coordinate" + Label upperLeftLabel = new Label("Upper left XY coordinate") + pane.add(upperLeftLabel, 0, row); // Span multiple columns if needed + + addToGrid(pane, new Label('X coordinate:'), x1Field, ++row); + addToGrid(pane, new Label('Y coordinate:'), y1Field, ++row); + + return pane } } diff --git a/src/main/groovy/qupath/ext/qp_scope/utilities/utilityFunctions.groovy b/src/main/groovy/qupath/ext/qp_scope/utilities/utilityFunctions.groovy index a11c354..d209777 100644 --- a/src/main/groovy/qupath/ext/qp_scope/utilities/utilityFunctions.groovy +++ b/src/main/groovy/qupath/ext/qp_scope/utilities/utilityFunctions.groovy @@ -17,9 +17,13 @@ import javafx.scene.control.Alert import javafx.stage.Modality import qupath.lib.projects.Project +import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import java.util.regex.Matcher +import java.util.regex.Pattern +import java.util.stream.Collectors import java.util.stream.Stream import java.io.*; import java.nio.file.*; @@ -297,4 +301,104 @@ class utilityFunctions { } } + static String transformBoundingBox(double x1, double y1, double x2, double y2, String pixelSize, String xCoordinate, String yCoordinate, boolean flip) { + def logger = LoggerFactory.getLogger(QuPathGUI.class) + //TODO handle flip + if (flip){ + + logger.info("handle flip") + } + // Convert pixel coordinates to microns + double x1Microns = x1 * (pixelSize as Double); + double y1Microns = y1 * (pixelSize as Double); + double x2Microns = x2 * (pixelSize as Double); + double y2Microns = y2 * (pixelSize as Double); + + // Adjust coordinates relative to the upper right coordinates + double adjustedX1 = xCoordinate as Double - x1Microns; + double adjustedY1 = yCoordinate as Double - y1Microns; + double adjustedX2 = xCoordinate as Double - x2Microns; + double adjustedY2 = yCoordinate as Double - y2Microns; + + // Create the bounding box string in the format "x1, y1, x2, y2" + String boundingBox = adjustedX1 + ", " + adjustedY1 + ", " + adjustedX2 + ", " + adjustedY2; + return boundingBox; + } + + + + /** + * Modifies the specified Groovy script by updating the pixel size and the JSON file path. + * + * @param groovyScriptPath The path to the Groovy script file. + * @param pixelSize The new pixel size to set in the script. + * @param jsonFilePathString The new JSON file path to set in the script. + * @throws IOException if an I/O error occurs reading from or writing to the file. + */ + public static String modifyTissueDetectScript(String groovyScriptPath, String pixelSize, String jsonFilePathString) throws IOException { + // Read, modify, and write the script in one go + List lines = Files.lines(Paths.get(groovyScriptPath), StandardCharsets.UTF_8) + .map(line -> { + if (line.startsWith("setPixelSizeMicrons")) { + return "setPixelSizeMicrons(" + pixelSize + ", " + pixelSize + ")"; + } else if (line.startsWith("createAnnotationsFromPixelClassifier")) { + return line.replaceFirst("\"[^\"]*\"", "\"" + jsonFilePathString + "\""); + } else { + return line; + } + }) + .collect(Collectors.toList()); + + return String.join(System.lineSeparator(), lines); + } + + /** + * Modifies the specified export script by updating the pixel size source and the base directory, and returns the modified script as a string. + * + * @param exportScriptPathString The path to the export script file. + * @param pixelSize The new pixel size to set in the script. + * @param tilesCSVdirectory The new base directory to set in the script. + * @return String representing the modified script. + * @throws IOException if an I/O error occurs reading from the file. + */ + public static String modifyCSVExportScript(String exportScriptPathString, String pixelSize, String tilesCSVdirectory) throws IOException { + // Read and modify the script + List lines = Files.lines(Paths.get(exportScriptPathString), StandardCharsets.UTF_8) + .map(line -> { + if (line.startsWith("double pixelSizeSource")) { + return "double pixelSizeSource = " + pixelSize + ";"; + } else if (line.startsWith("baseDirectory")) { + return "baseDirectory = \"" + tilesCSVdirectory.replace("\\", "\\\\") + "\";"; + } else { + return line; + } + }) + .collect(Collectors.toList()); + + // Join the lines into a single string + return String.join(System.lineSeparator(), lines); + } + + /** + * Extracts the file path from the server path string. + * + * @param serverPath The server path string. + * @return The extracted file path, or null if the path could not be extracted. + */ + public static String extractFilePath(String serverPath) { + // Regular expression to match the file path + String regex = "file:/(.*?\\.TIF)"; + + // Create a pattern and matcher for the regular expression + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(serverPath); + + // Check if the pattern matches and return the file path + if (matcher.find()) { + return matcher.group(1).replaceFirst("^/", "").replaceAll("%20", " "); + } else { + return null; // No match found + } + } + } \ No newline at end of file