Speed up ROI manipulation

Hi,

I wrote a short jython function to enlarge all ROIs in the ROI manager. The function itself works fine and gives the desired result, but it turns out that it can take a very long time in my use case where an image has ~4000 ROIs. I assume that this is because my function is using a loop that goes over each ROI one-by-on.

Is there any way to speed up this process?

Thanks so much for your help :slight_smile:

from ij.plugin import RoiEnlarger

#@ RoiManager rm
#@ Float (label="ROI expansion [microns]", value=1) enlarge

def enlarge_all_rois( amount_in_um, rm, pixel_size_in_um ):
    """enlarges all ROIs in the RoiManager by x scaled units

    Parameters
    ----------
    amount_in_um : float
        the value by which to enlarge in scaled units, e.g 3.5
    rm : RoiManager
        a reference of the IJ-RoiManager
    pixel_size_in_um : float
        the pixel size, e.g. 0.65 px/um
    """
    amount_px = amount_in_um / pixel_size_in_um
    all_rois = rm.getRoisAsArray()
    rm.reset()
    for roi in all_rois:
        enlarged_roi = RoiEnlarger.enlarge(roi, amount_px)
        rm.addRoi(enlarged_roi)
1 Like

Hello,
I have never tried it but the Overlay class (which is basically a collection of ROIs) has a scale method.

I am not sure it would do what you need, but might be worth the try !

Hi @LThomas,

thanks for the quick feedback!

I am not certain that the scale method would work here for me, in this function I am looking to “enlarge” the ROis by a fixed distance.

Could you create a binary mask from your ROIs, dilate the mask and then create a new set of ROIs?
It would probably be faster, but depends on whether or not you need to keep track of the ROIs IDs and could be a problem if dilation results in fusion of adjacent ROIs.
Just a thought,
Volko

Hi @Volko

thats a good idea, thanks for the input!
but I think it would indeed merge adjacent ROIs if they start touching after dilation. Since in my use case the ROIs are frequently close together, I a afraid this solution might not be an option for me

Hi @CellKai,
Just to support what @LThomas has said, I have found manipulating ROIs in an overlay to be quicker than interacting with the ROImanager in long loops. The speed hit using the ROImanager is also significantly mitigated by running it in batch mode (in a macro) or by hiding it when instatiating from a plugin.
For the quick way to implement an overlay version of your script consider the ‘moveRoisToOverlay’ function:

import ij.plugin.frame.RoiManager;

RoiManager rm = new RoiManager();
    //your ROImanager manipulations as they already exist
Overlay myOverlay = new Overlay();
rm.moveRoisToOverlay(myOverlay);

or the equivalent macro calls:

run("From ROI Manager");
run("To ROI Manager");

Apologies if you know all this already. It may be that your issue is more ROI manipulaiton focused, in which case the above may not help. For instance I recently noticed this in relation to the ‘makeBand’ function. However, I haven’t personally noitced similar for ‘enlarge’.

Kind regards.

1 Like

Also, having just read @Volko 's suggestion. I recently posted an ROI mask alternative for the makeBand function which might also apply here, and if you did want to try it for enlarge:

1 Like

@CellKai this uses quite a different approach from RoiEnlarger, but it’s based on the old way QuPath used to do it – adapted for Fiji as a Groovy script:

import ij.*
import ij.gui.*
import ij.plugin.frame.*
import java.awt.BasicStroke

double distance = 20

def imp = IJ.getImage()
def rois = RoiManager.getInstance().getRoisAsArray() as List
def expanded = rois.parallelStream().map({r -> expand(r, distance)}).toArray(Roi[]::new)

def overlay = new Overlay()
for (r in expanded)
	overlay.add(r)
imp.setOverlay(overlay)

Roi expand(Roi roi, double distance) {
	def shape = new ShapeRoi(roi).getShape()
	def area = new java.awt.geom.Area(shape)
	def stroke = new BasicStroke(distance*2 as float, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)
	def shape2 = stroke.createStrokedShape(area)
	def area2 = new java.awt.geom.Area(shape2)
	area2.add(area)
	def roi2 = new ShapeRoi(area2)
	roi2.setLocation(roi.getXBase()-distance, roi.getYBase()-distance)
	roi2.copyAttributes(roi)
	return roi2
}

