Qupath - Scripting the Auto- stain vector estimation

Hi there,

I am trying to script a cell detection + subsequent classification, but seem to be unable to implement Analyze -> Preprocessing -> Estimate stain vectors -> Auto (button at the bottom). Obviously, the script I extract from the workflow only takes the resulting values and applies those to future images. Is there any way to script this auto-stain vector estimation?

Thanks so much
TM

I’m afraid there isn’t, because it isn’t really intended to use the command in this way. It’s expected that the user will always manually select the representative region – without this human intervention, the command can’t really be trusted not to give some very wrong results (e.g. picking up totally wrong colors).

1 Like

on the one hand - understandable, but since I have already selected the regions manually, and would just need to automate the readjustment of the stain vectors (= clicking on preprocessing -> vectors -> Auto) in every single image, there still is no possibility for scripting this - right?
But anyway, thanks :wink:

Ah, I see I underestimated your annotations :slight_smile:

Still no easy way to do it. The main entry point in the code does a bit or processing alongside the interactive bit:

Although the main work is independent of UI here:

So… scriptable in principle (everything is!), but not really designed for it. Might take 10 lines of code or so.

Depends then how much you really want it :slight_smile:

I’m also interested in doing this, has anyone managed to make a script?

I created the script below in v0.2.0-m5 but I haven’t tested it in v0.2.2 but maybe it can provide a starting point for you. The rectangle annotation will need to be on the image prior to running the script. The result is a .txt file that contains the RGB values of all the images. You can modify it to include/exclude the modal values.

Hope this can help!

import static qupath.lib.gui.scripting.QPEx.*
import qupath.lib.gui.scripting.QPEx
import java.awt.image.BufferedImage;
import qupath.lib.EstimateStainVectors;
import qupath.lib.common.ColorTools;
import qupath.lib.analysis.algorithms.EstimateStainVectors
import qupath.lib.color.ColorDeconvolutionHelper;
import qupath.lib.color.ColorDeconvolutionStains;
import qupath.lib.color.StainVector;
import qupath.lib.common.GeneralTools;
import qupath.lib.gui.QuPathGUI;
import qupath.lib.gui.commands.interfaces.PathCommand;
import qupath.lib.gui.plots.ScatterPlot;
import qupath.lib.images.ImageData;
import qupath.lib.objects.PathObject;
import qupath.lib.plugins.parameters.ParameterList;
import qupath.lib.regions.RegionRequest;
import qupath.lib.roi.RectangleROI;
import qupath.lib.gui.scripting.QPEx
import qupath.lib.roi.interfaces.ROI;

ImageData<BufferedImage> imageData = QPEx.getCurrentImageData();
if (imageData == null || !imageData.isBrightfield() || imageData.getServer() == null || !imageData.getServer().isRGB()) {
    DisplayHelpers.showErrorMessage("Estimate stain vectors", "No brightfield, RGB image selected!");
    return;
}
ColorDeconvolutionStains stains = imageData.getColorDeconvolutionStains();
if (stains == null || !stains.getStain(3).isResidual(


)) {
    DisplayHelpers.showErrorMessage("Estimate stain vectors", "Sorry, stain editing is only possible for brightfield, RGB images with 2 stains");
    return;
}
    
///////Select objects as a list///////   
cores_name = ['PathAnnotationObject']; 

selectObjects { p -> cores_name.contains(p.getDisplayedName().toString()) == true };
cores_list = getSelectedObjects();
print(String.format("Selected objetcs: %s", cores_list.asList()))

//////////////////////////////////////////////////////////////////////////////////////////////
def outputFile = String.format('PATH_TO_DIR/%s_%sestimate_stain.txt', getProjectEntry().getImageName(), cores_list.asList()); //change the path if needed
def fileResults = new File(outputFile);
String delimiter = '\t';
    
def results = new ArrayList<Map<String, String>>()
def allColumns = new LinkedHashSet<String>()

def columns = ['Core', 'Hematoxylin', 'DAB', 'Residual', 'Background']
allColumns.addAll(columns)    

for (int i = 0; i < cores_list.size(); i++){
	
    PathObject pathObject = cores_list[i];
    ROI roi = pathObject == null ? null : pathObject.getROI();
    if (roi == null)
        roi = new RectangleROI(0, 0, imageData.getServer().getWidth(), imageData.getServer().getHeight());

    int MAX_PIXELS = 4000*4000;		
    double downsample = Math.max(1, Math.sqrt((roi.getBoundsWidth() * roi.getBoundsHeight()) / MAX_PIXELS));
    RegionRequest request = RegionRequest.createInstance(imageData.getServerPath(), downsample, roi);
    BufferedImage img = imageData.getServer().readBufferedImage(request);
    		
    // Apply small amount of smoothing to reduce compression artefacts
    img = EstimateStainVectors.smoothImage(img);
    // Check modes for background
    int[] rgb = img.getRGB(0, 0, img.getWidth(), img.getHeight(), null, 0, img.getWidth());
    int[] rgbMode = EstimateStainVectors.getModeRGB(rgb);
    int rMax = rgbMode[0];
    int gMax = rgbMode[1];
    int bMax = rgbMode[2];
    double minStain = 0.05;
    double maxStain = 1.0;
    double ignorePercentage = 1.0;


    ColorDeconvolutionStains stain_vec = EstimateStainVectors.estimateStains(img, stains, false)
        
    def hema_vec = stain_vec.getStain(1).toString().split(' ')[1..-1];
    def DAB_vec = stain_vec.getStain(2).toString().split(' ')[1..-1];
    def resi_vec = stain_vec.getStain(3).toString().split(' ')[1..-1];
    def background_rgb = rgbMode.toString().split(' ')[0..2];
    
    def map = ['File name': fileResults.getName()];
    map[columns[0]] = cores_name[i];
    map[columns[1]] = hema_vec.asList();
    map[columns[2]] = DAB_vec.asList();
    map[columns[3]] = resi_vec.asList();
    map[columns[4]] = background_rgb.asList();
    
    results.add(map);
}

fileResults.withPrintWriter {
    def header = String.join(delimiter, allColumns)
    it.println(header)
    // Add each of the results, with blank columns for missing values
    for (result in results) {
        for (column in allColumns) {
            it.print(result.getOrDefault(column, ''))
            it.print(delimiter)
        }
        it.println()
    }
}  

print('Done')

1 Like

The only changes that need to be made are the removal of two import statements that are not ever used, so it doesn’t impact the script:

import static qupath.lib.gui.scripting.QPEx.*
import qupath.lib.gui.scripting.QPEx
import java.awt.image.BufferedImage;
import qupath.lib.EstimateStainVectors;
import qupath.lib.common.ColorTools;
import qupath.lib.analysis.algorithms.EstimateStainVectors
import qupath.lib.color.ColorDeconvolutionHelper;
import qupath.lib.color.ColorDeconvolutionStains;
import qupath.lib.color.StainVector;
import qupath.lib.common.GeneralTools;
import qupath.lib.gui.QuPathGUI;
//import qupath.lib.gui.commands.interfaces.PathCommand;
//import qupath.lib.gui.plots.ScatterPlot;
import qupath.lib.images.ImageData;
import qupath.lib.objects.PathObject;
import qupath.lib.plugins.parameters.ParameterList;
import qupath.lib.regions.RegionRequest;
import qupath.lib.roi.RectangleROI;
import qupath.lib.gui.scripting.QPEx
import qupath.lib.roi.interfaces.ROI;

ImageData<BufferedImage> imageData = QPEx.getCurrentImageData();
if (imageData == null || !imageData.isBrightfield() || imageData.getServer() == null || !imageData.getServer().isRGB()) {
    DisplayHelpers.showErrorMessage("Estimate stain vectors", "No brightfield, RGB image selected!");
    return;
}
ColorDeconvolutionStains stains = imageData.getColorDeconvolutionStains();
if (stains == null || !stains.getStain(3).isResidual(


)) {
    DisplayHelpers.showErrorMessage("Estimate stain vectors", "Sorry, stain editing is only possible for brightfield, RGB images with 2 stains");
    return;
}
    
