OpenCV 中的图像处理 009_OpenCV 中的轮廓

本文主要内容来自于 OpenCV-Python 教程OpenCV 中的图像处理 部分,这部分的全部主要内容如下:

轮廓:入门

目标

轮廓是什么?

轮廓可以简单地解释为连接所有连续点(沿边界)的曲线,具有相同的颜色或强度。轮廓对于形状分析,目标探测和识别是一个很有用的工具。

  • 为了更高的精度,使用二值图像。因此,在寻找轮廓之前,应用阈值或 canny 边缘探测
  • 自 OpenCV 3.2 开始,findContours() 不再修改原始图像
  • 在 OpenCV 中,寻找轮廓就像从黑色背景中找出白色物体一样。因此要记得,要寻找的物体应该为白色的,背景应该为黑色的。

让我们看一下如何寻找二值图像的轮廓:

import numpy as np
import cv2 as cv


def simple_find_contours():
    cv.samples.addSamplesDataSearchPath("/home/hanpfei/data/multimedia/opencv/samples/data")
    img = cv.imread(cv.samples.findFile('butterfly.jpg'))
    imgray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(imgray, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    print("hierarchy shape ", hierarchy.shape)


if __name__ == '__main__':
    simple_find_contours()

可以看到,cv.findContours() 函数中有三个参数,第一个是源图像,第二个是轮廓检索模式,第三个是轮廓逼近法。它输出轮廓和层次结构。轮廓为图像中所有轮廓的 Python 列表。每个单独的轮廓为一个物体的边界点的 (x,y) 坐标的 Numpy 数组。

注意
我们将在后面讨论第二个和第三个参数,以及层次结构的详细内容。在那之前,代码示例中给它们的值将适用于所有图像。

如何绘制轮廓?

要想绘制轮廓,可以使用 cv.drawContours 函数。只要你有边界点,它也可以用于绘制任何形状。它的第一个参数是源图像,第二个参数是应该以 Python 列表的形式传入的轮廓,第三个参数是轮廓的索引(绘制单个轮廓时很有用。要绘制所有轮廓,请传递 -1),其余的参数为颜色,粗细度等等。

  • 在图像中绘制所有轮廓的方法如下:
cv.drawContours(img, contours, -1, (0,255,0), 3)
  • 在图像中绘制单个轮廓,比如第 4 个轮廓的方法如下:
cv.drawContours(img, contours, 3, (0,255,0), 3)
  • 但在大多数时候,下面的方法将很有用:
cnt = contours[4]
cv.drawContours(img, [cnt], 0, (0,255,0), 3)

在上面的示例代码中添加在原图像中绘制轮廓的逻辑,并显示原图像和包含轮廓的图像的代码如下:

import numpy as np
import cv2 as cv


def simple_find_contours():
    cv.samples.addSamplesDataSearchPath("/home/hanpfei/data/multimedia/opencv/samples/data")
    img = cv.imread(cv.samples.findFile('butterfly.jpg'))
    img_original = img.copy()
    imgray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(imgray, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)

    cv.drawContours(img, contours, -1, (0, 0, 255), 2)

    dst = cv.hconcat([img_original, img])
    cv.imshow("Butterfly contours", dst)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    simple_find_contours()

看到的结果应该像下面这样:

Simple find contours

轮廓逼近法

这是 cv.findContours 函数的第三个参数。它实际上表示什么呢?

上面,我们提到过轮廓是具有相同强度的形状的边界。它存储形状的边界的 (x,y) 坐标。但它存储所有的坐标么?这由轮廓逼近法指定。

如果传入 cv.CHAIN_APPROX_NONE,则存储所有的边界点。但实际上我们需要所有的点么?比如,你找到了一条直线轮廓。你需要直线上的所有点来表示它么?不,我们只需要直线两端的点即可。这就是 cv.CHAIN_APPROX_SIMPLE 所作的事情。它移除所有的冗余点并压缩轮廓,从而节省内存。

下面的矩形图像演示了这种技术。只需在轮廓数组中的所有坐标上画一个圆圈(以蓝色绘制)。第一张图像显示了我使用 cv.CHAIN_APPROX_NONE 获得的点(734 个点),第二张图像显示了使用 cv.CHAIN_APPROX_SIMPLE 获得的点(只有 4 个点)。

image

轮廓特征

目标

在这篇文章中,我们将学习

  • 找出轮廓的不同特征,如面积、周长、质心、边界框等
  • 你将看到许多与轮廓相关的函数。

1. 矩

图像矩可以帮助你计算一些特征,如物体的质心、物体的面积等。查看 图像矩 的维基百科页面。

函数 cv.moments() 给出了计算出的所有矩值的字典。 见下文:

import numpy as np
import cv2 as cv

def image_moments():
    img = cv.imread('/media/data2/Download/star.jpg', 0)
    ret, thresh = cv.threshold(img, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    M = cv.moments(cnt)
    print(M)


if __name__ == '__main__':
    image_moments()

从这个矩里,你可以提取有用的数据,比如面积、质心等。质心由这个关系给出,C_x = \frac{M_{10}}{M_{00}}C_y = \frac{M_{01}}{M_{00}}。这可以通过如下代码完成:

cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])

