diff --git a/.gradle/7.5.1/checksums/checksums.lock b/.gradle/7.5.1/checksums/checksums.lock index 4f61e64..2ce2221 100644 Binary files a/.gradle/7.5.1/checksums/checksums.lock and b/.gradle/7.5.1/checksums/checksums.lock differ diff --git a/.gradle/7.5.1/dependencies-accessors/dependencies-accessors.lock b/.gradle/7.5.1/dependencies-accessors/dependencies-accessors.lock index 38de2b5..88271a0 100644 Binary files a/.gradle/7.5.1/dependencies-accessors/dependencies-accessors.lock and b/.gradle/7.5.1/dependencies-accessors/dependencies-accessors.lock differ diff --git a/.gradle/7.5.1/dependencies-accessors/executionHistory.bin b/.gradle/7.5.1/dependencies-accessors/executionHistory.bin index 9031165..35bbc12 100644 Binary files a/.gradle/7.5.1/dependencies-accessors/executionHistory.bin and b/.gradle/7.5.1/dependencies-accessors/executionHistory.bin differ diff --git a/.gradle/7.5.1/executionHistory/executionHistory.bin b/.gradle/7.5.1/executionHistory/executionHistory.bin index 6d277cf..5610627 100644 Binary files a/.gradle/7.5.1/executionHistory/executionHistory.bin and b/.gradle/7.5.1/executionHistory/executionHistory.bin differ diff --git a/.gradle/7.5.1/executionHistory/executionHistory.lock b/.gradle/7.5.1/executionHistory/executionHistory.lock index b9b327b..bb4c412 100644 Binary files a/.gradle/7.5.1/executionHistory/executionHistory.lock and b/.gradle/7.5.1/executionHistory/executionHistory.lock differ diff --git a/.gradle/7.5.1/fileHashes/fileHashes.bin b/.gradle/7.5.1/fileHashes/fileHashes.bin index 63b3be7..297ee42 100644 Binary files a/.gradle/7.5.1/fileHashes/fileHashes.bin and b/.gradle/7.5.1/fileHashes/fileHashes.bin differ diff --git a/.gradle/7.5.1/fileHashes/fileHashes.lock b/.gradle/7.5.1/fileHashes/fileHashes.lock index 9ed9c00..45d719b 100644 Binary files a/.gradle/7.5.1/fileHashes/fileHashes.lock and b/.gradle/7.5.1/fileHashes/fileHashes.lock differ diff --git a/.gradle/7.5.1/fileHashes/resourceHashesCache.bin b/.gradle/7.5.1/fileHashes/resourceHashesCache.bin index 0e686c9..a6d4707 100644 Binary files a/.gradle/7.5.1/fileHashes/resourceHashesCache.bin and b/.gradle/7.5.1/fileHashes/resourceHashesCache.bin differ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index ac6128a..4874c36 100644 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe index 80cfcaa..5540f3f 100644 Binary files a/.gradle/file-system.probe and b/.gradle/file-system.probe differ 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 cc1acc4..9ba74e6 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 @@ -1,13 +1,16 @@ package qupath.ext.qp_scope.functions import javafx.scene.Node + import javafx.scene.control.ButtonType import javafx.scene.control.CheckBox +import javafx.scene.control.ChoiceDialog 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 @@ -21,12 +24,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.getAnnotationObjects import static qupath.lib.scripting.QP.project //Thoughts: @@ -51,8 +53,8 @@ class QP_scope_GUI { 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 TextField groovyScriptField = new TextField("C:\\ImageAnalysis\\QPExtensionTest\\qp_scope\\src\\main\\groovyScripts/DetectTissue.groovy") // Default empty + static TextField pixelSizeField = new TextField("7.2") // Default empty static CheckBox nonIsotropicCheckBox = new CheckBox("Non-isotropic pixels") static void createGUI1() { @@ -141,7 +143,7 @@ class QP_scope_GUI { //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) + String stitchedImagePathStr = stitchingImplementations.stitchCore("Coordinates in TileConfiguration.txt file", projectsFolderPath + File.separator + sampleLabel, stitchedImageOutputFolder, "J2K_LOSSY", 0, 1, scanTypeWithIndex) //utilityFunctions.showAlertDialog("Wait and complete stitching in other version of QuPath") @@ -195,8 +197,8 @@ class QP_scope_GUI { // 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('PycroManager .py path:'), pythonScriptField, row++) + addToGrid(pane, new Label('Projects parent folder:'), projectsFolderField, row++) // Listener for the checkbox useAnnotationsCheckBox.selectedProperty().addListener((observable, oldValue, newValue) -> { x1Field.setDisable(newValue) @@ -279,7 +281,7 @@ class QP_scope_GUI { //stitchingImplementations.stitchCore(stitchingType, folderPath, compressionType, pixelSize, downsample, matchingString) logger.info("Begin stitching") - String stitchedImagePathStr =stitchingImplementations.stitchCore("Coordinates in TileCoordinates.txt file", projectsFolderPath + File.separator + sampleLabel, stitchedImageOutputFolder, "J2K_LOSSY", 0, 1, scanTypeWithIndex) + String stitchedImagePathStr =stitchingImplementations.stitchCore("Coordinates in TileConfiguration.txt file", projectsFolderPath + File.separator + sampleLabel, stitchedImageOutputFolder, "J2K_LOSSY", 0, 1, scanTypeWithIndex) logger.info("Get project") Project currentQuPathProject = getProject() @@ -329,7 +331,7 @@ class QP_scope_GUI { // 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('PycroManager .py file path:'), pythonScriptField, row++) addToGrid(pane, new Label('Projects path:'), projectsFolderField, row++) // Listener for the checkbox @@ -378,7 +380,7 @@ class QP_scope_GUI { def projectsFolderPath = projectsFolderField.getText() // Check if data is all present - if ([xCoordinate, yCoordinate, pixelSize, groovyScriptPath].any { it == null || it.isEmpty() }) { + if ([pixelSize, groovyScriptPath].any { it == null || it.isEmpty() }) { Dialogs.showWarningNotification("Warning!", "Insufficient data to send command to microscope!") return } @@ -433,7 +435,6 @@ class QP_scope_GUI { //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() @@ -451,8 +452,50 @@ class QP_scope_GUI { logger.info(exportScriptPathString) 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(); + if (!gui4Success) { + // User cancelled GUI4, so end GUI3 and do not proceed + return; + } + // Execute Python command to move stage + def detections = QP.getDetectionObjects() + def topCenterTileXY = utilityFunctions.getTopCenterTile(detections) + QP.selectObjects(topCenterTileXY[2]) + List args = [topCenterTileXY[0], topCenterTileXY[1]] + 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) + //Validate the position that was moved to or update with an adjusted position + boolean updatePosition = createGUI5() + + if (updatePosition) { + //TODO get access to current stage coordinates + List currentStageCoordinates_um = utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, null) + transformation = utilityFunctions.updateTransformation(transformation, currentStageCoordinates_um, args) + } + + def leftCenterTileXY = utilityFunctions.getLeftCenterTile(detections) + QP.selectObjects(leftCenterTileXY[2]) + args = [leftCenterTileXY[0], leftCenterTileXY[1]] + 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) + //Once again, validate the position or update + updatePosition = createGUI5() + if (updatePosition) { + //TODO get access to current stage coordinates + + List currentStageCoordinates_um = utilityFunctions.runPythonCommand(virtualEnvPath, pythonScriptPath, null) + transformation = utilityFunctions.updateTransformation(transformation, currentStageCoordinates_um, args) + } + // Additional code for annotations - def annotations = QP.getAnnotationObjects(); + 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; @@ -473,7 +516,7 @@ class QP_scope_GUI { // scanTypeWithIndex will be the name of the folder where the tiles will be saved to - List args = [pythonScriptPath, + args = [pythonScriptPath, projectsFolderPath, sampleLabel, scanTypeWithIndex, @@ -487,7 +530,7 @@ class QP_scope_GUI { //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) + String stitchedImagePathStr = stitchingImplementations.stitchCore("Coordinates in TileConfiguration.txt file", projectsFolderPath + File.separator + sampleLabel, stitchedImageOutputFolder, "J2K_LOSSY", 0, 1, scanTypeWithIndex) //utilityFunctions.showAlertDialog("Wait and complete stitching in other version of QuPath") @@ -537,24 +580,76 @@ class QP_scope_GUI { // 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('Python Virtual Env folder:'), virtualEnvField, row++) + 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('.groovy file path:'), groovyScriptField, 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:'), pixelSizeField, nonIsotropicCheckBox); + 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 + //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); + //addToGrid(pane, new Label('X coordinate:'), x1Field, ++row); + //addToGrid(pane, new Label('Y coordinate:'), y1Field, ++row); 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!"); + // Add buttons to the dialog + dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + Optional result; + boolean validRectangle = false; + + while (!validRectangle) { + // Show the dialog and wait for the user response + 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()); + + if (expectedRectangles.size() != 1) { + // Use utilityFunctions to show a warning + utilityFunctions.showAlertDialog("There needs to be exactly one unclassified rectangle."); + } else { + validRectangle = true; + } + } else { + // User cancelled or closed the dialog + 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!"); + + Optional result = dialog.showAndWait(); + if (result.isPresent()) { + 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; + } + } 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 d209777..14619e0 100644 --- a/src/main/groovy/qupath/ext/qp_scope/utilities/utilityFunctions.groovy +++ b/src/main/groovy/qupath/ext/qp_scope/utilities/utilityFunctions.groovy @@ -4,6 +4,7 @@ import org.slf4j.LoggerFactory import qupath.lib.gui.QuPathGUI import qupath.lib.gui.dialogs.Dialogs import qupath.lib.images.servers.ImageServerProvider +import qupath.lib.objects.PathObject import qupath.lib.projects.ProjectIO import qupath.lib.scripting.QP @@ -32,6 +33,8 @@ import java.util.zip.*; class utilityFunctions { + static final logger = LoggerFactory.getLogger(utilityFunctions.class) + static void showAlertDialog(String message){ Alert alert = new Alert(Alert.AlertType.WARNING); alert.setTitle("Warning!"); @@ -45,7 +48,6 @@ class utilityFunctions { } static boolean addImageToProject(File stitchedImagePath, Project project){ - def logger = LoggerFactory.getLogger(QuPathGUI.class) def imagePath = stitchedImagePath.toURI().toString() //logger.info(imagePath) @@ -134,42 +136,90 @@ class utilityFunctions { * * @param anacondaEnvPath The path to the Python virtual environment. * @param pythonScriptPath The path to the Python script to be executed. - * @param x1 The first x-coordinate to be passed to the Python script. - * @param y1 The first y-coordinate to be passed to the Python script. - * @param x2 The second x-coordinate to be passed to the Python script. - * @param y2 The second y-coordinate to be passed to the Python script. + * @param arguments A list of arguments to pass to the python script. The amount may vary, and different scripts will be run depending on the number of arguments passed */ - static void runPythonCommand(String anacondaEnvPath, String pythonScriptPath, List arguments) { + static runPythonCommand(String anacondaEnvPath, String pythonScriptPath, List arguments) { try { - def logger = LoggerFactory.getLogger(QuPathGUI.class) - String pythonExecutable = new File(anacondaEnvPath, "python.exe").getCanonicalPath() - -// List arguments = [pythonScriptPath, projectsFolderPath, sampleLabel, imageType] -// if (x1) arguments.add(x1) -// if (y1) arguments.add(y1) -// if (x2) arguments.add(x2) -// if (y2) arguments.add(y2) -// if (annotationJsonFileLocation) arguments.add(annotationJsonFileLocation) + 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(); + // Construct the command + String command = "\"" + pythonExecutable + "\" -u \"" + pythonScriptPath + "\" " + arguments; + // Execute the command + Process process = command.execute(); + logger.info("Executing command: " + command); + logger.info("This should get stage coordinates back") + // Construct the command + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + String value1 + String value2 + while ((line = reader.readLine()) != null) { + // Process the line to retrieve value1 and value2 + // For example, if they are printed in a single line separated by space + String[] values = line.split(" "); + value1 = values[0]; + value2 = values[1]; + // Do something with the values + } + return [value1, value2] + } else if (arguments.size() == 2) { + // Change the script to 'moveStageToCoordinates.py' + File scriptFile = new File(pythonScriptPath); + pythonScriptPath = new File(scriptFile.getParent(), "moveStageToCoordinates.py").getCanonicalPath(); + } - String args = 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.waitFor() + Process process = command.execute(); + + process.waitFor(); // Read and log standard output - process.inputStream.eachLine { line -> logger.info(line) } + process.inputStream.eachLine { line -> logger.info(line) }; // Read and log standard error - process.errorStream.eachLine { line -> logger.error(line) } + process.errorStream.eachLine { line -> logger.error(line) }; + return null } catch (Exception e) { - e.printStackTrace() + e.printStackTrace(); } } +// static void runPythonCommand(String anacondaEnvPath, String pythonScriptPath, List arguments) { +// try { +// def logger = LoggerFactory.getLogger(QuPathGUI.class) +// String pythonExecutable = new File(anacondaEnvPath, "python.exe").getCanonicalPath() +// +// +// +// String args = arguments.collect { "\"$it\"" }.join(' ') +// +// // Construct the command +// String command = "\"${pythonExecutable}\" -u \"${pythonScriptPath}\" ${args}" +// logger.info("Executing command: ${command}") +// +// // Execute the command +// Process process = command.execute() +// process.waitFor() +// +// // Read and log standard output +// process.inputStream.eachLine { line -> logger.info(line) } +// +// // Read and log standard error +// process.errorStream.eachLine { line -> logger.error(line) } +// } catch (Exception e) { +// e.printStackTrace() +// } +// } static Map getPreferences() { @@ -177,9 +227,10 @@ 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 [installation: "C:\\ImageAnalysis\\python\\pycromanager_step_1.py", + return [installation: "C:\\ImageAnalysis\\QPExtensionTest\\qp_scope\\src\\main\\pythonScripts/4x_bf_scan_pycromanager.py", environment: "C:\\Anaconda\\envs\\paquo", projects: "C:\\ImageAnalysis\\slides", + tissueDetection: "C:\\ImageAnalysis\\QPExtensionTest\\qp_scope\\src\\main\\groovyScripts/DetectTissueSize.groovy", firstScanType: "4x_bf", secondScanType:"20x_bf", tileHandling:"Zip"] //Zip Delete or anything else is ignored @@ -241,7 +292,7 @@ class utilityFunctions { * @param folderPath The path to the folder containing the tiles to be deleted. */ public static void deleteTilesAndFolder(String folderPath) { - def logger = LoggerFactory.getLogger(QuPathGUI.class) + try { Path directory = Paths.get(folderPath) @@ -265,7 +316,7 @@ class utilityFunctions { } public static void zipTilesAndMove(String folderPath) { - def logger = LoggerFactory.getLogger(QuPathGUI.class) + try { Path directory = Paths.get(folderPath); Path parentDirectory = directory.getParent(); @@ -302,27 +353,40 @@ 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") + 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 * (pixelSize as Double); - double y1Microns = y1 * (pixelSize as Double); - double x2Microns = x2 * (pixelSize as Double); - double y2Microns = y2 * (pixelSize as Double); + 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 = xCoordinate as Double - x1Microns; - double adjustedY1 = yCoordinate as Double - y1Microns; - double adjustedX2 = xCoordinate as Double - x2Microns; - double adjustedY2 = yCoordinate as Double - y2Microns; + 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; + 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 + } } @@ -400,5 +464,95 @@ class utilityFunctions { 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 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 new file mode 100644 index 0000000..ca669d3 --- /dev/null +++ b/src/main/groovyScripts/DetectTissue.groovy @@ -0,0 +1,6 @@ +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) +createAnnotationsFromPixelClassifier("C:/ImageAnalysis/python/Tissue-lowres.json", 500000.0, 50000.0, "SPLIT", "SELECT_NEW") diff --git a/src/main/groovyScripts/save4xMacroTiling.groovy b/src/main/groovyScripts/save4xMacroTiling.groovy new file mode 100644 index 0000000..890ca82 --- /dev/null +++ b/src/main/groovyScripts/save4xMacroTiling.groovy @@ -0,0 +1,112 @@ +//Exports CSV files into the project folder for both 20x brightfield and SHG imaging tiles +//CURRENTLY NAME-MODIFIED FOR PDAC PROJECT - CHANGE OUTPUT FILE NAMES IN both PLACES FOR ANY OTHER PROJECT +// updated 20220126 to remove 1st line apparent 1 tile indent when tiling +//Modified for use in new CAMM setup Jan 2024. Requires more refactoring to remove hardcoded values for target resolutions and overlap percent +createTiles = true + +double pixelSizeSource = 1.105 +double pixelSizeTarget = 1.105 +double frameWidth = 1392 / pixelSizeSource * pixelSizeTarget +double frameHeight = 1040 / pixelSizeSource * pixelSizeTarget +//Overlap percent - 10% is 10, not 0.1 +double overlapPercent = 10 +baseDirectory = "to be replaced" +imagingModality = "4x-tiles" +/***********************************************/ +//Name each annotation in the image by its XY centroids + +getAnnotationObjects().each{ + + it.setName((int)it.getROI().getCentroidX()+"_"+ (int)it.getROI().getCentroidY()) + +} +/***********************************************/ +getAnnotationObjects().each{it.setLocked(true)} + +imageData = getQuPath().getImageData() +hierarchy = imageData.getHierarchy() +clearDetections() +//Potentially store tiles as they are created +newTiles = [] + +//Store XY coordinates in an array + +//Check all annotations. Use .findAll{expression} to select a subset +annotations = hierarchy.getAnnotationObjects() +imageName = GeneralTools.getNameWithoutExtension(getQuPath().getProject().getEntry(imageData).getImageName()) + +//Ensure the folder to store the csv exists +tilePath = buildFilePath(baseDirectory, imagingModality) +mkdirs(tilePath) + +//CSV will be only two columns with the following header +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 = []; + yline = 0 + roiA = a.getROI() + //generate a bounding box to create tiles within + bBoxX = a.getROI().getBoundsX() + bBoxY = a.getROI().getBoundsY() + bBoxH = a.getROI().getBoundsHeight() + bBoxW = a.getROI().getBoundsWidth() + y = bBoxY + x = bBoxX + while (y< bBoxY+bBoxH){ + //In order to serpentine the resutls, there need to be two bounds for X now + while ((x <= bBoxX+bBoxW) && (x >=bBoxX-bBoxW*overlapPercent/100)){ + + def roi = new RectangleROI(x,y,frameWidth,frameHeight, ImagePlane.getDefaultPlane()) + if(roiA.getGeometry().intersects(roi.getGeometry())){ + newAnno = PathObjects.createDetectionObject(roi, getPathClass(imagingModality)) + newAnno.setName(predictedTileCount.toString()) + newAnno.getMeasurementList().putMeasurement("TileNumber", actualTileCount) + newTiles << newAnno + xy << [x,y] + //print predictedTileCount + " good "+x + actualTileCount++ + }//else {print x} + if (yline%2 ==0){ + x = x+frameWidth-overlapPercent/100*frameWidth + } else { x = x-(frameWidth - overlapPercent/100*frameWidth)} + predictedTileCount++ + } + y = y+frameHeight-overlapPercent/100*frameHeight + if (yline%2 ==0){ + x = x-(frameWidth - overlapPercent/100*frameWidth) + } else {x = x+frameWidth-overlapPercent/100*frameWidth} + + yline++ + } + hierarchy.addObjects(newTiles) + //Does not use CLASS of annotation in the name at the moment. + annotationName = a.getName() + path = buildFilePath(baseDirectory, imagingModality, imageName+"-"+annotationName+".csv") + + new File(path).withWriter { fw -> + fw.writeLine(header) + + //Make sure everything being sent is a child and part of the current annotation. + + xy.each{ + String line = it[0] as String +","+it[1] as String + fw.writeLine(line) + } + } +} + + + +import qupath.imagej.gui.IJExtension +import qupath.imagej.tools.IJTools +import qupath.lib.objects.PathObjectTools +import qupath.lib.regions.RegionRequest +import qupath.lib.regions.ImagePlane +import qupath.lib.roi.RectangleROI; +import qupath.lib.gui.QuPathGUI + +import static qupath.lib.gui.scripting.QPEx.* diff --git a/src/main/groovyScripts/saveTilingCSV.groovy b/src/main/groovyScripts/saveTilingCSV.groovy new file mode 100644 index 0000000..95d63ea --- /dev/null +++ b/src/main/groovyScripts/saveTilingCSV.groovy @@ -0,0 +1,195 @@ +//Exports CSV files into the project folder for both 20x brightfield and SHG imaging tiles +//CURRENTLY NAME-MODIFIED FOR PDAC PROJECT - CHANGE OUTPUT FILE NAMES IN both PLACES FOR ANY OTHER PROJECT +// updated 20220126 to remove 1st line apparent 1 tile indent when tiling +//Modified for use in new CAMM setup Jan 2024. Requires more refactoring to remove hardcoded values for target resolutions and overlap percent +createTiles = true + +double pixelSizeSource = 1.105 +double pixelSizeTarget = 0.222 +double frameWidth = 1392 / pixelSizeSource * pixelSizeTarget +double frameHeight = 1040 / pixelSizeSource * pixelSizeTarget +//Overlap percent - 10% is 10, not 0.1 +double overlapPercent = 10 +baseDirectory = "to be replaced" + +/***********************************************/ +//Name each annotation in the image by its XY centroids + +getAnnotationObjects().each{ + + it.setName((int)it.getROI().getCentroidX()+"_"+ (int)it.getROI().getCentroidY()) + +} +/***********************************************/ +getAnnotationObjects().each{it.setLocked(true)} +//Logger logger = LoggerFactory.getLogger(QuPathGUI.class); + +imageData = getQuPath().getImageData() +hierarchy = imageData.getHierarchy() +clearDetections() +//Potentially store tiles as they are created +newTiles = [] + +//Store XY coordinates in an array + +//Check all annotations. Use .findAll{expression} to select a subset +annotations = hierarchy.getAnnotationObjects() +imageName = GeneralTools.getNameWithoutExtension(getQuPath().getProject().getEntry(imageData).getImageName()) +//logger.info(imageName.toString()) +//Ensure the folder to store the csv exists +tilePath = buildFilePath(baseDirectory, "20x-tiles") +mkdirs(tilePath) + +//CSV will be only two columns with the following header +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 = []; + yline = 0 + roiA = a.getROI() + //generate a bounding box to create tiles within + bBoxX = a.getROI().getBoundsX() + bBoxY = a.getROI().getBoundsY() + bBoxH = a.getROI().getBoundsHeight() + bBoxW = a.getROI().getBoundsWidth() + y = bBoxY + x = bBoxX + while (y< bBoxY+bBoxH){ + //In order to serpentine the resutls, there need to be two bounds for X now + while ((x <= bBoxX+bBoxW) && (x >=bBoxX-bBoxW*overlapPercent/100)){ + + def roi = new RectangleROI(x,y,frameWidth,frameHeight, ImagePlane.getDefaultPlane()) + if(roiA.getGeometry().intersects(roi.getGeometry())){ + newAnno = PathObjects.createDetectionObject(roi, getPathClass("20x Tile")) + newAnno.setName(predictedTileCount.toString()) + newAnno.getMeasurementList().putMeasurement("TileNumber", actualTileCount) + newTiles << newAnno + xy << [x,y] + //print predictedTileCount + " good "+x + actualTileCount++ + }else {print x} + if (yline%2 ==0){ + x = x+frameWidth-overlapPercent/100*frameWidth + } else { x = x-(frameWidth - overlapPercent/100*frameWidth)} + predictedTileCount++ + } + y = y+frameHeight-overlapPercent/100*frameHeight + if (yline%2 ==0){ + x = x-(frameWidth - overlapPercent/100*frameWidth) + } else {x = x+frameWidth-overlapPercent/100*frameWidth} + + yline++ + } + hierarchy.addPathObjects(newTiles) + //Does not use CLASS of annotation in the name at the moment. + annotationName = a.getName() + path = buildFilePath(baseDirectory, "20x-tiles", imageName+"-"+annotationName+".csv") + //logger.info(path.toString()) + new File(path).withWriter { fw -> + fw.writeLine(header) + //logger.info(header) + //Make sure everything being sent is a child and part of the current annotation. + + xy.each{ + String line = it[0] as String +","+it[1] as String + fw.writeLine(line) + } + } +} + +// Multiphoton settings +/***********************************************/ +createTiles = true + +pixelSizeTarget = 0.255 +frameWidth = 512 / pixelSizeSource * pixelSizeTarget +frameHeight = 512 / pixelSizeSource * pixelSizeTarget +//Overlap percent - 10% is 10, not 0.1 +overlapPercent = 10 + +/***********************************************/ + +//clearDetections() +//Potentially store tiles as they are created +newTiles = [] + +//Store XY coordinates in an array + +//Check all annotations. Use .findAll{expression} to select a subset +annotations = hierarchy.getAnnotationObjects().findAll{it.getPathClass()!=getPathClass("Background")} +imageName = GeneralTools.getNameWithoutExtension(getQuPath().getProject().getEntry(imageData).getImageName()) +//logger.info(imageName.toString()) +//Ensure the folder to store the csv exists +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 = []; + yline = 0 + roiA = a.getROI() + //generate a bounding box to create tiles within + bBoxX = a.getROI().getBoundsX() + bBoxY = a.getROI().getBoundsY() + bBoxH = a.getROI().getBoundsHeight() + bBoxW = a.getROI().getBoundsWidth() + y = bBoxY + x = bBoxX + while (y< bBoxY+bBoxH){ + //In order to serpentine the results, there need to be two bounds for X now + while ((x <= bBoxX+bBoxW) && (x >= bBoxX-bBoxW*overlapPercent/100)){ + + def roi = new RectangleROI(x,y,frameWidth,frameHeight, ImagePlane.getDefaultPlane()) + if(roiA.getGeometry().intersects(roi.getGeometry())){ + newAnno = PathObjects.createDetectionObject(roi, getPathClass("MP Tile")) + newAnno.setName(predictedTileCount.toString()) + newAnno.getMeasurementList().putMeasurement("TileNumber", actualTileCount) + newTiles << newAnno + xy << [x,y] + actualTileCount++; + //print predictedTileCount + " good "+x + }else {print x} + if (yline%2 ==0){ + x = x+frameWidth-overlapPercent/100*frameWidth + } else { x = x-(frameWidth - overlapPercent/100*frameWidth)} + predictedTileCount++ + } + y = y+frameHeight-overlapPercent/100*frameHeight + if (yline%2 ==0){ + x = x-(frameWidth - overlapPercent/100*frameWidth) + } else {x = x+frameWidth-overlapPercent/100*frameWidth} + + yline++ + } + hierarchy.addPathObjects(newTiles) + //Does not use CLASS of annotation in the name at the moment. + annotationName = a.getName() + path = buildFilePath(baseDirectory, "mp-tiles",imageName+"-"+annotationName+".csv") + //logger.info(path.toString()) + new File(path).withWriter { fw -> + fw.writeLine(header) + //logger.info(header) + //Make sure everything being sent is a child and part of the current annotation. + + xy.each{ + String line = it[0] as String +","+it[1] as String + fw.writeLine(line) + } + } +} + +import qupath.imagej.gui.IJExtension +import qupath.imagej.tools.IJTools +import qupath.lib.objects.PathObjectTools +import qupath.lib.regions.RegionRequest +import qupath.lib.regions.ImagePlane +import qupath.lib.roi.RectangleROI; +import qupath.lib.gui.QuPathGUI + +import static qupath.lib.gui.scripting.QPEx.* +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; \ No newline at end of file diff --git a/src/main/pythonScripts/getStageCoordinates.py b/src/main/pythonScripts/getStageCoordinates.py new file mode 100644 index 0000000..730368b --- /dev/null +++ b/src/main/pythonScripts/getStageCoordinates.py @@ -0,0 +1,17 @@ +import sys + +# Function to log messages +def log_message(message): + with open('D:/log.txt', 'a') as log_file: + print(message, file=log_file, flush=True) + +log_message("Python script started.") + +# Check if the first command-line argument is None +if len(sys.argv) >= 2: + first_argument = sys.argv[1] + if first_argument is None: + log_message("The first argument is None.") +else: + log_message("No arguments were provided.") +print('12345', '54321') \ No newline at end of file diff --git a/src/main/pythonScripts/mat_file_converter.py b/src/main/pythonScripts/mat_file_converter.py new file mode 100644 index 0000000..26701de --- /dev/null +++ b/src/main/pythonScripts/mat_file_converter.py @@ -0,0 +1,11 @@ +import scipy.io +import numpy as np +import cv2 +from skimage import io +matfile = scipy.io.loadmat('sequencenew.mat') +image_data = matfile['sequencenew'] + +data = np.transpose(image_data, (2, 0, 1)) +print(data.dtype) +io.imsave('Stack.tif' ,data) + diff --git a/src/main/pythonScripts/moveStageToCoordinates.py b/src/main/pythonScripts/moveStageToCoordinates.py new file mode 100644 index 0000000..5f1d524 --- /dev/null +++ b/src/main/pythonScripts/moveStageToCoordinates.py @@ -0,0 +1,20 @@ +import sys + +# Function to log messages +def log_message(message): + with open('D:/log.txt', 'a') as log_file: + print(message, file=log_file, flush=True) + +log_message("Python script started.") + +# Check if there are exactly two command-line arguments +if len(sys.argv) == 3: + try: + # Parse the two arguments as doubles (X and Y) + X = float(sys.argv[1]) + Y = float(sys.argv[2]) + log_message(f"X: {X}, Y: {Y}") + except ValueError: + log_message("Invalid arguments. Both X and Y must be doubles.") +else: + log_message("Usage: python script.py ") diff --git a/src/main/pythonScripts/py_dummydoc.py b/src/main/pythonScripts/py_dummydoc.py new file mode 100644 index 0000000..7df6819 --- /dev/null +++ b/src/main/pythonScripts/py_dummydoc.py @@ -0,0 +1,27 @@ +import os +import sys + +def find_next_filename(): + counter = 1 + filename = f"dummy{counter}.txt" + while os.path.exists(filename): + counter += 1 + filename = f"dummy{counter}.txt" + print(filename) + return filename + +def create_file(filename, args): + with open(filename, 'w') as file: + file.write("Arguments passed:\n") + for arg in args: + file.write(f"{arg}\n") + +# Find the next available filename +next_filename = find_next_filename() + +# Get command line arguments (excluding the script name) +arguments = sys.argv[1:] + +# Create the file and write the arguments to it +create_file(next_filename, arguments) +print("Anything goes here") diff --git a/src/main/pythonScripts/pycromanager_step_1.py b/src/main/pythonScripts/pycromanager_step_1.py new file mode 100644 index 0000000..f2dc72d --- /dev/null +++ b/src/main/pythonScripts/pycromanager_step_1.py @@ -0,0 +1,140 @@ +import os +import sys +import shutil +import glob + +# Function to log messages +def log_message(message): + with open('D:/log.txt', 'a') as log_file: + print(message, file=log_file, flush=True) + +def copy_tif_files(projectsFolderPath, sampleLabel, imageType): + + if "4x" in imageType: + TILES_LOCATION = 'C:/ImageAnalysis/Brightfield demo/Tiles/BurnTest2-4x-bf' + else: + TILES_LOCATION = 'C:/ImageAnalysis/Brightfield demo/Tiles/PDAC.3.A_Unstained-20x--C-7' + + log_message(f"Copying .tif files from {TILES_LOCATION} to {projectsFolderPath}/{sampleLabel}") + + dest_dir = os.path.join(projectsFolderPath, sampleLabel, imageType) + log_message(f"Destination directory: {dest_dir}") + if not os.path.exists(dest_dir): + log_message("Destination directory does not exist, creating it.") + os.makedirs(dest_dir) + + tif_files = [] + for extension in ['*.tif', '*.tiff', '*.txt']: + tif_files.extend(glob.glob(os.path.join(TILES_LOCATION, extension))) + + log_message(f"Number of .tif files found: {len(tif_files)}") + if not tif_files: + log_message(f"No .tif files found in {TILES_LOCATION}") + return False + + for file in tif_files: + try: + log_message(file) + shutil.copy(file, dest_dir) + except Exception as e: + print(f"Error copying file {file}: {e}") + + + return True + +log_message("Python script started.") +projectsFolderPath = sys.argv[2] +log_message(f"Projects Folder Path: {projectsFolderPath}") +sampleLabel = sys.argv[3] +log_message(f"Sample Label: {sampleLabel}") +imageType = sys.argv[4] +log_message(f"Image Type: {imageType}") + +success = copy_tif_files(projectsFolderPath, sampleLabel, imageType) + +if not success: + log_message("File copying did not complete successfully.") +else: + log_message("File copying completed successfully.") + + +# import os +# import sys +# import shutil +# import glob + +# # Redirecting print statements to a log file +# sys.stdout = open('logpython.txt', 'w') +# print(os.getcwd()) +# def copy_tif_files(projectsFolderPath, sampleLabel, imageType): + +# if "4x" in imageType: +# TILES_LOCATION = 'C:/ImageAnalysis/Brightfield demo/Tiles/BurnTest2-4x-bf' # Replace with the absolute path +# else: +# TILES_LOCATION = 'C:/ImageAnalysis/Brightfield demo/Tiles/PDAC_MetroHealth_N352L42-20x--null' + + +# print(f"Copying .tif files from {TILES_LOCATION} to {projectsFolderPath}/{sampleLabel}") + +# dest_dir = os.path.join(projectsFolderPath, sampleLabel, f"{imageType}{sampleLabel}") +# print(f"Destination directory: {dest_dir}") +# if not os.path.exists(dest_dir): +# print("Destination directory does not exist, creating it.") +# os.makedirs(dest_dir) + +# # Find all .tif files +# tif_files = [] +# for extension in ['*.tif', '*.tiff', '*.txt']: +# tif_files.extend(glob.glob(os.path.join(TILES_LOCATION, extension))) + +# print(f"Number of .tif files found: {len(tif_files)}") +# #print(f"Files: {tif_files}") +# # Check if any.tif files were found +# if not tif_files: +# print(f"No .tif files found in {TILES_LOCATION}") +# return False +# #Copy each .tif file +# #shutil.copy(tif_files[0], dest_dir) +# # print(f"Copying last file: {tif_files[-1]}") +# # shutil.copy(tif_files[-1], dest_dir) + +# # for file in tif_files[:42]: +# # print(f"Copying file: {file}") +# # shutil.copy(file, dest_dir) + +# for file in tif_files: +# try: +# print(f"Copying file: {file}") +# shutil.copy(file, dest_dir) +# except Exception as e: +# print(f"Error copying file {file}: {e}") + +# # print("Copy operation completed.") +# return True + + +# # if len(sys.argv) != 9: +# # print("Incorrect arguments for function: python script.py ") +# # print(len(sys.argv)) +# # return +# print("Python script started.") +# projectsFolderPath = sys.argv[2] +# print(f"Projects Folder Path: {projectsFolderPath}") +# sampleLabel = sys.argv[3] +# print(f"Sample Label: {sampleLabel}") +# imageType = sys.argv[4] +# print(f"Image Type: {imageType}") +# # x1, y1, x2, y2 are received but not used in this script. They can be used if needed. + +# #jsonFileLocation = sys.arv[9] +# #WOULD NEED FUNCTION HERE TO TRANSLATE COORDINATES TO STAGE COORDINATES + +# success = copy_tif_files(projectsFolderPath, sampleLabel, imageType) + +# if not success: +# print("File copying did not complete successfully.") +# else: +# print("File copying completed successfully.") + +# # Close the log file +# sys.stdout.close() \ No newline at end of file