///////Select objects as a list///////   
cores_name = ['PathAnnotationObject']; 

selectObjects { p -> cores_name.contains(p.getDisplayedName().toString()) == true };
cores_list = getSelectedObjects();
print(String.format("Selected objetcs: %s", cores_list.asList()))

//////////////////////////////////////////////////////////////////////////////////////////////
def outputFile1 = String.format('/%s_%s estimate_stain.txt', getProjectEntry().getImageName(), cores_list.asList()); //change the path if needed
def outputFile =  buildFilePath(PROJECT_BASE_DIR, outputFile1)
def fileResults = new File(outputFile);
String delimiter = '\t';
    
def results = new ArrayList<Map<String, String>>()
def allColumns = new LinkedHashSet<String>()

def columns = ['Core', 'Hematoxylin', 'DAB', 'Residual', 'Background']
allColumns.addAll(columns)    

for (int i = 0; i < cores_list.size(); i++){
	
    PathObject pathObject = cores_list[i];
    ROI roi = pathObject == null ? null : pathObject.getROI();
    if (roi == null)
        roi = new RectangleROI(0, 0, imageData.getServer().getWidth(), imageData.getServer().getHeight());

    int MAX_PIXELS = 4000*4000;		
    double downsample = Math.max(1, Math.sqrt((roi.getBoundsWidth() * roi.getBoundsHeight()) / MAX_PIXELS));
    RegionRequest request = RegionRequest.createInstance(imageData.getServerPath(), downsample, roi);
    BufferedImage img = imageData.getServer().readBufferedImage(request);
    		
    // Apply small amount of smoothing to reduce compression artefacts
    img = EstimateStainVectors.smoothImage(img);
    // Check modes for background
    int[] rgb = img.getRGB(0, 0, img.getWidth(), img.getHeight(), null, 0, img.getWidth());
    int[] rgbMode = EstimateStainVectors.getModeRGB(rgb);
    int rMax = rgbMode[0];
    int gMax = rgbMode[1];
    int bMax = rgbMode[2];
    double minStain = 0.05;
    double maxStain = 1.0;
    double ignorePercentage = 1.0;


    ColorDeconvolutionStains stain_vec = EstimateStainVectors.estimateStains(img, stains, false)
        
    def hema_vec = stain_vec.getStain(1).toString().split(' ')[1..-1];
    def DAB_vec = stain_vec.getStain(2).toString().split(' ')[1..-1];
    def resi_vec = stain_vec.getStain(3).toString().split(' ')[1..-1];
    def background_rgb = rgbMode.toString().split(' ')[0..2];
    
    def map = ['File name': fileResults.getName()];
    map[columns[0]] = cores_name[i];
    map[columns[1]] = hema_vec.asList();
    map[columns[2]] = DAB_vec.asList();
    map[columns[3]] = resi_vec.asList();
    map[columns[4]] = background_rgb.asList();
    
    results.add(map);
}

fileResults.withPrintWriter {
    def header = String.join(delimiter, allColumns)
    it.println(header)
    // Add each of the results, with blank columns for missing values
    for (result in results) {
        for (column in allColumns) {
            it.print(result.getOrDefault(column, ''))
            it.print(delimiter)
        }
        it.println()
    }
}  

print('Done')

I adjusted the write path as well.

1 Like

Also, this might be terrible, but you can also immediately update the current image using:
imageData.setColorDeconvolutionStains(stain_vec)
@petebankhead, what would be the easiest way to update only the background? For example, if I wanted to keep the color vectors all the same, but pull the background per image from an annotation dilation around the tissue.
Right now I am thinking editing a setColorDeconvolutionStains('{"Name" : line with one or three string variables for the background, but wasn’t sure if there was a neater way to do that.

Thanks for this. If I’ve understood correctly the script outputs the values from the automatic stain vector estimation in a txt file? Is it possible to automate the use of those values to set the colour deconvolution?