Interactive image alignment

So turns out when I was troubleshooting I was adding the comma in the wrong matrix and the wrong place :sweat:

However, now that I’ve done this correctly, I get the ROIs but still in the wrong place. But again, the transform matrix is from the interactive overlay alignment which previews as perfect…

PREVIEW:

OUTCOME:

Thoughts?

I toggled creatInverse and it worked! Thanks for all the help!!!

2 Likes

I’ve edited the script above for compatibility with v0.2.0-m3.

Two changes:

  • PathROIToolsAwt has become RoiTools (part of simplifying things)
  • Introduced ImagePlane to define z-stack and timepoint location (rather than supplying individual ints for both)

Hi all, I am trying to transfer an annotation (polygon) and detected cells to another image using the code provided in this thread, but am running into issues. I am using version 0.2.0-m3. Here is my code:

/**

  • Script to transfer QuPath objects from one image to another, applying an AffineTransform to any ROIs.
    */

// SET ME! Define transformation matrix
// Get this from 'Interactive image alignment (experimental)
def matrix = [
1.000, 0.001, -2.253,
-0.001, 1.000, 3.678
]

// SET ME! Define image containing the original objects (must be in the current project)
def otherImageName = ‘435.tif’

// SET ME! Delete existing objects
def deleteExisting = false

// SET ME! Change this if things end up in the wrong place
def createInverse = true

import qupath.lib.gui.helpers.DisplayHelpers
import qupath.lib.objects.PathCellObject
import qupath.lib.objects.PathDetectionObject
import qupath.lib.objects.PathObject
import qupath.lib.objects.PathObjects
import qupath.lib.objects.PathTileObject
import qupath.lib.roi.RoiTools
import qupath.lib.roi.interfaces.ROI

import java.awt.geom.AffineTransform

import qupath.lib.gui.scripting.QPEx

if (otherImageName == null) {
DisplayHelpers.showErrorNotification(“Transform objects”, “Please specify an image name in the script!”)
return
}

// Get the project & the requested image name
def project = getProject()
def entry = project.getImageList().find {it.getImageName() == otherImageName}
if (entry == null) {
print 'Could not find image with name ’ + otherImageName
return
}

def otherHierarchy = entry.readHierarchy()
def pathObjects = otherHierarchy.getRootObject().getChildObjects()

// Define the transformation matrix
def transform = new AffineTransform(
matrix[0], matrix[3], matrix[1],
matrix[4], matrix[2], matrix[5]
)
if (createInverse)
transform = transform.createInverse()

if (deleteExisting)
clearAllObjects()

def newObjects =
for (pathObject in pathObjects) {
tempObject = transformObject(pathObject, transform)
if (tempObject != null) {
tempObject.setName(pathObject.getName())
newObjects << tempObject
}}
addObjects(newObjects)

print ‘Done!’

/**

  • Transform object, recursively transforming all child objects
  • @param pathObject
  • @param transform
  • @return
    */
    PathObject transformObject(PathObject pathObject, AffineTransform transform) {
    // Create a new object with the converted ROI
    def roi = pathObject.getROI()
    def roi2 = transformROI(roi, transform)
    def newObject = null
    if (pathObject instanceof PathCellObject) {
    def nucleusROI = pathObject.getNucleusROI()
    if (nucleusROI == null)
    newObject = PathObjects.createCellObject(roi2, pathObject.getPathClass(), pathObject.getMeasurementList())
    else
    newObject = PathObjects.createCellObject(roi2, transformROI(nucleusROI, transform), pathObject.getPathClass(), pathObject.getMeasurementList())
    } else if (pathObject instanceof PathTileObject) {
    newObject = PathObjects.createTileObject(roi2, pathObject.getPathClass(), pathObject.getMeasurementList())
    } else if (pathObject instanceof PathDetectionObject) {
    newObject = PathObjects.createDetectionObject(roi2, pathObject.getPathClass(), pathObject.getMeasurementList())
    } else {
    newObject = PathObjects.createAnnotationObject(roi2, pathObject.getPathClass(), pathObject.getMeasurementList())
    }
    // Handle child objects
    if (pathObject.hasChildren()) {
    newObject.addPathObjects(pathObject.getChildObjects().collect({transformObject(it, transform)}))
    }
    return newObject
    }

/**

  • Transform ROI (via conversion to Java AWT shape)
  • @param roi
  • @param transform
  • @return
    */
    ROI transformROI(ROI roi, AffineTransform transform) {
    def shape = RoiTools.getShape(roi) // Should be able to use roi.getShape() - but there’s currently a bug in it for rectangles/ellipses!
    shape2 = transform.createTransformedShape(shape)
    return RoiTools.getShapeROI(shape2, roi.getImagePlane(), 0.5)
    }

