OpenCV 官翻 2 - 图像处理
文章目录
- 色彩空间转换
- 目标
- 色彩空间转换
- 目标追踪
- 如何确定要追踪的HSV值?
- 练习
- 图像的几何变换
- 目标
- 变换
- 缩放
- 翻译
- 旋转
- 仿射变换
- 透视变换
- 其他资源
- 图像阈值处理
- 目标
- 简单阈值化
- 自适应阈值化
- 大津二值化法
- Otsu二值化算法原理
- 其他资源
- 练习
- 图像平滑处理
- 目标
- 二维卷积(图像滤波)
- 图像模糊(图像平滑)
- 1、均值滤波
- 2、高斯模糊
- 3、中值模糊
- 4、双边滤波
- 额外资源
- 形态学变换
- 目标
- 理论基础
- 1、腐蚀操作
- 2、膨胀
- 3、开运算
- 4、闭合操作
- 5、形态学梯度
- 6、高帽变换
- 7、黑帽变换
- 结构元素
- 附加资源
- 图像梯度
- 目标
- 理论
- 1、Sobel 与 Scharr 导数算子
- 2、拉普拉斯导数
- 代码
- 一个重要问题!
- Canny边缘检测
- 目标
- 理论
- OpenCV 中的 Canny 边缘检测
- 其他资源
- 练习
- 图像金字塔
- 目标
- 理论基础
- 使用金字塔进行图像融合
- 附加资源
- OpenCV 中的轮廓
- OpenCV中的直方图
- OpenCV 中的图像变换
- 模板匹配
- 目标
- 理论
- OpenCV 中的模板匹配
- 多目标模板匹配
- 霍夫线变换
- 目标
- 理论
- OpenCV中的霍夫变换
- 概率霍夫变换
- 附加资源
- 霍夫圆变换
- 目标
- 理论
- 使用分水岭算法进行图像分割
- 目标
- 理论基础
- 代码
- 其他资源
- 练习
- 使用GrabCut算法进行交互式前景提取
- 目标
- 理论基础
- 演示
- 练习
色彩空间转换
https://docs.opencv.org/4.x/df/d9d/tutorial_py_colorspaces.html
目标
- 在本教程中,您将学习如何将图像从一种色彩空间转换到另一种色彩空间,例如 BGR ↔\leftrightarrow↔ 灰度、BGR ↔\leftrightarrow↔ HSV 等。
- 此外,我们将创建一个应用程序来提取视频中的彩色对象
- 您将学习以下函数:
cv.cvtColor()
、cv.inRange()
等。
色彩空间转换
OpenCV 提供了超过 150 种色彩空间转换方法。但我们主要关注其中最常用的两种:BGR ↔\leftrightarrow↔ 灰度 和 BGR ↔\leftrightarrow↔ HSV。
进行色彩转换时,我们使用函数 cv.cvtColor(input_image, flag)
,其中 flag 参数决定转换类型。
对于 BGR \(\rightarrow) 灰度转换,我们使用标志位 cv.COLOR_BGR2GRAY](https://docs.opencv.org/4.x/d8/d01/group__imgproc__color__conversions.html#gga4e0972be5de079fed4e3a10e24ef5ef0a353a4b8db9040165db4dacb5bcefb6ea "在RGB/BGR与灰度之间转换,色彩空间转换")。类似地,对于 BGR \\(\\rightarrow\) HSV 转换,我们使用标志位 [cv.COLOR_BGR2HSV
。要获取其他标志位,只需在 Python 终端中运行以下命令:
>>> import cv2 as cv
>>> flags = [i for i in dir(cv) if i.startswith('COLOR_')]
>>> print( flags )
对于HSV色彩空间,色调(hue)的范围是[0,179],饱和度(saturation)的范围是[0,255],明度(value)的范围是[0,255]。不同软件采用不同的标度范围,因此若需将OpenCV的数值与其他软件进行对比,必须对这些范围进行归一化处理。
目标追踪
既然我们已经知道如何将BGR图像转换为HSV色彩空间,接下来可以利用这一技术来提取特定颜色的目标物体。在HSV色彩空间中,颜色表示比BGR空间更为直观。在本案例中,我们将尝试提取蓝色目标物体。具体方法如下:
- 获取视频的每一帧画面
- 将图像从BGR色彩空间转换到HSV色彩空间
- 对HSV图像进行蓝色阈值处理
- 单独提取蓝色目标物体后,即可对该图像进行任意操作
下方是带有详细注释的代码实现:
import cv2 as cv
import numpy as npcap = cv.VideoCapture(0)while(1):# Take each frame_, frame = cap.read()# Convert BGR to HSVhsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)# define range of blue color in HSVlower_blue = np.array([110,50,50])upper_blue = np.array([130,255,255])# Threshold the HSV image to get only blue colorsmask = cv.inRange(hsv, lower_blue, upper_blue)# Bitwise-AND mask and original imageres = cv.bitwise_and(frame,frame, mask= mask)cv.imshow('frame',frame)cv.imshow('mask',mask)cv.imshow('res',res)k = cv.waitKey(5) & 0xFFif k == 27:breakcv.destroyAllWindows()
下图展示了蓝色物体的追踪效果:
注意:图像中存在一些噪点,我们将在后续章节中学习如何消除它们。
这是物体追踪中最简单的方法。一旦掌握了轮廓函数的使用,你可以实现许多功能,例如找到物体的质心并用它来追踪物体、通过在摄像头前移动手部来绘制图形等有趣的操作。
如何确定要追踪的HSV值?
这是一个在stackoverflow.com上常见的问题。解决方法非常简单,你可以使用相同的函数cv.cvtColor()`。你不需要传入图像,只需传入所需的BGR值即可。例如,要查找绿色的HSV值,可以在Python终端中尝试以下命令:
>>> green = np.uint8([[[0,255,0 ]]])
>>> hsv_green = cv.cvtColor(green,cv.COLOR_BGR2HSV)
>>> print( hsv_green )
[[[ 60 255 255]]]
现在,您将[H-10, 100,100]和[H+10, 255, 255]分别作为下限和上限。除了这种方法,您还可以使用GIMP等图像编辑工具或任何在线转换器来查找这些值,但别忘了调整HSV范围。
练习
1、尝试找到一种方法同时提取多个彩色物体,例如一次性提取红色、蓝色和绿色物体。
生成于 2025年4月30日 星期三 23:08:42,适用于 OpenCV,由 doxygen 1.12.0 生成
图像的几何变换
https://docs.opencv.org/4.x/da/d6e/tutorial_py_geometric_transformations.html
目标
- 学习如何对图像应用不同的几何变换,如平移、旋转、仿射变换等。
- 你将了解以下函数:
cv.getPerspectiveTransform
变换
OpenCV 提供了两个变换函数:cv.warpAffine
和 cv.warpPerspective
,通过它们可以实现各种类型的变换。cv.warpAffine
接收一个 2x3 的变换矩阵作为输入,而 cv.warpPerspective
则需要一个 3x3 的变换矩阵。
缩放
缩放即调整图像尺寸。OpenCV提供了 cv.resize()
函数实现这一功能。您既可以手动指定目标尺寸,也可以设置缩放比例系数。该操作支持多种插值方法:
- 缩小图像时推荐使用
cv.INTER_AREA
- 放大图像建议选择
cv.INTER_CUBIC
(速度较慢)或cv.INTER_LINEAR
默认情况下,所有缩放操作均采用 cv.INTER_LINEAR
插值方法。您可以通过以下任一方式调整输入图像尺寸:
import numpy as np
import cv2 as cvimg = cv.imread('messi5.jpg')
assert img is not None, "file could not be read, check with os.path.exists()"res = cv.resize(img,None,fx=2, fy=2, interpolation = cv.INTER_CUBIC)#ORheight, width = img.shape[:2]
res = cv.resize(img,(2*width, 2*height), interpolation = cv.INTER_CUBIC)
翻译
平移是指改变物体的位置。如果已知物体在(x,y)方向的位移量\((t_x,t_y)\),可以按以下方式构建变换矩阵\(\textbf{M}\):
111111 111
你可以将其转换为np.float32类型的Numpy数组,并传入**cv.warpAffine()
**函数。以下示例展示了(100,50)位移量的实现:
import numpy as np
import cv2 as cvimg = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
rows,cols = img.shapeM = np.float32([[1,0,100],[0,1,50]])
dst = [cv.warpAffine`](https://docs.opencv.org/4.x/da/d54/group__imgproc__transform.html#ga0203d9ee5fcd28d40dbc4a1ea4451983)(img,M,(cols,rows))cv.imshow('img',dst)
cv.waitKey((0)
cv.destroyAllWindows()
警告
cv.warpAffine()
函数的第三个参数是输出图像的尺寸,其格式应为 (宽度, 高度)。请注意:宽度 = 列数,高度 = 行数。
效果如下图所示:
旋转
图像旋转角度 \(\theta\) 可通过以下形式的变换矩阵实现:
\[M = \begin{bmatrix} cos\theta & -sin\theta \\ sin\theta & cos\theta \end{bmatrix}\]
但 OpenCV 提供了可调整旋转中心的缩放旋转功能,允许在任意指定位置进行旋转。修正后的变换矩阵为:
\[\begin{bmatrix} \alpha & \beta & (1- \alpha ) \cdot center.x - \beta \cdot center.y \\ - \beta & \alpha & \beta \cdot center.x + (1- \alpha ) \cdot center.y \end{bmatrix}\]
其中:
\[\begin{array}{l} \alpha = scale \cdot \cos \theta , \\ \beta = scale \cdot \sin \theta \end{array}\]
OpenCV 提供了 cv.getRotationMatrix2D
函数来计算该变换矩阵。参考以下示例,该示例将图像绕中心旋转90度且不进行缩放。
img = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
rows,cols = img.shape# cols-1 and rows-1 are the coordinate limits.
M = cv.getRotationMatrix2D(((cols-1)/2.0,(rows-1)/2.0),90,1)
dst = cv.warpAffine(img,M,(cols,rows))
查看结果:
仿射变换
在仿射变换中,原始图像中的所有平行线在输出图像中仍保持平行。要计算变换矩阵,我们需要从输入图像中选取三个点及其在输出图像中的对应位置。然后 cv.getAffineTransform
会生成一个2x3矩阵,该矩阵将被传递给 cv.warpAffine
。
查看以下示例,并注意我选择的点(这些点已用绿色标记):
img = cv.imread('drawing.png')
assert img is not None, "file could not be read, check with os.path.exists()"
rows,cols,ch = img.shapepts1 = np.float32([[50,50],[200,50],[50,200]])
pts2 = np.float32([[10,100],[200,50],[100,250]])M = cv.getAffineTransform(pts1,pts2)dst = cv.warpAffine(img,M,(cols,rows))plt.subplot(121),plt.imshow(img),plt.title('Input')
plt.subplot(122),plt.imshow(dst),plt.title('Output')
plt.show()
查看结果:
透视变换
要实现透视变换,需要一个3x3的变换矩阵。即使在变换后,直线仍将保持直线状态。要找到这个变换矩阵,需要在输入图像上指定4个点及其在输出图像中对应的位置。这4个点中有3个不能共线。然后可以通过函数 cv.getPerspectiveTransform
求得变换矩阵。接着使用这个3x3变换矩阵应用 cv.warpPerspective
。
参考以下代码:
img = cv.imread('sudoku.png')
assert img is not None, "file could not be read, check with os.path.exists()"
rows,cols,ch = img.shapepts1 = 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))plt.subplot(121),plt.imshow(img),plt.title('Input')
plt.subplot(122),plt.imshow(dst),plt.title('Output')
plt.show()
Result:
其他资源
1、《计算机视觉:算法与应用》,Richard Szeliski
由 doxygen 1.12.0 生成于 2025年4月30日 星期三 23:08:42,适用于 OpenCV
图像阈值处理
https://docs.opencv.org/4.x/d7/d4d/tutorial_py_thresholding.html
目标
- 本教程中,您将学习简单阈值处理、自适应阈值处理以及大津阈值法。
- 您将掌握函数
cv.threshold
和cv.adaptiveThreshold
的使用方法。
简单阈值化
这里的原理非常直接。对于每个像素,都应用相同的阈值。如果像素值小于或等于阈值,则将其设为0,否则设为最大值。函数 cv.threshold
用于实现阈值化处理。第一个参数是源图像,必须是灰度图像。第二个参数是用于分类像素值的阈值。第三个参数是当像素值超过阈值时赋予的最大值。OpenCV 提供了不同类型的阈值化方法,通过函数的第四个参数指定。上文描述的基本阈值化通过类型 cv.THRESH_BINARY
实现。所有简单阈值化类型包括:
cv.THRESH_BINARY
cv.THRESH_BINARY_INV
cv.THRESH_TRUNC
cv.THRESH_TOZERO
cv.THRESH_TOZERO_INV
具体差异请参阅各类型的文档说明。
该方法返回两个输出值:第一个是实际使用的阈值,第二个输出是经过阈值处理的图像。
以下代码演示了不同简单阈值化类型的比较效果:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as pltimg = cv.imread('gradient.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
ret,thresh1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
ret,thresh2 = cv.threshold(img,127,255,cv.THRESH_BINARY_INV)
ret,thresh3 = cv.threshold(img,127,255,cv.THRESH_TRUNC)
ret,thresh4 = cv.threshold(img,127,255,cv.THRESH_TOZERO)
ret,thresh5 = cv.threshold(img,127,255,cv.THRESH_TOZERO_INV)titles = ['Original Image','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',vmin=0,vmax=255)plt.title(titles[i])plt.xticks([]),plt.yticks([])plt.show()
注意:要绘制多张图像,我们使用了 plt.subplot()
函数。更多详情请查阅 matplotlib 文档。
代码运行结果如下:
自适应阈值化
在前一节中,我们使用单一全局值作为阈值。但这可能不适用于所有情况,例如当图像不同区域存在不同光照条件时。此时,自适应阈值化就能发挥作用。该算法会根据像素周围的小区域来确定其阈值。因此,同一图像的不同区域会获得不同阈值,从而在光照不均的图像上取得更好效果。
除了前文所述的参数外,方法 cv.adaptiveThreshold` 还接收三个输入参数:
adaptiveMethod 决定阈值计算方式:
- cv.ADAPTIVE_THRESH_MEAN_C`:阈值为邻域平均值减去常数 C
- cv.ADAPTIVE_THRESH_GAUSSIAN_C`:阈值为邻域值的高斯加权和减去常数 C
blockSize 决定邻域区域的大小,C 是从邻域像素均值或加权和中减去的常数。
以下代码对比了全局阈值化和自适应阈值化在光照不均图像上的效果:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as pltimg = cv.imread('sudoku.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
img = cv.medianBlur(img,5)ret,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
th2 = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_MEAN_C,\cv.THRESH_BINARY,11,2)
th3 = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,\cv.THRESH_BINARY,11,2)titles = ['Original Image', 'Global Thresholding (v = 127)','Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [img, 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()
Result:
大津二值化法
在全局阈值处理中,我们通常随意选择一个值作为阈值。而大津法则无需手动选择,能够自动确定最佳阈值。
设想一张仅包含两种不同像素值的图像(双峰图像),其直方图会呈现两个明显的波峰。理想的阈值应位于这两个波峰之间的谷底。大津法正是通过分析图像直方图,计算出最优的全局阈值。
具体实现时,需要使用 cv.threshold()
函数,并额外传入 cv.THRESH_OTSU
标志。此时可以随意指定初始阈值参数,算法会自动计算最优阈值并通过第一个返回值输出。
参考以下示例:输入图像含有噪声。第一种情况采用固定阈值127进行全局二值化;第二种情况直接应用大津阈值法;第三种情况先用5x5高斯核滤波去噪,再执行大津阈值处理。注意观察噪声过滤如何显著改善二值化效果。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as pltimg = cv.imread('noisy2.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"# global thresholding
ret1,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)# Otsu's thresholding
ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)# Otsu's thresholding after Gaussian filtering
blur = cv.GaussianBlur(img,(5,5),0)
ret3,th3 = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)# plot all the images and their histograms
images = [img, 0, th1,img, 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"]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()
Result:
Otsu二值化算法原理
本节通过Python实现演示Otsu二值化的实际工作原理。若不感兴趣可直接跳过。
针对双峰图像,Otsu算法的核心是寻找使加权类内方差最小化的阈值(t),其数学关系表示为:
\[\sigma_w^2(t) = q_1(t)\sigma_12(t)+q_2(t)\sigma_22(t)\]
其中
\[q_1(t) = \sum_{i=1}^{t} P(i) \quad \& \quad q_2(t) = \sum_{i=t+1}^{I} P(i)\]
\[\mu_1(t) = \sum_{i=1}^{t} \frac{iP(i)}{q_1(t)} \quad \& \quad \mu_2(t) = \sum_{i=t+1}^{I} \frac{iP(i)}{q_2(t)}\]
\[\sigma_1^2(t) = \sum_{i=1}^{t} [i-\mu_1(t)]^2 \frac{P(i)}{q_1(t)} \quad \& \quad \sigma_2^2(t) = \sum_{i=t+1}^{I} [i-\mu_2(t)]^2 \frac{P(i)}{q_2(t)}\]
该算法实质是寻找位于双峰之间的t值,使得两个类别的方差都达到最小。Python实现代码如下:
img = cv.imread('noisy2.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
blur = cv.GaussianBlur(img,(5,5),0)# find normalized_histogram, and its cumulative distribution function
hist = cv.calcHist([blur],[0],None,[256],[0,256])
hist_norm = hist.ravel()/hist.sum()
Q = hist_norm.cumsum()bins = np.arange(256)fn_min = np.inf
thresh = -1for i in range(1,256):p1,p2 = np.hsplit(hist_norm,[i]) # probabilitiesq1,q2 = Q[i],Q[255]-Q[i] # cum sum of classesif q1 < 1.e-6 or q2 < 1.e-6:continueb1,b2 = np.hsplit(bins,[i]) # weights# finding means and variancesm1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2v1,v2 = np.sum(((b1-m1)**2)*p1)/q1,np.sum(((b2-m2)**2)*p2)/q2# calculates the minimization functionfn = v1*q1 + v2*q2if fn < fn_min:fn_min = fnthresh = i# find otsu's threshold value with OpenCV function
ret, otsu = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
print( "{} {}".format(thresh,ret) )
其他资源
1、《数字图像处理》,Rafael C. Gonzalez
练习
1、Otsu二值化算法存在一些可优化的地方。你可以搜索并实现这些优化方法。
由 doxygen 1.12.0 生成于 2025年4月30日 星期三 23:08:42,用于 OpenCV
图像平滑处理
https://docs.opencv.org/4.x/d4/d13/tutorial_py_filtering.html
目标
学习如何:
- 使用各种低通滤波器对图像进行模糊处理
- 将自定义滤波器应用于图像(二维卷积)
二维卷积(图像滤波)
与一维信号类似,图像也可以通过各类低通滤波器(LPF)、高通滤波器(HPF)等进行滤波处理。低通滤波器可用于消除噪声、模糊图像等,而高通滤波器则有助于检测图像边缘。
OpenCV提供了 cv.filter2D()
函数来实现内核与图像的卷积运算。例如,我们可以对图像尝试平均滤波器。一个5x5的平均滤波器内核如下所示:
\[K = \frac{1}{25} \begin{bmatrix} 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 \end{bmatrix}\]
该运算的工作原理是:将内核覆盖在某个像素上,对内核覆盖的25个像素值求和后取平均值,并用该平均值替换中心像素值。此操作将在图像所有像素上重复执行。请尝试以下代码并观察效果:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as pltimg = cv.imread('opencv_logo.png')
assert img is not None, "file could not be read, check with os.path.exists()"kernel = np.ones((5,5),np.float32)/25
dst = cv.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()
Result:
图像模糊(图像平滑)
图像模糊是通过将图像与低通滤波器核进行卷积实现的。这种方法能有效去除噪声,实际上它会移除图像中的高频内容(例如噪声、边缘)。因此在此操作中边缘会略微模糊(也存在不模糊边缘的模糊技术)。OpenCV提供了四种主要的模糊技术类型。
1、均值滤波
通过对图像进行归一化盒式滤波卷积实现。该方法会计算核区域内所有像素的平均值,并用该值替换中心元素。可通过函数 cv.blur()
或 cv.boxFilter()
实现。关于卷积核的更多细节请查阅文档。需要指定核的宽度和高度,3x3归一化盒式滤波核如下所示:
\[K = \frac{1}{9} \begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{bmatrix}\]
注意:若需使用非归一化盒式滤波,请调用 cv.boxFilter()
并传入参数 normalize=False。
下方演示了使用5x5核的示例效果:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as pltimg = cv.imread('opencv-logo-white.png')
assert img is not None, "file could not be read, check with os.path.exists()"blur = cv.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()
**
Result:
**
2、高斯模糊
在这种方法中,我们使用高斯核代替了盒式滤波器。该操作通过函数 cv.GaussianBlur()
实现。需要指定核的宽度和高度,这两个值必须是正奇数。同时还需分别指定X和Y方向的标准差sigmaX和sigmaY。如果仅指定sigmaX,则sigmaY将与sigmaX取值相同。若两者都设为零,则会根据核尺寸自动计算标准差。高斯模糊能高效消除图像中的高斯噪声。
如需创建高斯核,可以使用函数 cv.getGaussianKernel()
。
上述代码可修改为高斯模糊实现:
blur = cv.GaussianBlur(img,(5,5),0)
**
Result:
**
3、中值模糊
这里,函数 cv.medianBlur()
会计算核区域内所有像素的中值,并将中心元素替换为该中值。这种方法对图像中的椒盐噪声特别有效。有趣的是,在上述滤波器中,中心元素是一个新计算的值,可能是图像中的某个像素值或新生成的值。但在中值模糊中,中心元素总是被图像中的某个像素值替换,从而有效降低噪声。其核大小应为正奇数。
在本演示中,我给原始图像添加了50%的噪声并应用中值模糊。效果如下:
median = cv.medianBlur(img,5)
**
Result:
**
4、双边滤波
cv.bilateralFilter()
在去除噪声的同时能有效保持边缘锐利,但运算速度比其他滤波器慢。我们已知高斯滤波器会取像素周围邻域并计算其高斯加权平均值。这种高斯滤波器仅基于空间距离函数,即在滤波时只考虑邻近像素,而不考虑像素间是否具有相近的强度值,也不区分像素是否属于边缘。因此它会导致边缘模糊,而这并非我们期望的效果。
双边滤波同样采用基于空间距离的高斯滤波器,但额外引入了一个基于像素差异的高斯滤波器。空间距离的高斯函数确保只有邻近像素参与模糊计算,而强度差异的高斯函数则保证仅那些与中心像素强度相近的像素才会被纳入模糊计算。因此当边缘像素存在较大强度差异时,该算法能有效保留边缘特征。
以下示例演示了双边滤波的使用方法(具体参数说明请参阅文档)。
blur = cv.bilateralFilter(img,9,75,75)
结果:
可以看到,表面纹理已经消失,但边缘仍然保留完好。
额外资源
1、关于双边滤波的详细信息
由 doxygen 1.12.0 生成于 2025年4月30日 星期三 23:08:42,适用于 OpenCV
形态学变换
https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html
目标
在本章中,
- 我们将学习不同的形态学操作,如腐蚀、膨胀、开运算、闭运算等。
- 我们将了解以下函数:
cv.erode()
、
cv.dilate()
、
cv.morphologyEx()
等。
理论基础
形态学变换是基于图像形状的一些简单操作,通常应用于二值图像。这类操作需要两个输入参数:原始图像和被称为结构元素(structuring element)或内核(kernel)的矩阵,后者决定了运算的性质。最基础的形态学操作是腐蚀(Erosion)和膨胀(Dilation),其衍生形式如开运算(Opening)、闭运算(Closing)、梯度运算(Gradient)等也具有重要意义。我们将借助下图逐一讲解这些操作:
1、腐蚀操作
腐蚀的基本概念类似于土壤侵蚀,它会逐渐侵蚀前景物体的边界(通常保持前景为白色)。那么它的作用是什么呢?内核在图像上滑动(如同二维卷积运算)。原始图像中的像素点(值为1或0)只有当内核覆盖下的所有像素都为1时,才会被判定为1,否则该像素会被腐蚀(置为0)。
这意味着,根据内核尺寸的不同,所有靠近边界的像素都将被舍弃。因此前景物体的厚度或尺寸会减小,或者说图像中的白色区域会缩减。该操作能有效去除细小白色噪点(如我们在色彩空间章节所见),也可用于分离相连的物体。
这里我将使用一个5x5的全1内核作为示例,让我们看看实际效果:
import cv2 as cv
import numpy as npimg = cv.imread('j.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
kernel = np.ones((5,5),np.uint8)
erosion = cv.erode(img,kernel,iterations = 1)
Result:
2、膨胀
膨胀与腐蚀正好相反。在此操作中,只要内核下至少有一个像素为’1’,该像素元素就会被置为’1’。因此它会增加图像中的白色区域,或者说前景物体的尺寸会增大。通常在去噪等场景中,腐蚀后会进行膨胀操作。因为腐蚀虽然能消除白色噪点,但同时也会缩小目标物体。通过膨胀处理,由于噪点已被清除不会再现,而目标物体的区域会得到恢复。该操作也适用于连接物体的断裂部分。
dilation = cv.dilate(img,kernel,iterations = 1)
Result:
3、开运算
开运算实际上是先腐蚀后膨胀的另一种说法。正如前文所述,它在去除噪声方面非常有效。这里我们使用函数 cv.morphologyEx()
。
opening = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)
Result:
4、闭合操作
闭合是开操作的逆过程,先膨胀后腐蚀。该操作适用于消除前景物体内部的小孔洞或物体上的小黑点。
closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
Result:
5、形态学梯度
它是图像膨胀与腐蚀之间的差异。
结果将呈现为物体的轮廓。
gradient = cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel)
Result:
6、高帽变换
这是输入图像与图像开运算之间的差异。以下示例使用9x9核进行处理。
tophat = cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel)
Result:
7、黑帽变换
黑帽变换是指输入图像的闭运算结果与原输入图像之间的差异。
blackhat = cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel)
Result:
结构元素
在前面的示例中,我们借助Numpy手动创建了一个结构元素。它是矩形形状的。但在某些情况下,可能需要椭圆形/圆形的核。为此,OpenCV提供了一个函数 cv.getStructuringElement()
。只需传入核的形状和大小,就能获得所需的核。
# Rectangular Kernel
>>> cv.getStructuringElement(cv.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
>>> cv.getStructuringElement(cv.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
>>> cv.getStructuringElement(cv.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)# Diamond-shaped Kernel
>>> cv.getStructuringElement(cv.MORPH_DIAMOND,(5,5))
array([[0, 0, 1, 0, 0],[0, 1, 1, 1, 0],[1, 1, 1, 1, 1],[0, 1, 1, 1, 0],[0, 0, 1, 0, 0]], dtype=uint8)
附加资源
1、形态学操作 - HIPR2 网站
生成于 2025年4月30日 星期三 23:08:42,使用 doxygen 1.12.0 为 OpenCV 生成
图像梯度
https://docs.opencv.org/4.x/d5/d0f/tutorial_py_gradients.html
目标
在本章中,我们将学习:
- 如何查找图像梯度、边缘等特征
- 我们将了解以下函数:
cv.Sobel()
cv.Scharr()
cv.Laplacian()
理论
OpenCV 提供了三种类型的梯度滤波器(或称高通滤波器):Sobel、Scharr 和 Laplacian。我们将逐一探讨它们。
1、Sobel 与 Scharr 导数算子
Sobel 算子是一种结合了高斯平滑与微分运算的算子,因此对噪声具有更强的抵抗能力。您可以通过参数 yorder 和 xorder 分别指定求导方向(垂直或水平)。此外,还可以通过 ksize 参数指定核的大小。当 ksize = -1 时,将使用 3x3 的 Scharr 滤波器,其效果优于 3x3 的 Sobel 滤波器。具体使用的核函数请参阅相关文档。
2、拉普拉斯导数
它通过关系式 \(\Delta src = \frac{\partial ^2{src}}{\partial x^2} + \frac{\partial ^2{src}}{\partial y^2}\) 计算图像的拉普拉斯算子,其中每个导数都是使用 Sobel 导数求得的。如果 ksize = 1,则使用以下核进行滤波:
\[kernel = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix}\]
代码
下方代码以单一图表展示所有算子。所有卷积核尺寸均为5x5。通过将输出图像深度设为-1,可获得np.uint8类型的结果。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as pltimg = cv.imread('dave.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"laplacian = cv.Laplacian(img,cv.CV_64F)
sobelx = cv.Sobel(img,cv.CV_64F,1,0,ksize=5)
sobely = cv.Sobel(img,cv.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(sobelx,cmap = 'gray')
plt.title('Sobel X'), plt.xticks([]), plt.yticks([])
plt.subplot(2,2,4),plt.imshow(sobely,cmap = 'gray')
plt.title('Sobel Y'), plt.xticks([]), plt.yticks([])plt.show()
Result:
一个重要问题!
在上一个示例中,输出数据类型是 cv.CV_8U
或 np.uint8
,但这存在一个小问题。黑到白的过渡被视为正斜率(具有正值),而白到黑的过渡被视为负斜率(具有负值)。因此,当将数据转换为 np.uint8
时,所有负斜率都会变为零。简单来说,你会丢失这部分边缘信息。
如果要同时检测两种边缘,更好的选择是将输出数据类型保持为更高精度的形式,例如 cv.CV_16S
、cv.CV_64F
等,取其绝对值后再转换回 cv.CV_8U
。下面的代码展示了水平 Sobel 滤波器执行此过程的结果差异。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as pltimg = cv.imread('box.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"# Output dtype = cv.CV_8U
sobelx8u = cv.Sobel(img,cv.CV_8U,1,0,ksize=5)# Output dtype = cv.CV_64F. Then take its absolute and convert to cv.CV_8U
sobelx64f = cv.Sobel(img,cv.CV_64F,1,0,ksize=5)
abs_sobel64f = np.absolute(sobelx64f)
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(sobelx8u,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边缘检测
https://docs.opencv.org/4.x/da/d22/tutorial_py_canny.html
目标
在本章中,我们将学习以下内容:
- Canny边缘检测的概念
- 相关的OpenCV函数:
cv.Canny()
理论
Canny边缘检测是一种流行的边缘检测算法,由John F. Canny开发。该算法包含多个处理阶段,我们将逐步解析每个阶段。
-
噪声抑制
由于边缘检测容易受到图像噪声的影响,首先需要使用5x5高斯滤波器去除噪声。我们在之前的章节中已经介绍过这种方法。 -
计算图像强度梯度
平滑后的图像分别与水平和垂直方向的Sobel核进行卷积,得到水平方向(\(G_x\))和垂直方向(\(G_y\))的一阶导数。通过这两个图像,可以计算每个像素的边缘梯度和方向:
梯度方向始终垂直于边缘,并被量化为代表垂直、水平及两个对角线方向的四个角度之一。
- 非极大值抑制
获得梯度幅值和方向后,对图像进行全扫描以去除可能不构成边缘的杂散像素。具体操作是:检查每个像素在其梯度方向邻域内是否为局部最大值。参考下图:
图中点A位于垂直方向的边缘上,梯度方向与边缘垂直。点B和C位于梯度方向上。通过比较点A与B、C的梯度值,若点A是局部最大值则保留,否则被抑制(置零)。最终得到的是具有"细边缘"的二值图像。
- 滞后阈值处理
此阶段用于判别真实边缘。需要设置两个阈值minVal和maxVal:
- 梯度值大于maxVal的像素确定为边缘
- 低于minVal的像素直接舍弃
- 介于两者之间的像素,若与"确定边缘"像素相连则保留,否则舍弃。见下图:
边缘A超过maxVal被判定为"确定边缘"。边缘C虽低于maxVal,但由于连接边缘A而被保留,从而获得完整曲线。而边缘B虽高于minVal且与C同区域,但因未连接任何"确定边缘"被舍弃。因此合理选择minVal和maxVal至关重要。此阶段还能基于"边缘是长线"的假设去除小像素噪声。
最终我们得到的是图像中清晰的强边缘轮廓。
OpenCV 中的 Canny 边缘检测
OpenCV 将所有上述功能整合到单一函数 cv.Canny()
中。下面介绍其使用方法:
第一个参数是输入图像。第二和第三个参数分别对应 minVal 和 maxVal。第四个参数 aperture_size 是用于计算图像梯度的 Sobel 核尺寸,默认值为 3。最后一个参数 L2gradient 用于指定梯度幅值的计算方式:若设为 True 则采用上文提到的更精确公式;若为 False 则使用以下函数:
Edge_Gradient(G)=∣Gx∣+∣Gy∣)Edge\_Gradient(G) = |G_x| + |G_y| )Edge_Gradient(G)=∣Gx∣+∣Gy∣)
默认值为 False。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as pltimg = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
edges = cv.Canny(img,100,200)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()
查看下方结果:
其他资源
1、维基百科上的 Canny边缘检测器
2、Bill Green 2002年编写的 Canny边缘检测教程
练习
1、编写一个小型应用程序,通过两个滑动条调整阈值参数来实现Canny边缘检测。这样可以帮助你理解阈值参数的影响效果。
由doxygen 1.12.0生成于2025年4月30日星期三23:08:42,用于OpenCV
图像金字塔
https://docs.opencv.org/4.x/dc/dff/tutorial_py_pyramids.html
目标
在本章中,
- 我们将学习图像金字塔
- 使用图像金字塔技术创建一种新水果"Orapple"
- 了解以下函数:
cv.pyrUp()
、cv.pyrDown()
理论基础
通常情况下,我们习惯于处理固定尺寸的图像。但在某些场景下,需要以不同分辨率处理(同一张)图像。例如在图像中搜索特定目标(如人脸)时,我们无法预知目标在图像中的具体尺寸。此时,就需要生成同一图像的多分辨率版本,并在所有版本中进行目标搜索。这种由不同分辨率图像组成的集合称为图像金字塔(当把这些图像按分辨率从高到低堆叠排列时,其形状类似金字塔)。
图像金字塔主要分为两类:1) 高斯金字塔 和 2) 拉普拉斯金字塔
在高斯金字塔中,较高层级(低分辨率)的图像是通过移除较低层级(高分辨率)图像中连续的行和列形成的。然后,较高层级中的每个像素值由底层5个像素通过高斯加权计算得到。通过这种方式,一个\(M \times N\)的图像会降采样为\(M/2 \times N/2\),面积缩减为原始图像的四分之一,这个过程称为一个"八度"(Octave)。随着金字塔层级的上升(即分辨率降低),这个模式会持续进行。反之在图像扩展时,每个层级的面积会变为原来的四倍。我们可以使用 cv.pyrDown()
和 cv.pyrUp()
函数来构建高斯金字塔。
img = cv.imread('messi5.jpg')
assert img is not None, "file could not be read, check with os.path.exists()"
lower_reso = cv.pyrDown(higher_reso)
现在你可以使用 cv.pyrUp()
函数来下采样图像金字塔。
higher_reso2 = cv.pyrUp(lower_reso)
与原图进行对比:
拉普拉斯金字塔(Laplacian Pyramids)由高斯金字塔生成,OpenCV并未提供专门函数。拉普拉斯金字塔图像呈现边缘特征,其大部分元素值为零,常用于图像压缩。每一层拉普拉斯金字塔由对应层高斯金字塔与其上层扩展版本的差值构成。下图展示了经对比度调整后的三层拉普拉斯金字塔效果(便于观察内容):
使用金字塔进行图像融合
金字塔的一个应用是图像融合。例如,在图像拼接时,你需要将两张图像叠加在一起,但由于图像间的不连续性,效果可能不理想。这种情况下,使用金字塔进行图像融合可以实现无缝过渡,同时保留图像中的大部分数据。
一个经典的例子是橙子和苹果的图像融合。现在看看效果,就能理解我的意思:
请查阅附加资源中的第一个参考,那里有关于图像融合、拉普拉斯金字塔等的完整图示说明。简单来说,步骤如下:
- 加载苹果和橙子的两张图像
- 为苹果和橙子构建高斯金字塔(本例中金字塔层数为6)
- 从高斯金字塔中获取它们的拉普拉斯金字塔
- 在拉普拉斯金字塔的每一层,将苹果的左半部分和橙子的右半部分拼接
- 最后从这个拼接后的图像金字塔重建原始图像
以下是完整代码。(为简单起见,每个步骤分开执行,可能会占用更多内存。如需优化可自行调整)
import cv2 as cv
import numpy as np,sysA = cv.imread('apple.jpg')
B = cv.imread('orange.jpg')
assert A is not None, "file could not be read, check with os.path.exists()"
assert B is not None, "file could not be read, check with os.path.exists()"# generate Gaussian pyramid for A
G = A.copy()
gpA = [G]
for i in range(6):G = cv.pyrDown(G)gpA.append(G)# generate Gaussian pyramid for B
G = B.copy()
gpB = [G]
for i in range(6):G = cv.pyrDown(G)gpB.append(G)# generate Laplacian Pyramid for A
lpA = [gpA[5]]
for i in range(5,0,-1):GE =cv.pyrUp(gpA[i])L = cv.subtract(gpA[i-1],GE)lpA.append(L)# generate Laplacian Pyramid for B
lpB = [gpB[5]]
for i in range(5,0,-1):GE =cv.pyrUp(gpB[i])L = cv.subtract(gpB[i-1],GE)lpB.append(L)# Now add left and right halves of images in each level
LS = []
for la,lb in zip(lpA,lpB):rows,cols,dpt = la.shapels = np.hstack((la[:,0:cols//2], lb[:,cols//2:]))LS.append(ls)# now reconstruct
ls_ = LS[0]
for i in range(1,6):ls_ =cv.pyrUp(ls_)ls_ = cv.add(ls_, LS[i])# image with direct connecting each half
real = np.hstack((A[:,:cols//2],B[:,cols//2:])) cv.imwrite('Pyramid_blending2.jpg',ls_)
cv.imwrite('Direct_blending.jpg',real)
附加资源
1、图像融合
生成于 2025年4月30日 星期三 23:08:42,由 doxygen 1.12.0 为 OpenCV 创建
OpenCV 中的轮廓
https://docs.opencv.org/4.x/d3/d05/tutorial_py_table_of_contents_contours.html
- 轮廓:入门指南
学习如何查找和绘制轮廓
- 轮廓特征
学习轮廓的不同特征,如面积、周长、外接矩形等
- 轮廓属性
学习轮廓的不同属性,如坚实度、平均强度等
- 轮廓:更多功能
学习如何查找凸性缺陷、点多边形测试、匹配不同形状等
- 轮廓层级
了解轮廓层级结构
生成于 2025年4月30日 星期三 23:08:42,由 doxygen 1.12.0 为 OpenCV 生成
OpenCV中的直方图
https://docs.opencv.org/4.x/de/db2/tutorial_py_table_of_contents_histograms.html
- 直方图 - 1:查找、绘制、分析!!!
学习直方图的基础知识
- 直方图 - 2:直方图均衡化
学习如何通过直方图均衡化来提升图像的对比度
- 直方图 - 3:二维直方图
学习如何查找并绘制二维直方图
- 直方图 - 4:直方图反向投影
学习使用直方图反向投影技术来分割彩色物体
生成于 2025年4月30日 星期三 23:08:42,适用于 OpenCV,由 doxygen 1.12.0 生成
OpenCV 中的图像变换
https://docs.opencv.org/4.x/dd/dc4/tutorial_py_table_of_contents_transforms.html
- 傅里叶变换` 学习如何获取图像的傅里叶变换
生成于 2025年4月30日 星期三 23:08:43 由 doxygen 1.12.0 为 OpenCV 生成
模板匹配
https://docs.opencv.org/4.x/d4/dc6/tutorial_py_template_matching.html
目标
在本章中,你将学习:
- 如何使用模板匹配在图像中查找对象
- 了解以下函数:
cv.matchTemplate()
cv.minMaxLoc()
(注:保留所有代码函数名及链接原格式,仅翻译描述性文本部分)
理论
模板匹配是一种在较大图像中搜索并定位模板图像位置的方法。OpenCV为此提供了**cv.matchTemplate()
**函数。该函数的工作原理是:将模板图像在输入图像上滑动(类似于二维卷积操作),并比较模板图像与输入图像对应区域的相似度。OpenCV实现了多种比较方法(具体细节可查阅文档)。函数会返回一个灰度图像,其中每个像素值表示该像素邻域与模板的匹配程度。
若输入图像的尺寸为(WxH),模板图像的尺寸为(wxh),则输出图像的尺寸将为(W-w+1, H-h+1)。获取结果后,可通过**cv.minMaxLoc()
**函数定位最大值/最小值的位置。将该位置作为矩形的左上角坐标,并以(w,h)作为矩形的宽高,此矩形即为模板匹配的目标区域。
注意:若使用cv.TM_SQDIFF
作为比较方法,则最小值表示最佳匹配。
OpenCV 中的模板匹配
这里我们以在梅西照片中搜索他的脸部为例。我创建了如下模板:
我们将尝试所有比较方法,以便观察它们的结果差异:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as pltimg = cv.imread('messi5.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
img2 = img.copy()
template = cv.imread('template.jpg', cv.IMREAD_GRAYSCALE)
assert template is not None, "file could not be read, check with os.path.exists()"
w, h = template.shape[::-1]# All the 6 methods for comparison in a list
methods = ['TM_CCOEFF', 'TM_CCOEFF_NORMED', 'TM_CCORR','TM_CCORR_NORMED', 'TM_SQDIFF', 'TM_SQDIFF_NORMED']for meth in methods:img = img2.copy()method = getattr(cv, meth)# Apply template Matchingres = cv.matchTemplate(img,template,method)min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)# If the method is TM_SQDIFF or TM_SQDIFF_NORMED, take minimumif method in [cv.TM_SQDIFF, cv.TM_SQDIFF_NORMED]:top_left = min_locelse:top_left = max_locbottom_right = (top_left[0] + w, top_left[1] + h)cv.rectangle(img,top_left, bottom_right, 255, 2)plt.subplot(121),plt.imshow(res,cmap = 'gray')plt.title('Matching Result'), plt.xticks([]), plt.yticks([])plt.subplot(122),plt.imshow(img,cmap = 'gray')plt.title('Detected Point'), plt.xticks([]), plt.yticks([])plt.suptitle(meth)plt.show()
查看以下结果:
- cv.TM_CCOEFF`
- cv.TM_CCOEFF_NORMED`
- cv.TM_CCORR`
- cv.TM_CCORR_NORMED`
- cv.TM_SQDIFF`
- cv.TM_SQDIFF_NORMED`
可以看到,使用 cv.TM_CCORR
得到的结果并不如预期理想。
多目标模板匹配
在前一节中,我们在图像中搜索梅西的面部(该图像中仅出现一次)。假设您要搜索具有多个出现位置的目标对象,cv.minMaxLoc()
将无法提供所有位置信息。这种情况下,我们需要使用阈值处理技术。在本示例中,我们将使用著名游戏 Mario 的截图,并尝试找出其中的金币。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as pltimg_rgb = cv.imread('mario.png')
assert img_rgb is not None, "file could not be read, check with os.path.exists()"
img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY)
template = cv.imread('mario_coin.png', cv.IMREAD_GRAYSCALE)
assert template is not None, "file could not be read, check with os.path.exists()"
w, h = template.shape[::-1]res = cv.matchTemplate(img_gray,template,cv.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where( res >= threshold)
for pt in zip(*loc[::-1]):cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)cv.imwrite('res.png',img_rgb)
结果:
生成于2025年4月30日 星期三 23:08:42,使用doxygen 1.12.0为OpenCV生成
霍夫线变换
https://docs.opencv.org/4.x/d6/d10/tutorial_py_houghlines.html
目标
在本章中,
- 我们将理解霍夫变换的概念
- 学习如何使用它来检测图像中的直线
- 了解以下函数:
cv.HoughLines()
、cv.HoughLinesP()
理论
霍夫变换是一种流行的形状检测技术,只要能用数学形式表示该形状即可使用。即使形状有断裂或轻微变形,它也能检测出来。我们将以直线为例说明其工作原理。
直线可以用方程 \(y = mx+c\) 表示,也可以用参数形式 \(\rho = x \cos \theta + y \sin \theta\) 表示。其中,\(\rho\) 是原点到直线的垂直距离,\(\theta\) 是该垂直线与水平轴按逆时针方向测量的夹角(具体方向取决于坐标系表示方式,OpenCV 采用此表示法)。如下图所示:
若直线穿过原点下方,则 \(\rho\) 为正且角度小于 180 度;若直线穿过原点上方,则角度仍取小于 180 度的值,但 \(\rho\) 为负。垂直线的角度为 0 度,水平线为 90 度。
霍夫变换检测直线的过程如下:每条直线都可由参数对 \((\rho, \theta)\) 表示。算法首先创建一个初始值为 0 的二维数组(累加器)来存储这两个参数。数组的行表示 \(\rho\),列表示 \(\theta\)。数组大小取决于精度需求:若角度精度为 1 度,则需要 180 列;\(\rho\) 的最大可能值为图像对角线长度,因此行数可取图像对角线长度(以像素为单位)。
以 100x100 图像中部的水平线为例:
- 取直线上第一个点,已知其 (x,y) 坐标。将 \(\theta = 0,1,2,…,180\) 代入直线方程计算对应的 \(\rho\),并在累加器的 \((\rho, \theta)\) 对应单元格中累加 1。此时单元格 (50,90) 的值变为 1。
- 对直线上第二个点重复上述过程,单元格 (50,90) 的值变为 2。此过程本质是对 \((\rho, \theta)\) 进行投票。
- 对直线上所有点处理后,单元格 (50,90) 将获得最多投票数。搜索累加器中最大值即可确定图像中存在一条距原点 50 像素、角度 90 度的直线。下图动画直观展示了该过程(图像来源:Amos Storkey):
这就是霍夫变换检测直线的原理。其实现简单,读者甚至可用 NumPy 自行实现。下图展示了累加器效果,亮点位置代表图像中可能直线的参数(图像来源:维基百科):
OpenCV中的霍夫变换
上述所有解释内容都被封装在OpenCV函数 cv.HoughLines()
中。
该函数直接返回一个 :math:(ρ, θ) 值数组。其中 \(ρ\) 以像素为单位,\(θ\) 以弧度为单位。第一个参数输入图像必须是二值图像,因此在应用霍夫变换前需要先进行阈值处理或使用Canny边缘检测。第二和第三个参数分别是 \(ρ\) 和 \(θ\) 的精度。第四个参数是阈值,表示检测为直线所需的最小投票数。请注意,投票数取决于直线上的点数,因此这个阈值实际上代表应该被检测到的最小直线长度。
import cv2 as cv
import numpy as npimg = cv.imread('sudoku.png'))
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray,50,150,apertureSize = 3)lines = cv.HoughLines(edges,1,np.pi/180,200)
for line in lines:rho,theta = line[0]a = np.cos(theta)b = np.sin(theta)x0 = a*rhoy0 = b*rhox1 = 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,0,255),2)cv.imwrite('houghlines3.jpg',img)
检查以下结果:
概率霍夫变换
在霍夫变换中,即使对于只有两个参数的直线检测,也需要大量计算。概率霍夫变换是对传统霍夫变换的优化改进。该方法不会考虑所有点,而是仅随机选取足以进行直线检测的点子集。我们只需适当降低阈值即可。下图展示了霍夫空间中的传统霍夫变换与概率霍夫变换对比效果。(图片来源:Franck Bettinger的主页)
OpenCV的实现基于Matas, J.、Galambos, C.和Kittler, J.V.提出的《使用渐进式概率霍夫变换的鲁棒直线检测》论文[186]。使用的函数是**cv.HoughLinesP()
**,该函数新增了两个参数:
- minLineLength - 线段最小长度,短于此值的线段将被舍弃
- maxLineGap - 允许将间断线段视为同一直线的最大间距
最大的优势在于该函数直接返回线段的两个端点坐标。而传统方法仅能获取直线参数,还需自行计算所有对应点。新方法使整个过程变得直观而简单。
import cv2 as cv
import numpy as npimg = cv.imread('sudoku.png'))
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray,50,150,apertureSize = 3)
lines = cv.HoughLinesP(edges,1,np.pi/180,100,minLineLength=100,maxLineGap=10)
for line in lines:x1,y1,x2,y2 = line[0]cv.line(img,(x1,y1),(x2,y2),(0,255,0),2)cv.imwrite('houghlines5.jpg',img)
查看下方结果:
附加资源
1、维基百科上的霍夫变换
由 doxygen 1.12.0 生成于 2025年4月30日 星期三 23:08:42,适用于 OpenCV
霍夫圆变换
https://docs.opencv.org/4.x/da/d53/tutorial_py_houghcircles.html
目标
在本章中,
- 我们将学习使用霍夫变换在图像中检测圆形。
- 将了解以下函数:
cv.HoughCircles()
理论
圆的数学表达式为 \((x-x_{center})^2 + (y - y_{center})^2 = r^2\),其中 \((x_{center},y_{center})\) 表示圆心坐标,\(r\) 表示圆的半径。从方程可以看出有三个参数,因此霍夫变换需要一个三维累加器,这会非常低效。为此,OpenCV 采用了一种更巧妙的方法——霍夫梯度法,该方法利用了边缘的梯度信息。
这里我们使用的函数是 cv.HoughCircles()
。该函数包含多个参数,文档中已有详细说明。下面我们直接进入代码部分。
import numpy as np
import cv2 as cvimg = cv.imread('opencv-logo-white.png', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
img = cv.medianBlur(img,5)
cimg = cv.cvtColor(img,cv.COLOR_GRAY2BGR)circles = cv.HoughCircles(img,cv.HOUGH_GRADIENT,1,20, param1=50,param2=30,minRadius=0,maxRadius=0)circles = np.uint16(np.around(circles))
for i in circles[0,:]:# draw the outer circlecv.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)# draw the center of the circlecv.circle(cimg,(i[0],i[1]),2,(0,0,255),3)cv.imshow('detected circles',cimg)
cv.waitKey((0)
cv.destroyAllWindows()
结果如下所示:
生成于 2025年4月30日 星期三 23:08:42,由 doxygen 1.12.0 为 OpenCV 生成
使用分水岭算法进行图像分割
https://docs.opencv.org/4.x/d3/db4/tutorial_py_watershed.html
目标
在本章中,
- 我们将学习使用基于标记的分水岭算法进行图像分割
- 我们将了解:
cv.watershed()
理论基础
任何灰度图像都可以视为一个地形表面,其中高亮度区域代表山峰和丘陵,低亮度区域代表山谷。想象你开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水位上升,根据附近山峰(梯度)的不同,来自不同山谷的水(显然颜色不同)将开始合并。为了防止这种情况,你需要在水的汇合处建造屏障。持续进行注水和建造屏障的过程,直到所有山峰都被水淹没。此时,你所创建的屏障就形成了最终的分割结果。这就是分水岭算法背后的"哲学思想"。你可以访问CMM网页上的分水岭专题,通过动画演示加深理解。
然而,由于图像中的噪声或其他不规则因素,这种方法会产生过度分割的结果。因此OpenCV实现了一种基于标记的分水岭算法,允许你指定哪些山谷点需要合并、哪些不需要。这是一种交互式图像分割技术。具体操作是:为我们已知的目标对象分配不同标签——用某种颜色(或亮度值)标记确定属于前景/对象的区域,用另一种颜色标记确定属于背景/非对象的区域,最后用0标记不确定区域。这些标记就是我们的初始信息。应用分水岭算法后,算法会根据我们提供的标签更新标记结果,而对象边界会被赋予-1值。
代码
下面我们将通过一个示例来展示如何结合距离变换与分水岭算法分割相互接触的物体。
以这张硬币图像为例,可以看到硬币之间相互接触。即使进行阈值处理,它们仍会保持接触状态。
我们首先需要获取硬币的近似估计区域。为此,可以采用Otsu二值化方法。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as pltimg = cv.imread('coins.png')
assert img is not None, "file could not be read, check with os.path.exists()"
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
结果:
现在我们需要去除图像中的细小白色噪点。为此可以使用形态学开运算。而要消除物体内部的小孔洞,则可以采用形态学闭运算。至此,我们可以确定:靠近物体中心的区域是前景,远离物体的区域是背景。唯一不确定的是硬币边缘的边界区域。
接下来需要提取确定属于硬币的区域。腐蚀操作会去除边界像素,因此保留下来的区域必定是硬币。这种方法在物体互不接触时有效,但由于当前硬币存在接触情况,更好的方案是通过距离变换并设置合适阈值。接着需要确定绝对不属于硬币的区域,这里对结果进行膨胀处理。膨胀操作会将物体边界扩展到背景区域,从而确保结果中所有背景区域都是真实的背景(因为边界区域已被移除)。
参见下图:
剩余区域是我们无法确定属于硬币还是背景的部分,这正是分水岭算法需要处理的区域。这些区域通常位于硬币边界处,即前景与背景交汇的位置(或不同硬币的交界处),我们称之为边界区域。该区域可通过从确定背景区域(sure_bg)中减去确定前景区域(sure_fg)获得。
# noise removal
kernel = np.ones((3,3),np.uint8)
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)# sure background area
sure_bg = cv.dilate(opening,kernel,iterations=3)# Finding sure foreground area
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)# Finding unknown region
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg,sure_fg)
观察结果。在阈值化后的图像中,我们获得了若干明确属于硬币且已分离的区域。(某些情况下,您可能仅关注前景分割而非分离相互接触的物体。此时无需使用距离变换,仅用腐蚀操作即可。腐蚀只是另一种提取确定前景区域的方法。)
现在我们已经明确识别出硬币区域、背景区域等。因此我们创建一个标记数组(该数组与原始图像尺寸相同,但采用int32数据类型)并对其中区域进行标注。已确认的区域(无论前景或背景)会被标记为不同的正整数,而不确定区域则保持为零值。这里我们使用 cv.connectedComponents()
,该函数会将图像背景标记为0,其他物体则从1开始依次标记。
但需注意:若背景被标记为0,分水岭算法会将其视为未知区域。因此我们需要用不同整数值标记背景,而将真正的不确定区域(由unknown定义)标记为0。
# Marker labelling
ret, markers = cv.connectedComponents(sure_fg)# Add one to all labels so that sure background is not 0, but 1
markers = markers+1# Now, mark the region of unknown with zero
markers[unknown==255] = 0
查看下方结果。对于部分硬币,它们接触的区域被正确分割,而另一些则未能实现。
其他资源
1、关于分水岭变换的CMM页面
练习
1、OpenCV示例中提供了一个关于分水岭分割的交互式示例watershed.py。运行它,体验它,然后学习它。
本文档由doxygen 1.12.0生成于2025年4月30日周三23:08:43,针对OpenCV项目
使用GrabCut算法进行交互式前景提取
https://docs.opencv.org/4.x/d8/d83/tutorial_py_grabcut.html
目标
在本章中
- 我们将学习使用GrabCut算法提取图像中的前景
- 我们将为此创建一个交互式应用程序
理论基础
GrabCut算法由微软研究院剑桥分院的Carsten Rother、Vladimir Kolmogorov和Andrew Blake联合提出,相关论文为"GrabCut": interactive foreground extraction using iterated graph cuts。该算法的设计初衷是实现用户交互最少的前景提取方案。
从用户视角看,算法工作流程如下:首先用户在目标前景区域外围绘制矩形框(需确保前景完全位于矩形内部)。随后算法通过迭代分割获得最优结果。但某些情况下分割效果可能不理想,例如将部分前景误判为背景或反之。此时用户可通过精细修正——在图像错误区域绘制笔触:白色笔触表示"此处应为前景",黑色笔触则表示"此处应为背景"。通过这种交互指导,算法在下次迭代中会生成更准确的结果。
如下图所示:先用蓝色矩形框选球员和足球,随后通过白色(前景标记)和黑色(背景标记)笔触进行修正,最终获得理想分割效果。
后台处理流程解析:
- 用户输入矩形框后,框外区域均视为确定背景(因此需确保矩形包含所有目标物体),框内区域初始状态为待定。用户后续标注的前景/背景区域将作为硬标签,在迭代过程中保持不变。
- 系统根据输入数据进行初始标记,确定前景与背景像素(硬标签)。
- 采用高斯混合模型(GMM)分别建模前景和背景的像素分布。
- GMM根据标注数据学习并建立新的像素分布模型,基于颜色统计特征将待定像素归类为可能前景或可能背景(类似聚类过程)。
- 构建像素分布图:图中节点代表像素,额外添加源节点和汇节点。所有前景像素连接源节点,背景像素连接汇节点。
- 像素与源/汇节点间边的权重由像素属于前景/背景的概率决定;像素间边的权重取决于边缘信息或像素相似度——颜色差异越大则边权重越低。
- 应用最小割算法分割图结构:以最小代价函数将图切割为分离源节点和汇节点的两部分,代价函数即所有被切割边的权重总和。切割后,连接源节点的像素归为前景,连接汇节点的归为背景。
- 重复上述过程直至分类结果收敛。
下图直观展示了该流程(图像来源:http://www.cs.ru.ac.za/research/g02m1682/)
演示
现在我们来学习使用OpenCV实现GrabCut算法。OpenCV提供了**cv.grabCut()
**函数,首先了解其参数:
- img - 输入图像
- mask - 掩码图像,用于指定哪些区域属于背景、前景或可能的背景/前景等。通过以下标志实现:
cv.GC_BGD
、cv.GC_FGD
、cv.GC_PR_BGD
、cv.GC_PR_FGD
,或直接在图像中用0、1、2、3表示 - rect - 包含前景对象的矩形坐标,格式为(x,y,w,h)
- bdgModel、fgdModel - 算法内部使用的数组,创建两个(1,65)大小的np.float64类型零数组即可
- iterCount - 算法运行的迭代次数
- mode - 可选**
cv.GC_INIT_WITH_RECT
或cv.GC_INIT_WITH_MASK
**或组合模式,决定使用矩形初始化还是掩码微调
首先演示矩形模式。我们加载图像并创建相似的掩码图像,初始化fgdModel和bgdModel数组,设置矩形参数。这些步骤都很直观。让算法运行5次迭代,由于使用矩形初始化,模式应设为**cv.GC_INIT_WITH_RECT
**。
运行grabcut后,算法会修改掩码图像。在新掩码中,像素将被标记为上述四种背景/前景标志。我们将所有0像素和2像素设为0(背景),所有1像素和3像素设为1(前景),最终掩码就准备好了。只需将其与输入图像相乘,即可得到分割结果。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as pltimg = cv.imread('messi5.jpg')
assert img is not None, "file could not be read, check with os.path.exists()"
mask = np.zeros(img.shape[:2],np.uint8)bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)rect = (50,50,450,290)
cv.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv.GC_INIT_WITH_RECT)mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]plt.imshow(img),plt.colorbar(),plt.show()
查看以下结果:
糟糕,梅西的头发不见了。谁喜欢没有头发的梅西呢? 我们需要把它恢复回来。因此,我们将用1像素(确定前景)进行精细修饰。同时,画面中出现了一些我们不想要的地面部分和标志。我们需要移除它们。为此,我们用0像素(确定背景)进行修饰。于是,我们按照上述方式修改了之前得到的掩膜结果。
实际上,我做了以下操作:在绘图应用中打开输入图像,并添加一个新图层。使用画笔工具,在新图层上用白色标记遗漏的前景(如头发、鞋子、球等),用黑色标记不需要的背景(如标志、地面等)。然后用灰色填充剩余的背景区域。接着在OpenCV中加载这个掩膜图像,并根据新添加的掩膜值编辑原始掩膜图像。查看以下代码:
# newmask is the mask image I manually labelled
newmask = cv.imread('newmask.png', cv.IMREAD_GRAYSCALE)
assert newmask is not None, "file could not be read, check with os.path.exists()"# wherever it is marked white (sure foreground), change mask=1
# wherever it is marked black (sure background), change mask=0
mask[newmask == 0] = 0
mask[newmask == 255] = 1mask, bgdModel, fgdModel = cv.grabCut(img,mask,None,bgdModel,fgdModel,5,cv.GC_INIT_WITH_MASK)mask = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()
查看下方结果:
就是这样。在这里,你无需以矩形模式初始化,可以直接进入掩码模式。只需在掩码图像中用2像素或3像素(可能背景/前景)标记矩形区域,然后像第二个示例那样用1像素标记确定前景。最后直接以掩码模式应用grabCut函数即可。
练习
1、OpenCV示例中包含一个grabcut.py示例,这是一个使用GrabCut算法的交互式工具。请查看该示例。同时观看这个YouTube视频了解如何使用它。
2、你可以将其改造成一个交互式示例:通过鼠标绘制矩形和描边,创建轨迹条来调整描边宽度等。
生成于2025年4月30日 星期三 23:08:42,由doxygen 1.12.0为OpenCV生成