Find extreme outer points in image with Python OpenCV

霸气de小男生 提交于 2020-01-24 03:25:26

问题


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()

回答1:


Here's a potential approach:

  • Convert image to grayscale and blur image
  • Threshold to obtain a binary image
  • Find contours
  • 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

image = cv2.imread('2.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]

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)

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])

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()



回答2:


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.




回答3:


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.




回答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.



来源:https://stackoverflow.com/questions/56781635/find-extreme-outer-points-in-image-with-python-opencv

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!