This should be exactly the same code as has been improved on above (I did change import static qupath.lib.gui.scripting.QPEx.* to import qupath.lib.gui.scripting.QPEx, as I believe this needs to be done with the new version)

When I run the code, I get this error at line 65 (which is tempObject = transformObject(pathObject, transform)):

ERROR: Error at line 65: No signature of method: static qupath.lib.objects.PathObjects.createCellObject() is applicable for argument types: (qupath.lib.roi.PolygonROI, null, qupath.lib.measurements.DefaultMeasurementList) values: [Polygon, null, [Cell: Area: 229.0, Cell: Perimeter: 57.24557357284336, Cell: Circularity: 0.8781354788185962, Cell: Max caliper: 20.452254322661084, Cell: Min caliper: 14.0, Cell: Eccentricity: 0.7036413955578946, Cell: Channel 1 mean: 497.32758620689657, Cell: Channel 1 std dev: 103.34054792817992, Cell: Channel 1 max: 745.0, Cell: Channel 1 min: 319.0, Cytoplasm: Channel 1 mean: 431.22627737226276, Cytoplasm: Channel 1 std dev: 69.37006396862219, Cytoplasm: Channel 1 max: 625.0, Cytoplasm: Channel 1 min: 319.0]]
Possible solutions: createCellObject(qupath.lib.roi.interfaces.ROI, qupath.lib.roi.interfaces.ROI, qupath.lib.objects.classes.PathClass, qupath.lib.measurements.MeasurementList)

Any help would be greatly appreciated. Thanks!

Hi, sorry no time to look in detail but the problem appears to be that the static method requires two ROIs (one for the nucleus, one for the cell boundary) but for some reason you only have one.

Since cell objects are usually created by detecting the nucleus and expanding it, both nucleus and cell ROIs are generally present. Did you use the Cell + membrane detection command, or some other way of generating cells?

It may be that replacing

newObject = PathObjects.createCellObject(roi2, pathObject.getPathClass(), pathObject.getMeasurementList())

with

newObject = PathObjects.createCellObject(roi2, null, pathObject.getPathClass(), pathObject.getMeasurementList())

may help (I haven’t checked it…) but since the primary distinguishing feature of cell objects as opposed to general detections is that they can have a nucleus then I’m not sure if this is the solution you really want… or if instead you might wish to filter out all cell objects that are missing nuclei first.

As a note, it does help anyone trying to run your code if you format it as code. It is perfectly readable as is, but attempting to copy it into QuPath would fail. Use the formatting button shown below to make code copy and pastable.
image

println(“Hello World”)
vs
println("Hello World")

Thanks for the quick reply. I used cell detection without the include cell nucleus feature selected. I replaced the line with your code and it worked! Thanks for your help.

Thanks for the tip, I will do this next time!

Thank you very much for this wonderful program QuPath and the amazing update!

I am new to QuPath and image analysis and do not even know how to do coding. I learned QuPath from the tutorial video. My research is also examining immune cells in tumor. I have multiple cases of IHC stained slides which are AEC multiplex stain of 5 different markers on a same slide (stain and destain repeated for each markers on the same slide).
I think the QuPath 0.2.0-m3 interactive image alignment is really great for this type of research.

I have some questions using QuPath 0.2.0-m3. I would appreciate any help.

  1. Is there a way to view all the ROI markings again after transferring the multiple ROIs to another slide? What I did was merge 4 ROIs -> transfer last ROI to another slide -> split annotations. Then I could not see the annotation but if I click the annotation tab I could see one annotation at a time upon clicking the roi list.
  2. Is it possible to annotate the overlapped image while the QuPath interactive image alignment is turned on with the original image selected for working on project?
    3.I tried the above script with the input of annotated file name for OtherImageName and matrix number from the interactive image alignment and clicked ‘run’ at the selection status of the slide I want the annotation and detection transferred to. And I got the error message as follows. What could have gone wrong ? Is there more input I should do? (I may have misinterpreted the script.)
    Line 26-29 are as follows:
    if (otherImageName == null) {
    DisplayHelpers.showErrorNotification(“Transform objects”, “Please specify an image name in the script!”)
    return
    }

