1

I'm trying to measure a polygon mask's longest diameter (feret diameter) while also getting the length of the orthogonal line from the center of the feret diameter. Diagram of what I'm trying to do can be found here: https://radiologyassistant.nl/assets/fleischner-2017-guideline-for-pulmonary-nodules/a59385c3993fd3_TAB-measurement.png

I cannot use the major_axis and minor_axis properties of skimage.measure.regionprops as that fits an ellipse over my mask and is found to be wildly unreliable for the mask shapes I have.

Are there any functions or other ways I can do this? I've only found skimage's methods to be most applicable for what I am trying to do. I don't know if there are other terms for what I'm trying to do. Any insight is helpful.

This is the basis of what I'm doing:

mask_ps = np.transpose(skimage.draw.polygon2mask(im2d_ps.shape, this_ps_seg_points)) #turn into a polygon mask

mask_ps_label = skimage.measure.label(mask_ps)
props_ps = skimage.measure.regionprops(mask_ps_label)
props_ps.sort(key=lambda x: x.area)
print("props_ps.area", props_ps[-1].area) #max area
this_feret = props_ps[-1].feret_diameter_max

The mask that I'm using (mask_ps) is quite large so I can't insert it here by copy/paste.

Claudio
  • 7,474
  • 3
  • 18
  • 48
hqp
  • 19
  • 6
  • 1
    [question needs pictures of ferets](https://twitter.com/scienceshitpost). -- opencv, `minAreaRect`. either that or some kind of PCA or Moments calculation to determine major axis. minor axis is then orthogonal to that. -- you _can_ use the major/minor axes (from ellipses or whatever else)... for their angles. project the polygon onto such a plane/line, find min/max, and there you have what you need – Christoph Rackwitz Aug 23 '22 at 08:07
  • 1
    If the masks are not too large, you could simply compute the distances between all pixels in your mask, and then select the pair with the largest distance. This computation can be sped up by computing the convex hull first, as the pixels that are the furthest away from each other have to be part of the hull. – Paul Brodersen Aug 24 '22 at 13:21

1 Answers1

1

I really like the idea that Christoph suggested in the comments.

Here is some code implementing that in Python, using SimpleITK.

import SimpleITK as sitk
import numpy as np

import matplotlib.pyplot as plt

# create an empty array
arr = np.zeros((512,512), dtype=bool)
xx,yy = np.indices(arr.shape)


# add a bunch of overlapping circles
cond = (xx-256)**2 + (yy-256)**2 < 50**2
arr[cond] = 1

cond = (xx-300)**2 + (yy-300)**2 < 50**2
arr[cond] = 1

cond = (xx-210)**2 + (yy-200)**2 < 80**2
arr[cond] = 1

cond = (xx-300)**2 + (yy-190)**2 < 90**2
arr[cond] = 1

# display
fig, ax = plt.subplots(1,1,figsize=(5,5))
ax.imshow(arr, interpolation=None, cmap=plt.cm.Greys_r)
fig.show()

img

# create a SimpleITK image
img = sitk.GetImageFromArray(arr.astype(int))

# generate label 
filter_label = sitk.LabelShapeStatisticsImageFilter()
filter_label.SetComputeFeretDiameter(True)
filter_label.Execute(img)

# compute the Feret diameter
# the 1 means we are computing for the label with value 1
filter_label.GetFeretDiameter(1)

Returns:

264.25177388240934

# we have to get a bit smarter for the principal moments
pc1_x, pc1_y, pc2_x, pc2_y = filter_label.GetPrincipalAxes(1)

# get the center of mass
com_y, com_x = filter_label.GetCentroid(1)

# now trace the distance from the centroid to the edge along the principal axes
# we use some linear algebra

# get the position of each point in the image
v_x, v_y = np.where(arr)

# convert these positions to a vector from the centroid
v_pts = np.array((v_x - com_x, v_y - com_y)).T

# project along the first principal component
distances_pc1 = np.dot(v_pts, np.array((pc1_x, pc1_y)))

# get the extent
dmax_1 = distances_pc1.max()
dmin_1 = distances_pc1.min()

# project along the second principal component
distances_pc2 = np.dot(v_pts, np.array((pc2_x, pc2_y)))

# get the extent
dmax_2 = distances_pc2.max()
dmin_2 = distances_pc2.min()

# the total diameter is the difference in these distances
print("Distance along major axis:", dmax_1 - dmin_1)
print("Distance along minor axis:", dmax_2 - dmin_2)

Distance along major axis: 258.5497770889507

Distance along minor axis: 251.97123773093475

# display
fig, ax = plt.subplots(1,1,figsize=(5,5))
ax.imshow(arr, interpolation=None, cmap=plt.cm.Greys_r)

ax.scatter(com_y, com_x, c="g", marker="o", s=50, zorder=99, label="COM")

ax.plot((com_y, com_y+dmax_1*pc1_y), (com_x, com_x+dmax_1*pc1_x), lw=2, c='b')
ax.plot((com_y, com_y+dmin_1*pc1_y), (com_x, com_x+dmin_1*pc1_x), lw=2, c='b', label="Major axis")

ax.plot((com_y, com_y+dmax_2*pc2_y), (com_x, com_x+dmax_2*pc2_x), lw=2, c='r')
ax.plot((com_y, com_y+dmin_2*pc2_y), (com_x, com_x+dmin_2*pc2_x), lw=2, c='r', label="Minor axis")

ax.legend()

fig.show()

img2

You'll see that the projected distances along the major/minor axes are not necessarily equal to the maximum (Feret) diameter. If you are truly only interested in these, then I'd recommend checking out the feret Python package. In any case, I hope this code gives you something helpful! Also, SimpleITK is actually really great for lots of image processing tasks, and is a nice supplement to SciKit-Image.

Robbie
  • 4,672
  • 1
  • 19
  • 24