Writing objects to another .qpdata file in the project

Hi @petebankhead, thanks for all the great work on QuPath so far! As usual, going off the deep end here…

Is there any way to write objects from one project entry into another entry’s hierarchy? So far I have been able to pull objects out of another entry in the project and put them into the image/entry (readHierarchy()) that the script is currently being run from, but in some cases it would be cleaner to be able to run one script from an image and distribute objects across a project, rather than organizing it so the user needs to “Run for all” but exclude the current image… and possibly a training image or two.

The specific use case, in this case, would be having N images that are aligned via affine/rigid transformation, where there is a list of those affine transformations stored somewhere. I would want the script to be run from Image X, access that list, and then for each image in that list, send it’s objects to all destinations using their affine transformations.

In this way, tumor margins, islet boundaries, etc, could be distributed out from a single image.

The alternative is still workable, but involves what feels like a slightly more awkward structure, running a script For Project across all destination images, accessing the affine list for their specific transform, and then pull the images from the source file. But that involves manually targeting the source file, and editing the script (per patient/per sample, etc).

I think this should do what you need:

// Define names of images within the project to which annotations should be added
def names = [
    'OS-1.ndpi', 'OS-2.ndpi'
]

// Identify the annotations for the current image
def annotations = getAnnotationObjects()

// Define parameters (modify the affine transform if needed)
def transform = new java.awt.geom.AffineTransform()
boolean keepMeasurements = true

// Loop through the entries and process the ones with specified names
def project = getProject()
for (entry in project.getImageList()) {
    if (entry.getImageName() in names) {
        // Open the entry
        println entry
        def imageData = entry.readImageData()
        def hierarchy = imageData.getHierarchy()
        
        // Copy objects, optionally transforming them & discarding measurements
        // (Should do this even if the transform is the identity, since it then has 
        // the effect of copying the annotations... otherwise behavior for the current image may be weird)
        def annotations2 = annotations.collect {
            return PathObjectTools.transformObject(it, transform, keepMeasurements)
        }
        
        // Optionally clear existing objects
        // hierarchy.clearAll()
        // Add the new objects & save the data
        
        hierarchy.addPathObjects(annotations2)
        entry.saveImageData(imageData)
    }
}
println 'Done!'

It’s really an extension of the example at https://qupath.readthedocs.io/en/latest/docs/scripting/overview.html#projects to include writing as well as reading. But that does of course make caution more critical… be sure to back up the project first :slight_smile:

1 Like

Ah, thanks, great! hierarchy.addPathObjects() is what I was looking for!

*and saving the image data as well. I guess technically the bigger issue was the saveImageData line, since I wasn’t sure how changes were applied back to the original file. I assume the entry.readImageData() line makes a copy of the information from the qpdata file, but isn’t really accessing it directly. Was stuck trying to figure out what was a copy and what was directly accessing the object.

@smcardle

QuPath never works directly from the data file, so whenever it is read it must be a copy. If you read the same file twice, you’ll have two independent copies.

Use describe if you want to quickly check available method names without leaving QuPath’s script editor. For example

def hierarchy = getCurrentHierarchy()
println describe(hierarchy)
1 Like

Yeah, I just saw that in another post and copied it to @smcardle. That looks really helpful, didn’t know about it till today.

It liveeeessss!
@smcardle @mesencephalon We can now automatically spam aligned objects across an entire project!!! ^______________^

After backing it up.

1 Like

@petebankhead and @Research_Associate
Thank you so much for your help! This will make my project a lot easier!

1 Like

Thanks @Research_Associate and @petebankhead.

This would save great amount of time and opens up many possibilities for my workflow!

Just to try and push my luck a little further, is there a way to run the (yes, I realize the name) Interactive image alignment as a script command with a specific set of settings (intensity/annotation, rigid/affine, X pixels) by passing it a fixed and rotating image/annotation/points?

I don’t understand the goal here… pop-up the window with predefined settings, or run some aspect of it?

Run some aspect of it, I guess. It seems like all of the inputs could all be scripted (though the file names or ‘entry’ values would be the most annoying), and the output could be either the matrix list, or a transform object.
If I understand the inputs correctly, it would be two file names/entrys, the type of transform/registration, pixel size, and the type of alignment (Image intensity/annotations). If a user wanted to spot check the alignments, the text could be pasted back into the Image overlay alignment dialog.

Popping up the window with pre-defined settings would be a nice secondary possibility, though.

Hmmm, if visual checking isn’t needed, anything can be scripting (it uses OpenCV for the alignment, and it’s already documented how to create a compatible Mat from QuPath here).

For me, the interactive alignment was always a rough proof-of-concept that has evolved a bit beyond that - but I wouldn’t want to / don’t have the time to push it further than that and make it more complex any time soon.

Oooh, if you are saying it should be possible, I’ll look into that. I feel like I have run into things before that weren’t entirely scriptable due to access to functions before.

But, as with many others, I seem to have time on my hands now… and this would be a huge time saver with hundreds of images, and more to come!