附加题part3:图像预处理(去噪、形态学处理及轮廓提取)
图像预处理主要步骤:去噪 -> 肤⾊检测 -> ⼆值化 -> 形态学处理 -> 轮廓提取
tis:特别鸣谢基于OpenCV的手势识别完整项目(Python3.7)博主就是参考本文学习的,以下是博主学习的笔记与个人想法
一、获取手势(主函数)注解
cap = cv2.VideoCapture(0) #开摄像头
if __name__ == "__main__":
while(1):
ret, frame = cap.read()
frame = cv2.flip(frame, 2)
roi = pic.binaryMask(frame, x0, y0, width, height)
key = cv2.waitKey(1) & 0xFF
#按'j''l''u''j'分别将选框左移,右移,上移,下移
#按'q'键退出录像
if key == ord('i'):
y0 += 5
elif key == ord('k'):
y0 -= 5
elif key == ord('l'):
x0 += 5
elif key == ord('j'):
x0 -= 5
if key == ord('q'):
break
cv2.imshow('frame', frame)
cap.release()
cv2.destroyAllWindows() #关闭所有窗口
1、cv2.flip(frame, 2):图像翻转函数
- 第二个参数:
- 1:水平翻转(沿垂直轴)->实际上是:大于0就表示是沿y轴翻转
- 0:垂直翻转(沿水平轴)
- -1:水平垂直翻转(各做一次)
- 注意:这个函数只会对图片旋转,x和y轴永远固定(左上角为原点,向右为x轴,向左为y轴)
- 如果没有这一步,视频显示的刚好和我们左右对称
2、binaryMask:掩膜操作(详细内容见后,此处仅为概述)
(1)相当于一个遮光板,对不感兴趣的位置进行遮挡(物理意义上的遮挡)
(2)位运算
-
掩膜就是两幅图像之间进行的各种位运算操作
Operation Function AND(与) cv2.bitwise_and OR (或) cv2.bitwise_or XOR(异或) cv2.bitwise_xor NOT(非) cv2.bitwise_not 对圆形和正方形区域做四种运算如下:
-
这里再详细解释一下cv2.bitwise_and()函数(因为后面会用到)
原型:dst = cv2.bitwise_and(src1, src2[, mask])- 例子:cv2.bitwise_and(roi, roi, mask=skin)
- cv2.bitwise_and(img1, img2)即表示两图像的与运算
- 使用mask参数则为掩膜操作,roi只保留在mask对应位置中值被置为255的区域
3、cv2.waitKey(1) & 0xFF
- cv2.waitKey()函数:在一个给定的时间内(单位ms)等待用户按键触发
设置为waitKey(0) , 则表示程序会无限制的等待用户的按键事件 - & 0xFF:掩码操作,转换到ASCII码型
4、ord():用于获取键位ASCII值
二、去噪(滤波)部分
tis:该部分核心原理详见part2,这里直接使用openCV的内置函数完成
1、均值滤波
- blur = cv2.blur(roi, (3,3))
- 参数传入(a,b)大小的卷积核即可
2、高斯滤波
-
blur = cv2.GaussianBlur(roi, (3,3), 0)
-
传入参数:对传入的sigmaX,sigmaY分别产生两个一维卷积核,再分别再行和列上做卷积
-
传入参数为0或负数,则由ksize计算得到
sigmaX=0.3×[(ksize.width -1)×0.5-1]+0.8 sigmaY=0.3×[(ksize.height-1)×0.5-1]+0.8
实际上大多时候都是传入0
3、中值滤波
- blur = cv2.medianBlur(roi,5)
- 参数ksize是滤波核的大小,默认为正方形、且只能为奇数
4、双边滤波
- blur = cv2.bilateralFilter(img,9,75,75)
- 9为区域的直径(注意是直径,也就是行/列的值)
- 后面两个参数分别为:空间高斯函数标准差、灰度值相似性高斯函数标准差
三、掩膜函数内部剖析
def binaryMask(frame, x0, y0, width, height):
cv2.rectangle(frame,(x0,y0),(x0+width, y0+height),(0,255,0)) #画出截取的手势框图
roi = frame[y0:y0+height, x0:x0+width] #获取手势框图
cv2.imshow("roi", roi) #显示手势框图
res = skinMask(roi) #进行肤色检测
cv2.imshow("res", res) #显示肤色检测后的图像
return res
1、cv2.rectangle()
(1)绘制矩形的函数,传入参数:对角的两个点的坐标
(2)它的好兄弟们
- 画线:cv2.line(img,start_point,end_point,color,thickness=0)
- 画圆:cv2.circle(img,center,R,color,thickness=0)
- 画多边形:cv2.polylines(img,pts,isClosed,color,thickness=0)
- pts:点的集合,以列表的形式填入
- isClosed:多边形是否闭合,如果为False则不闭合,如果为True则闭合
- 添加文字:cv2.putText(img,text,org,fontFace,fontScale,color,thickness,lineType)
- 画椭圆(这玩意儿后面还会用到的哦):
cv2.ellipes(img,center,(a,b),direction,angle_start,angle_end,color,thickness)- (a,b):长轴和短轴
- direction:顺时针方向的旋转角度
- angle_start:画椭圆开始的角度
- angle_end:画椭圆结束的角度
2、一个小白易错点
- 博主自己写的时候出现了一个小问题,还蛮有意思的
roi = img[y0:y0+height, x0:x0+width] # 正确
roi = img[x0:x0+width, y0:y0+height] # 错误
- 在将图片看做二维数组时,理应是先行后列
包括在切片操作时,其实质都是数组 - 但是在cv2眼里,可以有x轴和y轴的概念,这个时候将坐标以元组的方式投喂给它就是合法的
3、加上双边滤波
(1)首先一点
- 这个在摄像头清晰的情况下可能效果不大
- 即:如果没有特别明显的高斯噪声、或模糊化前后差距不大,则直接跳过(能提高代码运行速度)
(2)当然你也可以测试一下
- 我们可以手动添加高斯噪声,看看模糊化的效果如何
# 添加高斯噪声
def add_peppersalt_noise(image, n=10000):
result = image.copy()
# 测量图片的长和宽
w, h =image.shape[:2]
# 生成n个椒盐噪声
for i in range(n):
x = np.random.randint(1, w)
y= np.random.randint(1, h)
if np.random.randint(0, 2) == 0 :
result[x, y] = 0
else:
result[x,y] = 255
return result
- 然后你就会得到:
- 哈哈,挺有意思的吧?模糊化的效果还是很不错的
(3)参数选取
- 为寻求简单可以设置为同样的一个值
- 设置的值越大,模糊化效果越好(博主两个都设置到了1000)
四、肤色检测及二值化处理
- 该部分篇幅较长,所以单独写了一篇,敬请关注附加题part4(๑╹◡╹)ノ"“”
五、形态学处理
tis:该部分可参考学习
python图像处理(八)——形态学运算之图像腐蚀与图像膨胀
OpenCV 图像处理之膨胀与腐蚀
1、图像的膨胀与腐蚀
(1)图像的膨胀
- 即在二值化图像中将白色区域扩大化
- 底层逻辑:
- 选取卷积核中非0元素(一般是全为1)所覆盖区域中最大值作为传出数据
- 即:若全为0,则输出为0;有一个不为0,输出不为0
- 函数原型:cv2.dilate(src, kernel, iterations)
src:原始图像
kernel:卷积核
iterations:迭代次数(进行膨胀的词数,默认为1)
卷积核越大,迭代次数越多,膨胀越明显
(2)图像的腐蚀
-
在二值化图像中将黑色区域扩大化
-
底层逻辑:
- 选取卷积核中非0元素(一般是全为1)所覆盖区域中最小值作为传出数据
- 即:有一个为0,输出就为0
-
函数原型:dst = cv2.erode(src, kernel, iterations)(参数和效果与膨胀类似)
2、开运算和闭运算
- 函数原型:dst = cv2.morphologyEx(src, status, kernel)
status = cv2.MORPH_OPEN为开运算
status = cv2.MORPH_CLOSE为闭运算
kernel为运算卷积核
(1)开运算
- 先腐蚀,再膨胀(看图就知道为啥叫开运算了)
- 效果:能消除毛刺的同时保留主体内容不被改变
(2)闭运算
- 先膨胀,后腐蚀(看图就知道为啥叫闭运算了)
- 效果:关闭主体内部的小孔,或物体上的小黑点
3、图像梯度运算
- 膨胀图像-腐蚀图像
- 效果能够得到图像轮廓
- 函数原型:dst = cv2.morphologyEx(src, cv2.MORPH_GRADIENT, kernel)
相较于开闭运算只是改了个参数而已(毕竟底层逻辑相同)
4、高帽和黑帽计算(先了解了一下)
(1)高帽/顶帽/礼帽计算
- 原图-开运算
- 得到要消除的毛刺信息
- 函数:把上面的参数改为cv2.MORPH_TOPHAT即可
(2)黑帽/底帽计算
- 原图-闭运算
- 得到原图像中亮度较暗的区域(二值化图像中)
5、代码书写
- 这里的形态学处理需要用到开运算(必须得加,效果很明显!!)
kernel = np.ones((9, 9), dtype=np.uint8)
mp = cv2.morphologyEx(rgb, cv2.MORPH_OPEN, kernel) # 开运算
cv2.imshow("morphological processing", mp)
- 注意:千万幻想着肤色检测和二值化处理中吞掉的部分手指边缘能够通过膨胀涨回来
(由此也可以看出肤色检测及二值化处理模型正确选取的重要性)
6、效果展示
六、轮廓提取
binaryimg = cv2.Canny(res, 50, 200) #二值化,canny检测
h = cv2.findContours(binaryimg,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE) #寻找轮廓
contours = h[1] #提取轮廓
ret = np.ones(res.shape, np.uint8) #创建黑色幕布
cv2.drawContours(ret,contours,-1,(255,255,255),1) #绘制白色轮廓
cv2.imshow("ret", ret)
1、相关函数注解
(1)cv2.Canny()函数:边缘检测函数
①内部逻辑
tis:这部分博主写得比较简单,想要细致了解这个函数的底层原理,可以参考OpenCV——Canny边缘检测cv2.Canny()进行学习
- first:去噪(通常采用高斯滤波去除图像中的噪声)
- second:计算梯度的幅度与方向
- 方向一般简化为8个方向(水平(左、右)、垂直(上、下)、对角线(右上、左上、左下、右下))
- 梯度的方向与边缘的方向是垂直的
- third:非极大值抑制(能有效细化边缘)
- 只保留下降/上升速率最大值点,而将所有斜率非极大值的点全部设为0
- last:双阈值确定最终边缘
- 判定条件(高阈值maxVal,低阈值minVal):
- 梯度 >= maxVal:强边缘
- maxVal > 梯度 > minVal:虚边缘(需要保留)
- 梯度 <= minVal:直接抑制当前边缘像素
- 强边缘:直接保留
虚边缘:- 与强边缘相连:保留
- 不与强边缘相连:抑制
- 判定条件(高阈值maxVal,低阈值minVal):
②函数原型
cv.Canny(image, minVal, maxVal[, apertureSize[, L2gradient]])
- apertureSize:Sobel 算子的孔径大小
- L2gradient:计算图像梯度幅度的标识
- 默认为FALSE:使用 L1 范数(直接将两个方向导数的绝对值相加)
- 若为TURE:使用更精确的 L2 范数进行计算(即两个方向的导数的平方和再开方),但会减慢运算速度
(2)cv2.fineContours():边缘寻找函数
tis:关于边缘的详细解释可以参考findContours函数参数详解
①函数原型
contours, hierarchy=cv2.findContours(image, mode,method)
②函数参数
- mode
-
cv2.RETR_EXTERNAL:表示只检测外轮廓
-
cv2.RETR_LIST:检测的轮廓不建立等级关系
-
cv2.RETR_CCOMP:只建立两个等级的轮廓
上面的一层为外边界,里面的一层为内孔的边界信息
内孔内的连通物体边界算作顶层
-
cv2.RETR_TREE:建立一个等级树结构的轮廓(从外到内一层为一个等级)
-
- method
-
cv2.CHAIN_APPROX_NONE:
保存物体边界上所有连续的轮廓点到contours向量内
-
cv2.CHAIN_APPROX_SIMPLE:仅保存轮廓的拐点信息
把所有轮廓拐点处的点保存入contours向量内
拐点与拐点之间直线段上的信息点不予保留
-
cv2.CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS:
使用teh-Chinl chain近似算法(此算法可以参考多边形区域掩码与其轮廓线间的转换2.1学习)
-
- 所以要画出所有的轮廓就需要传入
cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE
(3)cv2.drawContours():边界绘制
- 函数原型
cv2.drawContours(image, contours, contourIdx, color, thickness=None, lineType=None, hierarchy=None, maxLevel=None, offset=None) - 和很多绘图函数一样,它也有一大堆参数在定义颜色、粗细、绘制种类等等杂七杂八不太重要的东西
- 比较重要的几个参数:
- image:在哪张张图上画(目标)
- contours:边界的信息
- contourIdx:指明画第几个轮廓,若为负值,则画全部轮廓
- 另外:thickness表示线条粗细,为负值则直接填充
2、最终效果
至此,图像的预处理也就完成辣!下面就是特征提取和模型构建了(≖ᴗ≖)✧
特别声明:以上的图片部分来自于网络,感谢CSDN、知乎等平台上各位博主的分享,本文用作交流学习予以引用,在此一并表示感谢!