Proposed fix for `ShapeRoi` issue

This is a follow-up to this an earlier thread about the
problem with ShapeRoi:

The core problem is that ShapeRoi doesn’t handle “open”
rois properly – things like Lines and polyline PolygonRois.

I’ve also created a github issue about this:

If people think that it makes sense to address this and
that this is the right approach, I will try to finish up
a replacement version of ShapeRoi.java (following the
draft ShapeRoi2).

Currently ShapeRoi2 is supposed to work correctly for the
“basic” ShapeRoi operations, and generally behave no worse
than ShapeRoi. (Constructing a ShapeRoi2 from a
Shape is not expected to work properly with this version.)

Please let me know if you find any issues with the core
functionality of ShapeRoi2.

Here are two test images that illustrate some of the
problems with ShapeRoi and compare ShapeRoi with a
draft of the proposed fix.

The first image shows ShapeRoi performing correct set
arithmetic with closed rois (in this case EllipseRois):

The second image shows how ShapeRoi fails with an open
roi (in this case a Line) while the proposed fix seems
to work:

These two test images are laid out as follows:

The first three rows show how two rois are rendered by:

  1. the “classical” roi (viz. EllipseRoi, Roi, and Line);
  2. ShapeRoi (constructed from the classical roi);
  3. ShapeRoi2, the proposed fix for ShapeRoi.

The orange outlines are boundaries drawn by
roi.drawPixels (ip). The cyan solid shapes are
from ip.fill (roi).

The next three rows (rows 4 through 6) are again classical,
ShapeRoi, and ShapeRoi2, now illustrating set arithmetic.
Classical rois don’t do set arithmetic, so they are just
the two rois drawn on top of one another to guide the eye.

For the ShapeRoi and ShapeRoi2 rows (rows 5 and 6), the
five columns (of boundary / interior pairs) are the results
of the or() (union), and() (intersection), xor()
(symmetric difference), and the two orders of not() (A-B
and B-A) operations.

In the first image everything works as expected. In the
second image, the Line roi displays correctly as the
classical Line and as the fixed ShapeRoi2 constructed
from the Line. But when a ShapeRoi is constructed from
Line, it displays only with drawPixels(), not with
fill(), and vanishes entirely when used in set arithmetic.

Here is the jython script that generates these (and other)
test images:

from java.awt import Color

from ij import IJ
from ij.gui import EllipseRoi
from ij.gui import Line
from ij.gui import PointRoi
from ij.gui import PolygonRoi
from ij.gui import Roi
from ij.gui import ShapeRoi

from ij.gui import ShapeRoi2

def setRoiCentroidLocation (roi, xc, yc):
    x1 = int (roi.getBounds().getX())
    y1 = int (roi.getBounds().getY())
    xd = xc - int (round (roi.getContourCentroid()[0]))
    yd = yc - int (round (roi.getContourCentroid()[1]))
    roi.setLocation (x1 + xd, y1 + yd)
    return roi

# image size
iWidth = 1024
iHeight = 640

# roi locations
hBase = 50    # centroid-x of first roi
vBase = 60    # centroid-y of first roi
hFOff = 85    # offset of "fill" from "drawPixels"
hOff  = 205   # x-offset for next roi column
vOff  = 95    # y-offset for next roi row
vXOff = 315   # y-offset for first set-arithmetic row
cBase = 375   # centroid-y of first "combo" roi

# rois
roisA = []
roisB = []
titles = []
roisA.append (EllipseRoi (0, 0, 50, 50, 0.2))        # ellipse
roisB.append (EllipseRoi (0, 50, 50, 0, 0.2))        # ellipse
titles.append ('ellipses')
roisA.append (Roi (0, 0, 50, 30, 25))                # rounded-rectangle
roisB.append (Line (0, 50, 50, 0))                   # line
titles.append ('rounded-rectangle / line')
ptx = [
    0,   4,  0,  4,  0,  4,  0,  4,  0,  4,
    10, 14, 10, 14, 10, 14, 10, 14, 10, 14,
    20, 24, 20, 24, 20, 24, 20, 24, 20, 24
    ]
pty = [
    0,   0, 10, 10, 20, 20, 30, 30, 40, 40,
    1,   1, 11, 11, 21, 21, 31, 31, 41, 41,
    2,   2, 12, 12, 22, 22, 32, 32, 42, 42
    ]
roisA.append (PointRoi (ptx, pty, len (ptx)))        # points
roisB.append (Roi (0, 1, 50, 20))                    # rectangle
titles.append ('points / rectangle')
pgx = [ 25, 50, 50, 25,  0,  0 ]
pgy = [  5, 20, 30, 45, 30, 20 ]
roisA.append (PolygonRoi (pgx, pgy, Roi.POLYGON))    # polygon (hexagon)
plx = [ 20, 40, 10, 30, 30, 10, 40, 20 ]
ply = [ 20,  0,  0, 20, 30, 50, 50, 30 ]
roisB.append (PolygonRoi (plx, ply, Roi.POLYLINE))   # polyline
titles.append ('polygon / polyline')
roisA.append (Line (0, 50, 50,  0))                  # line
roisB.append (Line (0,  0, 50, 50))                  # line
titles.append ('lines -- intersecting')
roisA.append (Line (0, 51, 51,  0))                  # line
roisB.append (Line (0,  0, 51, 51))                  # line
titles.append ('lines -- "missed" intersection')


ops = ['or', 'and', 'xor', 'A-B', 'B-A']

for  i in range (len (titles)):
    imp = IJ.createImage (titles[i], 'RGB ramp', iWidth, iHeight, 1)
    ip = imp.getProcessor()
    ip.multiply (0.125)
    ip.add (31.0)

    # draw rois
    for  ir in [0, 1]:
        r = roisA[i]  if  ir == 0  else  roisB[i]
        for  j in range (3):   # "classical", ShapeRoi, ShapeRoi2
            cx = hBase + ir * hOff
            cy = vBase + j * vOff
            rd = r.clone()
            rf = r.clone()
            setRoiCentroidLocation (rd, cx, cy)
            setRoiCentroidLocation (rf, cx + hFOff, cy)
            if  j == 1:
                rd = ShapeRoi (rd)
                rf = ShapeRoi (rf)
            if  j == 2:
                rd = ShapeRoi2 (rd)
                rf = ShapeRoi2 (rf)
            ip.setColor (Color.orange)
            rd.drawPixels (ip)
            ip.setColor (Color.cyan)
            ip.fill (rf)

    # draw set-arithmetic rois
    io = 0
    for  op in ops:
        for  j in range (3):
            rad = roisA[i].clone()
            raf = roisA[i].clone()
            rbd = roisB[i].clone()
            rbf = roisB[i].clone()
            cx = hBase + io * hOff
            cy = vBase + vXOff + j * vOff
            setRoiCentroidLocation (rad, cx, cy)
            setRoiCentroidLocation (raf, cx + hFOff, cy)
            setRoiCentroidLocation (rbd, cx, cy)
            setRoiCentroidLocation (rbf, cx + hFOff, cy)
            if  j == 0:
                if  op == 'or':
                    ip.setColor (Color.orange)
                    rad.drawPixels (ip)
                    rbd.drawPixels (ip)
                    ip.setColor (Color.cyan)
                    ip.fill (raf)
                    ip.fill (rbf)
            else:
                if  j == 1:
                    rad = ShapeRoi (rad)
                    rbd = ShapeRoi (rbd)
                    raf = ShapeRoi (raf)
                    rbf = ShapeRoi (rbf)
                if  j == 2:
                    rad = ShapeRoi2 (rad)
                    rbd = ShapeRoi2 (rbd)
                    raf = ShapeRoi2 (raf)
                    rbf = ShapeRoi2 (rbf)
                if  op == 'or':
                    rad.or (rbd)
                    raf.or (rbf)
                    rxd = rad
                    rxf = raf
                if  op == 'and':
                    rad.and (rbd)
                    raf.and (rbf)
                    rxd = rad
                    rxf = raf
                if  op == 'xor':
                    rad.xor (rbd)
                    raf.xor (rbf)
                    rxd = rad
                    rxf = raf
                if  op == 'A-B':
                    rad.not (rbd)
                    raf.not (rbf)
                    rxd = rad
                    rxf = raf
                if  op == 'B-A':
                    rbd.not (rad)
                    rbf.not (raf)
                    rxd = rbd
                    rxf = rbf
                ip.setColor (Color.orange)
                rxd.drawPixels (ip)
                ip.setColor (Color.cyan)
                ip.fill (rxf)
        io += 1

    imp.show()

