个人学习用,侵删
来源:各种帖子、视频课
文章目录
OpenCV
【黑马程序员人工智能教程_10小时学会图像处理OpenCV入门教程-哔哩哔哩】
OpenCV简介
图像是什么: 图是物体反射或投射光的分布,像是人的视觉系统所接受的图在人脑中形成的印象、认识
模拟图像: 通过某种物理量的(光、电)的强弱变化来记录图像亮度信息。易受到干扰,不够清晰
数字图像: 亮度值用离散数值表示
8位图像(人眼对灰度的敏感度在16~32位之间)
灰度值 0~255, 0最黑255最白(理解为反射能量多少)
分类: 二值图像(0/1)、灰度图(256级灰度)、彩色图(rgb,8位uintt)
基本操作
- 读取图像
cv.imread()
# cv.IMREAD_COLOR 默认参数;cv.IMREAD_GRAYSCALE 灰度图;cv.IMREAD_UNCHANGED 包括alpha通道的加载图像模式
# 路径错误不会报错!!返回None
- 显示图像
cv.imshow("窗口名称", 要显示的图像 )
cv.waitKey(0) # 参数0永远等待
也可用matplotlib展示
OpenCV存储用 b g r , plt是b g r,需要翻转
- 保存图像
cv.imwrite("文件名、保存路径", img)
- 绘制直线、圆、矩形、 文字
- 获取/修改像素点
px = img[100,100] # 返回三元组
blue = img[100,100,0] # 仅获取B通道值
img[100,100] = [0,0,255] # 改色
- 获取图像属性
img.shape # 形状 (200,200,3)200*200,3通道
img.dtype #数据类型
img.size # 像素数
- 通道拆分
b, g, r = cv.split( img ) # 拆分
img = cv2.merge(b, g, r ) # 合并
- 色彩空间改变
gray = cv2.cvtColor( img, cv.COLOR_BGR2GRAY) # 转Gray
hsv = cv2.cvtColor( img, cv.COLOR_BGR2HSV) # 转HSV(色相、饱和度、亮度)
- 图像的加法
# 能加的前提是像素一样
cv.add( img1, img2) # cv2 是饱和操作(超过255只取255)
# numpy是取模操作(超过255取余255)
- 图像混合
cv2.addWeighted( img1, 0.7, img2, 0.3, 0) # 0.7*img + 0.3*img2 + 0
# 同样要求像素相同
几何变换
- 缩放
cv2.resize( src, dsize, fx=0, fy=0, interpolation=cv2.INTER_LINEAR)
# 输入图像,
# 绝对尺寸(指定大小),
# 相对尺寸(dsize设为None,然后将fx和fy设置为比例因子(缩放倍数))
# interpolation插值方法:cv2.INTER_LINEAR双线性插值法 / cv2.INTER_NEAREST最近邻插值 / cv2.INTER_AREA像素区域重采样(默认) / cv2.INTER_CUBIC双三次插值
- == TODO:这几种插值法都是什么原理?==
- 平移
cv2.warpAffine( img, M, dsize )
# M 2×3矩阵 np.float32类型的numpy数组
# 1 0 tx
# 0 1 ty
# 平移方向、平移距离 (原点在左上角,右下是正方向)
# dsize 平移后输出图像的大小
- 旋转
- 要修改每个像素点的坐标:先根据旋转角度和旋转中心获取旋转矩阵
- 修正原点坐标:根据旋转矩阵进行变换
M = cv2.getRotationMatrix(center, angle, scale)
# 生成旋转矩阵:旋转中心坐标、旋转角度(而非弧度)、缩放比例
result = cv2.warpAffine( img, M, (w,h) )
# 进行旋转变换
- 仿射变换
平行的还平行,相连的仍相连。别的不一定(角度)
说一千道一万,就是原来的每个点都×一个变换矩阵
仿射变换矩阵 M = [A B],A是2×2的线性变换矩阵(改变形状),B是2×1的平移矩阵(改变位置)
M [ x y 1 ] = [ A B ] × [ x y 1 ] = A [ x y ] + B M\begin{bmatrix}x\\y\\1\end{bmatrix} =\begin{bmatrix}A&B\end{bmatrix}\times\begin{bmatrix} x\\y\\1\end{bmatrix}= A\begin{bmatrix} x\\y \end{bmatrix}+ B M xy1 =[AB]× xy1 =A[xy]+B
# 仿射变换
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread("scene.jpg")
h, w, ch = img.shape
pts_before = np.float32([[50,50],[200,50],[50,200]])
pts_after = np.float32([[100,100],[200,50],[100,250]])
# 需要三对仿射变换前后的点的坐标
M = cv2.getAffineTransform(pts_before, pts_after)
# 生成仿射变换矩阵M
rst = cv2.warpAffine( img, M, (w,h))
# 完成仿射变换
cv2.imshow("img",img)
cv2.imshow("rst",rst)
cv2.waitKey(0)
- 透射变换
就是投影
投影中心、原图像平面、新图像平面
通用变换公式:
[
x
′
y
′
z
′
]
=
[
u
v
w
]
[
a
00
a
01
a
02
a
10
a
11
a
12
a
20
a
21
a
22
]
\left[\begin{array}{lll} x^{\prime} & y^{\prime} & z^{\prime} \end{array}\right]=\left[\begin{array}{lll} u & v & w \end{array}\right]\left[\begin{array}{lll} a_{00} & a_{01} & a_{02} \\ a_{10} & a_{11} & a_{12} \\ a_{20} & a_{21} & a_{22} \end{array}\right]
[x′y′z′]=[uvw]
a00a10a20a01a11a21a02a12a22
其中, (u, v) 是原始的图像像素坐标, w 取值为1,
(
x
=
x
′
/
z
′
,
y
=
y
′
/
z
′
)
\left(x=x^{\prime} / z^{\prime}, y=y^{\prime} / z^{\prime}\right)
(x=x′/z′,y=y′/z′)
是透射变换后的结果。后面的矩阵称为透视变换矩阵,一般情况下,我们将其分为三部分:
T
=
[
a
00
a
01
a
02
a
10
a
11
a
12
a
20
a
21
a
22
]
=
[
T
1
T
2
T
3
a
22
]
T=\left[\begin{array}{lll} a_{00} & a_{01} & a_{02} \\ a_{10} & a_{11} & a_{12} \\ a_{20} & a_{21} & a_{22} \end{array}\right]=\left[\begin{array}{ll} T 1 & T 2 \\ T 3 & a_{22} \end{array}\right]
T=
a00a10a20a01a11a21a02a12a22
=[T1T3T2a22]
其中:T1表示对图像进行线性变换,T2对图像进行平移,T3表示对图像进行投射变换, a_{22} 一般设为1.
在opencv中找到4个点(任意3个不共线),通过cv.getPerspectiveTransform获取变换矩阵T, 将cv.warpPerspective应用于此3*3矩阵
import numpy as np
import cv2 as cv
# 1 read the image
img = cv.imread("")
# 2 透射变换
rows, cols = img.shape[:2]
# 透射前 点的坐标
pts1 = np.float32([[56,65],[368,52],[28,387],[389,390]])
# 透射后
pts2 = np.float32([[100,145],[300,100],[80,290],[310,300]]) # 透射后
T = cv.getPerspectiveTransform(pts1,pts2)
dst = cv.warpPerspective(img,T,(cols,rows))
# 3 图像显示
- 图像金字塔
好像就是图像分辨率的提升/降低
# 上采样 就是提升分辨率
up_img = cv.pyrUp(img)
# 下采样 就是降低分辨率
down_img = cv.pyrDown(img)
形态学
连通性
连通性:位置相邻,灰度值满足特定的相似性准则
4 邻接: 上下左右, N4§ -> 4连通
D邻接:4个对角, ND§
8 邻接:全8个, N8§ -> 8连通
m连通:要么4邻接,要么D邻接且二者4领域不满足相似性准则
形态学操作
- 腐蚀:
蚕食高亮部分
消除物体边界点,使目标缩小,可以消除小于结构元素的噪声点。
用结构元素与图像做匹配(与),匹配成功的中心设为高亮 - 膨胀:
扩张高亮部分
同上(或操作,模版/原图有一处是高亮则高亮)
# 1 读取图像
# 2 创建5*5全1的核结构
kernel = np.ones((5,5), np.uint8)
# 3 图像腐蚀和膨胀
erosion = cv.erode(img, kernel)
dilate = cv.dilate(img, kernel)
# 可以在这两个函数后面再传入一个参数iterations表示腐膨几轮
- 开闭运算
将膨胀和腐蚀按照一定的次序进行处理
此二者不可逆(先开后闭不能得到原图像)- 开运算:先腐蚀后膨胀,能消除噪点,去除小干扰快
- 闭运算:先膨胀后腐蚀,消除物体里面的空洞,填充闭合区域
cv.morphologyEx(img, op, kernel)
# op:处理方式,开cv.MORPH_OPEN,闭cv.MORPH_CLOSE
# kernel 核结构
- 礼帽和黑帽
- 礼帽:原图 - 开运算结果
分离外部亮点 - 黑帽:闭运算结果 - 原图
分离内部暗点
- 礼帽:原图 - 开运算结果
# 还是上面开闭运算的函数,op取
# cv.MORPH_TOPHAT:礼帽
# cv.MORPH_BLACKHAT:黑帽
图像噪声
- 椒盐噪声(脉冲噪声):随机出现的白点or黑点。可能是影像讯号收到突然的干扰,如数位转换器or位元传输错误等(失效的感应器导致像素值为最小,饱和的感应器导致像素值为最大)。
- 高斯噪声:噪声密度函数(各种灰度值的概率)服从高斯分布(正态分布)。
图像平滑
去除高频信息,保留低频信息
- 均值滤波
其实就是用一个区域里点值的平均代替其中心点的值
优点:算法简单计算快
缺点:丢失细节
# cv.blur(src, ksize, anchor, borderType)
# src: 输入图像
# ksize:卷积核的大小
# anchor:默认(-1,-1)表示核中心
# borderType:边界类型
# 1 输入图像
img = cv.imread("")
# 2 均值滤波
blur = cv.blur(img, (5,5))
# 3 图像显示,略
- 高斯滤波
理论有点复杂,没太明白,先学会用吧
均值滤波算术平均,高斯滤波加权平均(重点在权重矩阵怎么算)
- TODO: 这块算法流程都没搞明白
# cv2.GaussianBlur(src, ksize, sigmax, sigmay, borderType)
# ksize 高斯卷积核的大小,长宽均为奇数,可不同
# sigmaX/sigmaY:两个方向上的标准差。Y方向上默认为0,表示与sigmaX相同
# borderType:填充边界类型
- 中值滤波
中值滤波是一种典型的非线性滤波技术,基本思想是用像素点领域灰度值的中值来代替该点的灰度值。
对椒盐噪声尤其有用,因其不受领域内极大极小值的干扰
# cv.medianBlur(src, ksize)
直方图
灰度直方图
dims:需要统计的特征数目(灰度)
bins:子区段的数目(组数)
range:要统计特征的取值范围
# cv.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])
# images,原图像 传入函数时用[]括起来 [img]
# channels,灰度图[0] 彩色图BGR对应[0][1][2]
# mask,掩模图像 用于设置要统计的区域
# histSize,bins 也要用[]
# range, [0,255]
histr = cv.calcHist( [img], [0], None, [256], [0,256])
掩膜
类似掩码,要得到的置1
# 创建蒙版
mask = np.zeros(img.shape[:2], np.uint8) # 用图像的长宽
mask[100:200, 300:400] = 255 # =1也可
# 掩膜
masked_img = cv.bitwise_and(img, img, mask=mask)
# 做直方图
mask_histr = cv.calcHist([img],[0],mask,[256],[0,256])
均衡化
把小范围集中的分散
像素分布广-> 对比度高
在曝光不足or过度的图像中可以更好地突出细节
dst = cv.equalizeHist(img)
自适应均衡化
把整幅图分成很多tiles(opencv默认每个tile大小8*8),然后分别均衡化再用双线性差值拼接。
具体算法复杂,暂且知道what就行。
- TODO: 自适应均衡化具体怎么做的?
# cv.createCLAHE(clipLimit, tileGridSize)
# clipLimit 对比度限制,默认40
# tileGridSize 分块的大小,默认8*8
# 创建自适应均衡化的对象
clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
# 应用于图像
cl1 = clahe.apply(img)
边缘检测
- 基于搜索:通过寻找图像一阶导数最大值来检测边界,然后利用计算结果估计边缘的局部方向,通常采用梯度的方向,并利用此方向找到局部梯度模的最大值。代表算法:Sobel算子、Scharr算子
- 基于零穿越:寻找二阶导数变号零点(即图像拐点)。待变算法:Laplacian算子
Sobel检测算子
离散函数的一阶导数用下代替:
f
′
(
x
)
=
f
(
x
+
1
)
−
f
(
x
−
1
)
2
f'(x) =\frac{f(x+1)-f(x-1)}{2}
f′(x)=2f(x+1)−f(x−1)
对要处理的图像在两个方向上求导
水平变化:将图像与奇数大小的模板进行卷积,结果为Gx
G x = [ − 1 0 1 − 2 0 2 − 1 0 1 ] ∗ I G_x=\begin{bmatrix} -1 & 0 & 1\\ -2 & 0 & 2\\ -1 & 0 & 1 \end{bmatrix} * I Gx= −1−2−1000121 ∗I
垂直变化同理
G y = [ − 1 − 2 − 1 0 0 0 1 2 1 ] ∗ I G_y=\begin{bmatrix} -1 & -2 & -1\\ 0 & 0 & 0\\ 1 & 2 & 1 \end{bmatrix} * I Gy= −101−202−101 ∗I
结合以上两个结果求出
G = G x 2 + G y 2 G=\sqrt{G_x^{2} + G_y^{2} } G=Gx2+Gy2
统计极大值所在的位置,就是图像的边缘
当内核大小为3时,Sobel内核可能产生比较明显的误差。为解决此问题,采用Scharr函数,与Sobel一样块,结果更精准。但该函数仅作用于大小为3的内核。
其计算方法为:
G x = [ − 3 0 3 − 10 0 10 − 3 0 3 ] ∗ I G_x = \begin{bmatrix} -3 & 0 & 3\\ -10 & 0 & 10\\ -3 & 0 & 3 \end{bmatrix} * I Gx= −3−10−30003103 ∗IG y = [ − 3 − 10 − 3 0 0 0 3 10 3 ] ∗ I G_y = \begin{bmatrix} -3 & -10 & -3\\ 0 & 0 & 0\\ 3 & 10 & 3 \end{bmatrix} * I Gy= −303−10010−303 ∗I
- TODO: 为什么他俩卷积就是导数了?
- TODO:这个核是怎么算出来的?
# Sobel边缘检测
# cv.Sobel( src, ddepth, dx, dy, dst, ksize, scale, delta, borderType )
# src 传入图像
# ddepth 图像的深度(?)
# dx、dy 求导的阶数,0表示在这个方向上没有求导
# ksize 卷积核Sobel算子的大小,必须为奇数1357,默认3。如果ksize=-1自动变为3就是利用Scharr算子
# scale 缩放系数,默认无缩放
# borderType 图像边界的模式,默认cv2.BORDER_DEFAULT
# Sobel函数求导后有负值/大于255的值,要先使用16位有符号整型,cv2.CV_16S。处理完后再使用cv2.convertScaleAbs()函数将其转回原来的uint8格式。Sobel算子是在两个方向上计算的,最后还要用cv2.addWeighted()将图像混合
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread("", cv.IMREAD_GRAYSCALE)
# 计算Sobel卷积结果
x = cv.Sobel(img, cv.CV_16S, 1, 0)
y = cv.Sobel(img, cv.CV_16S, 0, 1)
# 数据转换
Scale_absX = cv.convertScaleAbs(x)
Scale_absY = cv.convertScaleAbs(y)
# 结果混合
result = cv.addWeighted(Scale_absX, 0.5, Scale_absY, 0.5, 0)
# 图像显示
plt.figure(figsize=(10,8), dpi=100)
plt.subplot(121),
plt.imshow(img, cmap=plt.cm.gray)
plt.title('原图')
plt.xticks([])
plt.yticks([])
plt.subplot(122),
plt.imshow(result, cmap=plt.cm.gray)
plt.title('Sobel滤波后结果')
plt.xticks([])
plt.yticks([])
plt.show()
Laplacian算子
利用二阶导数来检测边缘:
Δ
s
r
c
=
∂
2
s
r
c
∂
x
2
+
∂
2
s
r
c
∂
y
2
\Delta src= \frac{\partial^2src}{\partial x^{2}}+\frac{\partial^2src}{\partial y^{2}}
Δsrc=∂x2∂2src+∂y2∂2src
对于不连续函数的二阶导数:
f
′
′
(
x
)
=
f
′
(
x
+
1
)
−
f
′
(
x
)
=
f
(
x
+
1
)
+
f
(
x
−
1
)
−
2
f
(
x
)
f''(x)=f'(x+1)-f'(x)=f(x+1)+f(x-1)-2f(x)
f′′(x)=f′(x+1)−f′(x)=f(x+1)+f(x−1)−2f(x)
使用的卷积核:
k
e
r
n
e
l
=
[
0
1
0
1
−
4
1
0
1
0
]
kernel=\begin{bmatrix} 0 & 1 &0 \\ 1 & -4 & 1\\ 0 & 1 &0 \end{bmatrix}
kernel=
0101−41010
- TODO:依然不懂卷积核这块
# laplacian = cv2.Laplacian( src, ddepth, ksize)
# ddepth:图像的深度,-1表示采用原图像相同的深度。目标图像必须大于等于原图像的深度
# ksize:卷积核大小,奇数1357
result = cv.Laplacian(img, cv.CV_16S)
Scale_abs = cv.convertScaleAbs(result)
Canny边缘检测
被认为是最优的边缘检测算法
-
Step1:噪声去除。使用5*5高斯滤波器去除噪声
-
Step2:计算图像梯度。利用Sobel算子计算水平/竖直方向的一阶导数,再找到边界的梯度和方向(高数内容)
G r a d i e n t ( G ) = G x 2 + G y 2 Gradient(G) = \sqrt{G_x^2+G_y^2} Gradient(G)=Gx2+Gy2A n g l e ( θ ) = a r c t a n ( G y G x ) Angle(θ)=arctan(\frac{G_y}{G_x}) Angle(θ)=arctan(GxGy)
如果某个像素点是边缘,则其梯度方向(垂直、水平、2个对角线)总与边缘垂直。
-
Step3:非极大值抑制。获得梯度的方向和大小之后,对整幅图进行扫描,去除非边界上的点,对每一个像素进行检查,判断该点是否为周围具有相同梯度方向中的点中最大的。
- TODO:这一步没懂
-
Step4:滞后阈值。当图像的灰度值>maxVal时认定为边界,<minVal时被抛弃,介于二者之间查看其是否与某个被确定为边界的点相连。
# canny = cv2.Canny(image, threshold1, threshold2)
# threshold: minVal和maxVal
模板匹配
将模板从上往下、从左至右移动,每次移动一个像素,计算模板和原图对应区域的相似程度。
# result = cv2.matchTemplate(img, template, method)
# method:
# 1. CV_TM_SQDIFF 平方差匹配:计算模板与图像之间的平方差,最好为0,值越大匹配度越差
# 2. CV_TM_CCORR 相关匹配:利用模板与图像间的乘法进行匹配,数值越大匹配程度越高
# 3. CV_TM_CCOEFF 相关系数匹配:1表示完美匹配,-1表示最差匹配
# 完成匹配后,使用cv2.minMaxLoc()方法查找最大/小值所在位置即为最佳匹配位置
模板匹配不适用于尺度变换、视角变换、旋转变化、光照变化之后的图像。此时要使用关键点匹配算法(如SIFT、SURF),先通过关键点检测算法获取模板和测试图片中的关键点,然后使用关键点匹配算法处理。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('ori.jpg')
temp = cv.imread('temp.jpg')
h,w = temp.shape[:2]
# 相关系数匹配,1最好,-1最差
res = cv.matchTemplate(img, temp, cv.TM_CCOEFF)
# 最大最小值及其位置
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
top_left = max_loc
bottom_right = (top_left[0]+w, top_left[1]+h)
cv.rectangle(img, top_left, bottom_right, (255,0,255), 2)
plt.imshow(img[:,:,::-1]) # 实现左右翻转(bgr变成rgb)
plt.title('Template Matching')
plt.xticks([])
plt.yticks([])
plt.show()
霍夫变换
提取图像中的直线&圆
https://www.cnblogs.com/php-rearch/p/6760683.html
笛卡尔坐标系的点成线 -> 霍夫坐标系的线交点 -> 极坐标系的线交点
在极坐标系中表示直线的方程为
ρ
=
x
c
o
s
θ
+
y
s
i
n
θ
\rho = xcos\theta +ysin\theta
ρ=xcosθ+ysinθ
遍历图中的点(x,y),每次linguisti令θ从0变化到180度,求对应ρ。最终出现次数最多的ρθ组合就可求出对应直线方程
霍夫线检测
边缘检测获取一些点 -> 每个点都对应一条ρ、θ的曲线(ρ=xcosθ+ysinθ) -> ρ、θ坐标系里图像的每个交点都对应笛卡尔坐标系里的一条直线
# cv.HoughLines(img, rho, theta, threshold)
# img
# rho\theta:ρ和θ的精确度
# threshold:阈值,只有累加器中的值高于此,才能被认为是直线
import numpy as np
import random
import cv2 as cv
import matplotlib.pyplot as plt
img = cv.imread("")
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 边缘检测
edges = cv.Canny(gray, 50, 150)
# 霍夫直线检测
lines = cv.HoughLines(edges,, 0.8, np.pi/180, 150)
# 图像显示(极坐标转笛卡尔坐标)
for line in lines:
rho, theta = line[0]
a = np.cos(theta)
b = np.sin(theta)
x0 = a * rho
y0 = b * rho
x1 = int(x0 + 1000*(-b))
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv.line(img, (x1,y1), (x2,y2), (0,255,0))
plt.figure(figsize=(10,8),dpi=100)
plt.imshow(img[:,:,::-1])
plt.title("Hough Lines")
plt.xticks([])
plt.yticks([])
plt.show()
- TODO:画图坐标x1y1x2y2这里是对的吗?跑一下验证
霍夫圆检测
OpenCV-Python教程:霍夫变换~圆形(HoughCircles) – 桔子code (juzicode.com)
太复杂了(捂脸)
标准霍夫圆检测法是在(r,a,b)组成的三维空间累加器上进行,效率低(直线是ρθ二维)。
opencv使用霍夫梯度法进行圆形检测
- step1:检测圆心。圆心是法线的交汇处,设置一个阈值,在某点相交的直线条数大于这个阈值就认为该交点处为圆心
- step2:利用圆心推导出半径。圆心到圆周上各点距离是相同的,设置一个阈值,只要相同距离的数量大于该阈值,就认为该距离是该圆心的半径
原则上霍夫变换可以检测任何形状,但复杂的形状需要的参数多,霍夫空间维数多,不太好。霍夫梯度法可以有效减少维度,提高效率
# cv2.HoughCircles(image, method, dp, minDist, param1=100, param2=100, minRadius=0, maxRadius=0)
# image:8bit单通道图像
# method:使用霍夫变换圆检测算法,当前有cv2.HOUGH_GRADIENT和cv2.HOUGH_GRADIENT_ALT 2种方法,后者是前者的改进方法。
# dp:检测圆心的累加器精度和图像精度比的倒数,比如dp=1时累加器和输入图像有相同的分辨率,dp=2时累加器是输入图像一半大的宽高;method=cv2.HOUGH_GRADIENT_ALT时推荐设置dp=1.5。
# minDist:圆心之间的最小距离。如果两个圆心之间距离太小就视为一个圆心
# param1:特定方法参数,和method配合;当method=cv2.HOUGH_GRADIENT或method=cv2.HOUGH_GRADIENT_ALT时,该参数是canny检测的高阈值,低阈值是该参数的一半;method=cv2.HOUGH_GRADIENT_ALT时,内部使用Scharr计算图像梯度,这个值通常要设置得更大。
# param2:检测圆心和确定半径时所共有的阈值
# minRadius:检测圆半径的最小值
# maxRadius
# 返回:输出圆向量(圆心横坐标,圆心纵坐标,圆半径),是一个三维数组
由于霍夫圆检测对噪声敏感,故首先要对图像进行中值滤波
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# 读取图像
img = cv.imread("circle.jpg")
# 转换为灰度图像
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 进行中值模糊,去噪点
gray_blurred = cv.medianBlur(gray, 7)
# 霍夫圆检测
circles = cv.HoughCircles(gray_blurred, cv.HOUGH_GRADIENT, 1, 200, param1=100, param2=50, minRadius=0, maxRadius=100)
# 确保检测到了圆
if circles is not None:
# 四舍五入成int(画圆要int)
circles = np.round(circles[0, :]).astype("int")
for (x, y, r) in circles:
# 绘制圆
cv.circle(img, (x, y), r, (0, 255, 0), 2)
# 绘制圆心
cv.circle(img, (x, y), 2, (0, 255, 0), -1)
# 使用matplotlib显示图像
plt.figure(figsize=(10, 8), dpi=100)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB)) # 注意这里使用cv.cvtColor转换颜色通道
plt.title("Hough Circle")
plt.xticks([])
plt.yticks([])
plt.show()
cv.imread读进来的是三通道(bgr),cvtColor成gray后是单通道,画圆、降噪要在gray上操作,但输出图像时要三通道的图(rgb)。这就是为什么读入图像后还要倒腾两遍
角点检测
三维场景重建运动估计、目标跟踪、目标识别、图像配准与匹配
Harris算法
思想:通过图像的局部小窗口观察图像。角点的特征是窗口沿任意方向移动都会导致图像灰度的明显变化
二元函数 ( f(x, y) ) 在点 ( (a, b) ) 处的泰勒展开公式可以写成如下形式:
f ( x , y ) = f ( a , b ) + ∂ f ∂ x ( a , b ) ( x − a ) + ∂ f ∂ y ( a , b ) ( y − b ) + 1 2 ! [ ∂ 2 f ∂ x 2 ( a , b ) ( x − a ) 2 + 2 ∂ 2 f ∂ x ∂ y ( a , b ) ( x − a ) ( y − b ) + ∂ 2 f ∂ y 2 ( a , b ) ( y − b ) 2 ] + ⋯ f(x, y) = f(a, b) + \frac{\partial f}{\partial x}(a, b)(x-a) + \frac{\partial f}{\partial y}(a, b)(y-b) + \frac{1}{2!}\left[ \frac{\partial^2 f}{\partial x^2}(a, b)(x-a)^2 + 2\frac{\partial^2 f}{\partial x \partial y}(a, b)(x-a)(y-b) + \frac{\partial^2 f}{\partial y^2}(a, b)(y-b)^2 \right] + \cdots f(x,y)=f(a,b)+∂x∂f(a,b)(x−a)+∂y∂f(a,b)(y−b)+2!1[∂x2∂2f(a,b)(x−a)2+2∂x∂y∂2f(a,b)(x−a)(y−b)+∂y2∂2f(a,b)(y−b)2]+⋯
对于图像中的一个点 (x, y) ,当图像在该点周围移动一个小的位移向量 (u, v) 时,自相关函数(能量函数)C(u, v) 可以通过计算移动前后图像灰度的变化来得到。这个函数可以表示为:
C
(
u
,
v
)
=
∑
x
,
y
w
(
x
,
y
)
[
I
(
x
+
u
,
y
+
v
)
−
I
(
x
,
y
)
]
2
C(u, v) = \sum_{x, y} w(x, y) \left[ I(x+u, y+v) - I(x, y) \right]^2
C(u,v)=x,y∑w(x,y)[I(x+u,y+v)−I(x,y)]2
其中, w(x, y) 是一个窗口函数(通常是一个高斯窗口),用于给图像的不同区域分配不同的权重,I(x, y) 是点(x, y) 的灰度值。
为了简化计算,我们对( I(x+u, y+v) )进行Taylor展开,只考虑一阶项和二阶项:
I
(
x
+
u
,
y
+
v
)
≈
I
(
x
,
y
)
+
u
I
x
+
v
I
y
I(x+u, y+v) \approx I(x, y) + u I_x + v I_y \\
I(x+u,y+v)≈I(x,y)+uIx+vIy
将上述展开式代入自相关函数,并对其进行简化,可以得到一个关于位移向量 (u, v) 的二次函数:
C
(
u
,
v
)
=
∑
x
,
y
w
(
x
,
y
)
[
I
(
x
+
u
,
y
+
v
)
−
I
(
x
,
y
)
]
2
≈
w
(
x
,
y
)
∑
x
,
y
[
u
I
x
+
v
I
y
]
2
=
[
u
v
]
⋅
w
(
x
,
y
)
∑
x
,
y
[
I
x
2
I
x
I
y
I
x
I
y
I
y
2
]
⋅
[
u
v
]
=
[
u
v
]
⋅
M
⋅
[
u
v
]
\begin{align} C(u, v) & = \sum_{x, y} w(x, y) \left[ I(x+u, y+v) - I(x, y) \right]^2 \\ & \approx w(x, y) \sum_{x, y} \left [u I_x+v I_y \right ]^2 \\ &= \begin{bmatrix} u & v \end{bmatrix} \cdot w(x, y) \sum_{x, y} \begin{bmatrix} I_x^2 & I_xI_y \\ I_xI_y & I_y^2 \end{bmatrix} \cdot \begin{bmatrix} u \\ v \end{bmatrix}\\ &= \begin{bmatrix} u & v \end{bmatrix} \cdot M \cdot \begin{bmatrix} u \\ v \end{bmatrix} \\ \end{align}
C(u,v)=x,y∑w(x,y)[I(x+u,y+v)−I(x,y)]2≈w(x,y)x,y∑[uIx+vIy]2=[uv]⋅w(x,y)x,y∑[Ix2IxIyIxIyIy2]⋅[uv]=[uv]⋅M⋅[uv]
其中,(写成分块矩阵的方式是方便后面计算)
M
=
w
(
x
,
y
)
∑
x
,
y
[
I
x
2
I
x
I
y
I
x
I
y
I
y
2
]
=
[
A
B
B
C
]
M = w(x, y) \sum_{x, y} \begin{bmatrix} I_x^2 & I_xI_y \\ I_xI_y & I_y^2 \end{bmatrix}\\ = \begin{bmatrix} A & B \\ B & C \end{bmatrix}
M=w(x,y)x,y∑[Ix2IxIyIxIyIy2]=[ABBC]
C为二次型,M为实对称矩阵。可对C进行标准化:
C
(
u
′
,
v
′
)
=
[
u
′
v
′
]
⋅
[
λ
1
0
0
λ
2
]
⋅
[
u
′
v
′
]
即:
C
(
u
′
,
v
′
)
=
λ
1
u
′
2
+
λ
2
v
′
2
C(u',v')= \begin{bmatrix} u' & v' \end{bmatrix} \cdot \begin{bmatrix} \lambda_1 & 0 \\ 0 & \lambda_2 \end{bmatrix} \cdot \begin{bmatrix} u' \\ v' \end{bmatrix} \\ 即:C(u',v')=\lambda_1u'^2+\lambda_2v'^2
C(u′,v′)=[u′v′]⋅[λ100λ2]⋅[u′v′]即:C(u′,v′)=λ1u′2+λ2v′2
- TODO:从这步开始就跟不上思路了
化简到这一步就和(u,v),也就是方向的关系没那么大了。虽然最初(u,v)只能表示一个方向,但引入特征值λ后C的变换主要受特征值λ影响
正定二次型,特征值λ一定是正数
平坦区域:两个特征值都小,且近似相等,能量函数在各个方向上都较小;
边缘区域:一个特征值大,另一个特征值小,能量函数在某一方向上增大,其他方向较小;
角点区域:两个特征值都大,且近似相等,能量函数在所有方向上都增大。
这样一来,我们就可以仅通过矩阵M的特征值,来评估图像是否存在角点。
但Harris角点的计算方法甚至不需要用到特征值,只需要计算一个Harris响应值R:
R
=
d
e
t
(
M
)
−
k
(
t
r
a
c
e
(
M
)
)
2
其中,
d
e
t
(
M
)
=
λ
1
λ
2
是矩阵
M
的行列式,代表特征值的乘积
t
r
a
c
e
(
M
)
=
λ
1
+
λ
2
是矩阵
M
的迹,代表特征值的和
k
是一个常数,用于调整行列式和迹的相对重要性。一般取
0.04
−
0.06
R = det(M) - k(trace(M))^2\\ \begin{align} &其中,det(M) = \lambda_1 \lambda_2 是矩阵 M 的行列式,代表特征值的乘积\\ &trace(M) = \lambda_1 + \lambda_2 是矩阵M 的迹,代表特征值的和\\ &k 是一个常数,用于调整行列式和迹的相对重要性。一般取0.04-0.06 \end{align}
R=det(M)−k(trace(M))2其中,det(M)=λ1λ2是矩阵M的行列式,代表特征值的乘积trace(M)=λ1+λ2是矩阵M的迹,代表特征值的和k是一个常数,用于调整行列式和迹的相对重要性。一般取0.04−0.06
此公式是创造出来的,因其能反映:上述矩阵M的特征值 和 图像是否存在角点 2的关系
- TODO:这里好像涉及到最优化问题,熟悉又陌生,后面专门看一下
- TODO:如果是矩形窗口的话,平行于窗口边缘的直线岂不是检测不出来?哦对,所以需要w(x,y)正态分布
代码实现
# dst = cv2.cornerHarris(src, blockSize, ksize, k)
# src:数据类型为float32的输入图像
# blockSize:角点检测的邻域大小
# ksize:sobel求导使用的核的大小
# k:0.04~0.06
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread("")
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 输入图像必须是float32类型
gray = np.float32(gray)
# 角点检测
dst = cv.cornerHarris(gray, 2, 3, 0.04)
# 设置阈值,将角点绘制出来(把满足条件的点涂红色)
img[dst>0.001*dst.max()] = [0, 0, 255]
# 图像显示
plt.figure(figsize=(10, 8), dpi=100)
plt.title("Harris角点检测")
plt.xticks([])
plt.yticks([])
plt.show()
Shi-Tomas算法
在Harris的R响应函数基础上,若两个特征值中较小的一个大于最小阈值,则会得到强角点。
Shi-Tomasi 发现角点的稳定性其实和矩阵 M 的较小特征值有关,于是直接用较小的那个特征值作为分数。这样就不用调整k值了。如果矩阵M的两个特征值中较小的那一个大于设定的阈值,那么这个点是角点;如果两个特征值都小于阈值,那较小的特征值也必定小于阈值,这个点就是平坦区域的点;如果其中一个特征值大于阈值而另外一个特征值小于阈值,那么这个点就是边缘点。所以我们只需要判断矩阵M的两个特征值中较小的那一个特征值是否是大于阈值,如果大于阈值这个点就是强角点。
# corners = cv2.goodFeaturesToTrack( image, maxCorners, qualityLevel, minDistance)
# image:输入灰度图像
# maxCorners:获取角点数目最大值
# qualityLevel:最低可接受的角点质量水平,0-1之间
# minDistance:角点之间最小欧式距离,太近视为一个点
# corners:返回搜索到点的坐标(按质量排序)
.ravel() 将多维数组压平成一维
SIFT/SURF算法
Harris和Shi-Tomasi具有旋转不变性,但不具备尺度不变性
SIFT:Scale-invariant feature transform
- TODO:跳过了,这也太复杂了
视频
基本视频操作
- 读取并播放
cap = cv2.VideoCapture( filepath )
# 创建VideoCapture对象,用于捕获视频
retval = cap.get(propId)
# 获取视频某些属性 proId:0~18
# 0:播放到第几秒了
# 1:播放到哪一帧了
# 2:从0到1播放的相对位置
# 3:视频流的帧宽度
# 4:视频流的帧高度
# 5:帧率
# 6:编解码四字符代码(avi?mp4?)
# 7:视频文件的帧数
cap.set( propId, value )
# 修改视频的某些属性
flag = cap.isOpened()
# 判断是否读取成功
ret, frame = cap.read()
# 获取视频的下一帧
# ret:True/ False,表示获取成功还是失败
# Frame:获取到的帧
cap.release()
# 将视频释放
# 实例
import cv2 as cv
cap = cv.VideoCapture("bike.mp4")
while ( cap.isOpened() ):
ret, frame = cap.read()
if ret == True:
cv.imshow("frame", frame )
if cv.waitKey(25) & 0xFF == ord("q"):
break
cap.release()
cv.destroyAllWindows()
cv.waitKey(25) & 0xFF == ord("q")
& 0xFF的按位与操作只取cv2.waitKey(1)返回值最后八位,因为有些系统cv2.waitKey(1)的返回值不止八位
ord(‘q’)表示q的ASCII值
总体效果:按下q键后break
- 保存
out = cv2.VideoWriter( filename, fourcc, fps, frameSize )
# fourcc:指定视频编解码器的四字节代码
# fps:帧率
# frameSIze:帧大小
# 其中,fourcc由如下函数设置
retval = cv2.VideoWriter_fourcc( c1, c2, c3, c4)
# 参数分别是4个字符
# win中,DIVX(.avi)
# os中,MJPG(.mp4), DIVX(.avi), X264(.mkv)
# 在OpenCV 4.8.0.76版本中,cv2.VideoWriter_fourcc函数已被废弃,取而代之的是cv2.VideoWriter.fourcc属性。
参数前的单* 可将参数以元组形式传入
(*'MJPG') 也就相当于('M', 'J', 'P', 'G' )
# 例子
import cv2 as cv
import numpy as np
cap = cv.VideoCapture( "bike.mp4" )
frame_width, frame_height = int(cap.get(3)), int(cap.get(4))
output = cv.VideoWriter( 'output.avi', cv.VideoWriter.fourcc( *'MJPG' ), 10, (frame_width, frame_height) )
while( output.isOpened() ):
ret, frame = cap.read()
if ret == True:
output.write(frame)
else:
break
cap.release()
output.release()
cv.destroyAllWindows()
视频追踪
ROI:region of interest 感兴趣区域、目标区域
Meanshift
均值漂移算法
原理: 移动窗口到点集密度最大的区域当中。通过窗口内所有点计算出重心,每次将窗口形心移动至重心处。(这个操作看起来很合乎常识,不过在数学上它是可以被证明的,而且证明过程比想象中复杂一丢丢)(opencv中的meanshift严格意义上来说是最简化版本的算法,结合着数学原理讲反而不好理解)
meanshift算法除了应用在视频追踪当中,在聚类,平滑等等各种涉及到数据以及非监督学习的场合当中均有重要应用。
步骤:
- 读取视频文件
- 设置ROI:在第一帧图像上设置ROI
- 计算直方图:计算ROI的HSV直方图,并进行归一化
- 目标追踪:设置窗口搜索停止条件,直方图反向投影,进行目标追踪,并在目标位置绘制矩形框
ret, track_window = cv2.meanShift( probIamge, window, criteria )
# probImage: ROI,即目标的直方图反向投影(cv.calcBackProject计算得到)
# window:初始搜索窗口
# criteria:终止条件(最大迭代次数、窗口中心飘移最大距离)
# ret:
- TODO:看了很多文章,还是不能很好地理解反向直方图的作用
- TODO:当然了,meanshift的数学原理也没太看懂
import cv2 as cv
# 1 获取视频
cap = cv.VideoCapture("ball2.mp4")
# 2 获取第一帧图像,指定ROI
ret, frame = cap.read()
frame_height = cap.get(4)
frame_weight = cap.get(3) # 3宽4高
# ROI
r, c, h, w = int(frame_height * 0.8), 30, int(frame_height*0.2), int(frame_weight * 0.2) # 这里我不知道怎么找第一帧小球的位置,只能估摸着来
track_window = (c, r, w, h) # 下面meanshift追踪要用
roi = frame[r:r+h, c:c+h]
# 3 计算直方图
# 3.1 转换色彩空间
hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
# 3.2 去除低亮度的值
# 3.3 计算直方图
roi_hist = cv.calcHist( [hsv_roi], [0], None, [180], [0,180])
# 3.4 归一化
cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX)
# 4 目标追踪
# 4.1 设置窗口搜索终止条件: 最大迭代次数,窗口中心漂移最小值
term_crit = ( cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 1 )
while( True ):
# 4.2 对每一帧处理
ret, frame = cap.read()
if ret :
# 4.3 对每一帧计算直方图的反向投影
hsv = cv.cvtColor( frame, cv.COLOR_BGR2HSV )
dst = cv.calcBackProject([hsv], [0], roi_hist, [0,100], 1)
# 4.4 再进行meanshift追踪
ret, track_window = cv.meanShift( dst, track_window, term_crit )
# 4.5 将追踪的位置绘制在视频上,展示
x, y, w, h = track_window
img2 = cv.rectangle( frame, (x,y), (x+w,y+h), 255, 2 )
cv.imshow('frame', img2)
if cv.waitKey(60) & 0xFF == ord('q'):
break
else:
break
# 5 资源释放
cap.release()
cv.destroyAllWindows()
Camshift
meanshift问题:窗口大小固定,不能根据目标大小的变化来改变窗口的角度&大小
camshift原理:首先应用meanshift,一旦meanshift收敛,就更新窗口大小,计算最佳拟合椭圆的方向,从而根据目标的位置带线啊哦更新搜索窗口。
# 进行Camshift追踪
ret, track_window = cv2.Camshift( dst, track_window, term_crit )
# 绘制追踪结果
pts = cv2.boxPoints( ret ) # 得到一系列点
pts = np.int0( pts )
img2 = cv2.polylines( frame, [pts], True, 255, 2) # 根据这些点绘制多边形
缺点:不准
人脸检测
原理:
OpenCV中自带一幸运脸好的检测器,包括面部、眼睛、猫脸等,保存在xml文件中
步骤:
- 读取图片转换为灰度图
- 实例化人脸、眼睛检测的分类器对象
# 实例化级联分类器
classifier = cv2.CascadeClassifier( "haarcascade_frontalface_default.xml" )
# 加载分类器
classifier.load( 'haarcascade_frontalface_default.xml' )
- 进行人脸、眼睛检测
rect = classifier.detectMultiScale( gray, scaleFactor, minNeighbors, minSize, maxSize )
- scaleFactor: 前后两次扫描中搜索窗口的比例系数
- minneighbors: 目标至少被检测到这么多次才能被认为是目标
- minsize/maxsize: 目标的最小尺寸和最大尺寸
import cv2 as cv
from matplotlib import pyplot as plt
# 1 读入图片
img = cv.imread("chum.jpg")
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 2 实例化OpenCV人脸和眼睛识别的分类器
face_cas = cv.CascadeClassifier( "C:\\Users\\mcfly\\AppData\\Local\\Programs\\Python\\Python310\\Lib\\site-packages\\cv2\\data\\haarcascade_frontalface_default.xml" )
face_cas.load("C:\\Users\\mcfly\\AppData\\Local\\Programs\\Python\\Python310\\Lib\\site-packages\\cv2\\data\\haarcascade_frontalface_default.xml")
eyes_cas = cv.CascadeClassifier( "C:\\Users\\mcfly\\AppData\\Local\\Programs\\Python\\Python310\\Lib\\site-packages\\cv2\\data\\haarcascade_eye.xml" )
eyes_cas.load("C:\\Users\\mcfly\\AppData\\Local\\Programs\\Python\\Python310\\Lib\\site-packages\\cv2\\data\\haarcascade_eye.xml") # 这里要把路径写全,不然报错
# 3 调用识别人脸
faceRects = face_cas.detectMultiScale( gray, scaleFactor=1.2, minNeighbors=3, minSize=(32,33) )
for faceRect in faceRects:
x, y, w, h = faceRect
# 框出人脸
cv.rectangle(img, (x, y), (x+h, y+w), (0,255,0), 3)
# 4 在识别出的人脸中进行眼睛判定
roi_color = img[y:y+h, x:x+w]
roi_gray = gray[y:y+h, x:x+w]
eyes = eyes_cas.detectMultiScale(roi_gray)
for (ex, ey, ew, eh ) in eyes:
cv.rectangle( img, (ex,ey), (ex+ew, ey+eh), (0,0,255), 2)
# 5 绘制
plt.figure(figsize=(8, 6), dpi=100)
plt.imshow( img[:,:,::-1] )
plt.title("Face Recognition")
plt.xticks([])
plt.yticks([])
plt.show()