Created
January 28, 2026 08:31
-
-
Save romainGuiet/81bf5e601e3ddf414246ccd2765a4f0d to your computer and use it in GitHub Desktop.
a QuPath GUI to load points from csv file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import javafx.application.Platform | |
| import javafx.scene.Scene | |
| import javafx.scene.control.* | |
| import javafx.scene.layout.* | |
| import javafx.stage.Stage | |
| import javafx.stage.DirectoryChooser | |
| import javafx.geometry.Insets | |
| import javafx.geometry.Pos | |
| import javafx.collections.FXCollections | |
| import javafx.collections.ObservableList | |
| import javafx.scene.control.cell.PropertyValueFactory | |
| import javafx.scene.paint.Color | |
| import java.io.File | |
| // QuPath imports for creating detections | |
| import qupath.lib.objects.PathObjects | |
| import qupath.lib.roi.ROIs | |
| import qupath.lib.regions.ImagePlane | |
| Platform.runLater { | |
| // Create the main stage | |
| def stage = new Stage() | |
| stage.setTitle("QuPath Custom GUI - CSV Loader") | |
| // Create TabPane | |
| def tabPane = new TabPane() | |
| // Variables shared between tabs | |
| def topGenesFromTab1 = [] | |
| def allGenesFromTab1 = [] // Store all unique genes | |
| def currentCsvData = [] | |
| def currentHeaders = [] | |
| def selectedFolder = [null] as File[] | |
| def geneColumnIndex = -1 | |
| def xColumnIndex = -1 | |
| def yColumnIndex = -1 | |
| def zColumnIndex = -1 | |
| def dimensionUnit = "micron" | |
| // Variables for Tab 2 (declare them here so they can be accessed from Tab 1) | |
| def geneDropdowns = [] | |
| def colorPickers = [] | |
| def progressBar = null | |
| // ===== TAB 1: CSV File Loader ===== | |
| def tab1 = new Tab("CSV Loader") | |
| tab1.setClosable(false) | |
| // Create content for Tab 1 | |
| def vbox1 = new VBox(10) | |
| vbox1.setPadding(new Insets(15)) | |
| // Title | |
| def label1 = new Label("CSV File Loader") | |
| label1.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;") | |
| // === FILE BROWSER SECTION WITH EXTENSION AND DELIMITER === | |
| def folderHBox = new HBox(10) | |
| folderHBox.setAlignment(Pos.CENTER_LEFT) | |
| def folderLabel = new Label("Folder:") | |
| def folderField = new TextField() | |
| folderField.setPromptText("No folder selected") | |
| folderField.setPrefWidth(200) | |
| folderField.setEditable(false) | |
| def browseButton = new Button("Browse...") | |
| def extensionLabel = new Label("Ext:") | |
| def extensionField = new TextField(".part") | |
| extensionField.setPrefWidth(60) | |
| def delimiterLabel = new Label("Delim:") | |
| def delimiterField = new TextField(",") | |
| delimiterField.setPrefWidth(40) | |
| browseButton.setOnAction({ e -> | |
| def directoryChooser = new DirectoryChooser() | |
| directoryChooser.setTitle("Select Folder Containing CSV Files") | |
| def folder = directoryChooser.showDialog(stage) | |
| if (folder != null) { | |
| selectedFolder[0] = folder | |
| folderField.setText(folder.getAbsolutePath()) | |
| } | |
| }) | |
| folderHBox.getChildren().addAll(folderLabel, folderField, browseButton, | |
| extensionLabel, extensionField, delimiterLabel, delimiterField) | |
| // === LOAD BUTTON === | |
| def loadButton = new Button("Load CSV Files") | |
| loadButton.setPrefWidth(120) | |
| // === TABLE FOR DISPLAYING CSV DATA === | |
| def csvTable = new TableView<ObservableList<String>>() | |
| csvTable.setPrefHeight(150) | |
| csvTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY) | |
| // === ALL COORDINATE AND GENE DROPDOWNS IN ONE LINE === | |
| def coordHBox = new HBox(10) | |
| coordHBox.setAlignment(Pos.CENTER_LEFT) | |
| def xLabel = new Label("X:") | |
| def xCombo = new ComboBox<String>() | |
| xCombo.setPrefWidth(100) | |
| def yLabel = new Label("Y:") | |
| def yCombo = new ComboBox<String>() | |
| yCombo.setPrefWidth(100) | |
| def zLabel = new Label("Z:") | |
| def zCombo = new ComboBox<String>() | |
| zCombo.setPrefWidth(100) | |
| def geneLabel = new Label("Gene:") | |
| def geneCombo = new ComboBox<String>() | |
| geneCombo.setPrefWidth(100) | |
| def dimLabel = new Label("Dimension:") | |
| def dimensionCombo = new ComboBox<String>() | |
| dimensionCombo.getItems().addAll("micron", "pixel") | |
| dimensionCombo.setValue("micron") | |
| dimensionCombo.setPrefWidth(80) | |
| coordHBox.getChildren().addAll(xLabel, xCombo, yLabel, yCombo, zLabel, zCombo, | |
| geneLabel, geneCombo, dimLabel, dimensionCombo) | |
| // === ANALYZE ABUNDANCE BUTTON (moved up) === | |
| def analyzeButton = new Button("Analyse Abundancy") | |
| analyzeButton.setPrefWidth(150) | |
| // Variables to hold the abundance table and label (will be created dynamically) | |
| def abundanceLabel = null | |
| def abundanceTable = null | |
| // === LOAD BUTTON ACTION === | |
| loadButton.setOnAction({ e -> | |
| if (selectedFolder[0] == null) { | |
| showAlert("Error", "Please select a folder first.") | |
| return | |
| } | |
| def extension = extensionField.getText() | |
| def delimiter = delimiterField.getText() | |
| if (!extension || !delimiter) { | |
| showAlert("Error", "Please specify both extension and delimiter.") | |
| return | |
| } | |
| try { | |
| // Find CSV files with matching extension | |
| def csvFiles = selectedFolder[0].listFiles().findAll { | |
| it.isFile() && it.getName().endsWith(extension) | |
| } | |
| if (csvFiles.isEmpty()) { | |
| showAlert("Warning", "No files found with extension: $extension") | |
| return | |
| } | |
| // Read the first CSV file found | |
| def csvFile = csvFiles[0] | |
| def lines = csvFile.readLines() | |
| if (lines.isEmpty()) { | |
| showAlert("Warning", "Selected CSV file is empty.") | |
| return | |
| } | |
| // Parse CSV headers (first line) | |
| def headers = lines[0].split(delimiter).collect { it.trim().replace('"', '') } | |
| currentHeaders = headers | |
| // Store all CSV data for analysis (skip header line) | |
| currentCsvData = lines.drop(1).collect { line -> | |
| line.split(delimiter).collect { it.trim().replace('"', '') } | |
| } | |
| // Clear and setup table columns using headers | |
| csvTable.getColumns().clear() | |
| headers.eachWithIndex { header, index -> | |
| def column = new TableColumn<ObservableList<String>, String>(header) | |
| column.setCellValueFactory({ param -> | |
| return new javafx.beans.property.SimpleStringProperty( | |
| param.getValue().size() > index ? param.getValue().get(index) : "" | |
| ) | |
| }) | |
| csvTable.getColumns().add(column) | |
| } | |
| // Add first 5 DATA rows to table (skip header line) | |
| def tableData = FXCollections.observableArrayList() | |
| def dataLines = lines.drop(1) // Skip header | |
| def maxRows = Math.min(5, dataLines.size()) | |
| for (int i = 0; i < maxRows; i++) { | |
| def csvRowData = FXCollections.observableArrayList( | |
| dataLines[i].split(delimiter).collect { it.trim().replace('"', '') } | |
| ) | |
| tableData.add(csvRowData) | |
| } | |
| csvTable.setItems(tableData) | |
| // Update coordinate dropdowns | |
| xCombo.getItems().clear() | |
| yCombo.getItems().clear() | |
| zCombo.getItems().clear() | |
| geneCombo.getItems().clear() | |
| xCombo.getItems().addAll(headers) | |
| yCombo.getItems().addAll(headers) | |
| zCombo.getItems().addAll(headers) | |
| zCombo.getItems().add("Current annotation Z") | |
| geneCombo.getItems().addAll(headers) | |
| // Set default values if available | |
| if (headers.contains("x_location")) xCombo.setValue("x_location") | |
| if (headers.contains("y_location")) yCombo.setValue("y_location") | |
| if (headers.contains("z_location")) { | |
| zCombo.setValue("z_location") | |
| } else { | |
| zCombo.setValue("Current annotation Z") | |
| } | |
| if (headers.contains("feature_name")) geneCombo.setValue("feature_name") | |
| println("Loaded CSV file: ${csvFile.getName()}") | |
| println("Found ${csvFiles.size()} CSV files in folder") | |
| println("Headers: $headers") | |
| println("Data rows: ${currentCsvData.size()}") | |
| } catch (Exception ex) { | |
| showAlert("Error", "Failed to load CSV file: ${ex.getMessage()}") | |
| ex.printStackTrace() | |
| } | |
| }) | |
| // === ANALYZE ABUNDANCE ACTION === | |
| analyzeButton.setOnAction({ e -> | |
| if (currentCsvData.isEmpty() || currentHeaders.isEmpty()) { | |
| showAlert("Error", "Please load a CSV file first.") | |
| return | |
| } | |
| def geneColumn = geneCombo.getValue() | |
| if (!geneColumn) { | |
| showAlert("Error", "Please select a gene column.") | |
| return | |
| } | |
| def geneIndex = currentHeaders.indexOf(geneColumn) | |
| if (geneIndex == -1) { | |
| showAlert("Error", "Selected gene column not found.") | |
| return | |
| } | |
| // Store column indices for Tab 2 | |
| geneColumnIndex = geneIndex | |
| xColumnIndex = currentHeaders.indexOf(xCombo.getValue()) | |
| yColumnIndex = currentHeaders.indexOf(yCombo.getValue()) | |
| zColumnIndex = zCombo.getValue() == "Current annotation Z" ? -1 : currentHeaders.indexOf(zCombo.getValue()) | |
| dimensionUnit = dimensionCombo.getValue() | |
| try { | |
| // Count gene frequencies | |
| def geneCounts = [:] | |
| def allUniqueGenes = [] as Set | |
| currentCsvData.each { row -> | |
| if (row.size() > geneIndex) { | |
| def gene = row[geneIndex] | |
| if (gene && gene.trim()) { | |
| geneCounts[gene] = (geneCounts[gene] ?: 0) + 1 | |
| allUniqueGenes.add(gene) | |
| } | |
| } | |
| } | |
| // Sort by count and get top 10 for display | |
| def topGenes = geneCounts.entrySet() | |
| .sort { -it.value } | |
| .take(10) | |
| // Store top genes for reference and all genes for Tab 2 dropdowns | |
| topGenesFromTab1 = topGenes.collect { [gene: it.key, count: it.value] } | |
| allGenesFromTab1 = allUniqueGenes.sort() // Sort alphabetically | |
| // Update Tab 2 dropdowns with all genes | |
| geneDropdowns.each { dropdown -> | |
| dropdown.getItems().clear() | |
| dropdown.getItems().add("none") | |
| dropdown.getItems().addAll(allGenesFromTab1) | |
| } | |
| // Set default values for first 10 dropdowns based on top genes | |
| topGenesFromTab1.eachWithIndex { geneData, index -> | |
| if (index < geneDropdowns.size()) { | |
| geneDropdowns[index].setValue(geneData.gene) | |
| // Set different colors for each gene | |
| def colors = [ | |
| Color.RED, Color.BLUE, Color.GREEN, Color.ORANGE, Color.PURPLE, | |
| Color.CYAN, Color.MAGENTA, Color.YELLOW, Color.PINK, Color.BROWN | |
| ] | |
| colorPickers[index].setValue(colors[index % colors.size()]) | |
| } | |
| } | |
| // Remove existing abundance table and label if they exist | |
| if (abundanceLabel != null) { | |
| vbox1.getChildren().remove(abundanceLabel) | |
| } | |
| if (abundanceTable != null) { | |
| vbox1.getChildren().remove(abundanceTable) | |
| } | |
| // Create new abundance label and table | |
| abundanceLabel = new Label("Top 10 Gene Abundance Results:") | |
| abundanceLabel.setStyle("-fx-font-weight: bold;") | |
| abundanceTable = new TableView() | |
| abundanceTable.setPrefHeight(50) // Reduced height for single row | |
| abundanceTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY) | |
| // Disable row header and make table more compact | |
| abundanceTable.setRowFactory({ tv -> | |
| def row = new TableRow() | |
| row.setPrefHeight(25) // Set row height | |
| return row | |
| }) | |
| // Create a simple data structure - array of strings for counts only | |
| def abundanceRowData = new String[10] | |
| // Fill the array with count data only | |
| def numGenes = Math.min(topGenes.size(), 10) | |
| for (int i = 0; i < numGenes; i++) { | |
| def gene = topGenes[i] | |
| abundanceRowData[i] = "${gene.value}" // Only the count | |
| } | |
| // Fill remaining slots with empty strings | |
| for (int i = numGenes; i < 10; i++) { | |
| abundanceRowData[i] = "" | |
| } | |
| // Create columns with gene names as headers | |
| for (int i = 0; i < 10; i++) { | |
| def column = new TableColumn() | |
| // Set column header to gene name or empty if no gene | |
| if (i < numGenes) { | |
| def gene = topGenes[i] | |
| column.setText(gene.key.toString()) // Gene name as header | |
| } else { | |
| column.setText("") // Empty header for unused columns | |
| } | |
| column.setPrefWidth(100) | |
| column.setResizable(true) | |
| def columnIndex = i // Capture the index for the closure | |
| column.setCellValueFactory({ cellData -> | |
| def rowData = cellData.getValue() | |
| return new javafx.beans.property.SimpleStringProperty( | |
| rowData[columnIndex] != null ? rowData[columnIndex] : "" | |
| ) | |
| }) | |
| abundanceTable.getColumns().add(column) | |
| } | |
| // Create table data with single row | |
| def abundanceData = FXCollections.observableArrayList() | |
| abundanceData.add(abundanceRowData) | |
| abundanceTable.setItems(abundanceData) | |
| // Make table more compact - disable various interactive features | |
| abundanceTable.setTableMenuButtonVisible(false) | |
| abundanceTable.setEditable(false) | |
| // Optional: Style to make it even more compact | |
| abundanceTable.setStyle("-fx-table-cell-border-color: transparent;") | |
| // Add label and table to the layout | |
| vbox1.getChildren().addAll(abundanceLabel, abundanceTable) | |
| println("Gene abundance analysis completed:") | |
| println("Total unique genes found: ${allUniqueGenes.size()}") | |
| topGenes.eachWithIndex { entry, index -> | |
| println("${index + 1}. ${entry.key}: ${entry.value} occurrences") | |
| } | |
| println("Tab 2 dropdowns updated with ${allUniqueGenes.size()} genes") | |
| } catch (Exception ex) { | |
| showAlert("Error", "Failed to analyze gene abundance: ${ex.getMessage()}") | |
| ex.printStackTrace() | |
| } | |
| }) | |
| // Helper method to show alerts | |
| def showAlert = { title, message -> | |
| Platform.runLater { | |
| def alert = new Alert(Alert.AlertType.INFORMATION) | |
| alert.setTitle(title) | |
| alert.setHeaderText(null) | |
| alert.setContentText(message) | |
| alert.showAndWait() | |
| } | |
| } | |
| // Helper method to get current annotation Z plane | |
| def getCurrentAnnotationZPlane = { | |
| def imageData = getCurrentImageData() | |
| if (imageData == null) return ImagePlane.getDefaultPlane() | |
| def selectedObject = getSelectedObject() | |
| if (selectedObject != null && selectedObject.getROI() != null) { | |
| return selectedObject.getROI().getImagePlane() | |
| } | |
| return ImagePlane.getDefaultPlane() | |
| } | |
| // Add all widgets to Tab 1 | |
| vbox1.getChildren().addAll( | |
| label1, | |
| new Separator(), | |
| folderHBox, | |
| loadButton, | |
| new Label("CSV Preview (first 5 data rows):"), | |
| csvTable, | |
| new Label("Column Mapping:"), | |
| coordHBox, | |
| analyzeButton | |
| ) | |
| tab1.setContent(new ScrollPane(vbox1)) | |
| // ===== TAB 2: Point Loading & Gene Selection ===== | |
| def tab2 = new Tab("Point Loading") | |
| tab2.setClosable(false) | |
| def vbox2 = new VBox(10) | |
| vbox2.setPadding(new Insets(15)) | |
| def label2 = new Label("Point Loading & Gene Selection") | |
| label2.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;") | |
| // === FIRST LINE: Load Points Button and Points Number Field === | |
| def firstLineHBox = new HBox(10) | |
| firstLineHBox.setAlignment(Pos.CENTER_LEFT) | |
| def loadPointsButton = new Button("Load Points") | |
| loadPointsButton.setPrefWidth(120) | |
| def pointsLabel = new Label("Points number:") | |
| def pointsField = new TextField("10000") | |
| pointsField.setPrefWidth(80) | |
| firstLineHBox.getChildren().addAll(loadPointsButton, pointsLabel, pointsField) | |
| // === 10 LINES OF GENE DROPDOWNS AND COLOR PICKERS === | |
| def geneSelectionVBox = new VBox(5) | |
| geneSelectionVBox.setPadding(new Insets(10, 0, 10, 0)) | |
| for (int i = 0; i < 10; i++) { | |
| def lineHBox = new HBox(10) | |
| lineHBox.setAlignment(Pos.CENTER_LEFT) | |
| def geneDropdown = new ComboBox<String>() | |
| geneDropdown.setPrefWidth(150) | |
| geneDropdown.getItems().add("none") | |
| geneDropdowns.add(geneDropdown) | |
| def colorPicker = new ColorPicker(Color.rgb(255, 0, 0)) | |
| colorPickers.add(colorPicker) | |
| def lineLabel = new Label("Gene ${i + 1}:") | |
| lineLabel.setPrefWidth(60) | |
| lineHBox.getChildren().addAll(lineLabel, geneDropdown, colorPicker) | |
| geneSelectionVBox.getChildren().add(lineHBox) | |
| } | |
| // === FINAL BUTTON TO LOAD SELECTED GENES === | |
| def loadSelectedGenesButton = new Button("Load Selected Genes") | |
| loadSelectedGenesButton.setPrefWidth(180) | |
| // === PROGRESS BAR === | |
| progressBar = new ProgressBar(0) | |
| progressBar.setPrefWidth(300) | |
| progressBar.setVisible(false) | |
| def progressLabel = new Label("") | |
| progressLabel.setVisible(false) | |
| // === LOAD POINTS BUTTON ACTION (loads first N points grouped by gene) === | |
| loadPointsButton.setOnAction({ e -> | |
| if (currentCsvData.isEmpty() || currentHeaders.isEmpty()) { | |
| showAlert("Error", "Please load CSV data in Tab 1 first.") | |
| return | |
| } | |
| if (geneColumnIndex == -1 || xColumnIndex == -1 || yColumnIndex == -1) { | |
| showAlert("Error", "Please configure column mappings in Tab 1 first.") | |
| return | |
| } | |
| try { | |
| def pointsNumber = Integer.parseInt(pointsField.getText()) | |
| // Get pixel size from current image | |
| def imageData = getCurrentImageData() | |
| if (imageData == null) { | |
| showAlert("Error", "No image is currently open in QuPath.") | |
| return | |
| } | |
| def pixelSizeMicrons = imageData.getServer().getPixelCalibration().getAveragedPixelSizeMicrons() | |
| def pixelSizeYMicrons = imageData.getServer().getPixelCalibration().getPixelHeight() | |
| // Get the appropriate image plane | |
| def plane = ImagePlane.getPlane( getSelectedROI() ) | |
| // Show progress | |
| progressBar.setVisible(true) | |
| progressLabel.setVisible(true) | |
| progressLabel.setText("Loading first ${pointsNumber} points...") | |
| progressBar.setProgress(0.1) | |
| // Collect first N points grouped by gene | |
| def pointsByGene = [:] | |
| def pointCount = 0 | |
| currentCsvData.each { row -> | |
| if (pointCount >= pointsNumber) return // Stop if we've reached the limit | |
| if (row.size() > Math.max(geneColumnIndex, Math.max(xColumnIndex, yColumnIndex))) { | |
| try { | |
| def geneName = row[geneColumnIndex].trim() | |
| def xRaw = Double.parseDouble(row[xColumnIndex].trim()) | |
| def yRaw = Double.parseDouble(row[yColumnIndex].trim()) | |
| // Convert coordinates based on dimension unit | |
| def x, y | |
| if (dimensionUnit == "micron") { | |
| x = xRaw / pixelSizeMicrons | |
| y = yRaw / pixelSizeYMicrons | |
| } else { | |
| x = xRaw | |
| y = yRaw | |
| } | |
| if (!pointsByGene.containsKey(geneName)) { | |
| pointsByGene[geneName] = [] | |
| } | |
| pointsByGene[geneName].add([x: x, y: y]) | |
| pointCount++ | |
| } catch (NumberFormatException nfe) { | |
| // Skip invalid coordinate entries | |
| } | |
| } | |
| } | |
| progressBar.setProgress(0.5) | |
| if (!pointsByGene.isEmpty()) { | |
| def totalGenes = pointsByGene.size() | |
| def geneIndex = 0 | |
| def totalPointsCreated = 0 | |
| // Create separate detection objects for each gene | |
| pointsByGene.each { geneName, points -> | |
| if (!points.isEmpty()) { | |
| // Create arrays for coordinates | |
| def xCoords = points.collect { it.x } as double[] | |
| def yCoords = points.collect { it.y } as double[] | |
| // Create PointsROI using QuPath's ROIs class | |
| def pointsROI = ROIs.createPointsROI(xCoords, yCoords, plane) | |
| // Create detection object with gene-specific class | |
| def pathClass = getPathClass(geneName) | |
| if (pathClass == null) { | |
| pathClass = createPathClass(geneName) | |
| } | |
| def detection = PathObjects.createDetectionObject(pointsROI, pathClass) | |
| detection.setName("${geneName} (${points.size()} points)") | |
| // Add to current image hierarchy | |
| addObject(detection) | |
| totalPointsCreated += points.size() | |
| geneIndex++ | |
| } | |
| progressBar.setProgress(0.5 + (geneIndex as double) / totalGenes * 0.5) | |
| } | |
| progressBar.setProgress(1.0) | |
| progressLabel.setText("Completed: ${totalPointsCreated} points loaded across ${pointsByGene.size()} genes") | |
| // Update the hierarchy | |
| fireHierarchyUpdate() | |
| println("Created ${pointsByGene.size()} detection objects with total ${totalPointsCreated} points") | |
| showAlert("Success", "Successfully loaded ${totalPointsCreated} points across ${pointsByGene.size()} genes!") | |
| } else { | |
| progressLabel.setText("No valid points found") | |
| showAlert("Warning", "No valid coordinate data found in CSV.") | |
| } | |
| // Hide progress bar after 2 seconds | |
| Platform.runLater({ | |
| Thread.sleep(2000) | |
| progressBar.setVisible(false) | |
| progressLabel.setVisible(false) | |
| }) | |
| } catch (NumberFormatException nfe) { | |
| showAlert("Error", "Invalid points number. Please enter a valid integer.") | |
| progressBar.setVisible(false) | |
| progressLabel.setVisible(false) | |
| } catch (Exception ex) { | |
| showAlert("Error", "Failed to load points: ${ex.getMessage()}") | |
| ex.printStackTrace() | |
| progressBar.setVisible(false) | |
| progressLabel.setVisible(false) | |
| } | |
| }) | |
| // === LOAD SELECTED GENES ACTION (loads ALL points for each selected gene) === | |
| loadSelectedGenesButton.setOnAction({ e -> | |
| if (currentCsvData.isEmpty() || currentHeaders.isEmpty()) { | |
| showAlert("Error", "Please load CSV data in Tab 1 first.") | |
| return | |
| } | |
| if (geneColumnIndex == -1 || xColumnIndex == -1 || yColumnIndex == -1) { | |
| showAlert("Error", "Please configure column mappings in Tab 1 first.") | |
| return | |
| } | |
| // Count selected genes | |
| def selectedGenes = [] | |
| geneDropdowns.eachWithIndex { dropdown, index -> | |
| def selectedGene = dropdown.getValue() | |
| if (selectedGene != null && selectedGene != "none") { | |
| selectedGenes.add([gene: selectedGene, color: colorPickers[index].getValue(), index: index]) | |
| } | |
| } | |
| if (selectedGenes.isEmpty()) { | |
| showAlert("Warning", "No genes selected. Please select at least one gene.") | |
| return | |
| } | |
| try { | |
| // Get pixel size from current image | |
| def imageData = getCurrentImageData() | |
| if (imageData == null) { | |
| showAlert("Error", "No image is currently open in QuPath.") | |
| return | |
| } | |
| def pixelSizeMicrons = imageData.getServer().getPixelCalibration().getAveragedPixelSizeMicrons() | |
| def pixelSizeYMicrons = imageData.getServer().getPixelCalibration().getPixelHeight() | |
| // Get the appropriate image plane | |
| //def plane = (zColumnIndex == -1) ? getCurrentAnnotationZPlane() : ImagePlane.getDefaultPlane() | |
| def plane = ImagePlane.getPlane( getSelectedROI() ) | |
| // Show progress | |
| progressBar.setVisible(true) | |
| progressLabel.setVisible(true) | |
| progressBar.setProgress(0) | |
| def totalGenes = selectedGenes.size() | |
| // Process each selected gene | |
| selectedGenes.eachWithIndex { geneData, geneIndex -> | |
| def selectedGene = geneData.gene | |
| def selectedColor = geneData.color | |
| progressBar.setProgress((geneIndex as double) / totalGenes) | |
| progressLabel.setText("Processing ${selectedGene}... (${geneIndex + 1}/${totalGenes})") | |
| // Filter data for this gene and collect ALL coordinates | |
| def genePointsData = [] | |
| currentCsvData.each { row -> | |
| if (row.size() > Math.max(geneColumnIndex, Math.max(xColumnIndex, yColumnIndex))) { | |
| def geneName = row[geneColumnIndex].trim() | |
| if (geneName == selectedGene) { | |
| try { | |
| def xRaw = Double.parseDouble(row[xColumnIndex].trim()) | |
| def yRaw = Double.parseDouble(row[yColumnIndex].trim()) | |
| // Convert coordinates based on dimension unit | |
| def x, y | |
| if (dimensionUnit == "micron") { | |
| x = xRaw / pixelSizeMicrons | |
| y = yRaw / pixelSizeYMicrons | |
| } else { | |
| x = xRaw | |
| y = yRaw | |
| } | |
| genePointsData.add([x: x, y: y]) | |
| } catch (NumberFormatException nfe) { | |
| // Skip invalid coordinate entries | |
| } | |
| } | |
| } | |
| } | |
| if (!genePointsData.isEmpty()) { | |
| // Create arrays for coordinates | |
| def xCoords = genePointsData.collect { it.x } as double[] | |
| def yCoords = genePointsData.collect { it.y } as double[] | |
| // Create PointsROI using QuPath's ROIs class | |
| def pointsROI = ROIs.createPointsROI(xCoords, yCoords, plane) | |
| // Create or get path class with the selected color | |
| def pathClass = getPathClass(selectedGene) | |
| if (pathClass == null) { | |
| pathClass = createPathClass(selectedGene) | |
| } | |
| // Convert JavaFX Color to integers | |
| def red = (selectedColor.getRed() * 255) as int | |
| def green = (selectedColor.getGreen() * 255) as int | |
| def blue = (selectedColor.getBlue() * 255) as int | |
| // Set the color for this path class | |
| pathClass.setColor(red, green, blue) | |
| def detection = PathObjects.createDetectionObject(pointsROI, pathClass) | |
| detection.setName("${selectedGene} (${genePointsData.size()} points)") | |
| // Add to current image hierarchy | |
| addObject(detection) | |
| println("Created detection for ${selectedGene}: ${genePointsData.size()} points with color RGB(${red}, ${green}, ${blue})") | |
| } else { | |
| println("No data found for gene: ${selectedGene}") | |
| } | |
| } | |
| // Complete progress | |
| progressBar.setProgress(1.0) | |
| progressLabel.setText("Completed: ${totalGenes} genes processed") | |
| // Update the hierarchy to show the new detections | |
| fireHierarchyUpdate() | |
| //showAlert("Success", "Successfully loaded selected gene detections!") | |
| // Hide progress bar after 2 seconds | |
| Platform.runLater({ | |
| Thread.sleep(2000) | |
| progressBar.setVisible(false) | |
| progressLabel.setVisible(false) | |
| }) | |
| } catch (NumberFormatException nfe) { | |
| showAlert("Error", "Invalid points number. Please enter a valid integer.") | |
| progressBar.setVisible(false) | |
| progressLabel.setVisible(false) | |
| } catch (Exception ex) { | |
| showAlert("Error", "Failed to load gene detections: ${ex.getMessage()}") | |
| ex.printStackTrace() | |
| progressBar.setVisible(false) | |
| progressLabel.setVisible(false) | |
| } | |
| }) | |
| // Add all widgets to Tab 2 | |
| vbox2.getChildren().addAll( | |
| label2, | |
| new Separator(), | |
| firstLineHBox, | |
| new Label("Gene Selection & Colors:"), | |
| geneSelectionVBox, | |
| loadSelectedGenesButton, | |
| new Separator(), | |
| progressLabel, | |
| progressBar | |
| ) | |
| tab2.setContent(new ScrollPane(vbox2)) | |
| // Add tabs to TabPane | |
| tabPane.getTabs().addAll(tab1, tab2) | |
| // Create scene and show stage | |
| def scene = new Scene(tabPane, 700, 750) | |
| stage.setScene(scene) | |
| stage.show() | |
| stage.centerOnScreen() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment