Last active
September 22, 2023 09:32
-
-
Save tsikup/5b43ed20ec8886794af99ed3ae5273ae to your computer and use it in GitHub Desktop.
A collection of groovy and python (paquo) scripts used for image analysis in QuPath.
This file contains 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
A collection of groovy and python (paquo) scripts used for image analysis in QuPath. |
This file contains 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 org.apache.commons.csv.CSVFormat | |
import org.apache.commons.csv.CSVPrinter | |
import qupath.lib.objects.PathObject | |
import qupath.lib.scripting.QP | |
def imageData = QP.getCurrentImageData() | |
def hierarchy = imageData.getHierarchy() | |
def annotations = hierarchy.getAnnotationObjects() | |
def entry = QP.getProjectEntry() | |
def server = QP.getCurrentServer() | |
def imageName = entry == null ? server.getMetadata().getName() : entry.getImageName() | |
print('-------------') | |
println(imageName) | |
def class_tumor_tissue = QP.getPathClass('Tumor') | |
// Define classes | |
def class_tumor = QP.getPathClass('Tumor') | |
def class_tissue = QP.getPathClass('Tissue') | |
def class_other = QP.getPathClass('Other') | |
def class_tils = QP.getPathClass('Immune cells') | |
def class_stroma = QP.getPathClass('Stroma') | |
//HOVERNET | |
//def class_tumor = QP.getPathClass('Neoplastic epithelial') | |
////def class_tumor = QP.getPathClass('Tumor') | |
//def class_tissue = QP.getPathClass('Tissue') | |
//def class_other = QP.getPathClass('Other') | |
//def class_tils = QP.getPathClass('Immune cells') | |
//def class_stroma = QP.getPathClass('Stroma') | |
// Get pixel size | |
def cal = imageData.getServer().getPixelCalibration() | |
def pixelWidth = cal.getPixelWidth().doubleValue() | |
def pixelHeight = cal.getPixelHeight().doubleValue() | |
def csv_path = QP.buildFilePath(QP.PROJECT_BASE_DIR, 'digital_metrics.csv') | |
if(!(new File(csv_path)).exists()){ | |
def FILE_HEADER = [ | |
'image', | |
'biopsy', | |
'cellularity_(cell_area_%)', | |
'cellularity_(tissue_area_%)', | |
'tumor_cell_density_in_tumor(#/um^2)', | |
'purity_(cell_area_%)', | |
'purity_(tissue_area_%)', | |
'tumor_cell_density_in_biopsy(#/um^2)', | |
'tumor_biopsy_area_%', | |
'n_tumor_cells', | |
'n_cells_in_roi', | |
'n_cells_in_biopsy', | |
'tumor_area', | |
'biopsy_area', | |
'eTILs (%)', | |
'etTILs (%)', | |
'esTILs (%)', | |
"eaTILs (1/${cal.getPixelWidthUnit()}^2)", | |
'easTILs (%)', | |
"eStroma (%)", | |
"etStroma (%)", | |
"esStroma (%)", | |
"eaStroma (1/${cal.getPixelWidthUnit()}^2)", | |
"easStroma (%)", | |
"stroma_purity_(cell_area_%)", | |
"stroma_purity_(tissue_area_%)", | |
"stroma_purity_density_in_tumor(#/um^2)", | |
"stroma_tils_ratio_area", | |
"stroma_tils_ratio_numbers" | |
] | |
new File(csv_path).withWriter { fileWriter -> | |
def csvFilePrinter = new CSVPrinter(fileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord(FILE_HEADER) | |
} | |
} | |
// TODO: Dynamic measurements | |
//https://www.imagescientist.com/creating-and-editing-measurements | |
// Loop through and add child/descendant counts | |
annotations.each { | |
if (it.getPathClass() != class_tumor_tissue) | |
return false | |
def tissue_ann = annotations.find { PathObject it2 -> (it2.getPathClass() == class_tissue & it2.getName().split('-')[-1] == it.getName().split('-')[-1]) } | |
// Double totalTissueArea = tissue_ann.getROI().getScaledArea(pixelWidth, pixelHeight) | |
if(tissue_ann != null) | |
QP.removeObject(tissue_ann, true) | |
QP.resolveHierarchy() | |
QP.fireHierarchyUpdate() | |
def non_tumor_ann = annotations.find { PathObject it3 -> it3.getPathClass() == class_other & it3.getName().split('-')[-1] == it.getName().split('-')[-1] } | |
Double totalTissueArea = it.getROI().getScaledArea(pixelWidth, pixelHeight) | |
def nCellsNotInROI = 0 | |
Double cellsNotInROIArea = 0.0 | |
if(non_tumor_ann != null){ | |
totalTissueArea += non_tumor_ann.getROI().getScaledArea(pixelWidth, pixelHeight) | |
def ann_cells = non_tumor_ann.getChildObjects() | |
nCellsNotInROI = ann_cells.size() | |
ann_cells.each { PathObject cell -> cellsNotInROIArea += cell.getROI().getScaledArea(pixelWidth, pixelHeight) } | |
} | |
def children = it.getChildObjects() | |
def nTumorCells = children.count { it.getPathClass() == class_tumor }.intValue() | |
def nNonTumorCellsInROI = children.count { it.getPathClass() != class_tumor }.intValue() | |
def totalTumorArea = it.getROI().getScaledArea(pixelWidth, pixelHeight) | |
def tumorCells = children.findAll { it.getPathClass() == class_tumor } | |
Double totalTumorCellArea = 0.0 | |
tumorCells.each { PathObject cell -> totalTumorCellArea += cell.getROI().getScaledArea(pixelWidth, pixelHeight) } | |
def nonTumorCellsInROI = children.findAll { it.getPathClass() != class_tumor } | |
Double nonTumorCellsInROIArea = 0.0 | |
nonTumorCellsInROI.each { PathObject cell -> nonTumorCellsInROIArea += cell.getROI().getScaledArea(pixelWidth, pixelHeight) } | |
def stromaCells = children.findAll { it.getPathClass() == class_stroma } | |
def nStromaCells = stromaCells.size() | |
Double totalStromaCellArea = 0.0 | |
stromaCells.each { PathObject cell -> totalStromaCellArea += cell.getROI().getScaledArea(pixelWidth, pixelHeight) } | |
def nonStromaCellsInROI = children.findAll { it.getPathClass() != class_stroma } | |
Double nonStromaCellsInROIArea = 0.0 | |
nonStromaCellsInROI.each { PathObject cell -> nonStromaCellsInROIArea += cell.getROI().getScaledArea(pixelWidth, pixelHeight) } | |
def nTILs = children.count { it.getPathClass() == class_tils }.intValue() | |
Double TILsArea = 0.0 | |
def TILs = children.findAll { it.getPathClass() == class_tils } | |
TILs.each { PathObject cell -> TILsArea += cell.getROI().getScaledArea(pixelWidth, pixelHeight) } | |
def eTILs = 100 * nTILs / (nTILs + nTumorCells) | |
def etTILs = 100 * nTILs / (nTumorCells + nonTumorCellsInROI.size()) | |
def esTILs = 100 * nTILs / nonTumorCellsInROI.size() | |
def eaTILs = nTILs / it.getROI().getScaledArea(pixelWidth, pixelHeight) | |
def easTILs = 100 * TILsArea / (it.getROI().getScaledArea(pixelWidth, pixelHeight) - totalTumorCellArea) | |
def cellularity_cell_area_perc = totalTumorCellArea / (totalTumorCellArea + nonTumorCellsInROIArea) * 100 | |
def cellularity_tissue_area_perc = totalTumorCellArea / totalTumorArea * 100 | |
def cellularity_density = nTumorCells / totalTumorArea | |
def purity_cell_area_perc = totalTumorCellArea / (totalTumorCellArea + nonTumorCellsInROIArea + cellsNotInROIArea) * 100 | |
def purity_tissue_area_perc = totalTumorCellArea / totalTissueArea * 100 | |
def purity_density = nTumorCells / totalTissueArea * 100 | |
def eStroma = 100 * nStromaCells / (nStromaCells + nTumorCells) | |
def etStroma = 100 * nStromaCells / (nTumorCells + nonTumorCellsInROI.size()) | |
def esStroma = 100 * nStromaCells / nonTumorCellsInROI.size() | |
def eaStroma = nStromaCells / it.getROI().getScaledArea(pixelWidth, pixelHeight) | |
def easStroma = 100 * totalStromaCellArea / (it.getROI().getScaledArea(pixelWidth, pixelHeight) - totalTumorCellArea) | |
def stroma_purity_cell_area_perc = totalStromaCellArea / (totalStromaCellArea + nonStromaCellsInROIArea + cellsNotInROIArea) * 100 | |
def stroma_purity_tissue_area_perc = totalStromaCellArea / totalTissueArea * 100 | |
def stroma_purity_density = nStromaCells / totalTissueArea | |
def stroma_tils_ratio_area = totalStromaCellArea / TILsArea | |
def stroma_tils_ratio_numbers = nStromaCells / nTILs | |
it.getMeasurementList().putMeasurement("Cellularity Cell Area (%)", cellularity_cell_area_perc) | |
it.getMeasurementList().putMeasurement("Cellularity Tissue Area (%)", cellularity_tissue_area_perc) | |
it.getMeasurementList().putMeasurement("Tumor Cell Density in Tumor (#cells/${cal.getPixelWidthUnit()}^2)", cellularity_density) | |
it.getMeasurementList().putMeasurement("Purity Cell Area (%)", purity_cell_area_perc) | |
it.getMeasurementList().putMeasurement("Purity Tissue Area (%)", purity_tissue_area_perc) | |
it.getMeasurementList().putMeasurement("Tumor Cell Density in Biopsy (#cells/${cal.getPixelWidthUnit()}^2)", purity_density) | |
it.getMeasurementList().putMeasurement("eStroma (%)", eStroma) | |
it.getMeasurementList().putMeasurement("etStroma (%)", etStroma) | |
it.getMeasurementList().putMeasurement("esStroma (%)", esStroma) | |
it.getMeasurementList().putMeasurement("eaStroma (1/${cal.getPixelWidthUnit()}^2)", eaStroma) | |
it.getMeasurementList().putMeasurement("easStroma (%)", easStroma) | |
it.getMeasurementList().putMeasurement("Stroma Purity Cell Area (%)", stroma_purity_cell_area_perc) | |
it.getMeasurementList().putMeasurement("Stroma Purity Tissue Area (%)", stroma_purity_tissue_area_perc) | |
it.getMeasurementList().putMeasurement("Stroma Cell Density in Biopsy (#cells/${cal.getPixelWidthUnit()}^2)", stroma_purity_density) | |
it.getMeasurementList().putMeasurement("Stroma/TILs ratio (Area)", stroma_tils_ratio_area) | |
it.getMeasurementList().putMeasurement("Stroma/TILs ratio (Numbers)", stroma_tils_ratio_numbers) | |
def tissue_area = totalTissueArea | |
def tumor_tissue_percentage = 100 * totalTumorArea / tissue_area | |
it.getMeasurementList().putMeasurement("Tumor/Tissue percentage (%)", tumor_tissue_percentage) | |
it.getMeasurementList().putMeasurement("Tumor Area (${cal.getPixelWidthUnit()}^2)", totalTumorArea) | |
it.getMeasurementList().putMeasurement("Tissue Area (${cal.getPixelWidthUnit()}^2)", totalTissueArea) | |
it.getMeasurementList().putMeasurement('eTILs (%)', eTILs) | |
it.getMeasurementList().putMeasurement('etTILs (%)', etTILs) | |
it.getMeasurementList().putMeasurement('esTILs (%)', esTILs) | |
it.getMeasurementList().putMeasurement("eaTILs (1/${cal.getPixelWidthUnit()}^2)", eaTILs) | |
it.getMeasurementList().putMeasurement('easTILs (%)', easTILs) | |
it.getMeasurementList().close() | |
new File(csv_path).withWriterAppend { fileWriter -> | |
def csvFilePrinter = new CSVPrinter(fileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord([imageName.toString(), it.name.split('-')[-1], cellularity_cell_area_perc, cellularity_tissue_area_perc, cellularity_density, purity_cell_area_perc, purity_tissue_area_perc, purity_density, tumor_tissue_percentage, nTumorCells, nTumorCells + nonTumorCellsInROI.size(), nTumorCells + nonTumorCellsInROI.size() + nCellsNotInROI, totalTumorArea, totalTissueArea, eTILs, etTILs, esTILs, eaTILs, easTILs, eStroma, etStroma, esStroma, eaStroma, easStroma, stroma_purity_cell_area_perc, stroma_purity_tissue_area_perc, stroma_purity_density, stroma_tils_ratio_area, stroma_tils_ratio_numbers]) | |
} | |
} |
This file contains 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
""" | |
This script exports all annotations of a QuPath image to a `.geojson` file. | |
""" | |
import qupath.lib.io.GsonTools | |
import qupath.lib.scripting.QP | |
import qupath.lib.common.GeneralTools | |
// false results in smaller file sizes and thus faster loading times, at the cost of nice formatting | |
boolean prettyPrint = false | |
// Create the gson tool | |
def gson = GsonTools.getInstance(prettyPrint) | |
// Read the current image data | |
def imageData = QP.getCurrentImageData() | |
// Get the image name | |
def imageName = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName()) | |
// Get all annotations | |
annotations = QP.getAnnotationObjects() | |
// Define and create the output folder | |
def o_folder = QP.buildFilePath(QP.PROJECT_BASE_DIR, "annotations") | |
new File(o_folder).mkdirs() | |
// Define the output filepath | |
filePath = o_folder + '/' + imageName + '.geojson' | |
File file = new File(filePath) | |
// Write the annotations | |
file.withWriter('UTF-8') { | |
gson.toJson(annotations,it) | |
} |
This file contains 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 org.apache.commons.csv.CSVFormat | |
import org.apache.commons.csv.CSVPrinter | |
import qupath.lib.scripting.QP | |
import java.util.stream.Collectors | |
import qupath.lib.objects.PathCellObject | |
import qupath.lib.gui.tools.MeasurementExporter | |
// Get the list of all images in the current project | |
def project = QP.getProject() | |
def imageList = project.getImageList() | |
output_directory = QP.buildFilePath(QP.PROJECT_BASE_DIR, 'output') // Output directory | |
QP.mkdirs(output_directory) | |
def csvFileNamePixelSize = QP.buildFilePath(output_directory, 'pixel_size.csv') | |
if (!(new File(csvFileNamePixelSize)).exists()) { | |
new File(csvFileNamePixelSize).withWriter { fileWriter -> | |
def FILE_HEADER_PIXEL_SIZE = ['imageName','pixelWidth','pixelHeight', 'imageWidth', 'imageHeight'] | |
def csvFilePrinter = new CSVPrinter(fileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord(FILE_HEADER_PIXEL_SIZE) | |
} | |
} | |
def csvCellMeasurements = QP.buildFilePath(output_directory, 'cell_measurements.csv') | |
def headerWritten = false | |
def csvCellX = QP.buildFilePath(output_directory, 'cell_polygons_x.csv') | |
def csvCellY = QP.buildFilePath(output_directory, 'cell_polygons_y.csv') | |
if (!(new File(csvCellX)).exists()) { | |
new File(csvCellX).withWriter { fileWriter -> | |
def FILE_HEADER = ['',''] | |
def csvFilePrinter = new CSVPrinter(fileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord(FILE_HEADER) | |
} | |
} | |
if (!(new File(csvCellY)).exists()) { | |
new File(csvCellY).withWriter { fileWriter -> | |
def FILE_HEADER = ['',''] | |
def csvFilePrinter = new CSVPrinter(fileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord(FILE_HEADER) | |
} | |
} | |
def csvCellClassification = QP.buildFilePath(output_directory, 'cell_classification.csv') | |
if (!(new File(csvCellClassification)).exists()) { | |
new File(csvCellClassification).withWriter { fileWriter -> | |
def FILE_HEADER = ['imageName','annotationID','cellID','class','centroid_x','centroid_y'] | |
def csvFilePrinter = new CSVPrinter(fileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord(FILE_HEADER) | |
} | |
} | |
/* | |
* Iterate through every image in project | |
*/ | |
for(image in imageList){ | |
//-------------------------------------------------------------------------- | |
/* | |
* Get image server and annotation details | |
*/ | |
def imageData = image.readImageData() | |
def hierarchy = imageData.getHierarchy() | |
def server = imageData.getServer() | |
def serverPath = server.getPath() | |
def imageName = image.getImageName().take(image.getImageName().lastIndexOf('.')) | |
def annotations = hierarchy.getAnnotationObjects() | |
def serverHeight = server.getHeight() | |
def serverWidth = server.getWidth() | |
//-------------------------------------------------------------------------- | |
/* | |
* Create CSV file to save pixel size | |
*/ | |
def cal = server.getPixelCalibration() | |
double pixelWidth = cal.getPixelWidthMicrons() | |
double pixelHeight = cal.getPixelHeightMicrons() | |
new File(csvFileNamePixelSize).withWriterAppend { fileWriter -> | |
def csvFilePrinter = new CSVPrinter(fileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord([imageName, pixelWidth, pixelHeight, serverWidth, serverHeight]) | |
} | |
//-------------------------------------------------------------------------- | |
/* | |
* Processing | |
*/ | |
annotationLoop: | |
for(ann in annotations) { | |
def annID = ann.getName() | |
if (ann.getPathClass() != QP.getPathClass("Tumor")) { | |
continue | |
} | |
def detections = hierarchy.getObjectsForROI(qupath.lib.objects.PathCellObject, ann.getROI()) | |
// detections = hierarchy.getObjectsForROI(null, parent.getROI()).findAll { it.isDetection() } | |
if (detections.isEmpty()) { | |
println('Annotation #' + annID + ' has no detections. Skipping...') | |
continue | |
} | |
/* | |
* Save all cells of the annotation | |
*/ | |
println('Writing cells of annotation #' + annID) | |
// Export detected cell polygons | |
detections.eachWithIndex { PathCellObject det, index -> | |
// Append cell classification data to a global csv file | |
new File(csvCellClassification).withWriterAppend { csvFileWriter -> | |
def csvFilePrinter = new CSVPrinter(csvFileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord([imageName, annID, index, det.getPathClass(), det.getNucleusROI().getCentroidX(), det.getNucleusROI().getCentroidY()]) | |
} | |
// Cell x, y coordinates | |
def points = det.getNucleusROI().getAllPoints() | |
def coords_x = points.collect {element -> element.getX()} | |
def coords_y = points.collect {element -> element.getY()} | |
new File(csvCellX).withWriterAppend { csvFileWriter -> | |
def csvFilePrinter = new CSVPrinter(csvFileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord(coords_x) | |
} | |
new File(csvCellY).withWriterAppend { csvFileWriter -> | |
def csvFilePrinter = new CSVPrinter(csvFileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord(coords_y) | |
} | |
if(!headerWritten) { | |
new File(csvCellMeasurements).withWriterAppend { csvFileWriter -> | |
def csvFilePrinter = new CSVPrinter(csvFileWriter, CSVFormat.DEFAULT) | |
csvFilePrinter.printRecord(['imageName', 'annotationID', 'cellID', 'cellCentroidX', 'cellCentroidY', 'cellClassification'] + det.getMeasurementList().getMeasurementNames()) | |
} | |
headerWritten = true | |
} | |
new File(csvCellMeasurements).withWriterAppend { csvFileWriter -> | |
def csvFilePrinter = new CSVPrinter(csvFileWriter, CSVFormat.DEFAULT) | |
ArrayList _record = [imageName, annID, index, det.getNucleusROI().getCentroidX(), det.getNucleusROI().getCentroidY(), det.getPathClass()] | |
for (measurementName in det.getMeasurementList().getMeasurementNames()) { | |
_record.add(det.getMeasurementList().getMeasurementValue(measurementName)) | |
} | |
csvFilePrinter.printRecord(_record) | |
} | |
} | |
} | |
} |
This file contains 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
""" | |
This script exports a (binary) mask for all annotations in QuPath based on | |
the specified class (i.e. `Tissue` for tissue masks or `Tumor` for tumor masks). | |
One can augment the masks with additional or all classes by adding more `.addLabel()` calls. | |
The exported mask regards the downsampled slide by a factor of 32. | |
""" | |
import qupath.lib.scripting.QP | |
import qupath.lib.common.ColorTools | |
import qupath.lib.common.GeneralTools | |
import qupath.lib.images.servers.LabeledImageServer | |
// Which class to create the annotation masks for | |
def class_name = "Tissue" | |
// Read the current image data | |
def imageData = QP.getCurrentImageData() | |
// Get the image name | |
def imageName = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName()) | |
// Define and create the output folder | |
def o_folder = QP.buildFilePath(QP.PROJECT_BASE_DIR, class_name.toLowerCase() + '_masks/') | |
new File(o_folder).mkdirs() | |
// Define the output filepath | |
filePath = o_folder + '/' + imageName + '.png' | |
// Define how much to downsample during export | |
double downsample = 32 | |
// Create an ImageServer where the pixels are derived from annotations | |
def labelServer = new LabeledImageServer.Builder(imageData) | |
.backgroundLabel(0, ColorTools.BLACK) // Specify background label (usually 0 or 255) | |
.downsample(downsample) // Choose server resolution; | |
.addLabel(class_name, 1) // Choose output labels (the order matters!) | |
.multichannelOutput(false) // If true, each label refers to the channel of a multichannel binary image | |
.build() | |
// Write the image | |
QP.writeImage(labelServer, filePath) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment