diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..f4a2df5 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file 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 28a178e..f451613 100644 --- a/src/main/groovy/qupath/ext/qp_scope/QP_scope.groovy +++ b/src/main/groovy/qupath/ext/qp_scope/QP_scope.groovy @@ -33,8 +33,8 @@ class QP_scope implements QuPathExtension { * @return The description of the extension. */ @Override - public String getDescription() { - return "Control a microscope!"; + String getDescription() { + return "Control a microscope!" } /** @@ -43,8 +43,8 @@ class QP_scope implements QuPathExtension { * @return The name of the extension. */ @Override - public String getName() { - return "qp_scope"; + String getName() { + return "qp_scope" } private void addMenuItem(QuPathGUI qupath) { @@ -58,27 +58,27 @@ class QP_scope implements QuPathExtension { def menu = qupath.getMenu("Extensions>${name}", true) // First menu item - def qpScope1 = new MenuItem("Start qp_scope") + def qpScope1 = new MenuItem("Input bounding box - first scan type") // TODO: tooltip qpScope1.setOnAction(e -> { // TODO: check preferences for all necessary entries, and check for micromanager running+version // search java app with a subprocesses for MicroManager +version number - QP_scope_GUI.createGUI1() + QP_scope_GUI.boundingBoxInputGUI() }) // Second menu item - def qpScope2 = new MenuItem("Second scan on existing annotations") + def qpScope2 = new MenuItem("Use current image to detect tissue location - first scan type") // TODO: tooltip qpScope2.setOnAction(e -> { // TODO: check preferences for all necessary entries - QP_scope_GUI.createGUI2() + QP_scope_GUI.macroImageInputGUI() }) // Third menu item - "Use current image as macro view" - def qpScope3 = new MenuItem("Use current image as macro view") + def qpScope3 = new MenuItem("Scan non \'Tissue\' annotations - second scan type") // TODO: tooltip qpScope3.setOnAction(e -> { - QP_scope_GUI.createGUI3() + QP_scope_GUI.secondModalityGUI() }) // Add the menu items to the menu 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 515be77..8f53950 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 @@ -8,12 +8,19 @@ import javafx.stage.Modality import org.slf4j.LoggerFactory import qupath.ext.basicstitching.stitching.stitchingImplementations import qupath.ext.qp_scope.utilities.utilityFunctions +import qupath.ext.qp_scope.utilities.minorFunctions +import qupath.ext.qp_scope.utilities.transformationFunctions import qupath.lib.gui.QuPathGUI import qupath.lib.gui.dialogs.Dialogs import qupath.lib.gui.scripting.QPEx +import qupath.lib.objects.PathObjectTools +import qupath.lib.objects.PathObjects import qupath.lib.projects.Project +import qupath.lib.regions.ImagePlane +import qupath.lib.roi.RectangleROI import qupath.lib.scripting.QP +import java.awt.geom.AffineTransform import java.awt.image.BufferedImage import java.nio.file.Path @@ -50,7 +57,7 @@ class QP_scope_GUI { static TextField pixelSizeField = new TextField(preferences.pixelSizeSource) // Default empty static CheckBox nonIsotropicCheckBox = new CheckBox("Non-isotropic pixels") - static void createGUI1() { + static void boundingBoxInputGUI() { // Create the dialog def dlg = new Dialog() dlg.initModality(Modality.APPLICATION_MODAL) @@ -58,7 +65,7 @@ class QP_scope_GUI { //dlg.setHeaderText("Enter details (LOOK MA! " + BasicStitchingExtension.class.getName() + "!):"); // Set the content - dlg.getDialogPane().setContent(createContent()) + dlg.getDialogPane().setContent(createBoundingBoxInputGUI()) // Add Okay and Cancel buttons dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL) @@ -77,12 +84,11 @@ class QP_scope_GUI { def y1 = y1Field.getText() def x2 = x2Field.getText() def y2 = y2Field.getText() - def annotationJsonFileLocation = null // Handle full bounding box input def boxString = scanBox.getText() //Boolean to check whether to proceed with running the microscope data collection boolean dataCheck = true - def pixelSize = preferences.pixelSizeTarget + def pixelSize = preferences.pixelSizeFirstScanType // Continue with previous behavior using coordinates @@ -104,17 +110,17 @@ class QP_scope_GUI { // Check if any value is empty if (dataCheck) { Project currentQuPathProject = utilityFunctions.createProjectFolder(projectsFolderPath, sampleLabel, preferences.firstScanType) - def scanTypeWithIndex = utilityFunctions.getUniqueFolderName(projectsFolderPath + File.separator + sampleLabel + File.separator + preferences.firstScanType) + def scanTypeWithIndex = minorFunctions.getUniqueFolderName(projectsFolderPath + File.separator + sampleLabel + File.separator + preferences.firstScanType) def tempTileDirectory = projectsFolderPath + File.separator + sampleLabel + File.separator + scanTypeWithIndex def logger = LoggerFactory.getLogger(QuPathGUI.class) logger.info(tempTileDirectory) - Path groovyScriptDirectory = Paths.get(pythonScriptPath).getParent(); + Path groovyScriptDirectory = Paths.get(pythonScriptPath).getParent() groovyScriptDirectory = groovyScriptDirectory.resolveSibling("groovyScripts") // Combine the directory with the new filename Path exportScriptPath = groovyScriptDirectory.resolve("save4xMacroTiling.groovy") - String exportScriptPathString = exportScriptPath.toString().replace("\\", "/"); + String exportScriptPathString = exportScriptPath.toString().replace("\\", "/") String exportScript = utilityFunctions.modifyTXTExportScript(exportScriptPathString, pixelSize, preferences, sampleLabel) def boundingBox = "{$x1}, {$y1}, {$x2}, {$y2}" //Specifically for the case where there is only a bounding box provided @@ -124,9 +130,7 @@ class QP_scope_GUI { logger.info(exportScript) logger.info(boundingBox) - QuPathGUI.getInstance().runScript(null, exportScript); - - //Reduce the number of sent args + QuPathGUI.getInstance().runScript(null, exportScript) // scanTypeWithIndex will be the name of the folder where the tiles will be saved to @@ -134,7 +138,6 @@ class QP_scope_GUI { projectsFolderPath, sampleLabel, scanTypeWithIndex, - annotationJsonFileLocation, boundingBox] //TODO can we create non-blocking python code utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, args) @@ -179,7 +182,7 @@ class QP_scope_GUI { } } - private static GridPane createContent() { + private static GridPane createBoundingBoxInputGUI() { GridPane pane = new GridPane() pane.setHgap(10) pane.setVgap(10) @@ -210,20 +213,20 @@ class QP_scope_GUI { // 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 + pane.add(node, 0, rowIndex, 2, 1) // The node spans 2 columns } - static void createGUI2() { + static void secondModalityGUI() { //TODO check if in a project? def logger = LoggerFactory.getLogger(QuPathGUI.class) // Create the dialog def dlg = new Dialog() dlg.initModality(Modality.APPLICATION_MODAL) dlg.setTitle("Collect image data from an annotated subset of your current image.") - dlg.setHeaderText("Create annotations within your image, then click Okay to proceed with a second collection within those areas."); + dlg.setHeaderText("Create annotations within your image, then click Okay to proceed with a second collection within those areas.") // Set the content - dlg.getDialogPane().setContent(createContent2()) + dlg.getDialogPane().setContent(createSecondModalityGUI()) // Add Okay and Cancel buttons dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL) @@ -240,11 +243,10 @@ class QP_scope_GUI { def pythonScriptPath = pythonScriptField.getText() def projectsFolderPath = projectsFolderField.getText() - def annotationJsonFileLocation = null //Boolean to check whether to proceed with running the microscope data collection logger.info("getting annotation objects") - def annotations = QP.getAnnotationObjects() + def annotations = getAnnotationObjects() // Check if annotations are present if (annotations.isEmpty() || [sampleLabel, virtualEnvPath, pythonScriptPath].any { it == null || it.isEmpty() }) { @@ -254,14 +256,14 @@ class QP_scope_GUI { } - def scanTypeWithIndex = utilityFunctions.getUniqueFolderName(projectsFolderPath + File.separator + sampleLabel + File.separator + preferences.secondScanType) + def scanTypeWithIndex = minorFunctions.getUniqueFolderName(projectsFolderPath + File.separator + sampleLabel + File.separator + preferences.secondScanType) def tempTileDirectory = projectsFolderPath + File.separator + sampleLabel + File.separator + scanTypeWithIndex logger.info("Scan type with index: " + scanTypeWithIndex) logger.info(tempTileDirectory) - logger.info("Creating json") - annotationJsonFileLocation = utilityFunctions.createAnnotationJson(projectsFolderPath, sampleLabel, scanTypeWithIndex) - List args = [pythonScriptPath, projectsFolderPath, sampleLabel, scanTypeWithIndex, annotationJsonFileLocation] + + + List args = [pythonScriptPath, projectsFolderPath, sampleLabel, scanTypeWithIndex] //TODO how can we distinguish between a hung python run and one that is taking a long time? - possibly check for new files in target folder? utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, args) //utilityFunctions.runPythonCommand(virtualEnvPath, "C:\\ImageAnalysis\\python\\py_dummydoc.py", args) @@ -310,7 +312,7 @@ class QP_scope_GUI { //Create the second interface window for performing higher resolution or alternate modality scans - private static GridPane createContent2() { + private static GridPane createSecondModalityGUI() { GridPane pane = new GridPane() pane.setHgap(10) pane.setVgap(10) @@ -329,42 +331,36 @@ class QP_scope_GUI { return pane } - static void createGUI3() { + /********************************** + * Starting point for an overview or "macro" image + */ + static void macroImageInputGUI() { // 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) + def dlg = createMacroImageInputDialog() // 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 null // Prevent dialog from closing } } - return dialogButton; - }); + return dialogButton + }) // Show the dialog and capture the response - Optional result = dlg.showAndWait(); + 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(); + 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() @@ -375,42 +371,42 @@ class QP_scope_GUI { Dialogs.showWarningNotification("Warning!", "Insufficient data to send command to microscope!") return } - String imageName = QP.getCurrentImageName(); + String imageName = QP.getCurrentImageName() // Determine the pixel size based on imageName if (imageName.contains("3600")) { - pixelSize = "2.0"; + pixelSize = "2.0" } else if (imageName.contains("7200")) { - pixelSize = "1.0"; + pixelSize = "1.0" } // Expect the classifier file path to be in a specific location // get the classifier from the groovyScripts folder, which should be "next to" the pythonScripts folder - Path groovyScriptDirectory = Paths.get(pythonScriptPath).getParent(); + Path groovyScriptDirectory = Paths.get(pythonScriptPath).getParent() groovyScriptDirectory = groovyScriptDirectory.resolveSibling("groovyScripts") // Combine the directory with the new filename - Path jsonFilePath = groovyScriptDirectory.resolve("Tissue-lowres.json"); + Path jsonFilePath = groovyScriptDirectory.resolve("Tissue-lowres.json") Path exportScriptPath = groovyScriptDirectory.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("\\", "/"); + 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 scanTypeWithIndex = minorFunctions.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); + String macroImagePath = minorFunctions.extractFilePath(serverPath) if (macroImagePath != null) { - logger.info("Extracted file path: " + macroImagePath); + logger.info("Extracted file path: " + macroImagePath) } else { - logger.info("File path could not be extracted."); + logger.info("File path could not be extracted.") } //open the newly created project @@ -433,80 +429,110 @@ class QP_scope_GUI { String tissueDetectScript = utilityFunctions.modifyTissueDetectScript(groovyScriptPath, pixelSize, jsonFilePathString) //logger.info(tissueDetectScript) // Run the modified script - QuPathGUI.getInstance().runScript(null, tissueDetectScript); + QuPathGUI.getInstance().runScript(null, tissueDetectScript) //At this point the tissue should be outlined in an annotation String exportScript = utilityFunctions.modifyTXTExportScript(exportScriptPathString, pixelSize, preferences, sampleLabel) logger.info(exportScript) logger.info(exportScriptPathString) - QuPathGUI.getInstance().runScript(null, exportScript); + QuPathGUI.getInstance().runScript(null, exportScript) ////////////////////////////////////// //Dialog chain to validate stage location ////////////////////////////////////// - // the transformation consists of an X-shift in stage microns, a Y-shift in stage microns, and a pixelSize - def transformation = [0, 0, pixelSize as double] - boolean gui4Success = createGUI4(); + //create a basic affine transformation, add the scaling information and a possible Y axis flip + AffineTransform transformation = new AffineTransform() //start with the identity matrix + double scale = (preferences.pixelSizeFirstScanType as Double) / (pixelSize as Double) + double scaleY = isSlideFlipped ? -scale : scale // Invert the Y axis if flip is true + + transformation.scale(scale, scaleY) + + // the transformation consists of an X-shift in stage microns, a Y-shift in stage microns, and a magnification + def viewer = QuPathGUI.getInstance().getViewer() + viewer.centerImage() + def x= viewer.getCenterPixelX() + def y= viewer.getCenterPixelY() + def frameWidth = (preferences.frameWidth as Double) + def frameHeight= (preferences.frameHeight as Double) + def tileROI = new RectangleROI(x-frameWidth/2, y-frameHeight/2, frameWidth, frameHeight, ImagePlane.getDefaultPlane()) + logger.info("initial position of tile at $x $y") + def FOVAnnotation = PathObjects.createAnnotationObject(tileROI) + FOVAnnotation = PathObjectTools.transformObject(FOVAnnotation,transformation, true) + QP.addObject(FOVAnnotation) + boolean gui4Success = stageToQuPathAlignmentGUI1() if (!gui4Success) { // User cancelled GUI4, so end GUI3 and do not proceed - return; + return } - // Execute Python command to move stage + // Get the current stage coordinates to figure out the translation from the first alignment. + List coordinatesQP = [FOVAnnotation.getROI().getBoundsX(), FOVAnnotation.getROI().getBoundsY()] + logger.info("user adjusted position of tile at $coordinatesQP") + List currentStageCoordinates_um = utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, null) + transformation = transformationFunctions.updateTransformation(transformation, coordinatesQP as List, currentStageCoordinates_um) + def detections = QP.getDetectionObjects() - def topCenterTileXY = utilityFunctions.getTopCenterTile(detections) + def topCenterTileXY = transformationFunctions.getTopCenterTile(detections) QP.selectObjects(topCenterTileXY[2]) - List args = [topCenterTileXY[0], topCenterTileXY[1]] + + //Transform the QuPath coordinates into stage coordinates + def QPPixelCoordinates = [topCenterTileXY[0] as Double, topCenterTileXY[1] as Double] + + List expectedStageXYPositionMicrons = transformationFunctions.QPtoMicroscopeCoordinates(QPPixelCoordinates, transformation) + QuPathGUI.getInstance().getViewer().setCenterPixelLocation(topCenterTileXY[2].getROI().getCentroidX(), topCenterTileXY[2].getROI().getCentroidY()) - //TODO run python script to move the stage to the middle X value of the lowest Y value - utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, args) + + //Move the stage to the middle X value of the lowest Y value (center of top row of tile positions) + utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, expectedStageXYPositionMicrons) //Validate the position that was moved to or update with an adjusted position - boolean updatePosition = createGUI5() + boolean updatePosition = stageToQuPathAlignmentGUI2() if (updatePosition) { //TODO get access to current stage coordinates - List currentStageCoordinates_um = utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, null) + currentStageCoordinates_um = utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, null) logger.info(currentStageCoordinates_um.toString()) - transformation = utilityFunctions.updateTransformation(transformation, currentStageCoordinates_um, args) + transformation = transformationFunctions.updateTransformation(transformation, expectedStageXYPositionMicrons as List, currentStageCoordinates_um ) } + //returns [x, y, Object] + def leftCenterTileXY = transformationFunctions.getLeftCenterTile(detections) - def leftCenterTileXY = utilityFunctions.getLeftCenterTile(detections) QP.selectObjects(leftCenterTileXY[2]) - args = [leftCenterTileXY[0], leftCenterTileXY[1]] + QPPixelCoordinates = [leftCenterTileXY[0], leftCenterTileXY[1]] + expectedStageXYPositionMicrons = transformationFunctions.QPtoMicroscopeCoordinates(QPPixelCoordinates as List,transformation) QuPathGUI.getInstance().getViewer().setCenterPixelLocation(leftCenterTileXY[2].getROI().getCentroidX(), leftCenterTileXY[2].getROI().getCentroidY()) - //TODO run python script to move the stage to the a tile position with the lowest X value, mid Y value - utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, args) + + //move the stage to the a tile position with the lowest X value, mid Y value + + utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, expectedStageXYPositionMicrons) //Once again, validate the position or update - updatePosition = createGUI5() + updatePosition = stageToQuPathAlignmentGUI2() if (updatePosition) { //TODO get access to current stage coordinates - List currentStageCoordinates_um = utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, null) - transformation = utilityFunctions.updateTransformation(transformation, currentStageCoordinates_um, args) + currentStageCoordinates_um = utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, null) + transformation = transformationFunctions.updateTransformation(transformation, expectedStageXYPositionMicrons as List, currentStageCoordinates_um ) + //TODO Make this all an infinite loop if the sample can't be located correctly? } // Additional code for annotations def annotations = getAnnotationObjects().findAll { it.getPathClass() == QP.getPathClass('Tissue') } if (annotations.size() != 1) { - Dialogs.showWarningNotification("Error!", "Can only handle 1 annotation at the moment!"); - return; + 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) + //TODO update TileConfiguration.txt with stage values in microns + logger.info("export script path string $tempTileDirectory") + def tileconfigFolders = transformationFunctions.transformTileConfiguration(tempTileDirectory, transformation) + for (folder in tileconfigFolders){ + logger.info("modified TileConfiguration at $folder") + } // scanTypeWithIndex will be the name of the folder where the tiles will be saved to - args = [pythonScriptPath, + def args = [pythonScriptPath, projectsFolderPath, sampleLabel, - scanTypeWithIndex, - boundingBox] + scanTypeWithIndex] //TODO can we create non-blocking python code utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, args) @@ -549,13 +575,24 @@ class QP_scope_GUI { } } + + private static Dialog createMacroImageInputDialog() { + def dlg = new Dialog() + dlg.initModality(Modality.APPLICATION_MODAL) + dlg.setTitle("Macro View Configuration") + dlg.setHeaderText("Configure settings for macro view.") + dlg.getDialogPane().setContent(createMacroImageInputGUI()) + dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL) + return dlg + } + // Helper method to check if input is numeric private static boolean isValidInput(String input) { - return input.matches("\\d*"); + return input.matches("\\d*") } - private static GridPane createContent3() { + private static GridPane createMacroImageInputGUI() { GridPane pane = new GridPane() pane.setHgap(10) pane.setVgap(10) @@ -568,12 +605,12 @@ class QP_scope_GUI { addToGrid(pane, new Label('PycroManager control file:'), pythonScriptField, row++) addToGrid(pane, new Label('Projects path:'), projectsFolderField, row++) - //addToGrid(pane, new Label('Slide flipped:'), slideFlippedCheckBox, row++) + addToGrid(pane, new Label('Slide flipped:'), slideFlippedCheckBox, row++) addToGrid(pane, new Label('Tissue detection script:'), 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 um:'), pixelSizeField, nonIsotropicCheckBox); - addToGrid(pane, pixelSizeBox, row++); + HBox pixelSizeBox = new HBox(10) + pixelSizeBox.getChildren().addAll(new Label('Pixel Size XY um:'), 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 @@ -585,56 +622,56 @@ class QP_scope_GUI { return pane } - static boolean createGUI4() { - Dialog dlg = new Dialog<>(); - dlg.initModality(Modality.NONE); - dlg.setTitle("Identify Location"); - dlg.setHeaderText("Please identify a location of interest in the Live view in uManager and draw an unclassified rectangle in QuPath that matches that FOV.\n This will be used for matching QuPath's coordinate system to the microscope stage coordinate system, so be as careful as you can!"); + static boolean stageToQuPathAlignmentGUI1() { + Dialog dlg = new Dialog<>() + dlg.initModality(Modality.NONE) + dlg.setTitle("Identify Location") + dlg.setHeaderText("Please identify a location of interest in the Live view in uManager and draw an unclassified rectangle in QuPath that matches that FOV.\n This will be used for matching QuPath's coordinate system to the microscope stage coordinate system, so be as careful as you can!") // Add buttons to the dialog - dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL) - Optional result; - boolean validRectangle = false; + Optional result + boolean validRectangle = false while (!validRectangle) { // Show the dialog and wait for the user response - result = dlg.showAndWait(); + result = dlg.showAndWait() if (result.isPresent() && result.get() == ButtonType.OK) { // Check for expected rectangle List expectedRectangles = getAnnotationObjects().stream() .filter(a -> a.getPathClass() == null && a.getROI() instanceof qupath.lib.roi.RectangleROI) - .collect(Collectors.toList()); + .collect(Collectors.toList()) if (expectedRectangles.size() != 1) { // Use utilityFunctions to show a warning - utilityFunctions.showAlertDialog("There needs to be exactly one unclassified rectangle."); + minorFunctions.showAlertDialog("There needs to be exactly one unclassified rectangle.") } else { - validRectangle = true; + validRectangle = true } } else { // User cancelled or closed the dialog - return false; + return false } } return true } - static boolean createGUI5() { - List choices = Arrays.asList("Yes", "Use adjusted position"); - ChoiceDialog dialog = new ChoiceDialog<>("Yes", choices); - dialog.initModality(Modality.NONE); - dialog.setTitle("Position Confirmation"); - dialog.setHeaderText("Is the current position accurate? Compare with the uManager live view!\n The first time this dialog shows up, it should select the center of the top row! \n The second time, it should select the center of the left-most column!"); + static boolean stageToQuPathAlignmentGUI2() { + List choices = Arrays.asList("Yes", "Use adjusted position") + ChoiceDialog dialog = new ChoiceDialog<>("Yes", choices) + dialog.initModality(Modality.NONE) + dialog.setTitle("Position Confirmation") + dialog.setHeaderText("Is the current position accurate? Compare with the uManager live view!\n The first time this dialog shows up, it should select the center of the top row! \n The second time, it should select the center of the left-most column!") - Optional result = dialog.showAndWait(); + Optional result = dialog.showAndWait() if (result.isPresent()) { - return "Use adjusted position".equals(result.get()); + return "Use adjusted position".equals(result.get()) } // If no choice is made (e.g., dialog is closed), you can decide to return false or handle it differently - return false; + return false } } diff --git a/src/main/groovy/qupath/ext/qp_scope/utilities/minorFunctions.groovy b/src/main/groovy/qupath/ext/qp_scope/utilities/minorFunctions.groovy new file mode 100644 index 0000000..3100a34 --- /dev/null +++ b/src/main/groovy/qupath/ext/qp_scope/utilities/minorFunctions.groovy @@ -0,0 +1,106 @@ +package qupath.ext.qp_scope.utilities + +import javafx.scene.control.Alert +import javafx.stage.Modality +import org.slf4j.LoggerFactory + +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 + +class minorFunctions { + static final logger = LoggerFactory.getLogger(minorFunctions.class) + + static void showAlertDialog(String message) { + Alert alert = new Alert(Alert.AlertType.WARNING) + alert.setTitle("Warning!") + alert.setHeaderText(null) + alert.setContentText(message) + + // This line makes the alert a modal dialog + alert.initModality(Modality.APPLICATION_MODAL) + + alert.showAndWait() + } + /** + * Generates a unique folder name by checking the number of existing folders with a similar name + * in the current directory, and then appending that number to the folder name. + * The naming starts with _1 and increments for each additional folder with a similar base name. + * + * @param originalFolderPath The original folder path. + * @return A unique folder name. + */ + static String getUniqueFolderName(String originalFolderPath) { + Path path = Paths.get(originalFolderPath) + Path parentDir = path.getParent() + String baseName = path.getFileName().toString() + + int counter = 1 + Path newPath = parentDir.resolve(baseName + "_" + counter) + + // Check for existing folders with the same base name and increment counter + while (Files.exists(newPath)) { + counter++ + newPath = parentDir.resolve(baseName + "_" + counter) + } + + // Return only the unique folder name, not the full path + return newPath.getFileName().toString() + } + + private static int getNextImagingModalityIndex(String baseDirectoryPath, String firstScanType) { + File directory = new File(baseDirectoryPath) + if (!directory.exists() || !directory.isDirectory()) { + return 1 // If directory doesn't exist or isn't a directory, start with index 1 + } + + // Filter directories that match the pattern and find the highest index + int maxIndex = Arrays.stream(directory.listFiles()) + .filter(File::isDirectory) + .map(File::getName) + .filter(name -> name.startsWith(firstScanType + "_")) + .map(name -> { + try { + return Integer.parseInt(name.substring(name.lastIndexOf('_') + 1)) + } catch (NumberFormatException e) { + return 0 // If the part after '_' is not a number, return 0 + } + }) + .max(Integer::compare) + .orElse(0) // If no matching directories, start with index 1 + + return maxIndex + 1 // Increment the index for the next modality + } + /** + * 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. + */ + 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 + } + } + static double parseDoubleSafely(String str) { + try { + return str?.trim()?.toDouble() ?: 0.0 + } catch (NumberFormatException e) { + logger.error("NumberFormatException in parsing string to double: ${e.message}") + return 0.0 + } + } + +} diff --git a/src/main/groovy/qupath/ext/qp_scope/utilities/transformationFunctions.groovy b/src/main/groovy/qupath/ext/qp_scope/utilities/transformationFunctions.groovy new file mode 100644 index 0000000..3f3752d --- /dev/null +++ b/src/main/groovy/qupath/ext/qp_scope/utilities/transformationFunctions.groovy @@ -0,0 +1,177 @@ +package qupath.ext.qp_scope.utilities + +import org.slf4j.LoggerFactory +import qupath.lib.objects.PathObject + +import java.awt.geom.AffineTransform +import java.awt.geom.Point2D +import java.util.regex.Matcher +import java.util.regex.Pattern + +class transformationFunctions { + static final logger = LoggerFactory.getLogger(transformationFunctions.class) + + //Convert the QuPath pixel based coordinates for a location into the MicroManager micron based stage coordinates + + static List QPtoMicroscopeCoordinates(List qpCoordinates, AffineTransform transformation) { + Point2D.Double sourcePoint = new Point2D.Double(qpCoordinates[0], qpCoordinates[1]) + Point2D.Double destPoint = new Point2D.Double() + + transformation.transform(sourcePoint, destPoint) + + return [destPoint.x, destPoint.y] + } + +/** + * Transforms the coordinates in TileConfiguration.txt files located in all child directories + * of a specified parent directory, using an AffineTransform. It reads each file, applies the + * transformation to each tile's coordinates, and writes the transformed coordinates back to a + * new file in each directory. + * + * @param parentDirPath The path to the parent directory containing child directories with TileConfiguration.txt files. + * @param transformation The AffineTransform to be applied to each tile's coordinates. + * @return A list of folder names that contain TileConfiguration.txt files which were modified. + */ + static List transformTileConfiguration(String parentDirPath, AffineTransform transformation) { + logger.info("entering transform Tileconfiguration modification function") + logger.info(parentDirPath) + logger.info(transformation.toString()) + System.out.println("AffineTransform: " + transformation) + + File parentDir = new File(parentDirPath) + List modifiedFolders = [] + + // Check if the path is a valid directory + if (!parentDir.isDirectory()) { + System.err.println("Provided path is not a directory: $parentDirPath") + return modifiedFolders + } + + // Iterate over all child folders + File[] subdirectories = parentDir.listFiles(new FileFilter() { + @Override + boolean accept(File file) { + return file.isDirectory() + } + }) + + if (subdirectories) { + subdirectories.each { File subdir -> + File tileConfigFile = new File(subdir, "TileConfiguration.txt") + if (tileConfigFile.exists()) { + // Process the TileConfiguration.txt file + processTileConfigurationFile(tileConfigFile, transformation) + modifiedFolders.add(subdir.name) + } + } + } + + return modifiedFolders + } + + private static void processTileConfigurationFile(File tileConfigFile, AffineTransform transformation) { + List transformedLines = [] + Pattern pattern = Pattern.compile("\\d+\\.tif; ; \\((.*),\\s*(.*)\\)") + + tileConfigFile.eachLine { line -> + Matcher m = pattern.matcher(line) + if (m.find()) { // Use 'find()' to search for a match in the line + double x1 = Double.parseDouble(m.group(1)) + double y1 = Double.parseDouble(m.group(2)) + List qpCoordinates = [x1, y1] + List transformedCoords = QPtoMicroscopeCoordinates(qpCoordinates, transformation) + transformedLines.add(line.replaceFirst("\\(.*\\)", "(${transformedCoords[0]}, ${transformedCoords[1]})")) + } else { + transformedLines.add(line) // Add line as is if no coordinate match + } + } + + + // Write the transformed lines to a new file + File newTileConfigFile = new File(tileConfigFile.getParent(), "TileConfiguration_transformed.txt") + newTileConfigFile.withWriter { writer -> + transformedLines.each { writer.println(it) } + } + } + +/** + * Updates an AffineTransform based on the difference between coordinates in QPath and microscope stage. + * It applies the existing transformation to the QPath coordinates and then adjusts the transformation + * to align these with the given microscope stage coordinates. + * + * @param transformation The current AffineTransform object. + * @param coordinatesQP List of QPath coordinates (as Strings) to be transformed. + * @param coordinatesMM List of microscope stage coordinates (as Strings) for alignment. + * @return An updated AffineTransform object that reflects the necessary shift to align QPath coordinates + * with microscope stage coordinates after scaling. + */ +//TODO adjust for situations where the macro image is flipped + static AffineTransform updateTransformation(AffineTransform transformation, List coordinatesQP, List coordinatesMM) { + // Convert coordinatesQP and coordinatesMM elements from String to Double + double xQP = coordinatesQP[0].toDouble() + double yQP = coordinatesQP[1].toDouble() + double xMM = coordinatesMM[0].toDouble() + double yMM = coordinatesMM[1].toDouble() + + // Apply the existing transformation to the QP coordinates + Point2D.Double transformedPoint = new Point2D.Double() + transformation.transform(new Point2D.Double(xQP, yQP), transformedPoint) + + // Calculate the additional translation needed + double additionalXShift = xMM - transformedPoint.x + double additionalYShift = yMM - transformedPoint.y + + logger.info("Additional xShift: $additionalXShift") + logger.info("Additional yShift: $additionalYShift") + + // Create a new AffineTransform that includes this additional translation + AffineTransform updatedTransformation = new AffineTransform(transformation) + updatedTransformation.translate(additionalXShift, additionalYShift) + + return updatedTransformation + } + + + static List getTopCenterTile(Collection detections) { + // Filter out null detections and sort by Y-coordinate + List sortedDetections = detections.findAll { it != null } + .sort { it.getROI().getCentroidY() } + + // Get the minimum Y-coordinate (top tiles) + double minY = sortedDetections.first().getROI().getCentroidY() + + // Get all tiles that are at the top + List topTiles = sortedDetections.findAll { it.getROI().getCentroidY() == minY } + + // Find the median X-coordinate of the top tiles + List xCoordinates = topTiles.collect { it.getROI().getCentroidX() } + double medianX = xCoordinates.sort()[xCoordinates.size() / 2] + + // Select the top tile closest to the median X-coordinate + PathObject topCenterTile = topTiles.min { Math.abs(it.getROI().getCentroidX() - medianX) } + + return [topCenterTile.getROI().getCentroidX(), topCenterTile.getROI().getCentroidY(), topCenterTile] + } + + static List getLeftCenterTile(Collection detections) { + // Filter out null detections and sort by X-coordinate + List sortedDetections = detections.findAll { it != null } + .sort { it.getROI().getCentroidX() } + + // Get the minimum X-coordinate (left tiles) + double minX = sortedDetections.first().getROI().getCentroidX() + + // Get all tiles that are at the left + List leftTiles = sortedDetections.findAll { it.getROI().getCentroidX() == minX } + + // Find the median Y-coordinate of the left tiles + List yCoordinates = leftTiles.collect { it.getROI().getCentroidY() } + double medianY = yCoordinates.sort()[yCoordinates.size() / 2] + + // Select the left tile closest to the median Y-coordinate + PathObject leftCenterTile = leftTiles.min { Math.abs(it.getROI().getCentroidY() - medianY) } + + return [leftCenterTile.getROI().getCentroidX(), leftCenterTile.getROI().getCentroidY(), leftCenterTile] + } + +} 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 c9f3c36..6bf4169 100644 --- a/src/main/groovy/qupath/ext/qp_scope/utilities/utilityFunctions.groovy +++ b/src/main/groovy/qupath/ext/qp_scope/utilities/utilityFunctions.groovy @@ -1,18 +1,15 @@ package qupath.ext.qp_scope.utilities -import javafx.scene.control.Alert -import javafx.stage.Modality + import org.slf4j.LoggerFactory import qupath.lib.gui.commands.ProjectCommands import qupath.lib.gui.dialogs.Dialogs import qupath.lib.images.ImageData import qupath.lib.images.servers.ImageServerProvider -import qupath.lib.objects.PathObject import qupath.lib.projects.Project import qupath.lib.projects.ProjectIO import qupath.lib.projects.Projects -import qupath.lib.scripting.QP - +import qupath.ext.qp_scope.utilities.minorFunctions import java.awt.image.BufferedImage import java.nio.charset.StandardCharsets import java.nio.file.Files @@ -22,24 +19,12 @@ import java.util.regex.Matcher import java.util.regex.Pattern import java.util.stream.Collectors import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream; - +import java.util.zip.ZipOutputStream class utilityFunctions { static final logger = LoggerFactory.getLogger(utilityFunctions.class) - static void showAlertDialog(String message) { - Alert alert = new Alert(Alert.AlertType.WARNING); - alert.setTitle("Warning!"); - alert.setHeaderText(null); - alert.setContentText(message); - - // This line makes the alert a modal dialog - alert.initModality(Modality.APPLICATION_MODAL); - - alert.showAndWait(); - } static boolean addImageToProject(File stitchedImagePath, Project project) { @@ -69,13 +54,13 @@ class utilityFunctions { entry.saveImageData(imageData) // Write a thumbnail if we can - var img = ProjectCommands.getThumbnailRGB(imageData.getServer()); + var img = ProjectCommands.getThumbnailRGB(imageData.getServer()) entry.setThumbnail(img) // Add an entry name (the filename) entry.setImageName(stitchedImagePath.getName()) project.syncChanges() - return true; + return true } @@ -114,8 +99,8 @@ class utilityFunctions { Dialogs.showWarningNotification("Warning!", "Project is null!") } // Within projectsFolderPath, check for a folder with the name "SlideImages", if it does not exist, create it - String slideImagesFolderPathStr = projectsFolderPath + File.separator + sampleLabel + File.separator + "SlideImages"; - File slideImagesFolder = new File(slideImagesFolderPathStr); + String slideImagesFolderPathStr = projectsFolderPath + File.separator + sampleLabel + File.separator + "SlideImages" + File slideImagesFolder = new File(slideImagesFolderPathStr) if (!slideImagesFolder.exists()) { slideImagesFolder.mkdirs() @@ -134,18 +119,18 @@ class utilityFunctions { */ static runPythonCommand(String anacondaEnvPath, String pythonScriptPath, List arguments) { try { - String pythonExecutable = new File(anacondaEnvPath, "python.exe").getCanonicalPath(); + String pythonExecutable = new File(anacondaEnvPath, "python.exe").getCanonicalPath() // Adjust the pythonScriptPath based on arguments if (arguments == null) { // Change the script to 'getStageCoordinates.py' - File scriptFile = new File(pythonScriptPath); - pythonScriptPath = new File(scriptFile.getParent(), "getStageCoordinates.py").getCanonicalPath(); + File scriptFile = new File(pythonScriptPath) + pythonScriptPath = new File(scriptFile.getParent(), "getStageCoordinates.py").getCanonicalPath() // Construct the command - String command = "\"" + pythonExecutable + "\" -u \"" + pythonScriptPath + "\" " + arguments; + String command = "\"" + pythonExecutable + "\" -u \"" + pythonScriptPath + "\" " + arguments // Execute the command - Process process = command.execute(); - logger.info("Executing command: " + command); + Process process = command.execute() + logger.info("Executing command: " + command) logger.info("This should get stage coordinates back") List result = handleProcessOutput(process) if (result != null) { @@ -157,18 +142,18 @@ class utilityFunctions { } } else if (arguments.size() == 2) { // Change the script to 'moveStageToCoordinates.py' - File scriptFile = new File(pythonScriptPath); + File scriptFile = new File(pythonScriptPath) pythonScriptPath = new File(scriptFile.parent, "moveStageToCoordinates.py").canonicalPath } - String args = arguments != null ? arguments.collect { "\"$it\"" }.join(' ') : ""; + String args = arguments != null ? arguments.collect { "\"$it\"" }.join(' ') : "" // Construct the command - String command = "\"" + pythonExecutable + "\" -u \"" + pythonScriptPath + "\" " + args; - logger.info("Executing command: " + command); + String command = "\"" + pythonExecutable + "\" -u \"" + pythonScriptPath + "\" " + args + logger.info("Executing command: " + command) // Execute the command - Process process = command.execute(); + Process process = command.execute() // Redirect the output and error streams to the logger process.consumeProcessOutput(new StringWriter(), new StringWriter()) @@ -181,14 +166,15 @@ class utilityFunctions { logger.error(process.err.text) // This logs the standard error return null } catch (Exception e) { - e.printStackTrace(); + e.printStackTrace() } } + static List handleProcessOutput(Process process) { - BufferedReader outputReader = new BufferedReader(new InputStreamReader(process.getInputStream())); - BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + BufferedReader outputReader = new BufferedReader(new InputStreamReader(process.getInputStream())) + BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())) - String line; + String line List outputLines = [] List errorLines = [] String value1 = null @@ -198,9 +184,9 @@ class utilityFunctions { outputLines.add(line) // Assuming coordinates are on the first line if (outputLines.size() == 1) { - String[] values = line.split(" "); - value1 = values[0]; - value2 = values[1]; + String[] values = line.split(" ") + value1 = values[0] + value2 = values[1] } } @@ -210,10 +196,10 @@ class utilityFunctions { // Check for errors or invalid output if (!errorLines.isEmpty() || value1 == null || value2 == null) { - return null; + return null } - return [value1, value2]; + return [value1, value2] } // static void runPythonCommand(String anacondaEnvPath, String pythonScriptPath, List arguments) { @@ -249,52 +235,28 @@ class utilityFunctions { //If preferences are null or missing, throw an error and close //Open to discussion whether scan types should be included here or typed every time, or some other option //TODO fix the installation to be a folder with an expected .py file target? Or keep as .py file target? - return [ pycromanager : "C:\\ImageAnalysis\\QPExtensionTest\\qp_scope\\src\\main\\pythonScripts/4x_bf_scan_pycromanager.py", - environment : "C:\\Anaconda\\envs\\paquo", - projects : "C:\\ImageAnalysis\\QPExtensionTest\\data\\slides", - tissueDetection: "DetectTissue.groovy", - firstScanType : "4x_bf", - secondScanType : "20x_bf", - tileHandling : "Zip", - pixelSizeSource : "7.2", - pixelSizeTarget : "1.105", - frameWidth : "1392", - frameHeight : "1040", - overlapPercent : "0"] //Zip Delete or anything else is ignored + return [pycromanager : "C:\\ImageAnalysis\\QPExtensionTest\\qp_scope\\src\\main\\pythonScripts/4x_bf_scan_pycromanager.py", + environment : "C:\\Anaconda\\envs\\paquo", + projects : "C:\\ImageAnalysis\\QPExtensionTest\\data\\slides", + tissueDetection : "DetectTissue.groovy", + firstScanType : "4x_bf", + secondScanType : "20x_bf", + tileHandling : "Zip", + pixelSizeSource : "7.2", + pixelSizeFirstScanType : "1.105", + pixelSizeSecondScanType: "0.5", + frameWidth : "1392", + frameHeight : "1040", + overlapPercent : "0"] //Zip Delete or anything else is ignored } -/** - * Generates a unique folder name by checking the number of existing folders with a similar name - * in the current directory, and then appending that number to the folder name. - * The naming starts with _1 and increments for each additional folder with a similar base name. - * - * @param originalFolderPath The original folder path. - * @return A unique folder name. - */ - static String getUniqueFolderName(String originalFolderPath) { - Path path = Paths.get(originalFolderPath); - Path parentDir = path.getParent(); - String baseName = path.getFileName().toString(); - - int counter = 1; - Path newPath = parentDir.resolve(baseName + "_" + counter); - - // Check for existing folders with the same base name and increment counter - while (Files.exists(newPath)) { - counter++; - newPath = parentDir.resolve(baseName + "_" + counter); - } - - // Return only the unique folder name, not the full path - return newPath.getFileName().toString(); - } /** * Deletes all the tiles within the provided folder and the folder itself. * * @param folderPath The path to the folder containing the tiles to be deleted. */ - public static void deleteTilesAndFolder(String folderPath) { + static void deleteTilesAndFolder(String folderPath) { try { @@ -318,77 +280,40 @@ class utilityFunctions { } } - public static void zipTilesAndMove(String folderPath) { + static void zipTilesAndMove(String folderPath) { try { - Path directory = Paths.get(folderPath); - Path parentDirectory = directory.getParent(); - Path compressedTilesDir = parentDirectory.resolve("Compressed tiles"); + Path directory = Paths.get(folderPath) + Path parentDirectory = directory.getParent() + Path compressedTilesDir = parentDirectory.resolve("Compressed tiles") // Create "Compressed tiles" directory if it doesn't exist if (!Files.exists(compressedTilesDir)) { - Files.createDirectory(compressedTilesDir); + Files.createDirectory(compressedTilesDir) } // Create a Zip file - String zipFileName = directory.getFileName().toString() + ".zip"; - Path zipFilePath = compressedTilesDir.resolve(zipFileName); + String zipFileName = directory.getFileName().toString() + ".zip" + Path zipFilePath = compressedTilesDir.resolve(zipFileName) try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) { Files.walk(directory) .filter(Files::isRegularFile) .forEach(path -> { - ZipEntry zipEntry = new ZipEntry(directory.relativize(path).toString()); + ZipEntry zipEntry = new ZipEntry(directory.relativize(path).toString()) try { - zos.putNextEntry(zipEntry); - Files.copy(path, zos); - zos.closeEntry(); + zos.putNextEntry(zipEntry) + Files.copy(path, zos) + zos.closeEntry() } catch (IOException ex) { - logger.error("Error adding file to zip: " + path, ex); + logger.error("Error adding file to zip: " + path, ex) } - }); + }) } // Optionally, delete the original tiles and folder // deleteTilesAndFolder(folderPath); } catch (IOException ex) { - logger.error("Error zipping and moving tiles from: " + folderPath, ex); - } - } - - static String transformBoundingBox(double x1, double y1, double x2, double y2, String pixelSize, String xCoordinate, String yCoordinate, boolean flip) { - if (flip) { - logger.info("Handling flip") - } - // Log the values of all input parameters - logger.info("Input Parameters - x1: $x1, y1: $y1, x2: $x2, y2: $y2, pixelSize: $pixelSize, xCoordinate: $xCoordinate, yCoordinate: $yCoordinate, flip: $flip") - - double pixelSizeDouble = parseDoubleSafely(pixelSize) - double xCoordinateDouble = parseDoubleSafely(xCoordinate) - double yCoordinateDouble = parseDoubleSafely(yCoordinate) - - // Convert pixel coordinates to microns - double x1Microns = x1 * pixelSizeDouble - double y1Microns = y1 * pixelSizeDouble - double x2Microns = x2 * pixelSizeDouble - double y2Microns = y2 * pixelSizeDouble - - // Adjust coordinates relative to the upper right coordinates - double adjustedX1 = xCoordinateDouble - x1Microns - double adjustedY1 = yCoordinateDouble - y1Microns - double adjustedX2 = xCoordinateDouble - x2Microns - double adjustedY2 = yCoordinateDouble - y2Microns - - // Create the bounding box string in the format "x1, y1, x2, y2" - String boundingBox = "$adjustedX1, $adjustedY1, $adjustedX2, $adjustedY2" - return boundingBox - } - - static double parseDoubleSafely(String str) { - try { - return str?.trim()?.toDouble() ?: 0.0 - } catch (NumberFormatException e) { - logger.error("NumberFormatException in parsing string to double: ${e.message}") - return 0.0 + logger.error("Error zipping and moving tiles from: " + folderPath, ex) } } @@ -401,21 +326,21 @@ class utilityFunctions { * @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 { + 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 + ")"; + return "setPixelSizeMicrons(" + pixelSize + ", " + pixelSize + ")" } else if (line.startsWith("createAnnotationsFromPixelClassifier")) { - return line.replaceFirst("\"[^\"]*\"", "\"" + jsonFilePathString + "\""); + return line.replaceFirst("\"[^\"]*\"", "\"" + jsonFilePathString + "\"") } else { - return line; + return line } }) - .collect(Collectors.toList()); + .collect(Collectors.toList()) - return String.join(System.lineSeparator(), lines); + return String.join(System.lineSeparator(), lines) } /** @@ -427,41 +352,55 @@ class utilityFunctions { * @return String representing the modified script. * @throws IOException if an I/O error occurs reading from the file. */ - public static String modifyTXTExportScript(String exportScriptPathString, String pixelSize, Map preferences, String sampleLabel) throws IOException { - // Read and modify the script + static String modifyTXTExportScript(String exportScriptPathString, String pixelSize, Map preferences, String sampleLabel) throws IOException { + // Access necessary folder locations to ensure Groovy script saves files correctly + String baseDirectoryPath = "${preferences.projects}${File.separator}${sampleLabel}".replace("\\", "\\\\") + // Handle backslashes for Windows paths + String imagingBasePath = "${baseDirectoryPath}${File.separator}${preferences.firstScanType}" + + String uniqueFolderName = minorFunctions.getUniqueFolderName(imagingBasePath) + + // Extract the numeric part from the folder name using a regex pattern + Pattern pattern = Pattern.compile('(\\d+)$') // Using single quotes for regex + Matcher matcher = pattern.matcher(uniqueFolderName) + String numericPart = matcher.find() ? matcher.group(1) : '1' // Default to '1' if no numeric part is found + + + String imagingModalityValue = "${preferences.firstScanType}_${numericPart}" + List lines = Files.lines(Paths.get(exportScriptPathString), StandardCharsets.UTF_8) .map(line -> { if (line.startsWith("double pixelSizeSource")) { - return "double pixelSizeSource = " + pixelSize + ";"; + return "double pixelSizeSource = " + pixelSize + ";" } else if (line.startsWith("double pixelSizeTarget")) { - return "double pixelSizeTarget = " + preferences.pixelSizeTarget + ";"; + return "double pixelSizeTarget = " + preferences.pixelSizeFirstScanType + ";" } else if (line.startsWith("double frameWidth")) { - double frameWidth = Double.parseDouble(preferences.frameWidth) / Double.parseDouble(preferences.pixelSizeSource) * Double.parseDouble(preferences.pixelSizeTarget); - return "double frameWidth = " + frameWidth + ";"; + double frameWidth = Double.parseDouble(preferences.frameWidth) / Double.parseDouble(pixelSize) * Double.parseDouble(preferences.pixelSizeFirstScanType) + return "double frameWidth = " + frameWidth + ";" } else if (line.startsWith("double frameHeight")) { - double frameHeight = Double.parseDouble(preferences.frameHeight) / Double.parseDouble(preferences.pixelSizeSource) * Double.parseDouble(preferences.pixelSizeTarget); - return "double frameHeight = " + frameHeight + ";"; + double frameHeight = Double.parseDouble(preferences.frameHeight) / Double.parseDouble(pixelSize) * Double.parseDouble(preferences.pixelSizeFirstScanType) + return "double frameHeight = " + frameHeight + ";" } else if (line.startsWith("double overlapPercent")) { - return "double overlapPercent = " + preferences.overlapPercent + ";"; + return "double overlapPercent = " + preferences.overlapPercent + ";" } else if (line.startsWith("baseDirectory")) { - String pathSeparator = System.getProperty("file.separator"); - String baseDirectoryPath = preferences.projects + pathSeparator + sampleLabel; - baseDirectoryPath = baseDirectoryPath.replace("\\", "\\\\"); // Handle backslashes for Windows paths - String newLine = "baseDirectory = \"" + baseDirectoryPath + "\";"; - logger.info("Replacing baseDirectory line with: " + newLine); - return newLine; + + String newLine = "baseDirectory = \"" + baseDirectoryPath + "\";" + logger.info("Replacing baseDirectory line with: " + newLine) + return newLine } else if (line.startsWith("imagingModality")) { - return "imagingModality = \"" + preferences.firstScanType + "-tiles\";"; + + return "imagingModality = \"" + imagingModalityValue + "\";" } else { - return line; + return line } }) - .collect(Collectors.toList()); + .collect(Collectors.toList()) // Join the lines into a single string - return String.join(System.lineSeparator(), lines); + return String.join(System.lineSeparator(), lines) } + /** * Modifies a Groovy script content by setting the 'createTiles' variable to false and updating * the 'boundingBoxStageCoordinates_um' variable with provided bounding box values. @@ -470,7 +409,7 @@ class utilityFunctions { * @param boundingBox A list containing the bounding box coordinates (x1, y1, x2, y2). * @return A string representing the modified script content. */ - public static String boundingBoxReadyTXT(String scriptContent, List boundingBox) { + static String boundingBoxReadyTXT(String scriptContent, List boundingBox) { // Convert bounding box list to a string String boundingBoxStr = boundingBox.join(", ") @@ -481,136 +420,17 @@ class utilityFunctions { List modifiedLines = lines.stream() .map(line -> { if (line.trim().startsWith("createTiles")) { - return "createTiles = false"; + return "createTiles = false" } else if (line.trim().startsWith("boundingBoxStageCoordinates_um")) { - return "boundingBoxStageCoordinates_um = [" + boundingBoxStr + "]"; + return "boundingBoxStageCoordinates_um = [" + boundingBoxStr + "]" } else { - return line; + return line } }) - .collect(Collectors.toList()); + .collect(Collectors.toList()) // Join the modified lines into a single string - return String.join(System.lineSeparator(), modifiedLines); + return String.join(System.lineSeparator(), modifiedLines) } - - - /** - * 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 - } - } - - static List getTopCenterTile(Collection detections) { - // Filter out null detections and sort by Y-coordinate - List sortedDetections = detections.findAll { it != null } - .sort { it.getROI().getCentroidY() } - - // Get the minimum Y-coordinate (top tiles) - double minY = sortedDetections.first().getROI().getCentroidY() - - // Get all tiles that are at the top - List topTiles = sortedDetections.findAll { it.getROI().getCentroidY() == minY } - - // Find the median X-coordinate of the top tiles - List xCoordinates = topTiles.collect { it.getROI().getCentroidX() } - double medianX = xCoordinates.sort()[xCoordinates.size() / 2] - - // Select the top tile closest to the median X-coordinate - PathObject topCenterTile = topTiles.min { Math.abs(it.getROI().getCentroidX() - medianX) } - - return [topCenterTile.getROI().getCentroidX(), topCenterTile.getROI().getCentroidY(), topCenterTile] - } - - static List getLeftCenterTile(Collection detections) { - // Filter out null detections and sort by X-coordinate - List sortedDetections = detections.findAll { it != null } - .sort { it.getROI().getCentroidX() } - - // Get the minimum X-coordinate (left tiles) - double minX = sortedDetections.first().getROI().getCentroidX() - - // Get all tiles that are at the left - List leftTiles = sortedDetections.findAll { it.getROI().getCentroidX() == minX } - - // Find the median Y-coordinate of the left tiles - List yCoordinates = leftTiles.collect { it.getROI().getCentroidY() } - double medianY = yCoordinates.sort()[yCoordinates.size() / 2] - - // Select the left tile closest to the median Y-coordinate - PathObject leftCenterTile = leftTiles.min { Math.abs(it.getROI().getCentroidY() - medianY) } - - return [leftCenterTile.getROI().getCentroidX(), leftCenterTile.getROI().getCentroidY(), leftCenterTile] - } - - //TODO possibly use QuPath's affine transformation tools - //Convert the QuPath pixel based coordinates for a location into the MicroManager micron based stage coordinates - static List QPtoMicroscopeCoordinates(List qpCoordinates, Double imagePixelSize, Object transformation) { - //TODO figure out conversion - def xUpperLeft = qpCoordinates[0]*imagePixelSize - def yUpperLeft = qpCoordinates[1]*imagePixelSize - - - def mmCoordinates = qpCoordinates - return mmCoordinates - } - - - static List updateTransformation(List transformation, List coordinatesQP, List coordinatesMM) { - logger.info("Transformation input: $transformation (Type: ${transformation.getClass()})") - logger.info("Coordinates QP input: $coordinatesQP (Type: ${coordinatesQP.getClass()})") - logger.info("Coordinates MM input: $coordinatesMM (Type: ${coordinatesMM.getClass()})") - - // Extract transformation elements - double xShiftMicrons = transformation[0] - double yShiftMicrons = transformation[1] - double pixelSize = transformation[2] - - logger.info("Extracted xShiftMicrons: $xShiftMicrons") - logger.info("Extracted yShiftMicrons: $yShiftMicrons") - logger.info("Extracted pixelSize: $pixelSize") - - // Convert coordinatesQP and coordinatesMM elements from String to Double - double xQP = coordinatesQP[0].toDouble() - double yQP = coordinatesQP[1].toDouble() - double xMM = coordinatesMM[0].toDouble() - double yMM = coordinatesMM[1].toDouble() - - logger.info("Converted xQP from String to Double: $xQP") - logger.info("Converted yQP from String to Double: $yQP") - logger.info("Converted xMM from String to Double: $xMM") - logger.info("Converted yMM from String to Double: $yMM") - - // Calculate coordinate shift - double xShift = xQP * pixelSize - xMM - double yShift = yQP * pixelSize - yMM - - logger.info("Calculated xShift: $xShift") - logger.info("Calculated yShift: $yShift") - - // Update transformation values - transformation[0] = xShiftMicrons - xShift - transformation[1] = yShiftMicrons - yShift - - logger.info("Updated transformation: $transformation") - - return transformation - } } \ No newline at end of file diff --git a/src/main/groovyScripts/DetectTissue.groovy b/src/main/groovyScripts/DetectTissue.groovy index ca669d3..78e548a 100644 --- a/src/main/groovyScripts/DetectTissue.groovy +++ b/src/main/groovyScripts/DetectTissue.groovy @@ -1,5 +1,5 @@ -setImageType('BRIGHTFIELD_H_E'); -setColorDeconvolutionStains('{"Name" : "H&E default", "Stain 1" : "Hematoxylin", "Values 1" : "0.65111 0.70119 0.29049", "Stain 2" : "Eosin", "Values 2" : "0.2159 0.8012 0.5581", "Background" : " 255 255 255"}'); +setImageType('BRIGHTFIELD_H_E') +setColorDeconvolutionStains('{"Name" : "H&E default", "Stain 1" : "Hematoxylin", "Values 1" : "0.65111 0.70119 0.29049", "Stain 2" : "Eosin", "Values 2" : "0.2159 0.8012 0.5581", "Background" : " 255 255 255"}') //Set pixel size setPixelSizeMicrons(2.0, 2.0) //createFullImageAnnotation(true) diff --git a/src/main/groovyScripts/save4xMacroTiling.groovy b/src/main/groovyScripts/save4xMacroTiling.groovy index debdf11..281f269 100644 --- a/src/main/groovyScripts/save4xMacroTiling.groovy +++ b/src/main/groovyScripts/save4xMacroTiling.groovy @@ -16,10 +16,6 @@ double overlapPercent = 10 baseDirectory = "to be replaced" imagingModality = "4x-bf" -//Potentially store tiles as they are created -newTiles = [] - - //Ensure the folder to store the csv exists tilePath = buildFilePath(baseDirectory, imagingModality) mkdirs(tilePath) diff --git a/src/main/groovyScripts/saveTilingCSV.groovy b/src/main/groovyScripts/saveTilingCSV.groovy index 4933818..6b427d6 100644 --- a/src/main/groovyScripts/saveTilingCSV.groovy +++ b/src/main/groovyScripts/saveTilingCSV.groovy @@ -41,13 +41,13 @@ tilePath = buildFilePath(baseDirectory, "20x-tiles") mkdirs(tilePath) //CSV will be only two columns with the following header -String header = "x_pos,y_pos"; +String header = "x_pos,y_pos" annotations.eachWithIndex { a, i -> - predictedTileCount = 0; //Numbering tiles based on the tiles that would have been created from the bounding box - actualTileCount = 0; //Tile objects created and saved to CSV - tiles not overlapping the annotation are excluded - xy = []; + predictedTileCount = 0 //Numbering tiles based on the tiles that would have been created from the bounding box + actualTileCount = 0 //Tile objects created and saved to CSV - tiles not overlapping the annotation are excluded + xy = [] yline = 0 roiA = a.getROI() //generate a bounding box to create tiles within @@ -133,9 +133,9 @@ tilePath = buildFilePath(baseDirectory, "mp-tiles") mkdirs(tilePath) annotations.eachWithIndex { a, i -> - predictedTileCount = 0; //Numbering tiles based on the tiles that would have been created from the bounding box - actualTileCount = 0; //Tile objects created and saved to CSV - tiles not overlapping the annotation are excluded - xy = []; + predictedTileCount = 0 //Numbering tiles based on the tiles that would have been created from the bounding box + actualTileCount = 0 //Tile objects created and saved to CSV - tiles not overlapping the annotation are excluded + xy = [] yline = 0 roiA = a.getROI() //generate a bounding box to create tiles within @@ -156,7 +156,7 @@ annotations.eachWithIndex { a, i -> newAnno.getMeasurementList().putMeasurement("TileNumber", actualTileCount) newTiles << newAnno xy << [x, y] - actualTileCount++; + actualTileCount++ //print predictedTileCount + " good "+x } else { print x @@ -199,4 +199,4 @@ import qupath.lib.regions.ImagePlane import qupath.lib.roi.RectangleROI import static qupath.lib.gui.scripting.QPEx.getQuPath -import static qupath.lib.scripting.QP.*; \ No newline at end of file +import static qupath.lib.scripting.QP.* \ No newline at end of file