OpenCV学习笔记

文章目录

  • OpenCV
    • 基本操作
      • 读取图像
      • 显示图像
      • 保存图像
      • waitKey()
      • 创建窗口
      • 销毁窗口
    • 图像处理基础
      • item()
      • itemset()
      • 通道拆分
      • 通道合并
      • 获取图像属性
    • 图像运算
      • 加法运算
      • 图像加权和
      • 按位逻辑运算
      • 位平面分解
      • 图像加密与解密
      • 数字水印
    • 色彩空间类型转换
      • cv2.cvtColor()
      • cv2.inRange(img, min, max)
      • HSV色彩空间
      • BGRA色彩空间
    • 几何变换
      • 缩放
      • 翻转
      • 仿射
      • 透视
      • 重映射
    • 阈值处理
      • threshold()
      • adaptiveThreshold()
      • Otsu处理
    • 图像平滑处理
      • 均值滤波
      • 方框滤波
      • 高斯滤波
      • 中值滤波
      • 双边滤波
      • 2D卷积
    • 形态学操作
      • 腐蚀
      • 膨胀
      • 通用形态学函数
      • 开运算
      • 闭运算
      • 形态学梯度运算
      • 顶帽运算
      • 黑帽运算
      • 核函数
    • 图像梯度
      • Sobel算子
      • Scharr算子
      • Laplacian算子
    • 图像金字塔
      • 向下采样
      • 向上采样
    • 图像轮廓
      • Canny边缘检测
      • 查找并绘制轮廓
      • 矩特征
      • Hu矩
      • 轮廓拟合
      • 凸包
      • 利用形状场景算法比较轮廓
      • 轮廓的特征值
    • 直方图处理
      • 绘制直方图
      • 直方图均衡化
    • 傅里叶变换
      • 用Numpy实现傅里叶变换
      • 用OpenCV实现傅里叶变换
    • 模板匹配
      • 模板匹配基础
      • 多模板匹配
    • 霍夫变换
      • 霍夫变换原理
      • 霍夫直线变换
      • 概率霍夫变换
      • 霍夫圆变换
    • 图像分割与提取
      • 距离变换函数
      • 标注图像
      • 分水岭算法实现图像分割与提取
      • 交互式前景提取
    • 视频处理
      • VideoCapture类
      • VideoWriter类
    • 绘画及交互
      • 绘画基础
      • 滚动条

OpenCV

本文基于《OpenCV轻松入门:面向Python》,作者李立宗。
在RGB图像中,图像是由R、G、B三个通道构成的,但是在OpenCV中,通道是按照B、G、R的顺序存储的。

基本操作

读取图像

img = cv2.imread('lena.png')

显示图像

cv2.imshow('show', img)

保存图像

cv2.imwrite('new_lena.png', img)

waitKey()

若没有按键被按下,则返回-1;如果有按键被按下,则返回该按键的ASCII码。

创建窗口

cv2.namedWindow('window')

销毁窗口

销毁指定窗口

cv2.destroyWindow('window')  # 销毁指定窗口
cv2.destroyAllWindows()  # 销毁所有窗口

图像处理基础

item()

用于访问图像的像素点。

对于灰度图

img.item(row, col)

对于RGB图像

img.item(row, col, channel)

itemset()

用于修改图像的像素值。

对于灰度图

img.itemset((row, col), value)

对于RGB图像

img.itemset((row, col, channel), value)

通道拆分

b, g, r = cv2.split(img)

通道合并

bgr = cv2.merge([b, g, r])

获取图像属性

img.shape  # 获取行数、列数、通道数
img.size  # 行数×列数×通道数
img.dtype  # 图像的数据类型

图像运算

加法运算

  • 使用"+"对图像a,b进行求和运算时,若两个图像对应像素值的和小于或等于255时,则直接得到运算结果;若两个图像对应像素值的和大于255,则将运算结果对256取模。

  • 使用cv2.add()函数进行求和运算时,若两个图像对应像素值的和小于或等于255时,则直接得到运算结果;若两个图像对应像素值的和大于255,则将运算结果为饱和值255。

cv2.add()参数的三种形式

cv2.add(img1,img2)
cv2.add(img,value)
cv2.add(value,img)

图像加权和

output=img1×alpha+img2×beta+gamma

output = cv2.addWeighted(img1, alpha, img2, beta, gamma)

按位逻辑运算

cv2.bitwise_and(img1, img2)  # 按位与
cv2.bitwise_or(img1, img2)  # 按位或
cv2.bitwise_xor(img1, img2)  # 按位异或
cv2.bitwise_not(img)  # 按位取反

位平面分解

  1. 图像预处理:读取图像,获取图像的行(row)和列(col)。
  2. 构造提取矩阵:建立一个值均为2n的矩阵作为提取矩阵,用来与图像进行按位与运算,以提取第n个位平面。
  3. 提取位平面:将灰度图像与提取矩阵进行按位与运算,得到各个位平面。
  4. 阈值处理:先将大于0的值处理为True,等于0的值处理为False,得到mask矩阵。再将位平面矩阵中对应mask矩阵中相应位置为True的值替换为255。
  5. 显示图像:用for循环显示出图像。
img = cv2.imread('lena.png', 0)  # 将图像调整为灰度图
cv2.imshow('lena', img)
row, col = img.shape
x = np.zeros((row, col, 8), dtype=np.uint8)  # 用于提取各个位平面
for i in range(8):
    x[:, :, i] = 2 ** i
r = np.zeros((row, col, 8), dtype=np.uint8)
for i in range(8):
    r[:, :, i] = cv2.bitwise_and(img, x[:, :, i])  # 提取位平面
    mask = r[:, :, i] > 0  # 若值大于0则为True,否则为False
    r[mask] = 255  # 若为True则将值替换为255
    cv2.imshow(str(i), r[:, :, i])

图像加密与解密

通过对原始图像与密钥图像进行按位异或,可以实现加密;将加密后的图像与密钥图像再进行按位异或,可以实现解密。

img = cv2.imread('lena.png', 0)  # 将图像调整为灰度图
key = np.random.randint(0, 256, size=[512, 512], dtype=np.uint8)  # 生成密钥图像
encryption = cv2.bitwise_xor(img, key)  # 加密
decryption = cv2.bitwise_xor(encryption, key)  # 解密
cv2.imshow('encryption', encryption)
cv2.imshow('decryption', decryption)

数字水印

将图像的最低有效位层替换为需要隐藏的二值图像,可以实现将二值图像隐藏的目的。由于二值图像处于图像的最低有效位上,所以对图像的影响非常不明显。

img = cv2.imread('image/lena.png', 0)  # 将图像调整为灰度图
waterMark = cv2.imread('image/cmh.png', 0)  # 读取水印图像
w = waterMark[:, :] > 0  # 若值大于0则为True,否则为False
waterMark[w] = 1  # 若为True则将值替换为1
row, col = img.shape
array1 = np.ones([row, col], np.uint8)  # 构造值为1的矩阵
array254 = np.ones([row, col], np.uint8) * 254  # 构造值为254的矩阵
imgHigh7 = cv2.bitwise_and(img, array254)  # 取出图像的高7位

encryption = cv2.bitwise_or(imgHigh7, waterMark)  # 加密
decryption = cv2.bitwise_and(encryption, array1)  # 解密
d = decryption[:, :] > 0  # 若值大于0则为True,否则为False
decryption[d] = 255  # 若为True则将值替换为255
cv2.imshow('encryption', encryption)
cv2.imshow('decryption', decryption)

色彩空间类型转换

cv2.cvtColor()

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # 将图像转换为灰度图
HSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)  # 将图像转换为HSV空间

cv2.inRange(img, min, max)

通过函数cv2.inRange()来判断图像内像素点的像素值是否在指定的范围内。如果处于该指定区间内,则对应位置上的值为255,如果不处于该指定区间内,则对应位置上的值为0。

mask = cv2.inRange(img, 100, 200)  # 单通道图像

minValue = np.array([100, 100, 100])
maxValue = np.array([200, 200, 200])
mask = cv2.inRange(img, minValue, maxValue)  # 多通道图像

HSV色彩空间

H:色调(Hue,也成为色相)

S:饱和度(Saturation)

V:亮度(Value)

  • 色调H:取值范围是[0,360],在OpenCV中,可以直接把色调的值除以2,得到[0,180]之间的值,以适应8位二进制的存储和表示范围。

  • 饱和度S:取值范围是[0,1],灰度颜色的饱和度值为0。

  • 亮度V:取值范围是[0,1]。

BGRA色彩空间

A通道,也叫alpha通道,表示透明度,取值范围是[0,255],表示从透明到不透明。

bgra255 = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)  # 转换为BGRA色彩空间
b, g, r, a = cv2.split(bgra255)  # 分离b,g,r,a
a[:, :] = 125
bgra125 = cv2.merge([b, g, r, a])
a[:, :] = 0
bgra0 = cv2.merge([b, g, r, a])
cv2.imwrite('image/bgra255.png', bgra255)
cv2.imwrite('image/bgra125.png', bgra125)
cv2.imwrite('image/bgra0.png', bgra0)

几何变换

缩放

cv2.resize(img, dsize, fx, fy, interpolation)

在cv2.resize()函数中,图像的大小可以通过"dsize"或者"fx"、"fy"二者之一来指定。

  • 通过dsize指定:

如果指定参数dsize的值,无论是否指定了参数fx和fy的值,都由dsize来决定目标图像的大小。需要注意的是,dsize内的第一个参数对应缩放后图像的宽度(width,即列数col,与参数fx相关),第二个参数对应缩放后图像的高度(height,即行数row,与参数fy相关)。

  • 通过参数fx和fy指定:

如果参数dsize的值是None,那么目标图像的大小通过参数fx和fy来决定。

当缩小图像时,使用区域插值方式(cv2.INTER_AREA)能够得到最好的效果。当放大图像时,使用三次样条插值(cv2.INTER_CUBIC)方式和双线性插值(cv2.INTER_LINEAR)方式都能取得较好的效果。三次样条插值方式速度较慢。双线性插值方式速度相对较快且效果并不逊色,双线性插值方式是默认方式。

翻转

cv2.flip(img, flipCode)

flipCode参数的意义:

  • 0:绕x轴旋转

  • 正数:绕y轴旋转

  • 负数:绕x轴,y轴同时旋转

仿射

cv2.warpAffine(img, M, dsize)

  • M代表一个2×3的变换矩阵

  • dsize代表输出图像的尺寸大小

output(x,y)=img(M11x+M12y+M13,M21x+M22y+M23)

  1. 平移:

    将原始图像img向右移动100个像素,向下移动200个像素,则其对应关系为:

    output(x,y)=img(x+100,y+200),得到M=[[1,0,100],[0,1,200]]

    height, width = img.shape[:2]
    M = np.float32([[1, 0, 100], [0, 1, 200]])  # 构造平移矩阵
    move = cv2.warpAffine(img, M, (width, height))  # 平移
    cv2.imshow('original', img)
    cv2.imshow('move', move)
    
  2. 旋转:

    进行旋转之前可以先通过**cv2.getRotationMatrix2D()**函数获取转换矩阵。该函数语法格式为:

    retval = cv2.getRotationMatrix2D(center, angle, scale)

    • center:旋转的中心点

    • angle:旋转的角度,正数代表逆时针旋转,负数代表顺时针旋转

    • scale:变换尺度

    以图像中心为原点,逆时针旋转45°,并缩小为原始图像的0.6倍,代码如下:

    height, width = img.shape[:2]
    M = cv2.getRotationMatrix2D((height / 2, width / 2), 45, 0.6)  # 构造旋转矩阵
    rotate = cv2.warpAffine(img, M, (width, height))
    cv2.imshow('original', img)
    cv2.imshow('rotate', rotate)
    
  3. 更复杂的仿射变换:

    对于更复杂的仿射变换,可以用**cv2.getAffineTransform()**函数生成转换矩阵M。该函数语法格式为:

    retval = cv2.getAffineTransform(pts1, pts2)

    • pts1代表输入图像的三个点坐标

    • pts2代表输出图像的三个点坐标

    在该函数中,pts1、pts2都是包含三个二维数组(x,y)点的数组。上述参数通过函数定义了两个平行四边形。pts1、pts2的三个点分别对应平行四边形的左上角、右上角、左下角三个点。

    row, col = img.shape[:2]
    pts1 = np.float32([[0, 0], [col - 1, 0], [0, row - 1]])
    pts2 = np.float32([[0, row * 0.33], [col * 0.85, row * 0.25], [col * 0.15, row * 0.7]])
    M = cv2.getAffineTransform(pts1, pts2)
    output = cv2.warpAffine(img, M, (col, row))
    cv2.imshow('original', img)
    cv2.imshow('result', output)
    

透视

