StarDist cell detection for Hematoxylin channel

Dear colleagues

I am currently working with the StarDist cell detection within QuPath which is basically doing a great job so far!

I am working with brightfield H-DAB immunohistochemistry images, thus the pretrained H&E model is obviously not perfectly trained to detect the nuclei in such images (but still doing surprisingly well…).

So I thought it would be ideal if I could apply the Fluorescence-trained model and apply it on the Hematoxylin channel in Brightfield. However I did not yet manage to get the (prewritten) code the right way to do so for the line (bold) :

import qupath.tensorflow.stardist.StarDist2D

// Specify the model directory (you will need to change this!)
def pathModel = ‘path to my model’
double originalPixelSize = getCurrentImageData().getServer().getPixelCalibration().getAveragedPixelSizeMicrons();

def stardist = StarDist2D.builder(pathModel)
.threshold(0.5) // Probability (detection) threshold
.channels(0) // Select detection channel
.normalizePercentiles(1, 99) // Percentile normalization
.pixelSize(originalPixelSize) // Resolution for detection
.cellExpansion(3.0) // Approximate cells based upon nucleus expansion
.cellConstrainScale(1.5) // Constrain cell expansion using nucleus size
.measureShape() // Add shape measurements
.measureIntensity() // Add cell measurements (in all compartments)
.includeProbability(true) // Add probability as a measurement (enables later filtering)
.build()

// Run detection for the selected objects
def imageData = getCurrentImageData()
def pathObjects = getSelectedObjects()
if (pathObjects.isEmpty()) {
Dialogs.showErrorMessage(“StarDist”, “Please select a parent object!”)
return
}
stardist.detectObjects(imageData, pathObjects)
println ‘Done!’

I highly appreciate any help!

Best regards,
Luke

Hi @microluke add this line to the top of your script file

import qupath.lib.images.servers.*

and replace the .channels(0) with

.channels(ColorTransforms.createColorDeconvolvedChannel(getCurrentImageData().getColorDeconvolutionStains(), 1))
2 Likes

Thanks Pete! This script works like a charm, and is the most powerful way to use this tool in brightfield images of any staining.

One more issue: Is there a way to set a channel intensity threshold, rather than a probability threshold with StarDist? This would help to avoid detecting cells at the border of DAB-stained regions (which appear blue-ish, compare with images: 2nd is Hematoxylin channel only).

You might have an easy time removing something like that afterwards with a classifier. At least that has been my usual approach, though there are ImageOps methods that might help (adjust the intensity that is passed to StarDist) if you want to get really complicated.

1 Like

Great!

I’m afraid not, that info isn’t directly available at the point where the cells are generated and I can’t think of a straightforward way to integrate it with the probability information. For now, you’d need to filter out the false detections afterwards based upon the hematoxylin measurements made from the cells (although they will still have had their influence on cell expansion).

1 Like

Thanks @petebankhead and @Research_Associate for the insight. It will be an easy task to filter out these detections as their Nucleus’ Hematoxylin OD mean is actually negative all through… That’s just also why I thought it would be pretty to not even detect them :slight_smile:

1 Like

Hmmm, with the caveat that I haven’t really tried it then you might find a benefit in preprocess to clip the negative values first.

It seems that’s an approach I’d used before to deconvolve channels (albeit then forgotten I’d posted it):

Sadly I didn’t think to write a clip(0, 100) op at the time, so you’d need to hack something more awkward from what is available like

import qupath.lib.images.servers.*

// Set everything up
def pathModel = '/Users/pete/Documents/Git/others/stardist-imagej/src/main/resources/models/2D/dsb2018_heavy_augment'
def stardist = StarDist2D.builder(pathModel)
        .preprocess(
            ImageOps.Channels.deconvolve(stains),
            ImageOps.Channels.extract(0),
            ImageOps.Core.multiply(100),
            ImageOps.Core.ensureType(PixelType.UINT8),
            ImageOps.Core.ensureType(PixelType.FLOAT32),
            ImageOps.Core.divide(100)
         ) // Optional preprocessing (can chain multiple ops)
//        .normalizePercentiles(1, 99)         // Percentile normalization
        .pixelSize(0.5)              // Resolution for detection
        .includeProbability(true)     // Add probability as a measurement (enables later filtering)
        .threshold(0.5)             // Probability (detection) threshold
        .build()
3 Likes

This is realy awesome and is delivering very accurate results even in densely packed, multiple stained image areas. Thank you!

2 Likes