To run this script you will need ShapeRoi2. Here is its
jar file, shape_roi_2.jar:

shape_roi_2.jar (13.9 KB)

Add it to a directory from which Fiji / ImageJ loads classes.
(I put it in the plugins directory.)

For completeness, the code for ShapeRoi2.java appears below.
(It is renamed and posted as ShapeRoi2_java.tif to get by the
forum limitations.) It is almost entirely copy-pasted from the
original ShapeRoi.java, with changes isolated to the
roiToShape() method, primarily in the new
if (!roi.isArea()) if-block.

ShapeRoi2_java.tif (51.4 KB)

Thanks, mm

2 Likes

@mountain_man I would not try to “fix” ShapeRoi, which has had this behavior for many years and would break some existing extensions if changed. Instead, I encourage you to check out the imglib2-roi library:

  • Shape-based regions of interest in the net.imglib2.roi.geom package.
  • Both real-space and integer-space ROIs, including conversion between them.
  • Composition of ROIs including and, or, xor and subtract.
  • Open, closed and unspecified boundary types.
  • Deep equality comparison.
  • Easy iteration of contained points.
  • No dependency on Java AWT classes.

This work was discussed and directed here on the forum a couple of years ago; see:

3 Likes

I could not run the test script but this bug should be fixed in the latest ImageJ daily build (1.52q47). The source code changes are at

Hello Wayne -

Thank you for your quick turn-around in the daily build. I have
updated my ImageJ to 1.52q47 and I now see good “set
arithmetic” with ShapeRoi.

One note: The changes to the roi system (not just ShapeRoi)
seem to have changed the “definition” of Line slightly, causing
it to contain one less point than before (basically [a, b) instead
of [a, b]).

Here is a script with its output that illustrates this:

from ij.gui import Line
l = Line (0, 0, 3, 0)
print 'len (l.getContainedPoints()) =', len (l.getContainedPoints())
print 'l.getContainedPoints() =', l.getContainedPoints()
ImageJ 2.0.0-rc-69/1.52p; Java 1.8.0_172 [64-bit];

len (l.getContainedPoints()) = 4
l.getContainedPoints() = array(java.awt.Point, [java.awt.Point[x=0,y=0], java.awt.Point[x=1,y=0], java.awt.Point[x=2,y=0], java.awt.Point[x=3,y=0]])
ImageJ 2.0.0-rc-69/1.52q47; Java 1.8.0_172 [64-bit];

len (l.getContainedPoints()) = 3
l.getContainedPoints() = array(java.awt.Point, [java.awt.Point[x=0,y=0], java.awt.Point[x=1,y=0], java.awt.Point[x=2,y=0]])

Thanks, mm

Hello Wayne -

For completeness, let me post the full set of before and after images
generated by my test script. A few comments appear after the
image dump.

With the previous version (1.52p) of ImageJ and ShapeRoi:

With the Wayne’s fixed version (1.52q47) of ImageJ and ShapeRoi:

There’s a fair amount to parse here:

The main message – the new ShapeRoi does set arithmetic.

For some reason the new ShapeRoi draws roi boundaries
(Roi.drawPixels (ip)) more thickly than do the “classical”
Rois.

In lines_intersecting_q47.png (the second to last image) (the
new) ShapeRoi draws a non-empty boundary for an empty
intersection. (The image name is confusing because the
“change in definition” of Line has caused what had been
two Lines that intersected in one point (pixel) to become a
“missed intersection”.)

These nuances notwithstanding, I will mark Wayne’s post as
the solution, because it fixes the original issue.

Thank, mm