仿射可以将矩形映射为任意平行四边形,透视变换可以将矩形映射为任意四边形。

cv2.warpPerspective(img, M, dsize)

  • M代表一个3×3的变换矩阵

可以使用**cv2.getPerspectiveTransform()**函数生成转换矩阵M。该函数语法格式为:

retval = cv2.getPerspectiveTransform(pts1, pts2)

  • pts1代表输入图像的四个顶点的坐标
  • pts2代表输出图像的四个顶点的坐标
img = cv2.imread('image/perspective.bmp')
row, col = img.shape[:2]
pts1 = np.float32([[150, 50], [400, 50], [60, 450], [310, 450]])
pts2 = np.float32([[50, 50], [row - 50, 50], [50, col - 50], [row - 50, col - 50]])
M = cv2.getPerspectiveTransform(pts1, pts2)  # 构造透视变换矩阵
output = cv2.warpPerspective(img, M, (col, row))  # 透视
cv2.imshow('original', img)
cv2.imshow('perspective', output)

重映射

把一幅图像内的像素点放置到另外一幅图像内的指定位置,这个过程叫做重映射。

cv2.remap(img, map1, map2, interpolation)

  • map1参数有两种可能的值:

    • 表示(x,y)点的一个映射

    • 表示CV_16SC2,CV_32FC1,CV_32FC2类型(x,y)点的x值

  • map2参数同样有两种可能的值:

    • 当map1表示(x,y)时,该值为空
    • 当map1表示(x,y)点的x值时,该值是CV_16UC1,CV_32FC1类型(x,y)点的y值
  • interpolation表示插值方式

阈值处理

threshold()

retval, output = cv2.threshold(img, thresh, maxval, type)

  • retval代表返回的阈值
  • output代表阈值分割结果图像
  • img代表要进行阈值分割的图像
  • thresh代表要设定的阈值
  • maxval代表当type参数为THRESH_BINARY或者THRESH_BINARY_INV类型时,需要设定的最大值
  • type代表阈值分割的类型
阈值分割类型 阈值分割类型可视化表示
img = cv2.imread('image/lena.png', 0)
thresh1, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)  # 二值化阈值处理
thresh2, binary_inv = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)  # 反二值化阈值处理
thresh3, trunc = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC)  # 截断阈值化处理
thresh4, toZero_inv = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV)  # 超阈值零处理
thresh5, toZero = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO)  # 低阈值零处理
cv2.imshow('BINARY', binary)
cv2.imshow('BINARY_INV', binary_inv)
cv2.imshow('TRUNC', trunc)
cv2.imshow('TOZERO_INV', toZero_inv)
cv2.imshow('TOZERO', toZero)

adaptiveThreshold()

可以使用**cv2.adaptiveThreshold()**来实现自适应阈值处理,该函数的语法格式为:

cv2.adaptiveThreshold(img, maxValue, adaptiveMethod, thresholdType, blockSize, C)

  • maxValue代表最大值

  • adaptiveMethod代表自适应方法

  • thresholdType代表阈值处理方式,该值必须是cv2.THRESH_BINARY或者cv2.THRESH_BINARY_INV中的一个

  • blockSize代表块大小。表示一个像素在计算其阈值时所使用的邻域尺寸。

  • C是常量

自适应阈值等于每个像素由参数blockSize所指定邻域的加权平均值减去常量C。adaptiveMethod参数包含两种不同的方法:

  • cv2.ADAPTIVE_THRESH_MEAN_C:邻域所有像素点的权重值是一样的
  • cv2.ADAPTIVE_THRESH_GAUSSIAN_C:权重值与邻域各个像素点到中心点的距离有关,通过高斯方程得到各个点的权重值
img = cv2.imread('image/computer.jpg', 0)
thresh1, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)  # 二值化阈值处理
mean = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 3)
gaussian = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 5, 3)
cv2.imshow('BINARY', binary)
cv2.imshow('MEAN', mean)
cv2.imshow('GAUSSIAN', gaussian)

Otsu处理

Otsu方法能根据当前图像给出最佳的类间分割阈值。Otsu方法会遍历所有可能阈值,从而找到最佳的阈值。

通过在函数cv2.threshold()中对参数type的类型多传递一个参数cv2.THRESH_OTSU,即可实现Otsu方法的阈值分割。在使用Otsu方法时,要把阈值设为0

img = cv2.imread('image/lena.png', 0)
thresh, otsu = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)  # Otsu处理
cv2.imshow('OTSU', otsu)

图像平滑处理

均值滤波

均值滤波是指用当前像素点周围N×N个像素值的均值来代替当前的像素值。

cv2.blur(img, ksize, anchor, borderType)

  • img是需要处理的图像,它可以有任意数量的通道,并能对各个通道独立处理。
  • ksize是滤波核的大小。滤波核大小是指在均值处理中,其邻域图像的高度和宽度。
  • anchor是锚点,其默认值为(-1,-1),表示当前计算均值的点位于核的中心点位置。
  • borderType是边界样式,该值决定了以何种方式处理边界。

函数**cv2.blur()**的一般形式为:cv2.blur(img, ksize)

img = cv2.imread('image/lenaNoise.png')
output = cv2.blur(img, (5, 5))  # 均值滤波
cv2.imshow('original', img)
cv2.imshow('blur', output)

方框滤波

方框滤波可以自由选择是否对均值滤波的结果进行归一化,即可以自由选择滤波结果是邻域像素值之和的平均值,还是邻域像素值之和。

cv2.boxFilter(img, ddepth, ksize, anchor, normalize, borderType)

  • ddepth是处理结果图像的图像深度,一般使用-1表示与原始图像使用相同的图像深度。

  • ksize是滤波核的大小。滤波核大小是指在滤波处理中,其邻域图像的高度和宽度。

  • anchor是锚点,其默认值为(-1,-1),表示当前计算均值的点位于核的中心点位置。

  • normalize表示在滤波时是否进行归一化处理,默认值为1

    • normalize=1时,表示要进行归一化处理,要用邻域像素值之和除以面积
    • normalize=0时,表示不需要进行归一化处理,直接使用邻域像素值之和
  • borderType是边界样式,该值决定了以何种方式处理边界。

函数**cv2.boxFilter()**的一般形式为:cv2.boxFilter(img, ddepth, ksize)

img = cv2.imread('image/lenaNoise.png')
output = cv2.boxFilter(img, -1, (5, 5))  # 方框滤波
cv2.imshow('original', img)
cv2.imshow('boxFilter', output)

高斯滤波

在进行均值滤波和方框滤波时,其邻域内每个像素的权值是相等的。在高斯滤波中,会将中心点的权重值加大,远离中心点的权重值减小。

cv2.GaussianBlur(img, ksize, sigmaX, sigmaY, borderType)

  • ksize是滤波核的大小。滤波核大小是指在滤波处理中,其邻域图像的高度和宽度。ksize的宽度和高度可以不相同,但是它们必须是奇数

  • sigmaX是卷积核在X轴方向(水平方向)的标准差

  • sigmaY是卷积核在Y轴方向(垂直方向)的标准差。如果将该值设置为0,则只采用sigmaX的值;如果sigmaX和sigmaY都是0,则通过ksize.width和ksize.height计算得到

    • sigmaX=0.3×[(ksize.width-1)×0.5-1]+0.8
    • sigmaY=0.3×[(ksize.height-1)×0.5-1]+0.8
  • borderType是边界样式,该值决定了以何种方式处理边界

函数**cv2.GaussianBlur()**的一般形式为:cv2.GaussianBlur(img, ksize, 0, 0)

img = cv2.imread('image/lenaNoise.png')
output = cv2.GaussianBlur(img, (5, 5), 0, 0)  # 高斯滤波
cv2.imshow('original', img)
cv2.imshow('GaussianBlur', output)

中值滤波

中值滤波用邻域内所有像素值的中间值来代替当前像素点的像素值。

cv2.medianBlur(img, ksize)

  • ksize是滤波核的大小。滤波核大小是指在滤波处理中,其邻域图像的高度和宽度。核大小必须是比1大的奇数
img = cv2.imread('image/lenaNoise.png')
output = cv2.medianBlur(img, 3)  # 中值滤波
cv2.imshow('original', img)
cv2.imshow('medianBlur', output)

双边滤波

前述滤波方式基本只考虑了空间的权重信息,计算起来比较方便,但是在边缘信息的处理上存在较大的问题。双边滤波在计算某一个像素点的新值时,不仅考虑距离信息(距离越远,权重越小),还考虑色彩信息(色彩差别越大,权重越小)。双边滤波综合考虑距离和色彩的权重结果,既能够有效的去除噪声,又能够较好的保护边缘信息。

cv2.bilateralFilter(img, d, sigmaColor, sigmaSpace)

  • d是在滤波时选取的空间距离参数,这里表示以当前像素点为中心点的直径。如果该值为非正数,则会自动从参数sigmaSpace计算得到。推荐d=5。
  • sigmaColor是滤波处理时选取的颜色差值范围,该值决定了周围哪些像素点能够参与到滤波中来。与当前像素点的像素值差值小于sigmaColor的像素点,能够参与到当前的滤波中。
  • sigmaSpace是坐标空间中的sigma值。它的值越大,说明有越多的点能够参与到滤波计算中来。当d>0时,无论sigmaSpace的值如何,d都指定邻域大小;否则,d与sigmaSpace的值成比例。

为了简单起见,可以将sigmaColor和sigmaSpace的值设置为相同的。一般情况下,10

img = cv2.imread('image/lenaNoise.png')
output = cv2.bilateralFilter(img, 25, 100, 100)  # 双边滤波
cv2.imshow('original', img)
cv2.imshow('bilateralFilter', output)

2D卷积

我们有时希望使用特定的卷积核实现卷积操作,OpenCV允许用户自定义卷积核实现卷积操作,使用OpenCV的自定义卷积核实现卷积操作的函数是cv2.filter2D()

cv2.filter2D(img, ddepth, kernel)

  • ddepth是处理结果图像的图像深度,一般使用-1表示与原始图像使用相同的图像深度。
  • kernel是卷积核,是一个单通道的数组。如果想在处理彩色图像时让每个通道使用不同的核,则必须将彩色图像分解后使用不同的核完成操作。
kernel = np.ones((9, 9), np.float32) / 81
output = cv2.filter2D(img, -1, kernel)
cv2.imshow('original', img)
cv2.imshow('filter2D', output)

形态学操作

腐蚀

腐蚀能够将图像的边界点消除,使图像沿着边界向内收缩,也可以将小于指定结构体元素的部分去除。

在腐蚀过程中,通常使用一个结构元来逐个像素地扫描要被腐蚀的图像,并根据结构元和被腐蚀图像的关系来确定腐蚀结果。

  • 如果结构元完全处于前景图像中,就将结构元中心点所对应的腐蚀结果图像中的像素点处理为前景色
  • 如果结构元未完全处于前景图像中,就将结构元中心点所对应的腐蚀结果图像中的像素点处理为背景色

cv2.erode(img, kernel)

  • kernel代表腐蚀操作时所采用的结构类型。它可以自定义生成,也可以通过函数**cv2.getStructuringElement()**生成。
img = cv2.imread('image/erode.bmp')
kernel = np.ones((10, 10), np.uint8)
output = cv2.erode(img, kernel)  # 腐蚀
cv2.imshow('original', img)
cv2.imshow('erode', output)

膨胀

膨胀操作能对图像的边界进行扩张。

  • 如果结构元中任意一点处于前景图像中,就将膨胀结果图像中对应像素点处理为前景色
  • 如果结构元完全处于背景图像外,就将膨胀结果图像中对应像素点处理为背景色
img = cv2.imread('image/dilation.bmp')
kernel = np.ones((10, 10), np.uint8)
output = cv2.dilate(img, kernel)  # 膨胀
cv2.imshow('original', img)
cv2.imshow('dilate', output)

通用形态学函数

cv2.morphologyEx(img, op, kernel)

  • op代表操作类型
类型 说明 含义 操作
cv2.MORPH_ERODE 腐蚀 腐蚀 erode(img)
cv2.MORPH_DILATE 膨胀 膨胀 dilate(img)
cv2.MORPH_OPEN 开运算 先腐蚀后膨胀 dilate(erode(img))
cv2.MORPH_CLOSE 闭运算 先膨胀后腐蚀 erode(dilate(img))
cv2.MORPH_GRADIENT 形态学梯度运算 膨胀图减腐蚀图 dilate(img)-erode(img)
cv2.MORPH_TOPHAT 顶帽运算 原始图像减开运算所得图像 img-open(img)
cv2.MORPH_BLACKHAT 黑帽运算 闭运算所得图像减原始图像 close(img)-img
cv2.MORPH_HITMISS 击中击不中 前景背景腐蚀运算的交集 intersection(erode(img),erode(img))

开运算

