opencv进阶二#

直方图匹配#

直方图匹配又称为直方图规定化,是指将一幅图像的直方图变成规定形状的直方图而进行的图像增强方法。 即将某幅影像或某一区域的直方图匹配到另一幅影像上。使两幅影像的色调保持一致。可以在单波段影像直方图之间进行匹配,也可以对多波段影像进行同时匹配。两幅图像比对前,通常要使其直方图形式一致。

直方图规定化,也叫做直方图匹配,用于将图像变换为某一特定的灰度分布,也就是其目的的灰度直方图是已知的。这其实和均衡化很类似,均衡化后的灰度直方图也是已知的,是一个均匀分布的直方图;而规定化后的直方图可以随意的指定,也就是在执行规定化操作时,首先要知道变换后的灰度直方图,这样才能确定变换函数。规定化操作能够有目的的增强某个灰度区间,相比于,均衡化操作,规定化多了一个输入,但是其变换后的结果也更灵活。

直方图规定化的实现步骤如下:

  • 计算原图像的累积直方图
  • 计算规定直方图的累积直方图
  • 计算两累积直方图的差值的绝对值
  • 根据累积直方图最小差值建立灰度级的映射
灰度值 0 1 2 3 4 5 6 7
原图累计概率 0.19 0.44 0.65 0.81 0.89 0.95 0.98 1.00
目标图累计概率 0 0 0 0.15 0.35 0.65 0.85 1.00
映射差值最小 0->3 1->4 2->5 3->6 4->6 5->7 6->7 7->7

代码实现:

  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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import cv2 as cv
import matplotlib.pyplot as plt;
import numpy as np

"""
直方图匹配: 让一张图参考另一张图, 让他们的色调保持一致
    步骤:
        计算原图累计直方图
        计算参考图的累计直方图
        计算两个累计直方图的差异
        生成原图和参考图之间的颜色映射
"""
# 计算单通道图像 累计概率
def getAccumulateRatios(img):

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

    # 1.计算直方图
    hist = cv.calcHist([img],[0],None,[256],[0,255])
    # print(hist)
    # plt.plot(hist)
    # plt.show()

    # 2.计算每个灰度值出现的概率
    ratios = hist/(height*width);

    # 3. 计算累计概率
    sumRatios = np.zeros(256,np.float)
    sum1=0;
    for i,r in enumerate(ratios):
        sum1 = sum1 + r;
        sumRatios[i] = sum1;

    # 4. 绘制累计概率直方图
    # x = np.linspace(0,255,256);
    # plt.bar(x,sumRatios)
    # plt.show()

    return sumRatios;


"""
 传入进来的数据仍然上单通道数据
    1. 计算累计直方图的差异
        找到最小的差异
    2. 获取颜色映射    
"""
def map_color(src_channel,refer_channel):
    # src 单通道累计直方图
    src_sumRatios = getAccumulateRatios(src_channel);
    # refer 单通道累计直方图
    refer_sumRatios = getAccumulateRatios(refer_channel);

    colorMaps = np.zeros(256,np.uint8);

    # 遍历原图每一个灰度的累计概率
    for i,srcRatio in enumerate(src_sumRatios):
        # 得到 i: 灰度值  ratio :累计概率  0  0.19
        min = 100;
        referColor = 0;
        for j,referRatio in enumerate(refer_sumRatios):
            # j: 表示参考图的灰度值, referRatio累计概率
            diff = np.abs(srcRatio - referRatio)
            if diff < min:
                # 更新最小值
                min = diff;
                referColor = j;

        # 0 ---> referColor
        colorMaps[i] = referColor;

    # print(colorMaps)
    return colorMaps;


""" 完成单通道直方图匹配  """
def oneChannelMatch(src_channel,refer_channel):
    # 单通道颜色值映射
    colorMaps = map_color(src_channel, refer_channel);

    # 绘制原图直方图
    # cv.imshow("src",src_channel)
    # plt.hist(src_channel.ravel(),bins=256,color="blue")
    # plt.show()
    # 绘制参考图直方图
    # cv.imshow("refer", refer_channel)
    # plt.hist(refer_channel.ravel(), bins=256,color="green")
    # plt.show()

    one_channel = src_channel
    height = one_channel.shape[0]
    width = one_channel.shape[1]
    for row in range(height):
        for col in range(width):
            # 获取原来的灰度值
            gray = one_channel[row, col];
            # 去颜色映射表中查找新的颜色值
            referColor = colorMaps[gray];
            # 替换为新的颜色值
            one_channel[row, col] = referColor;

    # 绘制生成之后的直方图
    # cv.imshow("dst", one_channel)
    # plt.hist(refer_channel.ravel(), bins=256, color="red")
    # plt.show()

    return one_channel

if __name__ == '__main__':
    src = cv.imread("../img/1.jpg",cv.IMREAD_COLOR);
    cv.imshow("src",src)
    refer = cv.imread("../img/2.jpg",cv.IMREAD_COLOR);
    cv.imshow("refer",refer)

    # 先计算src的累计直方图 , 计算单通道
    src_channels = cv.split(src);

    # 先计算refer的累计直方图 , 计算单通道
    refer_channels = cv.split(refer);

    dst_channel0 = oneChannelMatch(src_channels[0],refer_channels[0])
    dst_channel1 = oneChannelMatch(src_channels[1],refer_channels[1])
    dst_channel2 = oneChannelMatch(src_channels[2],refer_channels[2])

    dst = cv.merge([dst_channel0,dst_channel1,dst_channel2])
    cv.imshow("dst",dst);


    cv.waitKey(0)
    cv.destroyAllWindows()

图片卷积#

图像滤波是尽量保留图像细节特征的条件下对目标图像的噪声进行抑制,是图像预处理中不可缺少的操作,其处理效果的好坏将直接影响到后续图像处理和分析的有效性和可靠性。

线性滤波是图像处理最基本的方法,它允许我们对图像进行处理,产生很多不同的效果。首先,我们需要一个二维的滤波器矩阵(卷积核)和一个要处理的二维图像。然后,对于图像的每一个像素点,计算它的邻域像素和滤波器矩阵的对应元素的乘积,然后加起来,作为该像素位置的值。这样就完成了滤波过程。

对图像和滤波矩阵进行逐个元素相乘再求和的操作就相当于将一个二维的函数移动到另一个二维函数的所有位置,这个操作就叫卷积

卷积需要4个嵌套循环,所以它并不快,除非我们使用很小的卷积核。这里一般使用3x3或者5x5。而且,对于滤波器/卷积核,也有一定的规则要求:

  1. 滤波器的大小应该是奇数,这样它才有一个中心,例如3x3,5x5或者7x7。有中心了,也有了半径的称呼,例如5x5大小的核的半径就是2
  2. 滤波器矩阵所有的元素之和应该要等于1,这是为了保证滤波前后图像的亮度保持不变。当然了,这不是硬性要求了。
  3. 如果滤波器矩阵所有元素之和大于1,那么滤波后的图像就会比原图像更亮,反之,如果小于1,那么得到的图像就会变暗。如果和为0,图像不会变黑,但也会非常暗。
  4. 对于滤波后的结构,可能会出现负数或者大于255的数值。对这种情况,我们将他们直接截断到0和255之间即可。对于负数,也可以取绝对值。

均值滤波#

将卷积核内的所有灰度值加起来,然后计算出平均值,用这个平均值填充卷积核正中间的值,这样做可以降低图像的噪声,同时也会导致图像变得模糊 $$ G =1/9\left[\begin{matrix}1 & 1 & 1 \1 & 1 & 1\1 & 1 & 1\end{matrix}\right] $$

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import cv2 as cv

img = cv.imread("./assets/itheima.jpg", cv.IMREAD_COLOR)
cv.imshow("src",img)

dst = cv.blur(img, (3,3))
cv.imshow("dst",dst)

cv.waitKey(0)
cv.destroyAllWindows()

高斯模糊#

采用均值滤波降噪会导致图像模糊的非常厉害,有没有一种方式既能保留像素点真实值又能降低图片噪声呢?那就是加权平均的方式. 离中心点越近权值越高,越远权值越低.

但是权重的大小设置非常麻烦,那么有没有一种方式能够自动生成呢? 这个就是需要用到高斯函数

高斯函数呈现出的特征就是中间高,两边低的钟形

高斯模糊通常被用来减少图像噪声以及降低细节层次。 $$ G =1/16\left[\begin{matrix}1 & 2 & 1 \2 & 4 & 2\1 & 2 & 1\end{matrix}\right] $$

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import cv2 as cv

# 回调函数
def updateSigma(val):
    # 高斯模糊   参数1:图像  参数2:卷积核大小, 参数3:标准差越大,去除高斯噪声能力越强,图像越模糊
    gaussian_blur = cv.GaussianBlur(img, (5,5), val)
    cv.imshow("gaussian",gaussian_blur)

img = cv.imread("assets/itheima.jpg", cv.IMREAD_GRAYSCALE)
cv.imshow("src",img)
# 创建一个窗口
cv.namedWindow("gaussian",cv.WINDOW_AUTOSIZE)
# 创建一个窗口进度条: 参数1:名称 参数2:窗口名称  参数3: 起始值  参数4: 最大值, 参数5:回调函数
cv.createTrackbar("sigma","gaussian",0,255,updateSigma)

updateSigma(0)

cv.waitKey(0)
cv.destroyAllWindows()

中值滤波#

对邻近的像素点进行灰度排序,然后取中间值,它能有效去除图像中的椒盐噪声

操作原理: 卷积域内的像素值从小到大排序 取中间值作为卷积输出

示例代码

1
2
3
4
5
6
7
8
9
import cv2 as cv
img = cv.imread("./assets/itheima_salt.jpg", cv.IMREAD_COLOR)
cv.imshow("src",img)

dst = cv.medianBlur(img, 3)
cv.imshow("dst",dst)

cv.waitKey(0)
cv.destroyAllWindows()

Sobel算子#

Sobel算子是像素图像边缘检测中最重要的算子之一,在机器学习、数字媒体、计算机视觉等信息科技领域起着举足轻重的作用。在技术上,它是一个离散的一阶差分算子,用来计算图像亮度函数的一阶梯度之近似值。在图像的任何一点使用此算子,将会产生该点对应的梯度矢量

  • 水平梯度 $$ G_x = \left[ \begin{matrix} -1 & 0 & 1 \ -2 & 0 & 2 \ -1 & 0 & 1 \end{matrix} \right] $$

  • 垂直梯度

G_y= \left[ \begin{matrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{matrix} \right]
  • 合成
G=\sqrt{G_x^2+G_y^2}

为了提高计算机效率我们通常会使用: G = |Gx|+|Gy|

这里我们使用sobel卷积算子来查看脑干图像

图片顺序: 原图---> x 方向sobel ----> y 方向sobel ----> xy合在一起

示例代码

 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
import cv2 as cv

img = cv.imread("./assets/brain.jpg",cv.IMREAD_GRAYSCALE)
cv.imshow("src",img)

# sobel算子  参数1:图像, 参数2:图像的深度 -1表示和原图相同, 参数3: x方向求导的阶数 参数4: y方向求导的阶数
x_sobel = cv.Sobel(img, cv.CV_32F, 1, 0)
# 将图像转成8位int
x_sobel = cv.convertScaleAbs(x_sobel)

cv.imshow("x sobel",x_sobel)


# sobel算子
y_sobel = cv.Sobel(img, cv.CV_16S, 0, 1)
# 将图像转成8位int
y_sobel = cv.convertScaleAbs(y_sobel)
cv.imshow("y_sobel",y_sobel)


# 将x,y方向的内容叠加起来
x_y_sobel = cv.addWeighted(x_sobel, 0.5, y_sobel, 0.5,0)
cv.imshow("x,y sobel",x_y_sobel)

cv.waitKey(0)
cv.destroyAllWindows()

由于使用Sobel算子计算的时候有一些偏差, 所以opencv提供了sobel的升级版Scharr函数,计算比sobel更加精细.

下面是使用Scharr计算出来的边缘图像

示例代码

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

img = cv.imread("./assets/brain.jpg",cv.IMREAD_GRAYSCALE)
cv.imshow("src",img)

# sobel算子
x_scharr = cv.Scharr(img, cv.CV_32F, 1, 0)
# 将图像转成8位int
x_scharr = cv.convertScaleAbs(x_scharr)
cv.imshow("x scharr",x_scharr)

# # sobel算子
y_scharr = cv.Scharr(img, cv.CV_16S, 0, 1)
# 将图像转成8位int
y_scharr = cv.convertScaleAbs(y_scharr)
cv.imshow("y scharr",y_scharr)


# 将x,y方向的内容叠加起来
xy_scharr = cv.addWeighted(x_scharr, 0.5, y_scharr, 0.5,0)
cv.imshow("x,y scharr",xy_scharr)

cv.waitKey(0)
cv.destroyAllWindows()

拉普拉斯算子#

通过拉普拉斯变换后增强了图像中灰度突变处的对比度,使图像中小的细节部分得到增强,使图像的细节比原始图像更加清晰。

  • 普通
\left[ \begin{matrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{matrix} \right]
  • 增强型
\left[ \begin{matrix} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \end{matrix} \right]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import cv2 as cv

img = cv.imread("./assets/grbq.jpg",cv.IMREAD_GRAYSCALE)
cv.imshow("src",img)

# 使用拉普拉斯算子
dst = cv.Laplacian(img,cv.CV_32F)
# 取绝对值,将数据转到uint8类型
dst = cv.convertScaleAbs(dst)

cv.imshow("dst",dst)

cv.waitKey(0)
cv.destroyAllWindows();

canny边缘检测算法#

Canny算法由John F.Canny于1986年开发,是很常用的边缘检测算法。

它是一种多阶段算法,内部过程共4个阶段:

  1. 噪声抑制(通过Gaussianblur高斯模糊降噪):使用5x5高斯滤波器去除图像中的噪声
  2. 查找边缘的强度及方向(通过Sobel滤波器)
  3. 应用非最大信号抑制(Non-maximum Suppression): 完成图像的全扫描以去除可能不构成边缘的任何不需要的像素
  4. 高低阈值分离出二值图像(Hysteresis Thresholding)
  5. 高低阈值比例为T2:T1 = 3:1 / 2:1
  6. T2为高阈值,T1为低阈值

canny-edge.png

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import cv2 as cv
import numpy as np
import random

# 将图片数据读取进来
img = cv.imread("img/itheima.jpg",cv.IMREAD_COLOR)
cv.imshow("img",img)

# 1. 将图片转成灰度图片
grayImg = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# 2. canny算法
dstImg = cv.Canny(grayImg,50,180)


# 显示效果图
cv.imshow('dstimg',dstImg)
cv.waitKey(0)

双边滤波#

双边滤波其综合了高斯滤波器和α-截尾均值滤波器的特点,同时考虑了空间域与值域的差别,而Gaussian Filter和α均值滤波分别只考虑了空间域和值域差别。高斯滤波器只考虑像素间的欧式距离,其使用的模板系数随着和窗口中心的距离增大而减小;α-截尾均值滤波器则只考虑了像素灰度值之间的差值,去掉α%的最小值和最大值后再计算均值。

在opencv中,双边滤波的api已经提供好啦!

1
2
3
4
5
6
cv.bilateralFilter(输入图像, d, sigmaColor, sigmaSpace)

src: 输入图像 
d: 表示在过滤过程中每个像素邻域的直径范围如果这个值是非正数则函数会从sigmaSpace计算该值 
sigmaColor: 颜色空间过滤器的sigma值这个参数的值越大表明该像素邻域内有越宽广的颜色会被混合到一起产生较大的半相等颜色区域
sigmaSpace: 坐标空间中滤波器的sigma值如果该值较大则意味着越远的像素将相互影响从而使更大的区域中足够相似的颜色获取相同的颜色.

双边滤波器可以很好的保存图像边缘细节而滤除掉低频分量的噪音,但是双边滤波器的效率不是太高,花费的时间相较于其他滤波器而言也比较长。

1
2
3
4
5
6
7
8
9
import cv2 as cv
# 将图片数据读取进来
img = cv.imread("img/timg.jpg",cv.IMREAD_COLOR)
cv.imshow('img',img)
# 双边滤波
dstImg = cv.bilateralFilter(img, 10, 50, 50)
# 显示改变之后的图像
cv.imshow('newimg',dstImg)
cv.waitKey(0)

补充知识点#

锐化滤波#

图像的锐化和边缘检测很像,首先找到边缘,然后把边缘加到原来的图像上面,这样就强化了图像的边缘,使图像看起来更加锐利了 $$ G =\left[ \begin{matrix} -1 & -1 & -1 \-1 & 9 & -1 \-1 & -1 & -1 \end{matrix} \right] $$ 实际上是计算当前点和周围点的差别,然后将这个差别加到原来的位置上。另外,中间点的权值要比所有的权值和大于1,意味着这个像素要保持原来的值。

我们还可以按照公式来创建锐化滤波核: $$ G =\left[\begin{matrix}-k & -k & -k \-k & 8k+1 & -k\-k & -k & -k\end{matrix}\right] $$

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import cv2 as cv
import numpy as np


img = cv.imread("./assets/hehua.jpg",cv.IMREAD_COLOR)
cv.imshow("src",img)

kernel = np.array([
                    [-1,-1,-1],
                    [-1,9,-1],
                    [-1,-1,-1]])

dst = cv.filter2D(img,-1,kernel)

cv.imshow("sharpness filter",dst)

cv.waitKey(0)
cv.destroyAllWindows()

霍夫变换#

霍夫直线变换#

霍夫直线变换(Hough Line Transform)用来做直线检测

霍夫直线变换官网文档

coordinate system

Hough Transform Demo

为了加升大家对霍夫直线的理解,我在左图左上角大了一个点,然后在右图中绘制出来经过这点可能的所有直线

绘制经过某点的所有直线的示例代码如下,这个代码可以直接拷贝运行

 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
import cv2 as cv
import matplotlib.pyplot as plt
import numpy as np

def draw_line():
    # 绘制一张黑图
    img = np.zeros((500, 500, 1), np.uint8)
    # 绘制一个点
    cv.line(img, (10, 10), (10, 10), (255), 1)
    cv.imshow("line",img)
    return img

def hough_lines(img):
    rho = 1;
    theta = np.pi/180
    threshold=0
    lines = cv.HoughLines(img,rho, theta, threshold)

    dst_img = img.copy()

    for line in lines[:,0]:
        rho,theta = line
        a = np.cos(theta)
        b = np.sin(theta)
        x = a*rho
        y=b*rho

        x1 = int(np.round(x + 1000*(-b)))
        y1 = int(np.round(y + 1000*a))

        x2 = int(np.round(x - 1000*(-b)))
        y2 = int(np.round(y - 1000*a))

        cv.line(dst_img,(x1,y1),(x2,y2),(255,0,0),1)

    cv.imshow("li",dst_img)

img = draw_line()
hough_lines(img)

cv.waitKey(0)
cv.destroyAllWindows()

寻找棋盘中的直线#

示例代码如下:

 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
import cv2 as cv
import matplotlib.pyplot as plt
import numpy as np

# 1. 将图片以灰度的方式读取进来
img = cv.imread("assets/weiqi.jpg", cv.IMREAD_COLOR)
cv.imshow("src",img)

gray_img = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# cv.imshow("gray",gray_img)
# 
flag,thresh_img = cv.threshold(gray_img,100,255,cv.THRESH_BINARY_INV)
cv.imshow("thresh_img",thresh_img)


# 3. 霍夫变换
#  线段以像素为单位的距离精度,double类型的,推荐用1.0
rho = 1
# 线段以弧度为单位的角度精度,推荐用numpy.pi/180
theta = np.pi/180
# 累加平面的阈值参数,int类型,超过设定阈值才被检测出线段,值越大,基本上意味着检出的线段越长,检出的线段个数越少。
threshold=10
# 线段以像素为单位的最小长度
min_line_length=25
# 同一方向上两条线段判定为一条线段的最大允许间隔(断裂),超过了设定值,则把两条线段当成一条线段,值越大,允许线段上的断裂越大,越有可能检出潜在的直线段
max_line_gap = 3

lines = cv.HoughLinesP(thresh_img,rho,theta,threshold,minLineLength=min_line_length,maxLineGap=max_line_gap)

dst_img = img.copy()

for line in lines:
    x1,y1,x2,y2 = line[0]
    cv.line(dst_img,(x1,y1),(x2,y2),(0,0,255),2)

cv.imshow("dst img",dst_img)

cv.waitKey(0)
cv.destroyAllWindows()

霍夫圆#

一个圆可以由以下公式表示 $$ (x - x_0)^2 + (y - y_0)^2=r^2 $$ ,其中 $$ (x_0, y_0) $$ 是圆心,r是半径。圆环需要3个参数来确定,所以进行圆环检测的累加器必须是三维的,这样效率就会很低,因此OpenCV使用了霍夫梯度法这个巧妙的方法,来使用边界的梯度信息,从而提升计算的效率。

使用步骤:

  1. 霍夫圆检测对噪声敏感,先对对象做中值滤波
  2. 检测边缘,先把可能的圆边缘过滤出来
  3. 根据边缘得到最大概率的圆心,进而得到最佳半径

hough_circle.jpg

OpenCV的Logo检测结果:

Hough Circles

  • 参数及代码
 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
def hough_circle(img):
    img_copy = img.copy()
    # 中值滤波降噪
    img_copy = cv2.GaussianBlur(img_copy, (3,3), 0)
    img_copy = cv2.medianBlur(img_copy, 5)


    gray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY)
    cv2.imshow("gray", gray)

    """
    @param 8-bit 单通道图片
    @param method 检测方法, 当前只有cv2.HOUGH_GRADIENT
    @param dp 累加器分辨率和图像分辨率的反比例, 例如:
        如果 dp=1 累加器和输入图像分辨率相同. 
        如果 dp=2 累加器宽高各为输入图像宽高的一半相同. 
    @param minDist 检测到圆的圆心之间的最小距离。
        如果参数太小,除了真实的一个之外,可能错误地检测到多个相邻的圆圈。
        如果参数太大,可能会遗漏一些圆
    @param param1 参数1。它是两个传递给Canny边缘检测器的较高阈值(较低的阈值是此值的一半)
    @param param2 参数2, 它是检测阶段圆心的累加器阈值。
        它越小,会检测到更多的假圆圈。较大累加器值对应的圆圈将首先返回。
    @param minRadius 最小圆半径.
    @param maxRadius 最大圆半径. 
        如果<=0, 会使用图片最大像素值
        如果< 0, 直接返回圆心, 不计算半径
    """
    # ../images/coins.jpg
    circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT,
                               dp = 1,
                               minDist = 50,
                               param1=160,
                               param2=50,
                               minRadius=0,
                               maxRadius=100)

    circles = np.uint16(np.around(circles))
    for i in circles[0, :]:
        # draw the outer circle
        cv2.circle(img_copy, (i[0], i[1]), i[2], (0, 255, 0), 2)
        # draw the center of the circle
        cv2.circle(img_copy, (i[0], i[1]), 2, (0, 0, 255), 3)

    cv2.imshow("detected circles", img_copy)

