img

小知识

一个BGR图像 0~255

ROI

更改图像的特定区域 Region Of Interest ROI 感兴趣区域

mask 掩膜

0 黑 255 白

与目标图像做mask操作 目标图像扣走mask中黑色轮廓部分,保留白色区域 => 保留ROI,其他区域为0

通常mask之前

  • 先转成灰度图像
  • 二值化、反二值化

作用

  • 对自己做mask 可以抠图
  • 提取ROI
  • 特征提取

hsv

BGR和HSV的转换使用 cv.COLOR_BGR2HSV

  • H表示色彩/色度,取值范围 [0,179]
  • S表示饱和度,取值范围 [0,255]
  • V表示亮度,取值范围 [0,255]

图像矩

图像矩一般都是原点矩

  • 一阶矩和零阶矩,计算形状的质心
  • 二阶矩,计算形状的方向

V(i,j)表示图像在(i,j)点上的灰度值。

二阶矩,计算形状的方向

基本

read write show

获取图片 路径不对,imread get None

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import cv2 as cv
from matplotlib import pyplot as plt


def show(*args):
    if args:
        for img in args:
            cv.imshow('', img)
            # wait 毫秒 0是永远
            cv.waitKey(0)
            cv.destroyAllWindows()


def plt_color_img(img):
    """
    彩色图片使用opencv加载是使用BGR模式,但是使用Matplotlib库是用RGB模式
    :param img:
    :return:
    """
    b, g, r = cv.split(img)
    img2 = cv.merge([r, g, b])
    plt.imshow(img2)
    plt.xticks([])
    plt.yticks([])
    plt.show()


def plt_gray_img(img):
    plt.imshow(img, cmap='gray', interpolation='bicubic')
    plt.xticks([])
    plt.yticks([])
    plt.show()


def load_img(path, mode=cv.IMREAD_COLOR):
    """
    cv.IMREAD_COLOR:加载彩色图像。任何图像的透明度都将被忽略。这是默认标志。
    cv.IMREAD_GRAYSCALE:以灰度模式加载图像
    cv.IMREAD_UNCHANGED:加载图像,包括alpha通道

    整数1,0或-1
    """
    return cv.imread(path, mode)


def save_img(path, img):
    cv.imwrite(path, img)

属性

1
2
3
4
5
6
img = util.load_img('img/messi5.jpg')
# 属性
print(img.shape)  # 行数 高、列数 宽、[通道数]
print(img.size)  # 总像素数
print(img.dtype)  # 数据类型

选取 ROI

选取区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 选择数组的区域 前5行和后3列
px = img[50, 50]
print(px)
px = img[50, 50, 0]  # 获取blue值
print(px)
px = img[50, 50, 1]  # 获取green值
print(px)
px = img[50, 50, 2]  # 获取red值
print(px)

img[50, 50] = [255, 255, 255]
print(img[50, 50])

# copy
ball = img[280:340, 330:390]
img[273:333, 100:160] = ball
util.save_img('out.jpg', img)

单个选取

1
2
3
4
5
6
7
8
# 单个像素访问item() itemset()
print('\n', img[10, 10])
sca = img.item(10, 10, 0)  # 获取bgr值
print(sca)
sca = img.item(10, 10, 1)
print(sca)
sca = img.item(10, 10, 2)
print(sca)

split merge

1
2
3
# 分割和合并图像通道 split 耗时 没什么必要可以用numpy的index
b, g, r = cv.split(img)
img2 = cv.merge([r, g, b])

运算

add addWeighted

Both images should be of same depth and type, (shape and 图片后缀)or second image can just be a scalar value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"""图像运算"""
x = np.uint8([250])
y = np.uint8([10])
"""
Both images should be of same depth and type, (shape and 图片后缀)
or second image can just be a scalar value.

OpenCV添加是饱和操作,而Numpy添加是模运算。
"""
print(cv.add(x, y))  # 250+10 = 260 => 255
print(x + y)  # 250+10 = 260 % 256(2^8) = 4
img1 = util.load_img('img/ml.png')
img2 = util.load_img('img/opencv-logo.png')
print(img1.shape, img2.shape)
img2 = cv.resize(img2, (img1.shape[1], img1.shape[0]))
dst = cv.add(img1, img2)
util.show(dst)