开运算先将图像腐蚀,再将所得图像膨胀。开运算可以用于去噪、计数等。

cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)

img1 = cv2.imread('image/opening1.bmp')
img2 = cv2.imread('image/opening2.bmp')
kernel = np.ones((10, 10), np.uint8)
output1 = cv2.morphologyEx(img1, cv2.MORPH_OPEN, kernel)  # 开运算
output2 = cv2.morphologyEx(img2, cv2.MORPH_OPEN, kernel)  # 开运算
cv2.imshow('img1', img1)
cv2.imshow('OPEN1', output1)
cv2.imshow('img2', img2)
cv2.imshow('OPEN2', output2)

闭运算

闭运算是先膨胀、后腐蚀的运算,它有助于关闭前景物体内部的小孔,或去除物体上的小黑点,还可以将不同的前景图像进行连接。

cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

img1 = cv2.imread('image/closing1.bmp')
img2 = cv2.imread('image/closing2.bmp')
kernel = np.ones((10, 10), np.uint8)
output1 = cv2.morphologyEx(img1, cv2.MORPH_CLOSE, kernel)  # 闭运算
output2 = cv2.morphologyEx(img2, cv2.MORPH_CLOSE, kernel, iterations=3)  # 闭运算,迭代3次
cv2.imshow('img1', img1)
cv2.imshow('CLOSE1', output1)
cv2.imshow('img2', img2)
cv2.imshow('CLOSE2', output2)

形态学梯度运算

形态学梯度运算是用图像的膨胀图像减腐蚀图像的操作,可以获取原始图像中前景图像的边缘。

img = cv2.imread('image/gradient.bmp')
kernel = np.ones((5, 5), np.uint8)
output = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)  # 形态学梯度运算
cv2.imshow('img', img)
cv2.imshow('GRADIENT', output)

顶帽运算

顶帽运算是用原始图像减去开运算所得图像的操作,能够获取图像的噪声信息,或者得到比原始图像的边缘更亮的边缘信息。

cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)

img1 = cv2.imread('image/tophat.bmp')
img2 = cv2.imread('image/lena.png')
kernel = np.ones((5, 5), np.uint8)
output1 = cv2.morphologyEx(img1, cv2.MORPH_TOPHAT, kernel)  # 顶帽运算
output2 = cv2.morphologyEx(img2, cv2.MORPH_TOPHAT, kernel)  # 顶帽运算
cv2.imshow('img1', img1)
cv2.imshow('TOPHAT1', output1)
cv2.imshow('img2', img2)
cv2.imshow('TOPHAT2', output2)

黑帽运算

黑帽运算是用闭运算图像减去原始图像的操作,能够获取图像内部的小孔,或者前景色中的小黑点,或者得到比原始图像的边缘更暗的边缘部分。

img1 = cv2.imread('image/blackhat.bmp')
img2 = cv2.imread('image/lena.png')
kernel = np.ones((5, 5), np.uint8)
output1 = cv2.morphologyEx(img1, cv2.MORPH_BLACKHAT, kernel)  # 黑帽运算
output2 = cv2.morphologyEx(img2, cv2.MORPH_BLACKHAT, kernel)  # 黑帽运算
cv2.imshow('img1', img1)
cv2.imshow('BLACKHAT1', output1)
cv2.imshow('img2', img2)
cv2.imshow('BLACKHAT2', output2)

核函数

在进行形态学操作时,必须使用一个特定的核,该核可以自定义生成,也可以通过函数**cv2.getStructuringElement()**生成。该函数的语法格式为:

cv2.getStructuringElement(shape, ksize)

  • shape代表形状类型
类型 说明
cv2.MORPH_RECT 矩形结构元素。所有元素值都是1
cv2.MORPH_CROSS 十字形结构元素。对角线元素值为1
cv2.MORPH_ELLIPSE 椭圆形结构元素
  • ksize代表结构元素的大小
kernel1 = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
kernel2 = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))
kernel3 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
print('kernel1=', kernel1)
print('kernel2=', kernel2)
print('kernel3=', kernel3)

图像梯度

Sobel算子

在OpenCV中,使用函数**cv2.Sobel()**实现Sobel算子运算,该函数的语法格式为:

cv2.Sobel(img, ddepth, dx, dy, [ksize])

  • ddepth代表输出图像的深度,通常设置为cv2.CV_64F

  • dx代表x方向上的求导阶数

  • dy代表y方向上的求导阶数

  • ksize代表Sobel核的大小,该值为-1时,则会使用Scharr算子进行运算

在实际操作中,计算梯度值可能会出现负数,如果处理的图像是8位图类型,则在ddepth为-1时,意味着指定运算结果也是8位图类型,那么所有负数会自动截断为0,发生信息丢失。所以,在运算时要先使用更高的数据类型cv2.CV_64F,再通过取绝对值将其映射为cv2.CV_8U(8位图)类型。所以通常将ddepth设置为cv2.CV_64F。

在OpenCV中,使用函数**cv2.convertScaleAbs()**对参数取绝对值,该函数的语法格式为:

cv2.convertScaleAbs(img[, alpha[, beta]])

  • alpha代表调节系数,默认为1
  • beta代表调节亮度值,默认为0

参数dx和dy通常的值为0或1,最大值为2。如果是0,表示在该方向上没有求导。dx和dy不能同时为0。

  • 计算x方向边缘(梯度):dx=1,dy=0
  • 计算y方向边缘(梯度):dx=0,dy=1
  • dx和dy的值均为1:dx=1,dy=1
  • 计算x方向和y方向的边缘叠加:通过组合方式实现
img = cv2.imread('image/sobel.bmp')
SobelX = cv2.Sobel(img, cv2.CV_64F, 1, 0)  # x方向边缘(梯度)
SobelX = cv2.convertScaleAbs(SobelX)  # x方向边缘(梯度)
SobelY = cv2.Sobel(img, cv2.CV_64F, 0, 1)  # y方向边缘(梯度)
SobelY = cv2.convertScaleAbs(SobelY)  # y方向边缘(梯度)
SobelXY = cv2.Sobel(img, cv2.CV_64F, 1, 1)  # dx和dy的值均为1
SobelXY = cv2.convertScaleAbs(SobelXY)  # dx和dy的值均为1
SobelXYAdd = cv2.addWeighted(SobelX, 0.5, SobelY, 0.5, 0)  # x方向和y方向的边缘叠加
cv2.imshow('original', img)
cv2.imshow('SobelX', SobelX)
cv2.imshow('SobelY', SobelY)
cv2.imshow('SobelXY', SobelXY)
cv2.imshow('SobelXYAdd', SobelXYAdd)

Scharr算子

Scharr算子具有和Sobel算子同样的速度,且精度更高。OpenCV提供了函数**cv2.Scharr()**来计算Scharr算子。其语法格式为:

cv2.Scharr(img, ddepth, dx, dy)

  • ddepth代表输出图像的深度,应该设置为cv2.CV_64F

  • dx代表x方向上的求导阶数

  • dy代表y方向上的求导阶数

**cv2.Scharr(img, ddepth, dx, dy)cv2.Sobel(img, ddepth, dx, dy, -1)**是等价的。

**cv2.Scharr()**中,要求dx和dy满足:dx>=0 && dy>=0 && dx+dy==1

  • 计算x方向边缘(梯度):dx=1,dy=0
  • 计算y方向边缘(梯度):dx=0,dy=1
  • 计算x方向和y方向的边缘叠加:通过组合方式实现
img = cv2.imread('image/scharr.bmp')
ScharrX = cv2.Scharr(img, cv2.CV_64F, 1, 0)  # x方向边缘(梯度)
ScharrX = cv2.convertScaleAbs(ScharrX)  # x方向边缘(梯度)
ScharrY = cv2.Scharr(img, cv2.CV_64F, 0, 1)  # y方向边缘(梯度)
ScharrY = cv2.convertScaleAbs(ScharrY)  # y方向边缘(梯度)
ScharrXYAdd = cv2.addWeighted(ScharrX, 0.5, ScharrY, 0.5, 0)  # x方向和y方向的边缘叠加
cv2.imshow('original', img)
cv2.imshow('ScharrX', ScharrX)
cv2.imshow('ScharrY', ScharrY)
cv2.imshow('ScharrXYAdd', ScharrXYAdd)

Laplacian算子

Laplacian算子是一种二阶导数算子,其具有旋转不变性,可以满足不同方向的图像边缘锐化(边缘检测)的要求。通常情况下,其算子的系数之和需要为0。

cv2.Laplacian(img, ddepth)

  • ddepth代表输出图像的深度
img = cv2.imread('image/laplacian.bmp')
Laplacian = cv2.Laplacian(img, cv2.CV_64F)
Laplacian = cv2.convertScaleAbs(Laplacian)  # Laplacian算子
cv2.imshow('original', img)
cv2.imshow('ScharrX', Laplacian)

图像金字塔

图像金字塔是由一幅图像的多个不同分辨率的子图所构成的图像集合。该组图像是由单个图像通过不断地降采样所产生的。图像金字塔的底部是待处理的高分辨率图像(原始图像),而顶部则是其低分辨率的近似图像。

向下采样

最简单的图像金字塔可以通过不断地删除图像的偶数行和偶数列得到,也可以先对原始图像滤波,得到原始图像的近似图像,然后将近似图像的偶数行和偶数列删除以获取向下采样的结果,滤波器可以选择邻域滤波器、高斯滤波器。

OpenCV提供了函数**cv2.pyrDown()**用于实现图像金字塔操作中的向下采样,其语法形式为:

cv2.pyrDown(img)

输出图像的大小为Size((img.col+1)/2, (img.row+1)/2)

img = cv2.imread('image/lena.png', 0)
output = cv2.pyrDown(img)  # 向下采样
cv2.imshow('original', img)
cv2.imshow('pyrDown', output)
print("shape:", output.shape)

向上采样

向上采样时,先对像素点以补零的方式完成插值,将图像变为原来的两倍宽、两倍高,再将图像用高斯滤波器进行卷积运算,最后将图像内的每个像素点的值乘以4,以保证得到的像素值的范围依旧在[0,255]内。

OpenCV提供了函数**cv2.pyrUp()**用于实现图像金字塔操作中的向下采样,其语法形式为:

cv2.pyrUp(img)

输出图像的大小为Size(img.col×2, img.row×2)

img = cv2.imread('image/lena.png', 0)
output = cv2.pyrUp(img)  # 向上采样
cv2.imshow('original', img)
cv2.imshow('pyrUp', output)
print("shape:", output.shape)

图像轮廓

Canny边缘检测

Canny边缘检测分为以下几个步骤:

  • 步骤一:去噪
  • 步骤二:计算梯度的幅度与方向
  • 步骤三:非极大值抑制
  • 步骤四:确定边缘。使用双阈值算法确定最终的边缘信息

OpenCV提供了函数**cv2.Canny()**来实现Canny边缘检测,其语法形式为:

cv2.Canny(img, threshold1, threshold2)

  • image为8位输入图像
  • threshold1表示处理过程中的第一个阈值
  • threshold2表示处理过程中的第二个阈值
img = cv2.imread('image/lena.png', 0)
output = cv2.Canny(img, 128, 200)  # Canny边缘检测
cv2.imshow('original', img)
cv2.imshow('Canny', output)

查找并绘制轮廓

在OpenCV中,函数cv2.findContours()用于查找图像的轮廓,并能够根据参数返回特定表示方式的轮廓。函数cv2.drawContours()能够将查找到的轮廓绘制到图像上,该函数可以根据参数在图像上绘制不同式样的轮廓。在OpenCV中,都是从黑色背景中查找白色对象

**cv2.findContours()**的语法格式为:

contours, hierarchy = cv2.findContours(img, mode, method)

返回值:

  • contours:返回的轮廓
    • contours[i]是第i个轮廓,contours[i] [j]是第i个轮廓内的第j个点
    • 轮廓的个数:len(contours)
    • 每个轮廓的点数:len(contours[i])
  • hierarchy:图像的拓扑信息。其形式是:[Next, Previous, First_Child, Parent]
    • Next:后一个轮廓的索引号
    • Previous:前一个轮廓的索引号
    • First_Child:第一个子轮廓的索引号
    • Parent:父轮廓的索引号
    • 如果上述各个参数所对应的关系为空时,则该参数为-1

参数:

  • image:原始图像,8位单通道二值图像
  • mode:轮廓检索模式
    • cv2.RETR_EXTERNAL:只检测外轮廓
    • cv2.RETR_LIST:对检测到的轮廓不建立等级关系
    • cv2.RETR_CCOMP:检索所有轮廓并将它们组织成两级层次结构。上面的一层为外边界,下面的一层为内孔的边界。
    • cv2.RETR_TREE:建立一个等级树结构的轮廓
  • method:轮廓的近似方法
    • cv2.CHAIN_APPROX_NONE:存储所有的轮廓点,相邻两个点的像素位置差不超过1
    • cv2.CHAIN_APPROX_SIMPLE:压缩水平方向、垂直方向、对角线方向的元素,只保留该方向的终点坐标