如对于如下图像 star.png

star.png

提取并绘制出质心的方法如下:

import numpy as np
import cv2 as cv


def image_moments():
    img = cv.imread('/media/data2/Download/star.png', 0)
    ret, thresh = cv.threshold(img, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    M = cv.moments(cnt)
    print(M)

    cx = int(M['m10'] / M['m00'])
    cy = int(M['m01'] / M['m00'])

    cv.circle(img, (cx, cy), 10, (0, 0, 255), -1)

    cv.imshow("Star", img)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    image_moments()

看到的结果应该像下面这样:

Star Centroid

2. 轮廓面积

轮廓面积由 cv.contourArea() 函数给出,或来自于矩,M['m00']

area = cv.contourArea(cnt)

3. 轮廓周长

它也称为弧长。可以使用 cv.arcLength() 函数找到它。第二个参数指定形状是闭合轮廓(如果传递 True),还是只是曲线。

perimeter = cv.arcLength(cnt,True)

4. 轮廓逼近

它根据我们指定的精度将轮廓形状近似为具有较少顶点数的另一个形状。它是 Douglas-Peucker 算法 的一种实现。查看维基百科页面以获取算法和演示。

为了理解这一点,假设你试图在图像中找到一个正方形,但由于图像中的一些问题,你没有得到一个完美的正方形,而是一个“坏形状”(如下图第一张所示)。现在你可以使用这个函数来逼近形状。在此,第二个参数称为 epsilon,它是从轮廓到近似轮廓的最大距离。它是一个精度参数。需要明智地选择 epsilon 以获得正确的输出。

epsilon = 0.1*cv.arcLength(cnt,True)
approx = cv.approxPolyDP(cnt,epsilon,True)

下面,在第二张图片中,绿线显示了 epsilon = 10% 弧长的近似曲线。第三张图片显示了相同的 epsilon = 1% 的弧长。第三个参数指定曲线是否闭合。

完整代码如下所示:

import numpy as np
import cv2 as cv


def contour_approximation():
    img = cv.imread('/media/data2/Download/approx.png')

    gray_image = cv.imread('/media/data2/Download/approx.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

    image1 = img.copy()
    epsilon = 0.1 * cv.arcLength(cnt, True)
    approx = cv.approxPolyDP(cnt, epsilon, True)
    cv.drawContours(image1, [approx], -1, (0, 0, 255), 2)

    image2 = img.copy()
    epsilon = 0.01 * cv.arcLength(cnt, True)
    approx = cv.approxPolyDP(cnt, epsilon, True)
    cv.drawContours(image2, [approx], -1, (0, 0, 255), 2)

    dst = cv.hconcat([img, image1, image2])
    cv.imshow("Contour Approximation", dst)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    contour_approximation()

结果如下图:

Contour Approximation

5. 凸包

凸包看起来类似于轮廓逼近,但实际上并非如此(在某些情况下两者可能提供相同的结果)。在这里,cv.convexHull() 函数检查曲线是否存在凸面缺陷并进行纠正。一般来说,凸曲线是总是凸出或至少平坦的曲线。如果它向内凸出,则称为凸面缺陷。例如,检查下面的手的图像。红线展示了手的凸包。双边箭头标记表示凸面缺陷,即包络与轮廓的局部最大偏差。

Image

关于它的语法有一点需要讨论:

hull = cv.convexHull(points[, hull[, clockwise[, returnPoints]]])

参数详情:

  • points 是我们传入的轮廓。
  • hull 是输出,通常我们会避免它。
  • clockwise:方向标志。如果为 True,则输出凸包为顺时针方向。否则,它是逆时针方向的。
  • returnPoints:默认情况下,为 True。然后它返回包络点的坐标。如果为 False,则返回与包络点对应的轮廓点的索引。

因此,要获得如上图所示的凸包,以下内容就足够了:

hull = cv.convexHull(cnt)

但是如果要查找凸性缺陷,则需要通过 returnPoints = False。为了理解它,我们将使用上面的矩形图像。首先我找到它的轮廓为cnt。现在我找到了 returnPoints = True 的凸包,我得到了以下值:[[[234 202]],[[ 51 202]],[[ 51 79]],[[234 79]]],它们是矩形四个角的点。现在如果设置 returnPoints = False 做同样的事情,我会得到以下结果:[[129],[ 67],[ 0],[142]]。这些是轮廓中对应点的索引。比如,检查第一个值:cnt[129] = [[234, 202]] 这与第一个结果相同(其它结果以此类推)。

当我们讨论凸性缺陷时,你会再次看到它。

上面例子中,手的图像如下:

Convex Hull Hands

完整代码如下所示:

import numpy as np
import cv2 as cv


def convex_hull():
    img = cv.imread('/media/data2/Download/hands.png')

    gray_image = cv.imread('/media/data2/Download/hands.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

    image1 = img.copy()

    hull = cv.convexHull(cnt)
    cv.drawContours(image1, [hull], -1, (0, 0, 255), 2)

    dst = cv.hconcat([img, image1])
    cv.imshow("Convex Hull", dst)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    convex_hull()

结果如下图:

Convex Hull

6. 检查凸度

有一个函数来检查曲线是否是凸的,cv.isContourConvex()。它只返回 True 或 False。没有大碍。

k = cv.isContourConvex(cnt)

7. 边界矩形

有两种类型的边界矩形。

7.a. 直边界矩形

它是一个直的矩形,它不考虑物体的旋转。所以边界矩形的面积不会是最小的。它由函数 cv.boundingRect() 找到。

令 (x,y) 为矩形的左上角坐标, (w,h) 为其宽度和高度。

7.b. 旋转矩形

在这里,边界矩形是用最小面积绘制的,所以它也考虑了旋转。用到的函数为 cv.minAreaRect()。它返回一个 Box2D 结构,其中包含以下详细信息 - (中心 (x,y)、(宽度、高度)、旋转角度)。但是要绘制这个矩形,我们需要矩形的 4 个角。由函数 cv.boxPoints() 获得。

rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int0(box)
cv.drawContours(img,[box],0,(0,0,255),2)

两个矩形都显示在一个图像中。绿色矩形显示正常边界矩形。红色矩形是旋转后的矩形。原始图像如下:

Image

完整代码如下所示:

import numpy as np
import cv2 as cv


def bounding_rectangle():
    img = cv.imread('/media/data2/Download/image.png')

    gray_image = cv.imread('/media/data2/Download/image.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

    image1 = img.copy()

    x, y, w, h = cv.boundingRect(cnt)
    cv.rectangle(image1, (x, y), (x + w, y + h), (0, 255, 0), 2)

    rect = cv.minAreaRect(cnt)
    box = cv.boxPoints(rect)
    box = np.int0(box)
    cv.drawContours(image1, [box], 0, (0, 0, 255), 2)

    cv.imshow("Bounding Rectangle", image1)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    bounding_rectangle()

结果如下图:

Bounding Rectangle

8. 最小封闭圆

接下来我们使用函数 cv.minEnclosingCircle() 寻找物体的外接圆。它是完全覆盖物体的面积最小的圆。

    (x, y), radius = cv.minEnclosingCircle(cnt)
    center = (int(x), int(y))
    radius = int(radius)
    cv.circle(img, center, radius, (0, 255, 0), 2)
Image

完整代码如下所示:

import numpy as np
import cv2 as cv


def minimum_enclosing_circle():
    img = cv.imread('/media/data2/Download/image.png')

    gray_image = cv.imread('/media/data2/Download/image.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

    (x, y), radius = cv.minEnclosingCircle(cnt)
    center = (int(x), int(y))
    radius = int(radius)
    cv.circle(img, center, radius, (0, 255, 0), 2)

    cv.imshow("Bounding Rectangle", img)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    minimum_enclosing_circle()

9. 拟合椭圆

下一个是将椭圆拟合到对象。它返回内接椭圆的旋转矩形。

    ellipse = cv.fitEllipse(cnt)
    cv.ellipse(img, ellipse, (0, 255, 0), 2)
Image

完整代码如下所示:

import numpy as np
import cv2 as cv


def fitting_an_ellipse():
    img = cv.imread('/media/data2/Download/image.png')

    gray_image = cv.imread('/media/data2/Download/image.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

    ellipse = cv.fitEllipse(cnt)
    cv.ellipse(img, ellipse, (0, 255, 0), 2)

    cv.imshow("Bounding Rectangle", img)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    fitting_an_ellipse()

9. 拟合直线

同样,我们可以将一条线拟合到一组点。 下图包含一组白点。 我们可以近似为一条直线。

    rows, cols = img.shape[:2]
    [vx, vy, x, y] = cv.fitLine(cnt, cv.DIST_L2, 0, 0.01, 0.01)
    lefty = int((-x * vy / vx) + y)
    righty = int(((cols - x) * vy / vx) + y)
    cv.line(img, (cols - 1, righty), (0, lefty), (0, 255, 0), 2)
Image

完整代码如下所示:

import numpy as np
import cv2 as cv


def fitting_a_ine():
    img = cv.imread('/media/data2/Download/image.png')

    gray_image = cv.imread('/media/data2/Download/image.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

    rows, cols = img.shape[:2]
    [vx, vy, x, y] = cv.fitLine(cnt, cv.DIST_L2, 0, 0.01, 0.01)
    lefty = int((-x * vy / vx) + y)
    righty = int(((cols - x) * vy / vx) + y)
    cv.line(img, (cols - 1, righty), (0, lefty), (0, 255, 0), 2)

    cv.imshow("Bounding Rectangle", img)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    fitting_a_ine()

其它资源

练习

参考文档

Contours in OpenCV

Contours : Getting Started

Contour Features

Contour Properties

Contours : More Functions

Contours Hierarchy

Done.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容