"""
图像混合 按比例混合起来,有不同的权重 修改透明度
dst=α⋅img1+β⋅img2+γ
"""
img1 = util.load_img('img/ml.png')
img2 = util.load_img('img/opencv-logo.png')
print(img1.shape, img2.shape)
img2 = cv.resize(img2, (img1.shape[1], img1.shape[0]))
assert img1.shape == img2.shape
print(img1.shape, img2.shape)
# 有不同的权重
dst = cv.addWeighted(img1, 0.7, img2, 0.3, 0)
util.show(dst)

位运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
"""
按位AND,OR,NOT和XOR运算 添加两个图像,它将改变颜色。如果我混合它,我会得到一个透明的效果
"""
img1 = util.load_img('img/messi5.jpg')
img2 = util.load_img('img/opencv-logo-white.png')

rows, cols, channels = img2.shape
roi = img1[0:rows, 0:cols]

"""
what is mask 掩膜

0 黑 255 白

与目标图像做mask操作 目标图像扣走mask中黑色轮廓部分,保留白色区域 => 保留ROI,其他区域为0

通常mask之前
先转成灰度图像
二值化、反二值化

对自己做mask 可以抠图
提取ROI
特征提取

"""
img2gray = cv.cvtColor(img2, cv.COLOR_BGR2GRAY)
ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)
mask_inv = cv.bitwise_not(mask)

img1_bg = cv.bitwise_and(roi, roi, mask=mask_inv)
img2_fg = cv.bitwise_and(img2, img2, mask=mask)

util.save_img('img1_bg.png', img1_bg)
util.save_img('img2_fg.png', img2_fg)

dst = cv.add(img1_bg, img2_fg)
img1[0:rows, 0:cols] = dst
util.show(mask_inv, img1_bg, mask, img2_fg)
util.show(img1)
# util.show(mask_inv)

color

灰度

1
2
3
4
img = util.load_img('img/messi5.jpg')
img2gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

util.show(img2gray)

HSV

选取颜色ROI视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
cap = cv.VideoCapture(0)
while (1):
    # Take each frame
    _, frame = cap.read()
    # Convert BGR to HSV
    hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)

    """
    BGR2HSV
    
    选取绿色
    green = np.uint8([[[0,255,0 ]]])
    hsv_green = cv2.cvtColor(green,cv2.COLOR_BGR2HSV)
    
    使用[H-10, 100,100] and [H+10, 255, 255] 做阈值上下限
    """
    # define range of blue color in HSV
    lower_blue = np.array([110, 50, 50])
    upper_blue = np.array([130, 255, 255])
    # 设定取值范围
    mask = cv.inRange(hsv, lower_blue, upper_blue)
    # Bitwise-AND mask and original image
    res = cv.bitwise_and(frame, frame, mask=mask)
    cv.imshow('frame', frame)
    cv.imshow('mask', mask)
    cv.imshow('res', res)
    k = cv.waitKey(5) & 0xFF
    if k == 27:
        break
cv.destroyAllWindows()

几何变换

放大缩小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"""
translation, rotation, affine transformation

img.shape height width channel

x 横坐标 y 纵坐标
resize( (width,height),fx,fy (因子) )
cv.INTER_AREA for 缩小
cv.INTER_LINEAR for 放大 default

cv.INTER_CUBIC slow

"""
img = util.load_img('img/messi5.jpg')
height, width = img.shape[:2]
print(img.shape)
res = cv.resize(img, None, fx=3, fy=2, interpolation=cv.INTER_LINEAR)
res = cv.resize(img, (int(0.5 * width), int(0.5 * height)), interpolation=cv.INTER_AREA)

print(res.shape)
util.show(res)

平移

1
2
3
4
5
6
7
8
9
10
"""
平移 (100,50)
"""
img = util.load_img('img/messi5.jpg', 0)
print(img.shape)
rows, cols = img.shape
M = np.float32([[1, 0, 100], [0, 1, 50]])
dst = cv.warpAffine(img, M, (cols, rows))
print(dst.shape)
util.show(dst)

旋转

1
2
3
4
5
6
7
8
9
10
11
"""
旋转 90度
"""
img = util.load_img('img/messi5.jpg', 0)
rows, cols = img.shape
# cols-1 and rows-1 are the coordinate limits.
# center, angle, scale
M = cv.getRotationMatrix2D(((cols - 1) / 2.0, (rows - 1) / 2.0), 90, 1)
dst = cv.warpAffine(img, M, (cols, rows))
print(dst.shape)
util.show(dst)

二值化 threshold

自定义全局值

1
2
3
4
5
6
7
8
9
10
11
12
13
img = util.load_img('img/gradient.png', 0)
ret, thresh1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
ret, thresh2 = cv.threshold(img, 127, 255, cv.THRESH_BINARY_INV)
ret, thresh3 = cv.threshold(img, 127, 255, cv.THRESH_TRUNC)
ret, thresh4 = cv.threshold(img, 127, 255, cv.THRESH_TOZERO)
ret, thresh5 = cv.threshold(img, 127, 255, cv.THRESH_TOZERO_INV)
titles = ['Original Image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(6):
    plt.subplot(2, 3, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

自适应阈值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"""
自适应阈值

照明条件

算法基于其周围的小区域确定像素的阈值。
因此,我们为同一图像的不同区域获得不同的阈值,这为具有不同照明的图像提供了更好的结果。

cv.ADAPTIVE_THRESH_MEAN_C 阈值= 取邻近区域的平均值 - 常数C
cv.ADAPTIVE_THRESH_MEAN_C 阈值= 邻近区域的高斯加权和 - 常数C
adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst]) → dst
"""
img = util.load_img('img/sudoku.png', 0)
util.show(img)
# smoothes an image using the median filter 二值化效果更好
img = cv.medianBlur(img, 5)
util.show(img)
ret, th1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
th2 = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 11, 2)
th3 = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)
titles = ['Original Image', 'Global Thresholding (v = 127)',
          'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [img, th1, th2, th3]
for i in range(4):
    plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

OTSU

适合双峰图

1
2
3
4
img_rgb = util.load_img('img/coins.png')
gray = util.gray(img_rgb)
plt.hist(gray.ravel(), 256, [0, 256])
plt.show()

cv.THRESH_OTSU效果更好

1
2
3
4
_, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV)
_, thresh1 = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
res = np.hstack((thresh,thresh1))
util.show(res)

过滤 smooth 消除噪音

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""
图像过滤
低通滤波LPF可以使图像去除噪声,高通滤波HPF可以找到图像的边缘。

图像平滑
内核卷积来实现图像模糊。它有助于消除噪音。实际上从图像中去除了高频内容(例如:噪声,边缘)。边缘会有点模糊。

均值过滤
调用blur()等效于调用将normalize=true的boxFilter().

中位数
 cv.medianBlur() 内核区域下所有像素的中值,并用该中值替换中心元素

双边过滤
cv.bilateralFilter() 降低噪音方面非常有效,同时保持边缘清晰。但与其他过滤器相比,操作速度较慢。
高斯滤波器采用像素周围的邻域并找到其高斯加权平均值

:return:
"""
img = util.load_img('img/opencv-logo-white.png')
cv.blur(img, (5, 5))
blur = cv.GaussianBlur(img, (5, 5), 0)
img = cv.medianBlur(img, 5)
cv.blur(img, (5, 5))

morphological 侵蚀和膨胀

形态学运算符是 侵蚀和膨胀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
img = util.load_img('img/j.png', 0)

"""
侵蚀 Erosion 如果与卷积核对应的原图像像素值都是1,那么中心元素保持原值,否则为0
有助于消除小的白噪声
"""
kernel = np.ones((5, 5), np.uint8)
erosion = cv.erode(img, kernel, iterations=1)

"""
扩张 Dilation 恰好与侵蚀相反 由于噪音消失了,它们不会再回来
"""
dilation = cv.dilate(img, kernel, iterations=1)
"""
开运算  先腐蚀再膨胀,一般用来去除噪声
"""
opening = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)
"""
闭运算  先膨胀再腐蚀,一般用来填充黑色的小像素点
"""
closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
util.plt_gray_img(img, closing)

边界识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""
Edge Detection

Canny Edge Detection

去除噪声 remove the noise in the image with a 5x5 Gaussian filter
计算图像梯度 在水平与竖直方向上计算一阶导数,图像梯度方向和大小
去除噪声 remove the noise in the image with a 5x5 Gaussian filter
去除噪声 remove the noise in the image with a 5x5 Gaussian filter
"""
img = util.load_img('img/messi5.jpg', 0)
edges = cv.Canny(img, 100, 200)
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('Original Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(edges, cmap='gray')
plt.title('Edge Image'), plt.xticks([]), plt.yticks([])
plt.show()

watershed

使用距离变换和分水岭来分割相互接触的物体

靠近对象中心的区域是前景,离对象远的区域是背景,不确定的区域是边界

  • 物体没有相互接触/只求前景 可用侵蚀消除了边界像素

  • 到距离变换并应用适当的阈值 膨胀操作会将对象边界延伸到背景,确保background区域只有background

边界 = 能否确认是否是背景的区域 - 确定是前景的区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
img = util.load_img('img/coins.png')
gray = util.gray(img)

ret, thresh = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)