ERROR: Error: startup failed:
Script3.groovy: 28: unexpected token: objects” @ line 28, column 49.
wErrorNotification(“Transform objects”,

1 error

ERROR: Script error
at org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:311)
at org.codehaus.groovy.control.ErrorCollector.addFatalError(ErrorCollector.java:151)
at org.codehaus.groovy.control.ErrorCollector.addError(ErrorCollector.java:121)
at org.codehaus.groovy.control.ErrorCollector.addError(ErrorCollector.java:133)
at org.codehaus.groovy.control.SourceUnit.addError(SourceUnit.java:325)
at org.codehaus.groovy.antlr.AntlrParserPlugin.transformCSTIntoAST(AntlrParserPlugin.java:224)
at org.codehaus.groovy.antlr.AntlrParserPlugin.parseCST(AntlrParserPlugin.java:190)
at org.codehaus.groovy.control.SourceUnit.parse(SourceUnit.java:226)
at org.codehaus.groovy.control.CompilationUnit$1.call(CompilationUnit.java:196)
at org.codehaus.groovy.control.CompilationUnit.applyToSourceUnits(CompilationUnit.java:965)
at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:647)
at org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:623)
at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:600)
at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:390)
at groovy.lang.GroovyClassLoader.access$300(GroovyClassLoader.java:89)
at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:330)
at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:327)
at org.codehaus.groovy.runtime.memoize.ConcurrentCommonCache.getAndPut(ConcurrentCommonCache.java:147)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:325)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:309)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:251)
at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.getScriptClass(GroovyScriptEngineImpl.java:331)
at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:153)
at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:766)
at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:696)
at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:676)
at qupath.lib.gui.scripting.DefaultScriptEditor$ProjectTask.call(DefaultScriptEditor.java:1287)
at qupath.lib.gui.scripting.DefaultScriptEditor$ProjectTask.call(DefaultScriptEditor.java:1236)
at javafx.concurrent.Task$TaskCallable.call(Task.java:1425)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)

4.What I would like to do is to duplicate all of the ROI and their detections onto the new image from the best stained slide, then running something like Add cell intensity measurements to get the color and intensity information for those copied detections.

  1. In the next milestone, would it be possible to get the numbering (or some kind of cell ID) for each cells on the measurement table so that the copied detected cells and the original detected cells can be compared in the merged tables? (In that way, all the measurements of same cells of the same ROIs might be recorded for each markers if there is a certain rule to generate the meaurement table … like in what order the cells in the ROI should be listed in the table. If the cell detection is copied onto the other image of the same tissue, the detected cell number and location should be the same and only the color and intensity should be different.)
    Or if I can succeed in copying annotation and cell detection with acquisition of the color and intensity measurement in the new image, I might want to try sorting the table by the x y coordinate to have them in the same order to compare. I thought about this because I am not familiar with imageJ or cellprofiler.

Any help would be appreciated.

Thank you for reading this long message.

I am guessing for the script error that you tried to copy @bluna301’s version of the script above, which won’t work since he did not post it as code.

You also did not post your code as code, so it wouldn’t be possible to copy that out and use it either.

Sorry, in a rush today and won’t have a chance to look at the rest for a bit.

I’m glad you like it :smile:

Are the annotations perhaps just hidden? See View → Show annotations (there’s also a button on the toolbar). Alternatively, is the opacity slider in the toolbar to the left (transparent) or right (opaque)?

I’m not sure I understand 100%. The overlapped image is just there for display and you can adjust the opacity with the slider on the toolbar. If you annotate, you’ll be annotating the image underneath.

However if you are displaying the overlapped image then it should impact the Wand tool, which can be useful when annotating.

I think that the quotation marks have subtly become curly during copy & paste…

“Transform objects”

should be

"Transform objects"

I totally agree that something like this is needed, but I’m afraid this is I won’t have time to work on it myself in the near future. Including an ID and making sure it is unique would be rather complicated with QuPath’s current design.

I did add some improvements this today and yesterday to adding intensity measurements (e.g. adding measurements within the nucleus only) that may help a bit.

There are probably ways to work around this through scripting. I strongly suspect @Research_Associate will come up something :wink:

Oh right, that was already done in a couple of places, but I can’t find them right now.
I guess you wanted something like this if you are copying over the whole measurement list:

i = 1
getCellObjects().each{
it.getMeasurementList().putMeasurement("Label", i)
i++
}

Each cell should have a Measurement Label, from 1 to N after running the script. Cells appear to be ordered by XY centroid coordinates.

If the measurements aren’t copied over, this likely won’t work, though, and I haven’t played with the transfer much. Maybe look into it later.

