Cells are not segmented properly in the exported mask

Not sure why the cells are connected sometimes. I used downsample =2 while exporting.

Sample image (1)


I used this code to export the detections as a binary mask. I’m using M9.

Analysis goals

However, after exporting, I found the cells are connected.


I can apply watershed at this point in Matlab to segment the cells; but was wondering if there’s any way to avoid this in the first place. The situation gets worse with downsample = 4.

1 Like

That looks like the behavior I would expect in any software if you downsample. Essentially you are merging every 4 pixels together, and if there is only a 1 pixel gap between cells, you will usually merge the two cells. No way around that using a mask. If you wanted to export the objects a different way, you might be able to use coordinates rather than a mask and maintain separation.

Otherwise, don’t downsample, I think.

1 Like

The code doesn’t work with downsample =1

If the image is too large, I suspect you won’t be able to export the whole thing at once. That is part of why special file formats are needed for whole slide images in the first place.

You may want to look into exporting tiles, or figuring out a way to write out the mask as a pyramidal OME-TIFF. There are some posts about StarDist that export sample areas for training deep learning algorithms, for example. They export both a subsection of the original image, and the mask for objects created within the area exported.

1 Like

Adding perhaps some more information regarding your actual image.

This may be true but also consider that with QuPath, you can create annotations touching each other, so there may not even be a one pixel gap at downsample 1…

In cases like these, the solution that is the simplest is to not return a mask but a labeled image of the objects. That way even if the pixels are touching each other, the pixel IDs are still unique. This would be the matlab equivalent of bwlabel

In the case of your code, you would have to replace

def img = new BufferedImage(w, h, BufferedImage.TYPE_USHORT_GRAY)

def g2d = img.createGraphics()
g2d.scale(1.0/downsample, 1.0/downsample)
getDetectionObjects().eachWithIndex { detection, i ->
  g2d.setColor(new Color(i))  
  roi = detection.getROI()
  def shape = roi.getShape()

Here is the link to that post.

Please let us know if this works.

1 Like

Hi @manaser, I agree with @Research_Associate and @oburri that it isn’t a problem with the segmentation but rather the expectation :slight_smile:

@oburri’s approach is one way to deal with it, but I don’t think it will work directly because of the way Java handles drawing for 16-bit images – when I tried it, I ended up with hard-to-interpret labels.

In general, I am trying to get away from users needing to grapple with Java BufferedImages and other such awkward things in favour of adding helper classes to QuPath to take care of all that.

This will allow scripts can be written at a higher level – and many of the complicated scripts floating around the internet (including this forum and my blog) will become unnecessary. You can see some examples of ‘newer’ scripts in Exporting annotations.

The TileExporter and LabeledImageServer helpers used there don’t (yet) support the kind of labelled images that Oli is referring to (they will in the next version…), but another approach you can use is to export a two-channel binary image where the second channel stores the outline of each ROI.

Based upon the script in the tutorial, it might looks like this:

def imageData = getCurrentImageData()

// Need to classify the cells for this to work in v0.2.0-m11

// Define output path (relative to project)
def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName())
def pathOutput = buildFilePath(PROJECT_BASE_DIR, 'tiles', name)

// Define downsample
double downsample = 2

// Create an ImageServer where the pixels are derived from annotations
def labelServer = new LabeledImageServer.Builder(imageData)
        .downsample(downsample)    // Choose server resolution; this should match the resolution at which tiles are exported
        .setBoundaryLabel('Boundary', 2) // Set the boundary to be some unique label
        .multichannelOutput(true)  // If true, each label is a different channel (required for multiclass probability)

// Create an exporter that requests corresponding tiles from the original & labelled image servers
new TileExporter(imageData)
        .downsample(downsample)     // Define export resolution
        .imageExtension('.tif')     // Define file extension for original pixels (often .tif, .jpg, '.png' or '.ome.tif')
        .tileSize(512)              // Define size of each tile, in pixels
        .labeledServer(labelServer) // Define the labelled image server to use (i.e. the one we just built)
        .labeledImageExtension('.tif') // Extension for the associated labelled image
        .annotatedTilesOnly(true)  // If true, only export tiles if there is a (labelled) annotation present
        .overlap(0)                // Define overlap, in pixel units at the export resolution
        .writeTiles(pathOutput)     // Write tiles to the specified directory

print 'Done!'

Note that you should read the script before running it - it’s quite likely you’ll need to adapt it for your purposes.


A few extra comments…

  • the above script is for QuPath v0.2.0-m11
  • it will change your cell classifications. You can change the script if you don’t want that – but note that m11 it won’t work with unclassified cells. However this may be an advantage, in that you can also solve this problem in a single step by only specifying a label for classification you want to export, and removing the bit that sets classifications to ‘Cell’.
  • you can subtract the outline channel from the area channel in MATLAB
  • there are be some weirdness in how Java paints shapes and how it handles boundaries; specifically, sometimes the a few pixels of the ‘filled’ area can appear outside the ‘drawn’ boundary. You might not see this problem, but if you do try inserting .lineThickness(2) in the parameters used to create the labelServer.