OpenCV - Advanced
or "CV with OpenCV"
1. Image Masking
Image Masking,图像掩模,用于提取ROI(Region of Interest)或屏蔽某些区域
例如要提取图中蓝色的部分,下图中,三个窗口从左到右依次是:原图,生成的mask,应用了mask的原图

1.1 制造Mask
-
cv2.inRange()抓取色彩ROI该函数可以指定某种颜色的最低数值与最高数值,然后抓取该色彩范围内所有的像素生成新的图片(即所谓的Mask)
xxxxxxxxxx101mask = cv2.inRange(img, lowerb, upperb)2# 参数3# img 被抓取的图片4# clr_lower 目标颜色的lower bound,高于这个值的pixel变黑(=0)5# clr_upper 目标颜色的upper bound,低于这个值的pixel变黑(=0)67# 在upper和lower bound之间的pixel变白(=255)89# 返回值 mask10# a binary image(mask) where pixels are white(255) for colors detected, and black(0) otherwise -
手 搓~屏蔽特定区域
即直接照着原图的尺寸和数据类型用
np.zeros()摁造一个maskxxxxxxxxxx31# 我们假设被抓取的是张叫img的uint8类型的图片2mask = np.zeros(img.shape, dtype = uint8) # 一张一模一样的全黑图片3mask[114:514, 191:981] = 255 # 开个恶臭天窗,作为抓取范围( -
WARNING
这俩mask虽然你用
mask.shape()一查,诶嘿,一模一样!但是它们实际上不一样!
用
cv2.inRange()产生的mask,打印出来格式是这样的:xxxxxxxxxx71[[0 0 0 ... 0 0 0]2[0 0 0 ... 0 0 0]3[0 0 0 ... 0 0 0]4...5[0 0 0 ... 0 0 0]6[0 0 0 ... 0 0 0]7[0 0 0 ... 0 0 0]]用
np.zeros()产生的mask,打印出来格式大致是这样的:xxxxxxxxxx171[[[0 0 0]2[0 0 0]3[0 0 0]4...5[0 0 0]6[0 0 0]7[0 0 0]]89...1011[[0 0 0]12[0 0 0]13[0 0 0]14...15[0 0 0]16[0 0 0]17[0 0 0]]]只有
np.zeros()通过复刻原图格式做出来的mask,才真正做到了与原图一模一样!!!就是这点区别导致这两种mask在使用的时候方法也不一样
1.2 施加Mask
cv2.bitwise_and() - 对两个数组进行位与运算,可以增加图像掩模进行masking
xxxxxxxxxx 9 1 neo_img = cv2.bitwise_and(img1, img2, mask = msk) 2 # 参数 3 # img1 进行位与运算的图片1 4 # img2 进行位与运算的图片2,与图片1的尺寸和类型相同 5 # msk 施加的掩模图像(Optional),怎么起作用的暂时不明,但是能用(半恼 6 # 图片1与图片2之间每个pixel都会进行位与运算 7
8 # 返回值 neo_img 9 # 输出的图像,与图片1的尺寸和类型相同 在2.1.1中介绍了两种mask在施加时要遵循不同的方法,以下便是示例:
xxxxxxxxxx 37 1 import numpy as np 2 import cv2 3
4 cap = cv2.VideoCapture(0) # 视频初始化 5
6 # 追踪视频中蓝色的区域 7 lower_blue = np.array([100, 110, 110]) # 设置蓝色的lower bound 8 upper_blue = np.array([130, 255, 255]) # 设置蓝色的upper bound 9
10 while True: 11 # 读帧并转化到HSV色彩空间 12 ret, frame = cap.read() 13 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) 14
15 # 追踪蓝色区域的mask1 16 mask1 = cv2.inRange(hsv, lower_blue, upper_blue) 17 18 # 截取屏幕的某个部分的mask2 19 mask2 = np.zeros(frame.shape, dtype=np.uint8) 20 mask2[100:200, 200:400] = 255 # 截取的部分设成白色 21
22 # 施加mask 23 res1 = cv2.bitwise_and(frame, frame, mask=mask1) 24 # mask1与frame格式不一样,只能写在mask参数上 25 # frame和自己进行位与运算,结果仍然是自己 26 res2 = cv2.bitwise_and(frame, mask2) 27 # mask2与frame格式一样,直接做位与运算就可以 28 # 但是它不能像mask1一样写在mask参数上,然后让frame与自己做位与运算 29
30 cv2.imshow('res1', res1) 31 cv2.imshow('res2', res2) 32
33 if cv2.waitKey(1) == ord('q'): 34 break 35 # 善后工作 36 cap.release() 37 cv2.destroyAllWindows() 当然,想想都知道,除了
cv2.bitwise_and()还有些其他的位运算函数:
cv2.bitwise_or()
cv2.bitwise_not()
cv2.bitwise_xor()
2. Image Thresholding
Image Thresholding,图像阈值分割,经典的图像分割方法;常见的做法是在图像的灰度图上,通过划定一个或几个不同的灰度阈值来把属于同一个灰度级的像素归类为同一个物体。下图是阈值分割的一种(二值化)