寻找棋盘中的棋子#

 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
import cv2 as cv
import numpy as np


img = cv.imread("assets/weiqi.jpg", cv.IMREAD_COLOR)
cv.imshow("src",img)
# 将图片转成灰色图片
gray_img = cv.cvtColor(img,cv.COLOR_BGR2GRAY)


# 霍夫圆形检测
def hough_circle(gray_img):
    # 定义检测图像中圆的方法。目前唯一实现的方法是cv2.HOUGH_GRADIENT
    method = cv.HOUGH_GRADIENT
    # 累加器分辨率与图像分辨率的反比。例如,如果dp = 1,则累加器具有与输入图像相同的分辨率。如果dp = 2,则累加器的宽度和高度都是一半。
    dp = 1
    # 检测到的圆的圆心之间最小距离。如果minDist太小,则可能导致检测到多个相邻的圆。如果minDist太大,则可能导致很多圆检测不到。
    minDist = 20
    # param1 Canny算法阈值上线
    # param2 cv2.HOUGH_GRADIENT方法的累加器阈值。阈值越小,检测到的圈子越多。
    # minRadius : 最小的半径,如果不确定,则不指定
    # maxRadius : 最大的半径,若不确定,则不指定
    circles = cv.HoughCircles(gray_img,method,dp,minDist=minDist,param1=70,param2=30,minRadius=0,maxRadius=20)

    for circle in circles[0,:]:
        # 圆心坐标,半径
        x,y,r = circle
        # 绘制圆心
        cv.circle(img,(x,y),2,(0,255,0),1)
        # 绘制圆形
        cv.circle(img,(x,y),r,(0,0,255),2)

    cv.imshow("result",img)

# 调用函数,寻找霍夫圆
hough_circle(gray_img)

cv.waitKey(0)
cv.destroyAllWindows()

边缘与轮廓#

  • 基于图像边缘提取或二值化的基础寻找对象轮廓
  • 边缘提取的阈值会最终影响轮廓发现的结果
  • 主要API有以下两个
  • findContours发现轮廓
  • drawContours绘制轮廓

查找轮廓#

1
2
3
处理的图像,轮廓列表,继承关系 = cv.findContours(图像,轮廓检索模式,检索的方法)

# hierarchy[i][3],分别表示第i个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号
  • 轮廓检索模式
RETR_EXTERNAL 只检测最外层轮廓
RETR_LIST 提取所有轮廓,并放置在list中,检测的轮廓不建立等级关系
RETR_CCOMP 提取所有轮廓,并将轮廓组织成双层结构(two-level hierarchy),顶层为连通域的外围边界,次层位内层边界
RETR_TREE 提取所有轮廓并重新建立网状轮廓结构
  • 轮廓检索算法
CHAIN_APPROX_NONE 获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1
CHAIN_APPROX_SIMPLE 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的重点坐标,如果一个矩形轮廓只需4个点来保存轮廓信息
CHAIN_APPROX_TC89_L1 Teh-Chinl链逼近算法
CHAIN_APPROX_TC89_KCOS Teh-Chinl链逼近算法

