本文基于opencv官方文档,是本人的学习笔记。版本是在linux下的opencv4.2.0,全部程序调通可运行,无bug。

import cv2 as cv
import numpy as np

# 引入图片并二值化
image = cv.imread('head.jpg', 1)
imgray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(imgray, 127, 255, 0)

# 寻找轮廓
# findcontour()函数中有三个参数,
# 第一个是源图像,第二个是轮廓检索模式,第三个是轮廓逼近方法。
# 输出  等高线和层次结构。
# 轮廓是图像中所有轮廓的Python列表。
# 每个单独的轮廓是一个(x,y)坐标的Numpy数组的边界点的对象。

# 如果传递cv.CHAIN_APPROX_NONE,则将存储所有边界点。
# 但是实际上我们需要所有这些要点吗?例如,您找到了一条直线的轮廓。
# 是否需要线上的所有点来代表该线?不,
# 我们只需要该线的两个端点即可。这就是cv.CHAIN_APPROX_SIMPLE所做的。
# 它删除所有冗余点并压缩轮廓,从而节省内存。
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)

# 画出轮廓
# 注意 findContours并不会直接改变图像,要想图像显示出轮廓则必须使用drawContours
# 要绘制轮廓,请使用cv.drawContours函数。只要有边界点,它也可以用来绘制任何形状。
# 它的第一个参数是源图像,第二个参数是应该作为Python列表传递的轮廓,
# 第三个参数是轮廓的索引(在绘制单个轮廓时有用。要绘制所有轮廓,请传递-1),
# 其余参数是颜色,厚度等等
# 第一个轮廓传入0。以此类推。-1为全部
cv.drawContours(image, contours, -1, (0, 255, 0), 3)
# 也可以
count = contours[0]
cv.drawContours(image, [count], 0, (0, 0, 255), 10)
#  两种方法相似,但是前进时,会发现最后一种更有用。

cv.imshow('wq', image)
cv.waitKey(0)

# 特征矩
# 特征矩可以帮助您计算一些特征,例如物体的质心,物体的面积等。
# 请查看特征矩上的维基百科页面。
# 函数cv.moments()提供了所有计算出的矩值的字典。
cnt = contours[0]
M = cv.moments(cnt)
print(M)

# 从这一刻起,您可以提取有用的数据,
# 例如面积,质心等。质心由关系给出,
# $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'])
print(cx, cy)

# 轮廓面积
# 轮廓区域由函数cv.contourArea()或从矩M['m00']中给出
aera = cv.contourArea(cnt)
print(aera)

# 轮廓周长
# 可以使用cv.arcLength()函数找到它。
# 第二个参数指定形状是闭合轮廓(True)还是曲线。
length = cv.arcLength(cnt, False)
print(length)

# 根据我们指定的精度,它可以将轮廓形状近似为顶点数量较少的其他形状。
# 它是Douglas-Peucker算法的实现。检查维基百科页面上的算法和演示。
# 为了理解这一点,假设您试图在图像中找到一个正方形,但是由于图像中的某些问题,
# 您没有得到一个完美的正方形,而是一个“坏形状”(如下图所示)。
# 现在,您可以使用此功能来近似形状。在这种情况下,第二个参数称为epsilon,
# 它是从轮廓到近似轮廓的最大距离。它是一个精度参数。需要正确选择epsilon才能获得正确的输出。
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
cnt = contours[1]
epsilon = 0.3 * cv.arcLength(cnt, True)
approx = cv.approxPolyDP(cnt, epsilon, True)
cv.drawContours(image, [approx], -1, (0, 255, 255), 5)
cv.imshow('mm', image)
cv.waitKey(0)

# 轮廓凸包
# 凸包观看起来与轮廓逼近相似,但不相似(在某些情况下两者可能提供相同的结果)。
# 在这里,cv.convexHull()函数检查曲线是否存在凸凹缺陷并对其进行校正。
# 一般而言,凸曲线是始终凸出或至少平坦的曲线。如果在内部凸出,则称为凸度缺陷。
# 例如,检查下面的手的图像。红线显示手的凸包。
# 双向箭头标记显示凸度缺陷,这是凸包与轮廓线之间的局部最大偏差。

# 关于它的语法,有一些需要讨论:
# hull = cv.convexHull(points[, hull[, clockwise[, returnPoints]]
# 参数详细信息: - 点是我们传递到的轮廓。 - 凸包是输出,通常我们忽略它。
# - 顺时针方向:方向标记。如果为True,则输出凸包为顺时针方向。否则,其方向为逆时针方向。
# - returnPoints:默认情况下为True。然后返回凸包的坐标。
# 如果为False,则返回与凸包点相对应的轮廓点的索引。

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

hull = cv.convexHull(cnt)
cv.drawContours(image, [hull], -1, (0, 255, 255), 5)
cv.imshow('mm', image)
cv.waitKey(0)
# 但是,如果要查找凸度缺陷,则需要传递returnPoints = False。
# 为了理解它,我们将拍摄上面的矩形图像。
# 首先,我发现它的轮廓为cnt。现在,我发现它的带有returnPoints = True的凸包,
# 得到以下值:[[[234 202]],[[51 202]],[[51 79]],[[234 79]]],
# 它们是四个角 矩形的点。现在,如果对returnPoints = False执行相同的操作,
# 则会得到以下结果:[[129],[67],[0],[142]]。
# 这些是轮廓中相应点的索引。
# 例如,检查第一个值:cnt [129] = [[234,202]]与第一个结果相同(对于其他结果依此类推)。