2.1 固定阈值分割
设置一个色彩的阈值threshold,pixel值大于threshold的变成一类值,pixel值小于threshold的变成另一类值
这不完全是“二值化”,而是“二类值化”
cv2.threshold()最基本的thresholding的实现方式
xxxxxxxxxx 12 1 src_img = cv2.imread("hao_kang_de.png", 0) # 以gray scale的方式读入图片数据 2
3 ret, threshed_img = cv2.threshold(src_img, thresh, maxval, type) 4 # 参数 5 # src_img - 源图片,必须是单通道图片 6 # thresh - 阈值,一般取值范围[0, 255] 7 # maxval - 填充色,一般取值范围[0, 255] 8 # type - 阈值分割类型,主要5种 9
10 # 返回值 11 # ret - 阈值(如果指定了阈值就返回指定的值,如果设定为Otsu算法,那就会返回算法找到的最优阈值) 12 # threshed_img - 处理好的图像 -
Threshold Types 阈值分割类型

xxxxxxxxxx101# cv2 args 对应号码2cv2.THRESH_BINARY 0 # Threshold Binary3cv2.THRESH_BINARY_INV 1 # Threshold Binary inverted4cv2.THRESH_TRUNC 2 # Truncate5cv2.THRESH_TOZERO 3 # Threshold to Zero6cv2.THRESH_TOZERO_INV 4 # Threshold to Zero inverted78# 两种写法都可9cv2.threshold(src_img, thresh, maxval, type = 0)10cv2.threshold(src_img, thresh, maxval, cv2.THRESH_BINARY)阈值分割类型里还有些其他的不属于固定阈值分割的,比如大津算法自适应阈值
cv.THRESH_OTSU,参考Otsu Method部分以下是5种不同Threshold Type的波形图与数学解释,附带与原图的对比

所谓的
maxval“填充色”,就是由阈值分割类型决定用法的指定色彩
2.2 自适应阈值分割
自适应阈值分割的核心是将图片分割为不同的“小区域”,每个区域都计算阈值,这样可以更好地处理复杂的图像:比如固定阈值分割就无法处理明暗分布不均的图像
cv2.adaptiveThreshold() - 自适应阈值分割函数
xxxxxxxxxx 12 1 threshed_img = cv2.adaptiveThreshold(src_img, maxval, adapt_method, thresh_type, block_size, C, dist) 2
3 # 参数 4 # src_img 源图片,必须为单通道 5 # maxval 最大阈值,一般为255 6 # adapt_method 自适应方法(应用于小区域) 7 # thresh_type 阈值分割类型 8 # block_size 每个小区域的大小,取奇数(如11就是11*11的区域) 9 # C 常数,可为负,每个区域的阈值减去该常数后为区域最终阈值 10 # dist (Optional)destination image,与src_img有相同的size和type,一般没人设置这个 11
12 # 返回值 threshed_img 自适应分割好的图像数据 关于一些参数的解释:
-
adapt_method自适应方法有两种cv2.ADAPTIVE_THRESH_MEAN_C- 小区域内取均值cv2.ADAPTIVE_THRESH_GAUSSIAN_C- 小区域内取高斯加权和
一般推荐用高斯加权和
thresh_type阈值分割类型只能设置为cv2.THRESH_BINARY和cv2.THRESH_BINARY_INV两种
2.3 Otsu桑のAlgorithm
Otsu Algorithm是一种通过最大化类间方差求得灰度图的自适应阈值的阈值分割算法,提出者为大津展之
大津算法认定一张图由“前景色”和“背景色”组成(需要分离的内容,即前文提到的“类”),下图是一种符合该描述的极端案例,如果在gray-scale space中分析pixel的分布(以灰度值为x-axis,pixel数量为y-axis),我们会发现这些图片都是所谓的“双峰图”

Pros:求全局阈值的最佳算法;速度快且不受亮度和对比度影响
-
Cons:
对图像噪点相当敏感(过滤下再用)
前景与背景大小比例差距悬殊(会导致双峰不明显)时效果欠佳
只能分割单一目标(双峰上手,多峰苦手)

