Python 机器视觉入门:第6-10章完整学习指南
Python 机器视觉入门:第6-10章完整学习指南
欢迎继续Python机器视觉的学习之旅!在前5章中,我们已经掌握了Python基础、NumPy数组操作和Matplotlib可视化技能。现在,让我们正式进入OpenCV的世界,学习图像处理的核心技术。本章内容将带你从零开始掌握OpenCV的基础操作,为后续项目打下坚实基础。
第6章:OpenCV安装与图像基础操作
6.1 OpenCV的安装与环境配置
重点:OpenCV-Python是OpenCV的Python接口,安装非常简单。
# 使用pip安装OpenCV核心库
pip install opencv-python
# 如果需要扩展模块(如特征提取等),可以安装
pip install opencv-contrib-python
安装后验证:
import cv2
print(cv2.__version__) # 输出OpenCV版本号,例如 4.8.0
6.2 图像的读取、显示与保存
这是OpenCV最基础的操作,必须熟练掌握。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 1. 读取图像
# cv2.imread(文件名, 读取方式)
# 读取方式:cv2.IMREAD_COLOR(1) 彩色图(默认),cv2.IMREAD_GRAYSCALE(0) 灰度图,cv2.IMREAD_UNCHANGED(-1) 包含Alpha通道
img_color = cv2.imread('test.jpg', cv2.IMREAD_COLOR)
img_gray = cv2.imread('test.jpg', cv2.IMREAD_GRAYSCALE)
# 2. 检查图像是否读取成功
if img_color is None:
print("图像读取失败,请检查文件路径")
exit()
# 3. 显示图像
cv2.imshow('Color Image', img_color) # 窗口名称为'Color Image'
cv2.imshow('Gray Image', img_gray)
# 等待按键,参数为等待毫秒数,0表示无限等待
key = cv2.waitKey(0)
if key == ord('s'): # 如果按下's'键,保存图像
cv2.imwrite('saved_image.jpg', img_color)
# 关闭所有窗口
cv2.destroyAllWindows()
# 4. 使用matplotlib显示(注意颜色问题)
# OpenCV读取的图像是BGR格式,matplotlib需要RGB格式
img_rgb = cv2.cvtColor(img_color, cv2.COLOR_BGR2RGB)
plt.subplot(1, 2, 1)
plt.imshow(img_rgb)
plt.title('RGB Image')
plt.subplot(1, 2, 2)
plt.imshow(img_gray, cmap='gray')
plt.title('Gray Image')
plt.show()
重点:
cv2.imread()读取图像,返回NumPy数组cv2.imshow()显示图像,cv2.waitKey()等待键盘输入,cv2.destroyAllWindows()关闭窗口- OpenCV默认使用BGR颜色格式,而matplotlib使用RGB,显示彩色图像时需要转换
6.3 图像的基本属性
# 获取图像属性
print(f"图像形状: {img_color.shape}") # (高度, 宽度, 通道数)
print(f"图像大小: {img_color.size}") # 总像素数 = 高 * 宽 * 通道数
print(f"数据类型: {img_color.dtype}") # uint8 (0-255)
print(f"宽度: {img_color.shape[1]}") # 列数
print(f"高度: {img_color.shape[0]}") # 行数
print(f"通道数: {img_color.shape[2]}") # 3(彩色)
# 灰度图属性
print(f"灰度图形状: {img_gray.shape}") # (高度, 宽度) - 单通道
print(f"灰度图通道数: {img_gray.ndim}") # 2(二维数组)
重点:图像在OpenCV中就是NumPy数组,形状为 (height, width, channels),数据类型通常是 uint8(0-255)。
6.4 像素访问与修改
# 访问单个像素
img = cv2.imread('test.jpg')
px = img[100, 200] # 第100行,第200列的像素,返回[B,G,R]数组
print(f"坐标(100,200)的像素值: {px}") # 例如 [128 64 255]
# 访问单个通道值
blue_value = img[100, 200, 0] # 通道0是蓝色
green_value = img[100, 200, 1] # 通道1是绿色
red_value = img[100, 200, 2] # 通道2是红色
# 修改像素
img[100, 200] = [255, 255, 255] # 设置为白色
# 使用NumPy操作更高效
roi = img[50:150, 100:200] # 获取感兴趣区域(50-150行,100-200列)
img[200:300, 300:400] = [0, 0, 255] # 将区域设置为红色
重点:像素坐标顺序是 [行, 列],通道顺序是 [B, G, R]。
6.5 色彩空间转换
# 读取图像
img = cv2.imread('test.jpg')
# BGR转灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# BGR转RGB(用于matplotlib显示)
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# BGR转HSV(色调、饱和度、明度)——常用于颜色分割
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# BGR转LAB
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
# 显示不同色彩空间
cv2.imshow('HSV Image', hsv)
cv2.waitKey(0)
cv2.destroyAllWindows()
重点:cv2.cvtColor() 是色彩空间转换函数,HSV空间对颜色分割非常有用,因为它将颜色信息(Hue)与亮度(Value)分离。
6.6 图像ROI(感兴趣区域)操作
# 选取ROI
img = cv2.imread('test.jpg')
roi = img[100:300, 200:400] # 选取行100-300,列200-400的区域
# 复制ROI到另一个位置
img[50:250, 400:600] = roi # 将ROI复制到新位置
# 显示ROI
cv2.imshow('ROI', roi)
cv2.waitKey(0)
cv2.destroyAllWindows()
重点:ROI操作通过NumPy切片实现,非常高效,常用于目标提取和复制。
6.7 通道拆分与合并
# 拆分通道
b, g, r = cv2.split(img) # 返回三个单通道图像
# 或者使用NumPy索引
b = img[:, :, 0]
g = img[:, :, 1]
r = img[:, :, 2]
# 合并通道
merged = cv2.merge([b, g, r])
# 创建单通道图像(例如只保留红色通道)
red_img = np.zeros_like(img) # 创建全黑图像
red_img[:, :, 2] = r # 只填充红色通道
# 显示各通道
cv2.imshow('Blue Channel', b)
cv2.imshow('Green Channel', g)
cv2.imshow('Red Channel', r)
cv2.waitKey(0)
cv2.destroyAllWindows()
重点:cv2.split() 和 cv2.merge() 用于通道操作,但NumPy索引通常更快。
6.8 综合案例:图像基础操作实战
import cv2
import numpy as np
# 创建一个简单的图像(300x300的彩色图)
img = np.zeros((300, 300, 3), dtype=np.uint8)
# 绘制一些基本形状
# 画一条线(从(50,50)到(250,250),红色,线宽5)
cv2.line(img, (50, 50), (250, 250), (0, 0, 255), 5)
# 画一个矩形(左上角(50,100),右下角(200,250),绿色,线宽3)
cv2.rectangle(img, (50, 100), (200, 250), (0, 255, 0), 3)
# 画一个圆(中心(150,150),半径50,蓝色,线宽-1表示填充)
cv2.circle(img, (150, 150), 50, (255, 0, 0), -1)
# 添加文字
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(img, 'OpenCV', (50, 280), font, 1, (255, 255, 255), 2)
# 显示图像
cv2.imshow('Drawing', img)
# 获取鼠标点击位置的像素值
def mouse_callback(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
print(f"坐标({x}, {y})的像素值: {img[y, x]}") # 注意:坐标是(x,y),但NumPy索引是[y,x]
cv2.setMouseCallback('Drawing', mouse_callback)
cv2.waitKey(0)
cv2.destroyAllWindows()
第7章:图像预处理(滤波与增强)
图像预处理是机器视觉中至关重要的一步,目的是去除噪声、增强特征,为后续处理做准备。
7.1 图像的几何变换(缩放、旋转、翻转)
import cv2
import numpy as np
img = cv2.imread('test.jpg')
# 1. 图像缩放
# 指定目标大小
resized = cv2.resize(img, (300, 300))
# 按比例缩放(fx,fy为缩放因子)
resized_ratio = cv2.resize(img, None, fx=0.5, fy=0.5)
# 2. 图像旋转
(h, w) = img.shape[:2]
center = (w // 2, h // 2)
# 获取旋转矩阵(中心点,角度,缩放因子)
M = cv2.getRotationMatrix2D(center, 45, 1.0) # 旋转45度
rotated = cv2.warpAffine(img, M, (w, h))
# 3. 图像翻转
flip_h = cv2.flip(img, 1) # 水平翻转(左右)
flip_v = cv2.flip(img, 0) # 垂直翻转(上下)
flip_both = cv2.flip(img, -1) # 同时翻转
# 显示结果
cv2.imshow('Original', img)
cv2.imshow('Resized', resized)
cv2.imshow('Rotated', rotated)
cv2.imshow('Flip Horizontal', flip_h)
cv2.waitKey(0)
cv2.destroyAllWindows()
重点:cv2.resize()、cv2.getRotationMatrix2D() + cv2.warpAffine()、cv2.flip() 是常用的几何变换函数。
7.2 图像的算术运算
# 创建两个图像
img1 = cv2.imread('image1.jpg')
img2 = cv2.imread('image2.jpg')
# 确保两张图大小相同
img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
# 图像加法(直接加法可能导致溢出,但NumPy会取模)
add_direct = img1 + img2 # 可能导致数值环绕(255+1=0)
# OpenCV加法(饱和操作,255+1=255)
add_cv = cv2.add(img1, img2)
# 图像加权混合(α*img1 + β*img2 + γ)
blended = cv2.addWeighted(img1, 0.7, img2, 0.3, 0)
# 显示结果
cv2.imshow('Add (NumPy)', add_direct)
cv2.imshow('Add (OpenCV)', add_cv)
cv2.imshow('Blended', blended)
cv2.waitKey(0)
cv2.destroyAllWindows()
重点:cv2.add() 是饱和操作,而NumPy加法是模运算;cv2.addWeighted() 常用于图像融合。
7.3 图像阈值处理(二值化)
阈值处理将灰度图像转换为二值图像,是图像分割的基础。
# 读取灰度图像
img = cv2.imread('test.jpg', cv2.IMREAD_GRAYSCALE)
# 1. 简单阈值
# cv2.threshold(图像, 阈值, 最大值, 阈值类型)
# 阈值类型:THRESH_BINARY, THRESH_BINARY_INV, THRESH_TRUNC, THRESH_TOZERO, THRESH_TOZERO_INV
ret, thresh_binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
ret, thresh_binary_inv = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
# 2. 自适应阈值(根据局部区域计算阈值,适合光照不均的图像)
# cv2.adaptiveThreshold(图像, 最大值, 自适应方法, 阈值类型, 邻域大小, 常数C)
# 自适应方法:ADAPTIVE_THRESH_MEAN_C(邻域均值减去C),ADAPTIVE_THRESH_GAUSSIAN_C(邻域加权均值减去C)
adaptive_mean = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY, 11, 2)
adaptive_gaussian = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
# 3. Otsu阈值法(自动计算最佳阈值,适合双峰直方图图像)
ret_otsu, thresh_otsu = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"Otsu自动计算阈值: {ret_otsu}")
# 显示结果
titles = ['Original', 'Binary', 'Binary Inv', 'Adaptive Mean', 'Adaptive Gaussian', 'Otsu']
images = [img, thresh_binary, thresh_binary_inv, adaptive_mean, adaptive_gaussian, thresh_otsu]
for i in range(6):
plt.subplot(2, 3, i+1)
plt.imshow(images[i], cmap='gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
重点:
- 简单阈值需要手动设定阈值
- 自适应阈值适合光照不均的图像,根据局部区域计算阈值
- Otsu算法自动计算最优阈值,适用于直方图呈双峰分布的图像
7.4 图像滤波(平滑与去噪)
滤波是图像预处理的核心,用于去除噪声、平滑图像 。
7.4.1 均值滤波
# 均值滤波:用邻域像素的平均值代替中心像素
# cv2.blur(图像, 卷积核大小)
blur_3x3 = cv2.blur(img, (3, 3)) # 3x3卷积核
blur_5x5 = cv2.blur(img, (5, 5)) # 5x5卷积核(更大核,更模糊)
7.4.2 高斯滤波
# 高斯滤波:用加权平均值代替,中心像素权重最大
# cv2.GaussianBlur(图像, 卷积核大小, 标准差)
gaussian_3x3 = cv2.GaussianBlur(img, (3, 3), 1)
gaussian_5x5 = cv2.GaussianBlur(img, (5, 5), 1.5)
7.4.3 中值滤波
# 中值滤波:用邻域像素的中值代替,对椒盐噪声特别有效
# cv2.medianBlur(图像, 卷积核大小) - 核大小必须是奇数
median_3 = cv2.medianBlur(img, 3) # 3x3邻域
median_5 = cv2.medianBlur(img, 5) # 5x5邻域
7.4.4 双边滤波
# 双边滤波:保留边缘的同时去噪
# cv2.bilateralFilter(图像, 邻域直径, 颜色标准差, 空间标准差)
# 颜色标准差越大,颜色差异大的像素越容易被考虑;空间标准差越大,越远的像素越容易被考虑
bilateral = cv2.bilateralFilter(img, 9, 75, 75)
7.4.5 滤波效果对比
# 完整示例:添加噪声并比较滤波效果
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图像并添加噪声
img = cv2.imread('test.jpg', cv2.IMREAD_GRAYSCALE)
# 添加椒盐噪声
def add_salt_pepper_noise(image, prob):
output = image.copy()
h, w = image.shape
num_noise = int(prob * h * w)
# 随机添加白点(盐噪声)
coords = [np.random.randint(0, i-1, num_noise) for i in [h, w]]
output[coords[0], coords[1]] = 255
# 随机添加黑点(椒噪声)
coords = [np.random.randint(0, i-1, num_noise) for i in [h, w]]
output[coords[0], coords[1]] = 0
return output
# 添加高斯噪声
def add_gaussian_noise(image, mean=0, sigma=25):
noise = np.random.normal(mean, sigma, image.shape)
output = image + noise
return np.clip(output, 0, 255).astype(np.uint8)
# 生成带噪声的图像
img_sp = add_salt_pepper_noise(img, 0.02) # 2%椒盐噪声
img_gaussian = add_gaussian_noise(img, 0, 25) # 高斯噪声
# 应用各种滤波
# 对椒盐噪声图像滤波
mean_sp = cv2.blur(img_sp, (3, 3))
gaussian_sp = cv2.GaussianBlur(img_sp, (3, 3), 1)
median_sp = cv2.medianBlur(img_sp, 3)
# 对高斯噪声图像滤波
mean_gauss = cv2.blur(img_gaussian, (3, 3))
gaussian_gauss = cv2.GaussianBlur(img_gaussian, (3, 3), 1)
bilateral_gauss = cv2.bilateralFilter(img_gaussian, 9, 75, 75)
# 显示结果
plt.figure(figsize=(15, 10))
# 椒盐噪声滤波效果
plt.subplot(2, 4, 1)
plt.imshow(img_sp, cmap='gray')
plt.title('Salt & Pepper Noise')
plt.axis('off')
plt.subplot(2, 4, 2)
plt.imshow(mean_sp, cmap='gray')
plt.title('Mean Filter')
plt.axis('off')
plt.subplot(2, 4, 3)
plt.imshow(gaussian_sp, cmap='gray')
plt.title('Gaussian Filter')
plt.axis('off')
plt.subplot(2, 4, 4)
plt.imshow(median_sp, cmap='gray')
plt.title('Median Filter')
plt.axis('off')
# 高斯噪声滤波效果
plt.subplot(2, 4, 5)
plt.imshow(img_gaussian, cmap='gray')
plt.title('Gaussian Noise')
plt.axis('off')
plt.subplot(2, 4, 6)
plt.imshow(mean_gauss, cmap='gray')
plt.title('Mean Filter')
plt.axis('off')
plt.subplot(2, 4, 7)
plt.imshow(gaussian_gauss, cmap='gray')
plt.title('Gaussian Filter')
plt.axis('off')
plt.subplot(2, 4, 8)
plt.imshow(bilateral_gauss, cmap='gray')
plt.title('Bilateral Filter')
plt.axis('off')
plt.tight_layout()
plt.show()
重点:
- 均值滤波:简单但边缘模糊
- 高斯滤波:加权平均,更自然,但也会模糊边缘
- 中值滤波:对椒盐噪声效果最好,能保留边缘
- 双边滤波:去噪同时保留边缘,但计算量大
- 噪声类型识别很重要:椒盐噪声用中值滤波,高斯噪声用高斯滤波或双边滤波
7.5 形态学操作
形态学操作是基于图像形状的一系列操作,常用于二值图像处理 。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 创建一个简单的二值图像
img = np.zeros((300, 300), dtype=np.uint8)
cv2.rectangle(img, (50, 50), (150, 150), 255, -1) # 白色矩形
cv2.circle(img, (200, 200), 50, 255, -1) # 白色圆形
# 添加一些噪声点
img[20, 20] = 255
img[250, 250] = 255
# 定义结构元素(卷积核)
kernel = np.ones((5, 5), np.uint8)
# 1. 腐蚀:消除边界点,使白色区域缩小
erosion = cv2.erode(img, kernel, iterations=1)
# 2. 膨胀:扩大边界点,使白色区域扩大
dilation = cv2.dilate(img, kernel, iterations=1)
# 3. 开运算:先腐蚀后膨胀,用于去除小噪点
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
# 4. 闭运算:先膨胀后腐蚀,用于填充小孔
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
# 5. 形态学梯度:膨胀减腐蚀,得到轮廓
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
# 6. 顶帽运算:原图减开运算,提取亮区域中的小物体
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
# 7. 黑帽运算:闭运算减原图,提取暗区域中的小物体
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
# 显示结果
titles = ['Original', 'Erosion', 'Dilation', 'Opening', 'Closing', 'Gradient', 'Tophat', 'Blackhat']
images = [img, erosion, dilation, opening, closing, gradient, tophat, blackhat]
plt.figure(figsize=(15, 10))
for i in range(8):
plt.subplot(2, 4, i+1)
plt.imshow(images[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
重点:
- 腐蚀:去除边界点,分离相连物体
- 膨胀:连接相邻物体,填补空洞
- 开运算:先腐蚀后膨胀,去除小噪点
- 闭运算:先膨胀后腐蚀,填补小孔
- 结构元素(kernel)的形状和大小影响操作效果,常用矩形、十字形、椭圆形
第8章:边缘检测与特征提取
边缘是图像中像素值发生剧烈变化的位置,边缘检测是图像分割和目标识别的基础。
8.1 图像梯度(Sobel、Scharr、Laplacian)
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('test.jpg', cv2.IMREAD_GRAYSCALE)
# 1. Sobel算子(一阶导数)
# cv2.Sobel(图像, 数据类型, dx, dy, ksize)
# dx=1, dy=0 计算x方向梯度(垂直边缘)
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
# dx=0, dy=1 计算y方向梯度(水平边缘)
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
# 转换为uint8类型(取绝对值)
sobel_x_abs = cv2.convertScaleAbs(sobel_x)
sobel_y_abs = cv2.convertScaleAbs(sobel_y)
# 合并梯度
sobel_combined = cv2.addWeighted(sobel_x_abs, 0.5, sobel_y_abs, 0.5, 0)
# 2. Scharr算子(更精确的梯度计算,核大小为3x3的优化版本)
scharr_x = cv2.Scharr(img, cv2.CV_64F, 1, 0)
scharr_y = cv2.Scharr(img, cv2.CV_64F, 0, 1)
scharr_x_abs = cv2.convertScaleAbs(scharr_x)
scharr_y_abs = cv2.convertScaleAbs(scharr_y)
scharr_combined = cv2.addWeighted(scharr_x_abs, 0.5, scharr_y_abs, 0.5, 0)
# 3. Laplacian算子(二阶导数,对噪声敏感)
laplacian = cv2.Laplacian(img, cv2.CV_64F)
laplacian_abs = cv2.convertScaleAbs(laplacian)
# 显示结果
titles = ['Original', 'Sobel X', 'Sobel Y', 'Sobel Combined', 'Scharr Combined', 'Laplacian']
images = [img, sobel_x_abs, sobel_y_abs, sobel_combined, scharr_combined, laplacian_abs]
plt.figure(figsize=(15, 8))
for i in range(6):
plt.subplot(2, 3, i+1)
plt.imshow(images[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
重点:
- Sobel算子:计算一阶导数,分别检测水平和垂直边缘
- Scharr算子:Sobel的改进版,精度更高
- Laplacian算子:计算二阶导数,对噪声敏感,通常先平滑再使用
8.2 Canny边缘检测(最常用的边缘检测算法)
Canny算法是多阶段边缘检测器,效果好、抗噪能力强 。
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('test.jpg', cv2.IMREAD_GRAYSCALE)
# 1. 基本Canny边缘检测
# cv2.Canny(图像, 低阈值, 高阈值)
# 低阈值:低于此值的点不是边缘;高阈值:高于此值的点是强边缘;介于之间的点如果与强边缘相连则视为边缘
edges1 = cv2.Canny(img, 50, 150)
# 2. 不同阈值的效果对比
edges2 = cv2.Canny(img, 100, 200) # 更高阈值,检测更少边缘
edges3 = cv2.Canny(img, 30, 80) # 更低阈值,检测更多边缘(可能包含噪声)
# 3. 自动计算阈值(使用中位数)
median = np.median(img)
low = int(max(0, 0.7 * median))
high = int(min(255, 1.3 * median))
edges_auto = cv2.Canny(img, low, high)
# 4. 结合高斯滤波(先平滑再检测)
blurred = cv2.GaussianBlur(img, (5, 5), 1.5)
edges_blur = cv2.Canny(blurred, 50, 150)
# 显示结果
titles = ['Original', 'Canny (50,150)', 'Canny (100,200)', 'Canny (30,80)', 'Auto Threshold', 'With Blur']
images = [img, edges1, edges2, edges3, edges_auto, edges_blur]
plt.figure(figsize=(15, 8))
for i in range(6):
plt.subplot(2, 3, i+1)
plt.imshow(images[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
重点:
- Canny算法步骤:高斯滤波去噪 → 计算梯度幅值和方向 → 非极大值抑制 → 双阈值检测
- 阈值选择:高阈值决定强边缘,低阈值控制边缘连续性
- 先滤波后检测能减少噪声引起的假边缘
8.3 霍夫变换(直线和圆检测)
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图像并转为灰度
img = cv2.imread('shapes.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 先用Canny检测边缘
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
# 1. 霍夫直线检测
# cv2.HoughLines(边缘图像, 距离分辨率rho, 角度分辨率theta, 阈值)
lines = cv2.HoughLines(edges, 1, np.pi/180, 150)
# 在原图上绘制检测到的直线
img_lines = img.copy()
if lines is not None:
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))
cv2.line(img_lines, (x1, y1), (x2, y2), (0, 0, 255), 2)
# 2. 概率霍夫直线检测(更常用,直接得到线段端点)
lines_p = cv2.HoughLinesP(edges, 1, np.pi/180, 100, minLineLength=100, maxLineGap=10)
img_lines_p = img.copy()
if lines_p is not None:
for line in lines_p:
x1, y1, x2, y2 = line[0]
cv2.line(img_lines_p, (x1, y1), (x2, y2), (0, 255, 0), 2)
# 3. 霍夫圆检测
# cv2.HoughCircles(图像, 检测方法, 分辨率比, 最小圆心距离, param1, param2, minRadius, maxRadius)
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, 20,
param1=50, param2=30, minRadius=0, maxRadius=0)
img_circles = img.copy()
if circles is not None:
circles = np.round(circles[0, :]).astype("int")
for (x, y, r) in circles:
# 画圆
cv2.circle(img_circles, (x, y), r, (0, 255, 0), 2)
# 画圆心
cv2.circle(img_circles, (x, y), 2, (0, 0, 255), 3)
# 显示结果
plt.figure(figsize=(15, 10))
plt.subplot(2, 2, 1)
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Original')
plt.axis('off')
plt.subplot(2, 2, 2)
plt.imshow(cv2.cvtColor(img_lines, cv2.COLOR_BGR2RGB))
plt.title('Hough Lines')
plt.axis('off')
plt.subplot(2, 2, 3)
plt.imshow(cv2.cvtColor(img_lines_p, cv2.COLOR_BGR2RGB))
plt.title('Probabilistic Hough Lines')
plt.axis('off')
plt.subplot(2, 2, 4)
plt.imshow(cv2.cvtColor(img_circles, cv2.COLOR_BGR2RGB))
plt.title('Hough Circles')
plt.axis('off')
plt.tight_layout()
plt.show()
重点:
- 霍夫直线检测:将图像空间中的直线映射到参数空间(极坐标)
- 概率霍夫变换:更高效,直接返回线段端点
- 霍夫圆检测:需要调节
param1(Canny高阈值)和param2(圆心检测阈值)
第9章:图像分割与轮廓分析
图像分割是将图像划分为有意义的区域,轮廓分析则是提取和处理这些区域的边界。
9.1 基于阈值的分割(OTSU、Triangle)
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图像
img = cv2.imread('test.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 1. OTSU阈值分割(自动阈值)
ret_otsu, thresh_otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"OTSU自动阈值: {ret_otsu}")
# 2. Triangle阈值分割(适用于单峰直方图)
ret_triangle, thresh_triangle = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_TRIANGLE)
print(f"Triangle自动阈值: {ret_triangle}")
# 3. 自适应阈值(局部阈值)
thresh_adaptive = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
# 显示结果
titles = ['Original', 'OTSU', 'Triangle', 'Adaptive']
images = [gray, thresh_otsu, thresh_triangle, thresh_adaptive]
plt.figure(figsize=(12, 8))
for i in range(4):
plt.subplot(2, 2, i+1)
plt.imshow(images[i], cmap='gray')
plt.title(titles[i])
plt.axis('off')
plt.tight_layout()
plt.show()
重点:
- OTSU:假设图像直方图呈双峰分布,自动计算峰谷阈值
- Triangle:适用于单峰直方图(如背景单一,前景较小)
- 自适应阈值:处理光照不均的图像效果好
9.2 轮廓查找与绘制
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图像并转为灰度
img = cv2.imread('shapes.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 阈值分割(便于找到轮廓)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# 查找轮廓
# cv2.findContours(二值图像, 轮廓检索模式, 轮廓近似方法)
# 检索模式:RETR_EXTERNAL(只检测最外层轮廓)、RETR_LIST(检测所有轮廓)、RETR_TREE(检测所有轮廓并建立等级关系)
# 近似方法:CHAIN_APPROX_SIMPLE(压缩水平、垂直、对角线方向,只保留端点),CHAIN_APPROX_NONE(保留所有点)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print(f"找到 {len(contours)} 个轮廓")
# 在原图上绘制所有轮廓
img_contours = img.copy()
cv2.drawContours(img_contours, contours, -1, (0, 255, 0), 2) # -1表示绘制所有轮廓
# 绘制特定轮廓(例如第0个)
img_contour0 = img.copy()
cv2.drawContours(img_contour0, contours, 0, (0, 0, 255), 3)
# 显示结果
plt.figure(figsize=(15, 10))
plt.subplot(2, 2, 1)
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Original')
plt.axis('off')
plt.subplot(2, 2, 2)
plt.imshow(thresh, cmap='gray')
plt.title('Threshold')
plt.axis('off')
plt.subplot(2, 2, 3)
plt.imshow(cv2.cvtColor(img_contours, cv2.COLOR_BGR2RGB))
plt.title('All Contours')
plt.axis('off')
plt.subplot(2, 2, 4)
plt.imshow(cv2.cvtColor(img_contour0, cv2.COLOR_BGR2RGB))
plt.title('First Contour')
plt.axis('off')
plt.tight_layout()
plt.show()
重点:
findContours()输入必须是二值图像- 轮廓是点集(NumPy数组),可用
drawContours()绘制 - 轮廓检索模式选择影响轮廓的组织方式
9.3 轮廓特征(面积、周长、外接矩形)
# 遍历每个轮廓,计算特征
for i, contour in enumerate(contours):
# 1. 轮廓面积
area = cv2.contourArea(contour)
# 2. 轮廓周长(弧长)
perimeter = cv2.arcLength(contour, True) # True表示闭合轮廓
# 3. 外接矩形(正矩形)
x, y, w, h = cv2.boundingRect(contour)
# 4. 最小外接矩形(带旋转角度)
rect = cv2.minAreaRect(contour)
box = cv2.boxPoints(rect)
box = np.int0(box)
# 5. 外接圆
(cx, cy), radius = cv2.minEnclosingCircle(contour)
center = (int(cx), int(cy))
radius = int(radius)
# 6. 轮廓近似(减少顶点数量)
epsilon = 0.02 * perimeter
approx = cv2.approxPolyDP(contour, epsilon, True)
# 7. 凸包
hull = cv2.convexHull(contour)
# 8. 判断轮廓是否是凸的
is_convex = cv2.isContourConvex(contour)
# 9. 计算矩(用于计算重心、方向等)
moments = cv2.moments(contour)
if moments['m00'] != 0:
centroid_x = int(moments['m10'] / moments['m00'])
centroid_y = int(moments['m01'] / moments['m00'])
else:
centroid_x, centroid_y = 0, 0
# 只打印面积较大的轮廓
if area > 100:
print(f"轮廓 {i}: 面积={area:.2f}, 周长={perimeter:.2f}, 顶点数={len(contour)}")
print(f" 外接矩形: x={x}, y={y}, w={w}, h={h}")
print(f" 重心: ({centroid_x}, {centroid_y})")
print(f" 凸性: {is_convex}")
print("---")
重点:轮廓特征是实现形状识别的基础,如区分圆形、矩形等 。
9.4 形状识别实战:检测几何形状
import cv2
import numpy as np
import matplotlib.pyplot as plt
def detect_shape(contour):
"""根据轮廓顶点数判断形状"""
# 轮廓近似
perimeter = cv2.arcLength(contour, True)
epsilon = 0.04 * perimeter
approx = cv2.approxPolyDP(contour, epsilon, True)
vertices = len(approx)
if vertices == 3:
return "Triangle"
elif vertices == 4:
# 判断是否为正方形
x, y, w, h = cv2.boundingRect(approx)
aspect_ratio = w / h
if 0.95 <= aspect_ratio <= 1.05:
return "Square"
else:
return "Rectangle"
elif vertices == 5:
return "Pentagon"
elif vertices == 6:
return "Hexagon"
elif vertices > 6:
# 判断是否为圆
area = cv2.contourArea(contour)
(x, y), radius = cv2.minEnclosingCircle(contour)
circle_area = np.pi * radius * radius
circularity = area / circle_area
if circularity > 0.85:
return "Circle"
else:
return "Polygon"
else:
return "Unknown"
# 读取图像
img = cv2.imread('shapes.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 阈值处理
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# 查找轮廓
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 在原图上绘制并标注形状
img_result = img.copy()
for i, contour in enumerate(contours):
# 忽略小轮廓
area = cv2.contourArea(contour)
if area < 100:
continue
# 识别形状
shape_name = detect_shape(contour)
# 绘制轮廓
cv2.drawContours(img_result, [contour], -1, (0, 255, 0), 2)
# 计算重心用于放置文字
M = cv2.moments(contour)
if M['m00'] != 0:
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
else:
cx, cy = 0, 0
# 标注形状名称
cv2.putText(img_result, shape_name, (cx-50, cy),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
# 显示结果
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Original')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(img_result, cv2.COLOR_BGR2RGB))
plt.title('Shape Detection')
plt.axis('off')
plt.tight_layout()
plt.show()
重点:
- 通过
approxPolyDP()轮廓近似,减少顶点数量 - 根据顶点数判断基本形状
- 圆形判断需要计算圆形度(面积/外接圆面积)
第10章:模板匹配与特征匹配
模板匹配是在大图中寻找与模板图像最相似的区域,常用于目标定位 。
10.1 模板匹配基础
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取源图像和模板图像
img = cv2.imread('source.jpg') # 大图
template = cv2.imread('template.jpg') # 小图(模板)
# 转为灰度
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
# 获取模板尺寸
h, w = template_gray.shape
# 模板匹配
# cv2.matchTemplate(源图像, 模板, 匹配方法)
# 匹配方法:
# TM_SQDIFF:平方差匹配(值越小越匹配)
# TM_CCORR:相关匹配(值越大越匹配)
# TM_CCOEFF:相关系数匹配(值越大越匹配)
# 还有对应的归一化版本:TM_SQDIFF_NORMED, TM_CCORR_NORMED, TM_CCOEFF_NORMED
result = cv2.matchTemplate(img_gray, template_gray, cv2.TM_CCOEFF_NORMED)
# 找到最佳匹配位置
# cv2.minMaxLoc() 返回最小值、最大值、最小值位置、最大值位置
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
# 对于TM_CCOEFF_NORMED,最大值位置是最佳匹配
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
# 在源图像上绘制矩形
img_result = img.copy()
cv2.rectangle(img_result, top_left, bottom_right, (0, 255, 0), 2)
# 显示匹配结果
print(f"匹配值: {max_val}") # 越接近1匹配越好
plt.figure(figsize=(12, 6))
plt.subplot(1, 3, 1)
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Source Image')
plt.axis('off')
plt.subplot(1, 3, 2)
plt.imshow(cv2.cvtColor(template, cv2.COLOR_BGR2RGB))
plt.title('Template')
plt.axis('off')
plt.subplot(1, 3, 3)
plt.imshow(cv2.cvtColor(img_result, cv2.COLOR_BGR2RGB))
plt.title('Matching Result')
plt.axis('off')
plt.tight_layout()
plt.show()
重点:
matchTemplate()返回一个结果矩阵,每个位置表示该区域的匹配度minMaxLoc()找到最佳匹配位置- 不同匹配方法的判断准则不同(最小值还是最大值)
10.2 多目标模板匹配
当图像中有多个相同目标时,需要设置阈值来匹配多个位置。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图像和模板
img = cv2.imread('multi_targets.jpg')
template = cv2.imread('target.jpg')
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
h, w = template_gray.shape
# 模板匹配
result = cv2.matchTemplate(img_gray, template_gray, cv2.TM_CCOEFF_NORMED)
# 设置阈值
threshold = 0.8
# 找到所有匹配值大于阈值的位置
locations = np.where(result >= threshold)
# 绘制所有匹配的矩形
img_result = img.copy()
for pt in zip(*locations[::-1]): # locations是(y,x),需要转为(x,y)
cv2.rectangle(img_result, pt, (pt[0] + w, pt[1] + h), (0, 255, 0), 1)
# 显示结果
print(f"找到 {len(locations[0])} 个匹配")
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Original')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(img_result, cv2.COLOR_BGR2RGB))
plt.title(f'Multiple Matches (threshold={threshold})')
plt.axis('off')
plt.tight_layout()
plt.show()
重点:通过阈值筛选,可以找到多个匹配位置 。
10.3 模板匹配的局限性及改进
模板匹配对旋转和缩放敏感,改进方法包括:
import cv2
import numpy as np
import matplotlib.pyplot as plt
def template_match_rotation_scale(img, template, angles, scales):
"""多角度多尺度的模板匹配"""
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
h, w = template_gray.shape
best_val = -1
best_loc = None
best_angle = 0
best_scale = 1
best_matrix = None
for scale in scales:
# 缩放模板
new_w = int(w * scale)
new_h = int(h * scale)
if new_w < 10 or new_h < 10:
continue
scaled_template = cv2.resize(template_gray, (new_w, new_h))
for angle in angles:
# 旋转模板
center = (new_w // 2, new_h // 2)
rot_matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
rotated_template = cv2.warpAffine(scaled_template, rot_matrix, (new_w, new_h))
# 确保模板尺寸小于源图像
if new_h > img_gray.shape[0] or new_w > img_gray.shape[1]:
continue
# 模板匹配
result = cv2.matchTemplate(img_gray, rotated_template, cv2.TM_CCOEFF_NORMED)
_, max_val, _, max_loc = cv2.minMaxLoc(result)
if max_val > best_val:
best_val = max_val
best_loc = max_loc
best_angle = angle
best_scale = scale
best_matrix = rot_matrix
return best_val, best_loc, best_angle, best_scale, best_matrix
# 使用示例
img = cv2.imread('source.jpg')
template = cv2.imread('template.jpg')
# 测试多个角度和尺度
angles = [0, 45, 90, 135, 180, 225, 270, 315]
scales = [0.5, 0.75, 1.0, 1.25, 1.5]
best_val, best_loc, best_angle, best_scale, _ = template_match_rotation_scale(img, template, angles, scales)
print(f"最佳匹配值: {best_val}")
print(f"最佳角度: {best_angle}°")
print(f"最佳尺度: {best_scale}")
print(f"最佳位置: {best_loc}")
重点:实际工业应用中,常用特征点匹配(如SIFT、ORB)来解决旋转缩放问题,而不是多角度模板匹配(计算量大)。
10.4 特征点匹配(SIFT/ORB)
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图像
img1 = cv2.imread('object.jpg') # 模板图像
img2 = cv2.imread('scene.jpg') # 场景图像
# 转为灰度
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
# 1. SIFT特征检测器(需要安装opencv-contrib-python)
# SIFT是尺度不变特征变换,对旋转和缩放具有不变性
sift = cv2.SIFT_create()
# 检测关键点和计算描述子
kp1, des1 = sift.detectAndCompute(gray1, None)
kp2, des2 = sift.detectAndCompute(gray2, None)
# 2. ORB特征检测器(速度更快,适合实时应用)
orb = cv2.ORB_create()
kp1_orb, des1_orb = orb.detectAndCompute(gray1, None)
kp2_orb, des2_orb = orb.detectAndCompute(gray2, None)
# 3. 特征匹配
# 使用BFMatcher(暴力匹配)
bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=True)
matches = bf.match(des1, des2)
# 按距离排序(距离越小匹配越好)
matches = sorted(matches, key=lambda x: x.distance)
# 绘制前10个匹配
img_matches = cv2.drawMatches(img1, kp1, img2, kp2, matches[:10], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
# 显示结果
plt.figure(figsize=(15, 8))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(img1, cv2.COLOR_BGR2RGB))
plt.title('Template')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(img2, cv2.COLOR_BGR2RGB))
plt.title('Scene')
plt.axis('off')
plt.figure(figsize=(15, 8))
plt.imshow(cv2.cvtColor(img_matches, cv2.COLOR_BGR2RGB))
plt.title('SIFT Matches')
plt.axis('off')
plt.show()
重点:
- SIFT:对旋转、缩放、光照变化都有很好的鲁棒性,但计算量较大
- ORB:速度很快,适合实时应用,但鲁棒性稍差
- 特征点匹配是目标检测和识别的基础
10.5 综合案例:使用特征点匹配实现对象检测
import cv2
import numpy as np
import matplotlib.pyplot as plt
def find_homography_and_detect(img_template, img_scene):
"""使用特征点匹配和单应性矩阵找到场景中的模板对象"""
# 初始化SIFT
sift = cv2.SIFT_create()
# 检测关键点和描述子
kp1, des1 = sift.detectAndCompute(img_template, None)
kp2, des2 = sift.detectAndCompute(img_scene, None)
# FLANN匹配器参数
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50)
flann = cv2.FlannBasedMatcher(index_params, search_params)
matches = flann.knnMatch(des1, des2, k=2)
# Lowe's ratio test 筛选好匹配
good_matches = []
for m, n in matches:
if m.distance < 0.7 * n.distance:
good_matches.append(m)
print(f"找到 {len(good_matches)} 个好匹配")
# 至少需要4个好匹配才能计算单应性矩阵
if len(good_matches) >= 4:
# 获取匹配点的坐标
src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
# 计算单应性矩阵
H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
# 获取模板图像的四个角点
h, w = img_template.shape[:2]
pts = np.float32([[0, 0], [0, h-1], [w-1, h-1], [w-1, 0]]).reshape(-1, 1, 2)
# 投影到场景图像中
dst = cv2.perspectiveTransform(pts, H)
# 绘制检测结果
img_result = img_scene.copy()
cv2.polylines(img_result, [np.int32(dst)], True, (0, 255, 0), 3)
return img_result, good_matches, mask
else:
print("匹配点不足,无法检测对象")
return img_scene, good_matches, None
# 使用示例
template = cv2.imread('template.jpg')
scene = cv2.imread('scene.jpg')
# 转为灰度
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
scene_gray = cv2.cvtColor(scene, cv2.COLOR_BGR2GRAY)
# 检测对象
img_detected, good_matches, mask = find_homography_and_detect(template_gray, scene_gray)
# 显示结果
plt.figure(figsize=(15, 8))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(template, cv2.COLOR_BGR2RGB))
plt.title('Template')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(img_detected, cv2.COLOR_BGR2RGB))
plt.title('Detected Object')
plt.axis('off')
plt.show()
重点:
- 单应性矩阵(Homography)描述了两个平面之间的投影变换
- 通过特征点匹配和单应性矩阵,可以在复杂场景中精确定位目标对象,即使目标有旋转、缩放和透视变化
- 这是增强现实、物体跟踪的基础技术
学习建议与下一阶段预告
恭喜你完成了OpenCV核心基础(第6-10章)的学习!现在你已经掌握了:
- 图像基础操作:读写显示、像素访问、色彩空间
- 图像预处理:几何变换、阈值处理、滤波去噪、形态学操作
- 边缘检测:Sobel、Canny、霍夫变换
- 图像分割:阈值分割、轮廓查找与分析
- 目标定位:模板匹配、特征点匹配
实战建议:
- 找一些包含简单几何图形的图片,练习轮廓检测和形状识别
- 尝试用模板匹配定位自己拍摄的照片中的特定物体
- 结合之前学的知识,实现一个简单的"图像中找圆形"的程序
下一阶段(第11-13章)预告:
- 相机标定与三维视觉
- 视频处理与实时采集
- 深度学习与OpenCV DNN模块(YOLO目标检测)
如果你在学习过程中遇到任何问题,或者想要深入某个专题的实战案例,随时可以提问!
更多推荐


所有评论(0)