Thank you very much for your kind and prompt reply. I was very surprised to see the reply in just a few hours!
I apologize for this basic question.
I read your posting before and tried to find the ‘</>’ in my mac notebook word but could not find it so I just pasted the script to QuPath.
Would it work if I copy and special paste the script with HTML format in the word file and then paste it to QuPath? (I googled “converting to code from word document”…)

And thank you for the script for labeling cells in the measurement table. I will try it after trying the measurement copy over.

Thank you again.

Thank you very much for your kind and prompt reply. I was very surprised to see the reply in just a few hours!
I really appreciate your taking time to kindly reply in such a detail.

  1. Thank you for your kind explanation. I found the “hidden” annotation by moving the slider.
  2. I thought it would be nice if I could see the original annotation and move the newly copied ROIs to the location I want to overlap the ROIs with the interactive image alignment since I cannot do coding. But I could see only the newly copied ROIs on the interactive image alignment screen.

    This is annotated image.


This is image to compare.


And this is the overlay snapshot with opacity about the half way in the middle.
52|576x130

  1. Thank you. Now I see that 46

</> is here in the reply window… But unfortunately I have no knowledge how to convert the copied script to code using this… Could I please have a recommendation where I can learn this? I apologize for a basic question. I wish I am more familiar with coding…

  1. I totally agree with you and imagine that it must be a complicated task to make a cell ID and make sure it is unique when the images get distorted at least a little bit after a few rounds of stain and destain even on the same tissue. But I greatly appreciate that you have already added some features regarding my opinion. I will look for the next update.

Thank you again.

That icon is used in the same way as bold or italics, select the text, and click it (or CTRL+SHIFT+C on PC). It doesn’t convert to code, just formats it.

Also, now that I am taking the time to read it, I am guessing Pete meant something more like this script from a while back that allows you to input color vectors to generate new intensity mean values for the nucleus and cytoplasm. The new measurements are saved by channel names, so you may want to rename your Stains something like Hematoxylin2, Hematoxylin3, etc.
image
It was original intended for 3plex and higher brightfield imaging, but should work well enough for restaining tissue slices. Obviously sequential slices would have problems due to non-overlapping cells and the same cells frequently not existing even in two sequential slices, so cell by cell data would be of limited usefulness.

You do need to manually paste in the color vector line and the add Intensity features line from the Workflow tab to make the script work, as described by the comments in the script. The script mostly works in m3, but you will need to change closeList() to close() in two locations, around line 55 and line 100.
cellMeasurements.closeList()
=>
cellMeasurements.close()

No, got it first time - I was thinking of the add-an-ID-as-a-measurement trick :slight_smile:

Some of the features in QuPath (including Interactive image alignment) are very provisional and there to show what could be done… but it will take a lot more time/effort to build on them to make them more complete and user-friendly.

@Aimee by replacing the curly quotation marks with straight ones, could you get the script to work?

Thank you very much for the prompt reply.
I tried CTRL+SHIFT+C and it opened the part where I could see the source code so I copied that part and pasted it to QuPath script editor, input the transform part with interactive alignment data, input the annotated file name, checked for the changes to be made comparing it with the webpage and ran.
I got the following error message.
ERROR: Error: startup failed:
Script11.groovy: 25: unable to resolve class qupath.lib.roi.ROITools
@ line 25, column 1.
import qupath.lib.roi.ROITools
^

1 error

ERROR: Script error
at org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:311)
at org.codehaus.groovy.control.CompilationUnit.applyToSourceUnits(CompilationUnit.java:980)
at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:647)
at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:596)
at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:390)
at groovy.lang.GroovyClassLoader.access$300(GroovyClassLoader.java:89)
at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:330)
at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:327)
at org.codehaus.groovy.runtime.memoize.ConcurrentCommonCache.getAndPut(ConcurrentCommonCache.java:147)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:325)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:309)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:251)
at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.getScriptClass(GroovyScriptEngineImpl.java:331)
at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:153)
at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:766)
at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:696)
at qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:676)
at qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1033)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)

Following is the code I entered in script editor.

// Get this from 'Interactive image alignment (experimental)
def matrix = [
  1.000, 0.000, -191.457,
  0.000, 1.000, 613.371
]

// SET ME! Define image containing the original objects (must be in the current project)
def otherImageName = '20190625_CD8.svs'

// SET ME! Delete existing objects
def deleteExisting = false

// SET ME! Change this if things end up in the wrong place
def createInverse = true