# noise removal
kernel = np.ones((3, 3), np.uint8)
opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2)

# sure background area
sure_bg = cv.dilate(opening, kernel, iterations=3)

# Finding sure foreground area
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5) # 计算每个像素离最近0像素的距离
ret, sure_fg = cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)

# Finding unknown region
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg, sure_fg)

# Marker labelling 用0标记图像的背景 其他对象用从1开始的整数标记
ret, markers = cv.connectedComponents(sure_fg)

"""
我们知道,如果背景标记为0,分水岭会将其视为未知区域。所以我们想用不同的整数来标记它。相反,我们将标记由未知定义的未知区域,为0。
"""
# Add one to all labels so that sure background is not 0, but 1
markers = markers + 1
# Now, mark the region of unknown with zero
markers[unknown == 255] = 0

markers = cv.watershed(img, markers)
# 修改标记图像。边界区域将标记为-1
img[markers == -1] = [255, 0, 0]

util.show(img,is_seq=True)

GrabCut

提取图像中的前景

  • 用户在前景区域周围绘制一个矩形(前景区域应该完全在矩形内)
  • 算法迭代地对其进行分段以获得最佳结果

某些情况下,分割将不会很好。某些前景区域标记为背景用户需要做标记前景和后景

算法过程 就是监督半监督学习 根据颜色相似性 聚类分割

Hough

检测直线 霍夫变换

极坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
img = util.load_img('img/sudoku.png')
img_gray = util.gray(img)
edges = cv.Canny(img_gray, 100, 200)

"""cv.HoughLinesP"""
lines = cv.HoughLinesP(edges, 1, np.pi / 180, 200, minLineLength=100, maxLineGap=10)
for line in lines:
    x1, y1, x2, y2 = line[0]
    cv.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2)

"""cv.HoughLines"""
# lines = cv.HoughLines(edges, 1, np.pi / 180, 200)
# for line in lines:
#     rho, theta = line[0]
#     a = np.cos(theta)
#     b = np.sin(theta)
#     x0 = a * rho
#     y0 = b * rho
#     x1 = int(x0 + 1000 * (-b))
#     y1 = int(y0 + 1000 * (a))
#     x2 = int(x0 - 1000 * (-b))
#     y2 = int(y0 - 1000 * (a))
#     cv.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
""""""

util.show(img)

pyramid

有不同分辨率的相同图像

不同分辨率的图像就是图像金字塔 小分辨率的图像在顶部,大的在底部

低分辨率的图像由高分辨率的图像去除连续的行和列得到

1
2
3
4
5
6
img = util.load_img('img/messi5.jpg')
# 分辨率减少 M×N的图像就变成了M/2×N/2的图像了,面积变为原来的四分之一
lower_reso = cv.pyrDown(img)
# 尺寸变大 分辨率不变
high_reso = cv.pyrUp(lower_reso)
util.show(img, lower_reso, high_reso)

复杂例子 模糊边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
A = util.load_img('img/apple.jpg')
B = util.load_img('img/orange.jpg')

# generate Gaussian pyramid for A
G = A.copy()
gpA = [G]
for i in range(6):
    G = cv.pyrDown(G)
    gpA.append(G)
