QuPath Multiplex Classifier

Hi all!
This post is for people who want another option for classification within QuPath, in addition to the machine learning classifiers, quick, single line classifier scripts (also covered in previous link), more complicated classifier scripts (my list here, here in detail from Pete with automatic thresholds, in many places on the old Google forum), or the built in single feature classifier (Classify menu).

Edit: Since it was buried in the text, I am also placing the link to the GUI cell classifier script here.

My variant creates a distinct class for every possible combination of base class types, which can help when running classifications on multiple images, some of which may not have a given cell type. Having some different numbers of columns per image can make lining up data export… tricky. In addition, while most basic classifiers can handle one or two colors easily, the total number of base classes starts to result in a very large number of possible combinations. 2^number of clases, in fact. At one class feature (say nuclear DAB intensity), you only have 2 classes, positive and negative. Throw in some red AP staining, and you have 2^2, or 4 classes (negative, brown nucleus, red cytoplasm, brown+red). Once you get to 6 base classes, that is 64 potential classes, which I don’t think anyone would want to deal with as far as training a machine learning classifier.

So if you are still interested, here we go! This will all be done on the Luca 8 channel image used in Pete’s examples from his blog, and is available here.
This is the base image, for reference.

As a mild segue, for a sample like this I often want separate regions of interest, and I use this script to generate annotations based on a particular channel. There is one line in the script (114) where getAvailableChannels() needs to be replaced with availableChannels() depending on which version of QuPath you are using. An error will let you know if you need to change it! Another error you might run into is that the downsample size needs to be high enough to send the entire image to ImageJ… meaning frequently 8-15 in whole slide samples. In the milestone release of QuPath, you should be able to use the pixel classifier to do something similar, and with greater precision.

I also like to use the brush tool to draw a small area to determine what values I should enter to remove small bits of annotation, or the size holes I want to fill in.
Which I use to get this:

And then merge the results and generate an inverse annotation to look at the remaining areas:

At this point, I perform my cell detection, and then run the classifier script that has a GUI. There is another version here where you can load a saved classifier without any UI, which enables you to run the classifier across an entire project as part of a script.

The first window in the upper right allows you to select how many base classes/features you are interested in using to create your classifier. This image has 6 channels of interest (plus DAPI and an empty [AF?] channel), so I will choose 6. Upon clicking Start Classifying, I get the second window in the upper left. The image shows me starting with the first channel (dropdown on the left) that I know by inspection to be a cytoplasmic marker; I open Measure->Measurement Maps (shown lower right) and adjust the right hand slider so that the cells I feel are positive are all shown in red. I then use that value as my lower threshold for positivity by adding it to the left “Lower Threshold” window. I normally do not use the Upper threshold window as it defaults to the maximum pixel value for your image, but it might allow you to eliminate some bright artifacts. Finally, I name the class and select a color from the dropdown on the right. I stroooooooongly recommend short class names, especially if you will have cells that are positive for 3 or more markers. The names get quite long.
After going through that process for all channels of interest, I name and save the classifier for later use, and click Run Classifier. Considering moving that button more to the center…

Excel output for the two annotations shown below… but those are just counts. I usually am more interested in percentages or densities. There is another script for that here, which will add percent positive and cells/mm^2 of each class to the parent annotation. Much easier than calculating things one at a time. Here are some of the “Tumor” annotation measurements.

Ok, so now there are potentially 64 classes, and this is all very hard to look at. Pete had some suggestions that definitely help with that in his post on classifying linked above, but I now have the added problem that… well, maybe my “C1c, C3n, C4c positive” cells are actually T-cells? This script helps both problems by allowing you to rename or recolor any class. In the following example I turned all of my tumor cells that were only C6c positive (Teal) black, so that any cells infiltrating the tumor would be emphasized. I also called them BadCells just because. As you can see in the image, the C6 cells no longer show up.

Another, more mature, way to handle this might be to add a class to my Annotations tab list of classes and toggle it off, as shown.
11%20toggle%201 11%20toggle%202

Apologies for how long this was, though I am sure I forgot some things along the way. So please do ask if you have any questions or anything could be clearer! Scripts were all run in QuPath 0.1.3, so some code alterations may be necessary for different versions, as mentioned.

If you are running a project like this across multiple images, I strongly recommend checking out Pete’s guide to creating a single spreadsheet from a project of images here, and here.



Ah, and one thing to note, the addition of the measurements to the annotation creates static values. So if I rename one class, it will not automatically rename the associated percentage/density measurement in the annotation. I would recommend doing any renaming prior to running the measurement script. Recoloring shouldn’t matter.

Thanks! This is amazingly helpful!

1 Like

This is wonderful, thanks for your efforts!

1 Like


