< Back to QuPath article list

Creating and Editing Measurements

Measurements are really what we are all here for, right? QuPath provides a lot of simple, straightforward measurements built in and ready to go, but here we will take a look at ways to expand our ability to measure!

There are two primary types of measurements in QuPath, as I see it.

  1. Well, I will immediately backtrack and expand the list to three, in order to include Name and Class fields. Both of these you can essentially write strings to, and if you are classifying objects with purpose, the Name field is really your only option to store string data about an object.

  2. The standard fixed measurements - Detections have a lot of these, they do not change if you do something to the object.

  3. Dynamic measurements, most commonly seen in annotations - centroids, perimeter or the area. These measurements need to update on the fly as the user draws and edits an object.

 
  1. Names

    I showed naming objects briefly in a prior example, but the main thing you need to be sure of when using setName is that you have the right object, and that what you are putting into the Name field is a string. toString() is your friend!

 
listOfStrings = ["This", "is", "a", "strange", "script", 1] getCellObjects().eachWithIndex{cell, x ->     position = x%6     cell.setName(listOfStrings[position]) }

This script will throw an error, because 1 is not a string, it is an integer. Using our friend toString() gets around that.

 
istOfStrings = ["This", "is", "a", "strange", "script", 1] getCellObjects().eachWithIndex{cell, x ->     position = x%6     cell.setName(listOfStrings[position].toString()) }

Notice I am calling toString on the object in position within listOfStrings. What I end up with is all of my cells being named one of those strings. Not terribly useful - but fun. It also demonstrates how to access a list of fixed strings, and apply it to your objects. listOfStrings[0] would be the string "This" since groovy starts arrays at 0. x%6 returns the remainder of x divided by 6. At x=0, it will be 0, and the first cell would be named "This." The 8th cell would have a position value of 2, and a name of "a".

2. Numerical measurements.

Adding numerical measurements to any object uses the following code snippet.

 
someObject.measurements.put("measurementName", double)

Accessing a numerical measurement follows this format.

 
objectMeasurement = someObject.measurements.get("measurementName")


//Example calculating the area of the cytoplasm
for (cell in getCellObjects()) {
  
   nucleusArea = cell.measurements.get("Nucleus: Area")
  
   cellArea = cell.measurements.get("Cell: Area")
  
   cell.measurements.put("Cytoplasmic area", cellArea-nucleusArea) 

}

How you use it is otherwise incredibly flexible. All of those loops we went through in previous examples using getCellObjects().each? Well, we could have been adding measurements to the cells. One use for this has been adding multiple classifications to an object - above and beyond multiplex classification and derived classes. Take an instance where we want to apply a classifier to all of our cells to determine:

A. What type of cell they are.

B. Whether their parent annotation is Tumor or Stroma

C. Whether they are in tissue slice 1 or 2 (different annotation objects) on the slide.

Well, A can be handled by object classifier, right? And for B, there is a Parent field in each object that tells you what class it is.

But what do we do about the C, the tissue slice? If I wanted to do some data processing downstream on the exported CSV file containing all of the cell objects, how would I tell which tissue slice each cell had come from? If the tissue slices were labeled or classified, we could use getParent().getParent(). You can chain that up as many levels of the hierarchy exist. The problem is there is no Parent of Parent field to store that information, so though we can get it in QuPath, it would not be available in the exported CSV. So we are going to make one.

For this example I will use two slices of tissue from the CMU brightfield image included in the demo project.

 
Two tissue slices, with numbered annotations

Two tissue slices, with numbered annotations

Cell detection was run in the initial annotations, and new annotations were added after

Cell detection was run in the initial annotations, and new annotations were added after

 

I have used the context menu->Annotations->Set properties to manually give each a name, “1” and “2”. I chose numbers rather than "A" and "B" since the numbers can be stored as new measurements, the letters would be more problematic.

I then ran Cell Detection and created a few "Tumor" class annotations within each piece of tissue. Finally, I used setCellIntensityClassification in the Classify->Object classification menu to apply some classes.

Great, the setup is all done. Now for the scripts.

First step, 

resolveHierarchy()

With that run, the cells within the tumor annotations will have those tumor annotations as their Parent, while when first drawn they float out on their own.

Initially, all cells are shown as children of the initial numbered annotations.

Initially, all cells are shown as children of the initial numbered annotations.

After resolving the hierarchy, the cells within the newly drawn Stroma annotations are counted as being below those annotations in the hierarchy

After resolving the hierarchy, the cells within the newly drawn Stroma annotations are counted as being below those annotations in the hierarchy

symbol Note

If you are using the loaded demo data, each cell currently has a class (Negative, 1+, 2+, 3+) and a Parent (Tumor if within a tumor annotation, 1 or 2 otherwise since the tissue annotation I named earlier is the Parent). If you are using your own data set, feel free to play around with the cell classification at this point.

Where were we… right, measurements.

That means we can tell which annotation the non-tumor cells are in - but anything within a Tumor could be in tissue 1 or 2.

 
getCellObjects().each{cell->

    //Check each cell to see if it is in a tumor annotation

    if( cell.getParent().getPathClass() == getPathClass("Tumor") ){

        tissueSlice = cell.getParent().getParent().getName() as double

        cell.measurements.put("Tissue slice", tissueSlice)

	}else{

	    //If the cell is outside the tumor annotation, we need to check ONE parent level

	    tissueSlice = cell.getParent().getName() as double

    	cell.measurements.put("Tissue slice", tissueSlice)

	}

}

"as double" was used to convert the name, which is a string, into a Double (a type of number variable) so that it could be saved as a measurement. It works in the same way as toString() but instead takes a String and makes it a Double.

At the bottom of each cell's measurement list, there is now a Tissue slice Key, and the number of the tissue slice as a Value.

Key Value pair area showing new tissue slice measurement

Key Value pair area showing new tissue slice measurement

creating measurements 5.PNG

symbol Note

In the above figure I did change the Brightness and Contrast to get a black background, and adjusted the measurement map display range for brighter colors. Controlling the visualization options can be vital for getting your point across! Just be careful you are not misrepresenting the science.

The Name of the cell, the class, and the Parent are all left untouched.

There are many other uses of measurements, including adding cell densities and percent positives per class to annotations, even if they are not parent annotations. More examples can be found on the forum and here.
A very useful example of adding measurements, but more complex, involves this 0.3.0 code from Pete, which adds measurements to objects based on a particular density map.


3. Dynamic measurements

Accessing the dynamic measurements in annotations is a little bit more tricky, as they could change at any time. Getting to them involves accessing an observable measurement table. One additional set of measurements that are included in this category are the XY centroids of all objects, even cells.

Start with these two lines:

 
import qupath.lib.gui.measure.ObservableMeasurementTableData def ob = new ObservableMeasurementTableData();

Import statements are often put at the top of scripts, but with Groovy, it is perfectly fine to place them at the end of the script as well, and I prefer to put them at the end for user readability. The "ob" (just a variable name, call it what you like) needs to be created before you use it, however. Generally, I think your use for the table will be annotations, so you can target the table to annotations using the following line.

 
ob.setImageData( getCurrentImageData(),  getAnnotationObjects() );

Now "ob" can access the fields of annotation objects in the image. How can we use that? 

We might first think that we can get access to the Area measurement using

 
getAnnotationObjects().each{     print measurement(it, "Area µm^2") }

symbol Info

Quick aside:
I use PathClassifierTools.getAvailableFeatures(getDetectionObjects()) all of the time to get lists of measurements, and especially that dastardly micron symbol.

Update: In newer versions of QuPath, the micron symbol can be accessed easily through the scripting interface Insert-->Symbol

It will NOT include observation table measurements, however, so you will need a fixed measurement that has the micron symbol. Add Shape Features works well to generate a measurement with a micron symbol. Accessing the micron symbol should become much easier in 0.3.0 once you can copy measurements out of the Annotation and Hierarchy tabs.

This will fail, as the entire point of this section is that Area is not a standard, fixed measurement. What we need to do is

 
annotations.each { print ob.getNumericValue(it, "Area µm^2") }

The result will be the current area of each annotation in the image. Putting that all together, it looks like this:

 
def ob = new ObservableMeasurementTableData(); ob.setImageData( getCurrentImageData(),  getAnnotationObjects() ); getAnnotationObjects().each { print ob.getNumericValue(it, "Area µm^2") } import qupath.lib.gui.measure.ObservableMeasurementTableData

And that is it for the basics! By combining these tools, lists, and loops, you can accomplish a lot in very little time. There are also more complex and powerful methods for adding large groups of methods, as shown in this script by Pete Bankhead that adds an entire set of cell measurements to a group of objects.