# generate Gaussian pyramid for B
G = B.copy()
gpB = [G]
for i in range(6):
    G = cv.pyrDown(G)
    gpB.append(G)
# generate Laplacian Pyramid for A
lpA = [gpA[5]]
for i in range(5, 0, -1):
    GE = cv.pyrUp(gpA[i])
    L = cv.subtract(gpA[i - 1], GE)
    lpA.append(L)
util.show(*lpA)
# generate Laplacian Pyramid for B
lpB = [gpB[5]]
for i in range(5, 0, -1):
    GE = cv.pyrUp(gpB[i])
    L = cv.subtract(gpB[i - 1], GE)
    lpB.append(L)
util.show(*lpB)
# Now add left and right halves of images in each level
LS = []
for la, lb in zip(lpA, lpB):
    rows, cols, dpt = la.shape
    ls = np.hstack((la[:, 0:int(cols / 2)], lb[:, int(cols / 2):]))
    LS.append(ls)
util.show(*LS)
# now reconstruct
ls_ = LS[0]
for i in range(1, 6):
    ls_ = cv.pyrUp(ls_)
    ls_ = cv.add(ls_, LS[i])
# image with direct connecting each half
real = np.hstack((A[:, :int(cols / 2)], B[:, int(cols / 2):]))
cv.imwrite('Pyramid_blending2.jpg', ls_)
cv.imwrite('Direct_blending.jpg', real)

Contour

轮廓

概念

what is contour

轮廓 是连接所有连续点(沿着边界)的曲线,具有相同的颜色或强度。轮廓是形状分析和物体检测和识别的有用工具。

从黑色背景中找到白色物体

hierarchy 层次结构

外部一个称为父项,将内部项称为子项,图像中的轮廓彼此之间存在某种关系。并且我们可以指定一个轮廓如何相互连接

每个轮廓都有自己的信息 [Next,Previous,First_Child,Parent] 没有为-1

  • Next 表示同一层级的下一个轮廓
  • Previous 表示同一层级的先前轮廓
  • First_Child 表示其第一个子轮廓
  • Parent 表示其父轮廓的索引

轮廓查找模式

  • RETR_LIST 检索所有轮廓,但不创建任何父子关系 它们都属于同一层级 First_Child Parent = -1
  • RETR_EXTERNAL 只返回最外边的轮廓,所有子轮廓会忽略
  • RETR_CCOMP 所有轮廓并将它们排列为2级层次结构 对象的外部轮廓(即其边界)放置在层次结构-1中。对象内部的轮廓(如果有的话)放在层次结构-2中 如此类推
  • RETR_TREE 检索所有轮廓并创建完整的族层次结构列表
1
2
3
4
5
6
7
8
9
img = util.load_img('img/apple.jpg')
imgray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(imgray, 127, 255, 0)

# cv.findContours函数中有三个参数,第一个是源图像,第二个是轮廓检索模式,第三个是轮廓逼近法。它输出轮廓和层次结构
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
# 第一个参数是源图像,第二个参数是轮廓,第三个参数是轮廓索引 -1是所有轮廓 其余参数是颜色,厚度
res = cv.drawContours(img, contours, -1, (0,255,0), 3)
util.show(res)

轮廓逼近法

轮廓中会存储所有边界上的(x,y)坐标。但是不一定需要全部存储

  • cv.CHAIN_APPROX_NONE,则存储所有边界点
  • cv.CHAIN_APPROX_SIMPLE 它删除所有冗余点并压缩轮廓,从而节省内存

质心 面积 周长

其他特征和性质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import cv2 as cv

import util

src = util.load_img('img/s.png', 0)
ret, thresh = cv.threshold(src, 127, 255, cv.THRESH_BINARY)

# cv.findContours()函数中有三个参数,第一个是源图像,第二个是轮廓检索模式,第三个是轮廓近似方法。它输出轮廓和层次结构
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)

height = src.shape[0]
width = src.shape[0]

out = util.blank_img(width*1.5, height, (255, 255, 255))

