Skip to content

Commit

Permalink
Restructuring and additional features
Browse files Browse the repository at this point in the history
Split utilityFunctions into three different types of functions. Modified some of the GUI elements to be more accurate in their descriptions.
Completed change where TileConfiguration.txt files can be used instead of CSV files for handling tiles.
Swapped to affine transformations from handling modifications to objects and coordinates line by line.
Still an issue with the resulting tile size.
  • Loading branch information
MichaelSNelson committed Jan 16, 2024
1 parent 6de7f78 commit 7cb98a7
Show file tree
Hide file tree
Showing 9 changed files with 570 additions and 423 deletions.
11 changes: 11 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 10 additions & 10 deletions src/main/groovy/qupath/ext/qp_scope/QP_scope.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
}

/**
Expand All @@ -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) {
Expand All @@ -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
Expand Down
271 changes: 154 additions & 117 deletions src/main/groovy/qupath/ext/qp_scope/functions/QP_scope_GUI.groovy

Large diffs are not rendered by default.

106 changes: 106 additions & 0 deletions src/main/groovy/qupath/ext/qp_scope/utilities/minorFunctions.groovy
Original file line number Diff line number Diff line change
@@ -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
}
}

}
Original file line number Diff line number Diff line change
@@ -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<Double> QPtoMicroscopeCoordinates(List<Double> 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<String> 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<String> 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<String> 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<Double> qpCoordinates = [x1, y1]
List<Double> 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<String> coordinatesQP, List<String> 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<Object> getTopCenterTile(Collection<PathObject> detections) {
// Filter out null detections and sort by Y-coordinate
List<PathObject> 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<PathObject> topTiles = sortedDetections.findAll { it.getROI().getCentroidY() == minY }

// Find the median X-coordinate of the top tiles
List<Double> 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<Object> getLeftCenterTile(Collection<PathObject> detections) {
// Filter out null detections and sort by X-coordinate
List<PathObject> 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<PathObject> leftTiles = sortedDetections.findAll { it.getROI().getCentroidX() == minX }

// Find the median Y-coordinate of the left tiles
List<Double> 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]
}

}
Loading

0 comments on commit 7cb98a7

Please sign in to comment.