绘制轮廓#

1
cv.drawContours(图像,轮廓列表,轮廓索引-1则绘制所有,轮廓颜色,轮廓的宽度)

绘制外切圆#

1
((x,y),radius) = cv.minEnclosingCircle(contour)

实现步骤:

  1. 读取图片
  2. 将图片转成一张灰色图片
  3. 对图片进行二值化处理
  4. 使用findContours查找轮廓
  5. 对轮廓进行处理
 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

def read_rgb_img(img_name):
    rgb_img = cv.imread(img_name,cv.IMREAD_COLOR)
    cv.imshow("rgb img",rgb_img)
    return rgb_img

def convert_rgb2gray(img):
    gray_img = cv.cvtColor(img,cv.COLOR_BGR2GRAY)

    cv.imshow("gray img", gray_img)
    return gray_img

def convert_gray2binary(img):
    binary_img = cv.adaptiveThreshold(img,
                                      255,
                                      cv.ADAPTIVE_THRESH_GAUSSIAN_C,
                                      cv.THRESH_BINARY,5,2)
    # _,binary_img = cv.threshold(img,50,255,cv.THRESH_BINARY_INV)
    cv.imshow("binary img", binary_img)
    return binary_img

def getContours(img):
    _,contours,hierarchy = cv.findContours(img,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE)
    print(contours,hierarchy)
    return contours


