Skip to content

Instantly share code, notes, and snippets.

@tsikup
Last active September 22, 2023 09:32
Show Gist options
  • Save tsikup/5b43ed20ec8886794af99ed3ae5273ae to your computer and use it in GitHub Desktop.
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.
A collection of groovy and python (paquo) scripts used for image analysis in QuPath.
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 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)
}
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 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