1.目标
- 了解轮廓是什么。
- 学习寻找轮廓,绘制轮廓等
- 您将看到以下功能:**cv.findContours()** ,**cv.drawContours()*
2.什么是轮廓
- 轮廓可以简单地解释为连接具有相同颜色或强度的所有连续点(沿边界)的曲线。轮廓是用于形状分析以及对象检测和识别的有用工具。
- 为了获得更高的准确性,请使用二进制图像。因此,在找到轮廓之前,请应用阈值或 Canny 边缘检测。
- 从OpenCV 3.2开始,**findContours()** 不再修改源图像。
- 在OpenCV中,找到轮廓就像从黑色背景中找到白色物体。因此请记住,要找到的对象应该是白色,背景应该是黑色。
让我们看看如何找到二进制图像的轮廓:
import numpy as np
import cv2 as cv
im = cv.imread('test.jpg')
imgray = cv.cvtColor(im, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(imgray, 127, 255, 0)
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
看到,在 cv.findContours() 函数中有三个参数,第一个是源图像,第二个是轮廓检索模式,第三个是轮廓逼近方法。并输出轮廓和层次。轮廓是图像中所有轮廓的Python列表。每个单独的轮廓都是对象边界点的(x,y)坐标的Numpy数组。
3.绘制图像轮廓
要绘制轮廓,请使用 cv.drawContours() 函数。只要有边界点,它也可以用来绘制任何形状。它的第一个参数是源图像,第二个参数是应该作为Python列表传递的轮廓,第三个参数是轮廓的索引(在绘制单个轮廓时很有用。要绘制所有轮廓,请传递-1),其余参数是颜色,厚度等等。
要在图像中绘制所有轮廓:
import numpy as np
import cv2 as cv
img = cv.imread(r'D:\study\EmotionDetection_RealTime-master\data\data\te\01.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)
cv.drawContours(img, contours, -1, (0,255,0), 3)
# 显示图像
cv.imshow('Contours', img)
cv.waitKey(0)
cv.destroyAllWindows()
-
要绘制单个轮廓,请说第四个轮廓:
-
cv.drawContours(img, contours, 3, (0,255,0), 3)
- 但是在大多数情况下,以下方法会很有用:
cnt = contours[4] cv.drawContours(img, [cnt], 0, (0,255,0), 3)
ret,thresh = cv.threshold(img,127,255,0)在OpenCV中,
cv.threshold
函数用于将图像转换为二值图像。这个函数的基本作用是将图像中的每个像素点与设定的阈值进行比较,并根据比较结果将像素点设置为0或255(黑色或白色)。函数的返回值包括两个部分:ret
和thresh
。
ret
是返回的阈值。这个值在阈值自动选择方法(如cv.THRESH_OTSU
或cv.THRESH_TRIANGLE
)时非常有用,它表示算法找到的最佳阈值。thresh
是应用了阈值处理后的图像。在这个图像中,像素值要么是0(黑色),要么是255(白色),这取决于它们与阈值的比较结果。img
:要应用阈值的灰度图像。127
:阈值值,像素值小于127的将设置为0,大于等于127的将设置为255。255
:当像素值大于等于阈值时,设置的像素值。cv.THRESH_BINARY
:阈值类型,这里表示二值阈值化。
在左下角,有一个绿色的点。
4. 轮廓近似法
这是 cv.findContours() 函数中的第三个参数。它实际上表示什么?
上面我们告诉我们轮廓是强度相同的形状的边界。它存储形状边界的(x,y)坐标。但是它存储所有坐标吗?这是通过这种轮廓近似方法指定的。
如果传递 cv.CHAIN_APPROX_NONE ,则会存储所有边界点。但是实际上我们需要所有这些要点吗?例如,您找到了一条直线的轮廓。您是否需要线上的所有点来代表该线?不,我们只需要该线的两个端点即可。这就是 cv.CHAIN_APPROX_SIMPLE 所做的。它删除所有冗余点并压缩轮廓,从而节省内存。
下面的矩形图像演示了此技术。只需在轮廓数组中的所有坐标上绘制一个圆(以蓝色绘制)。第一个图像显示点I与得到 cv.CHAIN_APPROX_NONE (734点)和第二图像显示了一个与 cv.CHAIN_APPROX_SIMPLE (仅4个点)。看,它可以节省多少内存!!!
5. 轮廓特征
学习找到轮廓的不同特征,如区域,周长,边界矩形等。
5.1 矩
图像矩(Image Moments)是图像处理中用来描述图像中形状特征的一组参数。它们提供了图像灰度分布的详细信息,可以用来计算图像中的某些几何特征,如质心、面积、主轴和形状等。图像矩是基于图像像素强度的加权平均,通常用于模式识别和图像分析中。
最常用的图像矩有以下几种:
1. **零阶矩(m00)**:这是图像的质心或面积。它表示图像中所有像素值的总和。
2. **一阶矩(m10, m01)**:这些矩与图像质心的位置有关。质心的x坐标可以通过m10/m00计算得出,y坐标可以通过m01/m00计算得出。
3. **二阶矩和更高阶矩**(如m20, m02, m11, m30, m12, m21等):这些矩用于描述图像的形状和分布,可以用来计算图像的倾斜度、椭圆的扁率、图像的不对称性等。
图像矩的计算通常涉及以下步骤:
1. **图像预处理**:将图像转换为灰度图像,并可能应用阈值处理来创建二值图像。
2. **计算矩**:使用适当的公式计算图像的各个矩。
3. **特征提取**:根据计算出的矩提取有用的形状特征。
在OpenCV中,`cv.moments`函数可以用来计算图像中轮廓的矩。这些矩可以帮助我们分析图像中的对象,例如计算对象的面积、质心位置、形状等。通过这些矩,我们可以得到对象的一些基本特征,从而进行进一步的分析和处理。
图像矩可帮助您计算某些特征,例如物体的重心,物体的面积等。请查看“图像矩”上的Wikipedia页面
函数 cv.moments() 提供了所有计算出的矩值的列表。见下文:
import numpy as np
import cv2 as cv
# 加载图像,转换为灰度图像
img = cv.imread('star.jpg', 0)
# 应用阈值
ret, thresh = cv.threshold(img, 127, 255, 0)
# 查找轮廓
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
# 假设我们关注第一个轮廓
cnt = contours[0]
# 计算轮廓的矩
M = cv.moments(cnt)
print(M)
# 计算质心
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
print(f'Centroid: ({cx}, {cy})')
# 显示图像和轮廓
cv.drawContours(img, [cnt], -1, (0, 255, 0), 3)
cv.imshow('Contours', img)
cv.waitKey(0)
cv.destroyAllWindows()
在OpenCV中,
cv.moments
函数用于计算图像中轮廓的几何矩。这些矩可以帮助我们理解轮廓的基本形状和特征,例如面积、质心位置等。cv.moments
函数返回一个包含所有计算出的矩的结构。在您提供的代码中,您首先导入了
numpy
和cv2
,然后加载了一个名为star.jpg
的图像,并将其转换为灰度图像。接着,您应用了一个阈值来创建一个二值图像,并找到了图像中的轮廓。然后,您选择了第一个轮廓(假设它是列表中的主要轮廓),并计算了这个轮廓的矩。在打印出的
M
中,您会看到诸如m00
、m10
、m01
等矩。其中:
m00
是轮廓的面积。m10
和m01
分别是在x轴和y轴方向上的质心坐标的分子部分。要得到质心的实际坐标,您需要将这些值除以m00
(轮廓的面积)。请注意,如果您使用的是OpenCV 4或更高版本,
cv.findContours
函数的第二个参数可能需要修改为cv.RETR_TREE
,因为从OpenCV 4开始,cv.RETR_TREE
的值已经从1更改为2。
5.2 轮廓面积
轮廓区域由函数 cv.contourArea() 或从力矩 $M_{00}$中给出。
图像轮廓的面积可以通过两种方式在OpenCV中计算:
1. **使用 `cv.contourArea()` 函数**:这个函数直接计算并返回轮廓的面积。其用法非常简单,只需要将轮廓作为参数传递给函数即可。```python import cv2 as cv # 假设 cnt 是一个轮廓 area = cv.contourArea(cnt) print(f'Area: {area}') ```
2. **使用几何矩 `M["m00"]`**:当使用 `cv.moments()` 函数计算轮廓的矩时,返回的结构中包含了一个名为 `"m00"` 的元素,它表示轮廓的面积。
```python import cv2 as cv # 假设 cnt 是一个轮廓 M = cv.moments(cnt) area = M["m00"] print(f'Area: {area}') ```
两种方法都可以得到轮廓的面积,但是 `cv.contourArea()` 通常更直接和易用。而使用矩来计算面积则在需要其他矩相关的特征时更为方便,因为只需要计算一次矩就可以获取多个特征值。
5.3 轮廓周长
是的,您说得正确。在OpenCV中,可以使用`cv.arcLength`函数来计算轮廓的周长,也称为弧长。这个函数的第二个参数用于指定轮廓是否是闭合的。如果轮廓是闭合的,则应使用`closed=True`(或者简单地使用`True`),如果轮廓是开放的,则使用`closed=False`(或者`False`)。
函数的基本用法如下:```python import cv2 as cv # 假设 cnt 是一个轮廓 perimeter = cv.arcLength(cnt, closed=True) print(f'Perimeter: {perimeter}') ```
在这个例子中,`cnt`是我们要计算周长的轮廓,`closed=True`表示这个轮廓是闭合的。如果轮廓是闭合的,通常情况下您可以使用`True`,因为`cv.findContours`函数返回的轮廓默认是闭合的。
如果轮廓是开放的,例如某些特定的线段或曲线,您需要将`closed`参数设置为`False`:```python perimeter = cv.arcLength(cnt, closed=False) ```
这样,`cv.arcLength`函数会计算从轮廓的第一个点到最后一个点的距离,而不会考虑闭合的路径。
5.4 轮廓近似
轮廓近似(Contour Approximation)是图像处理中的一种技术,用于简化轮廓的表示,减少轮廓点的数量,同时保持轮廓的基本形状。在OpenCV中,可以使用`cv.approxPolyDP`函数来实现轮廓近似。
`cv.approxPolyDP`函数使用Douglas-Peucker算法来近似轮廓。这个算法通过设置一个最大距离阈值,然后去除轮廓上那些对轮廓形状影响不大的点,从而减少轮廓点的数量。函数的基本用法如下:```python import cv2 as cv # 假设 cnt 是一个轮廓 epsilon = 0.01 * cv.arcLength(cnt, True) # 设置近似精度 approx = cv.approxPolyDP(cnt, epsilon, True) # approx 现在是一个近似的轮廓,点数更少 ```
在这个例子中,`cnt`是我们要近似的轮廓,`epsilon`是近似精度,它是一个距离阈值,表示轮廓上任意点到近似曲线的最大距离。这个值通常是以轮廓周长的百分比来设置的。`True`表示闭合的轮廓。
`approx`是返回的近似轮廓,它是一个包含较少点的简化轮廓。这个轮廓可以用来减少计算量,提高处理速度,同时仍然保持轮廓的基本形状,适用于后续的形状识别或分析。
轮廓近似在处理复杂形状或大量轮廓时非常有用,因为它可以减少数据的复杂性,同时仍然提供足够的信息来进行图像分析。
为了理解这一点,假设您试图在图像中找到一个正方形,但是由于图像中的某些问题,您没有得到一个完美的正方形,而是一个“坏形状”(如下图所示)。现在,您可以使用此功能来近似形状。在这种情况下,第二个参数称为epsilon,它是从轮廓到近似轮廓的最大距离。它是一个精度参数。需要正确选择 epsilon 才能获得正确的输出。下面,在第二张图片中,绿线显示了 精度 epsilon = 10% 时的近似曲线。第三幅图显示了精度 epsilon = 1% 时的情况。第三个参数指定曲线是否闭合。
5.4 凸包
凸包外观看起来与轮廓逼近相似,但并非如此(在某些情况下两者可能提供相同的结果)。在这里,**cv.convexHull()** 函数检查曲线是否存在凸凹缺陷并对其进行校正。一般而言,凸曲线是始终凸出或至少平坦的曲线。如果在内部凸出,则称为凸度缺陷。例如,检查下面的手的图像。红线显示手的凸包。双向箭头标记显示凸度缺陷,这是船体与轮廓线之间的局部最大偏差。、
在OpenCV中,`cv.convexHull`函数用于找到一个点集的凸包,即最小的凸多边形,这个多边形能够包含所有输入的点。凸包在图像处理中非常有用,特别是在形状分析和对象识别任务中。
函数的基本用法如下:```python import cv2 as cv # 假设 points 是一个包含点的 NumPy 数组,例如轮廓点 hull = cv.convexHull(points) # hull 现在是一个包含凸包点的 NumPy 数组 ```
函数的参数如下:
- `points`:一个二维点的数组,通常是一个`np.array`类型,表示输入的点集。
- `hull`(可选):一个输出数组,用于存储凸包的输出点索引。如果不需要,可以省略。
- `clockwise`(可选):一个布尔标志,如果为`True`,则输出的凸包点将按顺时针方向排列。否则,它们将按逆时针方向排列。默认值为`False`。
- `returnPoints`(可选):一个布尔标志,如果为`True`,则函数返回凸包的顶点。如果为`False`,则函数返回与凸包点对应的轮廓点的索引。默认值为`True`。
例如,如果您有一个轮廓`cnt`,并且想要找到它的凸包,您可以这样做:```python # 假设 cnt 是一个轮廓 hull = cv.convexHull(cnt, returnPoints=True) # 绘制凸包 cv.polylines(img, [hull], True, (255, 0, 0), 2) # 显示图像 cv.imshow('Convex Hull', img) cv.waitKey(0) cv.destroyAllWindows() ```
在这个例子中,`cv.polylines`函数用于在图像上绘制凸包。`returnPoints=True`确保`hull`包含凸包的顶点坐标,而不是索引。凸包被绘制为蓝色的线条。
但是,如果要查找凸度缺陷,则需要传递 returnPoints = False。为了理解它,我们将拍摄上面的矩形图像。首先,我发现它的轮廓为cnt。现在,我发现它的带有returnPoints = True的凸包,得到以下值:[[[234 202]],[[51 202]],[[51 79]],[[234 79]]],它们是四个角矩形的点。现在,如果对returnPoints = False执行相同的操作,则会得到以下结果:[[129],[67],[0],[142]]。这些是轮廓中相应点的索引。例如,检查第一个值:cnt [129] = [[234,202]]与第一个结果相同(对于其他结果依此类推)。
5.6 检查凸度
cv.isContourConvex() 是一个函数用来检查曲线是否为凸多边形。它只是返回True还是False。
您是对的,`cv.isContourConvex()` 是 OpenCV 中的一个函数,用于检查给定的轮廓是否为凸多边形。这个函数非常直接和简单,它接受一个轮廓作为输入,并返回一个布尔值,如果轮廓是凸多边形,则返回 `True`,否则返回 `False`。
函数的基本用法如下:```python import cv2 as cv # 假设 cnt 是一个轮廓 is_convex = cv.isContourConvex(cnt) # 检查并打印结果 if is_convex: print("The contour is convex.") else: print("The contour is not convex.") ```
在这个例子中,`cnt` 是我们要检查的轮廓。`is_convex` 变量将包含 `cv.isContourConvex()` 函数的返回值。然后,您可以使用这个值来执行相应的操作,例如在轮廓是凸多边形时进行特定的处理。
凸多边形的定义是,对于多边形内的任意两点,连接这两点的线段完全位于多边形内部。如果一个多边形不是凸多边形,那么至少存在一对点,使得连接这两点的线段部分位于多边形外部。
5.7 边界矩形
有两种类型的边界矩形。
5.7.1 直角矩形
它是一个直角矩形,不考虑对象的旋转。因此,边界矩形的面积将不会最小。它可以通过函数 cv.boundingRect() 找到。
在OpenCV中,`cv.boundingRect`函数用于找到一个轮廓的最小外包矩形。这个矩形是水平放置的,并且紧紧包围着轮廓内的所有点。函数返回一个包含矩形顶点的`x`、`y`坐标、宽度`w`和高度`h`的元组。
函数的基本用法如下:
```python
import cv2 as cv
# 假设 cnt 是一个轮廓
x, y, w, h = cv.boundingRect(cnt)
# 使用返回的值来绘制矩形
cv.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
# 显示图像
cv.imshow('Bounding Rectangle', img)
cv.waitKey(0)
cv.destroyAllWindows()
```
在这个例子中,`cnt`是我们要找到外包矩形的轮廓。`cv.boundingRect`函数返回的`x`和`y`是矩形的左上角坐标,`w`和`h`分别是矩形的宽度和高度。然后,使用`cv.rectangle`函数在图像上绘制这个矩形。
这个外包矩形对于许多图像处理任务来说非常有用,比如对象检测、跟踪和图像分割。它提供了一个简单的方式来确定轮廓在图像中的位置和大小。
5.7.2 旋转矩形
在这里,边界矩形是用最小面积绘制的,因此它也考虑了旋转。使用的函数是 cv.minAreaRect() 。它返回一个Box2D结构,其中包含以下细节-(中心(x,y),(宽度,高度),旋转角度)。但是要绘制此矩形,我们需要矩形的4个角。它是通过函数 cv.boxPoints() 获得的
`cv.minAreaRect` 函数是 OpenCV 中的一个函数,用于找到一个轮廓的最小外接矩形。这个矩形不一定是水平的或垂直的,它会根据轮廓的角度旋转,以最小化矩形的面积。函数返回一个包含矩形中心的坐标、尺寸、旋转角度的`RotatedRect`对象。
下面是如何使用 `cv.minAreaRect` 和 `cv.boxPoints` 函数来找到轮廓的最小外接矩形并在图像上绘制它的例子:```python import cv2 as cv # 加载图像 img = cv.imread('path_to_your_image.jpg') # 转换为灰度图像 gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 应用阈值处理,得到二值图像 _, thresh = cv.threshold(gray, 127, 255, cv.THRESH_BINARY) # 查找轮廓 contours, _ = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) # 假设我们关注第一个轮廓 cnt = contours[0] # 计算最小外接矩形 rect = cv.minAreaRect(cnt) # 获取矩形的四个顶点 box = cv.boxPoints(rect) # 将顶点转换为整数 box = np.int0(box) # 在原图上绘制最小外接矩形 cv.drawContours(img, [box], 0, (0, 255, 0), 2) # 显示图像 cv.imshow('Min Area Rectangle', img) # 等待按键,然后关闭窗口 cv.waitKey(0) cv.destroyAllWindows() ```
在这个例子中,您需要替换 `'path_to_your_image.jpg'` 为您的图像文件的路径。代码首先加载图像,将其转换为灰度图像,然后应用阈值处理以获得二值图像。接着,使用 `cv.findContours` 函数找到图像中的轮廓,并选择第一个轮廓(假设它是主要的轮廓)。
`cv.minAreaRect` 函数计算这个轮廓的最小外接矩形,`cv.boxPoints` 函数获取矩形的四个顶点。然后,使用 `cv.drawContours` 函数在原图上绘制这个矩形。最后,使用 `cv.imshow` 显示图像,`cv.waitKey(0)` 等待按键,`cv.destroyAllWindows()` 关闭所有窗口。
5.8 最小外圆
`cv.minEnclosingCircle` 是 OpenCV 中的一个函数,用于找到一个轮廓的最小外接圆。这个圆是能够完全包围轮廓中所有点的最小圆。函数返回一个包含圆心和半径的元组。
下面是如何使用 `cv.minEnclosingCircle` 函数来找到轮廓的最小外接圆,并在图像上绘制这个圆的例子:```python import cv2 as cv import numpy as np # 加载图像 img = cv.imread('path_to_your_image.jpg') # 转换为灰度图像 gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 应用阈值处理,得到二值图像 _, thresh = cv.threshold(gray, 127, 255, cv.THRESH_BINARY) # 查找轮廓 contours, _ = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) # 假设我们关注第一个轮廓 cnt = contours[0] # 计算最小外接圆 center, radius = cv.minEnclosingCircle(cnt) # 将圆心和半径转换为整数 center = np.int0(center) radius = int(radius) # 在原图上绘制最小外接圆 cv.circle(img, center, radius, (0, 255, 0), 2) # 显示图像 cv.imshow('Min Enclosing Circle', img) # 等待按键,然后关闭窗口 cv.waitKey(0) cv.destroyAllWindows() ```
在这个例子中,您需要替换 `'path_to_your_image.jpg'` 为您的图像文件的路径。代码首先加载图像,将其转换为灰度图像,然后应用阈值处理以获得二值图像。接着,使用 `cv.findContours` 函数找到图像中的轮廓,并选择第一个轮廓(假设它是主要的轮廓)。
`cv.minEnclosingCircle` 函数计算这个轮廓的最小外接圆,返回圆心和半径。然后,使用 `cv.circle` 函数在原图上绘制这个圆。最后,使用 `cv.imshow` 显示图像,`cv.waitKey(0)` 等待按键,`cv.destroyAllWindows()` 关闭所有窗口。
请确保您的图像文件位于正确的路径,并且您已经安装了 OpenCV。如果您使用的是 Python 的虚拟环境,请确保在虚拟环境中安装了 OpenCV。
5.9 拟合椭圆
在OpenCV中,`cv.fitEllipse`函数用于找到一个轮廓的最佳拟合椭圆。这个椭圆能够最好地适应轮廓的形状。函数返回一个包含椭圆中心的坐标、轴的长度、旋转角度的`RotatedRect`对象。
`cv.ellipse`函数用于在图像上绘制椭圆。下面是如何使用这两个函数来找到轮廓的最佳拟合椭圆,并在图像上绘制这个椭圆的例子:```python import cv2 as cv # 加载图像 img = cv.imread('path_to_your_image.jpg') # 转换为灰度图像 gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 应用阈值处理,得到二值图像 _, thresh = cv.threshold(gray, 127, 255, cv.THRESH_BINARY) # 查找轮廓 contours, _ = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) # 假设我们关注第一个轮廓 cnt = contours[0] # 计算最佳拟合椭圆 ellipse = cv.fitEllipse(cnt) # 在原图上绘制最佳拟合椭圆 cv.ellipse(img, ellipse, (0, 255, 0), 2) # 显示图像 cv.imshow('Fitted Ellipse', img) # 等待按键,然后关闭窗口 cv.waitKey(0) cv.destroyAllWindows() ```
在这个例子中,您需要替换 `'path_to_your_image.jpg'` 为您的图像文件的路径。代码首先加载图像,将其转换为灰度图像,然后应用阈值处理以获得二值图像。接着,使用 `cv.findContours` 函数找到图像中的轮廓,并选择第一个轮廓(假设它是主要的轮廓)。
`cv.fitEllipse` 函数计算这个轮廓的最佳拟合椭圆,`cv.ellipse` 函数在原图上绘制这个椭圆。最后,使用 `cv.imshow` 显示图像,`cv.waitKey(0)` 等待按键,`cv.destroyAllWindows()` 关闭所有窗口。
请确保您的图像文件位于正确的路径,并且您已经安装了 OpenCV。如果您使用的是 Python 的虚拟环境,请确保在虚拟环境中安装了 OpenCV。
5.10 拟合线
在OpenCV中,`cv.fitLine`函数用于拟合一条直线到一组点。这个函数可以根据不同的距离度量方法来计算最佳拟合直线的参数。拟合直线的参数包括直线的方向向量`(vx, vy)`和直线上的一点`(x, y)`。
下面是如何使用`cv.fitLine`函数来拟合轮廓上的直线,并在图像上绘制这条直线的例子:```python import cv2 as cv # 加载图像 img = cv.imread('path_to_your_image.jpg') # 转换为灰度图像 gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 应用阈值处理,得到二值图像 _, thresh = cv.threshold(gray, 127, 255, cv.THRESH_BINARY) # 查找轮廓 contours, _ = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) # 假设我们关注第一个轮廓 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('Fitted Line', img) # 等待按键,然后关闭窗口 cv.waitKey(0) cv.destroyAllWindows() ```
在这个例子中,您需要替换 `'path_to_your_image.jpg'` 为您的图像文件的路径。代码首先加载图像,将其转换为灰度图像,然后应用阈值处理以获得二值图像。接着,使用 `cv.findContours` 函数找到图像中的轮廓,并选择第一个轮廓(假设它是主要的轮廓)。
`cv.fitLine` 函数计算这个轮廓的最佳拟合直线的参数。然后,根据直线的参数计算直线在图像左右边界上的两个点,并使用 `cv.line` 函数在原图上绘制这条直线。最后,使用 `cv.imshow` 显示图像,`cv.waitKey(0)` 等待按键,`cv.destroyAllWindows()` 关闭所有窗口。
请确保您的图像文件位于正确的路径,并且您已经安装了 OpenCV。如果您使用的是 Python 的虚拟环境,请确保在虚拟环境中安装了 OpenCV。
6. 轮廓属性
学习找到轮廓的不同属性,如 Solidity,Mean Intensity 等。
在这里,我们将学习提取对象的一些常用属性,例如实体,等效直径,蒙版图像,平均强度等。更多功能可以在 Matlab regionprops文档中找到。
(注意:质心,面积,周长等也属于此类,但我们在上一章中已经看到了)
6.1 长宽比
它是对象边界矩形的宽度与高度的比率。 $$ Aspect ; Ratio = \frac{Width}{Height} $$
#cnt为图像的一个轮廓
x,y,w,h = cv.boundingRect(cnt)
aspect_ratio = float(w)/h
6.2 范围
范围是轮廓区域与边界矩形区域的比率。 $$ Extent = \frac{Object ; Area}{Bounding ; Rectangle ; Area} $$
area = cv.contourArea(cnt)
x,y,w,h = cv.boundingRect(cnt)
rect_area = w*h
extent = float(area)/rect_area
6.3 固实性
固实性是轮廓面积与其凸包面积的比率。
$$ Solidity = \frac{Contour ; Area}{Convex ; Hull ; Area} $$
area = cv.contourArea(cnt)
hull = cv.convexHull(cnt)
hull_area = cv.contourArea(hull)
solidity = float(area)/hull_area
cnt
是我们要找到凸包的轮廓。cv.convexHull
函数返回的hull
是一个包含凸包点的np.array
类型。
6.4 等效直径
等效直径是面积与轮廓面积相同的圆的直径。 $$ Equivalent ; Diameter = \sqrt{\frac{4 \times Contour ; Area}{\pi}} $$
area = cv.contourArea(cnt)
equi_diameter = np.sqrt(4*area/np.pi)
6.5 方向
方向是物体指向的角度。以下方法还给出了主轴和副轴的长度。
(x,y),(MA,ma),angle = cv.fitEllipse(cnt)
在这个例子中,
cnt
是我们要找到最佳拟合椭圆的轮廓。cv.fitEllipse
函数返回一个包含椭圆中心坐标(x, y)
、长轴和短轴长度(MA, ma)
以及旋转角度angle
的元组。椭圆中心坐标
(x, y)
表示椭圆的中心在图像中的位置。长轴和短轴长度(MA, ma)
分别表示椭圆在长轴和短轴上的长度。旋转角度angle
表示椭圆相对于水平轴的角度,以弧度为单位。
6.6 遮罩和像素点
在某些情况下,我们可能需要构成该对象的所有点。可以按照以下步骤完成:
mask = np.zeros(imgray.shape,np.uint8)
cv.drawContours(mask,[cnt],0,255,-1)
pixelpoints = np.transpose(np.nonzero(mask))
# pixelpoints = cv.findNonZero(mask)
cv.drawContours
函数的第二个参数[cnt]
是一个包含轮廓的列表,0
表示要绘制的第一个轮廓,255
是填充轮廓的颜色(白色),-1
表示填充轮廓内部。
np.transpose
函数用于将矩阵mask
的行和列互换,得到一个新的矩阵,其维度与mask
相同,但元素顺序不同。
np.nonzero
函数用于找到mask
中非零元素的索引,即轮廓的像素点。最后,
cv.findNonZero
函数也是一个用于找到图像中非零像素点的函数。它返回一个包含每个非零像素点的数组,其中每个像素点都是一个包含x和y坐标的元组。
pixelpoints
现在包含轮廓的所有像素点的坐标。这些坐标可以用于进一步的分析或处理,例如计算轮廓的面积或周长。
在这里,给出了两种方法,一种使用Numpy函数,另一种使用OpenCV函数(最后注释的行)执行相同的操作。结果也相同,但略有不同。Numpy以(行,列)格式给出坐标,而OpenCV以(x,y)格式给出坐标。因此,基本上答案是可以互换的。注意,row = x,column = y。
6.7 最大值,最小值及其位置
我们可以使用遮罩图像找到这些参数。
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(imgray,mask = mask)
6.8 平均颜色或平均强度
在这里,我们可以找到对象的平均颜色。或者可以是灰度模式下物体的平均强度。我们再次使用相同的蒙版进行此操作。
mean_val = cv.mean(im,mask = mask)
在这个例子中,
imgray
是您要分析的灰度图像,mask
是一个掩码矩阵,用于限制分析的范围。如果mask
是None
,则函数将分析整个图像。
cv.minMaxLoc
函数返回四个值:
min_val
:图像中的最小值。max_val
:图像中的最大值。min_loc
:最小值的位置,即(x, y)
坐标。max_loc
:最大值的位置,即(x, y)
坐标。这些值可以帮助您确定图像中特定的特征,例如找到图像中亮度最高或最低的区域。
6.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])
7 更多功能
学习找到凸性缺陷,pointPolygonTest,匹配不同的形状等。
- 凸性缺陷以及如何找到它们。
- 查找点到多边形的最短距离
- 匹配不同的形状
7.1 凸包缺陷
在计算机视觉中,凸缺陷(Convex Defects)是指在凸包(Convex Hull)中存在的缺陷。凸包是一个多边形,它包含所有轮廓点,并且任意两点之间的连线完全位于多边形内部。凸缺陷是凸包与原始轮廓之间的间隙,这些间隙在凸包内部,但在原始轮廓外部。
在OpenCV中,`cv.convexityDefects`函数用于计算凸包与原始轮廓之间的凸缺陷。这个函数返回一个数组,其中每个缺陷包含四个元素:
1. 起始点在原始轮廓中的索引。
2. 结束点在原始轮廓中的索引。
3. 远点在凸包中的索引。
起始点:这是凸缺陷在原始轮廓中的起点,它位于凸包内部。
结束点:这是凸缺陷在原始轮廓中的终点,它位于凸包外部。
远点:这是凸缺陷在凸包上的一个点,它与起始点和结束点形成一个三角形的顶点。远点位于凸包的外部,并且到起始点和结束点的距离之和是最小的。
4. 缺陷的深度,这是一个浮点数,表示缺陷的深度。
凸缺陷的深度可以用来确定缺陷的大小和方向。深度较大的缺陷通常表示较大的间隙。
在您提供的代码中,您正在遍历凸缺陷数组,并绘制每个凸缺陷的起始点、结束点和远点。起始点和结束点使用绿色线条连接,而远点则用红色圆圈标记。
请注意,凸缺陷的计算需要凸包的存在。在计算凸包之前,您需要使用`cv.convexHull`函数来计算凸包,该函数返回凸包的点,而不是索引。然后,您需要将这些点与原始轮廓点关联起来,以便在绘制时能够正确地引用它们。
如果您想要在图像上绘制凸缺陷,您需要确保您的代码正确地计算了凸包,并且将凸包的点与原始轮廓点关联起来。然后,您可以遍历凸缺陷数组,并使用`cv.line`和`cv.circle`函数在图像上绘制凸缺陷的起始点、结束点和远点。
在第二章中,我们看到了关于轮廓的凸包。物体与该船体的任何偏离都可以视为凸包缺陷。
OpenCV带有一个现成的函数 cv.convexityDefect() 来查找该函数。基本的函数调用如下所示:
hull = cv.convexHull(cnt,returnPoints = False)
defects = cv.convexityDefects(cnt,hull)
注意 请记住,在寻找凸包时,我们必须传递returnPoints = False,以便寻找凸缺陷。
它返回一个数组,其中每行包含这些值- [起点,终点,最远点,到最远点的近似距离]。我们可以使用图像对其进行可视化。我们画一条连接起点和终点的线,然后在最远的点画一个圆。请记住,返回的前三个值是cnt的索引。因此,我们必须从cnt带来这些值。
import cv2 as cv
import numpy as np
img = cv.imread('star.jpg')
img_gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ret,thresh = cv.threshold(img_gray, 127, 255,0)
contours,hierarchy = cv.findContours(thresh,2,1)
cnt = contours[0]
hull = cv.convexHull(cnt,returnPoints = False)
defects = cv.convexityDefects(cnt,hull)
for i in range(defects.shape[0]):
s,e,f,d = defects[i,0]
start = tuple(cnt[s][0])
end = tuple(cnt[e][0])
far = tuple(cnt[f][0])
cv.line(img,start,end,[0,255,0],2)
cv.circle(img,far,5,[0,0,255],-1)
cv.imshow('img',img)
cv.waitKey(0)
cv.destroyAllWindows()
并查看结果:
7.2 点多边形测试
此功能查找图像中的点与轮廓之间的最短距离。它返回的距离为:当点在轮廓外时为负;当点在轮廓内时为正;如果点在轮廓上,则返回零。
例如,我们可以如下检查点(50,50):
dist = cv.pointPolygonTest(cnt,(50,50),True)
在函数中,第三个参数是measureDist。如果为True,则找到带符号的距离。如果为False,它将查找该点是在轮廓内部还是外部或轮廓上(它分别返回+ 1,-1、0)。
注意 如果您不想查找距离,请确保第三个参数为False,因为这是一个耗时的过程。因此,将其设置为False可使速度提高2-3倍。
7.3 匹配形状
OpenCV带有函数 cv.matchShapes()**,使我们能够比较两个形状或两个轮廓,并返回显示相似性的度量。结果越低,匹配越好。它是基于 **hu-moment 值计算的。文档中介绍了不同的测量方法。
import cv2 as cv
import numpy as np
img1 = cv.imread('star.jpg',0)
img2 = cv.imread('star2.jpg',0)
ret, thresh = cv.threshold(img1, 127, 255,0)
ret, thresh2 = cv.threshold(img2, 127, 255,0)
contours,hierarchy = cv.findContours(thresh,2,1)
cnt1 = contours[0]
contours,hierarchy = cv.findContours(thresh2,2,1)
cnt2 = contours[0]
ret = cv.matchShapes(cnt1,cnt2,1,0.0)
print( ret )
我尝试匹配以下给出的不同形状的形状:
- 匹配图像A本身= 0.0
- 将图像A与图像B匹配= 0.001946
- 将图像A与图像C匹配= 0.326911 看,即使图像旋转也不会对该比较产生太大影响。
也可以看看 Hu-Moments是平移,旋转和缩放不变的七个矩。第七个是偏斜不变的。这些值可以使用 cv.HuMoments() 函数找到。
8. 轮廓层次
目标
这次,我们了解轮廓的层次结构,即Contours中的父子关系。
理论
在有关轮廓的最后几篇文章中,我们使用了与OpenCV提供的轮廓相关的一些功能。但是,当我们使用 cv.findContours() 函数在图像中找到轮廓时,我们传递了一个参数,即 Contour Retrieval Mode 。我们通常通过 cv.RETR_LIST 或 cv.RETR_TREE ,效果很好。但这实际上是什么意思?
另外,在输出中,我们得到了三个数组,第一个是图像,第二个是轮廓,另一个是我们命名为层次结构的输出(请检查上一篇文章中的代码)。但是,我们从未在任何地方使用此层次结构。那么,这个层次结构是什么呢?它与前面提到的函数参数有什么关系?
这就是本文要处理的内容。
8.1 什么是层次结构?
通常我们使用 cv.findContours() 函数来检测图像中的对象,对吗?有时对象位于不同的位置。但是在某些情况下,某些形状位于其他形状内。就像嵌套的数字一样。在这种情况下,我们将外部的一个称为 父级 ,将内部的一个称为 子级 。这样,图像中的轮廓彼此之间就具有某种关系。并且我们可以指定一个轮廓如何相互连接,例如是其他轮廓的子轮廓,还是父轮廓等。这种关系的表示称为 层次结构 。
考虑下面的示例图像:
在此图像中,我从 0-5 编号了一些形状。2和2a表示最外面的盒子的外部和内部轮廓。
在此,轮廓0,1,2在 外部或最外部 。我们可以说,它们处于 0层次结构 中,或者只是处于 相同的层次结构级别 中。
接下来是 轮廓2a 。可以将其视为 轮廓2的子级 (或者相反,轮廓2是轮廓2a的父级)。因此,将其设置为 hierarchy-1 。同样,contour-3是contour-2的子级,位于下一个层次结构中。最后,轮廓4,5是轮廓3a的子级,它们位于最后的层次结构级别。从编号方式上来说,轮廓4是轮廓3a的第一个子元素(也可以是轮廓5)。
我提到这些东西是为了理解诸如相同的层次结构级别,外部轮廓,子轮廓,父轮廓,第一个孩子等术语。现在让我们进入OpenCV。
OpenCV中的层次结构
因此,每个轮廓都有关于其层次结构,其子级,其父级等的信息。OpenCV将其表示为四个值的数组:**[Next,Previous,First_Child,Parent]**
Next 表示相同等级的下一个轮廓 例如,在我们的图片中选择轮廓0。谁是同一级别的下一个轮廓?它是轮廓1。因此,只需将Next = 1放进去。同样对于Contour-1,下一个就是轮廓线2。所以下一个= 2。
那轮廓2呢?在同一层中没有下一个轮廓。简而言之,将Next = -1。那轮廓4呢?与轮廓5处于同一水平。所以它的下一个轮廓是轮廓5,所以Next = 5。
Previous 表示相同轮廓级别的上一个轮廓 和上面一样。轮廓1的先前轮廓是同一级别的轮廓0。同样对于轮廓2,它是轮廓1。对于轮廓0,没有先前值,因此将其设为-1。
First_Child 表示其第一个子轮廓 无需任何解释。对于轮廓2,子级是轮廓2a。这样就得到了轮廓2a的相应索引值。那轮廓3a呢?它有两个孩子。但是我们只带第一个孩子。它是轮廓4。因此,轮廓3a的First_Child = 4。
Parent 代表示其父代轮廓的索引 它与First_Child相反。轮廓4和轮廓5的父轮廓均为轮廓3a。对于轮廓3a,它是轮廓3,依此类推。
注意 如果没有孩子或父母,则该字段为-1
因此,现在我们知道了OpenCV中使用的层次结构样式,我们可以借助上面给出的相同图像来检查OpenCV中的轮廓检索模式。即像 cv.RETR_LIST ,cv.RETR_TREE ,cv.RETR_CCOMP,cv.RETR_EXTERNAL 等标志是什么意思?
8.2 轮廓检索模式
1. RETR_LIST
这是四个标志中最简单的一个(从解释的角度来看)。它仅检索所有轮廓,但不创建任何父子关系。在这个规则下,父母和孩子是平等的,他们只是轮廓。即它们都属于同一层次结构级别。
因此,在这里,层次结构数组中的第3和第4项始终为-1。但是很明显,下一个和上一个术语将具有其相应的值。只需自己检查并验证即可。
以下是我得到的结果,每行是相应轮廓的层次结构详细信息。例如,第一行对应于轮廓0。下一个轮廓为轮廓1。因此Next =1。没有先前的轮廓,因此Previous = -1。如前所述,其余两个为-1。
>>> hierarchy
array([[[ 1, -1, -1, -1],
[ 2, 0, -1, -1],
[ 3, 1, -1, -1],
[ 4, 2, -1, -1],
[ 5, 3, -1, -1],
[ 6, 4, -1, -1],
[ 7, 5, -1, -1],
[-1, 6, -1, -1]]])
如果不使用任何层次结构功能,这是在代码中使用的不错选择。
2. RETR_EXTERNAL
如果使用此标志,则仅返回极端的外部标志。保留所有子轮廓。可以说,根据这项法律,只有每个家庭中的老大才能得到照顾。它不在乎家庭其他成员 :)。
那么,在我们的图像中,有多少个极端的外部轮廓?即在等级0级别?只有3个,即轮廓0,1,2,对吗?现在尝试使用该标志查找轮廓。在此,赋予每个元素的值也与上述相同。与上面的结果进行比较。以下是我得到的:
>>> hierarchy
array([[[ 1, -1, -1, -1],
[ 2, 0, -1, -1],
[ 3, 1, -1, -1],
[ 4, 2, -1, -1],
[ 5, 3, -1, -1],
[ 6, 4, -1, -1],
[ 7, 5, -1, -1],
[-1, 6, -1, -1]]])
如果只想提取外部轮廓,则可以使用此标志。在某些情况下可能有用
3. RETR_CCOMP
该标志检索所有轮廓并将它们排列为2级层次结构。即,对象的外部轮廓(即其边界)位于层次1中。然后,将对象(如果有)中的孔的轮廓放置在层次2中。如果其中有任何对象,则其轮廓将仅再次放置在等级1中。以及它在等级2中的漏洞等等。
只需考虑黑色背景上的“白色大零”图像即可。零外圈属于第一层级,零内圈属于第二层级。
我们可以用一个简单的图像来解释它。在这里,我用红色标记了轮廓的顺序,并用绿色(1或2)标记了它们所属的层次。该顺序与OpenCV检测轮廓的顺序相同。
因此考虑第一个轮廓,即轮廓0。它是等级1。它有两个孔,轮廓1和2,它们属于层次2。因此,对于轮廓0,相同层次结构级别中的下一个轮廓为轮廓3。而且没有以前的。它的第一个子对象是层次结构2中的轮廓1。它没有父级,因为它位于1层级中。因此其层次结构数组为 [3,-1,1,-1]
现在取轮廓1。它在等级2中。在同一层次结构中(轮廓1的父项下)下一个是轮廓2。没有上一个。没有孩子,但父母的轮廓为0。因此数组为 [2,-1,-1,0]。
同样的轮廓2:它位于层次2中。在轮廓-0下的相同层次结构中没有下一个轮廓。所以没有下一步。上一个是轮廓1。没有孩子,父母的轮廓为0。因此数组为 [-1,1,-1,0]。
轮廓-3:等级1中的下一个是轮廓5。上一个是轮廓0。孩子是轮廓4,没有父母。因此数组为 [5,0,4,-1]。
轮廓-4:位于轮廓3下的层次2中,并且没有同级。所以没有下一个,没有以前的,没有孩子,父母是轮廓3。因此数组为 [-1,-1,-1,3]。
剩下的可以填满。这是我得到的最终答案:
>>> hierarchy
array([[[ 3, -1, 1, -1],
[ 2, -1, -1, 0],
[-1, 1, -1, 0],
[ 5, 0, 4, -1],
[-1, -1, -1, 3],
[ 7, 3, 6, -1],
[-1, -1, -1, 5],
[ 8, 5, -1, -1],
[-1, 7, -1, -1]]])
4. RETR_TREE
这是最后一个家伙,Perfect先生。它检索所有轮廓并创建完整的族层次列表。它甚至告诉,谁是爷爷,父亲,儿子,孙子甚至更远... :)。
例如,我拍摄了上面的图片,重写了 cv.RETR_TREE 的代码,根据OpenCV给定的结果对轮廓进行重新排序并对其进行分析。同样,红色字母表示轮廓编号,绿色字母表示层次结构顺序。
取轮廓0:在层次0中。同一层次结构中的下一个轮廓是轮廓7。没有先前的轮廓。孩子是轮廓1。而且没有父母。因此数组为 [7,-1,1,-1]。
取轮廓2:在等级1中。同一级别无轮廓。没有上一个。孩子是轮廓3。父级是轮廓1。因此数组为 [-1,-1,3,1]。
还有,尝试一下。以下是完整答案:
>>> hierarchy
array([[[ 7, -1, 1, -1],
[-1, -1, 2, 0],
[-1, -1, 3, 1],
[-1, -1, 4, 2],
[-1, -1, 5, 3],
[ 6, -1, -1, 4],
[-1, 5, -1, 4],
[ 8, 0, -1, -1],
[-1, 7, -1, -1]]])