def draw_contours(img,contours):
    index = -1 # 所有的轮廓
    thickness = 2 # 轮廓的宽度
    color = (255,125,125) # 轮廓的颜色
    cv.drawContours(img,contours,index,color,thickness)
    cv.imshow('draw contours',img)


if __name__ == '__main__':
    img_name = "assets/shape0.jpg"

    rgb_img = read_rgb_img(img_name)
    gray_img = convert_rgb2gray(rgb_img)
    binary_imgage = convert_gray2binary(gray_img)
    contours = getContours(binary_imgage)
    draw_contours(rgb_img,contours)

    cv.waitKey(0)
    cv.destroyAllWindows()

网球案例#

实现步骤:

  1. 读取图片
  2. 过滤出球的颜色
  3. 使用轮廓检测
  4. 找到球的中心点
  5. 展示信息
 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import cv2 as cv
import numpy as np

def read_rgb_img(img_name):
    rgb_img = cv.imread(img_name,cv.IMREAD_COLOR)
    cv.imshow("rgb img",rgb_img)
    return rgb_img


def convert_rgb2gray(img):
    gray_img = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
    # 采用高斯滤波去掉噪点
    gray_img = cv.GaussianBlur(gray_img,(5,5),0)
    cv.imshow("gray img", gray_img)

    return gray_img

def convert_gray2binary(img):
    binary_img = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY_INV,5,2)
    cv.imshow("binary img", binary_img)
    return binary_img

def filter_tenis(img,lower_color,upper_color):
    hsv_img = cv.cvtColor(img,cv.COLOR_BGR2HSV)
    # 查找颜色
    mask_img = cv.inRange(hsv_img, lower_color, upper_color)
    cv.imshow("mask img",mask_img)
    return mask_img

def getContours(img):
    _,contours,hierarchy = cv.findContours(img,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
    print(contours,hierarchy)
    return contours


def process_tenis_contours(rgb_img,contours):
    black_img = np.zeros([rgb_img.shape[0],rgb_img.shape[1],3],np.uint8)

    for c in contours:
        # 计算面积
        area = cv.contourArea(c)
        # 该函数计算曲线长度或闭合轮廓周长。
        perimeter = cv.arcLength(c,True)
        # 获取最小的外切圆
        ((x,y),radius) = cv.minEnclosingCircle(c)

        # 绘制轮廓
        cv.drawContours(rgb_img,[c],-1,(150,250,150),2)
        cv.drawContours(black_img,[c],-1,(150,250,150),2)
        # 获取轮廓中心点
        # cx,cy = get_contour_center(c)
        # print(cx,cy)
        x = int(x)
        y = int(y)
        cv.circle(rgb_img,(x,y),int(radius),(0,0,255),2)
        cv.circle(black_img,(x,y),int(radius),(0,0,255),2)

        print("Area:{},primeter:{}".format(area,perimeter))

    print("number of contours:{}".format(len(contours)))
    cv.imshow("rgb img contours",rgb_img)
    cv.imshow("black img contours",black_img)

if __name__ == '__main__':
    img_name = "assets/tenis1.jpg"
    # 定义范围
    lower_color = (30, 120, 130)
    upper_color = (60, 255, 255)

    rgb_img = read_rgb_img(img_name)
    binary_imgage = filter_tenis(rgb_img,lower_color,upper_color)

    contours = getContours(binary_imgage)
    process_tenis_contours(rgb_img,contours)

    cv.waitKey(0)
    cv.destroyAllWindows()