在OpenCV中,可以使用函数**cv2.drawContours()**绘制图像轮廓。该函数的语法格式为:

image = cv2.drawContours(image, contours, contourIdx, color[, thickness])

  • image:待绘制轮廓的图像。函数会在图像image上直接绘制轮廓,参数image和返回值image在函数运算后的值是相同的,所以,如果图像image还有其他用途的话,则需要预先复制一份。

  • contours:需要绘制的轮廓

  • contourIdx:需要绘制的边缘索引,告诉函数要绘制某一条轮廓函数全部轮廓。如果该参数是一个正数或者零,则表示绘制对应索引号的轮廓;如果该值是负数,则表示绘制全部轮廓。

  • color:绘制的颜色,用BGR格式表示

  • thickness:可选参数,表示绘制轮廓时所用画笔的粗细。如果将该值设置为-1,则表示绘制实心轮廓。

img = cv2.imread('image/contours.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
image = cv2.drawContours(imgCopy, contours, -1, (0, 0, 255), 5)  # 绘制轮廓
cv2.imshow('original', img)
cv2.imshow('drawContours', image)

矩特征

OpenCV提供了函数**cv2.moments()**来获取图像的轮廓矩,其语法形式为:

cv2.moments(array)

  • array:可以是点集,也可以是灰度图像或者二值图像。当array是点集时,函数会把点集作为一条轮廓

该函数的返回值是矩特征,主要包括:

  1. 空间矩:
  • 零阶矩:m00 表示一个轮廓的面积
  • 一阶矩:m10,m01
  • 二阶矩:m20,m11,m02
  • 三阶矩:m30,m21,m12,m03
  1. 中心矩:具有平移不变性
  • 二阶中心矩:mu20,mu11,mu02
  • 三阶中心矩:mu30,mu21,mu12,mu03
  1. 归一化中心矩:具有平移不变性和缩放不变性
  • 二阶Hu矩:nu20,nu11,nu02
  • 三阶Hu矩:nu30,nu21,nu12,nu03
img = cv2.imread('image/moments.bmp')
cv2.imshow('original', img)
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
n = len(contours)  # 轮廓的个数
contoursImg = []
for i in range(n):
    temp = np.zeros(img.shape, np.uint8)
    contoursImg.append(temp)
    contoursImg[i] = cv2.drawContours(contoursImg[i], contours, i, (255, 0, 0), 3)
    cv2.imshow("contours[" + str(i) + "]", contoursImg[i])  # 显示轮廓
    print("轮廓" + str(i) + "的矩:\n", cv2.moments(contours[i]))  # 轮廓的矩
    print("轮廓" + str(i) + "的面积:%d" % cv2.moments(contours[i])['m00'])  # 轮廓的面积

函数**cv2.contourArea()**用于计算轮廓的面积,该函数的语法格式为:

cv2.contourArea(contour)

  • contour是轮廓
img = cv2.imread('image/moments.bmp')
cv2.imshow('original', img)
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
n = len(contours)  # 轮廓的个数
contoursImg = []
for i in range(n):
    temp = np.zeros(img.shape, np.uint8)
    contoursImg.append(temp)
    contoursImg[i] = cv2.drawContours(contoursImg[i], contours, i, (255, 0, 0), 3)
    cv2.imshow("contours[" + str(i) + "]", contoursImg[i])  # 显示轮廓
    print("轮廓" + str(i) + "的面积:%d" % cv2.contourArea(contours[i]))  # 轮廓的面积

函数**cv2.arcLength()**用于计算轮廓的面积,该函数的语法格式为:

cv2.arcLength(curve, closed)

  • curve是轮廓
  • closed是布尔值,用来表示轮廓是否是封闭的
img = cv2.imread('image/moments.bmp')
cv2.imshow('original', img)
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
n = len(contours)  # 轮廓的个数
contoursImg = []
for i in range(n):
    temp = np.zeros(img.shape, np.uint8)
    contoursImg.append(temp)
    contoursImg[i] = cv2.drawContours(contoursImg[i], contours, i, (255, 0, 0), 3)
    cv2.imshow("contours[" + str(i) + "]", contoursImg[i])  # 显示轮廓
    print("轮廓" + str(i) + "的长度:%d" % cv2.arcLength(contours[i], True))  # 轮廓的长度

Hu矩

Hu矩是归一化中心矩的线性组合。Hu矩在图像旋转、缩放、平移等操作后,仍能保持矩的不变性,所以经常会使用Hu矩来识别图像的特征。

在OpenCV中,使用函数**cv2.HuMoments()**可以得到Hu矩,该函数的语法格式为:

cv2.HuMoments(m)

  • m是由函数**cv2.moments()**计算得到的矩特征值
img = cv2.imread('image/cs.bmp', 0)
HuM1 = cv2.HuMoments(cv2.moments(img)).flatten()  # 将数组变为一维
print("HuM1:", HuM1)

利用Hu矩判断两个对象的一致性结果比较抽象。为了更直观方便地比较Hu矩值,OpenCV提供了函数**cv2.matchShapes()**对两个对象的Hu矩进行比较,该函数的语法格式为:

cv2.matchShapes(contour1, contour2, method, parameter)

  • contour1:第一个轮廓或灰度图像

  • contour2:第二个轮廓或灰度图像

  • method:比较两个对象的Hu矩的方法

  • parameter:应用于method的特定参数。设置为0。

img1 = cv2.imread('image/cs1.bmp', 0)
img2 = cv2.imread('image/cs2.bmp', 0)
img3 = cv2.imread('image/cc.bmp', 0)
ret0 = cv2.matchShapes(img1, img1, 1, 0)  # 相同图像
ret1 = cv2.matchShapes(img1, img2, 1, 0)  # 相似图像
ret2 = cv2.matchShapes(img1, img3, 1, 0)  # 不相似图像
print("相同图像的matchShapes=", ret0)
print("相似图像的matchShapes=", ret1)
print("不相似图像的matchShapes=", ret2)

相同图像的matchShapes= 0.0
相似图像的matchShapes= 0.0001154058519395873
不相似图像的matchShapes= 0.012935752303635195

同一幅图的Hu矩是不变的,二者差值为0;相似的图像返回值的差较小;不相似图像返回值的差较大。

轮廓拟合

在计算轮廓时,可能并不需要实际的轮廓,而仅需要一个接近于轮廓的近似多边形。OpenCV提供了多种计算轮廓近似多边形的方法。

  1. 矩形包围框

函数**cv2.boundingRect()**能够绘制轮廓的矩形边界,该函数的语法格式为:

retval = cv2.boundingRect(points)

  • retval表示返回的矩形边界的左上角顶点的坐标值及矩形边界的宽度和高度
  • points是灰度图像或轮廓

该函数还可以是具有4个返回值的形式:x, y, w, h = cv2.boundingRect(points)

  • x:矩形边界左上角顶点的x坐标
  • y:矩形边界左上角顶点的y坐标
  • w:矩形边界x方向的长度
  • h:矩形边界y方向的长度
img = cv2.imread('image/cc.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
x, y, w, h = cv2.boundingRect(contours[0])  # 矩形包围框
cv2.rectangle(imgCopy, (x, y), (x + w, y + h), (255, 255, 255), 2)  # 绘制矩形框
cv2.imshow('original', img)
cv2.imshow('boundingRect', imgCopy)
  1. 最小包围矩形框

函数**cv2.minAreaRect()**能够绘制轮廓的最小包围矩形框,该函数的语法格式为:

retval = cv2.minAreaRect(points)

  • retval表示返回的矩形特征信息。该值的结构是(最小外接矩形的中心(x, y), (宽度, 高度), 旋转角度)
  • points是轮廓

返回值retval的结构不符合函数cv2.drawContours()的参数结构,所以需要用函数**cv2.boxPoints()**将retval转换成符合要求的结构,该函数的语法格式为:

points = cv2.boxPoints(box)

  • points是能够用于函数cv2.drawContours()的轮廓点
  • box是函数cv2.minAreaRect()的返回值
img = cv2.imread('image/cc.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
rect = cv2.minAreaRect(contours[0])  # 最小包围矩形框
points = cv2.boxPoints(rect)  # 转换格式
points = np.int0(points)  # 取整
cv2.drawContours(imgCopy, [points], 0, (255, 255, 255), 2)
cv2.imshow('original', img)
cv2.imshow('minAreaRect', imgCopy)
  1. 最小包围圆形

函数**cv2.minEnclosingCircle()**通过迭代算法构造一个对象的面积最小包围圆形,该函数的语法格式为:

**center, radius = cv2.minEnclosingCircle(points) **

  • center是最小包围圆形的圆心
  • radius是最小包围圆形的半径
  • points是轮廓
img = cv2.imread('image/cc.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
(x, y), radius = cv2.minEnclosingCircle(contours[0])  # 最小包围圆形
center = (int(x), int(y))  # 取整
radius = int(radius)  # 取整
cv2.circle(imgCopy, center, radius, (255, 255, 255), 2)
cv2.imshow('original', img)
cv2.imshow('minEnclosingCircle', imgCopy)
  1. 最优拟合椭圆

函数**cv2.fitEllipse()**可以用来构造最优拟合椭圆,该函数的语法格式为:

retval = cv2.fitEllipse(points)

  • retval是RotatedRect类型的值
  • points是轮廓
img = cv2.imread('image/cc.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
ellipse = cv2.fitEllipse(contours[0])  # 最优拟合椭圆
cv2.ellipse(imgCopy, ellipse, (255, 0, 0), 2)
cv2.imshow('original', img)
cv2.imshow('fitEllipse', imgCopy)
  1. 最优拟合直线

函数**cv2.fitLine()**可以用来构造最优拟合直线,该函数的语法格式为:

line = cv2.fitLine(points, distType, param, reps, aeps)

  • line:对于二维直线,输出为4维,前两维代表拟合出的直线的方向,后两位代表直线上的一点。(即点斜式)

  • points:轮廓

  • distType:距离类型。拟合直线时,要使输入点到拟合直线的距离之和最小,其类型如下表所示

  • param:距离参数,与所选的距离类型有关。当此参数设置为0时,会自动选择最优值

  • reps:拟合直线所需要的径向精度,通常设置为0.01

  • aeps:拟合直线所需要的角度精度,通常设置为0.01

含义
cv2.DIST_USER 用户定义距离
cv2.DIST_L1 distance = |x1-x2|+|y1-y2|
cv2.DIST_L2 欧式距离,此时与最小二乘法相同
cv2.DIST_C distance =max(|x1-x2|, |y1-y2|)
cv2.DIST_L12 distance = 2(sqrt(1+x*x/2) - 1))
cv2.DIST_FAIR distance = c2(|x|/c-log(1+|x|/c)), c = 1.3998
cv2.DIST_WELSCH distance =c2/2(1-exp(-(x/c)2)), c = 2.9846
cv2.DIST_HUBER distance =|x|2/2 : c(|x|-c/2), c=1.345
img = cv2.imread('image/cc.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
row, col = img.shape[:2]
[vx, vy, x, y] = cv2.fitLine(contours[0], cv2.DIST_L2, 0, 0.01, 0.01)  # 最优拟合直线
left_y = int((-x * vy / vx) + y)
right_y = int(((col - x) * vy / vx) + y)
cv2.line(imgCopy, (col - 1, right_y), (0, left_y), (255, 0, 0), 2)
cv2.imshow('original', img)
cv2.imshow('fitLine', imgCopy)
  1. 最小外包三角形

函数**cv2.minEnclosingTriangle()**可以用来构造最小外包三角形,该函数的语法格式为:

retval, triangle = cv2.minEnclosingTriangle(points)

  • retval:最小外包三角形的面积
  • triangle :最小外包三角形的三个顶点集
  • points:轮廓
img = cv2.imread('image/cc.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
area, triangle = cv2.minEnclosingTriangle(contours[0])  # 最小外包三角形
for i in range(3):
    cv2.line(imgCopy, tuple(triangle[i][0]), tuple(triangle[(i + 1) % 3][0]), (255, 0, 0), 2)
cv2.imshow('original', img)
cv2.imshow('minEnclosingTriangle', imgCopy)
  1. 逼近多边形

函数**cv2.approxPolyDP()**可以用来构造指定精度的逼近多边形曲线,该函数的语法格式为:

approxCurve = cv2.approxPolyDP(curve, epsilon, closed)

  • approxCurve是逼近多边形的点集
  • curve是轮廓
  • epsilon是精度,原始轮廓的边界点与逼近多边形边界之间的最大距离。通常设置为多边形总长度的百分比形式
  • closed是布尔值,表示多边形是否封闭。
img = cv2.imread('image/cc.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
epsilon = 0.01 * cv2.arcLength(contours[0], True)  # 设置精度
approxCurve = cv2.approxPolyDP(contours[0], epsilon, True)  # 逼近多边形曲线
cv2.drawContours(imgCopy, [approxCurve], 0, (255, 0, 0), 2)
cv2.imshow('original', img)
cv2.imshow('minEnclosingTriangle', imgCopy)

凸包

  1. 凸包

凸包指的是完全包含原有轮廓,并且仅由轮廓上的点所构成的多边形。凸包的每一处都是凸的,即在凸包内连接任意两点的直线都在凸包的内部。

函数**cv2.convexHull()**可以用来获取轮廓的凸包,该函数的语法格式为:

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

  • points:轮廓

  • clockwise:布尔值。该值为True时,凸包角点将按顺时针方向排列;该值为False时,凸包角点将按逆时针方向排列。

  • returnPoints:布尔值。默认为True,函数返回凸包角点的x、y轴坐标;当为False时,函数返回轮廓中凸包角点的索引。

  1. 凸缺陷

凸包与轮廓之间的部分称为凸缺陷。函数**cv2.convexityDefects()**可以用来获取凸缺陷,该函数的语法格式为:

convexityDefects = cv2.convexityDefects(contour, convexhull)

  • convexityDefects为凸缺陷点集。它是一个数组,每一行包含的值是[起点,终点,轮廓上距离凸包最远的点,最远点到凸包的近似距离],前三个值是轮廓点的索引,需要到轮廓点中去找它们

  • contour是轮廓

  • convexhull是凸包

需要注意的是,用cv2.convexityDefects()计算凸缺陷时,要使用凸包作为参数。在查找凸包时,所使用函数cv2.convexHull()的参数returnPoints的值必须是False。

img = cv2.imread('image/hand.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
cnt = contours[0]
hull = cv2.convexHull(cnt, returnPoints=False)  # 凸包
defects = cv2.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])
    cv2.line(imgCopy, start, end, (255, 0, 0), 2)
    cv2.circle(imgCopy, far, 5, (255, 0, 0), -1)
cv2.imshow('original', img)
cv2.imshow('convexityDefects', imgCopy)
  1. 几何学测试

判断轮廓是否是凸形的

函数**cv2.isContourConvex()**可以用来判断轮廓是否是凸形的,该函数的语法格式为:

retval = cv2.isContourConvex(contour)

  • retval是布尔值,该值为True时表示轮廓是凸形的,为False时,不是凸形的
  • contour为要判断的轮廓
img = cv2.imread('image/hand.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
hull = cv2.convexHull(contours[0])  # 凸包
cv2.polylines(imgCopy, [hull], True, (255, 0, 0), 2)
result = cv2.isContourConvex(hull)  # 判断轮廓是否是凸形的
print(result)
cv2.imshow('original', img)
cv2.imshow('isContourConvex', imgCopy)

点到轮廓的距离

函数**cv2.pointPolygonTest()**可以用来计算点到多边形(轮廓)的最短距离,该函数的语法格式为:

retval = cv2.pointPolygonTest(contour, pt, measureDist)

  • contour为轮廓
  • pt为待判定的点
  • measureDist为布尔值,表示距离的判定方式。
    • 值为True时,表示计算点到轮廓的距离。如果点在轮廓的外部,返回值为负数;如果点在轮廓上,返回值为0;如果点在轮廓内部,返回值为正数。
    • 值为False时,不计算距离,只返回"-1",“0”,“1"中的一个值,表示点相对于轮廓的位置关系。如果点在轮廓的外部,返回值为”-1";如果点在轮廓上,返回值为"0";如果点在轮廓内部,返回值为"1"。
img = cv2.imread('image/cs1.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
hull = cv2.convexHull(contours[0])  # 凸包
cv2.polylines(imgCopy, [hull], True, (255, 0, 0), 2)
distA = cv2.pointPolygonTest(hull, (300, 150), True)  # 点到轮廓的距离
distB = cv2.pointPolygonTest(hull, (300, 250), True)  # 点到轮廓的距离
distC = cv2.pointPolygonTest(hull, (423, 112), True)  # 点到轮廓的距离
cv2.putText(imgCopy, "A", (300, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 3)
cv2.putText(imgCopy, "B", (300, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 3)
cv2.putText(imgCopy, "C", (423, 112), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 3)
print("distA:", distA)
print("distB:", distB)
print("distC:", distC)
cv2.imshow('original', img)
cv2.imshow('pointPolygonTest', imgCopy)

利用形状场景算法比较轮廓

  1. 计算形状场景距离

函数**cv2.createShapeContextDistanceExtractor()**可以用来计算形状场景距离,该函数的语法格式为:

retval = cv2.createShapeContextDistanceExtractor()

  • retval返回结果

该结果可以通过函数cv2.ShapeDistanceExtractor.computeDistance(contour1, contour2)

  • contour1和contour2是两个不同的轮廓
img1 = cv2.imread('image/cs1.bmp', 0)
img2 = cv2.imread('image/cs2.bmp', 0)
img3 = cv2.imread('image/hand.bmp', 0)
ret, binary1 = cv2.threshold(img1, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
ret, binary2 = cv2.threshold(img2, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
ret, binary3 = cv2.threshold(img3, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours1, hierarchy = cv2.findContours(binary1, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
contours2, hierarchy = cv2.findContours(binary2, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
contours3, hierarchy = cv2.findContours(binary3, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
cnt1 = contours1[0]
cnt2 = contours2[0]
cnt3 = contours3[0]
result = cv2.createShapeContextDistanceExtractor()  # 计算形状场景距离
d1 = result.computeDistance(cnt1, cnt1)
d2 = result.computeDistance(cnt1, cnt2)
d3 = result.computeDistance(cnt1, cnt3)
print("相同图像的距离=", d1)
print("相似图像的距离=", d2)
print("不相似图像的距离=", d3)
  1. 计算Hausdorff距离

函数**cv2.createHausdorffDistanceExtractor()**可以用来计算形状场景距离,该函数的语法格式为:

retval = cv2.createHausdorffDistanceExtractor()

img1 = cv2.imread('image/cs1.bmp', 0)
img2 = cv2.imread('image/cs3.bmp', 0)
img3 = cv2.imread('image/hand.bmp', 0)
ret, binary1 = cv2.threshold(img1, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
ret, binary2 = cv2.threshold(img2, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
ret, binary3 = cv2.threshold(img3, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours1, hierarchy = cv2.findContours(binary1, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
contours2, hierarchy = cv2.findContours(binary2, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
contours3, hierarchy = cv2.findContours(binary3, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
cnt1 = contours1[0]
cnt2 = contours2[0]
cnt3 = contours3[0]
result = cv2.createHausdorffDistanceExtractor()  # 计算Hausdorff距离
d1 = result.computeDistance(cnt1, cnt1)
d2 = result.computeDistance(cnt1, cnt2)
d3 = result.computeDistance(cnt1, cnt3)
print("相同图像的距离=", d1)
print("相似图像的距离=", d2)
print("不相似图像的距离=", d3)

轮廓的特征值

  1. 宽高比

宽高比 = 宽度(width) / 高度(height)

img = cv2.imread('image/cc.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
x, y, w, h = cv2.boundingRect(contours[0])
aspectRatio = float(w) / h  # 宽高比
print("宽高比=", aspectRatio)
  1. Extend

可以使用轮廓面积与矩形边界面积之比Extend来描述图像及其轮廓特征。

Extend= 轮廓面积(对象面积) / 矩形边界面积

img = cv2.imread('image/cc.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
x, y, w, h = cv2.boundingRect(contours[0])
rectArea = w * h
cntArea = cv2.contourArea(contours[0])
Extend = float(cntArea) / rectArea  # Extend
print("Extend=", Extend)
  1. Solidity

可以使用轮廓面积与凸包面积之比Solidity来衡量图像、轮廓及凸包的特征。

Solidity= 轮廓面积(对象面积) / 凸包面积

img = cv2.imread('image/hand.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
cntArea = cv2.contourArea(contours[0])
hull = cv2.convexHull(contours[0])
hullArea = cv2.contourArea(hull)
Solidity = float(cntArea) / hullArea  # Solidity
print("Solidity=", Solidity)
  1. 等效直径

该值是与轮廓面积相等的圆形的直径。

等效直径 = sqrt(4 × 轮廓面积 / π)

img = cv2.imread('image/cc.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
cntArea = cv2.contourArea(contours[0])
equivalentDiameter = np.sqrt(4 * cntArea / np.pi)  # 等效直径
print("等效直径=", equivalentDiameter)
  1. 轮廓像素点

使用Numpy函数获取轮廓像素点:

**numpy.nonzero()函数能够找出数组内非零元素的位置,但是其返回值是将行、列分别显示的。使用numpy.transpose()**函数处理上述返回值则可以得到这些点的(x,y)形式的坐标。

使用OpenCV函数获取轮廓像素点:

**cv2.findNonZero()**函数可以用于查找非零元素的索引。该函数的语法格式为:

idx = cv2.findNonZero(src)

  • idx表示非零元素的索引位置,在返回的索引中,每个元素对应的是(列号,行号)的格式
  • src表示要查找非零元素的图像
  1. 最大值和最小值及它们的位置
img = cv2.imread('image/boat.jpg')
cv2.imshow("original", img)
plt.hist(img.ravel(), 256)  # 使用Numpy绘制直方图
plt.show()
  • min_val:最小值
  • max_val:最大值
  • min_loc:最小值的位置
  • max_loc:最大值的位置
  • img:单通道图像
img = cv2.imread('image/ct.png')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
cnt = contours[2]
mask = np.zeros(gray.shape, np.uint8)
mask = cv2.drawContours(mask, [cnt], -1, (255, 0, 0), -1)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(gray, mask)  # 最大值和最小值及它们的位置
print("min_val:", min_val)
print("max_val:", max_val)
print("min_loc:", min_loc)
print("max_loc:", max_loc)
maskImg = np.zeros(img.shape, np.uint8)
maskImg = cv2.drawContours(maskImg, [cnt], -1, (255, 255, 255), -1)
loc = cv2.bitwise_and(img, maskImg)
cv2.imshow("original", img)
cv2.imshow("mask", loc)
  1. 平均颜色及平均灰度

函数**cv2.mean()**可以用来计算一个对象的平均颜色或平均灰度,该函数的语法格式为:

mean_val = cv2.mean(img)

  • mean_val:返回的平均值。返回值有4个值,分别是RGB和alpha通道的均值。
img = cv2.imread('image/ct.png')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
cnt = contours[2]
mask = np.zeros(gray.shape, np.uint8)
mask = cv2.drawContours(mask, [cnt], -1, (255, 0, 0), -1)
meanVal = cv2.mean(img, mask)  # 平均颜色及平均灰度
print("meanVal:", meanVal)
  1. 极点
img = cv2.imread('image/cs1.bmp')
imgCopy = img.copy()  # 复制图像
gray = cv2.cvtColor(imgCopy, cv2.COLOR_BGR2GRAY)  # 转换为灰度图
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)  # 将图像二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 查找轮廓
mask = np.zeros(gray.shape, np.uint8)
cnt = contours[0]
cv2.drawContours(mask, [cnt], 0, 255, -1)
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])  # 最下端
print("leftmost=", leftmost)
print("rightmost=", rightmost)
print("topmost=", topmost)
print("bottommost=", bottommost)
cv2.putText(imgCopy, 'A', leftmost, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
cv2.putText(imgCopy, 'B', rightmost, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
cv2.putText(imgCopy, 'C', topmost, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
cv2.putText(imgCopy, 'D', bottommost, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
cv2.imshow("result", imgCopy)

直方图处理

绘制直方图

  1. 使用Numpy绘制直方图

可以使用**matplotlib.pyplot.hist()**函数绘制直方图。此函数的作用是根据数据源和灰度级分组绘制直方图。其基本语法格式为:

matplotlib.pyplot.hist(x, bins)

  • x:数据源,必须是一维的。图像通常是二维的,需要使用**ravel()**函数将图像处理为一维数据源以后再作为参数使用
  • bins:表示灰度级的分组情况
img = cv2.imread('image/boat.jpg')
cv2.imshow("original", img)
plt.hist(img.ravel(), 256)  # 使用Numpy绘制直方图
plt.show()
  1. 使用OpenCV绘制直方图

函数**cv2.calcHist()**可以用来计算图像的统计直方图,该函数能统计各个灰度级的像素点个数。该函数的语法格式为:

hist = cv2.calcHist(img, channels, mask, histSize, ranges)

  • hist:返回的统计直方图,是一个一维数组
  • img:原始图像,该图像需要用"[]"括起来
  • channels:指定通道编号,通道编号需要用"[]"括起来,如果是单通道图像,该参数的值就是[0]。
  • mask:掩模图像。当统计整幅图像的直方图时,该值为None
  • histSize:BINS的值,该值需要用"[]"括起来
  • ranges:像素值范围
img = cv2.imread('image/girl.bmp')
cv2.imshow("original", img)
histB = cv2.calcHist([img], [0], None, [256], [0, 255])  # 得到直方图数据
histG = cv2.calcHist([img], [1], None, [256], [0, 255])  # 得到直方图数据
histR = cv2.calcHist([img], [2], None, [256], [0, 255])  # 得到直方图数据
plt.plot(histB, color='b', label='B')
plt.plot(histG, color='g', label='G')
plt.plot(histR, color='r', label='R')
plt.show()

直方图均衡化

直方图均衡化的算法主要包括两个步骤:

  1. 计算累计直方图
  2. 对累计直方图进行区间转换

在累计直方图的基础上,对原有灰度级空间进行转换可以在原有范围内对灰度级实现均衡化,也可以在更广泛的灰度空间范围内对灰度级实现均衡化。

  1. 在原有范围内实现均衡化

在原有范围内实现直方图均衡化时,用当前灰度级的累计概率乘以当前灰度级的最大值,得到新的灰度级,并作为均衡化结果。

  1. 在更广泛的范围内实现均衡化

在更广泛的范围内实现直方图均衡化时,用当前灰度级的累计概率乘以更广泛的范围灰度级的最大值,得到新的灰度级,并作为均衡化结果。

函数**cv2.equalizeHist()**可以用来实现直方图均衡化。该函数的语法格式为:

output = cv2.equalizeHist(img)

  • img是8位单通道原始图像
img = cv2.imread('image/equ.bmp', 0)
output = cv2.equalizeHist(img)  # 直方图均衡化
cv2.imshow("original", img)
cv2.imshow("equalizeHist", output)
plt.figure("原始图像直方图")
plt.hist(img.ravel(), 256)
plt.figure("均衡化图像直方图")
plt.hist(output.ravel(), 256)
plt.show()

傅里叶变换

用Numpy实现傅里叶变换

  1. 实现傅里叶变换

Numpy提供函数**numpy.fft.fft2()**可以用来实现傅里叶变换。该函数的语法格式为:

fft= numpy.fft.fft2(img)

  • img是灰度图像
  • fft是一个复数数组

图像频谱中的零频率分量位于频谱图像的左上角,为了便于观察通常会使用**numpy.fft.fftshift()**函数将零频率成分移动到频率图像的中心位置。该函数的语法格式为:

fftShift = numpy.fft.fftshift(fft)

对图像进行傅里叶变换后,得到的是一个复数数组,为了显示为图像,需要将它们的值调整到[0, 255]的灰度空间内,使用公式为:

output = 20×numpy.log(numpy.abs(fftShift))

img = cv2.imread('image/lena.png', 0)
fft = np.fft.fft2(img)  # 傅里叶变换
fftShift = np.fft.fftshift(fft)
output = 20 * np.log(np.abs(fftShift))
plt.subplot(121)
plt.imshow(img, cmap='gray')
plt.title('original'), plt.axis('off')
plt.subplot(122)
plt.imshow(output, cmap='gray')
plt.title('result'), plt.axis('off')
plt.show()
  1. 实现傅里叶逆变换

如果在傅里叶变换过程中使用了**numpy.fft.fftshift()函数移动零频率分量,那么在傅里叶逆变换过程中需要先使用numpy.fft.ifftshift()**函数将零频率分量移到原来的位置,再进行傅里叶逆变换。该函数的语法格式为:

ifftShift = numpy.fft.ifftshift(原始频谱)

函数**numpy.fft.ifft2()**可以用来实现傅里叶逆变换,返回空间域复数数组。该函数的语法格式为:

ifft = numpy.fft.ifft2(ifftShift)

傅里叶逆变换得到的空间域信息是一个复数数组需要将该信息调整到[0, 255]的灰度空间内,使用公式为:

img = numpy.abs(ifft)

img = cv2.imread('image/lena.png', 0)
fft = np.fft.fft2(img)  # 傅里叶变换
fftShift = np.fft.fftshift(fft)
ifftShift = np.fft.ifftshift(fftShift)
ifft = np.fft.ifft2(ifftShift)  # 傅里叶逆变换
output = np.abs(ifft)
plt.subplot(121)
plt.imshow(img, cmap='gray')
plt.title('original'), plt.axis('off')
plt.subplot(122)
plt.imshow(output, cmap='gray')
plt.title('output'), plt.axis('off')
plt.show()
  1. 实现高通滤波
img = cv2.imread('image/lena.png', 0)
fft = np.fft.fft2(img)  # 傅里叶变换
fftShift = np.fft.fftshift(fft)
row, col = img.shape
r, c = int(row / 2), int(col / 2)  # 图像中心点
fftShift[r - 30:r + 30, c - 30:c + 30] = 0  # 去除低频信息
ifftShift = np.fft.ifftshift(fftShift)
ifft = np.fft.ifft2(ifftShift)  # 傅里叶逆变换
output = np.abs(ifft)
plt.subplot(121)
plt.imshow(img, cmap='gray')
plt.title('original'), plt.axis('off')
plt.subplot(122)
plt.imshow(output, cmap='gray')
plt.title('output'), plt.axis('off')
plt.show()
  1. 实现低通滤波
img = cv2.imread('image/lena.png', 0)
fft = np.fft.fft2(img)  # 傅里叶变换
fftShift = np.fft.fftshift(fft)
row, col = img.shape
r, c = int(row / 2), int(col / 2)  # 图像中心点
mask = np.zeros((row, col), np.uint8)
mask[r - 30:r + 30, c - 30:c + 30] = 1
fftShift = fftShift * mask
ifftShift = np.fft.ifftshift(fftShift)
ifft = np.fft.ifft2(ifftShift)  # 傅里叶逆变换
output = np.abs(ifft)
plt.subplot(121)
plt.imshow(img, cmap='gray')
plt.title('original'), plt.axis('off')
plt.subplot(122)
plt.imshow(output, cmap='gray')
plt.title('output'), plt.axis('off')
plt.show()

用OpenCV实现傅里叶变换

  1. 实现傅里叶变换

函数**cv2.dft()**可以用来实现傅里叶变换,返回空间域复数数组。该函数的语法格式为:

dft= cv2.dft(img, flags)

  • img要先使用**np.float32()**函数将图像转换为np.float32格式
  • flags的值通常为cv2.DFT_COMPLEX_OUTPUT,用来输出一个复数阵列

此时,零频率分量并不在中心位置,为了处理方便需要将其移到中心位置,可以用**numpy.fft.fftshift()**实现。

dftShift = numpy.fft.fftshift(dft)

经过上述处理后,频谱图像还只是一个由实部和虚部构成的值。要将其显示出来,还要用**cv2.magnitude()**函数处理,该函数可以计算频谱信息的幅度,该函数的语法格式为:

output = cv2.magnitude(x, y)

  • output:频谱信息的幅度

  • x:实部

  • y:虚部

得到频谱信息的幅度后,还需要将幅度值映射到灰度空间[0, 255]内,使其以灰度图像的形式显示出来。

output = 20 * np.log(cv2.magnitude(x, y))

img = cv2.imread('image/lena.png', 0)
dft = cv2.dft(np.float32(img), flags=cv2.DFT_COMPLEX_OUTPUT)  # 傅里叶变换
dftShift = np.fft.fftshift(dft)
output = 20 * np.log(cv2.magnitude(dftShift[:, :, 0], dftShift[:, :, 1]))
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('original'), plt.axis('off')
plt.subplot(122), plt.imshow(output, cmap='gray')
plt.title('result'), plt.axis('off')
plt.show()
  1. 实现傅里叶逆变换

如果在傅里叶变换过程中使用了**numpy.fft.fftshift()函数移动零频率分量,那么在傅里叶逆变换过程中需要先使用numpy.fft.ifftshift()**函数将零频率分量移到原来的位置,再进行傅里叶逆变换。该函数的语法格式为:

idftShift = numpy.fft.ifftshift(原始频谱)

函数**cv2.idft()**可以用来实现傅里叶逆变换。该函数的语法格式为:

idft= cv2.idft(idftShift)

进行傅里叶逆变换后得到的仍旧是复数,需要使用函数**cv2.magnitude()**计算其幅度。

img = cv2.imread('image/lena.png', 0)
dft = cv2.dft(np.float32(img), flags=cv2.DFT_COMPLEX_OUTPUT)  # 傅里叶变换
dftShift = np.fft.fftshift(dft)
idftShift = np.fft.ifftshift(dftShift)
idft = cv2.idft(idftShift)  # 傅里叶逆变换
output = cv2.magnitude(idft[:, :, 0], idft[:, :, 1])
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('original'), plt.axis('off')
plt.subplot(122), plt.imshow(output, cmap='gray')
plt.title('result'), plt.axis('off')
plt.show()
  1. 实现高通滤波
img = cv2.imread('image/lena.png', 0)
dft = cv2.dft(np.float32(img), flags=cv2.DFT_COMPLEX_OUTPUT)  # 傅里叶变换
dftShift = np.fft.fftshift(dft)
row, col = img.shape
r, c = int(row / 2), int(col / 2)  # 图像中心点
dftShift[r - 30:r + 30, c - 30:c + 30] = 0  # 去除低频信息
idftShift = np.fft.ifftshift(dftShift)
idft = cv2.idft(idftShift)  # 傅里叶逆变换
output = cv2.magnitude(idft[:, :, 0], idft[:, :, 1])
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('original'), plt.axis('off')
plt.subplot(122), plt.imshow(output, cmap='gray')
plt.title('result'), plt.axis('off')
plt.show()
  1. 实现低通滤波
img = cv2.imread('image/lena.png', 0)
dft = cv2.dft(np.float32(img), flags=cv2.DFT_COMPLEX_OUTPUT)  # 傅里叶变换
dftShift = np.fft.fftshift(dft)
row, col = img.shape
r, c = int(row / 2), int(col / 2)  # 图像中心点
mask = np.zeros((row, col, 2), np.uint8)
mask[r - 30:r + 30, c - 30:c + 30] = 1
dftShift = dftShift * mask
idftShift = np.fft.ifftshift(dftShift)
idft = cv2.idft(idftShift)  # 傅里叶逆变换
output = cv2.magnitude(idft[:, :, 0], idft[:, :, 1])
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('original'), plt.axis('off')
plt.subplot(122), plt.imshow(output, cmap='gray')
plt.title('result'), plt.axis('off')
plt.show()

模板匹配

模板匹配基础

模板匹配是指在当前图像A内寻找与图像B最相似的部分。模板匹配的操作方法是将模板图像B在图像A上滑动,遍历所有像素以完成匹配。

函数**cv2.matchTemplate()**可以用来实现模板匹配。该函数的语法格式为:

cv2.matchTemplate(img, templ, method)

  • img为原始图像,必须是8位或者32位的浮点型图像
  • templ为模板图像,它的尺寸必须小于或等于原始图像,并且与原始图像有相同的类型
  • method为匹配方法,如下图所示
参数 对应数值 说明
CV_TM_SQDIFF 0 以方差为依据进行匹配;若完全匹配,则结果为0;若不匹配,则会得到一个很大的值
CV_TM_SQDIFF_NORMED 1 归一化平方差匹配
CV_TM_CCORR 2 该方法将模板图像与输入图像相乘;数值越大表明匹配程度越好
CV_TM_CCORR_NORMED 3 归一化相关匹配
CV_TM_CCOEFF 4 1表示完美匹配,-1表示最差匹配,0表示没有任何相关性
CV_TM_CCOEFF_NORMED 5 归一化相关系数匹配

用cv2.TM_SQDIFF方法进行模板匹配

img = cv2.imread('image/lena.png', 0)
template = cv2.imread('image/lenaEye.bmp', 0)
templateH, templateW = template.shape  # 模板的高、宽
match = cv2.matchTemplate(img, template, cv2.TM_SQDIFF)  # 模板匹配(cv2.TM_SQDIFF)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(match)  # 最大值和最小值及它们的位置
topLeft = min_loc  # 左上角
bottomRight = (topLeft[0] + templateW, topLeft[1] + templateH)  # 右下角
cv2.rectangle(img, topLeft, bottomRight, (255, 0, 0), 2)
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('result'), plt.axis('off')
plt.subplot(122), plt.imshow(match, cmap='gray')
plt.title('Match Result'), plt.axis('off')
plt.show()

用cv2.TM_CCOEFF方法进行模板匹配

img = cv2.imread('image/lena.png', 0)
template = cv2.imread('image/lenaEye.bmp', 0)
templateH, templateW = template.shape  # 模板的高、宽
match = cv2.matchTemplate(img, template, cv2.TM_CCOEFF)  # 模板匹配(cv2.TM_CCOEFF)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(match)  # 最大值和最小值及它们的位置
topLeft = max_loc  # 左上角
bottomRight = (topLeft[0] + templateW, topLeft[1] + templateH)  # 右下角
cv2.rectangle(img, topLeft, bottomRight, (255, 0, 0), 2)
plt.subplot(121), plt.imshow(img, cmap='gray')
plt.title('result'), plt.axis('off')
plt.subplot(122), plt.imshow(match, cmap='gray')
plt.title('Match Result'), plt.axis('off')
plt.show()

多模板匹配

多模板匹配分为以下5个步骤:

  1. 获取匹配位置的集合

函数**numpy.where()**能够获取模板匹配位置的集合。对于不同的输入,其返回值是不同的。

  • 当输入为一位数组时,返回值是一维索引,只有一组索引数组
  • 当输入为二维数组时,返回值是匹配值的位置索引,因此会有两组索引数组表示返回值的位置

所以,函数np.where()可以找出在函数cv2.matchTemplate()的返回值中,哪些位置上的值是大于阈值threshold的。可以采用语句:

loc = np.where(res >= threshold)

  • loc是满足"res >= threshold"的像素点的索引集合

  • res是函数**cv2.matchTemplate()**进行模板匹配后的返回值

  • threshold是阈值

  1. 循环

在获取匹配值的索引集合后,可以采用如下语句遍历所有匹配的位置,对这些位置做标记:

for i in 匹配位置集合:
	标记匹配位置
  1. 在循环中使用函数zip()

函数zip()用可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的元素。因此,如果希望循环遍历由np.where()返回的模板匹配索引集合,可以采用语句:

for i in zip(*模板匹配索引集合):
	标记处理
  1. 调整坐标

使用**np.where()得到的形式为"(行号,列号)"的位置索引,但是函数cv2.rectangle()**中指定顶点的参数的形式为"(列号,行号)"的位置索引。所以要进行行列互换,可以使用语句:loc[::-1]

  1. 标记匹配图像的位置

函数**cv2.rectangle()**可以标记匹配图像的位置。

  1. 总程序
img = cv2.imread('image/lena4.bmp', 0)
template = cv2.imread('image/lena4Temp.bmp', 0)
templateH, templateW = template.shape  # 模板的高、宽
res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)  # 模板匹配(cv2.TM_CCOEFF_NORMED)
threshold = 0.9  # 阈值
loc = np.where(res >= threshold)
for pt in zip(*loc[::-1]):
    cv2.rectangle(img, pt, (pt[0] + templateW, pt[1] + templateH), (255, 0, 0), 2)
plt.imshow(img, cmap='gray')
plt.xticks([]), plt.yticks([])
plt.show()

霍夫变换

霍夫变换原理

与笛卡尔坐标系对应,在霍夫坐标系中,横坐标采用笛卡尔坐标系中直线的斜率k,纵坐标使用笛卡尔坐标系中直线的截距b。

  • 笛卡尔空间内的一条直线确定了霍夫空间内的一个点

  • 霍夫空间内的一个点确定了笛卡尔空间内的一条直线

  • 笛卡尔空间内的两个点会映射为霍夫空间内两条相交于(k, b)的直线

  • 笛卡尔空间内的两个点对应的直线会映射为霍夫空间内的点(k, b)

在霍夫空间内,经过一个点的直线越多,说明其在笛卡尔空间内映射的直线是由越多的点所穿过的,那么它实际存在的可能性就越大,它的可靠性也越高。

假如有垂直于x轴的直线,它的斜率k为无穷大,截距b无法取值,可以将笛卡尔坐标系映射到极坐标系上。

霍夫直线变换

函数**cv2.HoughLines()**可以用来实现霍夫直线变换。该函数要求所操作的原图像是一个二值图像,所以在进行霍夫变换之前要先将原图像进行二值化或者进行Canny边缘检测。该函数的语法格式为:

lines = cv2.HoughLines(img, rho, theta, threshold)

  • lines中的每个元素都是一对浮点数,表示检测到的直线的参数,即(r, θ)
  • img为原图像,必须是8位单通道二值图像
  • rho是以像素为单位的距离r的精度,一般情况下设置为1
  • theta是角度θ的精度,一般情况下设置为π/180,表示要搜索所有可能的角度
  • threshold是阈值。该值越小,判断出的直线就越多
img = cv2.imread('image/computer.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edge = cv2.Canny(gray, 50, 150, apertureSize=3)  # Canny边缘检测
rgbImg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
rgbCopy = rgbImg.copy()
lines = cv2.HoughLines(edge, 1, np.pi / 180, 140)  # 霍夫直线变换
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)
    cv2.line(rgbImg, (x1, y1), (x2, y2), (0, 0, 255), 2)
plt.subplot(121)
plt.imshow(rgbCopy), plt.axis('off')
plt.subplot(122)
plt.imshow(rgbImg), plt.axis('off')
plt.show()

概率霍夫变换

为了更好地判断直线(线段),概率霍夫变换算法还对选取直线的方法作了两点改进:

  • 接受直线的最小长度。如果有超过阈值个数的像素点构成了一条直线,但是这条直线很短,那么就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原图中并不存在这条直线。

  • 接受直线时允许的最大像素点间距。如果有超过阈值个数的像素点构成了一条直线,但是这组像素点之间的距离都很远,就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原图中并不存在这条直线。

函数**cv2.HoughLinesP()**可以用来实现霍夫直线变换。该函数要求所操作的原图像是一个二值图像,所以在进行霍夫变换之前要先将原图像进行二值化或者进行Canny边缘检测。该函数的语法格式为:

lines = cv2.HoughLinesP(img, rho, theta, threshold, minLineLength, maxLineGap)

  • lines中的每个元素都是一对浮点数,表示检测到的直线的参数,即(r, θ)

  • img为原图像,必须是8位单通道二值图像

  • rho是以像素为单位的距离r的精度,一般情况下设置为1

  • theta是角度θ的精度,一般情况下设置为π/180,表示要搜索所有可能的角度

  • threshold是阈值。该值越小,判断出的直线就越多

  • minLineLength用来控制"接受直线的最小长度"的值

  • maxLineGap用来控制"接受直线时允许的最大像素点间距"

img = cv2.imread('image/computer.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edge = cv2.Canny(gray, 50, 150, apertureSize=3)  # Canny边缘检测
rgbImg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
rgbCopy = rgbImg.copy()
lines = cv2.HoughLinesP(edge, 1, np.pi / 180, 160, minLineLength=100, maxLineGap=10)  # 概率霍夫变换
for line in lines:
    x1, y1, x2, y2 = line[0]
    cv2.line(rgbImg, (x1, y1), (x2, y2), (255, 255, 255), 2)
plt.subplot(121)
plt.imshow(rgbCopy), plt.axis('off')
plt.subplot(122)
plt.imshow(rgbImg), plt.axis('off')
plt.show()

霍夫圆变换

在霍夫圆变换中需要考虑圆半径和圆心(x坐标、y坐标)共三个参数。在OpenCV中采用的策略是两轮筛选,第一轮筛选找出可能存在的圆的位置(圆心),第二轮再根据第一轮的结果筛选出半径大小。

函数**cv2.HoughCircles()**可以用来实现霍夫圆变换。该函数要求所操作的原图像是一个二值图像,所以在进行霍夫变换之前要先将原图像进行二值化或者进行Canny边缘检测。该函数的语法格式为:

circles = cv2.HoughCircles(img, method, dp, minDist, param1, param2, minRadius, maxRadius)

  • circles :由圆心坐标和半径构成

  • img:原图像,类型为8位单通道灰度图像

  • method:检测方法,设置为cv2.HOUGH_GRADIENT

  • dp:累计器分辨率,它是一个分割比率,用来指定图像分辨率与圆心累加器分辨率的比例

  • minDist:圆心间的最小间距,该值被当作阈值使用

  • param1:它对应的是Canny边缘检测器的高阈值,在缺省时默认值为100

  • param2:圆心位置必须收到的投票数。只有在第一轮筛选过程中投票数超过该值的圆才有资格进入第二轮的筛选,在缺省时默认值为100

  • minRadius:圆半径的最小值,小于该值的圆不会被检测出来。在缺省时默认值为0,该参数不起作用

  • maxRadius:圆半径的最大值,大于该值的圆不会被检测出来。在缺省时默认值为0,该参数不起作用

img = cv2.imread('image/chess.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
rgbImg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
rgbCopy = rgbImg.copy()
gray = cv2.medianBlur(gray, 5)
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, 300, param1=50, param2=30, minRadius=100, maxRadius=200)  # 霍夫圆变换
circles = np.uint16(np.around(circles))
for i in circles[0, :]:
    cv2.circle(rgbImg, (i[0], i[1]), i[2], (255, 0, 0), 12)
    cv2.circle(rgbImg, (i[0], i[1]), 2, (255, 0, 0), 12)
plt.subplot(121)
plt.imshow(rgbCopy), plt.axis('off')
plt.subplot(122)
plt.imshow(rgbImg), plt.axis('off')
plt.show()

图像分割与提取

距离变换函数

当图像内的各个子图没有连接时,可以直接使用形态学的腐蚀操作确定前景对象,但是如果图像内的子图连接在一起时,就很难确定前景对象了。此时借助距离变换函数**cv2.distanceTransform()**可以方便地将前景对象提取出来。

距离变换函数**cv2.distanceTransform()**计算二值图像内任意点到最近背景点的距离。一般情况下,该函数计算的是图像内非零值像素点到最近的零值像素点的距离,即计算二值图像中所有像素点距离其最近的值为0的像素点的距离。如果像素点本身的值为0,则这个距离也为0。

距离变换函数**cv2.distanceTransform()**的计算结果反映了各个像素与背景(值为0的像素点)的距离关系。通常情况下:

  • 如果前景对象的中心(质心)距离值为0的像素点距离较远,会得到一个较大的值。

  • 如果前景对象的边缘距离值为0的像素点较近,会得到一个较小的值。

如果对上述计算结果进行阈值化,就可以得到图像内子图的中心、骨架等信息。距离变换函数可以用于计算对象的中心,还能细化轮廓、获取图像前景等,有多种功能。

距离变换函数**cv2.distanceTransform()**的语法格式为:

cv2.distanceTransform(img, distanceType, maskSize)

  • img是8位单通道二值图像
  • distanceType是距离类型参数,具体值和含义如下图所示
  • maskSize是掩模的尺寸,当distanceType = cv2.DIST_L1或cv2.DIST_C时,maskSize强制为3
含义
cv2.DIST_USER 用户定义距离
cv2.DIST_L1 distance = |x1-x2|+|y1-y2|
cv2.DIST_L2 欧式距离,此时与最小二乘法相同
cv2.DIST_C distance =max(|x1-x2|, |y1-y2|)
cv2.DIST_L12 distance = 2(sqrt(1+x*x/2) - 1))
cv2.DIST_FAIR distance = c2(|x|/c-log(1+|x|/c)), c = 1.3998
cv2.DIST_WELSCH distance =c2/2(1-exp(-(x/c)2)), c = 2.9846
cv2.DIST_HUBER distance =|x|
img = cv2.imread('image/water_coins.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
imgCopy = img.copy()
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)  # 开运算
distanceTransform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)  # 距离变换
ret, fore = cv2.threshold(distanceTransform, 0.7 * distanceTransform.max(), 255, 0)
plt.subplot(131)
plt.imshow(imgCopy), plt.axis('off')
plt.subplot(132)
plt.imshow(distanceTransform), plt.axis('off')
plt.subplot(133)
plt.imshow(fore), plt.axis('off')
plt.show()

标注图像

函数**cv2.connectedComponents()**可以用来标注。该函数会将背景标注为0,将其他的对象使用从1开始的正整数标注。该函数的语法格式为:

retval, labels = cv2.connectedComponents(img)

  • retval为返回的标注的数量
  • labels为标注的结果图像
  • img为8位单通道的待标注图像
img = cv2.imread('image/water_coins.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
imgCopy = img.copy()
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)  # 开运算
sure_bg = cv2.dilate(opening, kernel, iterations=3)  # 确定背景
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, fore = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)
fore = np.uint8(fore)  # 前景图像的中心点图像
ret, markers = cv2.connectedComponents(fore)  # 对中心点图像进行标注
plt.subplot(131)
plt.imshow(imgCopy), plt.axis('off')
plt.subplot(132)
plt.imshow(fore), plt.axis('off')
plt.subplot(133)
plt.imshow(markers), plt.axis('off')
plt.show()

函数**cv2.connectedComponents()在标注图像时,会将背景标注为0,将其他的对象用从1开始的正整数标注。在分水岭算法中,标注值0代表未知区域,所以我们要对函数cv2.connectedComponents()**标注的结果进行调整:将标注的结果都加上数值1。为了能够使用分水岭算法,还需要对原始图像内的未知区域进行标注,将已经计算出来的未知区域标注为0即可。这里的关键代码为:

ret, markers = cv2.connectedComponents(fore)
markers = markers + 1
markers[未知区域] = 0
img = cv2.imread('image/water_coins.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)  # 开运算
sure_bg = cv2.dilate(opening, kernel, iterations=3)  # 确定背景
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, fore = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)
fore = np.uint8(fore)
ret, markers1 = cv2.connectedComponents(fore)
foreAdv = fore.copy()
unknown = cv2.subtract(sure_bg, foreAdv)  # 未知区域
ret, markers2 = cv2.connectedComponents(foreAdv)
markers2 = markers2 + 1  # 修正标注结果
markers2[unknown == 255] = 0
plt.subplot(121)
plt.imshow(markers1), plt.axis('off')
plt.subplot(122)
plt.imshow(markers2), plt.axis('off')
plt.show()

分水岭算法实现图像分割与提取

步骤:

  1. 通过开运算对原始图像O去噪
  2. 通过腐蚀操作获取确定背景B
  3. 利用距离变换函数**cv2.distanceTransform()**对原始图像进行运算,并进行阈值处理,得到确定前景F
  4. 计算未知区域UN(UN = O - B - F)
  5. 利用函数**cv2.connectedComponents()**对原始图像O进行标注
  6. 对函数**cv2.connectedComponents()**的标注结果进行修正
  7. 使用分水岭函数完成对图像的分割

函数**cv2.watershed()**可以用来实现分水岭算法。该函数的语法格式为:

cv2.watershed(img, markers)

  • img是8位三通道的图像。在对图像使用**cv2.watershed()**函数处理之前,必须先用正数大致勾画出图像中的期望分割区域。每一个分割的区域会被标注为1、2、3等。对于尚未确定的区域,需要将它们标注为0,我们可以将标注区域理解为进行分水岭算法分割的"种子"区域。

  • markers是32位单通道的标注结果。在markers中,每一个像素要么被设置为初期的种子值,要么被设置为"-1"表示边界。markers可以省略。

img = cv2.imread('image/water_coins.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
imgCopy = img.copy()
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)  # 开运算
sure_bg = cv2.dilate(opening, kernel, iterations=3)  # 确定背景
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)
sure_fg = np.uint8(sure_fg)  # 确定前景
unknown = cv2.subtract(sure_bg, sure_fg)  # 未知区域
ret, markers = cv2.connectedComponents(sure_fg)
markers = markers + 1  # 修正标注结果
markers[unknown == 255] = 0
markers = cv2.watershed(img, markers)  # 分水岭算法
img[markers == -1] = [0, 255, 0]
plt.subplot(121)
plt.imshow(imgCopy), plt.axis('off')
plt.subplot(122)
plt.imshow(img), plt.axis('off')
plt.show()

交互式前景提取

函数**cv2.grabCut()**可以用来实现交互式前景提取。该函数的语法格式为:

mask, bgdModel, fgdModel = cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode])

  • img是8位3通道的图像

  • mask是8位单通道的掩模图像,用于确定前景区域、背景区域和不确定区域,可以设置为4种形式:

    • cv2.GC_BGD:表示确定背景,也可以用数值0表示
    • cv2.GC_FGD:表示确定前景,也可以用数值1表示
    • cv2.GC_PR_BGD:表示可能的背景,也可以用数值2表示
    • cv2.GC_PR_FGD:表示可能的前景,也可以用数值3表示
  • rect是包含前景对象的区域,该区域外的部分被认为是确定背景。只有当参数mode的值设置为矩形模式cv2.GC_INIT_WITH_RECT时,参数rect才有意义。使用掩模模式时,将该值设置为None即可。

  • bgdModel为算法内部使用的数组,只需要创建大小为(1, 65)的numpy.float64数组

  • fgdModel为算法内部使用的数组,只需要创建大小为(1, 65)的numpy.float64数组

  • iterCount表示迭代的次数

  • mode表示迭代模式

GrabCut算法提取图像的前景(不使用掩模)

img = cv2.imread('image/lena.png')
rgbImg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
mask = np.zeros(img.shape[:2], np.uint8)  # 将掩模设置为0
bgdModel = np.zeros((1, 65), np.float64)
fgdModel = np.zeros((1, 65), np.float64)
rect = (50, 50, 400, 400)
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
ogc = img * mask2[:, :, np.newaxis]
ogc = cv2.cvtColor(ogc, cv2.COLOR_BGR2RGB)
plt.subplot(121)
plt.imshow(rgbImg), plt.axis('off')
plt.subplot(122)
plt.imshow(ogc), plt.axis('off')
plt.show()

GrabCut算法提取图像的前景(使用标记的掩模)

img = cv2.imread('image/lena.png')
rgbImg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
mask = np.zeros(img.shape[:2], np.uint8)
bgd = np.zeros((1, 65), np.float64)
fgd = np.zeros((1, 65), np.float64)
rect = (50, 50, 400, 500)
cv2.grabCut(img, mask, rect, bgd, fgd, 5, cv2.GC_INIT_WITH_RECT)
mask2 = cv2.imread('image/lenaMask.png', 0)
mask2Show = cv2.imread('image/lenaMask.png')
m2rgb = cv2.cvtColor(mask2Show, cv2.COLOR_BGR2RGB)
mask[mask2 == 0] = 0  # 将黑色值映射到掩模上
mask[mask2 == 255] = 1  # 将白色值映射到掩模上
mask, bgd, fgd = cv2.grabCut(img, mask, None, bgd, fgd, 5, cv2.GC_INIT_WITH_MASK)
mask = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
ogc = img * mask[:, :, np.newaxis]
ogc = cv2.cvtColor(ogc, cv2.COLOR_BGR2RGB)
plt.subplot(121)
plt.imshow(m2rgb), plt.axis('off')
plt.subplot(122)
plt.imshow(ogc), plt.axis('off')
plt.show()

视频处理

VideoCapture类

  1. 初始化

**cv2.VideoCapture()**函数可以用于打开摄像头并初始化。

初始化当前的摄像头

cap = cv2.VideoCapture(0)

初始化视频文件

cap = cv2.VideoCapture("test.avi")
  1. 检查初始化是否成功

可以使用函数**cv2.VideoCapture.isOpened()**检查初始化是否成功。

retval = cv2.VideoCapture.isOpened()
  • 若成功,返回值retval为True
  • 若失败,返回值retval为False

如果初始化失败,可以使用函数**cv2.VideoCapture.open()**打开摄像头。

retval = cv2.VideoCapture.open(0)

函数**cv2.VideoCapture.isOpened()和函数cv2.VideoCapture.open()**也能用于处理视频文件。

  1. 捕获帧
retval, image = cv2.VideoCapture.read()
  1. 释放

若当前有一个VideoCapture类的对象cap,要将其释放,可以使用语句

cap.release()
  1. 属性设置

函数**cv2.VideoCapture.get()**用于获取cv2.VideoCapture类对象的属性。该函数的语法格式为:

retval = cv2.VideoCapture.get(propId)

  • propId对应着cv2.VideoCapture类对象的属性

函数**cv2.VideoCapture.set()**用于设置cv2.VideoCapture类对象的属性。该函数的语法格式为:

retval = cv2.VideoCapture.set(propId, value)

propId 含义
cv2.CV_CAP_PROP_FRAME_WIDTH 3 帧的宽度
cv2.CV_CAP_PROP_FRAME_HEIGHT 4 帧的高度
cv2.CV_CAP_PROP_FPS 5 帧速
  1. 捕获摄像头视频
cap = cv2.VideoCapture(0)
while True:
    success, img = cap.read()
    cv2.imshow("Video", img)
    if cv2.waitKey(1) & 0XFF == 27:
        break

VideoWriter类

OpenCV中的cv2.VideoWriter类可以将图片序列保存为视频文件,也可以修改视频的各种属性,还可以完成对视频类型的转换。

  1. 初始化

cv2.VideoWriter(filename, fourcc, fps, frameSize)

  • filename指定输出目标视频的存放路径和文件名

  • fourcc表示视频编解码格式

  • fps为帧速率

  • frameSize为每一帧的长和宽

  1. write函数

**cv2.VideoWriter.write(img)**用于写入下一帧视频,若当前有一个VideoWriter类的对象out,要将一个视频帧frame写入,可以使用语句

out.write(frame)
  1. 释放

若当前有一个VideoWriter类的对象out,要将其释放,可以使用语句

out.release()
  1. 保存摄像头视频
cap = cv2.VideoCapture(0)
fourcc = cv2.VideoWriter_fourcc(*'XVID')  # 保存视频的编码
out = cv2.VideoWriter('output.avi', fourcc, 20, (640, 480))
while cap.isOpened():
    success, img = cap.read()
    if success:
        out.write(img)
        cv2.imshow('frame', img)
        if cv2.waitKey(1) == 27:
            break
    else:
        break
cap.release()
out.release()
cv2.destroyAllWindows()

绘画及交互

绘画基础

  1. 绘制直线

img = cv2.line(img, pt1, pt2, color, thickness[, lineType])

  • pt1表示线段的起点
  • pt2表示线段的终点
  • color表示绘制的颜色
  • thickness表示线条的粗细
  • lineType表示线条的类型
img = cv2.line(img, (50, 50), (300, 300), (255, 0, 0), 2)
  1. 绘制矩形

img = cv2.rectangle(img, pt1, pt2, color, thickness[, lineType])

  • pt1表示矩形的顶点
  • pt2表示矩形中与pt1对角的顶点
  • color表示绘制的颜色,-1代表填充为实心
  • thickness表示线条的粗细
  • lineType表示线条的类型
img = cv2.rectangle(img, (50, 50), (300, 300), (255, 0, 0), 2)
  1. 绘制圆形

img = cv2.circle(img, center, radius, color, thickness[, lineType])

  • center为圆心坐标
  • radius为半径
  • color表示绘制的颜色,-1代表填充为实心
  • thickness表示线条的粗细
  • lineType表示线条的类型
img = cv2.circle(img, (200, 200), 50, (255, 0, 0), 2)
  1. 绘制椭圆

img = cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness[, lineType])

  • center为椭圆的圆心坐标
  • axes为轴的长度
  • angle为偏转的角度
  • startAngle为圆弧起始角的角度
  • endAngle为圆弧终结角的角度
  • color表示绘制的颜色,-1代表填充为实心
img = cv2.ellipse(img, (200, 200), (200, 100), 30, 0, 360, (255, 0, 0), 2)
  1. 绘制多边形

img = cv2.polylines(img, pts, isClosed, color, thickness[, lineType])

  • pts为多边形的各个顶点
  • isClosed为闭合标记,用来指示多边形是否是封闭的
pts = np.array([[200, 50], [300, 200], [200, 350], [100, 200]], np.int32)
pts = pts.reshape((-1, 1, 2))
img = cv2.polylines(img, [pts], True, (255, 0, 0), 2)
  1. 绘制文字

img = cv2.putText(img, text, org, fontFace, fontScale, color)

  • text为多边形的各个顶点
  • org为绘制文字的位置,以文字的左下角为起点
  • fontFace表示字体类型
  • fontScale表示字体大小
  • color表示绘制的颜色
img = cv2.putText(img, "OpenCV", (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 0, 0), 2)

滚动条

函数**cv2.createTrackbar()**用来定义滚动条,其语法格式为:

cv2.createTrackbar(trackbarName, windowName, value, count, onChange)

  • trackbarName为滚动条的名称
  • windowName为滚动条所依附窗口的名称
  • value为初始值
  • count为滚动条的最大值
  • onChange为回调函数

滚动条的值可以通过函数**cv2.getTrackbarPos()**获取,其语法格式为:

retval = cv2.getTrackbarPos(trackbarname, winname)

  • retval为返回值,获取滚动条的值
  • trackbarname为滚动条的名称
  • winname为滚动条所依附窗口的名称

用滚动条实现调色板

def changeColor(x):
    r = cv2.getTrackbarPos('R', 'image')
    g = cv2.getTrackbarPos('G', 'image')
    b = cv2.getTrackbarPos('B', 'image')
    img[:] = [b, g, r]


img = np.zeros((100, 500, 3), np.uint8)
cv2.namedWindow('image')
cv2.createTrackbar('R', 'image', 0, 255, changeColor)
cv2.createTrackbar('G', 'image', 0, 255, changeColor)
cv2.createTrackbar('B', 'image', 0, 255, changeColor)
while True:
    cv2.imshow('image', img)
    if cv2.waitKey(0) & 0XFF == 27:
        cv2.destroyAllWindows()

你可能感兴趣的:(OpenCV,计算机视觉,学习笔记,opencv,计算机视觉)