-
用法:
直接在一般阈值分割函数
cv2.threshold()的阈值分割类型选cv.THRESH_OTSU就好xxxxxxxxxx41img = cv.imread("hao_kang_de.jpg")2gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)3ret, threshed_img = cv.threshold(gray, 0, 255, cv.THRESH_OTSU)4print(ret) # 可以看下得出来的阈值 -
算法推导与解释:
我们以
uint8图片为基础,pixel的灰度值取值范围为[0, 255]有阈值为
时,将图像中所有的pixel分类为Class_0( )与Class_1( ),则可用 表示Class_0和Class_1的pixels分别在全图中出现的概率,或所有pixel中的占比: 代表灰度值为 的pixel在全图中出现的概率(或所有pixel中的占比) 分别代表Class_0,Class_1和全图中pixel的数量那么Class_0,Class_1和全图的平均灰度值
分别为:以上信息可以推得以下公式:
大津算法所使用的类间方差
的定义如下:将公式(1) (2)代入公式(3)化简可得:
然后你就能step through all possible thresholds t = 0, 1, ... , 255, 能maximize公式(4)的 t 就是我们要的答案
你以为这就结束了?根据大津展之的原文,公式(4)能进一步化简变量:将到阈值
的pixel在全图中的累加均值记为 ,将公式(e) (1) (5)代入
后可得:将公式(6) (7)代入公式(4)可得到Otsu桑の最终类间方差公式:
配合公式(a) (e) (5),能maximize公式(8)的 t 就是所求的阈值
3. Image Convolution
二维图像卷积是用来给图片滤波/模糊化的,本部分是给2.5部分铺路
3.1 2D Convolution 二维卷积
二维卷积需要循环如下的步骤:
在原图(Input)中框出卷积核(Kernel)同样大小的区域
将该区域与卷积核逐个元素相乘后求和
将该和放在结果图(Output)中与原图框出区域相同的位置
将原图中的框移动一个像素
卷积核的边长一般为奇数(不是奇数会很怪不是吗

以上图为例,结果图红框中的“5”由原图中蓝色实线框的区域与卷积核计算得来:
而红框中的“5”右侧的“4”则是由原图中蓝色虚线框以相同的步骤计算的来的
3.2 Padding “填充”
-
你干嘛~嗨害哎呦~
上面那张图里可以看出:如果我们用一个
的kernel去卷一个 的原图,输出的是一个 的结果图,这是极坏的(如何保证卷完了尺寸还是原来的大小?简单,在原图外加“边框”!
原图为
,kernel为 ,那么结果图就是 ,所以扩充的“边框”层数为
然后卷积过程就会变得肥肠nice:

-
“加个相框叭”
cv2.copyMakeBorder()- 复制图像,并在该副本上添加paddingxxxxxxxxxx61neo = cv2.copyMakeBorder(src, top, bottom, left, right, borderType, value)2# 参数3# src 复制并加工的对象图片4# top/bottom/left/right 每个方向要扩展的pixel数5# borderType padding类型,默认cv2.BORDER_DEFAULT6# value (Conditional)如果borderType为cv2.BORDER_CONSTANT,它就是边框的颜色值padding有如下几种类型:
xxxxxxxxxx111# 示例格式:border | picture | border2cv2.BORDER_CONSTANT # pixel的颜色值为constant,需要指定参数中的value(比如0)3# 00000 | 12345678 | 000004cv2.BORDER_REPLICATE # 使用边界像素代替5# 11111 | 12345678 | 888886cv2.BORDER_REFLECT # 以边界线为法线,反射颜色值7# 54321 | 12345678 | 876548cv2.BORDER_DEFAULT # 以边界线内的第一个像素为法线,反色颜色值9# 65432 | 12345678 | 7654310cv2.BORDER_WRAP # 类似于”滚动边界”11# 45678 | 12345678 | 12345
3.3 如何用OpenCV卷积
cv2.filter2D()可以用于实现卷积操作(实际上就是滤波/模糊化)
比如定义具备如下效果的卷积核:
xxxxxxxxxx 7 1 kernel = np.ones((3, 3), np.float32) / 10 2
3 neo = cv2.filter2D(src, ddepth, kernel) 4 # 参数 5 # src 源图像 6 # ddepth 期望深度(直接填-1就对了,表示通道数与源图像相同) 7 # kernel 使用的卷积核 具体效果:

4. Image Filtering
-
关于滤波器
滤波属于卷积,对于线性滤波而言,不同的滤波方法的区别在于使用的卷积核不同
低通滤波器 - 图片模糊化 - 允许低频信号通过,而图像的边缘和噪点都属于高频信号,因此能平滑/模糊图像
高通滤波器 - 图片锐化 - 与低通滤波器相反,能强化图像边缘,突出细节
-
关于图像噪声
主要是以下两种:
-
Salt-and-pepper Noise 椒盐噪声
一般是出现在随机位置的白点或者黑点,噪点的深度基本固定

-
Gaussian Noise 高斯噪声
与椒盐噪声特性相反,高斯噪声几乎出现在每个点上,噪点深度完全随机,其概率密度函数服从高斯正态分布

-
-
关于下面介绍的滤波函数
每个滤波函数都有一个可选参数
borderType,这个详情请见2.4.2卷积的padding-
线性滤波:
均值滤波,方框滤波,高斯滤波
-
非线性滤波:
中值滤波,双边滤波
4.1 均值滤波cv2.blur()
取卷积核区域内元素的均值,以
xxxxxxxxxx 7 1 img = cv2.blur(src, ksize) 2 # 参数 3 # src 源图像 4 # ksize 卷积核的尺寸(x,y) 5
6 # 应用3*3的卷积核 7 neo = cv2.blur(img, (3, 3))
4.2 方框滤波cv2.boxFilter()
与均值滤波有相似性,以
如果可选参数normalize为True时,方框滤波就是均值滤波;为False时,方框滤波的输出相当于区域内元素的和
xxxxxxxxxx 6 1 img = cv2.boxFilter(src, ddepth, ksize, normalize) 2 # 参数 3 # src 源图像 4 # ddepth 期望深度(直接填-1就对了,表示通道数与源图像相同) 5 # ksize 卷积核的尺寸(x,y) 6 # normalize (Optional)specify whether the kernel is normalized by its area or not
4.3 高斯滤波cv2.GaussianBlur()
与4.1/2不同,高斯滤波的卷积核内并非每个值都一样,而是以pixel离中心的距离为标准进行对它们加权,离中心越远的pixel权重越小,和正态分布曲线是一样的,但是一般的高斯滤波会模糊掉图像的边缘

OpenCV中默认的
xxxxxxxxxx 10 1 img = cv2.GaussianBlur(src, ksize, sigmaX, sigmaY) 2 # 参数 3 # src 4 # ksize 卷积核的尺寸(x,y),必须为正奇数 5 # sigmaX/Y Standard Deviation in X/Y directions 6 # 如果只specify sigmaX,那sigmaY与sigmaX相同 7 # 如果两个数值都是0,那么它们的数值将由kernel size计算出来 8
9 # 应用3*3的卷积核 10 neo = cv2.GaussianBlur(img, (3, 3), 1)
4.4 中值滤波cv2.medianBlur()
将卷积核区域内的数排序后取中位数(median)作为输出值,这个原理使0和255这样的极端值很容易被消除,因此中值滤波可谓是对椒盐/斑点噪声の宝具(顺带一提它也能很好地保存边缘)
由于是非线性滤波,处理速度比其它几种线性的滤波方式要慢
xxxxxxxxxx 7 1 img = cv2.medianBlur(src, ksize) 2 # 参数 3 # src 源图像 4 # ksize 卷积核的尺寸x,必定是x*x的正方形,且x必须为奇数 5
6 # 应用3*3的卷积核 7 neo = cv2.medianBlur(img, 3)
4.5 双边滤波cv2.bilateralFilter()
双边滤波能在干死噪点的同时仍然keeping edge sharp,高斯滤波会糊边,因为it is just a function of space,它不考虑pixel的intensity,也不考虑pixel是不是在边缘
双边滤波除了有空间上的Gaussian Filter以外,还多了一个pixel (intensity) difference上的Gaussian Filter,以确保只有与central pixel颜色值相近的pixel被模糊化,而图像边缘上的pixel会有比较大的intensity variation
也是非线性滤波,也贼慢
xxxxxxxxxx 7 1 img = cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace) 2 # 参数 3 # src 源图片 4 # d 滤波时选的空间距离参数(filter size) 5 # filter过大(d>5)会让处理速度变慢,实时应用d=5,离线应用d=9 6 # sigmaColor/Space 懒得解释了,两个sigma参数值设置成一样的,差不多得了 7 # 数值过小(sigma<10)效果不好,数值过大(sigma>150)会让处理过的图片看起来像动漫
4.6 补充信息:Gaussian Kernel
OpenCV分两步计算二维的高斯卷积:
-
计算一维高斯卷积核
OpenCV的一维卷积公式类似于一维高斯函数:
cv2.getGaussianKernel会计算并返回 的Gaussian Filter Coefficientxxxxxxxxxx101k = cv2.getGaussianKernel(ksize, sigma)2# 参数3# ksize 一维高斯卷积核的长度,必须为正奇数4# sigma Gaussian Standard Deviation5# 若sigma<=0,sigma由ksize计算得来:sigma=0.3*((ksize-1)*0.5-1)+0.867# 制造1*3的高斯卷积核8gker = cv2.getGaussianKernel(3, 0)9>>> print(gker)10[[0.25][0.5][0.25]] -
分别进行水平/垂直卷积
水平:
垂直:
-
卷出来的矩阵乘起来
或者换种写法:
OpenCV中对小于
的高斯卷积核是直接计算好塞在数组里的,用这些会更快