Cell detection in more than one channel

Hi all,

I try to do some fluorescent multiplex virtual slide analysis in QuPath.
I managed to do it when images are “easy” (cell detection on dapi and classifiers on each channel…). But I very often encounter a problem: cells are sometimes not in the dapi channel. It happens when the nucleus is on the next cut and I only see the stained cytoplasm or when the staining on another channel masks the dapi. But at the end, this kind of cells are not detected on the dapi, and so they are not classified…
In QuPath, I didn’t managed to perform a cell detection on 2 (or more) channel. I tried to mix a cell detection on dapi and a pixel thresholding to get the missing cells, but I didn’t managed to have them with no class.
Do you have a clue to help me ?



I’ll lump this one in here:

And say that if the nucleus is not in the slice, that cell should probably not be counted. Say you were calculating a percentage positive. If that cell were negative, you would not even know it was there without a nucleus. That means you will be overcounting positive cells versus negative cells.

Of course, that’s my opinion and I am open to reasons why it would be wrong.

I talked to a few people after the linked post and no one has ever heard of or seen DAPI being “masked” by another fluorescent channel. I have no idea how that could even happen (in most cases) as DAPI/Hoechst would not be competing for similar binding sites, not being an antibody.

Hi @Research_Associate,

Thanks for your reply.
First I totally agree with you. But you know how clinician are : “your programm doesn’t work ; you didn’t count that cell althought it is well stained”…
Concerning the fact that dapi nucleus are masked, I agree too that sounds strange, but I have seen it lots of times with opal fluorophores.

In the linked post, you suggested to run 2 different cell detections and

then delete results from one that are inside cells of the second run.

How would you do that in QuPath ? By computing the distance between every centroids and removing the closest (but the computational complexity could be high) ?


This would be rather painful with very large data sets, but there is a “contains” method to check if something’s centroid is inside of something else. So you would run every cell in the first detection through every cell in the second detection, or vice versa.
Where parent is the cell you are searching inside of (probably “it” if you are cycling through with an “each”)
getCurrentHierarchy().getObjectsForROI(qupath.lib.objects.PathDetectionObject, parent.getROI())

More details in the original post


  1. Define area of interest
  2. Create cells using one channel
  3. Save those cells as an object/list
  4. Create cells using a second channel
  5. Add first set of cells back in.
  6. Cycle through each of first set of cells to see if any second cell is inside of any first cell. If anything is, delete the second cell (which here would be the one without the nucleus, I guess, so you keep the cell with a nucleus).

This should work as long as you are working in detection land, where objects in the hierarchy are basically their centroids (more complex in some ways, but when you resolveHeirarchy(), that seems to be what is used).

One last warning, this will not prevent OVERLAPS between cell membranes between the two data sets. It just checks if the centroid of one cell is within another cell. You could probably do something more complicated in JTS to find overlaps… but I am not going to jump into that just yet.

Can you give any more specifics? The people I was talking to were also using OPAL and thought that sounded really weird and they had not had any problems. Of course, I haven’t talked to anyone from Akoya so… still not sure what might be going on. Hollow nuclei, now that I see all the time.

And thanks, I think this may have helped with a rather tricky problem I was having with macrophages, which, of course, frequently should not be counted since they have no nucleus in frame, but even when they do, it is hard to pick up membrane staining. Luckily, StarDist includes a membrane option, and I can treat the giant cell body as a nucleus. At least it seems worth a try.

@petebankhead Crazy request, but at some point in the future it might be useful to have the same kind of “membrane” measurement (as per StarDist and the borders in the pixel classifier) for the Add intensity features function. Take measurements X pixels or microns on either side of the ROI.

For now, you can make those measurements as described here:

This is much newer code than that used internally for intensity measurements with StarDist, but it’s not sufficiently finalized to make the default.

Intensity measurements in QuPath are tricky in general, since they need to allow the the fact someone may want measurements made at a resolution that is too high to be able to get all the pixels for a region in one go.

Because small changes are already enough to break backwards compatibility / give different results, I try to avoid doing much with existing commands if I can help it. That’s why the StarDist implementation doesn’t build of the existing stuff: I’d rather work on a stronger replacement than try to evolve what is there across releases.

1 Like

I would like to share one of this kind of strange stainings: double or triple positive cells with nuclear FoxP3 (green) nuclei that are missing DAPI. Sometimes I can see a tiny DAPI ring surrounding FoxP3. What do you think about it?
I already contacted Akoya. Lets see how they can explain it. At least their Inform segmentation algorithm has an option to add extra nuclei and membrane markers.

1 Like

In my case, when one of the Opal nuclear (FoxP3) staining completely “blanked” DAPI signal:

In ImageJ I merged DAPI and FoxP3 channels in one, added this image in a existing stack and ran StarDist in QuPath. Most likely everything is possible to run in QuPath.
blue arrows show Fox3 nuclei

I am looking forward to hear prof feedback if it could be done more efficient.

1 Like

Hi Alice,

How do you merge the channel ? Because if they have different dynamics, just adding them won’t be enough ?


1 Like

Thanks for your question Nico.
I first ran ImageJ function “stack to images” and then at “image calculator - “add”function.
Surely, it will make only sense if a signal comes exclusively from nuclear location.

Apologies for my beginner question: Could you please elaborate your question about different dynamics?

I meant that if your dapi values range from (let’s say) 0 to 10 000 and your FoxP3 values range from 0 to 100, foxP3 values would be flooded among dapi ones. But if they both have the same dynamics (let’s say 0 to 100), adding them is meaningful.


Their ranges are rather similar, still I am losing a few dim DAPI nuclei comparing with only-DAPI- segmentation.

If either of you want to dig into it, that MAY be the sort of thing you can do with ImageOps. If you search for “StarDist ImageOps” on the forum there are a fairly small number of posts showing some of the things that can be done with them.
I will post my code later today for a variation of what I described above with the cell overlap checking.

Meanwhile, is there any chance your unmixing is causing the lack of DAPI signal where other channels are showing up? Can you look at the same cells without unmixing and see if the DAPI signal is there?

I did check it, and unmixing was not the issue this time.

And Akoya just replied that they are aware of “this phenomenon when FOXp3 was super high and so the stain was preventing DAPI to stain the nuclei properly, as they’re both very nuclear.”

In this particular case the best solution might be just to go to lab and adjust FoxP3 staining.

1 Like

Thanks! Interesting that that could even happen, but good to know they are aware of it. Makes me wonder about the physics/chemistry of what is happening. Or whether you would even see the same thing on a confocal system.

Is it what they call umbrella effect ? (and why they ask for fluorophore intensities (Unmixed Units) to be balanced (between 5UU and 30UU and nore more than 1 to 3 ratio bewteen 2 fluo) ?

And here is the code… sort of. This shows me getting cells that were created earlier, but in a real example you would save stardist1 after running stardist the first time, and stardist2 after running the second stardist on the second channel.

stardist1 = getCellObjects().findAll{it.getPathClass() == null}
stardist2 = getCellObjects().findAll{it.getPathClass() != null}
toRemove = []
    //Key point, this list WILL include the cell itself, so will always be at least size 1.
    cellsWithCentroidsInThisCell = getCurrentHierarchy().getObjectsForROI(qupath.lib.objects.PathDetectionObject, it.getROI())
    if (cellsWithCentroidsInThisCell.size() > 1){
        cellsWithCentroidsInThisCell.each{ c ->
            if (c != it) {toRemove << c}

What this does, you have two populations of cells, each saved to stardist1 or 2 (rename those to whatever makes sense). My first set of cells were completely classified, so I took the unclassified vs classified cells.
It then looks through all of the new, unclassified cells in StarDist1, to see if there is more than 1 object within it’s ROI. All cells have at least themselves, so 2 indicates a second cell.

In this case, I want to keep the new unclassified cell (in stardist1) and get rid of the old classified cell (from stardist2), so I check for which cell in the list of the two cells is the old cell (it, in this case), then add that to the removal list. You may need to play with this logic to make sure you are removing the correct cell for your experiment.

At the end, remove that list of cells to get rid of the overlap.

ImageOps would likely be easier once it was figured out, but in my case, I want to do this since it means I can use the CD68 outline for macrophages, and that makes it possible to check membrane staining.

Though I really need to get a version of StarDist that uses a higher number of vertices.

1 Like

I think it was about sterical hindrance.
Anyway, I came back to the initial file and found out that the FoxP3 signal intensity was about 50-80 Inform Signal Counts, which was too high and a clear sign for re-titration of FoxP3 abs. So it was done and it helped!


Thanks for reporting back! After thinking about it a little more, that does make sense as it is something seen quite frequently when attempting to image cleared tissue. There is a lot more tissue to penetrate in that case, but you can find very interesting workarounds like antibody gradients and electric fields to propel the antibodies through the sample. I suppose high enough concentrations of target and antibodies could cause similar in thin tissue slices.