This should take all the ROIs in the RoiManager and enlarge them by effectively outsourcing the task to Java AWT, then adds the results to a new overlay. This avoids any extra rasterization, and so does not give the same result as the ROI enlarger.

It’s parallelized because… well, why not?

If you use it, please be on the lookout for any weirdness since it’s not the standard ImageJ way.

1 Like

Hi all,

Fun little topic! I compiled the suggestions here. First I created a large enough image using this

run("Close All");
roiManager("reset");
setBatchMode(true);
run("Blobs (25K)");
k=12
for(i=0; i< k*k-1; i++) run("Duplicate...", " ");
run("Images to Stack", "name=Stack title=[] use");
run("Make Montage...", "columns="+k+" rows="+k+" scale=1");

setAutoThreshold("Default");
run("Analyze Particles...", "add");
roiManager("Show All without labels");

setBatchMode(false);

Then I dirtily implemented the different methods to compare speeds. I think I did something wring with Pete’s because it’s the slowest in Groovy and I was expecting it to be faster…

#@ RoiManager rm

def amount_px = 4
def rois = rm.getRoisAsArray();
rm.reset()

// Base Method
def start = System.currentTimeMillis()

rois.each{ roi ->
	enlarged_roi = RoiEnlarger.enlarge(roi, amount_px)
	rm.addRoi(enlarged_roi)
}
def time = System.currentTimeMillis() - start

println "Time to make for "+rois.size()+" rois using RoiManager: "+time+"ms"

// Parallel with collect
rm.reset()

start = System.currentTimeMillis()
GParsExecutorsPool.withPool(10) {
newrois = rois.collectParallel{ roi ->
	enlarged_roi = RoiEnlarger.enlarge(roi, amount_px)
}
}

newrois.each{
	rm.addRoi(it)
}

time = System.currentTimeMillis() - start

println "Time to make parallel for "+rois.size()+" rois using RoiManager: "+time+"ms"

// Using the Overlay
start = System.currentTimeMillis()
def ov = new Overlay()
rois.each{ roi ->
	enlarged_roi = RoiEnlarger.enlarge(roi, amount_px)
	ov.add(enlarged_roi)
}
// Add Overlay to RoiManager
rm.setOverlay(ov)
time = System.currentTimeMillis() - start

println "Time to make for "+rois.size()+" rois using Overlay: "+time+"ms"

start = System.currentTimeMillis()
// With The overlay
ov = new Overlay()
GParsExecutorsPool.withPool(10) {
rois.eachParallel{ roi ->
	enlarged_roi = RoiEnlarger.enlarge(roi, amount_px)
	ov.add(enlarged_roi)
}
}
// Add Overlay to RoiManager
rm.setOverlay(ov)
time = System.currentTimeMillis() - start

println "Time to make parallel for "+rois.size()+" rois using Overlay: "+time+"ms"

//Pete's solution
start = System.currentTimeMillis()
// With The overlay
ov = new Overlay()
GParsExecutorsPool.withPool(10) {
rois.eachParallel{ roi ->
	enlarged_roi = expand(roi, amount_px)
	ov.add(enlarged_roi)
}
}
rm.setOverlay(ov)
time = System.currentTimeMillis() - start

println "Time using Pete's method for "+rois.size()+" rois: "+time+"ms"

def expand(Roi roi, double distance) {
	def shape = new ShapeRoi(roi).getShape()
	def area = new java.awt.geom.Area(shape)
	def stroke = new BasicStroke(distance*2 as float, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)
	def shape2 = stroke.createStrokedShape(area)
	def area2 = new java.awt.geom.Area(shape2)
	area2.add(area)
	def roi2 = new ShapeRoi(area2)
	roi2.setLocation(roi.getXBase()-distance, roi.getYBase()-distance)
	roi2.copyAttributes(roi)
	return roi2
}

import ij.plugin.RoiEnlarger
import ij.*
import ij.gui.*
import ij.plugin.frame.*
import java.awt.BasicStroke