This is fantastic, thanks! I couldn’t manage to get % positive and cell/mm^2 using by adding the script you mentioned. I don’t get any errors, but only counts. Could it be beacuse it’s a TMA, so in’t not objects annotations, but TMA annotations? Do I need to change it?
for (annotation in getAnnotationObjects()){



TMA objects are not annotations. The measurements will get added to the annotation within the TMA Core object if you ran some kind of tissue detection on the whole TMA array, but otherwise you are correct that it won’t work.
If you look at the next script down from that link, it shows an older version that cycles through the cores to give percentages only, based off of the classes present in a list (I hadn’t figured out how to cycle through all classes at that point). You can combine the two, or use something like
for (core in hierarchy.getTMAGrid().getTMACoreList())
which assumes you defined the hierarchy previously. Actually it might be simpler to leave it as
for (annotation in hierarchy.getTMAGrid().getTMACoreList())
The variable name annotation doesn’t really relate to it being a core or an annotation.

1 Like

You might want to edit out the cells/mm^2 section as well, since that won’t make much sense for a TMA core, unless you edit the script to somehow pick that up from a tissue detection annotation.

I just noticed upon revisiting after mentioning this in another thread, that I hadn’t made note of the fact that there is a run for project version of the tissue detection script as well, found here.

It wouldn’t be so useful to have to run that manually for each image over hundreds of images! You do need to plug in all of the values manually, so it may help to take a Snip of your values from the GUI version as reference in case you accidentally close the window.

These step by step instructions are really great ! Thank you.

1 Like

As noted in this post, most of the scripts are intended to work in projects, and will fail due to file path names when run on single images.

@Research_Associate, I have got a spreadsheet with each cell and its positive/negative class per channel. Have you done any spatial analysis after you phenotype each detection! Do you have any information or links that can help me get started. I am looking to write a MatLab script that can plot relation as connections to detection by distance.

Thank you,

Only thought R scripting so far, there are modules there for nearest neighbor analysis and other spatial quantification methods. I was planning on writing a groovy script for a couple of types of neighbor based analysis in QuPath at some point, but was also going to wait until the coding stabilized in 0.2.0m#.

The closest thing you can get that is currently built in would the the Delaunay cluster analysis, which might tell you something if you look at sizes of clusters (within the same class). I am also still looking into all of the variables that might need to be taken into account to make something like that useful, as some people might just want k-NN = 1, some might want a neighbors within a certain distance, maybe proportions of one or more kind of neighbor within a set distance, etc.

Almost forgot, but in m2/m3, there is also the Distance transform, which only works for annotations (distance to a class of annotation). However, you can also tun anything you want into an annotation with a script…

Mike, thank you for that information. I am in very early stages of learning different type of spatial analysis that can be useful. I will look in to the Delaunay triangulation via a MatLab script and the Distance transform via QuPath.

Is there a R script that you can share which enables nearest neighbor analysis !

Unfortunately it has been a very long time since I ran anything like that, so I am guessing I would have trouble figuring out what I even did with my scripts. You are probably better off looking at something like this:

to start your own basic script. There are enough different options for that kind of analysis, you will probably want to tailor any script to your own needs, anyway. I also can’t seem to find any of my old R scripts at the moment.

thank you for your great work!

I am having trouble with two things:

  • I cannot get the script running in the new version v0.2.0-m3.
  • in v0.2.0-m2 I am having trouble loading a saved classifier:

I get an error: Qupath Exception which I attached as a txt file.


log.txt (153.2 KB)

1 Like

I’ll take a look, but I am a bit hesitant to fix the script with the scripting changes that have already been made, and will be made in the future that will break many, many scripts :slight_smile: At some point I hope to comb through and fix a bunch of the scripts for the new stable version (and possibly make a semi-decent scripting guide), but between the new version of Groovy and other changes, I have a feeling I won’t have the time to update everything. If the script cannot work in m2 and m3, I’ll try and at least add a warning for future users that it won’t work in those versions. For the moment I am still using 0.1.3 for most projects. Thanks for the heads up.

** I can confirm I am seeing the same errors in m2 and m3.

It seems like the m2 error is due to access to the text field, but you can temporarily bypass that by either running the non-GUI script, or directly calling the classifier by name, manually. That involves editing line 69 from:

    path = buildFilePath(PROJECT_BASE_DIR, "classifiers",classFile.getText())
    new File(path).withObjectInputStream {
        cObj = it.readObject()

to “MyClassifier” or whatever your classifier might be called.

    path = buildFilePath(PROJECT_BASE_DIR, "classifiers","MyClassifier")
    new File(path).withObjectInputStream {
        cObj = it.readObject()

If you don’t need the GUI, that version of the script should still work for for m2.


Interestingly enough, that same error on loading does not seem to happen in m3, so I think I am going to leave it alone. There the problem comes from a change to:
maxPixel =
around line 34. The metadata type has changed, and I don’t have a quick and easy way to calculate it at the moment. If you set it to a value that is the max for your data set, you should be fine.

def server = getCurrentImageData().getServer()
//Upper thresholds will default to the max bit depth, since that is likely the most common upper limit for a given image.
maxPixel = 250000
positive = []

Worked for me.

Further edit, Pete pointed me in the right direction and that line can also be fixed with:
maxPixel = Math.pow((double) 2,(double)server.getPixelType().bitsPerPixel())-1

1 Like

Unfortunately I can’t edit the top post, but one danger about this script that I recently became aware of… when running a script for multiple images, it will accurately determine all classes, BUT, when running it for project for a bunch of images, there is no guarantee that the classes will come out named in the same order.

In other words, “Ch1, Ch2 positive” might show up in one image, while “Ch2, Ch1 positive” might show up in the next, depending on the order in which the classes were encountered. Still thinking about how to handle this for a project, since there is no guarantee all potential classes will show up in any given image, not even the first. Might have to try and order the class list alphabetically.

Recently move the m4 from m2 and noticed that I’m getting script errors running the percentage and density script that I love.
ERROR: Error at line 9: No signature of method: qupath.lib.images.servers.bioformats.BioFormatsImageServer.getPixelHeightMicrons() is applicable for argument types: () values:

I added a version of the script that works for M5 (testing) here. It seems to work for M4 as well though I have not tested it extensively.

1 Like