# 检查凸度
# cv.isContourConvex()具有检查曲线是否凸出的功能。它只是返回True还是False。
k = cv.isContourConvex(cnt)
print(k)

# 边界矩形
# 有两种类型的边界矩形。

# 第一种是直角矩形
# 他是一个矩形,不考虑物体的旋转,所以他不一定是最小的矩形
x, y, w, h = cv.boundingRect(cnt)
cv.rectangle(image, (x, y), (x + w, y + h), (255, 0, 0), 2)
cv.imshow('mm', image)
cv.waitKey(0)
# 第二种是旋转矩形
# 边界矩形是用最小面积绘制的,所以它也考虑了旋转。
# 使用的函数是cv.minAreaRect()。它返回一个Box2D结构,
# 其中包含以下细节 -(中心(x,y),(宽度,高度),旋转角度)。
# 但要画出这个矩形,我们需要矩形的四个角。它由函数cv.boxPoints()获得
rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int0(box)
cv.drawContours(image, [box], 0, (22, 45, 95), 2)
cv.imshow('mm', image)
cv.waitKey(0)

# 最小闭合圆
# 接下来,使用函数*cv.minEnclosingCircle(()查找对象的圆周。
# 它是一个以最小面积完全覆盖物体的圆。
# 很明显他返回的是圆心 和半径
(x, y), radius = cv.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
cv.circle(image, center, radius, (0, 255, 0), 2)
cv.imshow('mm', image)
cv.waitKey(0)

# 下一个是把一个椭圆拟合到一个物体上。它返回内接椭圆的旋转矩形。
ellipse = cv.fitEllipse(cnt)
cv.ellipse(image, ellipse, (0, 255, 0), 2)
cv.imshow('mm', image)
cv.waitKey(0)

# 拟合直线
# 同样,我们可以将一条直线拟合到一组点。下图包含一组白点。我们可以近似一条直线。
image = cv.imread('bw.jpg', 1)
imgray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(imgray, 127, 255, 0)
rows, cols = image.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(image, (cols - 1, righty), (0, lefty), (0, 255, 0), 2)
cv.imshow('mm', image)
cv.waitKey(0)

# 轮廓的部分属性
# 1.长宽比
# 它是对象边界矩形的宽度与高度的比值。
# 2. 范围
# 范围是轮廓区域与边界矩形区域的比值。
# 3.坚实度
# 坚实度是等高线面积与其凸包面积之比。
# 4. 等效直径
# 等效直径是面积与轮廓面积相同的圆的直径。
# 5. 取向
# 取向是物体指向的角度。以下方法还给出了主轴和副轴的长度。
(x, y), (MA, ma), angle = cv.fitEllipse(cnt)
# 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)
# 这里提供了两个方法,一个使用Numpy函数,
# 另一个使用OpenCV函数(最后的注释行)。结果也是一样的,只是略有不同。
# Numpy给出的坐标是(行、列)格式,而OpenCV给出的坐标是(x,y)格式。
# 所以基本上答案是可以互换的。注意,row = x, column = y。
# 7. 最大值,最小值和它们的位置
# 我们可以使用掩码图像找到这些参数。
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(imgray, mask=mask)
# 8. 平均颜色或平均强度
# 在这里,我们可以找到对象的平均颜色。或者可以是灰度模式下物体的平均强度。我们再次使用相同的掩码进行此操作。
mean_val = cv.mean(image, 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])

# 轮廓的更多属性
# 1. 凸性缺陷
# 从这个凸包上的任何偏差都可以被认为是凸性缺陷。
# OpenCV有一个函数来找到这个,cv.convexityDefects()。
# 一个基本的函数调用如下:
hull = cv.convexHull(cnt, returnPoints=False)
defects = cv.convexityDefects(cnt, hull)
# 它返回一个数组,其中每行包含这些值 —[起点、终点、最远点、到最远点的近似距离]。
# 我们可以用图像把它形象化。我们画一条连接起点和终点的线,
# 然后在最远处画一个圆。记住,返回的前三个值是cnt的索引。
# 所以我们必须从cnt中获取这些值。
# 2. 点多边形测试
#
# 这个函数找出图像中一点到轮廓线的最短距离。
# 它返回的距离,点在轮廓线外时为负,点在轮廓线内时为正,
# 点在轮廓线上时为零。
dist = cv.pointPolygonTest(cnt, (50, 50), True)
# 在函数中,第三个参数是measureDist。如果它是真的,它会找到有符号的距离。
# 如果为假,则查找该点是在轮廓线内部还是外部(分别返回+1、-1和0)。
# 注意 如果您不想找到距离,请确保第三个参数为False,因为这是一个耗时的过程。
# 因此,将其设置为False可使速度提高2-3倍。

# 3. 形状匹配
# OpenCV附带一个函数cv.matchShapes(),
# 该函数使我们能够比较两个形状或两个轮廓,
# 并返回一个显示相似性的度量。
# 结果越低,匹配越好。它是根据矩值计算出来的。
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)
# 即使是图像旋转也不会对这个比较产生很大的影响