import groovyx.gpars.GParsExecutorsPool

For me this is the result:

Time to make for 5860 rois using RoiManager: 674ms
Time to make parallel for 5860 rois using RoiManager: 304ms
Time to make for 5860 rois using Overlay: 378ms
Time to make parallel for 5860 rois using Overlay: 84ms
Time using Pete's method for 5860 rois: 3495ms

So the fastest one seems to be using the overlay in parallel.
I noticed also that in Pete’s case, it seemed to miss some shapes when running in parallel this way

This does not happen if I first create the expanded rois and then add them to the overlay

def newrois = []
GParsExecutorsPool.withPool(10) {
newrois = rois.eachParallel{ roi ->
	enlarged_roi = expand(roi, amount_px)
}
}

newrois.each{ov.add(it)}
rm.setOverlay(ov)

So the cleanest fastest that worked for me was


// With The overlay
def ov = new Overlay()

// Define the array that will contain the new Rois outside the scope of the parallel processing
def newrois= []

// Parallel process with 10 threads
GParsExecutorsPool.withPool(10) {
	newrois = rois.collectParallel{ roi ->
		enlarged_roi = RoiEnlarger.enlarge(roi, amount_px)
		ov.add(enlarged_roi);
	}
}

// Add the rois to the overlay
newrois.each{ ov.add(it) }

// Give that to the RoiManager
rm.setOverlay(ov)
4 Likes

Thanks @oburri, nice test!

I didn’t know what to expect from performance, but from repeating your test I can see that the conversion of the stroked shape to an area is the slow bit. I guess it would be quite different depending upon the number of vertices in the original ROIs… since these are traced, there are many vertices and this makes the vector-based (rather than raster-based) way too slow.

Here’s a version that uses the same lower-level Java AWT stuff, but in a rasterized way:

import ij.*
import ij.gui.*
import ij.plugin.RoiEnlarger
import ij.plugin.filter.ThresholdToSelection
import ij.plugin.frame.*
import ij.process.ByteProcessor
import ij.process.ImageProcessor

import java.awt.BasicStroke
import java.awt.Color
import java.awt.image.BufferedImage

double amount_px = 10

//Pete's solution
start = System.currentTimeMillis()

// With The overlay
def imp = IJ.getImage()
def rois = RoiManager.getInstance().getRoisAsArray() as List
def expanded = rois.parallelStream().map({r -> expand(r, amount_px)}).toArray(Roi[]::new)

def overlay = new Overlay()
for (r in expanded)
    overlay.add(r)
imp.setOverlay(overlay)
time = System.currentTimeMillis() - start

println "Time using Pete's method for "+rois.size()+" rois: "+time+"ms"

RoiEnlarger
def expand(Roi roi, double distance) {
    def shapeROI = new ShapeRoi(roi)
    def shape = shapeROI.getShape()
    def stroke = new BasicStroke(distance*2 as float, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)
    def bounds = shapeROI.getBounds()
    int n = Math.round(distance)
    def mask = new BufferedImage(bounds.width + n*2 as int, bounds.height + n*2 as int, BufferedImage.TYPE_BYTE_GRAY)
    def g2d = mask.createGraphics()
    g2d.setColor(Color.WHITE)
    g2d.translate(n, n)
    g2d.fill(shape)
    if (distance < 0)
        g2d.setColor(Color.BLACK)
    g2d.setStroke(stroke)
    g2d.draw(shape)
    g2d.dispose()
    def bpMask = new ByteProcessor(mask)
    bpMask.setThreshold(127, Double.MAX_VALUE, ImageProcessor.NO_LUT_UPDATE)
    def roi2 = new ThresholdToSelection().convert(bpMask)
    roi2.setLocation(roi.getXBase()-distance, roi.getYBase()-distance)
    roi2.copyAttributes(roi)
    return roi2
}

Curious how it might compare with the distance transform :slight_smile:

PS. I think better to add to the overlay outside the loop in generally to avoid risk of any synchronization trouble… although since Overlay is internally using a Java Vector I’m not sure why it’s going wrong.

1 Like

thanks everyone for the quick and helpful replies, really awesome :star_struck: