'How to find extreme outer points in an image with Python OpenCV
I have this image of a statue.
I'm trying to find the top, bottom, left, and right most points on the statue. Is there a way to measure the edge of each side to determine the outer most point on the statue? I want to get the (x,y)
coordinate of each side. I have tried to use cv2.findContours()
and cv2.drawContours()
to get an outline of the statue.
import cv2
img = cv2.imread('statue.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours = cv2.findContours(gray, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0]
cv2.drawContours(img, contours, -1, (0, 200, 0), 3)
cv2.imshow('img', img)
cv2.waitKey()
Solution 1:[1]
Here's a potential approach:
Convert image to grayscale and Gaussian blur
Threshold to obtain a binary image
Obtain outer coordinates
After converting to grayscale and blurring image, we threshold to get a binary image
Now we find contours using cv2.findContours()
. Since OpenCV uses Numpy arrays to encode images, a contour is simply a Numpy array of (x,y)
coordinates. We can slice the Numpy array and use argmin()
or argmax()
to determine the outer left, right, top, and bottom coordinates like this
left = tuple(c[c[:, :, 0].argmin()][0])
right = tuple(c[c[:, :, 0].argmax()][0])
top = tuple(c[c[:, :, 1].argmin()][0])
bottom = tuple(c[c[:, :, 1].argmax()][0])
Here's the result
left: (162, 527)
right: (463, 467)
top: (250, 8)
bottom: (381, 580)
import cv2
import numpy as np
# Load image, grayscale, Gaussian blur, threshold
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(blur, 220, 255, cv2.THRESH_BINARY_INV)[1]
# Find contours
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
c = max(cnts, key=cv2.contourArea)
# Obtain outer coordinates
left = tuple(c[c[:, :, 0].argmin()][0])
right = tuple(c[c[:, :, 0].argmax()][0])
top = tuple(c[c[:, :, 1].argmin()][0])
bottom = tuple(c[c[:, :, 1].argmax()][0])
# Draw dots onto image
cv2.drawContours(image, [c], -1, (36, 255, 12), 2)
cv2.circle(image, left, 8, (0, 50, 255), -1)
cv2.circle(image, right, 8, (0, 255, 255), -1)
cv2.circle(image, top, 8, (255, 50, 0), -1)
cv2.circle(image, bottom, 8, (255, 255, 0), -1)
print('left: {}'.format(left))
print('right: {}'.format(right))
print('top: {}'.format(top))
print('bottom: {}'.format(bottom))
cv2.imshow('thresh', thresh)
cv2.imshow('image', image)
cv2.waitKey()
Solution 2:[2]
Here's a possible improvement to nathancy's answer, where most of the code comes from, also the main idea of using np.argmax
. So, please have a look at that answer before!
Since we already have a binarized image from cv2.threshold
, such that the (white) background of the input image is set to zero, we can use the ability of cv2.boundingRect
to "calculate the up-right bounding rectangle of a point set or non-zero pixels of gray-scale image". The method returns a tuple (x, y, w, h)
with (x, y)
the upper left point as well as width w
and height h
of the bounding rectangle. From there, the mentioned points left
, right
, etc. can be obtained easily using np.argmax
on the corresponding slice of the thresh
image.
Here comes the full code:
import cv2
import numpy as np
image = cv2.imread('images/dMXjY.png')
blur = cv2.GaussianBlur(image, (3,3), 0)
gray = cv2.cvtColor(blur, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 220, 255, cv2.THRESH_BINARY_INV)[1]
x, y, w, h = cv2.boundingRect(thresh) # Replaced code
#
left = (x, np.argmax(thresh[:, x])) #
right = (x+w-1, np.argmax(thresh[:, x+w-1])) #
top = (np.argmax(thresh[y, :]), y) #
bottom = (np.argmax(thresh[y+h-1, :]), y+h-1) #
cv2.circle(image, left, 8, (0, 50, 255), -1)
cv2.circle(image, right, 8, (0, 255, 255), -1)
cv2.circle(image, top, 8, (255, 50, 0), -1)
cv2.circle(image, bottom, 8, (255, 255, 0), -1)
print('left: {}'.format(left))
print('right: {}'.format(right))
print('top: {}'.format(top))
print('bottom: {}'.format(bottom))
cv2.imshow('thresh', thresh)
cv2.imshow('image', image)
cv2.waitKey()
The image outputs look like the ones in nathancy's answer.
Nevertheless, one of the resulting points differs a bit:
left: (162, 527)
right: (463, 461) (instead of (463, 467))
top: (250, 8)
bottom: (381, 580)
If we have a closer look on the thresh
image, we'll see that for the 463
-th column, all pixels in the range of 461 ... 467
have a value of 255
. So, for the right edge, there's no unique extreme value.
The contour c
found in nathancy's approach holds the two points (463, 467)
and (463, 461)
in that order, such that np.argmax
will find (463, 467)
first. In my approach, the 463
-th column is examined from 0
to (height of image)
, such that np.argmax
will find (463, 461)
first instead.
From my point of view, both (or even all other points in between) are suitable results, since there's no additional constraint on the handling of multiple extreme points.
Using cv2.boundingRect
saves two lines of code, and also performs faster, at least according to some short tests using timeit
.
Disclosure: Again, most of the code and the main idea come from nathancy's answer.
Solution 3:[3]
Rather than inspecting every single element (and stalling the CPU with an if
statement for every pixel) it is probably faster to sum all the elements down every column. They should come to 600*255, or 153,000 if they are all white. So, then find where 153,000 minus the column-total is non-zero. The first and last will be the top and bottom of the statue.
Then repeat across the rows to find left and right extrema.
So, starting with the greyscale image, run down each row totalling up the pixels:
import numpy as np
# Total up all the elements in each column
colsums = np.sum(gray, axis=0)
The sums of each column now look like this:
array([153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 152991, 153000, 152976, 152920,
152931, 152885, 151600, 148818, 147448, 146802, 146568, 146367,
146179, 145888, 145685, 145366, 145224, 145066, 144745, 144627,
144511, 144698, 144410, 144329, 144162, 143970, 143742, 143381,
141860, 139357, 135358, 133171, 131138, 129246, 128410, 127866,
127563, 127223, 126475, 125614, 125137, 124848, 122906, 121653,
119278, 115548, 114473, 113800, 113486, 112655, 112505, 112670,
111845, 111124, 110378, 110315, 109996, 109693, 109649, 109411,
110626, 110628, 112247, 112348, 111865, 111571, 110601, 108308,
107213, 106768, 105546, 103971, 103209, 101866, 100215, 98964,
98559, 97008, 94981, 94513, 92490, 91555, 91491, 90072,
88642, 87210, 86960, 86834, 85759, 84496, 83237, 81911,
80249, 78942, 77715, 76918, 75746, 75826, 75443, 75087,
75156, 75432, 75730, 75699, 77028, 77825, 76813, 76718,
75958, 75207, 74216, 73042, 72527, 72043, 71819, 71384,
70693, 69922, 69537, 69685, 69688, 69876, 69552, 68937,
68496, 67942, 67820, 67626, 67627, 68113, 68426, 67894,
67868, 67365, 66191, 65334, 65752, 66438, 66285, 66565,
67616, 69090, 69386, 69928, 70470, 70318, 70228, 71028,
71197, 71827, 71712, 71312, 72013, 72878, 73398, 74038,
75017, 76270, 76087, 75317, 75210, 75497, 75099, 75620,
75059, 75008, 74146, 73531, 73556, 73927, 75395, 77235,
77094, 77229, 77463, 77808, 77538, 77104, 76816, 76500,
76310, 76331, 76889, 76293, 75626, 74966, 74871, 74950,
74931, 74852, 74885, 75077, 75576, 76104, 76208, 75387,
74971, 75878, 76311, 76566, 77014, 77205, 77231, 77456,
77983, 78379, 78793, 78963, 79154, 79710, 80777, 82547,
85164, 88944, 91269, 92438, 93646, 94836, 96071, 97918,
100244, 102011, 103553, 104624, 104961, 105354, 105646, 105866,
106367, 106361, 106461, 106659, 106933, 107055, 106903, 107028,
107080, 107404, 107631, 108022, 108194, 108261, 108519, 109023,
109349, 109873, 110373, 110919, 111796, 112587, 113219, 114143,
115161, 115733, 116531, 117615, 118338, 119414, 120492, 121332,
122387, 123824, 124938, 126113, 127465, 128857, 130411, 131869,
133016, 133585, 134442, 135772, 136440, 136828, 137200, 137418,
137705, 137976, 138167, 138481, 138788, 138937, 139194, 139357,
139375, 139583, 139924, 140201, 140716, 140971, 141285, 141680,
141837, 141975, 142260, 142567, 142774, 143154, 143533, 143853,
144521, 145182, 145832, 147978, 149006, 150026, 151535, 152753,
152922, 152960, 152990, 152991, 153000, 152995, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000,
153000, 153000, 153000, 153000, 153000, 153000, 153000, 153000],
dtype=uint64)
Now find where those columns do not sum up to 153,000:
np.nonzero(153000-colsums)
That looks like this:
(array([156, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169,
170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182,
183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208,
209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221,
222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234,
235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,
248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260,
261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273,
274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286,
287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299,
300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312,
313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325,
326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338,
339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351,
352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364,
365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377,
378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390,
391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403,
404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416,
417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429,
430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442,
443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455,
456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 469]),)
So the top row that doesn't consist entirely of white pixels is row 156 (the first entry) and the bottom row that doesn't entirely consist of white pixels is row 469 (the last entry).
Now sum across the other axis (axis=1) and do the same thing again to get left and right extrema.
Solution 4:[4]
You don't need expensive code like findContours
. You just need to scan the image line-by-line from 4 sides outside-in until you find the first non-white pixel.
From the left, start scanning top left to bottom left. If no white pixel is found, move 1 pixel to the right and go from top to bottom again. Once you find a non-white-pixel, this is your left
.
Do the same for all sides.
Solution 5:[5]
import cv2
import numpy as np
img = cv2.imread('statue.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 220, 255, cv2.THRESH_BINARY_INV)[1]
sz=thresh.shape
top=divmod(np.flatnonzero(thresh)[0], sz[0])[::-1]
botton=divmod(np.flatnonzero(thresh)[-1], sz[0])[::-1]
thresh=thresh.T
left=divmod(np.flatnonzero(thresh)[0], sz[1])
right=divmod(np.flatnonzero(thresh)[-1], sz[1])
print(left, right, top, botton, sep='\n')
Solution 6:[6]
skew the contours for points x,y, z. assuming the left corner is calculated using the trapezoidal rule. it applies for both top, bottom, to the right tail of the distribution. if the value of z is guassian. we take y as the langrange form of the distribution. interpolate x,y,z to get x,y points. when we say interpolation, we mean skewing the matrix for N-1 points from y. it would give a clearer picture of the statue.
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 | |
Solution 2 | Community |
Solution 3 | Mark Setchell |
Solution 4 | Rob Audenaerde |
Solution 5 | Alex Alex |
Solution 6 | charles |