import qupath.lib.gui.helpers.DisplayHelpers
import qupath.lib.objects.PathCellObject
import qupath.lib.objects.PathDetectionObject
import qupath.lib.objects.PathObject
import qupath.lib.objects.PathObjects
import qupath.lib.objects.PathTileObject
import qupath.lib.roi.ROITools
import qupath.lib.roi.interfaces.ROI

import java.awt.geom.AffineTransform

import qupath.lib.gui.scripting.QPEx

if (otherImageName == null) {
    DisplayHelpers.showErrorNotification("Transform objects", "Please specify an image name in the script!")
    return
}

// Get the project &amp; the requested image name
def project = getProject()
def entry = project.getImageList().find {it.getImageName() == otherImageName}
if (entry == null) {
    print 'Could not find image with name ' + otherImageName
    return
}

def otherHierarchy = entry.readHierarchy()
def pathObjects = otherHierarchy.getRootObject().getChildObjects()

// Define the transformation matrix
def transform = new AffineTransform(
        matrix[0], matrix[3], matrix[1],
        matrix[4], matrix[2], matrix[5]
)
if (createInverse)
    transform = transform.createInverse()

if (deleteExisting)
    clearAllObjects()

def newObjects = []
for (pathObject in pathObjects) {
tempObject = transformObjects(pathObject, transform)
if (tempObject !=null) {
tempObject.setName(pathObject.getName())
newObjects<<tempObject
}}  
addObjects(newObjects)

print 'Done!'

PathObject transformObject(PathObject pathObject, AffineTransform transform) {
    // Create a new object with the converted ROI
    def roi = pathObject.getROI()
    def roi2 = transformROI(roi, transform)
    def newObject = null
    if (pathObject instanceof PathCellObject) {
        def nucleusROI = pathObject.getNucleusROI()
        if (nucleusROI == null)
            newObject = PathObjects.createCellObject(roi2, null, pathObject.getPathClass(), pathObject.getMeasurementList())
        else
            newObject = PathObjects.createCellObject(roi2, transformROI(nucleusROI, transform), pathObject.getPathClass(), pathObject.getMeasurementList())
    } else if (pathObject instanceof PathTileObject) {
        newObject = PathObjects.createTileObject(roi2, pathObject.getPathClass(), pathObject.getMeasurementList())
    } else if (pathObject instanceof PathDetectionObject) {
        newObject = PathObjects.createDetectionObject(roi2, pathObject.getPathClass(), pathObject.getMeasurementList())
    } else {
        newObject = PathObjects.createAnnotationObject(roi2, pathObject.getPathClass(), pathObject.getMeasurementList())
    }
    // Handle child objects
    if (pathObject.hasChildren()) {
        newObject.addPathObjects(pathObject.getChildObjects().collect({transformObject(it, transform)}))
    }
    return newObject
}

ROI transformROI(ROI roi, AffineTransform transform) {
    def shape = ROITools.getShape(roi) 
    shape2 = transform.createTransformedShape(shape)
    return ROITools.getShapeROI(shape2, roi.getImagePlane(), 0.5)
    }


I am sorry to bother you so much.

Thank you again for your great help.

Are you using QuPath v0.2.0-m3 or an earlier version? There was a small change to the script made to make it compatible with v0.2.0-m3.


Is what should have been copied. It looks like the import lines were manually edited.

import qupath.lib.roi.ROITools

should be
import qupath.lib.roi.RoiTools
There seem to be a few other changes made as well, so what was pasted here was definitely not the 0.2.0m3 version. I didn’t dig through all of the problems.

It works!!!
Thank you so much for your great help, all the people here!
It’s amazing how you help a beginner with full of mistakes like me with such kindness and generousness. I really appreciate your great help, Dr. @petebankhead and Dr. @Research_Associate.
Yes… I am so sorry I thought the lines in light colors were just some notes and deleted some of them when copying to the script editor and edited the part with “ROITools” because the script I copied looked like the old version with “PathROIToolsAwt”. (I thought the ones like " /** * Script to transfer QuPath… " were just notes for the users)
I downloaded the QuPath 0.2.0-m3 again today and ran the script and voilà !!!
It can copy over all the annotations and cell detections in the original slide. So exciting to learn!

Since the new slide shows shrinkage or distorsion in some part and the original annotation does not exactly match the cells in the new slide because I copied multiple ROIs at once. But I think I may be able to partly merge that ROI marking and the cell detection in it at the misaligned part and adjust a little bit to align that part.

I will try the next step to get the color and optical density infomation for those cell dections in the new slide and also try numbering the cells while acquiring the cell detection copy. Could you give me any suggestion where in the above script I might want to put the cell numbering script in? or should I run it in separate workflow?

Thank you again and again.