for index, cnt in enumerate(contours):
    # 画轮廓
    cv.drawContours(out, contours, index, (0, 0, 255), 1)
    # 质心坐标
    M = cv.moments(cnt)
    cx = M['m10'] / M['m00']
    cy = M['m01'] / M['m00']
    cv.circle(out, (int(cx), int(cy)), 2, (255, 0, 0), -1)
    # 轮廓面积 M['m00']
    area = cv.contourArea(cnt)
    # 轮廓周长   轮廓是否闭合True
    perimeter = cv.arcLength(cnt, True)

    print('({},{}) 面积={} 周长={}'.format(cx, cy, area, perimeter))

util.show(out)

approx 轮廓近似

用更少点组成的轮廓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
img = util.load_img('img/t.png',0)
height = img.shape[0]
width = img.shape[0]

out = util.blank_img(width * 1.5, height, (255, 255, 255))
contours = get_cnt(img)
# 蓝色
a = cv.drawContours(out, contours, -1, (255, 0, 0), 5)

# factor  越小越接近轮廓 用的点越多
factor = 0.01
epsilon = factor * cv.arcLength(contours[0], True)
approx = cv.approxPolyDP(contours[0], epsilon, True)
# 红色
cv.drawContours(out, [approx], -1, (0, 0, 255), 5)
util.show(a,out)

Convex Hull

类似于轮廓近似

凸包: 在多维空间中有一群散佈各处的点 ,凸包 是包覆这群点的所有外壳当中, 表面积暨容积最小的一个外壳,而最小的外壳一定是凸的。

凸的定义: 图形内任意两点的连线不会经过图形外部

凸性缺陷

求凸包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
img = util.load_img('img/horse.png', 0)
convex = util.load_img('img/horse1.png',0)
height = img.shape[0]
width = img.shape[0]

out = util.blank_img(width * 1.5, height, (255, 255, 255))
# 求轮廓
contours = get_cnt(img)
convex_cnt = get_cnt(convex)[0]

# 求凸包
cv.drawContours(out, contours, -1, (255, 0, 0), 5)
cnt = contours[0]
hull = cv.convexHull(cnt)

# 检测一个曲线是不是凸的
print(cv.isContourConvex(cnt))

cv.drawContours(out, [hull], -1, (0, 0, 255), 5)
util.show(out)

边界矩形 边界标注

Bounding Rectangle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
img = util.load_img('img/rect.png', 0)
contours = get_cnt(img)

height = img.shape[0]
width = img.shape[0]

out = util.blank_img(width * 1.5, height, (255, 255, 255))
# out = img
for index, cnt in enumerate(contours):
    x, y, w, h = cv.boundingRect(cnt)
    # 原型轮廓 红
    cv.drawContours(out, contours, index, (0, 0, 255), 3)

    # 直边矩形 绿
    cv.rectangle(out, (x, y), (x + w, y + h), (0, 255, 0), 2)

    # 最小面积绘制边界矩形 蓝
    rect = cv.minAreaRect(cnt)
    box = cv.boxPoints(rect)
    box_ = np.int0(box)
    cv.drawContours(out, [box_], 0, util.rgb2bgr((30, 144, 255)), 2)

    # 最小封闭圈 橙
    (x, y), radius = cv.minEnclosingCircle(cnt)
    center, radius = (int(x), int(y)), int(radius)
    cv.circle(out, center, radius, util.rgb2bgr((255, 140, 0)), 2)

    # 拟合椭圆 蓝
    ellipse = cv.fitEllipse(cnt)
    cv.ellipse(out, ellipse, util.rgb2bgr((135, 206, 250)), 2)
    print(index)
    # 线 黑
    # rows, cols = out.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)
    # print((cols - 1, righty), (0, lefty))
    # cv.line(out, (cols - 1, righty), (0, lefty), (0, 0, 0), 2)

    # util.show(out)

util.show(out)

其他

凸性缺陷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
img = util.load_img('img/star.png', 0)
contours = get_cnt(img)
cnt = contours[0]
# 凸包时传递returnPoints = False,以便找到凸起缺陷
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)

util.show(img)

点多边形距离

(50,50) 点与轮廓之间的最短距离 当点在轮廓外时返回负值,当点在内部时返回正值,如果点在轮廓上则返回零

1
dist = cv.pointPolygonTest(cnt,(50,50),True)

匹配形状

越相似越小 一样 =0.0

1
ret = cv.matchShapes(cnt1,cnt2,1,0.0)