'Determine if a point is inside or outside of a shape with opencv

I have images with white background and simple shapes in them (each image has one shape). I want to determine if a certain point (x,y) is inside the shape or not. How can I do that with opencv?



Solution 1:[1]

Use pointPolygonTest function. Here's tutorial.

Solution 2:[2]

To determine if a point is inside, outside, or on the edge of a shape you can check if the point is within a contour using cv2.pointPolygonTest(). The function returns +1, -1, or 0 to indicate if a point is inside, outside, or on the contour, respectively. Assuming we already have the contour of the shape, we can simply pass the contour and the (x,y) point to the function.

result = cv2.pointPolygonTest(contour, (x,y), False) 

In the function, the third argument is measureDist. If it is True, it finds the shortest distance between a point in the image and a contour. If False, it finds whether the point is inside, outside, or on the contour. Since we don't want to find the distance, we set the measureDist argument to False

Here's an example that finds the square contour then checks if the points are within the contour


Test image

Image after finding contour and checking points

Results

point1: -1.0

point2: 1.0

point3: 0.0

Therefore point1 is outside, point2 is inside, and point3 is on the contour

import cv2

image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
canny = cv2.Canny(gray, 120, 255, 1)
cnts = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]

point1 = (25, 50)
point2 = (200, 250)
point3 = (200, 350)

# Perform check if point is inside contour/shape
for c in cnts:
    cv2.drawContours(image, [c], -1, (36, 255, 12), 2)
    result1 = cv2.pointPolygonTest(c, point1, False)
    result2 = cv2.pointPolygonTest(c, point2, False)
    result3 = cv2.pointPolygonTest(c, point3, False)

# Draw points
cv2.circle(image, point1, 8, (100, 100, 255), -1)
cv2.putText(image, 'point1', (point1[0] -10, point1[1] -20), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), lineType=cv2.LINE_AA)
cv2.circle(image, point2, 8, (200, 100, 55), -1)
cv2.putText(image, 'point2', (point2[0] -10, point2[1] -20), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), lineType=cv2.LINE_AA)
cv2.circle(image, point3, 8, (150, 50, 155), -1)
cv2.putText(image, 'point3', (point3[0] -10, point3[1] -20), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), lineType=cv2.LINE_AA)

print('point1:', result1)
print('point2:', result2)
print('point3:', result3)
cv2.imshow('image', image)
cv2.waitKey()

Solution 3:[3]

In my case I needed to select all points inside the contour. I have a scatter plot for x and y, which are lists of 2D Cartesian coordinates, of the same length

plt.scatter(x, y) 

and a contour plot

cs = axes.contour(x[left:right], y[bottom:top], quantity(x,y), level = [value])

where quantity is some other value, for example density, overlapped on the scatter plot.

enter image description here

It is important, not only to select the pixels inside the contour, but also to retrieve the coordinates of those points used in the scatter plot. If the contour is well defined and you really have a single closed line than you can access it in Matplotlib with

cs.allsegs[0][0]

and this looks like:

[[array([[1933.769124  ,  690.53960716],
     [1933.904277  ,  690.52641719],
     [1934.03943   ,  690.5241056 ],
     ...,
     [1933.633971  ,  690.56338213],
     [1933.76492346,  690.540361  ],
     [1933.769124  ,  690.53960716]])]]

Now loop along all tuples in the contour and increment a counter as soon as:

  • a vertical tuple coordinate from the contour is greater than the one of the point being checked;
  • a vertical tuple coordinate from the contour is smaller than the one of the point being checked;
  • an horizontal tuple coordinate from the contour is greater than the one of the point being checked;
  • an horizontal tuple coordinate from the contour is smaller than the one of the point being checked;

If the counter reaches 4, the point is inside, provided that the contour is closed. The definition makes use of a tolerance value tol which should be smaller comparable with the distance between the points which make the contour:

def OnePointInsideContour(contourArray, PointTuple, tol):
L = len(contourArray);
y0 = PointTuple[0]; # horizontal
x0 = PointTuple[1]; # vertical
ret = [];
inside = False;
counter = 0

for k in range(L):
    ycont = list(contourArray[k])[0];
    xcont = list(contourArray[k])[1];
    if ycont > y0 - tol and ycont < y0 + tol and xcont > x0: 
        p = (ycont, xcont); 
        counter += 1; 
        ret.append(p);
        break

for k in range(L):
    ycont = list(contourArray[k])[0];
    xcont = list(contourArray[k])[1];
    if ycont > y0 - tol and ycont < y0 + tol and xcont < x0: 
        p = (ycont, xcont); 
        counter += 1; 
        ret.append(p);
        break           

for k in range(L):
    ycont = list(contourArray[k])[0];
    xcont = list(contourArray[k])[1];
    if xcont > x0 - tol and xcont < x0 + tol and ycont < y0:
        p = (ycont, xcont); 
        counter += 1; 
        ret.append(p);
        break  

for k in range(L):
    ycont = list(contourArray[k])[0];
    xcont = list(contourArray[k])[1];
    if xcont > x0 - tol and xcont < x0 + tol and ycont > y0: 
        p = (ycont, xcont); 
        counter += 1; 
        ret.append(p);
        break

if counter == 4:
    inside = True

return inside, ret

Example: point not inside (only 2 hits on the contour)

enter image description here

Example: point inside (4 hits on the contour)

enter image description here

Solution 4:[4]

if you want to access all the points inside the convex hull, you can do masking

I solve this by first painting my convex hull white colour with cv2.fillPoly() on a black frame

  1. First create black frame that follows your frame's shape
    black_frame = np.zeros_like(your_frame).astype(np.uint8)
  2. Paint the convex hull with white
    cv2.fillPoly(black_frame , [hull], (255, 255, 255))
  3. Create a mask by using numpy boolean indexing, it will produce a mask with True/False values inside, it will be True is the pixel value is white
    mask = black_frame == 255
  4. You can access your pixel values by getting the product between your frame and mask, if False, value will
    targetROI = your_frame * mask
  5. Access your pixels by using the mask.
black_frame = np.zeros_like(your_frame).astype(np.uint8)
cv2.fillPoly(black_frame , [hull], (255, 255, 255))
mask = black_frame == 255
targetROI = your_frame * mask

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Pylyp Dukhov
Solution 2
Solution 3
Solution 4 yptheangel