计算机中,图像是由像素点构成的,而像素点又可以理解成一个数值。数值的范围为0~255,表示该像素点的亮度,0表示黑,255表示白。
一个彩色图是由三色通道的R、G、B组成的,即假设图像的长和高是500*500,则这个彩色图像的最终维度是500 * 500 * 3,而灰度图只由一个通道表示亮度即可。例如:
注意:opencv读取的图像是BGR的方式。
opencv图像的基础操作
-
图像读取:imread(“图像路径”,图像的打开方式),其中图像的打开方式有三种,分别为:
cv2.IMREAD_COLOR:加载一个彩色图片。图片的透明度会被忽略,默认值
cv2.IMREAD_GRAYSCALE:用灰度模式加载图片
cv2.IMREAD_UNCHANGED:包含alpha 通道的方式加载图片 -
图像显示:imshow(“显示的窗口名称”,显示的图像名)
在显示的时候,通常还需要加上cv2.waitKey(0) 括号里面的值为等待时间,表示等待多少毫秒后窗口关闭,这里的0是按任何键窗口关闭。
cv2.destroyAllWindows() 销毁窗口。
3.图像保存:imwrite(“保存图像的名称”,要保存的图像)
4. 切片操作
例如:我们取img的上方200 * 200的部分
img_roi = img[0:200, 0:200]
5.颜色通道操作
注意:opencv读取图像是bgr的方式进行读取的。
# 1. 切分颜色通道
b, g, r = cv2.split(img)
# 2. 组合颜色通道
img = cv2.merge((b, g, r))
# 3. 只保留某一个颜色通道
# 由于opencv读取图像是以bgr的形式读取的,所以对应的索引为0, 1, 2,下面展示只保留r通道的操作:
img[:,:,0] = 0 # 将b通道赋值为0
img[:,:,1] = 0 # 将g通道赋值为0
opencv视频的基础操作
视频也是由图像组成,是由很多帧静止的图像组成,随着时间轴图像连续切换。
帧数慢,图像不连贯 ,肉眼可见的图像变换,就会觉得卡
帧数高,图像连贯,肉眼看不出图像的切换
视频的读取是一个连贯的过程,具体的步骤如下:
# 1. 视频读取
video = cv2.VideoCapture("读取的视频路径")
# 2. 检查是否打开
if video.isOpened():
# 如果可以打开,逐帧打开 open是bool值,如果可以读取则返回True frame是当前这一帧图像的值
open, frame = video.read()
else:
open = false
# 3. 利用死循环,将视频拆解为逐帧的图像进行显示
while open: # 可以成功读取的话,利用死循环
ret, frame = video.read() # 读取每一帧
if frame is None:
break
if ret == True: # 当前读取到的这帧图像存在
cv2.imshow('frame', frame) # 展示结果
if cv2.waitKey(0) & 0xFF == 27: # 0xFF为指定退出键
break
# 4.销毁窗口
video.release()
cv2.destroyAllWindows()
对于视频数据的读取,有很多种,例如:
(1)读取IP摄像头
# 摄像头的IP地址,http://用户名:密码@IP地址:端口/
cap = cv2.VideoCapture('http://admin:admin@192.168.1.6:8081/video')
(2)读取rtsp推流
# rtsp推流的方式链接
cap = cv.VideoCapture("rtsp://admin:admin@192.168.1.6:8554/live")
等等…
江苏学蠡信息科技有限公司的LPKT030试验箱的视频采集共有三种方式:
(1)USB摄像头
import cv2 as cv
import time
cap = cv.VideoCapture("/dev/video10")
cv.namedWindow("usb")
cv.moveWindow("usb", 40, 80)
if not cap.isOpened():
print("Cannot capture from camera. Exiting.")
os._exit(1)
last_time = time.time()
while(True):
ret, frame = cap.read()
cv.imshow('usb', frame)
if cv.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv.destroyAllWindows()
(2)mipi 摄像头
# coding:utf-8
import numpy as np
import cv2 as cv
import time
import os
# selfpath
previewDevs=[]
# mainpath
pictureDevs=[]
# camera type
cameraTypes=[]
# isp1
if os.path.exists("/sys/devices/platform/ff910000.rkisp1/video4linux/v4l-subdev2"):
previewDevs.append("/dev/video1")
pictureDevs.append("/dev/video0")
cameraTypes.append("mipi")
# isp2
if os.path.exists("/sys/devices/platform/ff920000.rkisp1/video4linux/v4l-subdev1") or os.path.exists("/sys/devices/platform/ff920000.rkisp1/video4linux/v4l-subdev5"):
previewDevs.append("/dev/video6")
pictureDevs.append("/dev/video5")
cameraTypes.append("mipi")
# usb camera
filename="/sys/class/video4linux/video10/name"
if os.path.isfile(filename):
file = open(filename,mode='r')
filetxt = file.read().lower()
file.close()
if "camera" in filetxt or "uvc" in filetxt or "webcam" in filetxt:
previewDevs.append("/dev/video10")
pictureDevs.append("/dev/video10")
cameraTypes.append("usb")
cam_width=800
cam_height=448
def get_camerasrc(index):
if cameraTypes[index] == "mipi":
return 'rkisp device='+previewDevs[index]+' io-mode=4 ! video/x-raw,format=NV12,width='+str(cam_width)+',height='+str(cam_height)+',framerate=30/1 ! videoconvert ! appsink'
if cameraTypes[index] == "usb":
return 'v4l2src device='+previewDevs[index]+' io-mode=4 ! videoconvert ! video/x-raw,format=NV12,width='+str(cam_width)+',height='+str(cam_height)+',framerate=30/1 ! videoconvert ! appsink'
if len(previewDevs) == 0:
print("Please connect a camera.")
os._exit(1)
cap = cv.VideoCapture(get_camerasrc(0), cv.CAP_GSTREAMER)
cv.namedWindow("left")
cv.moveWindow("left", 40, 80)
if not cap.isOpened():
print("Cannot capture from camera. Exiting.")
os._exit(1)
last_time = time.time()
while(True):
ret, frame = cap.read()
cv.imshow('left', frame)
if cv.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv.destroyAllWindows()
(3)rtsp推流
图像的展示技巧
-
利用matplotlib展示
例如:当前有6张图片,将其放在一起展示,对比效果明显。
import matplotlib.pyplot as plt
# subplot(231) 表示2*3的布局中的第一张图片
plt.subplot(231), plt.imshow(img, "gray"), plt.title("org")
plt.subplot(232), plt.imshow(replicate, "gray"), plt.title("BORDER_REPLICATE")
plt.subplot(233), plt.imshow(reflect, "gray"), plt.title("BORDER_REFLECT")
plt.subplot(234), plt.imshow(reflect_101, "gray"), plt.title("BORDER_REFLECT_101")
plt.subplot(235), plt.imshow(wrap, "gray"), plt.title("BORDER_WRAP")
plt.subplot(236), plt.imshow(constant, "gray"), plt.title("BORDER_CONSTANT")
plt.show()
plt.imshow() 可以直接显示 OpenCV 灰度图像,不需要格式转换,但需要使用 cmap=‘gray’ 进行参数设置。因为OpenCV 使用 BGR 格式,颜色分量按照蓝/绿/红的次序排列,而 matplotlib 使用 RGB 格式,颜色分量按照红/绿/蓝的次序排序。
如果想直接用matplotlib.pyplot 显示 Opencv 读取的图像,需要先调换BGR转换成RGB,在显示
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)
plt.show()
-
利用numpy展示
由于图片是像素矩阵组成的,故也可以通过numpy来操作矩阵,进而达成操纵图片的效果。
但是该种显示有一个缺点,超过三张图片的显示并不好,而且要求维度必须相同。
import numpy as np
import cv2
img1 = cv2.imread("1.jpg")
img2 = cv2.imread("2.jpg")
img3 = cv2.imread("3.jpg")
res1 = np.hstack((img1, img2, img3)) # 横向显示
res2 = np.vstack((img1, img2, img3)) # 纵向显示
cv2.imshow("res1", res1)
cv2.imshow("res2", res2)
cv2.waitKey(0)
cv2.destroyAllWindows()
边界填充
用途为:将图像扩大一圈。
需要先指定上下左右扩大的大小,如下:
top_size, bottom_size,left_size,right_size = (50, 50, 50, 50)
填充函数:copyMakeBorder(要填充的图像,上填充大小,下,左,右~, borderType = “填充的策略”) ,其中 borderType是可以省略,有以下几个策略
- BORDER_REPLICATE: 复制法,也就是复制最边缘像素 例如:123456789 =》1112345678999
- BORDER_REFLECT: 反射法,对感兴趣的区域的图像中的像素在两边进行复制 例如:123456789 =》2112345678998
- BORDER_REFLECT_101: 对称法,也就是以最边缘像素为轴,对称 例如:123456799 =》 2312345678987
- BORDER_WRAP: 外包装法 例如:123456789 => 89113454678912
- BORDER_CONSTANT: 常量法,用常数值进行填充。例如:value=5 123456789 => 5512345678955
replicate = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_REPLICATE)
reflect = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_REFLECT)
reflect_101 = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_REFLECT_101)
wrap = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_WRAP)
constant = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_CONSTANT, value = 0)
利用matplotlib进行对比展示:
色彩空间
常见的色彩空间有RGB、HSV、HIS、YCrCb、YUV,其中最常用的是RGB、HSV、YUV局。
RGB是彩色图像在机器中的颜色显示
HSV是针对用户观感的一种颜色模型,侧重于色彩表示,什么颜色、深浅如何、明暗如何,HSV 表达彩色图像的方式由三个部分组成:
- H 色调 取值范围为:0-180
- S 饱和度 取值范围为:0-255
- V 明度 取值范围为:0-255
YUV 就是YCrCb。
- Y 表示明亮度,也就是灰阶值;
- U 和 V 表示的则是色度,作用是描述影像色彩及饱和度,用于指定像素的颜色。
OpenCV 色彩空间转换的函数为: cv.cvtColor()
img = cv2.imread('img/brid.jpg') # 读入原图像
gray = cv2.cvtColor(img, cv.COLOR_BGR2GRAY) #RGB 转换为GRAY 这里的GRAY 是单通道的
hsv = cv2.cvtColor(img, cv.COLOR_BGR2HSV) #RGB 转换为HSV
yuv = cv2.cvtColor(img, cv.COLOR_RGB2YUV) #RGB 转换为YUV
Ycrcb = cv2.cvtColor(img, cv.COLOR_RGB2YCrCb) #RGB 转换为YCrCb
利用numpy进行展示后三种颜色模型:
像素计算
- 算数运算
add = cv2.add(m1, m2) # 像素加法
sub = cv2.subtract(m1, m2) # 像素减法
div = cv2.divide(m1, m2) # 像素除法
mul = cv2.multiply(m1, m2) # 像素乘法
利用matplotlib进行展示:
- 逻辑运算
dst = cv.bitwise_and(m1, m2) # 与运算每个像素点每个通道的值按位与
dst = cv.bitwise_or(m1, m2) # 或运算每个像素点每个通道的值按位或
dst = cv.bitwise_not(m1) # 非运算每个像素点每个通道的值按位取反
利用numpy进行展示:
-
尺寸变换
rse = cv2.resize(img, (400, 400)) # 将img的尺寸变换为400 * 400 rse = cv2.resize(img, (0, 0), fx = 1, fy = 3) # 将img的尺寸变换为高是宽的3倍
-
图像融合
rse = cv2.addWeighted(img1, 0.4, img2, 0.6, 0) # 将img1 与 img2进行融合 融合公式为:img1*0.4 + img2*0.6 + 0
RoI区域
ROI区域又称为感兴趣区域。在机器视觉、图像处理中,从被处理的图像以方框、圆、椭圆、不规则多边形等方式勾勒出需要处理的区域,称为感兴趣区域,ROI。
img_roi = img[0:200, 0:200]
泛洪填充
泛洪填充算法也叫漫水填充算法。
泛洪填充分为CV_FLOODFILL_FIXED_RANGE与CV_FLOODFILL_MASK_ONLY两种不同的填充策略,两种策略对应的填充思想,对应的函数的填充参数也不同。
- CV_FLOODFILL_FIXED_RANGE策略的泛洪填充
cv2.floodFill(image, # 需要进行泛洪填充的原始图像,
msak = np.zeros([h+2, W+2],np.uint8),
'''
0ask必须行和列都加2,且必须为uint8单通道阵列 因为描时,mask 多出来的2可以保证扫描的边界上的像素都会被处理
mask的值只能为0或1 当mask对应位置为0的区域会被填充;当mask对应位置为1的区域会被锁定,也就是不能被填充。
可以通过修改mask区域的值来设置处自己想要的ROI区域 例如:mask[151:501, 161:551] = 1 则这一区域不被填充
上面我将mask设置为全部都是0,所以作用范围是比整张图片还大
(注意,作用范围是整张图片不代表会填充整张图,同时还要满足像素和填充颜色的像素差在[loDiff,upDiff]范围内)
'''
seedPoint = (220, 220), # 填充的起始点像素, 从(220, 220)开始填充
newVal = (0, 255, 255), # 要填充的颜色, 黄色
loDiff = (100, 100, 100), # 规定像素值的下限,
upDiff = (30, 20 ,20), # 规定像素值的上限,
flags = CV_FLOODFILL_FIXED_RANGE #填充的方法。
)
以seedPoint为起点,将mask区域待处理的像素点与起点作比较,二者的差值在范围loDiff~upDiff之内,则填充此像素,填充的像素值为newVal,改变图像。
- CV_FLOODFILL_MASK_ONLY策略的泛洪填充
在这个填充策略下,函数不会去改变原始图像,而是去填充掩码图像mask,mask的指定的位置为0时才填充,不为0不填充,可以看作是强制填充,即不需要loDiff和upDiff,范围内全部强制填充,这里值得注意是floodFill不会填充他认为不需要填充的地方
cv.floodFill(image = Img,
mask = np.zeros([h + 2, w + 2],np.uint8),
seedPoint = (200, 200), # 从左上角开始填充
newVal = (255, 0, 0), # 填充的值 为蓝色
flags = cv.CV_FLOODFILL_MASK_ONLY # 填充的策略
)
形态学操作
腐蚀操作
一般情况下,通常对二值图像进行腐蚀操作,作用是去掉图像轮廓边的毛刺,
腐蚀操作的函数为:erode(img:处理的图像。kernel:卷积核, 相当于一个小区域,在图像上移动,进行腐蚀。iternations:迭代次数腐蚀的次数)
kernel = np.ones((3, 3), np.uint8) # 指定核的大小为 3*3的值为1的矩阵
ession = cv2.erode(img, kernel, iterations = 1)
res = np.hstack((img, ession))
特征丢失了,毛刺没了,并且线条也有粗变细了
设置迭代次数分别为1、2、3观察区别:
发现线越来越细!!!
膨胀操作
膨胀操作是腐蚀操作的逆运算
腐蚀操作的函数为:dilate(img:处理的图像。 kernel:卷积核, 相当于一个小区域,在图像上移动,进行腐蚀。 iternations:迭代次数腐蚀的次数)
kernel = np.ones((3, 3), np.uint8) # 指定核的大小为 3*3的值为1的矩阵
ession = cv2.dilate(img, kernel, iterations = 1)
res = np.hstack((img, ession))
特征明显了,毛刺和线条变粗了
设置迭代次数为1、2、3观察:
开运算与闭运算
开运算:先进行腐蚀操作将毛刺去掉同时线会变细,再进行膨胀操作 由于已经没有毛刺了,此时线会变粗
闭运算:先进行膨胀操作会将毛刺和线同时变粗,再进行腐蚀操作此时毛刺和线都会变细
opencv执行开、闭运算的函数为:morphologyEx(img:待处理的图像。 cv2.MORPH_OPEN :开运算 (cv2.MORPH_OPEN 闭运算)。 kernel :卷积核)
利用numpy将原图、开运算结果和闭运算结果进行展示:
梯度运算
常用于得到图像的轮廓信息和边界信息,梯度运算的思想是膨胀操作 - 腐蚀操作
opencv执行梯度运算的函数为:morphologyEx(img:待处理的图像。 cv2.MORPH_GRADIENT:梯度运算。 kernel:卷积核)
利用numpy将原图和梯度运算的结果进行展示:
礼帽和黑帽
礼帽 = 原始图像 - 开运算
黑帽 = 闭运算 - 原始图像
opencv执行礼帽和黑帽的函数为:morphologyEx(img:待处理的图像。 cv2.MORPH_TOPHAT:礼帽运算 ( cv2.MORPH_BLACKHAT:黑帽运算)。 kernel: 卷积核)
利用numpy将原图、顶帽和黑帽的运算结果进行展示:
图像滤波处理
在尽量保留图像细节特征的条件下对目标图像的噪声进行抑制,是图像预处理中不可缺少的操作,其处理效果的好坏将直接影响到后续图像处理和分析的有效性和可靠性。
均值滤波
定义一个卷积核在图像上进行移动,求的卷积核内的所有像素点的平均值对卷积核中心的点值进行替换,故要求卷积核的大小最好为奇数,例如:1 * 1、3 * 3 、5 * 5
opencv执均值滤波的函数为:blur(img:待处理的图像。(n, n) : 卷积核)
利用numpy将原图和均值滤波后的图进行展示:
img_blur = cv2.blur(img, (3, 3))
res = np.hstack((img, img_blur))
方框滤波
与均值滤波的原理大多相同,并且可以进行归一化
opencv执行均值滤波的函数为:boxFilter(img:待处理的图像。-1:当指定为-1时,需要进行自动计算,得到的结果的颜色通道要和原图像的颜色通道一致。(n, n) : 卷积核。 normalize = True:是否进行归一化 ,当指定为True的时方框滤波与均值滤波的作用相同 当指定为False 将所有像素相加可能会产生越界的现象)
利用numpy将原图、方框滤波归一化设置为True和False的图进行展示:
img_blur_T = cv2.boxFilter(img, -1, (3, 3), normalize=True)
img_blur_F = cv2.boxFilter(img, -1, (3, 3), normalize=False)
res = np.hstack((img, img_blur_T,img_blur_F))
高斯滤波
高斯滤波是按照加权平均的,距离越近的点权重越大,距离越远的点权重越小。通俗的讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。
opencv执行高斯滤波的函数为:GaussianBlur(img:参数表示待处理的输入图像, (n,n):参数表示高斯滤波器模板大小, 0:x的标准差, 0:y的标准差)
img_GaussianBlur = cv2.GaussianBlur(img, (3, 3), 0, 0)
res = np.hstack((img, img_GaussianBlur))
利用numpy将原图、高斯滤波图进行展示:
中值滤波
中值滤波去噪的效果是最好的,中值滤波会将卷积核内的像素点进行排序,取位于中间点的像素值
opencv执行高斯滤波的函数为:medianBlur(img:参数表示待处理的输入图像, kernel_size:卷积核大小)
img_medianBlur = cv2.medianBlur(img, 3)
res = np.hstack((img, img_medianBlur))
利用numpy将原图、中值滤波的图进行展示:
双边滤波
它是结合图像的空间邻近度和像素值相似度的一种折中处理,同时考虑空域信息和灰度相似性,达到保边去噪的目的。
opencv执行高斯滤波的函数为:bilateralFilter(img:参数表示待处理的输入图像, kernel_size:卷积核大小,sigmaColor:色彩空间的标准方差,sigmaSpace:坐标空间的标准方差)
img_bilateralFilter=cv2.bilateralFilter(img, 50, 100, 100)
res = np.hstack((img, img_bilateralFilter))
利用numpy将原图、双边滤波的图进行展示:
图像直方图
其实,简单来说,直方图就是对数据进行统计的一种方法,图像直方图是用以表示数字图像中亮度分布的直方图,标绘了图像中每个亮度值的像素数。这种直方图中,横坐标的左侧为纯黑、较暗的区域,而右侧为较亮、纯白的区域。因此一张较暗图片的直方图中的数据多集中于左侧和中间部分,而整体明亮、只有少量阴影的图像则相反。CV 领域常借助图像直方图来实现图像的二值化。
直方图的意义如下:
● 直方图是图像中像素强度分布的图形表达方式。
● 它统计了每一个强度值所具有的像素个数。
img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB) #将BGR 格式转换为RGB 格式
# 绘制图像直方图
plt.subplot(2,1,1) # 2 * 1布局的 上面
hist1=plt.hist(img.ravel(), 256, [0, 256]) #numpy 的ravel 函数功能是将多维数组降为一维数组
# 画三通道图像的直方图
plt.subplot(2,1,2)
color = ('b', 'g', 'r') #这里画笔颜色的值可以为大写或小写或只写首字母或大小写混合
for i , color in enumerate(color):
hist = cv2.calcHist([img], [i], None, [256], [0, 256]) #计算直方图
plt.plot(hist, color)
plt.xlim([0, 256])
plt.show()
模板匹配
模板匹配:是一种最原始、最基本的模式识别方法,研究某一特定对象物的图案位于图像的什么地方,进而识别对象物,这就是一个匹配问题。它是图像处理中最基本、最常用的匹配方法。模板匹配具有自身的局限性,主要表现在它只能进行平行移动,若原图像中的匹配目标发生旋转或大小变化,该算法无效。简单来说,模板匹配就是在整个图像区域发现与给定子图像匹配的小块区域。
工作原理:在待检测图像上,从左到右,从上向下计算模板图像与重叠子图像的匹配度,匹配程度越大,两者相同的可能性越大。
OpenCV 提供了6 种模板匹配算法(推荐使用归一化的,结果更可靠些):
-
归一化平方差匹配法TM_SQDIFF_NORMED 计算归一化平方不同,计算出来值越接近0,越相关
-
归一化相关匹配法TM_CCORR_NORMED 计算归一化相关性,计算出来的值越接近1,越相关
-
归一化相关系数匹配法TM_CCOEFF_NORMED用T 表示模板图像,I 表示待匹配图像,切模板图像的宽为w 高为h,用R 表示匹配结果,匹配过程
-
平方差匹配法TM_SQDIFF 计算平方不同,计算出来的值越小,越相关
-
相关匹配法TM_CCORR 计算相关性,计算出来的值越大,越相关
-
相关系数匹配法TM_CCOEFF 计算相关系数,计算出来的值越大,越相关
opencv执行模板匹配的函数为:matchTemplate,函数原型为:res = matchTemplate(搜索的图像, 匹配的模板图像, 匹配的方法)
常集合 min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) 来获取匹配的结果
min_val 是最小值 max_val是最大值 min_loc最小值点的坐标位置 max_loc 最大值点的坐标位置
例如:TM_SQDIFF计算出来的值越小越相关,所以我们着重关注 最小值点,根据最小值点的坐标min_loc和模板的h,w,在原图可以画出匹配的区域
def template_demo():
tpl =cv2.imread('img/cateye.jpg')
target = cv2.imread('img/cat.jpg')
cv2.namedWindow('template image', cv2.WINDOW_NORMAL)
cv2.imshow("template image", tpl)
cv2.namedWindow('target image', cv2.WINDOW_NORMAL)
cv2.imshow("target image", target)
methods = [cv2.TM_SQDIFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_CCOEFF_NORMED,cv2.TM_SQDIFF, cv2.TM_CCORR, cv2.TM_CCOEFF]
th, tw = tpl.shape[:2]
for md in methods:
print(md)
result = cv2.matchTemplate(target, tpl, md)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
if md in [cv2.TM_SQDIFF_NORMED, cv2.TM_SQDIFF]: # 这两种方法计算出来的值越小越相关
tl = min_loc
else:
tl = max_loc
br = (tl[0]+tw, tl[1]+th)
cv2.rectangle(target, tl, br, (0, 0, 255), 2)
cv2.namedWindow("match-" + np.str(md), cv2.WINDOW_NORMAL)
cv2.imshow("match-" + np.str(md), target)
图像二值化
图像二值化就是根据阈值将数字图像分为两部分:大于阈值的像素集合和小于阈值的像素集合。二值化操作分为全局二值化和局部二值化:
全局二值化
它是针对整幅数字图像来说的,设置一个阈值,让整幅数字图像的所有像素值与该阈值进行比较;
实现全局二值化的函数为 ret, thresh = threshold(src:灰度图,threshold:阈值(通常设置为127),maxValue:当像素值高于阈值时应该被赋予的新的像素值(通常设置为255),Methods:二值化的方法)
二值化方法具体包括如下五种:
cv2.THRESH_BINARY:大于阈值的像素点的灰度值设定为maxValue(如8 位灰度值最大为255),灰度值小于阈值的像素点的灰度值设定为0。
cv2.THRESH_BINARY_INV:大于阈值的像素点的灰度值设定为0,而小于该阈值的设定为maxValue。
cv2.THRESH_TRUNC:像素点大于阈值的像素点就设定为该阈值,否则不变。
cv2.THRESH_TOZERO:像素点大于该阈值的部分不变,否则为0。
cv2.THRESH_TOZERO_INV:像素点小于该阈值的部分不变,否则为0。
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh1 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
ret, thresh2 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY_INV)
ret, thresh3 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TRUNC)
ret, thresh4 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TOZERO)
ret, thresh5 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TOZERO_INV)
titles = ["original", "BINARY", "BINARY_INV", "TRUNC", "TOZERO", "TOZERO_INV"]
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(6):
plt.subplot(2, 3, i + 1), plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.show()
局部二值化
它是针对一幅数字图像的某一部分(区域)来说的,设置一个阈值,让该区域的所有像素值与该阈值进行比较,因此,当一幅数字图像上的不同部分具有不同亮度时,在对其进行二值化操作时,需要采用自适应阈值,让不同的区域取不同的阈值;
实现局部二值化的函数为adaptiveThreshold(src:灰度图。
maxValue:像素值高于(有时是小于)阈值时应该被赋予的新的像素值。
adaptive_method:自适应阈值计算方法:CV_ADAPTIVE_THRESH_MEAN_C 均值滤波 和CV_ADAPTIVE_THRESH_GAUSSIAN_C 高斯滤波。 threshold_type:二值化的方法CV_THRESH_BINARY 大于阈值的像素点的灰度值设定为maxValue(如8 位灰度值最大为255),灰度值小于阈值的像素点的灰度值设定为0和CV_THRESH_BINARY_INV 大于阈值的像素点的灰度值设定为0,而小于该阈值的设定为maxValue。。
block_size:用来计算阈值的像素邻域大小。
param1:与自适应阈值计算方法有关的参数,当自适应阈值计算方法为CV_ADAPTIVE_THRESH_MEAN_C,先求出块中的均值,再减掉param1;当自适应阈值计算方法为CV_ADAPTIVE_THRESH_GAUSSIAN_C ,先求出块中的加权和(gaussian), 再、减掉param1。 )
img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
part_thresh_img = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,11,5)
globle_thresh,globle_thresh_img = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
res = np.hstack((img, part_thresh_img, globle_thresh_img))
cv2.imshow("res", res)
中间为局部二值化方法,在本图片的二值化中效果比全局二值化方法好一些。
图像金字塔
图像金字塔是图像中多尺度表达的一种,最主要用于图像的分割,是一种以多分辨率来解释图像的有效但概念简单的结构。简单来说,图像金字塔就是用来进行图像缩放的,它常用于图像特征提取,通过图像金字塔,每一层的特征可能是不一样的,将每一层的特征提取出来进行结合,从而继续进行我们的图像操作。
两种类型的金字塔:
高斯金字塔:
下采样(缩小),它的方向是由塔的底部向塔的顶部进行采样。原理:首先将原图像作为最底层图像G0(高斯金字塔的第0 层),利用高斯核(5*5)对其进行卷积,然后对卷积后的图像进行下采样(去除偶数行和列)得到上一层图像G1,将此图像作为输入,重复卷积和下采样操作得到更上一层图像,反复迭代多次,形成一个金字塔形的图像数据结构,即高斯金字塔。
上采样(放大),它的方向是由塔的顶部向塔的底部进行采样。原理:将图像在每个方向扩大为原来的两倍,新增的行和列以0填充,利用高斯核再对其进行卷积,获得近似值。
高斯金字塔的实现函数为:pyrUp(img)、pyrDown(img) 使用一次函数,则会上、下采样一次
三次下采样:
三次上采样:
总之,上、下采样都存在一个严重的问题,那就是图像变模糊了,因为缩放的过程中发生了信息丢失的问题。要解决这个问题,就得用拉普拉斯金字塔。
拉普拉斯金字塔:
用于重建图像,也就是预测残差,对图像进行最大程度的还原。比如一幅小图像重建为一幅大图,原理:拉普拉斯金字塔中的第 i 层,等于**“高斯金字塔中的第 i 层”与“高斯金字塔中的第 i+1 层的向上采样结果”**之差。
L
i
=
G
i
−
p
y
r
U
p
(
G
i
+
1
)
Li = Gi - pyrUp(Gi + 1)
Li=Gi−pyrUp(Gi+1)
down = cv2.pyrDown(img) # 高斯金字塔中的第i+1层
down_up = cv2.pyrUp(down) # 对高斯金字塔中的第i+1层进行向上采样
l_1 = img - down_up # 高斯金字塔中的第i层与高斯金字塔中的第i+1层的向上采样结果之差
cv2.imshow("l1", l_1)
图像梯度
图像梯度可以把图像看成二维离散函数,图像梯度其实就是这个二维离散函数的求导。
OpenCV 提供了三种不同的梯度滤波器或者说高通滤波器: Sobel,Scharr 和Laplacian。其中Sobel,Scharr 是求一阶导数。Scharr 是对Sobel(使用小的卷积核求解求解梯度角度时)的优化,而Laplacian 是求二阶导数。
Sobel算子
opencv实现sobel算子的函数为dst = cv2.Sobel(img:待处理的图像, ddepth:图像的深度(一般源图像都为CV_8U,为了避免溢出,指定为-1即可代表输出图像与输入图像相同的深度,一般ddepth参数选择CV_32F或CV_64F是在运算过程中会产生负数,保证负数也能参与运算,后面利用convertScaleAbs绝对值进行转换), dx:x 方向上的差分阶数,1 或0 , dy:y 方向上的差分阶数,1 或0 , ksize:Sobel算子的大小,必须为1、3、5、7(一般3*3的多))
Sobel 算子用来计算图像灰度函数的近似梯度。Sobel 算子根据像素点上下、左右邻点灰度加权差,在边缘处达到极值这一现象检测边缘。对噪声具有平滑作用,提供较为精确的边缘方向信息,边缘定位精度不够高。当对精度要求不是很高时,是一种较为常用的边缘检测方法。
Sobel 具有平滑和微分的功效。即:Sobel 算子先将图像横向或纵向平滑,然后再纵向或横向差分,得到的结果是平滑后的差分结果。
def sobel_demo(image):
grad_x = cv.Sobel(image, cv.CV_32F, 1, 0)
grad_y = cv.Sobel(image, cv.CV_32F, 0, 1)
gradx = cv.convertScaleAbs(grad_x)
grady = cv.convertScaleAbs(grad_y)
cv.imshow("gradient_x", gradx)
cv.imshow("gradient_y", grady)
gradxy = cv.addWeighted(gradx, 0.5, grady, 0.5, 0) # 利用局部二值化,将x和y梯度的进行融合
cv.imshow("gradient", gradxy)
src = cv.imread('img/bird.jpg')
cv.namedWindow('input_image', cv.WINDOW_NORMAL)
cv.imshow('input_image', src)
sobel_demo(src)
cv.waitKey(0)
cv.destroyAllWindows()
左上角为原图,右上叫为对x和y的融合梯度,西面是分别对x和y梯度。
直接grad = cv.Sobel(image, cv.CV_32F, 1, 1) 效果并没有二者融合的好!!!
Scharr 算子
与Sobel算子的整体计算方式相同,只是核中的数值比Sobel算子的值更大,导致的显示是对差异更加的敏感。
opencv实现Scharr 算子的函数为dst = cv2.Scharr(img, ddepth, dx, dy)
Laplace算子
求二阶导,导致对差异更敏感,但是同时也会对噪音点敏感,故效果不会特别好,需要与其他方法融合使用。
opencv实现Laplace算子的函数为dst = cv2.Laplacian(img, ddepth)
Canny 边缘检测
Canny 的目标是找到一个最优的边缘检测算法,最优边缘检测的含义是:
好的检测- 算法能够尽可能多地标识出图像中的实际边缘。
好的定位- 标识出的边缘要尽可能与实际图像中的实际边缘尽可能接近。
最小响应- 图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。
算法步骤:
-
使用高斯滤波,以平滑图像,滤除噪声 GaussianBlur
-
灰度转换 cvtColor
-
计算像素点梯度值 Sobel/Scharr
-
使用非最大值抑制,消除边缘检测带来的杂散影响
-
使用高低阈值输出二值图像,来确定真实的和潜在的边缘
梯度值>maxVal,则处理为边界
minVal<梯度值<maxVal,连有边界则保留,否则舍弃
梯度值<minVal则舍弃
-
通过抑制孤立的弱边缘最终完成边缘检测
opencv实现Canny 边缘检测的函数为dst = cv2.Canny(img, minVal,maxVal)
blurred = cv2.GaussianBlur(image, (3, 3), 0)
gray = cv2.cvtColor(blurred, cv.COLOR_RGB2GRAY)
edge_output = cv2.Canny(gray, 50, 150)
cv2.imshow("Canny Edge", edge_output)
再利用bitwise_and对原图像取色,最终可以得到带颜色的轮廓
dst = cv.bitwise_and(image, image, mask= edge_output)
形状检测
直线检测
霍夫变换是图像处理中的一种特征提取技术,它通过一种投票算法检测具有特定形状的物体。该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合作为霍夫变换结果。经典霍夫变换用来检测图像中的直线,后来霍夫变换扩展到任意形状物体的识别,多为圆和椭圆.霍夫线变换是一种用来寻找直线的方法.使用用霍夫线变换之前, 首先要对图像进行边缘检测的处理,也即霍夫线变换的直接输入只能是边缘二值图像.
opencv提供了两种直线函数,一种为标准霍夫线变换函数,另一种为统计概率霍夫线变换函数
opencv 的HoughLines 函数是标准霍夫线变换函数,该函数的功能是通过一组参数对的集合来表示检测到的直线,其函数原型为:
HoughLines(image:表示边缘检测的输出图像,该图像为单通道8 位二进制图像,
rho:表示参数极径以像素值为单位的分辨率,这里一般使用1 像素,
theta:表示参数极角以弧度为单位的分辨率,这里使用1 度,
threshold: 参数表示检测一条直线所需最少的曲线交点。)
opencv 的HoughLinesP 函数是统计概率霍夫线变换函数,该函数能输出检测到的直线的端点,其函数原型为:
HoughLinesP(image:表示边缘检测的输出图像,该图像为单通道8 位二进制图像,
rho:表示参数极径以像素值为单位的分辨率,这里一般使用1 像素,
theta:表示参数极角以弧度为单位的分辨率,这里使用1 度,
threshold:参数表示检测一条直线所需最少的曲线交点。)
圆检测
OpenCV 实现的是一个比标准霍夫圆变换更为灵活的检测方法——霍夫梯度法,该方法运算量相对于标准霍夫圆变换大大减少。其检测原理是依据圆心一定是在圆上的每个点的模向量上,这些圆上点模向量的交点就是圆心,霍夫梯度法的第一步就是找到这些圆心,这样三维的累加平面就又转化为二维累加平面。第二步是根据所有候选中心的边缘非0 像素对其的支持程度来确定半径。注:模向量即是圆上点的切线的垂直线。
霍夫圆变换的基本思路是认为图像上每一个非零像素点都有可能是一个潜在的圆上的一点,跟霍夫线变换一样,也是通过投票,生成累积坐标平面,设置一个累积权重来定位圆。
opencv提供的函数为:HoughCircles
轮廓检测
轮廓发现是基于图像边缘提取的基础寻找对象轮廓的方法,所以边缘提取的阈值选定会影响最终轮廓发现结果。
Opencv 轮廓检测的函数原型为:
findContours(image:参数表示8 位单通道图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图,
mode:参数表示轮廓检索模式,模式有:
CV_RETR_EXTERNAL:只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略。
CV_RETR_LIST:检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓。
CV_RETR_CCOMP:检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层。
CV_RETR_TREE:检测所有轮廓,所有轮廓建立一个等级树结构,外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。 常用
method:表示轮廓的近似方法,方法有:
CV_CHAIN_APPROX_NONE 存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max(abs (x1 - x2), abs(y2 - y1) == 1。
CV_CHAIN_APPROX_SIMPLE 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4 个点来保存轮廓信息。
CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS 使用teh-Chinl chain 近似算法。
函数返回值为:
contours 是一个列表,其中包含了所有的轮廓。每个轮廓都是一个点的列表,表示轮廓的形状。
hierarchy 这个变量描述了轮廓之间的关系。具体来说,它是一个嵌套的列表,其中每个元素都是一个包含四个值的列表:[next, previous, first_child, parent]
。这四个值描述了轮廓在层次结构中的位置,不是很常用。
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 二值化
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) # 轮廓检测
轮廓绘制
Opencv 绘制轮廓的函数原型为:
drawContours(image: 参数表示目标图像,
contours: 参数表示所有输入轮廓,一般由findContours去获取,
contourIdx:参数表示绘制轮廓list 中的哪条轮廓, -1 是绘制所有的轮廓
color:表示轮廓的颜色 (0,0,255) 红色
thickness :表示绘制的轮廓线条粗细,如果是负数,则绘制轮廓内部。
lineType: 表示线型。)
注意使用轮廓绘制函数drawContours会在原图上进行修改,如果不想在原图上进行轮廓绘制,需要传入copy图像
def contours_demo(image):
dst = cv.GaussianBlur(image, (3, 3), 0)
gray = cv.cvtColor(dst, cv.COLOR_RGB2GRAY)
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)
contours, heriachy = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
for i, contour in enumerate(contours):
cv.drawContours(image, contours, i, (0, 0, 255), 2)
cv.imshow("contours", image)