7

VI have a set of contour points drawn on an image which is stored as a 2D numpy array. The contours are represented by 2 numpy arrays of float values for x and y coordinates each. These coordinates are not integers and do not align perfectly with pixels but they do tell you the location of the contour points with respect to pixels.

I would like to be able to select the pixels that fall within the contours. I wrote some code that is pretty much the same as answer given here: Access pixel values within a contour boundary using OpenCV in Python

temp_list = []
for a, b in zip(x_pixel_nos, y_pixel_nos):
    temp_list.append([[a, b]]) # 2D array of shape 1x2
temp_array = np.array(temp_list)
contour_array_list = []
contour_array_list.append(temp_array)



lst_intensities = []

# For each list of contour points...
for i in range(len(contour_array_list)):
    # Create a mask image that contains the contour filled in
    cimg = np.zeros_like(pixel_array)
    cv2.drawContours(cimg, contour_array_list, i, color=255, thickness=-1)

# Access the image pixels and create a 1D numpy array then add to list
pts = np.where(cimg == 255)
lst_intensities.append(pixel_array[pts[0], pts[1]])

When I run this, I get an error error: OpenCV(3.4.1) /opt/conda/conda-bld/opencv-suite_1527005509093/work/modules/imgproc/src/drawing.cpp:2515: error: (-215) npoints > 0 in function drawContours

I am guessing that at this point openCV will not work for me because my contours are floats, not integers, which openCV does not handle with drawContours. If I convert the coordinates of the contours to integers, I lose a lot of precision.

So how can I get at the pixels that fall within the contours?

enter image description here

This should be a trivial task but so far I was not able to find an easy way to do it.

Jeru Luke
  • 20,118
  • 13
  • 80
  • 87
Semihcan Doken
  • 776
  • 3
  • 10
  • 23
  • 1
    If your pixels are floating point then there are theoretically an infinite amount of pixels that fall within the contour due to the locations now being continuous. What constraints do you want to impose to limit the length of this list of pixels? Also remember that the list of contour pixels is such that each element needs to be a 3d array with a singleton second dimension (i.e. A N x 1 x 2 array as noted in the other post). – rayryeng Jun 14 '18 at 00:05
  • @rayryeng My pixels are not continuous. They are integers. It is the contours that are floats. I am guessing this is caused by the fact that these contours were drawn manually by a human who zoomed in on the image while drawing the contours. – Semihcan Doken Jun 14 '18 at 00:06
  • 1
    Ah I see. However it doesn't look like your contour list is constructed properly. See my note earlier about them being 3d arrays. – rayryeng Jun 14 '18 at 00:10
  • @rayryeng I edited the question (line 3 in code) so that it now has the correct contour list construction which is what I ran on my computer. It now constructs a list of 3D array of Nx1x2 – Semihcan Doken Jun 14 '18 at 00:15
  • @rayryeng Please let me know if you still think that `contour_array_list` is constructed improperly. It should be correct as I designed it as you recommended in the other question. – Semihcan Doken Jun 14 '18 at 00:35
  • 1
    Interesting. I need to experiment. I'm not at a computer right now so I won't be able to try until later tonight. Do you happen to have the contour data available somewhere? – rayryeng Jun 14 '18 at 00:40
  • @rayryeng I put the x and y coordinates of the contours into a gist here: https://gist.github.com/sdoken/173fae1f9d8673ffff5b481b3872a69d Referring to a 512 by 512 numpy array, these coordinates tell you the location of the contour points. – Semihcan Doken Jun 14 '18 at 02:56
  • @CrisLuengo Does it matter that my contours are pretty irregular and do not make up what most people would identify as a polygon? Or can any set of contour points be considered to make up a polygon? This has not been trivial in my experience. – Semihcan Doken Jun 14 '18 at 02:59
  • 1
    @CrisLuengo I am not concerned with computational efficiency at this point. I would appreciate if you could show me the code that would accomplish this. – Semihcan Doken Jun 14 '18 at 03:17
  • @itsnotme: There exist algorithms that work only for convex polygons, but many of them support even self-intersecting polygons (with [one](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) or [another](https://en.wikipedia.org/wiki/Nonzero-rule) winding rule to define the interior). – Davis Herring Jun 14 '18 at 06:00
  • @rayryeng Actually I think that `contour_array_list` was constructed properly as a list of Nx1x2 numpy arrays in the way that I originally published the code. Can you double-check if you still think it was not constructed properly or explain what you mean? – Semihcan Doken Jun 15 '18 at 23:05

3 Answers3

5

I think that the simplest way of finding all pixels that fall within the contour is as follows.

The contour is described by a set of non-integer points. We can think of these points as vertices of a polygon, the contour is a polygon.

We first find the bounding box of the polygon. Any pixel outside of this bounding box is not inside the polygon, and doesn't need to be considered.

For the pixels inside the bounding box, we test if they are inside the polygon using the classical test: Trace a line from some point at infinity to the point, and count the number of polygon edges (line segments) crossed. If this number is odd, the point is inside the polygon. It turns out that Matplotlib contains a very efficient implementation of this algorithm.

I'm still getting used to Python and Numpy, this might be a bit awkward code if you're a Python expert. But it is straight-forward what it does, I think. First it computes the bounding box of the polygon, then it creates an array points with the coordinates of all pixels that fall within this bounding box (I'm assuming the pixel centroid is what counts). It applies the matplotlib.path.contains_points method to this array, yielding a boolean array mask. Finally, it reshapes this array to match the bounding box.

import math
import matplotlib.path
import numpy as np

x_pixel_nos = [...]
y_pixel_nos = [...] # Data from https://gist.github.com/sdoken/173fae1f9d8673ffff5b481b3872a69d

temp_list = []
for a, b in zip(x_pixel_nos, y_pixel_nos):
   temp_list.append([a, b])

polygon = np.array(temp_list)
left = np.min(polygon, axis=0)
right = np.max(polygon, axis=0)
x = np.arange(math.ceil(left[0]), math.floor(right[0])+1)
y = np.arange(math.ceil(left[1]), math.floor(right[1])+1)
xv, yv = np.meshgrid(x, y, indexing='xy')
points = np.hstack((xv.reshape((-1,1)), yv.reshape((-1,1))))

path = matplotlib.path.Path(polygon)
mask = path.contains_points(points)
mask.shape = xv.shape

After this code, what is necessary is to locate the bounding box within the image, and color the pixels. left contains the pixel in the image corresponding to the top-left pixel of mask.


It is possible to improve the performance of this algorithm. If the ray traced to test a pixel is horizontal, you can imagine that all the pixels along a horizontal line can benefit from the work done for the pixels to the left. That is, it is possible to compute the in/out status for all pixels on an image line with a little bit more effort than the cost for a single pixel.

The matplotlib.path.contains_points algorithm is much more efficient than performing a single-point test for all points, since sorting the polygon edges and vertices appropriately make each test much cheaper, and that sorting only needs to be done once when testing many points at once. But this algorithm doesn't take into account that we want to test many points on the same line.


These are what I see when I do

pp.plot(x_pixel_nos, y_pixel_nos)
pp.imshow(mask)

after running the code above with your data. Note that the y axis is inverted with imshow, hence the vertically mirrored shapes.

enter image description here

enter image description here

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
  • I am very grateful for your help here. This code, which I tried on multiple images, enabled me to find pixels within the 'contours'. (I am not picky about code style and just care about a working solution) A few questions that remain for me: a) I would love to know the thinking that went into using `hstack` and `meshgrid` here as I was not able to decipher how you ended up coming up with using those functions and how this differs from their normal use. b) Given a set of contour points (or vertices), does `matplotlib.path.Path` always find a convex polygon? What if the shape we need is concave? – Semihcan Doken Jun 21 '18 at 19:51
  • 1
    @itsnotme: The polygon does not need to be convex, there are no such assumptions in this code. `Path` can encode any polygon (actually, I'm not sure about self-intersections, but you don't have those). `meshgrid` and `hstack` I used in the way I'm used to in MATLAB. Basically, `meshgrid` generates in `xv` and `yv` 2D arrays that together contain the (x,y) coordinates at each point within a rectangular area, described by the vectors `x` and `y`. I reshaped these two to 1D arrays and concatenated them (`hstack`) to form a 2xN array of coordinates, as expected by `contains_points`. – Cris Luengo Jun 21 '18 at 20:11
  • Here I plotted the contours and the pixels that came out of your code. https://github.com/sdoken/pydicom_rtstruct_contour_mapping/blob/master/Screen%20Shot%202018-06-21%20at%2018.22.12.png If you look closely at the bottom of the colored regions, you will see that the green area is missing the concaveness that the red boundary in the bottom image has. I am not sure if this is an actual problem (the algorithm selected some points to be within when they are without) or an artifact of the visualization process, and not a problem – Semihcan Doken Jun 22 '18 at 02:38
  • @itsnotme: see the plots I just attached to the answer. There is no reason to believe the functions called create a convex hull. I'm not sure what happened in the image you posted. Does that polygon have any self-intersections? – Cris Luengo Jun 22 '18 at 04:34
1

With Help of Shapely library in python, it can easily be done as:

from shapely.geometry import Point, Polygon

Convert all the x,y coords to shapely Polygons as:

coords = [(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)]
pl = Polygon(coords)

Now find pixels in each of polygon as:

minx, miny, maxx, maxy = pl.bounds
minx, miny, maxx, maxy = int(minx), int(miny), int(maxx), int(maxy)
box_patch = [[x,y] for x in range(minx,maxx+1) for y in range(miny,maxy+1)]
pixels = []
for pb in box_patch: 
  pt = Point(pb[0],pb[1])
  if(pl.contains(pt)):
    pixels.append([int(pb[0]), int(pb[1])])
return pixels

Put this loop for each set of coords and then for each polygons.

good to go :)

sandeepsign
  • 539
  • 6
  • 11
1

skimage.draw.polygon can handle this 1, see the example code of this function on that page.

If you want just the contour, you can do skimage.segmentation.find_boundaries 2.

Hung-Yi Wu
  • 183
  • 2
  • 7