OpenCV-Python (官方)中文教程(部分一)_Day18

21.OpenCV中的轮廓

21.1初识轮廓

(1).轮廓的概念

轮廓可以简单认为成将连续的点(连着边界)连在一起的曲线,具有相同 的颜色或者灰度。轮廓在形状分析和物体的检测和识别中很有用。

• 为了更加准确,要使用二值化图像。在寻找轮廓之前,要进行阈值化处理 或者 Canny 边界检测。

• 查找轮廓的函数会修改原始图像。如果你在找到轮廓之后还想使用原始图像的话,你应该将原始图像存储到其他变量中。

• 在 OpenCV 中,查找轮廓就像在黑色背景中找白色物体。你应该记住, 要找的物体应该是白色而背景应该是黑色。

让我们看看如何在一个二值图像中查找轮廓:

函数 cv2.findContours() 有三个参数,第一个是输入图像,第二个是 轮廓检索模式,第三个是轮廓近似方法。返回值有三个,第一个是图像,第二个 是轮廓,第三个是(轮廓的)层析结构。轮廓(第二个返回值)是一个 Python 列表,其中存储这图像中的所有轮廓。每一个轮廓都是一个 Numpy 数组,包 含对象边界点(x,y)的坐标。

注意:我们后边会对第二和第三个参数,以及层次结构进行详细介绍。在那之 前,例子中使用的参数值对所有图像都是适用的。

(2).轮廓的绘制

函数 cv2.drawContours() 可以被用来绘制轮廓。它可以根据你提供 的边界点绘制任何形状。它的第一个参数是原始图像,第二个参数是轮廓,一 个 Python 列表。第三个参数是轮廓的索引(在绘制独立轮廓是很有用,当设置 -1时绘制所有轮廓)。接下来的参数是轮廓的颜色和厚度等。

在一幅图像上绘制所有的轮廓:

import cv2

im = cv2.imread('11111.png')

imgray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)

ret, thresh = cv2.threshold(imgray, 127, 255, 0)

# OpenCV 4.x 版本写法(推荐)

contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 绘制轮廓(可选)

cv2.drawContours(im, contours, -1, (0, 255, 0), 2)

cv2.imshow('Contours', im)

cv2.waitKey(0)

cv2.destroyAllWindows()

结果:

绘制独立轮廓,如第四个轮廓:

img = cv2.drawContour(img, contours, -1, (0,255,0), 3)

但是大多数时候,下面的方法更有用:

img = cv2.drawContours(img, contours, 3, (0,255,0), 3)

注意:最后这两种方法结果是一样的,但是后边的知识会告诉你最后一种方法 更有用。

(3).轮廓的近似方法

这是函数  cv2.findCountours()  的第三个参数。它到底代表什么意思呢?

上边我们已经提到轮廓是一个形状具有相同灰度值的边界。它会存贮形状边界上所有的 (x,y) 坐标。但是需要将所有的这些边界点都存储吗?这就是这 个参数要告诉函数   cv2.findContours  的。

这个参数如果被设置为 cv2.CHAIN_APPROX_NONE,所有的边界点 都会被存储。但是我们真的需要这么多点吗?例如,当我们找的边界是一条直 线时。你用需要直线上所有的点来表示直线吗?不是的,我们只需要这条直线 的两个端点而已。这就是cv2.CHAIN_APPROX_SIMPLE 要做的。它会将轮廓上的冗余点都去掉,压缩轮廓,从而节省内存开支。 我们用下图中的矩形来演示这个技术。在轮廓列表中的每一个坐标上画一个蓝色圆圈。第一个图显示使用 cv2.CHAIN_APPROX_NONE 的效果, 一共 734 个点。第二个图是使用 cv2.CHAIN_APPROX_SIMPLE 的结果,只有 4 个点。

21.2轮廓特征

(1).矩

图像的矩可以用于计算图像的质心,面积等。详细信息请查看Image Moments。

函数 cv2.moments() 会将计算得到的矩以一个字典的形式返回。如下:

import cv2

img = cv2.imread('star.jpg', 0)

ret, thresh = cv2.threshold(img, 127, 255, 0)

contours, hierarchy = cv2.findContours(thresh, 1, 2)

cnt = contours[0]

M = cv2.moments(cnt)

print(M)

根据这些矩的值,我们可以计算出对象的重心:

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

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

1. 什么是图像矩(Image Moments)?​

在图像处理中,矩(Moments)用于描述物体的形状、位置和分布。OpenCV 的 cv2.moments() 计算的是空间矩(Spatial Moments),其数学定义为:

I(x,y) 是像素点 (x,y) 的强度(二值图像中通常是 0 或 255)。

p 和 q 是矩的阶数(如 M 00、M 10 、M 01)。

2. 为什么 M['m10']/M['m00'] 和 M['m01']/M['m00'] 是重心?​​

​​(1) M['m00'](零阶矩)​​

表示轮廓或区域的总面积(二值图像中等于所有非零像素的总和)。

公式:

​(2) M['m10'](一阶矩,x 方向)​​

表示x 方向的加权和(权重是像素值)。

公式:

​​(3) M['m01'](一阶矩,y 方向)​​

表示y 方向的加权和(权重是像素值)。

公式:

​​(4) 重心计算公式​

​​x 坐标(cx)

相当于所有 x 坐标的加权平均值(权重是像素值)。

​​y 坐标(cy)

相当于所有 y 坐标的加权平均值(权重是像素值)。

​3. 直观理解​

​​M['m00']​​ 是总“质量”(面积)。

​​M['m10']​​ 是所有像素的 x 坐标 × 像素值的总和。

​​M['m01']​​ 是所有像素的 y 坐标 × 像素值的总和。

​​cx 和 cy​​ 就是整个区域的“平均位置”,即重心。

​​4. 代码示例​​

import cv2

import numpy as np

# 创建一个黑色背景,并在中间画一个白色矩形

img = np.zeros((400, 400), dtype=np.uint8)

cv2.rectangle(img, (150, 150), (250, 250), 255, -1)

# 计算矩

M = cv2.moments(img)

# 计算重心

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

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

# 在重心位置画一个红点

cv2.circle(img, (cx, cy), 5, 128, -1)

cv2.imshow('Centroid', img)

cv2.waitKey(0)

cv2.destroyAllWindows()

​​输出:

重心 (cx, cy) 会落在矩形的正中心 (200, 200)。

​​5. 特殊情况处理​​

如果 M['m00'] == 0(例如空轮廓或全黑图像),计算会出错,因此建议加一个判断:

if M['m00'] != 0:

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

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

else:

cx, cy = 0, 0  # 避免除以零

​​总结​

(2).轮廓面积

轮廓的面积可以使用函数 cv2.contourArea() 计算得到,也可以使用矩(0 阶矩),M['m00']

area = cv2.contourArea(cnt)

(3).轮廓周长

也被称为弧长。可以使用函数 cv2.arcLength() 计算得到。这个函数 的第二参数可以用来指定对象的形状是闭合的(True),还是打开的(一条曲 线)。

perimeter = cv2.arcLength(cnt,True)

(4).轮廓近似

将轮廓形状近似到另外一种由更少点组成的轮廓形状,新轮廓的点的数目 由我们设定的准确度来决定。使用的Douglas-Peucker算法,你可以到维基百 科获得更多此算法的细节。

为了帮助理解,假设我们要在一幅图像中查找一个矩形,但是由于图像的 种种原因,我们不能得到一个完美的矩形,而是一个“坏形状”(如下图所示)。 现在你就可以使用这个函数来近似这个形状()了。这个函数的第二个参数叫 epsilon,它是从原始轮廓到近似轮廓的最大距离。它是一个准确度参数。选 择一个好的 epsilon 对于得到满意结果非常重要。

epsilon = 0.1*cv2.arcLength(cnt,True)

approx = cv2.approxPolyDP(cnt,epsilon,True)

下边,第二幅图中的绿线是当 epsilon = 10% 时得到的近似轮廓,第三幅 图是当  epsilon = 1%  时得到的近似轮廓。第三个参数设定弧线是否闭合。

(5).凸包

凸包与轮廓近似相似,但不同,虽然有些情况下它们给出的结果是一样的。 函数 cv2.convexHull() 可以用来检测一个曲线是否具有凸性缺陷,并能纠正缺陷。一般来说,凸性曲线总是凸出来的,至少是平的。如果有地方凹进去了就被叫做凸性缺陷。例如下图中的手。红色曲线显示了手的凸包,凸性缺陷被双箭头标出来了。

关于他的语法还有一些需要交代:

hull = cv2.convexHull(points[, hull[, clockwise[, returnPoints]]

参数:

• points 我们要传入的轮廓

• hull 输出,通常不需要

• clockwise 方向标志。如果设置为 True,输出的凸包是顺时针方向的。 否则为逆时针方向。

• returnPoints 默认值为 True。它会返回凸包上点的坐标。如果设置 为False,就会返回与凸包点对应的轮廓上的点。

要获得上图的凸包,下面的命令就够了:hull = cv2.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]],这与前面我们得到结 果的第一个值是一样的。

(6).凸性检测

函数 cv2.isContourConvex() 可以可以用来检测一个曲线是不是凸 的。它只能返回 True 或 False。没什么大不了的。

k = cv2.isContourConvex(cnt)

(7).边界矩形

有两类边界矩形:

直边界矩形 一个直矩形(就是没有旋转的矩形)。它不会考虑对象是否旋转。 所以边界矩形的面积不是最小的。可以使用函数 cv2.boundingRect() 查 找得到。(x,y)为矩形左上角的坐标,(w,h)是矩形的宽和高。

x,y,w,h = cv2.boundingRect(cnt)

img = cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)

旋转的边界矩形 这个边界矩形是面积最小的,因为它考虑了对象的旋转。用到的函数为 cv2.minAreaRect()。返回的是一个 Box2D 结构,其中包含矩形左上角角点的坐标(x,y),矩形的宽和高(w,h),以及旋转角度。但是 要绘制这个矩形需要矩形的 4 个角点,可以通过函数 cv2.boxPoints() 获 得。

x,y,w,h = cv2.boundingRect(cnt)

img = cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)

把这两中边界矩形显示在下图中,其中绿色的为直矩形,红的为旋转矩形。

(8).最小外接圆

函数 cv2.minEnclosingCircle() 可以帮我们找到一个对象的外切圆。它是所有能够包括对象的圆中面积最小的一个。

(x,y),radius = cv2.minEnclosingCircle(cnt)

center = (int(x),int(y))

radius = int(radius)

img = cv2.circle(img,center,radius,(0,255,0),2)

(9).椭圆拟合

使用的函数为cv2.ellipse(),返回值其实就是旋转边界矩形的内切圆。

ellipse = cv2.fitEllipse(cnt)

im = cv2.ellipse(im,ellipse,(0,255,0),2)

(10).直线拟合

我们也可以为图像中的白色点拟合出一条直线。

import cv2

import numpy as np

# 创建一个黑色背景

img = np.zeros((400, 400, 3), dtype=np.uint8)

# 模拟一组轮廓点(这里用斜线示例)

points = np.array([[50, 50], [100, 100], [150, 150], [200, 200]], dtype=np.float32)

# --- 绘制原始点(红色)---

for point in points:

    x, y = point

    cv2.circle(img, (int(x), int(y)), 5, (0, 0, 255), -1)  # 红色点,半径为5

# 拟合直线

#cnt​​: 输入的点集(通常是轮廓点,格式为 np.array 或 list)。cv2.DIST_L2​​: 使用最小二乘法(欧氏距离)拟合直线。

#0​​: 距离参数(param),0 表示自动选择最优值。0.01, 0.01​​: 半径 (reps) 和角度 (aeps) 的精度阈值。

#返回值 [vx, vy, x, y]​​:(vx, vy): 单位向量,表示直线的方向。(x, y): 直线上的一个点

[vx, vy, x, y] = cv2.fitLine(points, cv2.DIST_L2, 0, 0.01, 0.01)

# 计算直线端点

rows, cols = img.shape[:2] #返回图像的高度 (rows) 和宽度 (cols),忽略通道数(如果是彩色图)

lefty = int((-x * vy / vx) + y)

righty = int(((cols - x) * vy / vx) + y)

# 绘制直线

#img: 输入图像。(cols-1, righty): 直线右端点(图像最右侧)。(0, lefty): 直线左端点(图像最左侧)。

#(0, 255, 0): 线条颜色(绿色)。2: 线条粗细(2 像素)。

cv2.line(img, (cols-1, righty), (0, lefty), (0, 255, 0), 2)

# 显示结果

cv2.imshow("Fitted Line", img)

cv2.waitKey(0)

cv2.destroyAllWindows()

结果:

21.3轮廓的属性

本小节我们将要学习提取一些经常使用的对象特征。你可以在Matlab regionprops documentation学习更多的图像特征。

(1).纵横比(Aspect Ratio)

边界矩形的宽高比:

x,y,w,h = cv2.boundingRect(cnt)

aspect_ratio = float(w)/h

(2). 范围(Extent)

范围是轮廓面积与边界矩形面积的比值:

area = cv2.contourArea(cnt)

x,y,w,h = cv2.boundingRect(cnt)

rect_area = w*h

extent = float(area)/rect_area

(3). 固体度(Solidity)

固体度是轮廓面积与凸包面积的比:

area = cv2.contourArea(cnt)

hull = cv2.convexHull(cnt)

hull_area = cv2.contourArea(hull)

solidity = float(area)/hull_area

(4).等效直径(Equivalent Diameter)

与轮廓面积相等的圆形的直径:

area = cv2.contourArea(cnt)

equi_diameter = np.sqrt(4*area/np.pi)

(5).方向(Orientation)

方向是物体被指向的角度。下面的方法还会返回长轴和短轴的长度:

(x,y),(MA,ma),angle = cv2.fitEllipse(cnt)

(6).掩模和像素点

有时我们需要构成对象的所有像素点,我们可以这样做:

import cv2

import numpy as np

# 读取图像并二值化

img = cv2.imread("apple.png")

imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

_, thresh = cv2.threshold(imgray, 127, 255, 0)

# 查找轮廓

contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

cnt = contours[0]  # 取第一个轮廓

# 创建掩膜并绘制填充轮廓

#创建一个全黑的单通道图像(大小和 imgray 相同),初始值为 0(黑色)

mask = np.zeros(imgray.shape, np.uint8)

#[cnt]: 要绘制的轮廓(需放在列表中,因为函数支持多轮廓)。0: 轮廓的索引(这里只有一个轮廓 cnt)。

#255: 填充颜色(白色)。-1: 线宽参数,-1 表示填充轮廓内部(如果是正数,则只绘制轮廓线)。

#效果​​:掩膜 mask 中,轮廓内部区域变为白色(255),外部保持黑色(0)。

cv2.drawContours(mask, [cnt], 0, 255, -1)

# 提取轮廓内像素坐标

#np.nonzero(mask)​​返回掩膜中非零像素的坐标,格式为两个数组(行索引和列索引)

#np.transpose()​​将坐标转换为 (x, y) 形式的二维数组

pixelpoints = np.transpose(np.nonzero(mask))  # 方法1

#直接返回 OpenCV 格式的非零点坐标(Nx1x2 数组),等价于 np.transpose(np.nonzero()) 的结果

# pixelpoints = cv2.findNonZero(mask)         # 方法2

print("轮廓内像素坐标(前5个):\n", pixelpoints[:5])

这里用了两种方法:第一种方法使用 Numpy  函数,第二种使用  OpenCV 函数。结果相同,但还是有点不同。Numpy 给出的坐标是(row, colum)形式的。而 OpenCV 给出的格式是(x,y)形式的。所以这两个结果基本是可以互换的。row=x,colunm=y。

(7).最大值和最小值及它们的位置

我们可以使用掩模图像得到这些参数:

min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(imgray,mask = mask)

imgray: 单通道图像(如灰度图)。

mask: 可选参数,指定只计算掩膜非零区域的像素。

关键注意事项​

​​(1) 像素值的范围​​

如果是 8位灰度图(dtype=np.uint8),像素值范围是 [0, 255]。

如果是 浮点型图像(如归一化后的图像),值可能是 [0.0, 1.0]。

​​(2) 掩膜的作用​​

​​mask=mask 表示只计算掩膜中白色区域(255)的像素,忽略黑色区域(0)。

如果没有掩膜,则计算整个图像的极值。

​​(3) 多通道图像​​

如果输入是多通道图像(如BGR彩色图),需先转换为单通道(如灰度图),否则会报错。

常见问题​

​​Q1: 如果掩膜全黑(mask全为0)会怎样?​​

函数会返回 min_val 和 max_val 均为 0,且 min_loc 和 max_loc 为 (0, 0)(但实际无意义)。

​​Q2: 如何找到图像中的最亮点?​​

直接使用 max_val 和 max_loc,例如:

cv2.circle(imgray, max_loc, 5, (255, 0, 0), -1)  # 在最大值位置画红点

​​Q3: 为什么有时返回值是浮点数?​

如果图像是 float 类型(如通过 cv2.normalize() 处理过),极值可能是浮点数。

掩膜用于限定计算区域,仅分析非零部分。

(8).平均颜色及平均灰度

我们也可以使用相同的掩模求一个对象的平均颜色或平均灰度

mean_val = cv2.mean(im,mask = mask)

(9).极点

如下图所示:

一个对象最上面,最下面,最左边,最右边的点。

leftmost = tuple(cnt[cnt[:,:,0].argmin()][0])

rightmost = tuple(cnt[cnt[:,:,0].argmax()][0])

topmost = tuple(cnt[cnt[:,:,1].argmin()][0])

bottommost = tuple(cnt[cnt[:,:,1].argmax()][0])

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值