读取显示图像
# 读取并显示图像
import cv2
path_to_image = r'pby.jpg'
"""
第二个参数
1 读取彩色,默认
0 读取灰度图
-1 加载图像,包括alpha通道
"""
original_image = cv2.imread(path_to_image, 1)
cv2.imshow('original image', original_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.namedWindow('image', cv2.WINDOW_NORMAL)
cv2.imshow('image', original_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
保存图像
# 保存图像
import cv2
img = cv2.imread('pby.jpg', 0)
cv2.imshow('image', img)
k = cv2.waitKey(0)
if k == 27: # 等待ESC退出
cv2.destroyAllWindows()
elif k == ord('s'): # 等待关键字,保存和退出
cv2.imwrite('gray.png', img)
cv2.destroyAllWindows()
使用matplotlib
本文章不深入讲解matplotlib的使用,后续更新matplotlib的详细使用教程
"""
OpenCV加载的彩色图像处于BGR模式。但是Matplotlib以RGB模式显示。
"""
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('pby.jpg', 0)
plt.imshow(img, cmap='gray', interpolation='bicubic')
plt.xticks([]), plt.yticks([]) # 隐藏 x 轴和 y 轴上的刻度值
plt.show()
访问摄像头
import cv2 as cv
cap = cv.VideoCapture(0) # 参数可以是0-3,0表示默认摄像头
print("width is %s" % cap.get(cv.CAP_PROP_FRAME_WIDTH))
print("height is %s" % cap.get(cv.CAP_PROP_FRAME_HEIGHT))
if cap.set(cv.CAP_PROP_FRAME_WIDTH, 320):
print('width now sets to 320')
if cap.set(cv.CAP_PROP_FRAME_HEIGHT, 240):
print('height now sets to 240')
if not cap.isOpened():
print("Cannot open camera")
exit()
while True:
# 逐帧捕获
ret, frame = cap.read()
# 如果正确读取帧,ret为True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
# 显示结果帧
cv.imshow('frame', gray)
if cv.waitKey(1) == ord('q'):
break
# 完成所有操作后,释放捕获器
cap.release()
cv.destroyAllWindows()
读取视频文件
import cv2 as cv
cap = cv.VideoCapture('output.avi') # 传入视频路径
while cap.isOpened():
ret, frame = cap.read()
# 如果正确读取帧,ret为True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
cv.imshow('frame', gray)
if cv.waitKey(25) == ord('q'):
break
cap.release()
cv.destroyAllWindows()
写视频文件
import cv2 as cv
cap = cv.VideoCapture(0)
# if cap.set(cv.CAP_PROP_FRAME_WIDTH, 320):
# print('width now sets to 320')
# if cap.set(cv.CAP_PROP_FRAME_HEIGHT, 240):
# print('height now sets to 240')
fourcc = cv.VideoWriter_fourcc(*'XVID') # 定义编解码器并创建VideoWriter对象
out = cv.VideoWriter('output.avi', fourcc, 20.0, (640, 480))
while cap.isOpened():
ret, frame = cap.read()
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
# gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
out.write(frame)
cv.imshow('frame', frame)
if cv.waitKey(1) == ord('q'):
break
# 完成工作后释放所有内容
cap.release()
out.release()
cv.destroyAllWindows()
在图像上画形状、文字
import cv2 as cv
import numpy as np
# 创建黑色的图像
img = np.zeros((512, 512, 3), np.uint8)
# 绘制一条厚度为5的蓝色对角线
cv.line(img, (0, 0), (511, 511), (255, 0, 0), 5)
# 画矩形
cv.rectangle(img, (384, 0), (510, 128), (0, 255, 0), 3)
# 画圆圈
cv.circle(img, (447, 63), 63, (0, 0, 255), -1)
# 画椭圆
cv.ellipse(img, (256, 256), (100, 50), 0, 0, 180, 255, -1)
# 画多边形
pts = np.array([[10, 5], [20, 30], [70, 20], [50, 10]], np.int32)
pts = pts.reshape((-1, 1, 2))
pts2 = np.array([[255, 240], [198, 189], [99, 345]], np.int32)
pts2 = pts2.reshape(-1, 1, 2)
cv.polylines(img, [pts], True, (0, 255, 255)) # 闭合多边形
cv.polylines(img, [pts2], False, (0, 255, 255)) # 只连接不闭合
# 向图片中添加文本
font = cv.FONT_HERSHEY_SIMPLEX
cv.putText(img, 'OpenCV', (10, 500), font, 4, (255, 255, 255), 2, cv.LINE_AA)
cv.imshow('img', img)
cv.waitKey(0)
cv.destroyAllWindows()
用鼠标画
import cv2 as cv
import numpy as np
import random
def main():
# 鼠标回调函数
def draw_circle(event, x, y, flags, param):
if event == cv.EVENT_LBUTTONDBLCLK:
cv.circle(img, (x, y), random.randint(50, 200), (255, 0, 0), -1)
# 创建一个黑色的图像,一个窗口,并绑定到窗口的功能
img = np.zeros((512, 512, 3), np.uint8)
cv.namedWindow('image')
cv.setMouseCallback('image', draw_circle)
while True:
cv.imshow('image', img)
if cv.waitKey(20) & 0xFF == 27: # 按esc退出
break
cv.destroyAllWindows()
if __name__ == '__main__':
main()
轨迹栏的使用
import cv2 as cv
import numpy as np
def nothing(x):
pass
# 创建一个黑色的图像
img = np.zeros((300, 512, 3), np.uint8)
cv.namedWindow('image')
# 创建颜色变化的轨迹栏
cv.createTrackbar('R', 'image', 0, 255, nothing)
cv.createTrackbar('G', 'image', 0, 255, nothing)
cv.createTrackbar('B', 'image', 0, 255, nothing)
# 为 ON/OFF 功能创建开关
switch = 'OFF/ON'
cv.createTrackbar(switch, 'image', 0, 1, nothing)
while True:
cv.imshow('image', img)
k = cv.waitKey(1) & 0xFF
if k == 27:
break
# 得到四条轨迹的当前位置
r = cv.getTrackbarPos('R', 'image')
g = cv.getTrackbarPos('G', 'image')
b = cv.getTrackbarPos('B', 'image')
s = cv.getTrackbarPos(switch, 'image')
if s == 0:
img[:] = 0
else:
img[:] = [b, g, r]
cv.destroyAllWindows()
访问像素点
# 访问像素点
def access_px():
img = cv.imread('pby.jpg') # 加载彩色图像,默认是彩色图像
px = img[100, 100] # 通过行和列坐标来访问像素值
print(px) # [37 61 73]
# 仅访问蓝色像素
blue = img[100, 100, 0] # opencv读取图片是BGR格式
print(blue) # 37
img[100, 100] = [255, 255, 255] # 修改该坐标对应的像素值
print(img[100, 100]) # [255 255 255]
# 上面的方法通常用于选择数组的区域,例如前5行和后3列。对于单个像素访问,Numpy数组方法array.item()和array.itemset())被认为更好,但是它们始终返回标量。
# 如果要访问所有B,G,R值,则需要分别调用所有的array.item()
# 访问 RED 值
print(img.item(10, 10, 2)) # 60
# 修改 RED 值
img.itemset((10, 10, 2), 100)
print(img.item(10, 10, 2)) # 100
访问图像属性
# 访问图像属性
def access_properties():
img = cv.imread('pby.jpg')
print(img.shape) # 访问图片的形状,返回行,列,通道数(如果读取的是彩色图片)如果图像是灰度的,则返回的元组仅包含行数和列数
print(img.size) # 获取图片像素总数
print(img.dtype) # 获取图像数据类型
裁剪感兴趣区域
# 图像感兴趣区域
def roi():
img = cv.imread('pby.jpg')
ball = img[280:340, 330:390]
cv.imshow('ball', ball)
cv.waitKey(3000)
cv.destroyAllWindows()
拆分合并通道
# 拆分/合并 通道
def split_merge():
img = cv.imread('pby.jpg')
b, g, r = cv.split(img) # 拆分图像通道,此操作比较耗时
b2 = img[:, :, 0] # 前面两个切片划定区域,第三个数取0,1,2 指定通道
cv.imshow('b2', b2)
img[:, :, 2] = 0 # 指定红色通道,并修改所有像素值为0
cv.imshow('changed every red px into 0', img)
# cv.imshow('b', b)
# cv.imshow('g', g)
# cv.imshow('r', r)
img_merge = cv.merge((b, g, r)) # 合并图像通道
cv.imshow('merged image', img_merge)
cv.waitKey(0)
cv.destroyAllWindows()
添加边框
def board():
BLUE = [255, 0, 0]
img1 = cv.imread('opencv-logo.png')
replicate = cv.copyMakeBorder(img1, 10, 10, 10, 10, cv.BORDER_REPLICATE)
reflect = cv.copyMakeBorder(img1, 10, 10, 10, 10, cv.BORDER_REFLECT)
reflect101 = cv.copyMakeBorder(img1, 10, 10, 10, 10, cv.BORDER_REFLECT_101)
wrap = cv.copyMakeBorder(img1, 10, 10, 10, 10, cv.BORDER_WRAP)
constant = cv.copyMakeBorder(img1, 10, 10, 10, 10, cv.BORDER_CONSTANT, value=BLUE)
plt.subplot(231), plt.imshow(img1, 'gray'), plt.title('ORIGINAL')
plt.subplot(232), plt.imshow(replicate, 'gray'), plt.title('REPLICATE')
plt.subplot(233), plt.imshow(reflect, 'gray'), plt.title('REFLECT')
plt.subplot(234), plt.imshow(reflect101, 'gray'), plt.title('REFLECT_101')
plt.subplot(235), plt.imshow(wrap, 'gray'), plt.title('WRAP')
# 图像由matplotlib显示 因此红色和蓝色通道将互换
plt.subplot(236), plt.imshow(constant, 'gray'), plt.title('CONSTANT')
plt.show()
执行结果:
图像加法&图像融合
def add_func():
x = np.uint8([250])
y = np.uint8([10])
print(x) # [250]
print(y) # [10]
print(cv.add(x, y)) # [[255]] # 250+10 = 260 => 255 OpenCV加法是饱和运算
print(x + y) # [4] Numpy加法是模运算
# 图像融合 也是图像加法,但是对图像赋予不同的权重,以使其具有融合或透明的感觉。
def addWeighted_func():
img1 = cv.imread('1.jpg')
img2 = cv.imread('2.jpg')
cv.imshow('img1', img1)
cv.imshow('img2', img2)
dst = cv.addWeighted(img1, 0.5, img2, 0.5, 0)
cv.imshow('dst', dst)
cv.waitKey(0)
cv.destroyAllWindows()
addWeighted_func
的执行结果:
位操作&掩码
def bit_operation():
img1 = cv.imread('1.jpg')
img2 = cv.imread('opencv-logo.png')
rows, cols, channels = img2.shape
roi = img1[0:rows, 0:cols]
# 现在创建logo的掩码,并同时创建其相反掩码
img2gray = cv.cvtColor(img2, cv.COLOR_BGR2GRAY)
ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)
cv.imshow('img2gray', img2gray)
cv.imshow('mask', mask)
mask_inv = cv.bitwise_not(mask)
cv.imshow('mask_inv', mask_inv)
# 现在将ROI中logo的区域涂黑
img1_bg = cv.bitwise_and(roi, roi, mask=mask_inv)
# 仅从logo图像中提取logo区域
img2_fg = cv.bitwise_and(img2, img2, mask=mask)
# 将logo放入ROI并修改主图像
dst = cv.add(img1_bg, img2_fg)
img1[0:rows, 0:cols] = dst
cv.imshow('res', img1)
cv.waitKey(0)
cv.destroyAllWindows()
执行结果:
以下图像依次为
灰度图
掩码一
掩码二
图像加法
使用opencv衡量代码性能
import cv2 as cv
# 使用opencv衡量代码性能
img1 = cv.imread('pby.jpg')
e1 = cv.getTickCount()
for i in range(5, 49, 2):
img1 = cv.medianBlur(img1, i)
e2 = cv.getTickCount()
t = (e2 - e1) / cv.getTickFrequency()
print(t) # 3.6950288
改变颜色空间
OpenCV中有超过150种颜色空间转换方法。但是我们将研究只有两个最广泛使用的,
BGR ↔ 灰色 和 BGR↔HSV。
对于颜色转换,我们使用cv函数。cvtColor(input_image, flag),其中flag决定转换的类型。
对于BGR→灰度转换,我们使用标志cv.COLOR_BGR2GRAY。
类似地,对于BGR→HSV,我们使用标志cv.COLOR_BGR2HSV。
要获取其他标记,只需在Python终端中运行以下命令
import cv2 as cv
# OpenCV中有超过150种颜色空间转换方法。但是我们将研究只有两个最广泛使用的,
# BGR ↔ 灰色 和 BGR↔HSV。
# 对于颜色转换,我们使用cv函数。cvtColor(input_image, flag),其中flag决定转换的类型。
# 对于BGR→灰度转换,我们使用标志cv.COLOR_BGR2GRAY。
# 类似地,对于BGR→HSV,我们使用标志cv.COLOR_BGR2HSV。
# 要获取其他标记,只需在Python终端中运行以下命令
flags = [i for i in dir(cv) if i.startswith('COLOR_')]
print(flags) # ['COLOR_BAYER_BG2BGR', 'COLOR_BAYER_BG2BGRA', ....,'COLOR_YUV420sp2RGBA', 'COLOR_mRGBA2RGBA']
HSV的色相范围为[0,179],饱和度范围为[0,255],值范围为[0,255]。不同的软件使用不同的规模。因此,如果你要将OpenCV值和它们比较,你需要将这些范围标准化。
HSV颜色模型
HSV(Hue, Saturation, Value)是根据颜色的直观特性由A. R. Smith在1978年创建的一种颜色空间, 也称六角锥体模型(Hexcone Model)。、这个模型中颜色的参数分别是:色调(H),饱和度(S),亮度(V)。
色调H:用角度度量,取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。它们的补色是:黄色为60°,青色为180°,品红为300°;
饱和度S:取值范围为0.0~1.0;
亮度V:取值范围为0.0(黑色)~1.0(白色)。
RGB和CMY颜色模型都是面向硬件的,而HSV(Hue Saturation Value)颜色模型是面向用户的。
HSV模型的三维表示从RGB立方体演化而来。设想从RGB沿立方体对角线的白色顶点向黑色顶点观察,就可以看到立方体的六边形外形。六边形边界表示色彩,水平轴表示纯度,明度沿垂直轴测量。
HSV颜色分量范围
一般对颜色空间的图像进行有效处理都是在HSV空间进行的,然后对于基本色中对应的HSV分量需要给定一个严格的范围,下面是通过实验计算的模糊范围(准确的范围在网上都没有给出)。
H: 0— 180
S: 0— 255
V: 0— 255
此处把部分红色归为紫色范围:
HSV六棱锥
H参数表示色彩信息,即所处的光谱颜色的位置。该参数用一角度量来表示,红、绿、蓝分别纯度S为一比例值,范围从0到1,它表示成所选颜色的纯度和该颜色最大的纯度之间的比率。S=0时,只有灰度。相隔120度。互补色分别相差180度。
V表示色彩的明亮程度,范围从0到1。有一点要注意:它和光强度之间并没有直接的联系。
HSV对用户来说是一种直观的颜色模型。我们可以从一种纯色彩开始,即指定色彩角H,并让V=S=1,然后我们可以通过向其中加入黑色和白色来得到我们需要的颜色。增加黑色可以减小V而S不变,同样增加白色可以减小S而V不变。例如,要得到深蓝色,V=0.4 S=1 H=240度。要得到淡蓝色,V=1 S=0.4 H=240度。
一般说来,人眼最大能区分128种不同的色彩,130种色饱和度,23种明暗度。如果我们用16Bit表示HSV的话,可以用7位存放H,4位存放S,5位存放V,即745或者655就可以满足我们的需要了。由于HSV是一种比较直观的颜色模型,所以在许多图像编辑工具中应用比较广泛,如Photoshop(在Photoshop中叫HSB)等等,但这也决定了它不适合使用在光照模型中,许多光线混合运算、光强运算等都无法直接使用HSV来实现。
追踪指定颜色的物体
def track_color():
cap = cv.VideoCapture(0)
while True:
# 读取帧
_, frame = cap.read()
# 转换颜色空间 BGR 到 HSV
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
# 定义HSV中蓝色的范围
lower_blue = np.array([110, 50, 50])
upper_blue = np.array([130, 255, 255])
# 设置HSV的阈值使得只取蓝色
mask = cv.inRange(hsv, lower_blue, upper_blue)
# 将掩膜和图像逐像素相加
res = cv.bitwise_and(frame, frame, mask=mask)
cv.imshow('frame', frame)
cv.imshow('mask', mask)
cv.imshow('res', res)
k = cv.waitKey(5) & 0xFF
if k == 27:
break
cv.destroyAllWindows()
执行结果如下:
可以看到还是有不少噪点的,这里是用电脑的前置摄像头拍摄,手机纯蓝色背景,受光照影响,显示效果不好
找到指定颜色的HSV值
def get_hsv_value():
# 绿色的图片
green = np.uint8([[[0, 255, 0]]])
# 获取绿色的hsv值
hsv_green = cv.cvtColor(green, cv.COLOR_BGR2HSV)
print(hsv_green) # [[[60 255 255]]]
现在把 [H- 10,100,100] 和 [H+ 10,255, 255] 分别作为下界和上界即可追踪指定颜色了
图像几何变换
1.缩放
# 缩放图片
def resize():
img = cv.imread('pby.jpg')
res1 = cv.resize(img, None, fx=0.25, fy=0.25, interpolation=cv.INTER_CUBIC)
cv.imshow('res1', res1)
# 或者
height, width = img.shape[:2]
res2 = cv.resize(img, (int(0.25 * width), int(0.25 * height)), interpolation=cv.INTER_CUBIC)
cv.imshow('res2', res2)
cv.waitKey(0)
cv.destroyAllWindows()
2.平移
# 平移
def translate():
img = cv.imread('pby.jpg', 0)
rows, cols = img.shape
M = np.float32([[1, 0, 100], [0, 1, 50]])
dst = cv.warpAffine(img, M, (cols, rows))
cv.imshow('img', dst)
cv.waitKey(0)
cv.destroyAllWindows()
3.旋转
# 旋转
def rotate():
img = cv.imread('pby.jpg', 0)
rows, cols = img.shape
# cols-1 和 rows-1 是坐标限制
M = cv.getRotationMatrix2D(((cols - 1) / 2.0, (rows - 1) / 2.0), 90, 1)
print(M)
# [[ 6.12323400e-17 1.00000000e+00 -1.13686838e-13]
# [-1.00000000e+00 6.12323400e-17 1.07900000e+03]]
dst = cv.warpAffine(img, M, (cols, rows))
cv.imshow('img', dst)
cv.waitKey(0)
cv.destroyAllWindows()
4.仿射变换
# 仿射变换
# 在仿射变换中,原始图像中的所有平行线在输出图像中仍将平行。为了找到变换矩阵,我们需要输入图像中的三个点及其在输出图像中的对应位置。
# 然后cv.getAffineTransform将创建一个2x3矩阵,该矩阵将传递给cv.warpAffine
def affine():
img = cv.imread('drawing.png')
rows, cols, ch = img.shape
pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
pts2 = np.float32([[10, 100], [200, 50], [100, 250]])
M = cv.getAffineTransform(pts1, pts2)
print(M)
dst = cv.warpAffine(img, M, (cols, rows))
cv.imshow('input', img)
cv.imshow('output', dst)
cv.waitKey(0)
cv.destroyAllWindows()
5.透视变换
# 透视变换
# 对于透视变换,您需要3x3变换矩阵。即使在转换后,直线也将保持直线。要找到此变换矩阵,需要在输入图像上有4个点,在输出图像上需要相应的点。
# 在这四个点中,其中三个不应共线。然后通过函数cv.getPerspectiveTransform找到变换矩阵。然后将cv.warpPerspective应用于此3x3转换矩阵
def perspective_transform():
img = cv.imread('sudoku.png')
# rows, cols, ch = img.shape
pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]])
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])
M = cv.getPerspectiveTransform(pts1, pts2)
dst = cv.warpPerspective(img, M, (300, 300))
cv.imshow('img', img)
cv.imshow('dst', dst)
cv.waitKey(0)
cv.destroyAllWindows()
# plt.subplot(121), plt.imshow(img), plt.title('Input')
# plt.subplot(122), plt.imshow(dst), plt.title('Output')
# plt.show()
图像阈值处理
简单阈值处理
threshold
函数用于应用阈值。其参数分别为:
1.源图像(必须为灰度图)
2.阈值
3.超出阈值时所分配的最大值
4.阈值类型
阈值类型主要有以下几种:
import cv2 as cv
from matplotlib import pyplot as plt
# 简单阈值,对于每个像素,应用相同的阈值。如果像素值小于阈值,则将其设置为0,否则将其设置为最大值。
def simple_threshold():
img = cv.imread('pby.jpg') # 读取图片
img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 转成灰度图
# threshold函数 用于应用阈值。其参数分别为:1.源图像(必须为灰度图)2.阈值 3.超出阈值时所分配的最大值4.阈值类型
ret, thresh1 = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY)
ret, thresh2 = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY_INV)
ret, thresh3 = cv.threshold(img_gray, 127, 255, cv.THRESH_TRUNC)
ret, thresh4 = cv.threshold(img_gray, 127, 255, cv.THRESH_TOZERO)
ret, thresh5 = cv.threshold(img_gray, 127, 255, cv.THRESH_TOZERO_INV)
titles = ['Original Image', 'gray image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
img_rgb = cv.cvtColor(img, cv.COLOR_BGR2RGB)
images = [img_rgb, img_gray, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(7):
plt.subplot(2, 4, i + 1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
运行结果如下:
自适应阈值处理
# 当同一幅图像上的不同部分具有不同亮度时。这种情况下我们需要采用自适应阈值。此时的阈值是根据图像上的每一个小区域计算与其对应的阈值。
# 因此在同一幅图像上的不同区域采用的是不同的阈值,从而使我们能在亮度不同的情况下得到更好的结果。
def adaptive_threshold():
img = cv.imread('pby.jpg')
img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 中值滤波
img_gray = cv.medianBlur(img_gray, 5)
ret, th1 = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY)
# 11 为 Block size,即邻域大小,用于计算阈值的窗口大小, 2 为 常数,可以理解为偏移量
th2 = cv.adaptiveThreshold(img_gray, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 11, 2)
th3 = cv.adaptiveThreshold(img_gray, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)
titles = ['Original Image', 'Global Thresholding (v = 127)', 'Adaptive Mean Thresholding',
'Adaptive Gaussian Thresholding']
img_rgb = cv.cvtColor(img, cv.COLOR_BGR2RGB)
images = [img_rgb, th1, th2, th3]
for i in range(4):
plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
运行结果:
中值滤波
这里介绍一下中值滤波的概念
无论是直接获取的灰度图像,还是由彩色图像转换得到的灰度图像,里面都有噪声的存在,噪声对图像质量有很大的影响。进行中值滤波不仅可以去除孤点噪声,而且可以保持图像的边缘特性,不会使图像产生显著的模糊,比较适合于实验中的人脸图像。
中值滤波是一种非线性的信号处理方法,因此中值滤波器也就是一种非线性的滤波器。在一定条件下,其可以克服线性滤波器处理图像细节模糊的问题,而且它对滤除脉冲干扰和图像扫描噪声非常有效,但是,对点、线、尖顶等细节较多的图像,则会引起图像信息的丢失。中值滤波器最先被应用于一维信号的处理中,后来被人们引用到二维图像的处理中来。
中值滤波是对一个滑动窗口内的诸像素灰度值排序,用其中值代替窗口中心像素的原来灰度值,它是一种非线性的图像平滑法,它对脉冲干扰级椒盐噪声的抑制效果好,在抑制随机噪声的同时能有效保护边缘少受模糊。
中值滤波可以过滤尖峰脉冲。目的在于我们对于滤波后的数据更感兴趣。滤波后的数据保留的原图像的变化趋势,同时去除了尖峰脉冲对分析造成的影响。
以一维信号的中值滤波举例。对灰度序列80、120、90、200、100、110、70,如果按大小顺序排列,其结果为70、80、90、10O、110、120、200,其中间位置上的灰度值为10O,则该灰度序列的中值即为100。一维信号中值滤波实际上就是用中值代替规定位置(一般指原始信号序列中心位置)的信号值。对前面所举的序列而言,中值滤波的结果是用中值100替代序列80、120、90、200、100、110、70中的信号序列中心位置值200,得到的滤波序列就是80、120、90、100、100、110、70。如果在此序列中200是一个噪声信号,则用此方法即可去除这个噪声点。
二维中值滤波算法是对于一幅图像的像素矩阵,取以目标像素为中心的一个子矩阵窗口,这个窗口可以是33 ,55 等根据需要选取,对窗口内的像素灰度排序,取中间一个值作为目标像素的新灰度值。窗口示例如ooooxoooo上面x为目标像素,和周围o组成3*3矩阵Array,然后对这9个元素的灰度进行排序,以排序后的中间元素Array[4]为x的新灰度值,如此就完成对像素x的中值滤波,再迭代对其他需要的像素进行滤波即可。
中值滤波的基本思想是,把局部区域的像素按灰度等级进行排序,取该领域中灰度的中值作为当前像素的灰度值。
中值滤波的步骤为:
-
将滤波模板(含有若干个点的滑动窗口)在图像中漫游,并将模板中心与图中某个像素位置重合;
-
读取模板中各对应像素的灰度值;
-
将这些灰度值从小到大排列;
-
取这一列数据的中间数据,将其赋给对应模板中心位置的像素。如果窗口中有奇数个元素,中值取元素按灰度值大小排序后的中间元素灰度值。如果窗口中有偶数个元素,中值取元素按灰度值大小排序后,中间两个元素灰度的平均值。
因为图像为二维信号,中值滤波的窗口形状和尺寸对滤波器效果影响很大,不同图像内容和不同应用要求往往选用不同的窗口形状和尺寸。
由以上步骤,可以看出,中值滤波对孤立的噪声像素即椒盐噪声、脉冲噪声具有良好的滤波效果。由于其并不是简单的取均值,所以,它产生的模糊也就相对比较少。
二值化
# otsu 二值化
# 在使用全局阈值时,我们就是随便给了一个数来做阈值,那我们怎么知道我们选取的这个数的好坏呢?答案就是不停的尝试。
# 如果是一副双峰图像(简单来说双峰图像是指图像直方图中存在两个峰)呢?我们岂不是应该在两个峰之间的峰谷选一个值作为阈值?
# 这就是 Otsu 二值化要做的。简单来说就是对一副双峰图像自动根据其直方图计算出一个阈值。(对于非双峰图像,这种方法得到的结果可能会不理想)。
# 这里用到的函数还是 cv2.threshold(),但是需要多传入一个参数(flag):cv2.THRESH_OTSU。这时要把阈值设为 0。然后算法会找到最优阈值,
# 这个最优阈值就是返回值 retVal。如果不使用 Otsu 二值化,返回的retVal 值与设定的阈值相等。
# 下面的例子中,输入图像是一副带有噪声的图像。
# 第一种方法,我们设127 为全局阈值。
# 第二种方法,我们直接使用 Otsu 二值化。
# 第三种方法,我们首先使用一个 5x5 的高斯核除去噪音,然后再使用 Otsu 二值化。
def otsu():
img = cv.imread('noisy.png')
img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# global thresholding
ret1, th1 = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY)
# Otsu's thresholding
ret2, th2 = cv.threshold(img_gray, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
# Otsu's thresholding after Gaussian filtering
# (5,5)为高斯核的大小,0 为标准差
blur = cv.GaussianBlur(img_gray, (5, 5), 0)
# 阈值一定要设为 0!
ret3, th3 = cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
# plot all the images and their histograms
images = [img_gray, 0, th1,
img_gray, 0, th2,
blur, 0, th3]
titles = ['Original Noisy Image', 'Histogram', 'Global Thresholding (v=127)',
'Original Noisy Image', 'Histogram', "Otsu's Thresholding",
'Gaussian filtered Image', 'Histogram', "Otsu's Thresholding"]
# 这里使用了 pyplot 中画直方图的方法,plt.hist, 要注意的是它的参数是一维数组
# 所以这里使用了(numpy)ravel 方法,将多维数组转换成一维,也可以使用 flatten 方法
# ndarray.flat 1-D iterator over an array.
# ndarray.flatten 1-D array copy of the elements of an array in row-major order.
for i in range(3):
plt.subplot(3, 3, i * 3 + 1), plt.imshow(images[i * 3], 'gray')
plt.title(titles[i * 3]), plt.xticks([]), plt.yticks([])
plt.subplot(3, 3, i * 3 + 2), plt.hist(images[i * 3].ravel(), 256)
plt.title(titles[i * 3 + 1]), plt.xticks([]), plt.yticks([])
plt.subplot(3, 3, i * 3 + 3), plt.imshow(images[i * 3 + 2], 'gray')
plt.title(titles[i * 3 + 2]), plt.xticks([]), plt.yticks([])
plt.show()
执行结果:
otsu二值化工作原理
# 手动计算阈值
def calculate_otsu():
img = cv.imread('noisy.png')
img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
blur = cv.GaussianBlur(img_gray, (5, 5), 0)
# find normalized_histogram, and its cumulative distribution function
# 计算归一化直方图
# CalcHist(image, accumulate=0, mask=NULL)
hist = cv.calcHist([blur], [0], None, [256], [0, 256])
hist_norm = hist.ravel() / hist.max()
Q = hist_norm.cumsum()
bins = np.arange(256)
fn_min = np.inf
thresh = -1
for i in range(1, 256):
p1, p2 = np.hsplit(hist_norm, [i]) # probabilities
q1, q2 = Q[i], Q[255] - Q[i] # cum sum of classes
b1, b2 = np.hsplit(bins, [i]) # weights
# finding means and variances
m1, m2 = np.sum(p1 * b1) / q1, np.sum(p2 * b2) / q2
v1, v2 = np.sum(((b1 - m1) ** 2) * p1) / q1, np.sum(((b2 - m2) ** 2) * p2) / q2
# calculates the minimization function
fn = v1 * q1 + v2 * q2
if fn < fn_min:
fn_min = fn
thresh = i
# find otsu's threshold value with OpenCV function
ret, otsu, = cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
print(thresh)
print(ret)
图像平滑处理
自定义滤波器进行卷积
# 在介绍四种模糊(平滑)滤波器之前,先使用自定义滤波器对图像进行卷积处理
def custom_filter():
img = cv2.imread('opencv-logo.png')
kernel = np.ones((5, 5), np.float32) / 25
print(kernel)
# cv.Filter2D(src, dst, kernel, anchor=(-1, -1))
# depth –desired depth of the destination image;
# if it is negative, it will be the same as src.depth();
# the following combinations of src.depth() and depth are supported:
# src.depth() = CV_8U, depth = -1/CV_16S/CV_32F/CV_64F
# src.depth() = CV_16U/CV_16S, depth = -1/CV_32F/CV_64F
# src.depth() = CV_32F, depth = -1/CV_32F/CV_64F
# src.depth() = CV_64F, depth = -1/CV_64F
# when depth=-1, the output image will have the same depth as the source.
dst = cv2.filter2D(img, -1, kernel)
plt.subplot(121), plt.imshow(img), plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(dst), plt.title('Averaging'), plt.xticks([]), plt.yticks([])
plt.show()
执行结果
打印输出
[[0.04 0.04 0.04 0.04 0.04]
[0.04 0.04 0.04 0.04 0.04]
[0.04 0.04 0.04 0.04 0.04]
[0.04 0.04 0.04 0.04 0.04]
[0.04 0.04 0.04 0.04 0.04]]
均值滤波器
# 用卷积框覆盖区域所有像素的平均值来代替中心元素。
def average_filter():
img = cv2.imread('opencv-logo.png')
blur = cv2.blur(img, (5, 5))
plt.subplot(121), plt.imshow(img), plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(blur), plt.title('Blurred'), plt.xticks([]), plt.yticks([])
plt.show()
高斯滤波器
# 高斯核(简单来说,方框不变,将原来每个方框的值是相等的,现在里面的值是符合高斯分布的,方框中心的值最大,其余方框根据
# 距离中心元素的距离递减,构成一个高斯小山包。原来的求平均数现在变成求加权平均数,权就是方框里的值)
# 高斯滤波器是求中心点邻近区域像素的高斯加权平均值。这种高斯滤波器只考虑像素之间的空间关系,而不会考虑像素值之间的关系(像素的相似度)。
# 所以这种方法不会考虑一个像素是否位于边界。因此边界也会模糊掉
def gaussian_blur():
img = cv2.imread('opencv-logo.png')
blur = cv2.GaussianBlur(img, (5, 5), 0, 0)
plt.subplot(121), plt.imshow(img), plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(blur), plt.title('Blurred'), plt.xticks([]), plt.yticks([])
plt.show()
中值滤波器
# 用与卷积框对应像素的中值来替代中心像素的值。这个滤波器经常用来去除椒盐噪声。前面的滤波器都是用计算得到的一个新值来取代中
# 心像素的值,而中值滤波是用中心像素周围(也可以使他本身)的值来取代他。他能有效的去除噪声。卷积核的大小也应该是一个奇数。
def median_blur():
img = cv2.imread('opencv-logo.png')
blur = cv2.medianBlur(img, (5, 5))
plt.subplot(121), plt.imshow(img), plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(blur), plt.title('Blurred'), plt.xticks([]), plt.yticks([])
plt.show()
双边滤波器
# 双边滤波在同时使用空间高斯权重和灰度值相似性高斯权重。空间高斯函数确保只有邻近区域的像素对中心点有影响,灰度值相似性高斯函数确保只有
# 与中心像素灰度值相近的才会被用来做模糊运算。所以这种方法会确保边界不会被模糊掉,因为边界处的灰度值变化比较大。
# 通常用于处理图片纹理,同时保留边界
def bilateral_blur():
img = cv2.imread('opencv-logo.png')
# cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace)
# d – Diameter of each pixel neighborhood that is used during filtering.
# If it is non-positive, it is computed from sigmaSpace
# 9 邻域直径,两个 75 分别是空间高斯函数标准差,灰度值相似性高斯函数标准差
blur = cv2.bilateralFilter(img, 9, 75, 75)
plt.subplot(121), plt.imshow(img), plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(blur), plt.title('Blurred'), plt.xticks([]), plt.yticks([])
plt.show()
形态学操作 腐蚀&膨胀&开运算&闭运算&形态学梯度&礼帽&黑帽
腐蚀和膨胀
def erode_dilate():
img = cv2.imread('j.png', cv2.IMREAD_COLOR)
kernel = np.ones((5, 5), np.uint8)
erosion = cv2.erode(img, kernel, iterations=1) # 腐蚀
dilation = cv2.dilate(img, kernel, iterations=1) # 膨胀
titles = ['original image', 'eroded image', 'dilated image']
images = [img, erosion, dilation]
for i in range(3):
plt.subplot(1, 3, i + 1), plt.imshow(images[i], 'gray'), plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
执行结果:
开运算【先腐蚀再膨胀】
# 先进行腐蚀再进行膨胀就叫做开运算,常用来去除噪声
def morphologyEx_open():
img = cv2.imread('j with noise.png', cv2.IMREAD_COLOR)
kernel = np.ones((5, 5), np.uint8)
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
titles = ['original image', 'opening image']
images = [img, opening]
for i in range(2):
plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray'), plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
运行结果
闭运算【先膨胀再腐蚀】
# 先膨胀再腐蚀。它经常被用来填充前景物体中的小洞,或者前景物体上的小黑点。
def morphologyEx_close():
img = cv2.imread('j with noise2.png', cv2.IMREAD_COLOR)
kernel = np.ones((5, 5), np.uint8)
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
titles = ['original image', 'closing image']
images = [img, closing]
for i in range(2):
plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray'), plt.title(titles[i]),
plt.xticks([]), plt.yticks([])
plt.show()
运行结果:
形态学梯度
# 一幅图像膨胀与腐蚀的差,看上去就像是前景物体的轮廓
def morphologyEx_gradient():
img = cv2.imread('j.png', cv2.IMREAD_COLOR)
kernel = np.ones((5, 5), np.uint8)
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
titles = ['original image', 'gradient image']
images = [img, gradient]
for i in range(2):
plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray'), plt.title(titles[i]),
plt.xticks([]), plt.yticks([])
plt.show()
运行结果:
礼帽&黑帽
# 原始图像与进行开运算之后得到的图像的差。
# tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
# 进行闭运算之后得到的图像与原始图像的差
# blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
def morphologyEx_tophat_blackhat():
img = cv2.imread('j.png', cv2.IMREAD_COLOR)
kernel = np.ones((5, 5), np.uint8)
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel) # 礼帽
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel) # 黑帽
titles = ['original image', 'tophat image', 'blackhat image']
images = [img, tophat, blackhat]
for i in range(3):
plt.subplot(1, 3, i + 1), plt.imshow(images[i], 'gray'), plt.title(titles[i]),
plt.xticks([]), plt.yticks([])
plt.show()
运行结果:
形态学操作之间的关系
-
opening:
dst = open(src,element) = dilate(erode(src,element),element)
-
closing:
dst = close(src,element) = erode(dilate(src,element),element)
-
morphological gradient:
dst = morph_grad(src,element) = dilate(src,element) - erode(src,element)
-
tophat:
dst = tophat(src,element) = src - open(src,element)
-
blackhat:
dst = blackhat(src,element) = close(src,element) - src
结构化元素
在前面的例子中我们使用 Numpy 构建了结构化元素,它是正方形的。但有时我们需要构建一个椭圆形/圆形的核。为了实现这种要求,提供了 OpenCV 函数 cv2.getStructuringElement()。你只需要告诉他你需要的核的形状 和大小。
# Rectangular Kernel
>>> cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))
array(
[[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]], dtype=uint8)
# Elliptical Kernel
>>> cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
array([
[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0]], dtype=uint8)
# Cross-shaped Kernel
>>> cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5))
array(
[[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]], dtype=uint8)
图像梯度
梯度简单来说就是求导。 OpenCV
提供了三种不同的梯度滤波器,或者说高通滤波器:Sobel, Scharr 和 Laplacian。Sobel,Scharr 其实就是求一阶或二阶导数。Scharr 是对 Sobel(使用小的卷积核求解梯度角度时)的优化。Laplacian 是求二阶导数。
Sobel算子和Scharr算子
Sobel 算子是高斯平滑与微分操作的结合体,所以它的抗噪声能力很好。 你可以设定求导的方向(xorder 或 yorder)。还可以设定使用的卷积核的大 小(ksize)。如果 ksize=-1,会使用 3x3 的 Scharr 滤波器,它的效果要 比 3x3 的 Sobel 滤波器好(而且速度相同,所以在使用 3x3 滤波器时应该尽量使用 Scharr 滤波器)。3x3 的 Scharr 滤波器卷积核如下
Laplacian 算子
拉普拉斯算子可以使用二阶导数的形式定义,可假设其离散实现类似于二阶 Sobel 导数,事实上,OpenCV 在计算拉普拉斯算子时直接调用 Sobel 算子。计算公式如下:
拉普拉斯滤波器使用的卷积核:
def sobel_laplacian():
img = cv2.imread('dave.png', cv2.IMREAD_GRAYSCALE)
# cv2.CV_64F 输出图像的深度(数据类型),可以使用-1, 与原图像保持一致 np.uint8
laplacian = cv2.Laplacian(img, cv2.CV_64F)
# 参数 1,0 为只在 x 方向求一阶导数,最大可以求 2 阶导数。
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
# 参数 0,1 为只在 y 方向求一阶导数,最大可以求 2 阶导数。
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5)
plt.subplot(2, 2, 1), plt.imshow(img, cmap='gray'), plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(2, 2, 2), plt.imshow(laplacian, cmap='gray'), plt.title('Laplacian'), plt.xticks([]), plt.yticks([])
plt.subplot(2, 2, 3), plt.imshow(sobel_x, cmap='gray'), plt.title('Sobel X'), plt.xticks([]), plt.yticks([])
plt.subplot(2, 2, 4), plt.imshow(sobel_y, cmap='gray'), plt.title('Sobel Y'), plt.xticks([]), plt.yticks([])
plt.show()
运行结果:
在查看上面这个例子的注释时不知道你有没有注意到:当我们可以通过参数 -1 来设定输出图像的深度(数据类型)与原图像保持一致,但是我们在代码中使用的却是 cv2.CV_64F。这是为什么呢?想象一下一个从黑到白的边界 的导数是整数,而一个从白到黑的边界点导数却是负数。如果原图像的深度是 np.int8 时,所有的负值都会被截断变成 0,换句话说就是把边界丢失掉。 所以如果这两种边界你都想检测到,最好的办法就是将输出的数据类型设置的更高,比如 cv2.CV_16S,cv2.CV_64F 等。取绝对值然后再把它转回到 cv2.CV_8U。下面的示例演示了输出图片的深度不同造成的不同效果。
def depth_of_output():
img = cv2.imread('boxs.png', cv2.IMREAD_GRAYSCALE)
# Output dtype = cv2.CV_8U
sobel_x8u = cv2.Sobel(img, cv2.CV_8U, 1, 0, ksize=5)
# 也可以将参数设为-1
# sobel_x8u = cv2.Sobel(img,-1,1,0,ksize=5)
# Output dtype = cv2.CV_64F. Then take its absolute and convert to cv2.CV_8U
sobel_x64f = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
abs_sobel64f = np.absolute(sobel_x64f)
sobel_8u = np.uint8(abs_sobel64f)
plt.subplot(1, 3, 1), plt.imshow(img, cmap='gray'), plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(1, 3, 2), plt.imshow(sobel_x8u, cmap='gray'), plt.title('Sobel CV_8U'), plt.xticks([]), plt.yticks([])
plt.subplot(1, 3, 3), plt.imshow(sobel_8u, cmap='gray'), plt.title('Sobel abs(CV_64F)'), plt.xticks([]), plt.yticks([])
plt.show()
运行结果:
Canny边缘检测
Canny 边缘检测是一种非常流行的边缘检测算法,是 John F.Canny 在 1986 年提出的。它是一个有很多步构成的算法。
1.噪声去除
由于边缘检测很容易受到噪声影响,所以第一步是使用 5x5 的高斯滤波器去除噪声
2.计算图像梯度
对平滑后的图像使用 Sobel 算子计算水平方向和竖直方向的一阶导数(图像梯度)(Gx 和 Gy)。根据得到的这两幅梯度图(Gx 和 Gy)找到边界的梯度和方向,公式如下
梯度的方向一般总是与边界垂直。梯度方向被归为四类:垂直,水平,和 两个对角线
3.非极大值抑制
在获得梯度的方向和大小之后,应该对整幅图像做一个扫描,去除那些非边界上的点。对每一个像素进行检查,看这个点的梯度是不是周围具有相同梯度方向的点中最大的。
现在你得到的是一个包含“窄边界”的二值图像
4.滞后阈值
现在要确定那些边界才是真正的边界。这时我们需要设置两个阈值: minVal 和 maxVal。
当图像的灰度梯度高于 maxVal 时被认为是真的边界, 那些低于 minVal 的边界会被抛弃。
如果介于两者之间的话,就要看这个点是否与某个被确定为真正的边界点相连,如果是就认为它也是边界点,如果不是就抛弃。
A 高于阈值 maxVal 所以是真正的边界点,C 虽然低于 maxVal 但高于 minVal 并且与 A 相连,所以也被认为是真正的边界点。而 B 就会被抛弃,因为他不仅低于 maxVal 而且不与真正的边界点相连。所以选择合适的 maxVal 和 minVal 对于能否得到好的结果非常重要。 在这一步,一些小的噪声点也会被除去,因为我们假设边界都是一些长的线段。
def canny():
img = cv2.imread('tree.jpg', cv2.IMREAD_GRAYSCALE)
edges = cv2.Canny(img, 80, 100)
plt.subplot(121), plt.imshow(img, cmap='gray'), plt.title('Original Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(edges, cmap='gray'), plt.title('Edge Image'), plt.xticks([]), plt.yticks([])
plt.show()
运行结果: