代办事项:
-
- 章节8之后的代码参数含义尽量注释起来,便于理解
-
- 10 章之后的内容听得比较迷糊,重复听下
前言
感谢极棒的数字图像处理入门到进阶教程:Python OpenCV实战数图_哔哩哔哩_bilibili老师开源了部分课程,基于这门课做的笔记,用到了老师的课件截图(侵删),推荐到腾讯课堂支持下这个老师
numpy
参考:NumPy 中文
Numpy全面语法教程 & 更好理解Numpy用法 - 知乎 (zhihu.com)
- 矩阵创建及切片
objp = np.zeros((6 * 8, 3), np.float32) # 从索引为2行切到5行(不包括5),2列切到4列(不包括4列), 索引的起始下标为0 objp[2:5, 2:4] # 创建二位矩阵 objp = np.zeros((6, 3), np.float32) print(objp) [[0. 0. 0.] [0. 0. 0.] [0. 0. 0.] [0. 0. 0.] [0. 0. 0.] [0. 0. 0.]]
- 绘图矩阵
# 返回两个矩阵 print(np.mgrid[0:8, 0:6]) [[[0 0 0 0 0 0] [1 1 1 1 1 1] [2 2 2 2 2 2] [3 3 3 3 3 3] [4 4 4 4 4 4] [5 5 5 5 5 5] [6 6 6 6 6 6] [7 7 7 7 7 7]] [[0 1 2 3 4 5] [0 1 2 3 4 5] [0 1 2 3 4 5] [0 1 2 3 4 5] [0 1 2 3 4 5] [0 1 2 3 4 5] [0 1 2 3 4 5] [0 1 2 3 4 5]]]
- 将 3 rows 4 cols 上的点填充
# 参数分别指定列号的变化关系,及行号的变化关系 x,y = np.mgrid[0:4, 0:3] print(x) # 打印输出 # x 矩阵 [[0 0 0] [1 1 1] [2 2 2] [3 3 3]] print(y) # y 矩阵 [[0 1 2] [0 1 2] [0 1 2] [0 1 2]] # 绘图 plt.plot(x, y, '*', color = 'r') plt.show() # 将点格式化成2列, [x, y]的形式 print(np.mgrid[0:4, 0:3].T.reshape(-1, 2)) # 2列的矩阵 [[0 0] [1 0] [2 0] [3 0] [0 1] [1 1] [2 1] [3 1] [0 2] [1 2] [2 2] [3 2]]
plt
show图片的函数import cv2 as cv ''' import matplotlib.pyplot as plt ''' from matplotlib, import pyplot as plt def show(img): if img.ndim == 2: plt.imshow(img, cmap = 'gray', vmin = 0, vmax = 255) else: plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB)) plt.show() img = cv.imread("./1.png") show(img) img = cv.imread("./1.png", 0) show(img)
- 最简单的绘图
- 绘单条曲线
# 得到数组 x = np.arange(2, 20) y = 2*x + np.random.randint(5, 20, 18) plt.plot(x, y, '*-', color = 'r') # plt.plot(x, y, color = 'r') plt.show()
- 绘多条曲线
分开绘图# 均分切片,100个元素的数组 x = np.linspace(0, 1, 100) y1 = np.power(x, 0.5) y2 = np.power(x, 1) y3 = np.power(x, 1.5) plt.plot(x, y1, x, y2, x, y3) plt.show()
# 均分切片,100个元素的数组 x = np.linspace(0, 1, 100) y1 = np.power(x, 0.5) y2 = np.power(x, 1) y3 = np.power(x, 1.5) # 分开写 plt.plot(x, y1, label = '0.5') plt.plot(x, y2, label = '1') plt.plot(x, y3, label = '1.5') # 显示图例 plt.legend() # 显示网格 plt.grid() # 轴起始坐标 plt.xlim([0, 1]) plt.ylim([0, 1]) # 设置轴名称 plt.xlabel("x") plt.ylabel('y') plt.show()
- 绘单条曲线
- 绘直方图
- 最简单绘制
# 生成1000个随机数,范围在0,101之间(不包括101) a = np.random.randint(0, 101, 1000) # 直方图,参数二规范感兴趣的直方范围, 只显示0.9部分,间隔0.1 plt.hist(a, rwidth = 0.9, color = 'g') plt.show()
- 规定感兴趣区域
# 拿到数组,起点是-0.5,终点是101,步长是1 # 这样的话,间隔1,落到直方里, 比如 0, 1, 2 这些整数刚好能落到-0.5~1.5的自方里 bins = np.arange(-0.5, 101, 1) # 将数组a直方化 plt.hist(a, bins) plt.show()
- 最简单绘制
数图
-
灰度图矩阵
a = np.random.randint(0, 256, (2, 4), dtype = uint8) show(a)
-
真彩色矩阵,三维矩阵
# 定义三维时一定要是(x, x, 3) b = np.random.randint(0, 256, (2, 4, 3), dtype = uint8) show(b)
-
查看矩阵行列,通道数
a.shape
输出
# 二维矩阵 (200, 300) # 三维矩阵 (200, 400, 3)
-
通道合并
img = cv.merge([b, g, r])
-
float
型矩阵转uint8
的矩阵uint8_array = np.uint8(float_array) # 或者 uint8_array = float_array.astype(np.uint8)
-
彩色转灰度图
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
-
阈值二值化
thresh = 125 gray[gray > 125] = 255 gray[gray <= 125] = 0
opencv 的
_, img_bin = cv.threshold(gray, 125, 255, cv.THRESH_BINARY)
-
几张图片矩阵拼接
mat = np.hstack([mat1, mat2, mat3])
-
图片相加,矩阵相加的话,溢出会做循环处理
opencv提供图片相加的方法,超过类型(比如uint8)会截断img = cv,add(img1, img2)
一般加权相加也就是图像融合
# 注意,img1*0.5后就是float,不会截断成uint8,即img为float img = cv,add(img1*0.5, img2*0.5) # 最有一个参数 offset,得到uint8的img img = cv.addWeighted(img1, 0.5, img2, 0.5, 0)
-
图片相减
如果图片本身是由两张合成的,那么相减考虑权重
考虑是否将减后的图像 *2
考虑类型是否转成uint8(截断)img = np.uint8((img_inter*1.0 - img1*0.5) * 2)
opencv的
考虑两个矩阵的数据类型要一致,img_inter*1.0
得到的img
是float64
-
- 在
cv.imshow()
动作前最好np.uint8()
类型转换下
- 在
-
- 抑或不转成
uint8
,使用自定义的show()
函数
- 抑或不转成
img = cv.subtract(img_inter*1.0, img1*0.5) # 会得到符合肉眼期望的效果 img = np.uint8(img) cv.imshow(img) # 抑或直接使用自定义的 show() 亦能将 float64 的矩阵图像正常显示 show(img) # 直接show可能会造成二值化 cv.imshow(img)
-
-
矩阵中找出
0
元素,并修改它的值
找出0元素,并修改为1, 非0元素不动obj = np.where(obj == 0, 1, obj)
0元素不能做被除数, 把它改为1
意为两个矩阵相比,取数最大的noise = np.maximum(noise, 1)
元素为0的改动为1
obj[obj == 0] = 1
-
图像乘法,掩膜矩阵最好是 0 - 1 之间的元素
掩膜矩阵不是 0 - 1 的可以先转成 0 - 1mask = mask / 255 # 乘以掩码,再放大*255,,再把类型转成uint8 img = np.uint8((img * mask)*255)
-
掩码矩阵(元素在 0 - 1中间)取补
mask = 1 - mask
-
图片的除法
因为该方法会将除数做处理,0元素会被改为非0值
因此,即使两张图片一模一样,计算出的结果也不会全是1img = cv.divide(noise, obj)
注意,参与运算的几个数都是
uint8
的话,结果默认也是uint8
,结果在处理溢出是按循环,非截断的方式 -
np
数据的转换img.astype(np.int32)
-
数据截断
np.clip(img, 0, 255)
-
opencv像素值的变换
- 线性缩放,倍率 alpha=2,偏移 beta=20
img = cv.convertScaleAbs(gray, alpha=2, beta=20)
- 对数缩放
img = np.log(gray.astype(np.float32)+1) / b
- gama缩放,首先把矩阵缩放到 0~1 之间
# 系数(倍率)c = 1, gama = 0.5 gama = 0.5 img = np.power(img, gama) * 255
- 线性缩放,倍率 alpha=2,偏移 beta=20
-
图像截断
先截断行,后列, 然后是通道数,选取全部通道img_cut = img[150:500, 50:300, :]
-
图像的仿射变换
-
先准备变换矩阵
-
矩阵写法
M = np.array([[1, 0, 50], [0, 1, 100]],dtype=np.float32)
-
仿射变换
指定输出图像尺寸
注意,和Mat::resize(x, y)类似,指定x 轴宽,y 轴宽,而不是 rows, cols
注意, rows, cols = obj.shape记住这么返回的
要想图像能被完整显示,不被截断rows, cols = obj.shape img = cv.warpAffine(obj, M, (cols + 50, rows + 100))
opencv 提供的翻转函数
# 水平翻转 img1 = cv.flip(obj, 1) # 垂直翻转 img2 = cv.flip(obj, 0) # 水平+垂直翻转 img3 = cv.flip(obj, -1)
按矩阵旋转,因为矩阵就说明了旋转的原心,即左上角
rows, cols = obj.shape # 旋转矩阵 beta = np.pi / 4 M = np.array([[np.sin(beta), np.cos(beta), 0], [-np.cos(beta), np.sin(beta), 0]],dtype=np.float32) img = cv.warpAffine(obj, M, (cols, rows)) show(np.hstack([obj, img]))
opencv 旋转函数,只能按n * 90°
img = cv.rotate(obj, cv.ROTATE_90_CLOCKWISE)
-
透视变换
# 获取透视变换矩阵 src = np.array([[190, 120], [590, 310], [650, 480], [140, 420]], dtype = np.float32) dst = np.array([[20, 20], [600, 20], [600, 500], [20, 500]], dtype = np.float32) M = cv.getPerspectiveTransform(src, dst) # 透视变换 img = cv.warpPerspective(obj, M, (cols, rows)) show(img)
-
-
图像线性放缩
# 线性 img = cv.resize(obj, (800, 800), interpolation=cv.INTER_LINEAR) # 双线性 img1 = cv.resize(obj, (800, 800), interpolation=cv.INTER_LINEAR_EXACT) show(np.hstack([img, img1]))
滤波
- 卷积实现
比如 3 * 3 的均值卷积size = 3 kernel = np.ones((size, size)) kernel /= (size*size) dst = cv.filter2D(obj, -1, kernel)
- opencv 均值滤波
或者dst = cv.blur(src, (3, 3))
dst = cv.boxFilter(obj, cv.CV_8UC1, (3,3))
- 中位值滤波
# 参数二是 3 5 7 9 这些奇数值 dst = cv.medianBlur(obj, 9)
- 高斯滤波(高斯卷积核)
方差越小,数据越集中, 越大越平坦
方差过小达不到滤波效果,方差过大等效于均值滤波sigmaX = 0.5 dst = cv.GaussianBlur(obj, (9, 9), sigmaX) show(dst)
- 双边滤波
要求既实现滤波又能保留高频的边缘信息
卷积核特征:卷积核权值不固定,两个卷积核共同决定
空间距离卷积核:离目标越远权重越小
灰度差异卷积核:差异越大权重越小
两个卷积核逐元素相乘# 参数二:,自动卷积核大小, 后面分别是 颜色卷积核方差, 空间距离卷积核方差 dst = cv.bilateralFilter(obj, -1, sigmaColor=50, sigmaSpace=3)
锐化
-
premitt 算子
注意, 指定类型 CV_64F
后续处理,
梯度是有+-的,可以取绝对值
为方便显示,会截断到(0~255)
梯度合成
kx = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype = np.float32) ky = np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]], dtype = np.float32) # 注意, 指定类型 CV_64F img_x = cv.filter2D(img, cv.CV_64F, kx) img_y = cv.filter2D(img, cv.CV_64F, ky) img_x = np.abs(img_x).clip(0, 255) img_y = np.abs(img_y).clip(0, 255) img_xy = np.sqrt(img_x**2+img_y**2) img_xy2 = np.abs(img_x) + np.abs(img_y)
-
Sobel
算子(换成Scharr
函数一样的)
# 参数3: x 方向偏导系数 img_x = cv.Sobel(img, cv.CV_16S, 1, 0) # 参数4: y 方向偏导系数 img_y = cv.Sobel(img, cv.CV_16S, 0, 1) # 合成梯度 img_xy = np.abs(img_x) + np.abs(img_y)
-
拉氏算子,会产生双边缘,应用较少
img_lap = cv.Laplacian(img, cv.CV_64F) # 可视化处理 np.abs(img_lap)
-
LoG
算子img_blur = cv.GaussianBlur(img, (3, 3), 1) img_log = cv.Laplacian(img_blur, cv.CV_64F)
-
Canny
算子推荐
# 20 200 阈值 img_edge = cv.Canny(img, 20, 200)
-
阈值分割
img = cv.imread("./1.bmp", 0) # plt.hist(img.ravel(), 256, [0, 256]) plt.hist(img.flatten(), np.arange(-0.5, 256, 1), color='g') plt.show() # 阈值分割 _, img_bin = cv.threshold(img, 250, 255, cv.THRESH_BINARY) show(img)
一个不是很好的光斑检测思路
img_edge = cv.Canny(img, 200, 250) sigmaX = 0.6 img_edge = cv.GaussianBlur(img_edge, (3, 3), sigmaX) _, img_edge = cv.threshold(img_edge, 10, 255, cv.THRESH_BINARY)
光斑检测方案二
sigmaX = 2 img = cv.GaussianBlur(img, (9, 9), sigmaX) _, img = cv.threshold(img, 100, 255, cv.THRESH_BINARY) img_edge = cv.Canny(img, 200, 250)
-
自适应阈值分割
# 参二: 阈值目标 # 参三: 适应阈值取得的算法, 均值或高斯加权 # 参四: 二值化 # 参五: 局部矩阵大小 # 参六: 偏移值 img_adpt = cv.adaptive(img, 255, cv.ADAPTIVE_MEAN_C, cv.THRESH_BINARY, 25, 0)
形态学
-
腐蚀:消除毛刺和小区域(噪声被腐蚀掉,同时线条变细)
- 分步进行
K1 = np.ones((2,3), np.uint8) img_erode2 = cv.morphologyEx(img, cv.MORPH_ERODE, K1)
- opencv
I = cv.imread('pic/bird1000x800.jpg', 0) K = np.ones((11,11), dtype=np.int) img_e = cv.erode(I, K)
- 分步进行
-
膨胀:连接边缘,可能扩大噪声
- 分步
img = cv.imread('pic/ninety_bin50x50.png', -1) K = cv.getStructuringElement(cv.MORPH_RECT, (3,1)) img_dilate2 = cv.morphologyEx(img, cv.MORPH_DILATE, K)
- opencv
img_dilate = cv.dilate(img, K) show(np.hstack([img, img_dilate, img_dilate2]))
注意,元矩阵(元素为1)的写法
# 推荐 K = np.ones((3, 3), dtype=np.int) # 要求掌握 array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=uint8)
注意,以二值的形式读入图片,参数二为 -1
img_bin = cv.imread(\"pic/ninety_bin50x50.png\", -1)
- 分步
-
取得一个矩形的结构元
kernel = cv.getStructuringElement(cv.MORPH_RECT, (3,3))
-
获取椭圆的结构元
K2 = cv.getStructuringElement(cv.MORPH_ELLIPSE, (7,7))
-
开运算 既想消除噪声又想保持线条的粗细
开运算: 腐蚀 --> 膨胀- 分步
K = cv.getStructuringElement(cv.MORPH_RECT, (3,3)) img_open = cv.dilate(cv.erode(img, K), K)
- opencv
img_open2 = cv.morphologyEx(img, cv.MORPH_OPEN, K) show(np.hstack([img, img_open, img_open2]))
-
闭运算 线条上有孔洞,填充孔洞,同时保持线条粗细
闭运算: 膨胀 --> 腐蚀- 分步
K = cv.getStructuringElement(cv.MORPH_RECT, (3,3)) img_close = cv.erode(cv.dilate(img, K), K)
- opencv
img_close1 = cv.morphologyEx(img, cv.MORPH_CLOSE, K) show(np.hstack([img, img_close, img_close1]))
-
形态学梯度,简单认为就是 膨胀 - 腐蚀
相减,推荐
cv.subtract
超过存储空间的自动截断,不会循环到高bit
位img = cv.imread('pic/flower_bin500x500.png', -1) K = np.ones((3,3), np.uint8) img_grad1 = cv.morphologyEx(img, cv.MORPH_GRADIENT, K) img_grad2 = cv.subtract(cv.dilate(img, K), img) show(np.hstack([img, img_grad1, img_grad2]))
-
顶帽与底帽 二值图的顶帽与底帽没有意义
顶帽(效果类似局部阈值)- 顶帽中开运算的意义:获取背景
原图 - 开运算(背景) = 突出的图 - 分步
I = cv.imread('pic/page760x900.jpg', 0)\n", Ic = 255 - I K = np.ones((21, 21), np.uint8) # 开运算 获取背景 Ic_open = cv.morphologyEx(Ic, cv.MORPH_OPEN, K) Ic_tophat = cv.subtract(Ic, Ic_open) _, Ic_bin = cv.threshold(Ic_tophat, 25, 255, 0) I_bin = 255 - Ic_bin show(I_bin)
- opencv
I = cv.imread('pic/page760x900.jpg', 0) Ic = 255 - I K = np.ones((21, 21), np.uint8) Ic_tophat = cv.morphologyEx(Ic, cv.MORPH_TOPHAT, K) _, Ic_bin = cv.threshold(Ic_tophat, 25, 255, 0) I_bin = 255 - Ic_bin show(I_bin)
- 顶帽中开运算的意义:获取背景
-
底帽与顶帽是对偶操作,用底帽处理上个顶帽例子可以第一步不进行颜色反转的操作
- 底帽
I = cv.imread('pic/page760x900.jpg', 0) K = np.ones((21, 21), np.uint8) I_blackhat = cv.morphologyEx(I, cv.MORPH_BLACKHAT, K) _, Ic_bin = cv.threshold(I_blackhat, 25, 255, 0) I_bin = 255 - Ic_bin
- 底帽
-
击中击不中
代码很少,过程重要
用白色的结构元来腐蚀相当于与运算
注意:
卷积的获取
两矩阵求交,逐cv.bitwise_and()
I = cv.imread('pic/rectangle_find35.png', -1) K = np.zeros((37,37), np.uint8) # 卷积的获取 K[1:36, 1:36] = 1 IeK = cv.erode(I, K) Ic = 255 - I Kc = 1 - K IceKc = cv.erode(Ic, Kc) hitmiss = cv.bitwise_and(IeK, IceKc) show(hitmiss)
-
查找某像素值的坐标
可能得到 x y 是一个数组x, y = np.where(hitmiss == 255)
-
下/上采样
图像金字塔:多分辨率解释图像的有效结构
img = cv.imread('pic/apple.jpg') img_down = cv.pyrDown(img) img_up = cv.pyrUp(img) show(img_down) show(img_up)
-
图像融合,拉氏金字塔的应用
拉氏金字塔
S0 = cv.imread('pic/apple.jpg') S1 = cv.pyrDown(S0) L0 = cv.subtract(S0, cv.pyrUp(S1)) S2 = cv.pyrDown(S1) L1 = cv.subtract(S1, cv.pyrUp(S2))
图像融合
注意学习
for
循环的写法
连续下采样的写法apple = cv.imread('pic/apple.jpg') orange = cv.imread('pic/orange.jpg') A = [apple] B = [orange] LA, LB, L = [], [], [] n = 5 for i in range(1, n+1): A.append(cv.pyrDown(A[-1])) B.append(cv.pyrDown(B[-1])) for i in range(n): LA.append(cv.subtract(A[i], cv.pyrUp(A[i+1]))) LB.append(cv.subtract(B[i], cv.pyrUp(B[i+1]))) LA.append(A[n]) LB.append(B[n]) for la, lb in zip(LA, LB): h, w, c = la.shape L.append(np.hstack([la[:, :w//2], lb[:, w//2:]])) B = L[n] for i in range(n, 0, -1) B = cv.add(cv.pyrUp(B), L[i-1]) show(B)
-
几何绘图
- 直线
cv.line(img, (50, 25), (300, 175), (255, 0, 0), 2)
- 矩形
# 左上角,右下角 cv.rectangle(img, (50, 25), (300, 175), (255, 255, 0))
- 圆
cv.circle(img, (150, 100), 80, (255, 0, 255), 2)
- 椭圆
box = ((150, 100), (100, 50), 30) cv.ellipse(img, box, (0, 255, 255))
- 多边形
pts = np.array([ [[100, 50]], [[250, 50]], [[200, 150]] ]) cv.polylines(img, [pts, pts+20], True, (255, 0, 125), 2)
- 文字
cv.putText(img, \"Lion\", (25,25), cv.FONT_HERSHEY_COMPLEX, 1.0, (125, 255, 125))
- 直线
-
轮廓绘制
img = cv.imread('pic/contour_bin.png', -1) contours, hierarchy = cv.findContours(img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_NONE) bgr = cv.merge([img, img, img]) cv.drawContours(bgr, contours, 1, (255, 255, 0), 3) show(bgr) # 只取通道2 contours[2].shape
-
轮廓提取
img = cv.imread('pic/contours2_bin.png', -1) contours, hierarchy = cv.findContours(img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE) bgr = cv.merge([img, img, img]) cv.drawContours(bgr, contours, -1, (255, 255, 0), 3) show(bgr) array([[[ 5, -1, 1, -1], [ 2, -1, -1, 0], [-1, 1, 3, 0], [ 4, -1, -1, 2], [-1, 3, -1, 2], [ 6, 0, -1, -1], [-1, 5, -1, -1]]], dtype=int32)
-
提取图片轮廓并添加 alph 通道
img = cv.imread('pic/goldfish500x500.jpg') b, g, r = cv.split(img) edge1 = cv.Canny(r, 5, 50) K = cv.getStructuringElement(cv.MORPH_RECT, (3,3)) edge2 = cv.morphologyEx(edge1, cv.MORPH_CLOSE, K, iterations=3) show(edge2) cnts, hiers = cv.findContours(edge2, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) mask = np.zeros((500, 500), np.uint8) cv.drawContours(mask, cnts, 0, 255, -1) show(mask) goldfish = np.zeros((500, 500, 4), np.uint8) goldfish[:, :, :3] = img goldfish[:, :, 3] = mask show(goldfish) cv.imwrite('test/goldfish.png', goldfish)