唐宇迪-OpenCV-YOLO-课程笔记-全-
唐宇迪 OpenCV+YOLO 课程笔记(全)

课程P1:OpenCV计算机视觉实战课程简介 📚

在本节课中,我们将要学习《OpenCV计算机视觉实战》课程的整体安排、风格、难度要求以及学习前的准备工作。通过本次介绍,你将清晰地了解这门课程的学习路径和预期收获。
课程整体安排 🗺️

本课程的整体安排是以项目实战为驱动。在讲解几个知识点之后,我们会动手完成一个稍大型的项目。

上一节我们介绍了课程的整体安排,本节中我们来看看课程的具体风格与特点。

课程风格与特点 ✨

本课程的风格是通俗易懂的。课程将采用最接地气的方式,讲解其中比较复杂难懂的知识点。

以下是本课程的核心特点:

- 实战驱动:学习过程由项目实战驱动,帮助巩固知识点。
- 循序渐进:课程难度从零基础开始,逐渐进阶。
- 业务导向:在项目实战中,不仅练习技术,也熟悉完成一个实际项目的业务流程。
课程难度与要求 ⚙️

课程难度相当于从零基础到进阶。课程开始时,会讲解计算机视觉中最基本的操作,以及如何在OpenCV中使用函数构建小案例。后续我们会将这些知识融入更大型的项目中。

课程难度会逐渐提升,并且后续部分包含了非常丰富的项目实战内容,这些需要大家投入时间和精力来熟悉。

学习本课程有一个基本前提:需要掌握Python语言。

以下是具体的要求说明:

- Python基础:所有实战案例、大型项目以及OpenCV函数的使用均基于Python。不要求精通,但需要熟悉并能看懂代码。
- 环境准备:关于如何安装OpenCV、配置Notebook及相关IDE,将在下一节课中详细说明。

提示:如果你对Python完全不熟悉,可以参考讲师提供的Python快速入门教程。

课程资料与更新 📦
在课程中,会提供所有的数据和代码。课程将持续更新,相关数据和代码均放在课程资料中,大家可以直接点击下载。

本节课中我们一起学习了《OpenCV计算机视觉实战》课程的整体框架。我们了解到这是一门以实战为核心、从基础到进阶的课程,学习前需要具备Python基础,并且课程会提供全部的学习资料。接下来,就请大家开始准备安装OpenCV,进入精彩的计算机视觉世界吧。


课程 P1:YOLO11 核心模块解析与源码解读 🚀
在本节课中,我们将学习 YOLO11 的核心变化。YOLO11 并非一篇独立的学术论文,而是 Ultralytics 官方将 YOLOv8 项目升级并更名为 YOLO11 后的默认版本。这意味着未来的基准实验和模型对比将主要基于 YOLO11。本节课我们将通过解读配置文件和分析源码,深入理解 YOLO11 相较于 YOLOv8 所做的具体改动。

YOLO11 概述与背景 📜
YOLO11 的核心变化主要体现在项目名称和默认配置上。其项目名称已从 “YOLOv8” 变更为 “YOLO”,而 YOLO11 成为了该项目的默认版本。与之前作为独立论文出现的 YOLOv9、YOLOv10 不同,YOLO11 是官方直接集成到代码库中的版本更新。因此,掌握 YOLO11 对于跟进最新的目标检测技术发展至关重要。
代码使用方法对比

在代码使用层面,YOLO11 与 YOLOv8 完全一致,没有任何区别。这意味着用户之前为 YOLOv8 编写的训练、推理代码可以无缝迁移到 YOLO11 上。未来的默认版本将是 YOLO11。

配置文件核心变化 🔧


要理解 YOLO11 的改动,核心在于分析其配置文件。与 YOLOv8 的经典配置文件进行对比,可以发现主要发生了两处模块替换。
以下是 YOLO11 配置文件中的关键变化:



- C2F 模块的演进:在 YOLOv8 中,骨干网络(Backbone)和颈部网络(Neck)大量使用了名为 C2F 的模块。在 YOLO11 中,这个模块被替换为 C3k2。
C3k2模块内部根据一个布尔参数(true或false)来决定执行不同的子结构。 - 引入注意力机制:YOLOv8 的默认配置中没有集成任何注意力机制模块。而在 YOLO11 中,在 Neck 网络的末端引入了一个名为 C2fPSA 的模块,它集成了注意力机制,用于在特征融合的最后阶段对特征进行加权。

源码深度解析:C3k2 模块
为了理解 C3k2 模块,最有效的方法是直接阅读和调试其源码。所有核心模块的实现都位于 ultralytics/nn/modules/block.py 文件中。
C3k2 模块结构
C3k2 模块的核心是一个条件判断,它根据传入的参数决定执行哪条路径。
# 简化示意代码
if self.add:
# 执行 C3k 子模块
return self.c3k(x)
else:
# 执行与 YOLOv8 中 C2f 基本相同的残差结构
return self.c2f(x)
当参数为 false 时,其执行路径与 YOLOv8 中的 C2f 模块完全相同。我们可以通过调试来梳理其数据流。




当 add=False 时的流程


- 卷积层:输入特征图
X首先经过一个卷积层cv1。 - 特征图切分:卷积后的特征图被沿着通道维度平均切分成两份,记为
A和B。 - 残差处理:其中一份(例如
B)会通过一个由多个Bottleneck残差块组成的序列进行处理。 - 结果拼接:处理后的
B与未处理的A,以及B在残差块中产生的中间特征(如果存在)进行拼接。 - 最终卷积:拼接后的特征图再经过一个卷积层
cv2输出。
这个过程体现了 YOLO 系列一个重要的设计思想:“保本”策略。即总是保留一部分原始或浅层特征直接参与后续计算,另一部分进行更复杂的变换,最后再将它们融合。这保证了梯度流动和信息保留。

当 add=True 时的流程


当参数为 true 时,C3k2 会调用 C3k 子模块。其与 false 路径的主要区别在于对 B 部分进行处理的核心单元不同。


false路径:使用单个Bottleneck残差块。true路径:使用C3模块,该模块内部串联了两个Bottleneck残差块。


这意味着 add=True 时,特征变换的深度和层级被拉长了,允许融合来自更“远”(不同深度)的特征信息。在 YOLO11 的默认配置中,浅层特征图通常使用 add=False,而在深层特征图中使用 add=True,以在更深层次进行更复杂的特征整合。

源码深度解析:C2fPSA 注意力模块 🧠

C2fPSA 模块是 YOLO11 新引入的,它位于 Neck 网络的末端,负责在预测之前对融合后的特征进行全局调整。


C2fPSA 模块结构


- 特征图切分:输入特征图
X被平均切分为A和B两部分。 - 注意力变换:
B部分会送入一个PSA块进行处理。PSA块的结构非常简单,就是一个标准的 Transformer Encoder 层,包含:- 多头自注意力机制:计算特征图不同位置间的关联性。
- 前馈神经网络:一个简单的全连接层。
- 残差连接:每个子层周围都包含残差连接。
- 结果拼接:处理后的
B与原始的A直接拼接起来。 - 卷积输出:拼接后的特征经过卷积后输出。

这个设计再次运用了“保本”策略。A 部分保留了未经注意力修饰的原始融合特征,而 B 部分则经过了注意力机制的重新加权,强调重要特征,抑制次要特征。两者的结合使得最终特征既全面又有侧重。

视觉大模型简单应用示例 👁️

除了 YOLO11 本身的解析,我们还可以探索其与前沿技术的结合。例如,可以将 YOLO11 与开源的视觉大模型结合,构建一个简单的应用流水线。
以下是该应用的基本思路:
- 目标检测:使用 YOLO11 对输入图片进行检测,定位出图片中的物体(如人)。
- 区域裁剪:将检测到的每个边界框区域从原图中裁剪出来。
- 视觉问答:将裁剪出的单人图像输入到视觉大模型中,通过自然语言提问(例如:“这个人的性别是什么?”)。
- 生成标签:视觉大模型会基于图像内容生成文本回答,从而为检测目标自动添加属性标签。
这种结合无需为属性识别单独训练模型,利用了大模型的零样本或小样本理解能力,展示了 YOLO11 作为基础感知模块在更复杂系统中的潜力。
总结与学习建议 📚
本节课我们一起学习了 YOLO11 的核心内容。总结如下:


- 版本定位:YOLO11 是 Ultralytics 官方项目升级后的默认版本,将逐步取代 YOLOv8 成为新的基准模型。
- 核心改动:主要改动在于配置文件中的两个模块替换:
C2f->C3k2:通过一个开关参数,在深层网络引入更长的特征融合路径。- 新增
C2fPSA:在 Neck 末端引入标准的 Transformer 注意力模块,对特征进行全局加权。
- 设计思想:YOLO 系列广泛采用“特征图切分-部分变换-结果拼接”的“保本”设计思想,该思想稳定且有效,值得在其他任务中借鉴。
- 学习方法:对于深度学习模型,阅读和调试源码是理解其精髓的最直接方式。当论文描述晦涩时,清晰的代码往往能揭示本质。建议使用调试工具逐步跟踪数据流,并结合绘图来梳理模块结构。

YOLO11 的改进看似细微,但它标志着官方技术路线的更新。理解这些变化,有助于我们更好地使用最新工具,并为自己的模型优化提供灵感。

课程P10:4-边界填充 🖼️
在本节课中,我们将要学习图像处理中的一个重要操作——边界填充。边界填充是指在图像周围添加额外的像素区域,这在许多图像处理算法(如卷积)中是一个常见的预处理步骤。
上一节我们介绍了图像的几何变换,本节中我们来看看如何为图像添加边界。
边界填充概述
当对图像进行变换或特征提取时,有时需要将图像扩大一圈。这个过程称为边界填充或边缘填充。例如,在卷积操作中,通常会先对图像进行填充(padding),以控制输出图像的尺寸或保留边缘信息。


填充参数
进行填充时,需要指定在图像的上、下、左、右四个方向分别填充多少像素。在代码中,我们可以用一个参数来指定这四个方向的填充大小。
以下是填充参数的一个示例,表示在每个方向都填充50个像素:
top = 50
bottom = 50
left = 50
right = 50

填充方法与效果

填充的核心函数是相同的,但可以通过指定不同的 type 参数来选择填充方法。OpenCV提供了多种填充方式。

以下是几种常见的填充方法及其效果描述:


- 复制法 (BORDER_REPLICATE)
这种方法直接复制图像最边缘的像素值进行填充。例如,如果边缘像素是A,那么填充的所有像素都是A。

- 反射法 (BORDER_REFLECT)
这种方法以图像边缘为轴进行镜像反射。假设图像边缘像素序列是ABCDEFG,那么填充的像素序列将是GFEDCBA。

- 反射法101 (BORDER_REFLECT_101)
这是另一种反射方法,它使反射看起来更对称。它会忽略最边缘的像素,从次边缘开始反射。例如,序列ABCDEFGH的填充会从B开始反射。

-
外包装法 (BORDER_WRAP)
这种方法将图像视为可以循环重复的。例如,序列ABCDEFGH的填充会是ABCDEFGHABCDEFGH...,以此类推。 -
常量法 (BORDER_CONSTANT)
这种方法使用一个固定的常数值(如0,代表黑色)来填充边界区域。使用此方法时,需要额外指定这个常数值。
代码实现
所有填充方法都使用同一个函数,仅 borderType 参数不同。以下是一个示例代码结构:
import cv2
import matplotlib.pyplot as plt
# 读取图像
img = cv2.imread('image.jpg')
# 定义填充大小
top = bottom = left = right = 50
# 使用不同的方法进行填充
replicate = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REPLICATE)
reflect = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REFLECT)
reflect101 = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REFLECT_101)
wrap = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_WRAP)
constant = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=0)
# 使用matplotlib显示结果(此处省略具体绘图代码)

效果对比
- 复制法:填充区域是边缘像素的简单重复。
- 反射法:填充区域是图像边缘的镜像,看起来像在边缘放置了一面镜子。
- 反射法101:同样是镜像,但边界处理更平滑,避免了边缘像素的重复感。
- 外包装法:填充区域是图像内容的循环重复。
- 常量法:填充区域是统一的颜色(如黑色),形成一个边框。


总结

本节课中我们一起学习了图像处理中的边界填充技术。我们了解了填充的目的,掌握了如何指定填充范围,并重点介绍了五种不同的填充方法:复制法、反射法、反射法101、外包装法以及常量法。理解这些方法的原理和效果,对于后续进行更复杂的图像处理操作(如卷积)至关重要。在实际应用中,可以根据具体需求选择合适的填充方式。
图像处理课程 P11:图像阈值操作 📊
在本节课中,我们将要学习图像处理中的核心操作之一——阈值处理。阈值操作是图像分割、边缘检测等高级任务的基础,其核心思想是根据一个设定的临界值(阈值)对图像中的每个像素点进行分类或转换。
概述

阈值操作的基本原理是:给定一个图像(由像素点组成的矩阵)和一个阈值,对图像中的每一个像素值进行判断。根据像素值与阈值的大小关系,将其转换为新的值。例如,一个像素值为56,阈值为127,我们需要决定大于127和小于127的像素分别如何处理。这就是阈值函数的核心功能。
阈值函数详解

OpenCV 提供了 cv2.threshold() 函数来执行阈值操作。该函数需要四个输入参数:

- 原始图像:通过
cv2.imread()读取的图像。 - 阈值:一个具体的数值(例如127),用于判断像素值。注意,这不是百分比,而是0-255范围内的实际像素值。
- 最大值:通常设置为255,因为图像像素值范围是0-255。
- 阈值类型:一个关键参数,决定了如何应用阈值以及如何处理结果。它完全由
type参数控制。



函数的基本调用格式如下:
retval, dst = cv2.threshold(src, thresh, maxval, type)
其中:
retval:实际使用的阈值(在某些自适应阈值方法中会用到)。dst:处理后的输出图像。


五种阈值处理方法
OpenCV 主要提供了五种阈值处理方法。上一节我们介绍了阈值函数的基本结构,本节中我们来看看具体的处理类型及其效果。


以下是五种阈值处理方法的详细说明和效果对比:
- 二值化
- 原理:像素值大于阈值时,设为最大值(如255,即白色);否则设为0(即黑色)。
- 公式/逻辑:
dst(x, y) = maxval if src(x, y) > thresh else 0 - OpenCV 类型:
cv2.THRESH_BINARY - 效果:图像被转换为只有纯黑和纯白的二值图像。较亮区域变白,较暗区域变黑。

- 反二值化
- 原理:与二值化相反。像素值大于阈值时,设为0(黑色);否则设为最大值(白色)。
- 公式/逻辑:
dst(x, y) = 0 if src(x, y) > thresh else maxval - OpenCV 类型:
cv2.THRESH_BINARY_INV - 效果:得到与二值化结果颜色完全反转的图像。较亮区域变黑,较暗区域变白。

-
截断
- 原理:像素值大于阈值时,被限制为阈值本身;小于等于阈值时,保持不变。
- 公式/逻辑:
dst(x, y) = thresh if src(x, y) > thresh else src(x, y) - OpenCV 类型:
cv2.THRESH_TRUNC - 效果:图像中亮部区域(大于阈值的部分)的亮度被“截断”至阈值水平,暗部区域保持不变。整体图像对比度降低。
-
阈值化为0
- 原理:像素值小于阈值时,设为0(黑色);大于等于阈值时,保持不变。
- 公式/逻辑:
dst(x, y) = 0 if src(x, y) < thresh else src(x, y) - OpenCV 类型:
cv2.THRESH_TOZERO - 效果:图像中暗部区域被置黑,亮部区域保留原样。相当于突出了图像中的亮部特征。

- 反阈值化为0
- 原理:与“阈值化为0”相反。像素值大于阈值时,设为0(黑色);小于等于阈值时,保持不变。
- 公式/逻辑:
dst(x, y) = 0 if src(x, y) > thresh else src(x, y) - OpenCV 类型:
cv2.THRESH_TOZERO_INV - 效果:图像中亮部区域被置黑,暗部区域保留原样。相当于突出了图像中的暗部特征。

效果对比与总结

为了直观理解,我们将同一张原始图像(一只小猫)应用上述五种方法(阈值设为127,最大值设为255)进行处理,并将结果并列展示。



- 二值化:猫的白色身体等亮部变为纯白,黑色背景等暗部变为纯黑。
- 反二值化:结果与二值化完全相反,白变黑,黑变白。
- 截断:猫的亮部区域(如身体)亮度被压低至127的灰度,暗部区域(如背景)保持不变。
- 阈值化为0:猫的暗部区域(如背景、眼睛)变为纯黑,亮部区域(如身体)保持原有灰度。
- 反阈值化为0:猫的亮部区域(如身体)变为纯黑,暗部区域(如背景)保持原有灰度。

通过对比可以清晰看到,选择不同的阈值类型会得到截然不同的图像效果,适用于不同的场景需求。


本节课中我们一起学习了图像阈值操作。我们掌握了 cv2.threshold() 函数的使用方法,并深入理解了二值化、反二值化、截断、阈值化为0、反阈值化为0这五种核心的阈值处理类型及其视觉表现。这是进行图像预处理和特征提取的重要一步,请务必理解每种方法背后的逻辑。
课程P12:Sobel算子 🧮 - 迪哥的AI世界


在本节课中,我们将要学习如何计算图像的梯度。首先,我们将介绍第一种方法——Sobel算子。我们将从整体上了解这个算子能做什么。

概述:什么是梯度?
我们先读取一张图像。

以这个圆为例,梯度是什么意思呢?它通常指图像中的边界点。我们来看,比如在白色区域内部,左边是白色,右边也是白色。在这条线上,它的左边和右边都是同样的白色,那么这条线能产生梯度吗?不能。因为左右两边的像素值相同。
那么,什么样的位置会产生梯度呢?应该是图像的边缘位置。我们来看这个点,它的左边是黑色的,右边是白色的。在像素层面,它们的数值完全不同。对于这样的边界点,一个是255(白色),一个是0(黑色),存在巨大的差异,我们就说它的梯度应该比较大。

因此,我们的第一个任务就是:如何在图像中计算梯度,或者说,如何找出图像中有梯度的位置。这本质上类似于边缘检测,因为只有边缘处的像素值会发生剧烈变化。在正常图像中,不同主体之间的连接缝隙就是边缘。我们可以通过Sobel算子来进行这种检测。
如何计算梯度?

我们该如何计算梯度呢?正如刚才所说,我们需要计算一个点与其左右、上下邻居的差异。在像素层面上,如何执行这个操作呢?之前我们在讲解形态学操作时,都使用一个卷积核(或滤波器)对图像中的点进行计算。在这里也是一样的。
我们分别定义了 Gx 和 Gy。其中,Gx 代表水平方向的梯度(即左右对比),Gy 代表垂直方向的梯度(即上下对比)。因此,在计算梯度时,我们主要考虑两个方向:水平方向(Gx)和垂直方向(Gy)。
现在的问题是,既然任务明确了——计算左右和上下的差异,那么我们该如何定义这个滤波器(卷积核)呢?
这里直接给出了核的数值。计算方式其实很简单。假设我们有一个3x3的图像区域,中心点是我们关注的点。按照卷积运算的规则,我们将核与图像区域对应位置相乘,然后求和。
对于水平梯度核 Gx:
核 = [[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]]
计算过程本质上是 (右侧像素值) - (左侧像素值)。核中的权重(1和2)体现了距离中心点的远近,离中心越近的像素点权重越大(类似于高斯加权),这有助于更准确地捕捉边缘。
同理,对于垂直梯度核 Gy:
核 = [[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]]
计算过程本质上是 (下方像素值) - (上方像素值)。
这样,我们就通过右减左、下减上的操作,得到了像素点在水平和垂直方向上的差异值,即梯度 Gx 和 Gy。
动手实践:使用OpenCV

现在,我们来做第一个实验。我们将读入一个中间为白色、周围为黑色的圆形图像。


我们来看一下在OpenCV中如何使用这个函数。主要使用 cv2.Sobel() 函数。

函数参数说明:
src: 输入图像。ddepth: 输出图像的深度。通常指定为cv2.CV_64F,这是因为梯度计算可能产生负值,我们需要一个能存储负值的数据类型。dx: 在x方向(水平方向)求导的阶数。设为1表示计算Gx。dy: 在y方向(垂直方向)求导的阶数。设为1表示计算Gy。ksize: Sobel核的大小,通常是3x3(即ksize=3)。
一个重要细节:
OpenCV默认的图像像素值范围是0到255。如果梯度计算得到负值,OpenCV可能会将其截断为0。但我们关心的是差异的大小,而不是正负。因此,一种常见的做法是先使用 cv2.CV_64F 这类数据类型保存计算结果(包含负值),然后取绝对值。
让我们先计算水平方向的梯度(Gx)。

我们读入这个圆形图像,并将其传入Sobel函数。

设置参数:ddepth=cv2.CV_64F, dx=1, dy=0, ksize=3。这表示我们计算水平方向的Sobel梯度。



这是计算完成后得到的结果图像。计算出的梯度在哪里呢?大家想一想,只有边界位置才会有明显的梯度。

结果图像中的这些白点,正是圆形图像的边界位置。这表明Sobel算子成功地检测出了图像在水平方向上的边缘。
总结
本节课中,我们一起学习了图像处理中的Sobel算子。

- 我们首先理解了图像梯度的概念,它反映了图像中像素值变化的剧烈程度,通常出现在边缘区域。
- 接着,我们学习了Sobel算子的原理。它通过两个特定的卷积核(Gx 和 Gy)分别计算图像在水平方向和垂直方向的梯度,其核心思想是“右减左”和“下减上”。
- 最后,我们通过OpenCV进行了实践,使用
cv2.Sobel()函数计算并可视化了一张图像的水平梯度图,成功检测出了图像的边缘。

Sobel算子是边缘检测的基础工具,理解它对于后续学习更复杂的图像处理技术至关重要。
📘 课程 P13:梯度计算方法详解
在本节课中,我们将学习图像处理中梯度计算的核心方法。我们将从原理出发,解释如何分别计算水平(X方向)和垂直(Y方向)的梯度,如何处理计算中出现的负值,以及如何将两个方向的梯度有效地融合在一起,最终得到清晰的图像轮廓信息。
🔍 梯度计算的基本原理
梯度计算的核心是衡量图像中每个像素点与其相邻像素在亮度上的变化。这种变化能有效反映出图像的边缘和轮廓信息。
在计算时,我们通常分别考虑水平方向(X方向)和垂直方向(Y方向)的变化。

➡️ 水平梯度(GX)的计算与问题
上一节我们介绍了梯度的概念,本节中我们来看看水平方向梯度的具体计算方法。
水平梯度的计算规则是:对于图像中的每一个像素点,用其右边像素的亮度值减去其左边像素的亮度值。公式可以表示为:
GX = 右边像素值 - 左边像素值
然而,直接应用这个规则会遇到一个问题。请看下图示例:


在图像左半部分,内部是白色(高亮度值),外部是黑色(低亮度值)。因此,白 - 黑的结果是一个正数,能够正常显示为边缘。

但在图像右半部分,情况正好相反:右边是黑色,左边是白色。计算黑 - 白会得到一个负数。在默认的图像显示中,负数值会被截断为0(即黑色),因此右半部分的边缘无法显示出来。
✅ 解决方案:绝对值转换
为了解决负数被截断导致边缘信息丢失的问题,我们需要对梯度计算结果进行转换。
核心思路是:我们只关心亮度变化的幅度,而不关心变化的方向(是变亮还是变暗)。因此,可以对计算出的梯度值取绝对值。

以下是处理步骤:
- 先按照
右边减左边的规则计算出原始梯度值(可能包含负数)。 - 然后对结果应用
cv2.convertScaleAbs()函数,该函数会计算每个值的绝对值。
经过绝对值转换后,无论是正梯度还是负梯度,都会以正数的形式显示其变化强度。效果如下图所示:


可以看到,经过处理,图像左右两边的轮廓都完整地显示出来了。
⬇️ 垂直梯度(GY)的计算
理解了水平梯度的计算后,垂直梯度的计算就很容易类推了。
垂直梯度的计算规则是:对于图像中的每一个像素点,用其下方像素的亮度值减去其上方像素的亮度值。公式表示为:
GY = 下方像素值 - 上方像素值

同样,在计算后也需要进行绝对值转换,以确保所有边缘信息得以保留。下图展示了垂直梯度的计算结果:



🧩 融合GX与GY:得到总梯度
在实际应用中,我们通常需要得到一个综合了水平和垂直方向变化的整体梯度图。这可以通过将GX和GY融合来实现。
常见的融合方法有两种:
- 平方和开方:
G = sqrt(GX² + GY²) - 绝对值相加:
G = |GX| + |GY|

在OpenCV中,我们可以使用cv2.addWeighted()函数方便地实现加权融合。该函数的基本用法如下:
G_total = cv2.addWeighted(GX, 0.5, GY, 0.5, 0)
其中,0.5是分配给GX和GY的权重,最后的0是偏置项(通常设为0)。

下图展示了分别计算的GX、GY以及它们融合后的总梯度G:

(左:GX, 中:GY, 右:G_total)
可以看到,融合后的图像(G_total)结合了两个方向的边缘信息,使得轮廓更加完整和连续。
⚠️ 为什么不建议直接计算整体梯度?
你可能会问,既然最终要融合,为什么不直接在Sobel算子中同时设置dx=1和dy=1来一次性计算整体梯度呢?
以下是直接计算与分别计算再融合的对比结果:

(左:分别计算再融合, 右:直接设置dx=1, dy=1计算)
通过对比可以发现:
- 直接计算的结果存在明显的重影和模糊,边缘不够清晰,部分区域还有断裂。
- 分别计算再融合的结果则轮廓清晰,边缘连续性好。
因此,在实践中建议采用先分别计算GX和GY,再进行加权融合的方法,这样可以获得更优的边缘检测效果。权重的具体分配可以根据实际任务进行调整。
📝 课程总结
本节课中我们一起学习了图像梯度计算的完整流程:
- 原理:梯度反映了图像亮度的变化率,是边缘检测的基础。
- 分向计算:
- 水平梯度
GX= 右边像素 - 左边像素 - 垂直梯度
GY= 下边像素 - 上边像素
- 水平梯度
- 关键处理:计算出的梯度值可能为负数,需通过
cv2.convertScaleAbs()取绝对值,以避免信息丢失。 - 融合策略:使用
cv2.addWeighted()函数将GX和GY按权重融合,得到总梯度图。分别计算再融合的效果优于一次性计算。 - 实践建议:对于需要清晰轮廓的任务,推荐采用分步计算和融合的方法。

通过掌握这些步骤,你就能有效地计算出图像的梯度,并用于后续更复杂的图像分析和计算机视觉任务中。
课程P14:Scharr与Laplacian算子 🧮
在本节课中,我们将学习图像梯度计算中的另外两种重要算子:Scharr算子和Laplacian算子。我们将了解它们与之前学过的Sobel算子的区别,并通过代码演示直观地比较它们的效果。
Scharr算子:更敏感的梯度检测
上一节我们介绍了Sobel算子,本节中我们来看看Scharr算子。Scharr算子的核心思想与Sobel算子相同,但其卷积核中的数值被设计得更大,这使得它对图像中梯度的变化更为敏感。
以下是Scharr算子在x方向和y方向上的卷积核:
x方向核:
[ -3, 0, 3]
[-10, 0, 10]
[ -3, 0, 3]
y方向核:
[-3, -10, -3]
[ 0, 0, 0]
[ 3, 10, 3]
与Sobel算子相比,Scharr算子将离中心点较远的权重从2增大到3,将相邻点的权重从1增大到10。这种设计使得它对边缘的响应更强,能检测到更细微的梯度变化。
Laplacian算子:基于二阶导的边缘检测
接下来我们介绍Laplacian算子。与Sobel和Scharr算子基于一阶导数不同,Laplacian算子基于二阶导数,即梯度的变化率。这使得它对图像中强度的快速变化(如边缘和噪声)都极为敏感。
Laplacian算子的一个常见卷积核如下:
[ 0, 1, 0]
[ 1, -4, 1]
[ 0, 1, 0]
其计算原理可以理解为:用中心像素点与其四个直接相邻的像素点进行比较。对于一个中心点P5及其上下左右邻居P2、P4、P6、P8,其响应值计算公式为:
结果 = P2 + P4 + P6 + P8 - 4 * P5
如果中心点与周围点差异很大(例如位于边界),计算结果就会是一个较大的值。然而,正因为它对变化极为敏感,所以对图像噪声也特别敏感,这通常不是一件好事。因此,Laplacian算子很少单独使用,而是常与其他图像处理方法结合使用。
代码实现与效果对比
了解了基本概念后,我们来看看如何在OpenCV中使用这些算子。它们的调用方式与Sobel算子类似,但各有特点。

以下是使用OpenCV实现三种算子的核心代码:

import cv2
import numpy as np

# 读取图像并转为灰度图
img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)

# 1. 使用Sobel算子
sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
sobel_combined = cv2.convertScaleAbs(cv2.addWeighted(cv2.convertScaleAbs(sobelx), 0.5, cv2.convertScaleAbs(sobely), 0.5, 0))
# 2. 使用Scharr算子
scharrx = cv2.Scharr(img, cv2.CV_64F, 1, 0)
scharry = cv2.Scharr(img, cv2.CV_64F, 0, 1)
scharr_combined = cv2.convertScaleAbs(cv2.addWeighted(cv2.convertScaleAbs(scharrx), 0.5, cv2.convertScaleAbs(scharry), 0.5, 0))

# 3. 使用Laplacian算子
# 注意:Laplacian算子直接计算,无需分x和y方向
laplacian = cv2.Laplacian(img, cv2.CV_64F)
laplacian_abs = cv2.convertScaleAbs(laplacian)
代码说明:
cv2.Sobel和cv2.Scharr函数需要分别指定dx和dy参数来计算x方向和y方向的梯度,最后需要合并。cv2.Laplacian函数直接计算,无需指定方向,使用起来更简单。cv2.convertScaleAbs函数用于将结果转换为8位无符号整数并取绝对值,便于显示。
结果分析与对比

执行上述代码后,我们可以对比三种算子的效果:

以下是不同算子的效果差异总结:
- Sobel算子:能够有效检测出主要的边界信息,结果较为清晰。
- Scharr算子:与Sobel算子相比,它能捕捉到更丰富、更细致的梯度信息。例如,在头发丝或纹理密集的区域,Scharr算子能显示出更多线条,对边缘的描绘更细致。
- Laplacian算子:得到的结果对噪声非常敏感,边缘线条可能不够清晰或包含大量无关的噪声响应。因此,其单独使用的效果通常不理想。

从对比中可以得出结论:Scharr算子由于其对梯度更高的敏感性,在需要检测细微边缘时可能优于Sobel算子。而Laplacian算子因其特性,更适合作为其他高级图像处理技术(如图像锐化或斑点检测)中的一个组件,而非独立的边缘检测工具。
总结
本节课中我们一起学习了Scharr算子和Laplacian算子。
- Scharr算子是Sobel算子的一个增强变体,通过增大卷积核权重来获得更高的梯度检测灵敏度。
- Laplacian算子基于二阶导数,对变化极为敏感,但同时也易受噪声干扰,通常不单独用于边缘检测。

理解不同算子的特性有助于我们在实际应用中根据具体需求(如对噪声的容忍度、对细节的要求)选择合适的工具。在后续课程中,当我们学习更复杂的图像处理技术时,还会看到Laplacian算子如何与其他方法协同工作。
课程P15:1-Canny边缘检测流程 🧠
在本节课中,我们将学习经典的Canny边缘检测算法的完整流程。Canny算法由John Canny在1986年提出,并以他的名字命名。该算法通过一系列精心设计的步骤,能够有效地从图像中提取出清晰、准确的边缘信息。
第一步:高斯滤波去噪 🧹
上一节我们介绍了边缘检测的基本概念,本节中我们来看看Canny算法的第一步。在进行边缘检测时,原始图像可能包含噪声。这些噪声点在计算梯度时会产生干扰,影响检测效果。因此,第一步需要对图像进行平滑处理以去除噪声。
Canny算法使用高斯滤波器来实现平滑。高斯滤波器的核心思想是赋予中心像素点最高的权重,周围像素点的权重随着距离的增加而减小。
以下是高斯滤波的核心操作公式(以3x3核为例):
新像素值 = 加权求和(核内所有像素值 * 对应高斯权重)
完成高斯滤波后,图像变得平滑,为后续的梯度计算做好了准备。
第二步:计算梯度幅值与方向 📈
在完成图像平滑后,下一步是计算图像的梯度。梯度能够反映图像灰度变化的强度和方向。Canny算法不仅需要梯度的大小(幅值),还需要梯度的方向。
这里使用Sobel算子来计算梯度。Sobel算子包含两个卷积核,分别用于计算水平方向(Gx)和垂直方向(Gy)的梯度。

以下是Sobel算子的两个核心卷积核:
Gx = [[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]]
Gy = [[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]]
计算过程如下:
- 分别用Gx和Gy核与图像进行卷积,得到每个像素点的水平梯度值
Gx和垂直梯度值Gy。 - 计算梯度幅值:
G = sqrt(Gx^2 + Gy^2) - 计算梯度方向:
θ = arctan(Gy / Gx)
至此,我们得到了每个像素点的边缘强度(幅值)和边缘方向。

第三步:非极大值抑制 🎯
计算完梯度后,图像中可能存在许多较宽的边缘区域。非极大值抑制(Non-Maximum Suppression, NMS)的目的是“细化”边缘,只保留局部梯度最大的点,抑制非极大值。

其核心思想是:在当前像素点的梯度方向上,比较该点与其前后两个邻接点的梯度幅值。如果当前点的幅值不是最大的,则将其抑制(置为0)。

为了帮助理解NMS,我们可以看一个在目标检测中的类似应用:
- 假设人脸检测算法在同一个位置附近给出了三个重叠的检测框A、B、C,其置信度分别为99%、97%、96%。
- NMS的作用就是只保留置信度最高的A框(99%),而抑制掉B框和C框。

在Canny的边缘NMS中,我们比较的是梯度幅值,目的是确保检测到的边缘是单像素宽的细线。

第四步:双阈值检测与边缘连接 🔗

经过非极大值抑制后,图像中仍然可能包含一些由噪声或颜色变化引起的假边缘。双阈值检测用于进一步筛选出真正的强边缘。

以下是双阈值处理的具体步骤:
- 设定两个阈值:一个高阈值(
high_threshold)和一个低阈值(low_threshold)。 - 将梯度幅值高于高阈值的像素点标记为强边缘(确定是边缘)。
- 将梯度幅值低于低阈值的像素点标记为非边缘并直接舍弃。
- 对于梯度幅值介于两个阈值之间的像素点,标记为弱边缘(可能是边缘)。
- 边缘连接:检查每一个弱边缘像素。如果它在8邻域内与任何一个强边缘像素相连,则将其保留为真正的边缘;否则,将其舍弃。

这种方法可以有效地连接断开的边缘,同时抑制孤立的噪声点。

总结 📝

本节课中我们一起学习了Canny边缘检测算法的完整流程。我们首先使用高斯滤波器平滑图像以去除噪声;接着利用Sobel算子计算图像的梯度幅值和方向;然后通过非极大值抑制来细化边缘;最后应用双阈值检测和边缘连接来筛选并连接出最终准确、连贯的边缘。这个过程综合运用了图像平滑、梯度计算和阈值处理等技术,是计算机视觉中一个非常经典且实用的边缘检测方法。

课程P16:2-非极大值抑制 🎯

在本节课中,我们将学习Canny边缘检测算法中的一个核心步骤——非极大值抑制。它的目的是在初步检测到的边缘中,只保留局部梯度最大的点,从而让边缘变得更细、更准确。
上一节我们介绍了如何计算图像的梯度和方向,本节中我们来看看如何利用这些信息来“细化”边缘。
非极大值抑制的原理
非极大值抑制的核心思想是:只保留在梯度方向上局部梯度值最大的像素点,抑制所有非极大值的点。
对于一个像素点C,我们已知它的梯度幅值(强度)和梯度方向。为了判断C是否为边缘,我们需要将其与沿着梯度方向的相邻两个像素点进行比较。如果C的梯度幅值比这两个相邻点都大,则保留C;否则,将C抑制(即不视为边缘点)。

方法一:线性插值法(精确方法)
这种方法更为精确,但计算稍复杂。关键在于,梯度方向上的相邻点(如图中的Q和Z)可能并不正好落在实际的像素坐标上(即亚像素点),因此我们需要通过插值来计算它们的梯度值。
以下是具体步骤:
- 确定比较点:根据C点的梯度方向,找到该方向与相邻像素网格线的两个交点,记为Q和Z。
- 计算Q和Z的梯度值:由于Q和Z不是实际像素点,其梯度值需要通过其周围的实际像素点(如G1, G2)的梯度值,通过线性插值法计算得出。
- 公式可以表示为:
Q点的梯度值 = w1 * G1的梯度值 + w2 * G2的梯度值 - 其中,权重
w1和w2由距离比例决定。例如,w1 = 距离(Q, G2) / 距离(G1, G2),w2 = 距离(Q, G1) / 距离(G1, G2)。
- 公式可以表示为:
- 进行比较:将C点的梯度幅值与计算得到的Q点和Z点的梯度幅值进行比较。
- 做出决策:
- 如果 C > Q 且 C > Z,则保留C点作为边缘。
- 否则,抑制C点。

虽然线性插值法更准确,但计算量较大。为了简化,我们通常采用第二种近似方法。

方法二:方向近似法(简化方法)
这种方法将连续的梯度方向近似到有限的几个离散方向上,从而直接使用实际的像素点进行比较,避免了插值计算。

以下是具体步骤:
- 离散化方向:将一个像素点周围的360度方向,近似为4个或8个固定方向(例如:0°(水平)、45°、90°(垂直)、135°等)。
- 寻找比较点:根据C点的梯度方向,找到与其最接近的那个离散方向。然后,沿着这个正、负方向,找到C点最近的两个实际像素点(例如,对于水平方向,就是左邻点和右邻点)。
- 进行比较:直接使用这两个实际像素点的梯度幅值与C点进行比较。
- 做出决策:如果C点的梯度幅值大于这两个邻点的梯度幅值,则保留C点。




OpenCV官网提供了一个清晰的例子:对于中心像素点A,其梯度方向为水平方向。那么我们就将其与水平方向上的两个实际像素点B和C进行比较。如果A的梯度值大于B和C,则A被保留为边缘点。


总结
本节课中我们一起学习了非极大值抑制的两种实现方法:

- 线性插值法:通过插值计算亚像素点的梯度值进行比较,结果精确但计算复杂。
- 方向近似法:将梯度方向近似到几个固定方向,直接使用实际像素点进行比较,计算简单高效,是实践中常用的方法。

无论采用哪种方法,非极大值抑制的本质都是在梯度方向上,只保留局部梯度最大的像素点,从而将粗宽的边缘“细化”为单像素宽的精确边缘,为后续的双阈值处理做好准备。




课程P17:Canny边缘检测效果详解 🎯
在本节课中,我们将深入学习Canny边缘检测算法中的关键步骤——双阈值检测,并了解如何在OpenCV中应用此算法。我们将通过对比不同参数设置的效果,来理解阈值选择对边缘检测结果的影响。
上一节我们介绍了Canny边缘检测的基本流程,本节中我们来看看其核心步骤之一:双阈值检测。
双阈值检测原理
双阈值检测是Canny边缘检测算法中的一个关键步骤。它涉及两个参数:minVal(最小阈值)和maxVal(最大阈值)。这两个参数用于对计算出的梯度幅值进行分类和筛选。
以下是梯度值处理的三种情况:


- 梯度值 > maxVal:该点被直接判定为强边缘。
- 公式表示:
gradient(x, y) > maxVal→ 强边缘点
- 公式表示:
- 梯度值 < minVal:该点被直接舍弃,认为不是边缘。
- 公式表示:
gradient(x, y) < minVal→ 非边缘点
- 公式表示:
- minVal < 梯度值 < maxVal:该点被标记为弱边缘候选点。其最终命运取决于它是否与强边缘点相连。
- 如果该弱边缘点与任何强边缘点相连,则它被保留为最终边缘。
- 如果该弱边缘点不与任何强边缘点相连,则它被舍弃。


这个步骤有效地过滤了噪声和虚假边缘,只保留真正有意义的边缘信息。


OpenCV中的Canny边缘检测

理解了双阈值检测的原理后,我们来看看如何在OpenCV中轻松实现Canny边缘检测。OpenCV将高斯滤波、梯度计算、非极大值抑制和双阈值检测等步骤封装成了一个简单的函数。
核心代码如下:
import cv2
# 读取图像并转换为灰度图
img = cv2.imread('lena.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)


# 应用Canny边缘检测
# 参数:输入图像,最小阈值(minVal),最大阈值(maxVal)
edges1 = cv2.Canny(gray, 80, 150) # 阈值设置较高
edges2 = cv2.Canny(gray, 50, 100) # 阈值设置较低

阈值参数的影响

minVal和maxVal的取值会显著影响边缘检测的结果。以下是参数设置的一般规律:

- 阈值设置较高(如80, 150):标准严格。只检测梯度变化非常明显的强边缘,结果中的边缘线条较少、较粗,但可能丢失一些细节。
- 阈值设置较低(如50, 100):标准宽松。会检测出更多的边缘,包括一些弱边缘和细节,但同时也可能引入更多噪声。
通过对比不同阈值下的输出图像,可以直观地看到这种差异。阈值较低时,图像的纹理和细节更丰富;阈值较高时,只保留最主体、最确信的边缘轮廓。

总结


本节课中我们一起学习了Canny边缘检测算法的双阈值检测步骤。我们明确了minVal和maxVal两个参数如何将像素点分为强边缘、弱边缘和非边缘三类,并通过连接性分析确定最终的边缘。最后,我们掌握了使用OpenCV的cv2.Canny()函数进行边缘检测的方法,并通过实验对比,理解了阈值参数大小对检测结果的直接影响:阈值越低,边缘越丰富(可能包含噪声);阈值越高,边缘越简洁(可能丢失细节)。在实际应用中,需要根据具体任务调整这两个参数以达到最佳效果。
课程P18:图像金字塔定义 🏛️
在本节课中,我们将要学习图像金字塔的概念。图像金字塔是一种将图像以多分辨率(即不同尺寸)进行表示的方法,其结构类似于金字塔,底层是原始的大尺寸图像,越往上尺寸越小。这种结构在计算机视觉的多个领域,如图像特征提取、图像融合和目标检测中,都有广泛应用。
图像金字塔概述
上一节我们介绍了课程主题,本节中我们来看看图像金字塔的具体形态。
金字塔的形状底层较大,越往上越小。图像金字塔就是将图像组合成类似金字塔的形状。
例如,底层可以是一张800×800像素的大图像。向上一层变换,图像尺寸缩小,例如变为400×400像素。继续向上,图像尺寸进一步缩小,例如变为200×200像素、100×100像素,最后可能变为50×50像素。这样,一张图像就被变换成了多种不同尺寸的形式。

构建图像金字塔的用途很多。例如,在进行图像特征提取时,我们可能不仅需要对原始输入图像提取特征,还需要对整个金字塔的每一层图像进行特征提取。不同层提取出的特征可能不同,将这些特征结果汇总在一起,可以获得更丰富或更鲁棒的特征表示。

接下来,我们将主要讲解图像金字塔的两种类型:高斯金字塔和拉普拉斯金字塔。它们的核心原理相似,都是构建金字塔形状的多分辨率图像。

高斯金字塔

高斯金字塔是图像金字塔的一种常见形式,其构建过程涉及高斯滤波和下采样操作。下面我们来看看它的具体做法。

高斯金字塔主要包含两种操作:向下采样(缩小图像)和向上采样(放大图像)。首先,我们来了解向下采样。
向下采样

向下采样的方向是沿着金字塔从底层向顶层(即从大图像向小图像)进行。其目标是实现图像的缩小操作。

以下是构建高斯金字塔(向下采样)的两个核心步骤:

第一步,使用高斯核对原始图像进行卷积滤波。高斯核是一个权重符合高斯(正态)分布的矩阵,用于对图像进行平滑处理。卷积操作是图像中每个像素点与其邻域像素按照核的权重进行加权求和的过程。通常,卷积后需要进行归一化。
第二步,进行下采样,即缩小图像尺寸。具体做法是,删除经过高斯滤波后的图像中的所有偶数行和偶数列。


例如,假设原始图像尺寸为8×8像素。经过高斯滤波后,我们删除其第2、4、6、8行和第2、4、6、8列,最终得到一个4×4像素的图像。这样,图像的长度和宽度都变为原来的一半,总面积变为原来的四分之一。

这个过程可以概括为:先高斯平滑,再隔行隔列删除像素。
向上采样

向上采样的方向与向下采样相反,是沿着金字塔从顶层向底层(即从小图像向大图像)进行。其目标是实现图像的放大操作。
以下是向上采样的具体步骤:

第一步,在每个方向上将图像尺寸扩大为原来的两倍。对于原始图像中的每个像素点,将其扩展成一个2×2的块。新增的像素位置用零值进行填充。
例如,一个2×2的原始图像,其像素值为 [[10, 13], [15, 16]]。扩大后,每个像素点变成2×2的块并用零填充,初步得到一个4×4的中间结果。
第二步,使用与向下采样中相同的高斯核,对这个填充零后的中间图像进行卷积操作。这个卷积过程会基于已有的像素值(如上例中的10, 13, 15, 16),计算出新插入位置(原为零值的位置)的像素值,从而生成最终的放大图像。其效果类似于将原始像素点的值“扩散”或“平均”到周围的区域。
总结
本节课中我们一起学习了图像金字塔的定义与构建方法。
我们首先了解了图像金字塔是一种多尺度图像表示方法,形状类似金字塔,底层图像尺寸最大,顶层最小。这种结构有助于进行多尺度的图像分析。


接着,我们重点学习了高斯金字塔的构建过程,它包含两种核心操作:
- 向下采样:通过高斯滤波后,删除所有偶数行和偶数列来实现图像缩小。
- 向上采样:通过在每个方向插入零值并将尺寸扩大两倍,再使用相同的高斯核进行卷积来实现图像放大。

理解向下采样和向上采样的原理,是掌握图像金字塔及其后续应用的基础。

OpenCV图像金字塔教程:P19 🏔️
在本节课中,我们将学习OpenCV中图像金字塔的概念与操作方法。图像金字塔是一种多尺度图像表示方法,常用于图像缩放、图像融合和特征提取等任务。我们将重点介绍两种常见的金字塔:高斯金字塔和拉普拉斯金字塔。
读取与展示图像
首先,我们需要读取一张图像并展示其原始样貌。以下是读取和展示图像的代码。
import cv2

# 读取图像
img = cv2.imread('antimage.jpg')
# 展示图像
cv2.imshow('Original Image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

执行上述代码后,我们可以看到原始图像。这是一张“敌法师”的游戏角色图片。接着,我们打印图像的形状值,以了解其尺寸信息。

print(img.shape)
输出结果为 (442, 340, 3)。前两个数字代表图像的高度和宽度(像素),第三个数字代表颜色通道数(RGB三通道)。
高斯金字塔:上采样与下采样
上一节我们介绍了如何读取图像,本节中我们来看看如何对图像进行缩放操作,即上采样和下采样,这构成了高斯金字塔的基础。

在OpenCV中,我们可以使用 pyrUp 函数进行上采样(放大图像),使用 pyrDown 函数进行下采样(缩小图像)。函数名中的 pyr 是金字塔(pyramid)的缩写。

以下是执行上采样的方法。
# 上采样
up_img = cv2.pyrUp(img)
cv2.imshow('Upsampled Image', up_img)
cv2.waitKey(0)
print(up_img.shape)
执行上采样后,图像尺寸会变为原来的两倍,打印出的形状值应为 (884, 680, 3)。从视觉上看,图像变大了,但清晰度会有所下降。
以下是执行下采样的方法。
# 下采样
down_img = cv2.pyrDown(img)
cv2.imshow('Downsampled Image', down_img)
cv2.waitKey(0)
print(down_img.shape)

执行下采样后,图像尺寸会减半,打印出的形状值应为 (221, 170, 3)。图像变小,同时会损失部分细节信息。

多次采样操作
我们不仅可以执行单次采样,还可以对采样结果进行连续操作。例如,对上采样的结果再次进行上采样。

# 连续两次上采样
up_img_once = cv2.pyrUp(img)
up_img_twice = cv2.pyrUp(up_img_once)
cv2.imshow('Twice Upsampled Image', up_img_twice)
cv2.waitKey(0)
print(up_img_twice.shape)
连续两次上采样后,图像尺寸变为原始的四倍。同样,下采样也可以连续进行。这种多尺度操作是构建图像金字塔的关键。
采样操作的信息损失
现在我们来思考一个问题:先对图像进行上采样,再对结果进行下采样,得到的图像会和原始图像完全一样吗?
答案是否定的。原因在于,上采样过程通常使用插值(如填充零值或平均)来生成新的像素,这会引入原图中不存在的信息。而下采样过程则会丢弃部分像素信息。因此,经过“上采样->下采样”这一过程后,图像会损失部分原始信息,导致清晰度下降。

我们可以通过代码来验证这一点。
# 先上采样,再下采样
up_then_down = cv2.pyrDown(cv2.pyrUp(img))
# 将原始图像与处理后的图像水平拼接对比
comparison = cv2.hconcat([img, up_then_down])
cv2.imshow('Original vs Up->Down', comparison)
cv2.waitKey(0)
cv2.destroyAllWindows()
在对比图中可以观察到,右侧经过“上采样->下采样”处理的图像,其边缘和细节会比左侧原始图像显得略微模糊。这直观地证明了两次采样过程带来的信息损失。
拉普拉斯金字塔
上一节我们探讨了高斯金字塔及信息损失,本节中我们来看看另一种金字塔——拉普拉斯金字塔。拉普拉斯金字塔用于保存图像在不同尺度下的细节信息,其每一层由高斯金字塔的某一层与其上一层的上采样结果之差构成。
拉普拉斯金字塔第 i 层的计算公式如下:
LPᵢ = Gᵢ - PyrUp(Gᵢ₊₁)
其中:
- LPᵢ 是拉普拉斯金字塔的第 i 层。
- Gᵢ 是高斯金字塔的第 i 层。
- Gᵢ₊₁ 是高斯金字塔的第 i+1 层(即 Gᵢ 下采样后的结果)。
- PyrUp(Gᵢ₊₁) 表示对 Gᵢ₊₁ 进行上采样,使其尺寸与 Gᵢ 相同。

简单来说,拉普拉斯金字塔的每一层,记录的是高斯金字塔某一层与其经过“下采样再上采样”重建后的版本之间的差异,这个差异主要包含了图像的边缘和纹理等高频细节信息。
以下是计算拉普拉斯金字塔第一层的代码示例。
# 生成高斯金字塔的下采样层(第一层)
gaussian_down = cv2.pyrDown(img)
# 对下采样结果进行上采样,使其尺寸与原始图像匹配
gaussian_up = cv2.pyrUp(gaussian_down)
# 计算拉普拉斯金字塔第一层:原始图像 - 上采样结果
laplacian_layer = cv2.subtract(img, gaussian_up)

# 展示结果
cv2.imshow('Laplacian Pyramid Layer', laplacian_layer)
cv2.waitKey(0)
cv2.destroyAllWindows()

拉普拉斯金字塔的结果图像看起来像是原始图像的轮廓信息。通过组合拉普拉斯金字塔的不同层与高斯金字塔的顶层,可以无损地重建原始图像。在后续的实际案例中,我们可能会利用金字塔的不同层来抽取对任务有价值的特征信息。
总结

本节课中我们一起学习了OpenCV中的图像金字塔技术。我们首先介绍了如何读取和展示图像。然后,详细讲解了高斯金字塔的上采样(pyrUp)和下采样(pyrDown)操作,并通过实验观察了连续采样及“上采样->下采样”过程带来的信息损失。最后,我们探讨了拉普拉斯金字塔的原理与计算方法,它通过保存不同尺度下的细节差异,为图像的多尺度分析提供了有力工具。掌握这两种金字塔是进行图像缩放、融合及高级特征提取的基础。
课程P2:Python与OpenCV环境配置安装 🛠️
在本节课中,我们将学习如何配置和安装本系列课程所需的Python与OpenCV开发环境。整个过程将分为几个清晰的步骤,确保即使是初学者也能顺利完成。
概述
本教程的核心目标是指导你完成Python环境的搭建,并安装OpenCV库。我们将使用Anaconda这一集成工具来简化安装过程,并详细说明如何通过命令行安装指定版本的OpenCV。
第一步:安装Python环境
首先,你需要安装Python。如果你已经安装过Python,可以直接使用现有环境。对于新用户,我们推荐使用Anaconda进行配置。
Anaconda是一个集成了Python、常用工具包和开发环境的“全家桶”。它包含了课程中所需的大部分组件,例如包管理工具pip和代码编写环境Jupyter Notebook,省去了单独配置的麻烦。

以下是安装Anaconda的步骤:
- 访问Anaconda官网。
- 根据你的操作系统(如Windows、macOS或Linux)选择对应的安装程序。
- 在版本选择中,请务必选择Python 3.x版本(例如3.7或3.8),不要选择已被淘汰的Python 2.7版本。
- 根据你的系统架构(通常是64位)下载对应的安装程序。
- 在Windows系统中,运行下载的
.exe文件,按照提示(下一步、选择安装目录、继续安装)即可完成安装。安装过程会自动配置好环境变量。
安装完成后,你无需进行任何额外配置。
第二步:验证与使用Anaconda环境
安装好Anaconda后,你可以在开始菜单中找到名为“Anaconda”的文件夹。其中有两个主要工具我们会用到:
- Anaconda Prompt:这是一个命令行工具,类似于Windows的CMD,但已配置好Anaconda环境。
- Jupyter Notebook:一个基于网页的交互式代码编写环境。
首先,我们打开Anaconda Prompt。接下来,我们将在这个命令行环境中安装OpenCV。
在安装前,需要确认你的Python环境。如果你只安装了一个Python环境(即Anaconda),可以直接使用pip命令。如果你有多个Python环境,则需要确保命令在正确的环境中执行。
你可以通过以下命令检查当前环境已安装的包:
pip list

执行该命令会列出所有已安装的Python包。你可以从中查找是否已存在opencv-python。

第三步:安装OpenCV
我们将使用pip命令来安装OpenCV,这是最简单的方法,无需下载源码进行复杂编译。
在课程中,我们使用OpenCV的特定版本 3.4.1.15。这是因为在3.4.2版本之后,一些特征提取算法因专利问题在开源版本中无法使用。为确保课程所有内容都能正常运行,建议安装此版本。
请在Anaconda Prompt中执行以下安装命令:
pip install opencv-python==3.4.1.15
这个命令会从网络下载OpenCV及其所有依赖包并自动安装。由于源服务器可能在国外,下载过程可能较慢,请耐心等待。
第四步:安装OpenCV扩展包
OpenCV从3.x版本开始,将部分额外功能(如某些特征提取算法)分离到了一个独立的扩展包中。为了使用完整功能,我们需要额外安装这个扩展包。
确保扩展包的版本号与核心包一致。在Anaconda Prompt中执行以下命令:
pip install opencv-contrib-python==3.4.1.15
第五步:验证安装
安装完成后,最好先在基础命令行环境中测试OpenCV是否能被正确导入,以排除IDE(集成开发环境)自身配置的问题。

在Anaconda Prompt中,输入Python进入交互模式,然后尝试导入OpenCV并查看版本:

import cv2
print(cv2.__version__)
如果以上命令没有报错,并且打印出版本号 3.4.1,则说明OpenCV已成功安装并配置完毕。
总结

本节课中,我们一起完成了Python与OpenCV开发环境的搭建。我们首先通过安装Anaconda获得了完整的Python环境,然后使用pip命令分别安装了指定版本的opencv-python核心包和opencv-contrib-python扩展包,最后通过简单的导入测试验证了安装成功。现在,你的开发环境已经准备就绪,可以开始后续的OpenCV学习了。
课程P2:YOLOv9详解 🚀
在本节课中,我们将学习目标检测领域的新星——YOLOv9。我们将了解其核心创新点、技术优势以及它如何超越前代模型。

距离YOLOv8发布仅一年,YOLOv9便已诞生。此次YOLOv9由中国台湾的台北科技大学等机构联合开发。


核心创新:可编程梯度信息(PGI)💡
上一节我们介绍了YOLOv9的诞生背景,本节中我们来看看其核心创新。研究者提出了可编程梯度信息的概念,以应对深度网络在实现多个目标时所需的各种变化。
该技术主打用可编程梯度信息来学习任何所需内容。此外,研究者基于梯度路径规划,设计了一种新的轻量级网络架构,即通用高效层聚合网络。
该架构证实了,PGI可以在轻量级模型上取得优异的结果。


性能表现:全面领先 🏆

了解了PGI的概念后,我们来看看YOLOv9的实际表现。无论是轻量级还是大型模型,YOLOv9都表现卓越,一举成为目标检测领域的新标杆。
与基于深度卷积开发的先进方法相比,YOLOv9仅使用传统卷积算子即可实现更好的参数利用率。

PGI的广泛适用性与优势 🔧

PGI的适用性很强,可用于从轻型到大型的各种模型。它能够帮助模型获取更完整的信息。
从而使从头开始训练的模型,能够比使用大型数据集预训练的先进模型获得更好的结果。


资源与社区评价 📚
YOLOv9的论文和代码已由社区整理。此外,YOLOv8到YOLOv9的全套课件与代码也已共享。
对于新发布的YOLOv9,曾参与开发YOLOv7等的作者给予了高度评价,表示YOLOv9优于任何基于卷积或Transformer架构的目标检测器。


对比实验与组件分析 ⚖️
为了更深入了解,研究者将YOLOv9与其他从头开始训练的目标检测器进行了全面比较。
使用传统卷积的YOLOv9,在参数利用率上,甚至比使用深度卷积的YOLO-MS模型还要好。


为了探究YOLOv9中各个组件的作用,研究者进行了深入的消融实验分析。




本节课中我们一起学习了YOLOv9的核心创新——可编程梯度信息,了解了其卓越的性能表现和广泛的适用性,并查看了相关的资源与对比实验。YOLOv9通过其新颖的设计,在目标检测领域树立了新的标准。

课程P20:轮廓检测方法 🖼️
在本节课中,我们将要学习图像处理中的一个重要概念——轮廓检测。我们将了解轮廓与边缘的区别,并掌握使用OpenCV库进行轮廓检测的具体步骤和方法。

轮廓与边缘的区别

上一节我们介绍了图像处理的基础,本节中我们来看看轮廓检测。首先需要明确轮廓与边缘的区别。
边缘检测通常得到的是图像中梯度变化显著的点,这些点可能形成零零散散的线段。然而,从轮廓的定义出发,轮廓必须是一个整体,其各个部分需要连接在一起。因此,轮廓是连续的、完整的边界,而边缘可能是离散的、不完整的线段。
轮廓检测函数
了解了轮廓的概念后,我们来看看如何进行轮廓检测。OpenCV提供了一个核心函数来实现此功能。
该函数的基本调用格式如下:
contours, hierarchy = cv2.findContours(image, mode, method)
该函数返回两个值:检测到的轮廓列表 contours 和轮廓之间的层次结构 hierarchy。

以下是该函数三个主要参数的解释:
- image: 输入图像。需要注意的是,该函数要求输入图像必须是二值图像。
- mode: 轮廓检索模式。它决定了函数如何检索和返回轮廓。
- method: 轮廓逼近方法。它决定了如何存储轮廓上的点。
轮廓检索模式 (mode)

对于 mode 参数,有多个可选值,但最常用的是 cv2.RETR_TREE。该模式会检测图像中所有的轮廓,并建立一个完整的层次结构(嵌套关系)来保存它们。这样,无论你需要最外层的轮廓还是所有内部轮廓,都可以方便地获取。

轮廓逼近方法 (method)
对于 method 参数,有两种最常用的方法。
cv2.CHAIN_APPROX_NONE: 存储轮廓上的所有点。例如,一个矩形的轮廓会存储四条边上的每一个像素点。cv2.CHAIN_APPROX_SIMPLE: 对轮廓进行压缩,仅存储关键点。例如,一个矩形的轮廓经过压缩后,只会存储四个顶点。这种方法可以显著减少内存占用和计算负担。
轮廓检测步骤

上一节我们介绍了检测函数,本节中我们来看看具体如何操作。轮廓检测不能直接应用于彩色或灰度图,需要遵循特定的预处理流程。


以下是进行轮廓检测的标准步骤:

- 读取图像:使用
cv2.imread()函数加载原始图像。 - 转换为灰度图:使用
cv2.cvtColor()函数将彩色图像转换为灰度图像,这是图像处理的常见第一步。 - 图像二值化:使用
cv2.threshold()函数将灰度图转换为二值图像(只有黑色0和白色255)。这是轮廓检测的关键前提,因为cv2.findContours()函数只在二值图像上工作。 - 执行轮廓检测:在得到的二值图像上调用
cv2.findContours()函数,获取轮廓信息。


步骤演示

我们以一张汽车图片为例进行说明。

- 原始图像:一张彩色的汽车图片。
- 灰度图像:将彩色图转换为灰度图。
- 二值图像:对灰度图应用阈值处理(例如,大于127的设为255白色,小于等于127的设为0黑色),得到黑白分明的二值图。
- 检测轮廓:最后,将这张二值图输入到
cv2.findContours()函数中,即可得到汽车的所有轮廓。


总结

本节课中我们一起学习了图像轮廓检测的核心知识。我们首先区分了轮廓(连续整体)与边缘(离散线段)的概念。然后,重点介绍了OpenCV中 cv2.findContours() 函数的使用,包括其参数含义,特别是 mode(检索模式)和 method(逼近方法)的选择。最后,我们明确了轮廓检测的标准流程:读取图像 -> 转灰度图 -> 二值化 -> 检测轮廓。掌握这些步骤是进行后续轮廓分析(如绘制、计算面积周长等)的基础。
课程P21:轮廓检测结果详解 🎯
在本节课中,我们将学习如何使用OpenCV的findContours函数检测图像中的轮廓,以及如何将检测到的轮廓绘制在图像上。我们将详细讲解函数的参数、返回值,并重点解决一个常见的绘图陷阱。
概述
上一节我们介绍了图像二值化的预处理步骤。本节中,我们来看看如何对处理后的二值图像进行轮廓检测,并正确地将轮廓可视化。
轮廓检测操作
以下是执行轮廓检测的核心代码步骤。首先,我们需要调用cv2.findContours函数。
contours, hierarchy = cv2.findContours(binary_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
该函数接收几个关键参数:
- binary_image: 经过二值化处理后的图像。
- cv2.RETR_TREE: 轮廓检索模式。此模式会检测所有轮廓并重建完整的嵌套层次结构。
- cv2.CHAIN_APPROX_SIMPLE: 轮廓近似方法。此方法压缩水平、垂直和对角线段,仅保留其端点,从而节省内存。
执行这行代码后,函数会返回两个值:
- contours: 一个Python列表,其中包含了图像中所有轮廓的信息。每个轮廓本身又是一个由点构成的数组。
- hierarchy: 一个包含轮廓之间层级关系(如父子关系)的数组。本节课我们暂时不深入讨论这个参数。
绘制检测到的轮廓
检测到轮廓后,我们需要将其绘制出来以观察效果。我们将使用cv2.drawContours函数。
在绘制轮廓前,有一个非常重要的步骤。drawContours函数会直接在传入的原图上进行修改。为了避免污染原始图像数据,我们需要先创建原图的一个副本。
# 创建原图的副本,用于绘制轮廓
image_copy = original_image.copy()
如果不进行复制,原始图像会在每次绘制轮廓时被改变,这会影响后续的任何操作或分析。
以下是绘制轮廓的完整代码:
# 在图像副本上绘制所有轮廓
result = cv2.drawContours(image_copy, contours, -1, (0, 0, 255), 2)
cv2.drawContours函数的参数含义如下:
- image_copy: 要在其上绘制的图像(我们使用副本)。
- contours: 之前检测到的轮廓列表。
- -1: 要绘制的轮廓索引。-1 表示绘制列表中的所有轮廓。
- (0, 0, 255): 轮廓的颜色,使用BGR格式。此处(0,0,255)代表红色。
- 2: 轮廓线条的宽度(以像素为单位)。
参数效果演示


为了更好地理解参数的作用,我们可以进行一些调整。
以下是drawContours函数中contourIdx参数(第三个参数)的演示:
- 设置为 -1 或 1 时,会绘制
contours列表中的所有轮廓。 - 设置为 0 时,只绘制列表中的第一个轮廓(索引为0)。
- 设置为 1 时,只绘制列表中的第二个轮廓(索引为1),依此类推。
以下是线条宽度参数(最后一个参数)的演示:
- 将线条宽度从 2 改为 5,轮廓线会明显变粗。
- 注意:如果线条宽度设置过大,相邻的轮廓(尤其是嵌套轮廓)可能会在视觉上融合在一起,难以区分。
总结

本节课中我们一起学习了OpenCV轮廓检测的核心流程。我们首先使用findContours函数从二值图像中提取轮廓信息,然后重点强调了在调用drawContours绘制轮廓前,必须创建原图副本以避免数据被意外修改。最后,我们探讨了如何通过调整绘制函数的参数来控制显示哪些轮廓以及轮廓的外观。掌握这些步骤是进行物体识别、形状分析等高级计算机视觉任务的基础。
📐 课程 P22:轮廓特征与近似
在本节课中,我们将学习如何计算轮廓的特征,例如面积和周长,并探索轮廓近似的方法。这些技术对于从图像中提取结构化信息和简化形状表示至关重要。


轮廓特征计算

上一节我们介绍了如何检测和绘制轮廓。本节中,我们来看看如何计算轮廓的几何特征。OpenCV 提供了多个函数来计算这些特征,但在使用前,需要将具体的轮廓提取出来。

以下是计算轮廓面积和周长的核心方法:
- 计算面积:使用
cv2.contourArea(contour)函数,传入一个轮廓即可得到其面积。 - 计算周长:使用
cv2.arcLength(contour, closed)函数。参数closed通常设为True,表示轮廓是闭合的。
# 假设 contours 是之前检测到的轮廓列表
cnt = contours[0] # 提取第一个轮廓
area = cv2.contourArea(cnt) # 计算面积
perimeter = cv2.arcLength(cnt, True) # 计算周长
轮廓近似原理
有时,检测到的轮廓可能包含许多不规则的“毛刺”。轮廓近似(Contour Approximation)的目标是用更少、更规则的线段来近似原始轮廓,从而简化其形状。
其基本原理是 Douglas-Peucker 算法,过程可以概括为:
- 连接曲线(轮廓)的起点 A 和终点 B,得到一条直线段 AB。
- 在原始曲线上找到离直线段 AB 距离最远的点 C。
- 如果点 C 到直线 AB 的距离 小于 设定的阈值(T),则认为直线 AB 足以近似整条曲线。
- 如果该距离 大于 阈值 T,则算法会递归处理。即,分别用直线 AC 近似曲线 AC 部分,用直线 CB 近似曲线 CB 部分,并重复步骤 2 和 3 的判断。
- 递归过程持续进行,直到所有曲线段都能用直线段在阈值范围内近似为止。
最终,复杂的曲线被一系列首尾相连的直线段所替代。

轮廓近似实践

了解了原理后,我们来看看如何在 OpenCV 中实现轮廓近似。以下是一个完整的示例流程:


import cv2
# 1. 读取图像并转为灰度图
img = cv2.imread('shape.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 2. 二值化处理
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)


# 3. 查找轮廓
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)


# 4. 提取一个轮廓并进行近似
cnt = contours[0] # 以第一个轮廓为例
epsilon = 0.01 * cv2.arcLength(cnt, True) # 设置阈值为周长的 1%
approx = cv2.approxPolyDP(cnt, epsilon, True) # 进行轮廓近似


# 5. 绘制近似后的轮廓
# 注意:approx 本身也是一个轮廓,需要重新绘制
img_approx = img.copy()
cv2.drawContours(img_approx, [approx], -1, (0, 255, 0), 3)
cv2.imshow('Approximated Contour', img_approx)
cv2.waitKey(0)


参数 epsilon 的影响:
epsilon值越小,近似结果越接近原始轮廓,细节保留越多。epsilon值越大,近似结果越简化,可能变成一个三角形甚至一条直线。- 通常,
epsilon被设置为轮廓周长的一个百分比(如 0.01 * 周长),需要根据实际需求调整。



轮廓的外接形状
除了近似,我们还可以为轮廓计算外接的几何形状,这有助于获取轮廓的包围框或拟合形状,进而计算更多特征(如面积比)。
以下是计算外接矩形和外接圆的方法:


- 外接矩形(Bounding Rectangle):
cv2.boundingRect(cnt)返回一个矩形的 (x, y, w, h),即左上角坐标和宽高。 - 最小外接矩形(Rotated Rectangle):
cv2.minAreaRect(cnt)返回一个可以旋转的矩形。 - 外接圆:
cv2.minEnclosingCircle(cnt)返回圆心坐标和半径。



# 计算并绘制外接矩形
cnt = contours[0]
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2) # 用绿色绘制

# 计算并绘制外接圆
(x, y), radius = cv2.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
cv2.circle(img, center, radius, (255, 0, 0), 2) # 用蓝色绘制
特征衍生示例:通过外接矩形,可以计算轮廓的“紧密度”或“饱满度”特征,例如 轮廓面积 / 外接矩形面积。这个比值越接近1,说明轮廓形状越饱满,越接近矩形。

总结


本节课中我们一起学习了轮廓分析的两个核心操作:特征计算与形状近似。
- 我们掌握了如何计算轮廓的面积和周长。
- 我们理解了轮廓近似(Douglas-Peucker算法) 的原理,并学会了使用
cv2.approxPolyDP函数来简化轮廓形状。 - 我们探索了如何为轮廓计算外接矩形和外接圆,并了解到这些外接形状可用于衍生出更多有价值的图像特征。


这些技术是图像处理和计算机视觉中形状分析的基础,在物体识别、测量和分类等任务中有着广泛的应用。

课程 P23:模板匹配方法 🧩


在本节课中,我们将学习如何在 OpenCV 中进行模板匹配。这是一种在较大图像中定位和查找与给定模板图像最相似区域的技术。
概述
模板匹配的核心思想是,将一个较小的模板图像(例如一张人脸)在一个较大的原始图像上滑动,并逐块计算相似度,以找到最佳匹配位置。
上一节我们介绍了图像处理的基础概念,本节中我们来看看如何具体实现模板匹配。
什么是模板匹配?
首先,我们通过一个例子来理解模板匹配的工作。
假设我们有两张图像:
- 第一张是角色“丽娜”的脸部截图,作为我们的模板。
- 第二张是“丽娜”的完整原始图像。
我们的目标是:在原始图像中,找到与脸部模板最相似的那个区域。这就像拿着一个固定大小的“框”(模板),在原始图像上从左到右、从上到下移动,并逐一比较框内内容与模板的相似度。
匹配原理

模板匹配的过程类似于卷积操作中的滑动窗口过滤。

- 滑动窗口:将模板窗口在原始图像上从左到右、从上到下移动。
- 计算相似度:在每个停留位置,计算模板与当前图像窗口的匹配程度。
- 匹配度计算:计算方法有多种,核心是比较对应像素点的差异。例如,对于一个 3x3 的模板和图像区域,可以逐个像素进行减法运算。


公式示例(平方差方法):
R(x, y) = Σ [T(x‘, y’) - I(x + x‘, y + y’)]²
其中,T 是模板图像,I 是原始图像,(x, y) 是当前窗口的左上角坐标。这个值越小,表示该区域与模板越相似。

OpenCV 提供了多种匹配方法,不同方法对“最佳匹配”的定义不同(例如有的值越小越匹配,有的值越大越匹配)。


OpenCV 中的实现步骤

以下是实现模板匹配的关键步骤。


1. 读取图像与模板

首先,需要读取原始图像和模板图像,并通常将它们转换为灰度图以简化计算。

import cv2

# 读取原始图像和模板图像,并转为灰度图
img = cv2.imread(‘lena.jpg‘, 0)
template = cv2.imread(‘lena_face.jpg‘, 0)



2. 执行模板匹配

使用 cv2.matchTemplate 函数进行匹配。需要指定原始图像、模板图像和匹配方法。


# 执行模板匹配
result = cv2.matchTemplate(img, template, cv2.TM_SQDIFF_NORMED)

OpenCV 提供了多种匹配方法,例如:
cv2.TM_SQDIFF_NORMED:归一化平方差匹配法,结果越接近0匹配度越高。cv2.TM_CCORR_NORMED:归一化相关匹配法,结果越接近1匹配度越高。

建议:通常使用带“NORMED”(归一化)的方法,结果更可靠。


3. 理解匹配结果


匹配函数返回一个结果矩阵(result)。这个矩阵的大小由原始图像和模板图像的尺寸决定。

结果矩阵尺寸公式:
结果宽度 = 原始图像宽度 - 模板宽度 + 1
结果高度 = 原始图像高度 - 模板高度 + 1

这个矩阵中的每个值,代表了模板在原始图像对应滑动窗口位置的匹配得分。


4. 定位最佳匹配点
由于结果矩阵包含大量数据,OpenCV 提供了 cv2.minMaxLoc 函数来快速找到最值及其位置。


# 寻找结果矩阵中的最小值、最大值及其位置
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
根据所使用的匹配方法,决定使用最小值位置(min_loc)还是最大值位置(max_loc)作为最佳匹配点。例如,对于 TM_SQDIFF_NORMED 方法,最小值点就是最佳匹配的左上角坐标。

5. 绘制匹配区域


获得最佳匹配的左上角坐标后,结合模板的宽度(w)和高度(h),即可在原始图像上绘制出匹配的矩形框。


# 获取模板尺寸
h, w = template.shape[:2]
# 对于 TM_SQDIFF_NORMED 方法,使用最小值位置
top_left = min_loc
bottom_right = (top_left[0] + w, top_left[1] + h)


# 在原始图像上绘制红色矩形框
cv2.rectangle(img, top_left, bottom_right, (0, 0, 255), 2)
cv2.imshow(‘Matched Image‘, img)
cv2.waitKey(0)

核心流程总结


让我们再梳理一遍完整的操作流程:

- 准备阶段:读取并转换原始图像和模板图像为灰度图。
- 匹配计算:调用
cv2.matchTemplate函数,选择一种匹配方法进行计算。 - 分析结果:使用
cv2.minMaxLoc函数,根据所选方法找到最佳匹配点(最小值或最大值位置)。 - 可视化结果:利用模板尺寸和找到的最佳点坐标,在原始图像上绘制矩形框,标出匹配区域。


总结


本节课中我们一起学习了 OpenCV 中的模板匹配技术。我们了解了其基本思想是在大图中滑动小模板以寻找最相似区域,掌握了使用 cv2.matchTemplate 和 cv2.minMaxLoc 函数实现匹配和定位的关键步骤,并学会了根据不同的匹配方法正确解读结果。模板匹配是计算机视觉中一项基础且实用的技术,为后续更复杂的图像识别任务奠定了基础。
课程P24:模板匹配效果展示与多对象匹配 🎯
在本节课中,我们将学习OpenCV中模板匹配的六种不同方法,并比较它们的结果差异。同时,我们还将探讨如何从图像中匹配多个对象,而不仅仅是单个最佳匹配。
模板匹配方法差异对比
上一节我们介绍了模板匹配的基本原理,本节中我们来看看不同匹配方法得到的结果有何不同。
在代码中,我们使用 cv2.matchTemplate 函数,传入图像模板和指定的匹配方法(method)。
result = cv2.matchTemplate(img, template, method)
关于 method 参数,需要注意以下几点:
- 可以传入一个数值(如
0、1),也可以传入cv2.开头的常量(如cv2.TM_CCOEFF)。 - 不能传入字符串形式(例如
"TM_CCOEFF"),否则会报错。
为了判断匹配结果的最佳位置,我们根据方法类型进行判断:
- 如果方法是寻找最小值(如
cv2.TM_SQDIFF),则取结果中的最小值位置。 - 如果方法是寻找最大值(如
cv2.TM_CCOEFF),则取结果中的最大值位置。
这涉及到 cv2.minMaxLoc 函数返回的 min_loc 和 max_loc。
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

匹配任务类似于检测任务,我们需要将最佳匹配位置用矩形框标注出来。我们已经通过模板的宽高(w, h)和最佳位置坐标确定了矩形框。

以下是绘制矩形的代码:
cv2.rectangle(img, top_left, bottom_right, color, thickness)
result 矩阵是根据不同方法计算出的每个滑动窗口的匹配结果值。对于某些方法(如相关系数法),最亮的位置就代表最接近模板的区域。
下面展示了不同匹配方法的效果对比图:








观察发现,只要方法名称中带有“归一化”(_NORMED)后缀的,匹配结果通常更稳定、更准确。







因此,建议在实际使用中优先选择归一化的方法。
多对象模板匹配 🎮
我们已经学会了如何匹配单个最佳对象。但新的问题来了:如果图像中有多个相似对象(例如,一张图中有两张人脸),而我们有一个模板,如何找到所有匹配的位置,而不是仅仅一个?



这就需要用到多对象模板匹配。其核心思想是:不再只取一个最小值或最大值,而是自己设定一个阈值,找出所有符合该阈值的匹配位置。
以下是实现多对象匹配的关键步骤:
首先,读取图像并转换为灰度图,这与单对象匹配的第一步相同。
img = cv2.imread('image.jpg')
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
然后,进行模板匹配,得到结果矩阵 result。
接着,设定一个阈值。例如,使用相关系数法(cv2.TM_CCOEFF_NORMED)时,值越接近1表示匹配度越高。我们可以设定阈值为 0.8。
threshold = 0.8
使用 np.where 函数在 result 矩阵中找出所有匹配度大于该阈值的位置。

locations = np.where(result >= threshold)
最后,遍历这些找到的所有位置,并在每个位置绘制矩形框。

for pt in zip(*locations[::-1]):
cv2.rectangle(img, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 2)

让我们看一个具体例子。我们的模板是一个金币:


输入图像是《超级玛丽》的游戏画面,其中包含多个金币:


应用多对象匹配后,我们可以将所有的金币都框选出来:






课程总结 📝
本节课中我们一起学习了OpenCV模板匹配的进阶应用。
我们首先对比了六种不同匹配方法的效果,了解到归一化方法(如 TM_CCOEFF_NORMED)通常能提供更稳定的结果。其原理是从图像左上角开始滑动窗口,计算模板与图像局部区域的差异程度。

cv2.matchTemplate 函数返回一个结果矩阵,我们可以通过 cv2.minMaxLoc 找到单个最佳匹配位置(min_loc 或 max_loc)。
当需要匹配图像中出现的多个相似对象时,我们可以通过自行设定阈值,并使用 np.where 来找出所有满足条件的匹配位置,从而实现多对象模板匹配。

这就是OpenCV中模板匹配从单对象到多对象的核心操作方法。

图像处理基础课程 P25:直方图定义 📊
在本节课中,我们将要学习图像处理中的一个核心概念——直方图。我们将了解直方图的定义、如何在OpenCV中计算直方图,并初步探索其应用场景。
什么是直方图? 📈
直方图是一种统计图表。当我们第一次听到“直方图”时,可能会联想到统计数值。然而,我们现在处理的对象是图像。我们需要将图像分解为像素点,统计的对象就是这些像素点。

上图左侧的A图展示了一张灰度图像及其像素点。右侧则是对应的直方图。那么,直方图与像素点之间是如何联系的呢?
例如,图中有一个像素值为109的点。这个值并非唯一,图像中其他位置也可能存在值为109的像素点。图中就有两个值为120的像素点。由于像素值范围是0到255,总共256个值,因此相同的像素值在图像中会重复出现。
我们要统计的是:在图像中,每个像素值(或称为灰度级)出现的次数。



直方图的横坐标通常是像素值,从0到255。纵坐标则是对应像素值在图像中出现的个数。这样,我们就得到了一个描述图像像素值分布的直方图。
如何在OpenCV中计算直方图? 🛠️

上一节我们介绍了直方图的基本概念,本节中我们来看看如何在OpenCV中具体计算它。
OpenCV提供了一个专门计算直方图的函数:cv2.calcHist。

以下是该函数主要参数的解释:

- images:输入图像。通常我们输入灰度图像。
- channels:指定要统计的通道索引。对于BGR彩色图像,可以用0、1、2分别表示B、G、R通道。
- mask:掩码。用于指定只统计图像的某一部分区域。如果不使用,则统计整张图像。
- histSize:直方图的柱子(bins)数量。例如,设置为256表示统计0到255每个值的出现次数。也可以分组,如设置为[10],则表示将0-255分成10组进行统计。
- ranges:像素值的取值范围。对于8位灰度图,通常是[0, 256]。
对于后两个参数,如果我们希望得到每个像素值的精确统计,通常固定设置为 histSize=[256], ranges=[0, 256]。


动手实践:计算并绘制直方图 🎨
现在,让我们通过代码来实际计算一张图像的直方图。
首先,读取一张图像。这里我们读取一张小猫的图像并将其转换为灰度图。
import cv2
img = cv2.imread('cat.jpg', 0) # 参数0表示以灰度模式读取

接下来,使用 cv2.calcHist 函数计算直方图。需要注意,images 参数需要放在中括号 [] 内传入。
hist = cv2.calcHist([img], [0], None, [256], [0, 256])
print(hist.shape) # 输出类似 (256, 1),表示有256个值,每个值对应一个计数

执行后,我们得到了一个包含256个值的数组,每个值代表对应像素值(0-255)在图像中出现的次数。

最后,我们可以使用Matplotlib库将直方图绘制出来。
import matplotlib.pyplot as plt
plt.figure()
plt.title("Grayscale Histogram")
plt.xlabel("Bins") # 像素值
plt.ylabel("# of Pixels") # 像素数量
plt.plot(hist)
plt.xlim([0, 256]) # 限制x轴范围
plt.show()


彩色图像的直方图 🌈
对于彩色图像,我们可以分别计算B、G、R三个通道的直方图。以下是操作方法:

img_color = cv2.imread('cat.jpg') # 读取彩色图像
colors = ('b', 'g', 'r') # 对应BGR通道的颜色

plt.figure()
plt.title("Color Histogram")
plt.xlabel("Bins")
plt.ylabel("# of Pixels")
for i, color in enumerate(colors):
# 计算每个通道的直方图
hist = cv2.calcHist([img_color], [i], None, [256], [0, 256])
plt.plot(hist, color=color) # 用对应颜色绘制
plt.xlim([0, 256])
plt.show()



观察与分析直方图 🔍
观察我们得到的小猫图像直方图,可以发现其分布并不均匀。

在像素值150到200的区间内,像素点数量非常集中,而其他区间的像素点则相对较少。这种分布特点反映了图像本身的明暗和对比度信息。
直方图是图像分析的重要工具。通过观察直方图,我们可以判断图像是偏亮、偏暗还是对比度不足。在下一节课中,我们将学习如何利用直方图进行图像增强,例如直方图均衡化,来改善图像的视觉效果。



总结 📝
本节课中我们一起学习了:
- 直方图的定义:统计图像中每个像素值出现次数的图表。
- OpenCV计算直方图:使用
cv2.calcHist(images, channels, mask, histSize, ranges)函数。 - 绘制直方图:使用Matplotlib库可视化灰度图及彩色图各通道的直方图。
- 直方图的分析:直方图形状反映了图像的亮度、对比度等整体特征。

理解直方图是进行更高级图像处理(如图像增强、分割、识别)的基础。

📊 课程 P26:直方图均衡化原理与掩码操作
在本节课中,我们将学习图像处理中的两个核心概念:掩码(Mask)操作与直方图均衡化(Histogram Equalization)的基本原理。我们将了解如何创建和使用掩码来选取图像特定区域,并深入探讨直方图均衡化的数学原理,以实现图像对比度的增强。

🎭 掩码(Mask)操作

上一节我们介绍了直方图的基本概念,本节中我们来看看如何利用掩码对图像的特定区域进行操作。

掩码是一个与原始图像尺寸相同的矩阵,用于指定图像中需要处理或保留的区域。在OpenCV中,掩码通常由0和255两种值构成,其中255代表需要保留的区域,0代表需要忽略的区域。
以下是创建和使用掩码的基本步骤:


- 创建掩码矩阵:使用
np.zeros函数创建一个与原始图像尺寸相同的全零矩阵。
代码解释:mask = np.zeros(image.shape[:2], dtype=np.uint8)image.shape[:2]获取图像的高度和宽度,dtype=np.uint8指定数据类型为8位无符号整数(范围0-255)。

- 指定保留区域:将需要保留的区域像素值设置为255。
代码解释:此操作将掩码矩阵中行100到300、列150到400的矩形区域设置为白色(255)。mask[100:300, 150:400] = 255


- 应用掩码:使用按位与操作(
cv2.bitwise_and)将原始图像与掩码结合,仅保留掩码中白色区域对应的图像部分。
公式解释:masked_image = cv2.bitwise_and(image, image, mask=mask)masked_image(x, y) = image(x, y) if mask(x, y) > 0 else 0。即,当掩码值大于0时,输出原图像素值;否则输出0。

掩码操作的核心作用在于区域截取。通过构造特定的掩码矩阵并与原始图像组合,我们可以精确地提取出图像中感兴趣的部分,例如猫的脸部,而忽略背景。


⚖️ 直方图均衡化原理

了解了如何利用掩码操作图像局部后,我们回到直方图本身。观察图像的直方图,我们常发现像素灰度值分布不均,导致图像对比度低、细节不清。直方图均衡化的目标就是将“瘦高”的直方图分布转换为“矮胖”的、更均匀的分布,从而增强图像的整体对比度。


那么,如何实现从一种分布到另一种分布的映射呢?其核心是一个基于累积分布函数(CDF)的变换。


以下是直方图均衡化的计算原理:


-
统计原始直方图:计算图像中每个灰度级
r_k出现的像素数目n_k。- 设图像总像素数为
N,则灰度级r_k的概率为:p_r(r_k) = n_k / N。
- 设图像总像素数为
-
计算累积分布函数(CDF):计算从最小灰度级到当前灰度级的累积概率。
- 灰度级
r_k的累积概率cdf(r_k)计算公式为:cdf(r_k) = Σ_{j=0}^{k} p_r(r_j)。
- 灰度级


- 进行灰度映射:利用CDF将原始灰度值
r_k映射到新的灰度值s_k。- 映射公式为:
s_k = round( cdf(r_k) * (L - 1) )。 - 公式解释:
L是灰度级总数(例如对于8位图像,L=256)。round()表示四舍五入取整。此步骤将累积概率范围[0, 1]线性映射到新的灰度范围[0, L-1]。
- 映射公式为:


举例说明:
假设有一小片图像的灰度值集合为 [50, 50, 50, 50, 128, 128, 128, 200, 200, 200, 200, 200, 255, 255, 255, 255],共16个像素(N=16)。
-
统计概率:
- p(50) = 4/16 = 0.25
- p(128) = 3/16 = 0.1875
- p(200) = 5/16 = 0.3125
- p(255) = 4/16 = 0.25
-
计算累积概率(CDF):
- cdf(50) = 0.25
- cdf(128) = 0.25 + 0.1875 = 0.4375
- cdf(200) = 0.4375 + 0.3125 = 0.75
- cdf(255) = 0.75 + 0.25 = 1.0
-
映射到新灰度值(L=256):
- s(50) = round(0.25 * 255) = 64
- s(128) = round(0.4375 * 255) = 112
- s(200) = round(0.75 * 255) = 191
- s(255) = round(1.0 * 255) = 255
经过均衡化,原始灰度值被重新分配。原本聚集在少数灰度级的像素被分散到一个更宽的范围内,使得直方图分布更为平坦,图像的亮度和对比度从而得到增强。
📝 课程总结
本节课中我们一起学习了:
- 掩码操作:我们掌握了如何创建二值掩码矩阵,并通过按位与运算提取图像的特定区域。这是许多高级图像处理任务(如ROI分析)的基础。
- 直方图均衡化原理:我们深入理解了均衡化的核心数学原理——基于累积分布函数(CDF)的灰度映射。通过将原始直方图的累积概率线性拉伸到整个灰度范围,实现了图像对比度的有效增强。

理解这些基本原理,是后续应用OpenCV等库中相应函数(如cv2.equalizeHist)进行实际图像处理的关键。下一节课,我们将学习如何在代码中实现这些操作。

课程P27:直方图均衡化效果实战 🖼️

在本节课中,我们将学习如何使用OpenCV进行直方图均衡化操作,并对比全局均衡化与自适应均衡化的效果差异。

读取示例图像

首先,我们读取一张具有特点的猫图像。这张图像的直方图分布非常不均衡,适合用来演示均衡化效果。
import cv2
# 读取图像
img = cv2.imread('cat.jpg', 0) # 以灰度模式读取

全局直方图均衡化

上一节我们介绍了直方图的概念,本节中我们来看看如何使用OpenCV进行均衡化。OpenCV提供了cv2.equalizeHist()函数来实现全局直方图均衡化。


以下是进行全局均衡化的步骤:

- 调用
cv2.equalizeHist()函数。 - 将原始图像作为参数传入。
- 函数会返回均衡化后的图像。
# 进行全局直方图均衡化
equ = cv2.equalizeHist(img)


均衡化完成后,我们可以绘制并对比原始图像与均衡化后图像的直方图。原始图像的直方图分布集中(较“尖”),而均衡化后的直方图分布变得更为平缓和分散(变“胖”),这表明像素值的分布变得更加均匀。

均衡化效果视觉对比
接下来,我们直观地对比均衡化前后的图像。以下是两张图像的并排展示:

- 左侧:原始图像,整体色彩较淡,对比度较低。
- 右侧:全局均衡化后的图像,整体亮度提升,对比度增强,视觉效果更明显。


我们换用另一张人像(如Lena图)进行测试,可以观察到类似的趋势:均衡化后的图像看起来更亮、更显眼。

全局均衡化的局限性
然而,全局均衡化并非总是最佳选择。当我们处理一些包含丰富细节的图像时,全局操作可能会带来问题。
例如,在某张测试图像中,均衡化后整体亮度虽然得到提升,但人脸部分的细节却丢失了。这是因为全局均衡化是对整幅图像的像素值进行重新分配,可能会将某些局部区域的特征“平均”掉。
自适应直方图均衡化
为了解决全局均衡化可能丢失细节的问题,我们可以采用分块处理的思路,即自适应直方图均衡化。其核心思想是将图像划分为多个小格子(例如8x8的块),每个格子独立进行自身的均衡化。

OpenCV提供了cv2.createCLAHE()方法来创建自适应均衡化器。

以下是使用自适应均衡化的步骤:

- 创建CLAHE对象,可设置格子大小(
tileGridSize)和对比度限制(clipLimit)。 - 应用该对象到输入图像上。
# 创建自适应直方图均衡化器
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
# 应用自适应均衡化
cl1 = clahe.apply(img)

注:为了避免分块处理在格子边界处产生不自然的痕迹,OpenCV在内部还进行了插值等额外处理。

综合效果对比
最后,我们将原始图像(A)、全局均衡化结果(B)和自适应均衡化结果(C)放在一起进行对比。
通过观察可以发现:
- 图像C(自适应) 在提升整体亮度的同时,很好地保留了图像中的纹理和细节,效果优于图像B。
- 图像B(全局) 虽然提升了对比度,但损失了部分细节。
- 图像A(原始) 对比度最低。

这个对比说明,当需要保留图像具体细节时,自适应均衡化通常是比全局均衡化更好的选择。当然,局部处理也可能受到噪声的影响,在实际应用中需要根据具体情况权衡。


课程总结

本节课中我们一起学习了:
- 直方图的含义:图像中像素值分布的统计图表。
- 直方图均衡化的操作:使用
cv2.equalizeHist()对图像进行全局对比度增强。 - 均衡化的效果与局限:能提升整体对比度,但可能导致局部细节丢失。
- 自适应均衡化:使用
cv2.createCLAHE()进行分块均衡化,能在增强对比度的同时更好地保留细节。
课程P28:傅里叶变换概述 🌀
在本节课中,我们将学习傅里叶变换的基本概念,并探讨它在图像处理领域能够完成哪些任务。
什么是傅里叶变换?
首先,我们来理解什么是傅里叶变换。想象一个现实生活中的场景:我们早上七点起床吃早饭,八点去挤地铁,九点开始上班,十点工作,十一点开会,十二点吃午饭,下午四点盼着下班,五点等大家走得差不多了再离开。
这段话是以时间为参照进行描述的,我们称之为时域分析。生活中绝大多数事情都与时间相关,例如篮球比赛分为上半场、中场休息和下半场,每节比赛打满12分钟。我们的生活也是如此,随着时间推移,我们长大并经历各种事情。
然而,傅里叶变换从一个更抽象的视角来看待问题,可以理解为一种“上帝视角”。在这个视角下,它并不关心你每天具体按顺序做了什么,而是关注你做了哪些事以及这些事发生的间隔或频率。
例如,从上帝视角看:
- 你每天都要吃早饭。
- 你每个工作日(而非休息日)都要挤地铁。
- 你每个工作日都需要上班。
因此,傅里叶变换更关心的是在频域中发生的事情。时域描述的是随着时间进行、不断运动的序列;而频域描述的则是事件发生的规律和频率,更像是一种静止的、模式化的描述。
为了更清晰地说明,我们来看一个篮球比赛的例子。假设球星库里在8分钟的比赛中得分,我们以分钟为单位记录他投进三分球和两分球的情况。
在时域中,我们这样描述:
- 第1分钟:投进1个三分球,0个两分球。
- 第2分钟:投进0个三分球,1个两分球。
- 第3分钟:投进1个三分球,0个两分球。
- ... 以此类推。
这描述了随着比赛时间推进,库里得分事件的具体序列。
而在频域中,描述则简化为:
- 在整个比赛中,每隔一分钟投进一个三分球。
- 同时,每隔一分钟投进一个两分球。
频域描述的核心在于规律和频率。

时域与频域的直观联系

上一节我们介绍了时域和频域的基本概念。本节中,我们通过一个生动的例子来看看它们如何联系起来。


这里分享一篇讲解得非常通俗易懂的文章(非本人所写),它逐步阐述了傅里叶变换的原理。虽然其中涉及较多知识点,图像处理中未必全部用到,但其核心图示极具启发性。

傅里叶提出:任何周期函数都可以用一系列正弦波叠加而成。如下图所示,作者试图用正弦波来近似表示一个矩形波。


观察上图:
- 仅用一个正弦波(第一排)无法表示矩形。
- 叠加两个正弦波(第二排)后,近似效果有所改善。
- 叠加的正弦波数量越多(第三排及之后),对矩形的近似就越精确。
这直观地展示了傅里叶变换的核心思想:用不同频率的正弦波组合来构建复杂波形。
那么,这如何从时域转换到频域呢?关键在于一句话:换个方向观察。


当我们从侧面观察这些叠加的正弦波时,就得到了频域的结果。在上图右侧:
- 横轴代表频率(即正弦波变化的快慢,频率 = 1/周期)。越靠右,频率越高,波形变化越剧烈。
- 纵轴代表振幅(即正弦波振动的幅度)。不同频率的正弦波对应不同的振幅。
因此,只需理解一点:时域中的波形,换个角度(侧面)观察,就映射到了频域。在频域中,我们以频率和振幅来描述信号,这使得分析某些问题(如图像中的纹理、噪声)变得更加容易。
总结

本节课我们一起学习了傅里叶变换的概述。我们首先通过生活实例对比了时域(随时间变化的序列)和频域(描述事件发生规律和频率)的区别。然后,我们借助正弦波叠加的图示,直观理解了傅里叶变换如何将复杂的时域信号分解为不同频率的正弦波组合,并通过“换个方向观察”这一比喻,建立了时域信号与其频域表示之间的对应关系。理解时域与频域这两种观察世界的不同视角,是掌握傅里叶变换及其在图像处理中应用的基础。

课程P29:2-频域变换结果 🔄


在本节课中,我们将要学习傅里叶变换在图像处理中的具体应用,特别是低通与高通滤波器的概念和作用。我们将通过OpenCV的代码实践,展示如何将图像转换到频域,并理解不同频率分量在图像中的意义。
上一节我们介绍了傅里叶变换的基本概念,本节中我们来看看它在图像处理中的具体作用。

傅里叶变换的作用 📊

傅里叶变换将图像从空间域转换到频域。在频域中,我们可以分析图像的不同频率分量。
低通与高通滤波器 🎛️
既然提到滤波器,其目的是保留一些成分并排除另一些成分。以下是低通和高通滤波器的定义:
- 低频:在图像中,变化缓慢的灰度分量。例如,一片颜色均匀的大海或草原,其像素值变化不大。
- 高频:在图像中,变化剧烈的灰度分量。例如,物体与背景之间的边界,其像素值在短距离内发生显著变化。


以下是两种滤波器的具体作用:
- 低通滤波器:只保留低频信息,去除高频信息。由于图像边界(高频)被模糊,处理后的图像会变得模糊。
- 高通滤波器:只保留高频信息,去除低频信息。这会增强图像的边缘和细节,产生锐化的效果。
OpenCV中的DFT与IDFT ⚙️

在OpenCV中,我们主要使用两个函数进行傅里叶变换及其逆变换。

cv2.dft():执行离散傅里叶变换,将图像从空间域转换到频域。cv2.idft():执行逆离散傅里叶变换,将图像从频域转换回空间域。

频域数据(包含实部和虚部)无法直接显示,通常需要逆变换回图像才能观察效果。

代码实现要点 💻
使用OpenCV进行傅里叶变换时,需要注意以下几点:
- 输入格式:输入图像必须转换为
np.float32格式。 - 频谱中心化:变换后得到的频谱,其低频部分默认在四角。为了方便观察和处理,通常使用
np.fft.fftshift()函数将低频部分移动到频谱中心。 - 结果转换:
cv2.dft()输出的是双通道(实部和虚部)结果。为了将其显示为图像,需要转换为单通道的幅度谱。公式为:
magnitude_spectrum = 20 * np.log(cv2.magnitude(real, imag))
此公式计算幅度并对数缩放,以便更好地显示。
代码演示与结果分析 🖼️
以下是核心代码步骤分析:

import cv2
import numpy as np
# 1. 读取图像并转换为灰度图
img = cv2.imread('lena.png', 0)
# 2. 将图像转换为np.float32格式
dft_input = np.float32(img)
# 3. 执行傅里叶变换
dft_output = cv2.dft(dft_input, flags=cv2.DFT_COMPLEX_OUTPUT)

# 4. 将频谱的低频部分移动到中心
dft_shift = np.fft.fftshift(dft_output)

# 5. 计算幅度谱(实部real和虚部imag)
real, imag = cv2.split(dft_shift)
magnitude_spectrum = 20 * np.log(cv2.magnitude(real, imag))

# 6. 将幅度谱值缩放到0-255范围以便显示
magnitude_spectrum_normalized = cv2.normalize(magnitude_spectrum, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)

运行代码后,我们将得到原始图像和其对应的频域幅度谱图。

在幅度谱图中,越亮的点代表该频率成分的幅度越大。中心区域最亮,代表低频成分(图像的主体和缓慢变化部分)能量最高。从中心向外发散,亮度逐渐降低,代表高频成分(图像的边缘和细节)能量较低。


本节课中我们一起学习了傅里叶变换在图像处理中的应用,理解了低频与高频分量的图像意义,掌握了低通与高通滤波器的概念,并实践了使用OpenCV进行傅里叶变换及频谱可视化的完整流程。
课程P3-1:腐蚀操作详解 🧽
在本节课中,我们将要学习图像形态学处理中的基础操作之一——腐蚀操作。腐蚀操作主要用于处理二值图像,能够有效地去除图像中的细小毛刺、分离粘连的物体,并使前景物体的边界向内收缩。
概述
腐蚀操作是形态学处理的核心操作之一。它通常应用于二值图像(即像素值仅为0或255的图像),通过一个预定义的“核”在图像上滑动并进行逻辑判断,使得图像中的白色前景区域(高亮部分)根据其邻域情况被“侵蚀”或缩小。
腐蚀操作原理
上一节我们介绍了腐蚀操作的基本概念,本节中我们来看看其具体的工作原理。
腐蚀操作的核心思想是:使用一个结构元素(通常是一个小矩阵,称为“核”)扫描图像中的每一个像素。对于每一个像素位置,只有当核覆盖下的所有像素值都为前景值(例如255)时,该中心像素才被保留为前景;否则,该像素将被置为背景值(例如0)。
这个过程可以用一个简单的公式来描述。假设 A 是原始二值图像,B 是结构元素核,腐蚀操作 A ⊖ B 定义为:
A ⊖ B = { z | (B)_z ⊆ A }
其中,(B)_z 表示将核 B 的原点平移到位置 z。这意味着,只有当核 B 完全包含在图像 A 的前景区域内时,输出图像在位置 z 的点才为前景。

为了更直观地理解,我们可以看一个简单的代码示例。在OpenCV中,腐蚀操作通过 cv2.erode() 函数实现。
import cv2
import numpy as np
# 读取一张二值图像(例如黑色背景,白色前景)
image = cv2.imread('di_ge.png', cv2.IMREAD_GRAYSCALE)
# 定义一个3x3的矩形核,所有值均为1
kernel = np.ones((3, 3), np.uint8)
# 应用腐蚀操作,迭代次数为1
eroded_image = cv2.erode(image, kernel, iterations=1)
# 显示结果
cv2.imshow('Original', image)
cv2.imshow('Eroded', eroded_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
执行上述代码后,可以观察到图像中的白色线条变细,并且一些细小的毛刺被去除了。
核的大小与迭代次数

理解了腐蚀的基本原理后,我们来看看影响腐蚀效果的两个关键参数:核的大小和迭代次数。

核的大小决定了在每次判断时,考察的邻域范围。一个较大的核意味着在判断一个点是否被腐蚀时,需要其周围更大范围内的点都是前景,因此腐蚀效果更强烈,物体收缩得更快。
迭代次数则决定了腐蚀操作被重复执行的次数。每次腐蚀都会在前一次结果的基础上,使前景区域进一步向内收缩。
以下是不同参数设置对腐蚀效果影响的说明:

- 核的大小:核越大,腐蚀作用越强,前景物体收缩得越明显。
- 迭代次数:迭代次数越多,腐蚀操作被重复应用的次数越多,前景物体收缩得越小。
我们可以通过一个实验来观察迭代次数的影响。对同一张图像(例如一个白色的圆形)分别进行1次、2次和3次腐蚀操作。

原始输入图像是一个圆形。经过不同次数的腐蚀后,结果如下:


从结果可以清晰地看到,随着迭代次数(i)的增加,圆形区域逐渐变小。i=1时,边界均匀内缩一圈;i=2时,内缩更明显;i=3时,圆形已经变得非常小。这直观地展示了迭代次数对腐蚀效果的影响。

总结

本节课中我们一起学习了形态学中的腐蚀操作。
我们首先了解了腐蚀操作能够去除图像毛刺、细化线条和分离物体的作用。然后,我们深入探讨了其工作原理,即通过一个结构元素核在图像上滑动,仅当核完全覆盖前景区域时才保留中心像素。最后,我们分析了影响腐蚀效果的两个关键参数——核的大小和迭代次数,并通过实验观察了它们如何改变处理结果。

掌握腐蚀操作是理解更复杂形态学操作(如膨胀、开运算、闭运算)的重要基础。
课程P3:YOLOv8详解与实战 🚀
在本节课中,我们将要学习物体检测的核心概念,并重点掌握当前前沿的YOLOv8模型。我们将梳理YOLO系列的发展历程,理解每一代版本的核心改进,并最终学习如何使用YOLOv8进行训练、推理以及源码层面的理解与自定义修改。
概述:什么是物体检测?
物体检测是计算机视觉中的一项核心任务。其目标是识别图像中特定物体的位置,并用边界框将其标注出来。
具体来说,物体检测需要完成两件事:
- 确定物体在图像中的位置(即边界框的坐标)。
- 识别该物体是什么(即物体的类别)。
无论是YOLO系列还是其他检测算法,其根本目标都是一致的。
YOLO系列发展简史 📜
上一节我们介绍了物体检测的基本概念,本节中我们来看看YOLO系列是如何一步步演进的。
YOLOv1 (2016):奠定基础框架
YOLOv1在2016年出现,奠定了YOLO系列的基本框架。其核心思想非常直接:
- 将输入图像通过卷积神经网络提取特征,得到特征图。
- 特征图上的每个点都对应原始图像中的一个区域。
- 对每个区域,模型需要预测两个核心信息:
- 该区域存在物体的置信度(Confidence)。
- 如果存在物体,则预测该物体边界框的精确位置(中心点坐标和长宽)。
因此,每个区域需要预测5个值:(置信度, 中心点x, 中心点y, 宽度w, 高度h)。
YOLOv1从一开始就确立了追求速度快的目标。
YOLOv2 (2018):引入锚框(Anchor Boxes)
YOLOv1中,每个区域只预测一种形状的边界框。但在现实中,物体有“高矮胖瘦”之分。
YOLOv2的改进在于:为每个区域预设多种不同尺寸和比例的锚框(Anchor Boxes)。这样,模型在每个区域会同时检查是否存在符合不同锚框形状的物体,使得检测更加全面。
YOLOv3 (2020):多尺度预测(FPN)
在YOLOv1和v2中,模型只在网络最深层的特征图上进行预测,这主要适合检测大目标。因为深层特征感受野大,能看到全局信息。
YOLOv3引入了特征金字塔网络(FPN) 的思想,实现了多尺度预测:
- 深层特征:感受野大,负责预测大目标。
- 中层特征:感受野适中,负责预测中等目标。
- 浅层特征:感受野小,关注细节,负责预测小目标。
这种“术业有专攻”的设计,显著提升了模型对不同尺寸目标的检测能力,尤其是小目标的召回率。
YOLOv4 & YOLOv5 (2020):集大成与工程化
YOLOv4可以看作是一个“集百家之长”的算法。它广泛吸收了当时计算机视觉领域各种有效的技巧和模块(如新的激活函数、注意力机制、数据增强方法等),并将其融合到YOLO框架中,使网络结构更复杂、性能更强。
YOLOv5与v4几乎同期出现。可以简单理解为:YOLOv4是侧重创新的研究论文,而YOLOv5是高度优化、易于使用的工程项目。YOLOv5因其出色的工程实现和易用性,被广泛应用于工业界。
YOLOv6 & YOLOv7 (2022):效率与结构优化
YOLOv6和v7发布时间非常接近。其中YOLOv7的应用更广泛,其主要改进点包括:
- 层级堆叠的模块设计:在网络中并行地融合不同层级的特征,再进行聚合。这种设计能综合利用不同感受野的信息,相当于为模型提供了多条学习路径,增强了鲁棒性。
- 改进的正负样本分配策略:在训练时,更精细地定义哪些锚框是正样本(对应真实物体),哪些是负样本(对应背景),这有助于模型更有效地学习。
YOLOv8 (2023):统一、便捷的新框架
现在,让我们聚焦到今天的重点——YOLOv8。它给人的整体印象可以用两个词概括:方便和统一。
- 方便:相比前几代,YOLOv8的使用门槛极低。通过pip安装一个包,几行代码就能开始训练或推理,2分钟即可上手。
- 统一:YOLOv8不再仅仅是一个目标检测框架,而是一个统一的视觉框架。它使用相同的主干网络(Backbone),通过不同的输出头(Head)来支持多种任务,包括:
- 目标检测
- 实例分割
- 图像分类
- 姿态估计
- 目标跟踪
这意味着,学习一个框架,就能解决多种视觉任务,极大地提升了学习和使用效率。
YOLOv8 网络结构解析 🏗️
了解了YOLO的发展史后,我们具体来看YOLOv8的网络结构。其整体架构与前几代一脉相承,主要由三部分组成:
- 主干网络(Backbone):负责从输入图像中提取多层次的特征。
- 颈部网络(Neck):负责融合和聚合来自主干网络不同层级的特征(例如,使用FPN+PAN结构)。
- 检测头(Head):基于融合后的特征,进行最终的分类和边界框回归预测。
YOLOv8在结构上一个显著的变化是引入了 C2f 模块(替换了YOLOv5中的C3模块)。C2f模块的设计借鉴了梯度流的思想,通过更丰富的跨层连接,在保证轻量化的同时,促进了信息流动。
以下是C2f模块的一个简化示意代码,帮助理解其多分支结构:
# 伪代码,示意C2f模块结构
class C2f(nn.Module):
def __init__(self, c1, c2, n=1, shortcut=False):
super().__init__()
self.c = c2 // 2 # 通道数减半
self.cv1 = Conv(c1, 2 * self.c, 1) # 初始卷积,通道数翻倍以用于分割
self.cv2 = Conv((2 + n) * self.c, c2, 1) # 最终融合卷积
# 多个Bottleneck模块构成的核心处理块
self.m = nn.ModuleList(Bottleneck(self.c, self.c, shortcut) for _ in range(n))
def forward(self, x):
y = list(self.cv1(x).chunk(2, 1)) # 将特征图在通道维度切分成两部分
y.extend(m(y[-1]) for m in self.m) # 一部分经过多个Bottleneck处理
return self.cv2(torch.cat(y, 1)) # 将所有分支的结果拼接并融合
YOLOv8 实战:快速训练自定义模型 ⚡
理论部分已经清晰,本节我们将手把手学习如何使用YOLOv8训练你自己的模型。整个过程非常简单。
第一步:安装与环境准备
YOLOv8的安装极其简单,只需一行命令:
pip install ultralytics
这个ultralytics包包含了YOLOv8的所有源码和依赖。
第二步:准备数据配置文件

YOLOv8要求将数据集的路径和类别信息写在一个YAML配置文件中。这是你需要准备的核心文件。

以下是一个数据配置文件的示例模板:
# 数据集配置文件示例:data_custom.yaml
path: /home/user/datasets/my_custom_data # 数据集根目录
train: images/train # 训练集图像路径(相对于path)
val: images/val # 验证集图像路径(相对于path)
test: images/test # 测试集图像路径(可选)

# 类别名称列表
names:
0: person
1: bicycle
2: car
# ... 你的其他类别
你需要按照此格式,将自己的数据集整理好,并修改path和names等内容。
第三步:选择模型与开始训练
准备好数据后,只需几行Python代码即可启动训练。
以下是训练代码示例:
from ultralytics import YOLO
# 1. 加载一个预训练模型(YOLOv8提供了不同尺寸的模型,如n, s, m, l, x)
model = YOLO('yolov8n.pt') # 这里加载最小的 yolov8n 模型

# 2. 使用自定义数据训练模型
results = model.train(
data='path/to/your/data_custom.yaml', # 上一步准备的数据配置文件路径
epochs=100, # 训练轮数
imgsz=640, # 输入图像尺寸
batch=16, # 批次大小
name='my_custom_model' # 本次训练实验的名称
)

训练完成后,模型权重会自动保存,你可以直接使用训练好的模型进行推理。

第四步:使用模型进行推理
训练完成后,对新图像或视频进行检测同样简单。
以下是推理代码示例:
from ultralytics import YOLO
# 加载训练好的最佳模型
model = YOLO('runs/detect/my_custom_model/weights/best.pt')
# 对图像进行推理
results = model('path/to/your/test_image.jpg')
# 结果会自动保存,并可视化显示检测框

# 对视频进行推理
results = model('path/to/your/test_video.mp4', save=True)
总结

本节课中我们一起学习了YOLOv8的方方面面。

我们从物体检测的基本概念出发,回顾了YOLO系列从v1到v8的发展历程,理解了每一代的核心贡献:v1奠定快速检测框架,v2引入锚框,v3实现多尺度预测,v4/v5集大成并工程化,v7优化模块与样本分配,直至最新的v8成为一个统一、便捷的视觉框架。
我们解析了YOLOv8的网络结构,认识了其标志性的C2f模块。最后,我们通过一个完整的实战流程,学习了如何安装YOLOv8、准备数据、训练自定义模型并进行推理。整个过程凸显了YOLOv8设计哲学:让最先进的技术变得简单易用。

希望本教程能帮助你快速入门YOLOv8,并将其应用到你的实际项目中。

课程P30:3-低通与高通滤波 📊

在本节课中,我们将学习如何使用傅里叶变换在频域中实现图像的低通滤波和高通滤波。我们将通过构建掩码来保留或过滤特定的频率成分,从而实现对图像不同特征的提取。
概述
上一节我们介绍了傅里叶变换的基本概念。本节中,我们来看看如何利用傅里叶变换的结果,通过构建掩码来分离图像中的低频和高频信息,从而实现低通与高通滤波。

计算图像中心与尺寸

首先,我们需要读取图像并进行傅里叶变换。为了后续构建掩码,我们需要计算图像的中心点坐标。

以下是计算图像尺寸和中心点的代码:

# 假设 `dft_shift` 是经过傅里叶变换并中心化后的结果
rows, cols = dft_shift.shape[:2]
crow, ccol = int(rows/2), int(cols/2) # 计算中心点坐标


计算中心点的目的是为了在频域中定位低频区域,因为经过 fftshift 操作后,低频成分集中在频谱图的中心。

构建低通滤波器 🎯

低通滤波器的目的是保留图像中的低频信息(如平滑区域和整体轮廓),过滤掉高频信息(如边缘和噪声)。在频域中,低频位于中心区域。
以下是构建低通滤波器掩码的步骤:
- 创建一个与图像尺寸相同的全零矩阵。
- 以频谱中心点为原点,指定一个矩形区域(例如上下左右各30像素)。
- 将该矩形区域内的值设为1(白色),其余区域保持为0(黑色)。这个矩阵就是我们的低通掩码。

# 创建低通滤波器掩码
mask_low = np.zeros((rows, cols, 2), np.uint8)
mask_low[crow-30:crow+30, ccol-30:ccol+30] = 1
这个掩码中,值为1的区域(中心白色矩形)将被保留,值为0的区域(周围黑色部分)将被过滤掉。


应用掩码与逆变换
构建好掩码后,我们需要将其应用到频域数据上,然后进行逆傅里叶变换以恢复图像。

以下是应用低通滤波的完整流程:

- 结合掩码:将掩码与傅里叶变换后的频谱图进行逐元素相乘。这样,只有掩码中为1的频率成分被保留。
fshift_low = dft_shift * mask_low - 频率中心还原:在进行逆变换之前,需要将频谱中心移回左上角,即执行
ifftshift操作。f_ishift_low = np.fft.ifftshift(fshift_low) - 逆傅里叶变换:执行逆离散傅里叶变换(IDFT),将处理后的频域数据转换回空间域。
img_back_low = cv2.idft(f_ishift_low) - 计算幅度谱:逆变换的结果包含实部和虚部,我们需要计算其幅度来得到可显示的图像。
img_back_low = cv2.magnitude(img_back_low[:,:,0], img_back_low[:,:,1])

应用低通滤波后,得到的图像会变得模糊,因为只保留了低频信息,丢失了构成清晰边缘的高频细节。

构建高通滤波器 🔍

高通滤波器的目的与低通相反,它保留图像中的高频信息(如边缘和纹理),过滤掉低频信息(如平滑区域)。

构建高通滤波器掩码的逻辑与低通正好相反:

- 创建一个全一矩阵(表示最初保留所有频率)。
- 将中心矩形区域的值设为0(表示过滤掉低频)。

# 创建高通滤波器掩码
mask_high = np.ones((rows, cols, 2), np.uint8)
mask_high[crow-30:crow+30, ccol-30:ccol+30] = 0

这个掩码中,中心区域为0(黑色)被过滤,周围区域为1(白色)被保留。

应用高通滤波的后续步骤(结合掩码、逆变换)与低通滤波完全相同。得到的结果图像将主要包含物体的轮廓和边界,而内部细节区域会变暗或消失。

频域处理的优势

你可能会问,为什么要在频域进行这些处理?


在空间域(原始图像)中直接区分或操作低频与高频信息非常困难。而在频域中,不同频率的成分被分离开来,低频集中在中心,高频分布在四周。这种结构化的分布使得我们能够通过简单的掩码操作(如画矩形)来精确地选择或过滤特定频率,操作变得直观且高效。因此,许多图像处理任务会先将图像转换到频域进行处理,以获得更好的效果和更高的效率。
总结

本节课中我们一起学习了傅里叶变换在图像滤波中的应用。我们掌握了如何通过构建掩码来实现低通滤波(保留低频使图像模糊)和高通滤波(保留高频突出边缘)。关键在于理解频域中频率的分布规律,并利用 fftshift、掩码乘法以及 ifftshift 和 idft 这一套流程在频域与空间域之间进行转换。这为我们后续进行更复杂的图像处理奠定了基础。

课程31:银行卡号识别项目实战流程与方法讲解 💳
在本节课中,我们将学习如何利用OpenCV完成一个银行卡号识别的实战项目。我们将把之前学过的图像处理、轮廓检测、模板匹配等知识点串联起来,实现从一张银行卡图像中定位并识别出卡号的功能。
项目概述与目标
我们要做的是:输入一张银行卡图像,程序需要完成两件事。第一,识别出卡上的数字序列,例如“4000 1234 5678 9010”。第二,不仅要输出数字,还需要在图像上标出每组数字对应的位置,例如用方框将第一组“4000”框起来。

这个任务与我们生活中停车场自动识别车牌的原理类似,只不过我们处理的对象是银行卡图像。

总体流程与方法

上一节我们明确了项目目标,本节中我们来看看实现这个目标的具体流程和方法。核心思想是结合轮廓检测与模板匹配。
核心方法:模板匹配
识别单个数字的核心方法是模板匹配。我们需要预先准备一个包含数字0-9的模板图像。当程序从银行卡上截取到一个数字区域(例如一个“4”)后,会拿这个区域与模板中的每一个数字进行匹配计算(例如计算平方差)。匹配度最高(即差异最小或相似度最大)的那个模板数字,就被判定为当前截取的数字。
核心公式/概念:
在模板匹配中,常使用归一化平方差匹配法,其公式可表示为:
R(x, y) = Σ [T(x', y') - I(x + x', y + y')]^2
其中,T 是模板图像,I 是输入图像,R 是结果矩阵,值越小表示匹配度越高。
关键步骤:轮廓检测与外接矩形
为了进行模板匹配,我们首先需要从模板图像和银行卡图像中分别提取出每个独立的数字区域。
以下是实现这一目标的关键步骤:
- 准备专用模板:模板的字体必须与待识别银行卡上的数字字体高度相似,否则匹配准确率会很低。
- 轮廓检测:对模板图像和预处理后的银行卡图像进行轮廓检测。我们需要的是每个数字的外轮廓。
- 获取外接矩形:对检测到的每个轮廓,计算其外接矩形。这个矩形框定了每个数字的区域。
- 模板数字提取:根据外接矩形,从模板图像中裁剪出每个数字(0-9)的独立图像,并存储起来以备匹配使用。
- 银行卡数字区域提取:同样,根据银行卡图像上检测到的轮廓和外接矩形,初步定位可能的数字区域。
预处理与轮廓过滤
在实际操作中,直接从原始图像进行轮廓检测会得到大量无关轮廓(如卡面上的文字、Logo等)。因此,需要一系列预处理和过滤操作。
以下是关键的预处理与过滤步骤:
- 图像预处理:将彩色银行卡图像转换为灰度图,然后通过阈值处理(二值化)为后续轮廓检测做准备。
- 形态学操作:利用膨胀、腐蚀等形态学操作,帮助连接数字笔画、去除细小噪声,使数字区域的轮廓更完整、更易于检测。
- 轮廓过滤:对检测到的所有轮廓,根据其外接矩形的长宽比和面积进行过滤。银行卡数字具有特定的长宽比例,而“MC”、“VISA”等标志的轮廓比例与之不同。通过设定合理的阈值,可以过滤掉大部分非数字轮廓。
- 轮廓排序:银行卡号通常分为4组。我们需要将过滤后得到的数字轮廓,按照其水平位置(x坐标)进行排序,以确保最终输出的数字顺序是正确的。
执行匹配与输出
经过上述步骤,我们得到了模板数字库和银行卡上待识别的数字区域列表。
以下是匹配与输出的最终步骤:
- 尺寸归一化:将银行卡上截取到的每个数字区域图像,缩放(
resize)到与模板数字相同的尺寸。 - 循环匹配:对于银行卡上的每一个数字区域,循环与模板中的10个数字进行匹配,找到最相似的那个,并记录其值。
- 绘制与输出:根据匹配结果,在原始银行卡图像上,在每个数字区域的外接矩形位置绘制方框,并标注识别出的数字。同时,在控制台输出完整的卡号序列。

总结


本节课中,我们一起学习了银行卡号识别项目的完整流程。我们首先明确了项目目标,即定位并识别卡号。然后,我们深入探讨了实现这一目标的核心方法——模板匹配,并详细阐述了支撑该方法的轮廓检测与外接矩形提取技术。最后,我们梳理了从图像预处理、轮廓过滤到最终匹配输出的全链路步骤,其中涉及了灰度转换、二值化、形态学操作等关键预处理技术。这个项目是将OpenCV多个基础知识点综合应用的典型范例。

课程P32:OCR模板匹配实战 - 环境配置与预处理 🛠️
在本节课中,我们将学习如何配置OCR模板匹配项目的开发环境,并完成图像预处理的第一步。我们将使用Eclipse IDE进行代码调试,并详细解释从读取图像到二值化的每一个步骤。
环境配置与参数设置
上一节我们介绍了OCR模板匹配的基本概念,本节中我们来看看如何配置环境并设置运行参数。
首先,在代码中需要指定两个核心参数:输入图像和模板图像。在命令行中,这通常通过 -I 和 -T 参数完成。在本教程中,我们使用Eclipse IDE进行演示,因为它支持多种语言(如C、Java、Python)并具备强大的调试功能,便于逐行理解代码逻辑。
以下是配置运行参数的步骤:

- 在Eclipse中,右键点击当前的Python文件(例如
ocr_template_match.py)。 - 选择 Run As -> Run Configurations...。
- 在打开的配置窗口中,确保左侧选中了你的Python文件。
- 在右侧的 Arguments 选项卡中,填写程序运行所需的参数。

具体参数格式如下:
--image /path/to/your/credit_card_image.jpg --template /path/to/your/template_image.png
--image参数指定输入图像的路径,例如一张包含信用卡的图片。--template参数指定模板图像的路径,即包含数字0-9的单个字符图片。


配置完成后,点击 Apply 和 Close。这样,程序在运行时就会自动加载这些参数。



代码执行与整体流程预览


在深入每一行代码之前,我们先整体运行一次程序,观察完整的处理流程和最终效果。这有助于建立对项目目标的直观认识。

程序运行后,会依次展示以下图像处理步骤的结果图:


- 读取原始模板:加载0-9的数字模板图片。
- 模板灰度化:将彩色模板转换为灰度图。
- 模板二值化:将灰度图转换为黑白二值图,便于轮廓检测。
- 模板轮廓检测与外接矩形计算:找出每个数字的轮廓,并计算其外接矩形框。
- 读取输入图像:加载待识别的信用卡图片。
- 输入图像灰度化。
- 输入图像二值化。
- 形态学操作(顶帽运算):突出明亮的区域。
- Sobel梯度计算:检测边缘。
- 闭操作:连接相邻的白色区域,形成完整的数字块。
- 轮廓检测:识别出信用卡上可能的数字组区域。
- 数字分割与匹配:将每个数字组区域单独提取、二值化,并分割成单个数字,最后与模板进行匹配。
- 输出结果:在原始图像上框出识别出的数字并显示结果。

最终,程序会成功输出信用卡上的数字序列,例如“5412 7512 3456 7890”。



逐步调试与代码详解


理解了整体流程后,我们现在通过调试模式,逐行分析代码,理解每个操作的具体实现和目的。建议你在自己的IDE中为关键步骤打上断点,跟随教程一步步执行。


第一步:读取与预处理模板图像

首先,程序读取模板图像并进行预处理,为后续的轮廓提取做准备。

以下是核心代码步骤:

# 1. 读取模板图像
template = cv2.imread(args["template"])
# 2. 转换为灰度图
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
# 3. 转换为二值图像
template_binary = cv2.threshold(template_gray, 10, 255, cv2.THRESH_BINARY_INV)[1]

代码解释:
cv2.imread(): 读取图像文件。cv2.cvtColor(..., cv2.COLOR_BGR2GRAY): 将BGR格式的彩色图像转换为灰度图。这是图像处理的常见第一步,能减少计算量。cv2.threshold(..., cv2.THRESH_BINARY_INV): 应用阈值函数将灰度图转换为二值图(黑白图)。THRESH_BINARY_INV表示进行反向二值化,即原图中较暗的部分(数字)在新图中变为白色(255),背景变为黑色(0)。这符合轮廓检测函数对输入的要求。

预处理后的模板图像中,每个数字都是独立的白色连通区域,背景为黑色,为下一步轮廓检测做好了准备。

第二步:检测模板轮廓并排序
预处理完成后,我们需要从二值化的模板图像中提取出每个数字的轮廓,并按照从左到右的顺序进行排序,以便与后续识别出的数字正确对应。

以下是关键操作:

# 1. 查找轮廓
contours = cv2.findContours(template_binary.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = imutils.grab_contours(contours)
# 2. 对轮廓进行从左到右的排序
contours = sorted(contours, key=lambda c: cv2.boundingRect(c)[0])

代码解释:
cv2.findContours(): 在二值图像中查找轮廓。参数RETR_EXTERNAL表示只检测最外层轮廓,CHAIN_APPROX_SIMPLE是轮廓的近似方法,压缩水平、垂直和对角线段,仅保留端点。imutils.grab_contours(): 一个兼容性函数,确保在不同OpenCV版本下都能正确获取轮廓列表。sorted(..., key=lambda c: cv2.boundingRect(c)[0]): 对轮廓列表进行排序。cv2.boundingRect(c)会返回轮廓c的外接矩形(x, y, width, height)。我们根据矩形的左上角x坐标([0])进行排序,从而实现从左到右的排列。


排序后,contours 列表中的第一个轮廓对应数字“0”,第二个对应数字“1”,依此类推。这个顺序关系将在最后的模板匹配阶段起到关键作用。



本节课中我们一起学习了OCR模板匹配项目的初始配置与图像预处理阶段。我们配置了Eclipse的运行参数,预览了完整的处理流程,并详细分析了读取、灰度化、二值化模板图像以及检测并排序轮廓的代码。下一节课,我们将继续调试,学习如何对输入(信用卡)图像进行一系列复杂的预处理操作,以提取出待识别的数字区域。
课程P33:3-模板处理方法 📄➡️🔢
在本节课中,我们将学习如何从一张包含数字的图像中提取并处理数字模板。核心步骤包括轮廓检测、轮廓排序以及根据排序结果提取每个数字的图像区域,最终构建一个数字到其模板图像的映射字典。

轮廓检测与提取 🔍


上一节我们介绍了图像预处理,本节中我们来看看如何检测并提取图像中的数字轮廓。

首先,对输入图像进行轮廓检测。需要使用 cv2.findContours 函数,并指定参数为只检测最外层轮廓,因为内层轮廓对于计算数字的外接矩形没有用处。
contours, _ = cv2.findContours(image_copy, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

以下是关键参数说明:
cv2.RETR_EXTERNAL:表示只检测外轮廓。cv2.CHAIN_APPROX_SIMPLE:压缩轮廓的坐标点,节省内存。

执行后,我们获得了图像中所有的轮廓。可以将其绘制出来以验证结果。

cv2.drawContours(show_img, contours, -1, (0, 255, 0), 2)

绘制结果显示共有10个轮廓,与图像中的数字数量(0-9)一致,说明检测正确。

轮廓排序 📊

虽然检测到了10个轮廓,但它们的顺序并不一定对应数字0到9的顺序。因此,我们需要根据每个轮廓在图像中的水平位置(从左到右)进行排序。
我们定义一个工具函数来实现排序。其核心思想是:
- 计算每个轮廓的最小外接矩形。
- 获取每个矩形左上角的X坐标。
- 根据X坐标的大小对所有轮廓进行排序。


def sort_contours(contours):
bounding_boxes = [cv2.boundingRect(c) for c in contours] # 计算外接矩形
(contours, bounding_boxes) = zip(*sorted(zip(contours, bounding_boxes), key=lambda b: b[1][0])) # 按X坐标排序
return contours, bounding_boxes

排序完成后,contours 列表中的第一个轮廓就对应数字“0”,第二个对应数字“1”,依此类推。



构建模板字典 🗂️

上一节我们完成了轮廓排序,本节中我们来看看如何根据排序后的轮廓提取每个数字的图像并构建模板字典。

现在,我们已经有了按顺序排列的轮廓。接下来需要遍历这些轮廓,从原始图像中抠出每个数字所在的区域,并将其与对应的数字索引(0-9)关联起来。

以下是实现步骤:

- 初始化字典:创建一个空字典,用于存储数字和其模板图像的对应关系。
- 遍历轮廓:使用
enumerate同时获取轮廓索引i(即数字值)和轮廓本身c。 - 提取区域:计算当前轮廓的外接矩形
(x, y, w, h),利用切片操作从原始图像中抠出该矩形区域。 - 调整大小:将抠出的图像调整到一个统一、合适的大小。
- 存入字典:将数字索引
i作为键,调整大小后的图像作为值,存入字典。
digits = {} # 初始化模板字典

for (i, c) in enumerate(sorted_contours):
(x, y, w, h) = cv2.boundingRect(c) # 获取外接矩形
roi = ref[y:y + h, x:x + w] # 抠出数字区域
roi = cv2.resize(roi, (57, 88)) # 调整图像大小
digits[i] = roi # 存入字典
执行完循环后,digits 字典中就完整地包含了从0到9每个数字的模板图像。

总结 📝

本节课中我们一起学习了模板处理的全过程。
- 首先,我们使用轮廓检测函数定位图像中的所有数字。
- 接着,通过计算外接矩形并按其X坐标排序,确保了轮廓顺序与数字大小顺序一致。
- 最后,遍历排序后的轮廓,提取每个数字的图像区域并存入字典,成功构建了数字模板库。


至此,模板处理阶段完成。我们获得了一个结构清晰的 digits 字典,为后续的模板匹配识别任务打下了基础。

课程P34:4-输入数据处理方法 📊

在本节课中,我们将学习如何对输入图像进行一系列预处理操作,以提取信用卡上的数字区域。原始图像包含背景干扰,因此需要通过形态学操作、边缘检测和阈值处理等方法,将目标区域清晰地分离出来。
定义形态学操作核

上一节我们介绍了模板图像的简单处理。本节中我们来看看如何处理更复杂的信用卡图像。首先,我们需要定义形态学操作中使用的核。
以下是定义两个不同大小核的代码:
# 定义第一个核,大小为9x3
kernel_9x3 = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
# 定义第二个核,大小为5x5
kernel_5x5 = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))

核的大小需要根据实际任务指定,目的是为了突出特定大小的区域,例如信用卡上的数字区域。

图像读取与初步处理
接下来,我们开始对输入数据进行预处理。第一步是读取图像并进行基础调整。
以下是读取图像并进行初步处理的步骤:

- 读取原始图像数据。
- 使用
resize函数将图像调整至合适大小。 - 将彩色图像转换为灰度图。
完成这些步骤后,我们得到了一个便于后续处理的灰度图像。
顶帽操作突出明亮区域

得到灰度图后,我们希望突出图像中的明亮区域(如数字),同时过滤掉背景。这可以通过形态学中的顶帽操作实现。
顶帽操作使用之前定义的核,能够突出比周围更亮的区域。以下是执行顶帽操作的代码:
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel_9x3)

此操作并非必需,但能有效帮助我们聚焦于数字区域。预处理方法多样,可根据实际情况选择。

索贝尔算子边缘检测

突出明亮区域后,我们需要进一步检测数字的边缘。这里使用索贝尔算子计算图像在X方向的梯度。
以下是计算X方向梯度的步骤:
- 使用
cv2.Sobel函数计算X方向的梯度。 - 取计算结果的绝对值。
- 将结果归一化到0-255范围。

实验发现,在此任务中仅使用X方向梯度比结合Y方向效果更好。索贝尔算子帮助我们得到了数字的边缘信息。

闭操作连接区域

边缘检测后,数字的笔画可能还是分离的。我们希望将属于同一个数字的笔画连接起来,形成一个完整的“块”。

形态学中的闭操作(先膨胀后腐蚀)可以实现这个目的。以下是执行闭操作的代码:

closed = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, kernel_9x3)

执行闭操作后,原本分散的笔画被连接起来,每个数字区域更接近一个完整的白色块状结构,同时背景干扰被进一步过滤。
自动阈值二值化
为了将前景(数字)和背景完全分离,需要进行二值化处理。由于图像灰度直方图呈现双峰形态,适合使用Otsu方法自动寻找最佳阈值。
以下是使用Otsu方法进行自动阈值二值化的代码:
thresh = cv2.threshold(closed, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
参数0表示让OpenCV自动判断阈值,而非使用固定值。二值化后,数字区域为白色,背景为黑色。

二次闭操作填充空隙
二值化图像中,数字块内部可能存在一些小孔洞。为了使轮廓检测更准确,需要填充这些空隙。
我们可以再次使用闭操作来填充白色区域内部的黑点。以下是代码:

thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel_5x5)

经过这次操作,数字区域变得更加饱满和连续,为轮廓检测做好了准备。

轮廓检测与筛选
最后,我们在处理后的二值图像上检测所有轮廓。但检测到的轮廓包含许多不规则形状,我们需要从中筛选出代表四个数字组的轮廓。
以下是轮廓检测与绘制的基本步骤:
- 使用
cv2.findContours函数在二值图像thresh上查找轮廓。 - 将找到的轮廓绘制到原始彩色图像上以便观察。
- 根据轮廓的宽高比、面积等特征,编写过滤逻辑,只保留代表数字的四个主要轮廓。
轮廓检测是在经过所有预处理的二值图像上进行的,绘制则是为了在原始图像上可视化结果。

本节课中我们一起学习了信用卡数字识别的完整预处理流程。我们从读取图像开始,依次进行了顶帽操作、索贝尔边缘检测、闭操作连接区域、Otsu自动阈值二值化、二次闭操作填充空隙,最后进行轮廓检测与筛选。这一系列步骤有效地去除了背景干扰,将四个数字区域清晰地分离出来,为后续的数字识别奠定了坚实基础。
课程 P35:5-模板匹配得出识别结果 📊


在本节课中,我们将学习如何通过模板匹配技术,从信用卡图像中识别出数字。整个过程分为两大步:首先定位数字所在的区域,然后在这些区域内与预先准备好的数字模板进行匹配,从而得出最终的识别结果。


轮廓筛选与排序 🔍
上一节我们介绍了如何检测图像中的轮廓。本节中我们来看看如何从众多轮廓中筛选出我们需要的数字区域,并对它们进行排序。

首先,我们需要遍历检测到的所有轮廓。对于每一个轮廓,计算其外接矩形。这是因为我们需要根据轮廓的特征(如长宽比)来判断它是否是我们想要的数字区域。

# 伪代码示例:计算轮廓的外接矩形
x, y, w, h = cv2.boundingRect(contour)

计算外接矩形后,我们可以根据其长宽比进行筛选。信用卡上的数字区域有其特定的长宽比例,通过设定合适的阈值,可以过滤掉不相关的轮廓。

以下是筛选轮廓的步骤:
- 计算每个轮廓的外接矩形。
- 根据外接矩形的长宽比进行判断。
- 将符合条件(即可能是数字组)的轮廓保存起来。

经过筛选,我们得到了四个大的轮廓区域,每个区域对应信用卡上的一组数字。接下来,我们需要对这些轮廓从左到右进行排序,以确保按正确的顺序处理数字组。


# 伪代码示例:对轮廓按x坐标排序
locations = sorted(locations, key=lambda loc: loc[0])

提取并处理单个数字区域 🔢

在定位并排序了数字组的大轮廓后,我们需要在每个大轮廓内部进一步提取出单个的数字。
首先,取出第一个大轮廓(例如对应数字“5412”的区域)。为了确保提取完整,我们通常会将轮廓区域稍微向外扩展一些像素。
# 伪代码示例:扩展轮廓区域
x_expanded = x - 5
y_expanded = y - 5
w_expanded = w + 10
h_expanded = h + 10
group_image = original_image[y_expanded:y_expanded+h_expanded, x_expanded:x_expanded+w_expanded]
然后,对这个扩展后的区域图像进行预处理(如灰度化、二值化),并再次进行轮廓检测。这次检测是为了找到该组数字内部的每一个小数字的轮廓。
找到所有小轮廓后,同样需要按从左到右的顺序进行排序,以确保数字顺序正确。
执行模板匹配 🎯
现在,我们有了单个数字的图像。接下来,就是通过模板匹配来判断这个数字具体是几。
首先,需要将待识别的数字图像调整到与模板图像完全相同的大小。这是因为模板匹配要求输入图像和模板图像尺寸一致。
# 伪代码示例:调整图像大小以匹配模板
digit_resized = cv2.resize(digit_roi, (57, 88))

然后,让这个调整后的数字图像与0到9这十个数字模板依次进行匹配。OpenCV提供了多种匹配方法,这里我们使用相关系数法(cv2.TM_CCOEFF)。

以下是匹配过程的步骤:
- 初始化一个列表
scores,用于存储与每个模板(0-9)的匹配得分。 - 遍历十个数字模板。
- 对每个模板,使用
cv2.matchTemplate函数进行匹配,并获取匹配得分。 - 将得分存入
scores列表。

匹配完成后,scores列表中得分最高的那个索引,就对应了识别出的数字。例如,如果scores[5]的值最大,那么识别结果就是数字5。


# 伪代码示例:模板匹配并找出最佳匹配
method = cv2.TM_CCOEFF
scores = []
for digit in template_digits:
result = cv2.matchTemplate(digit_resized, digit, method)
_, max_val, _, _ = cv2.minMaxLoc(result)
scores.append(max_val)

recognized_digit = scores.index(max(scores))

对一组数字中的每一个都重复上述过程,就能识别出整组数字(如“5412”)。最后,在原图上绘制出识别出的数字和边界框进行可视化。




流程总结与扩展应用 📈

本节课中我们一起学习了如何利用模板匹配完成信用卡数字识别的完整流程。

整个流程可以总结为以下几个核心步骤:
- 模板准备:预处理0-9的数字模板,并调整至统一大小。
- 图像输入与预处理:对输入的信用卡图像进行灰度化、二值化等操作。
- 轮廓检测与筛选:检测图像轮廓,根据长宽比等特征筛选出数字组区域,并进行排序。
- 数字提取:在每个数字组区域内,再次检测并提取出单个数字轮廓。
- 模板匹配:将每个数字与模板库匹配,得分最高者即为识别结果。
- 结果输出:在原图上标注识别出的数字。


这个方法的原理不仅适用于信用卡识别,稍加修改即可应用于其他类似场景,例如车牌号码识别。其核心思想都是先定位目标区域,再在区域内进行特征匹配。


通过本课程,你掌握了使用OpenCV基础函数解决实际图像识别问题的完整思路。
课程P36:文档轮廓提取 📄

在本节课中,我们将学习如何从一张图像中提取文档的轮廓。整个过程涉及图像读取、预处理、边缘检测、轮廓查找以及关键区域的定位。我们将通过代码一步步实现,并解释每个步骤的作用。
概述
文档轮廓提取是计算机视觉中的一项常见任务,例如在扫描文档时,我们需要自动识别并校正文档的边界。本节课将介绍实现这一功能的核心步骤。
第一步:读取输入图像
首先,我们需要将目标图像读入程序。这是所有图像处理任务的第一步。
# 读取图像
image = cv2.imread('document.jpg')
第二步:计算图像缩放比例
在对图像进行缩放操作前,我们需要计算缩放比例。这是因为后续的坐标变换(如轮廓点坐标)需要根据原始图像尺寸进行还原。
假设我们将图像的高度(H)固定为500像素,宽度(W)会按比例自动计算。
# 定义目标高度并计算缩放比例
ratio = image.shape[0] / 500.0
orig = image.copy()
# 执行缩放操作
image = imutils.resize(image, height=500)

公式解释:ratio = 原始图像高度 / 目标高度。这个比例将用于后续将处理后的坐标映射回原始图像尺寸。

第三步:图像预处理
在检测轮廓之前,需要对图像进行预处理以消除噪声并增强特征。

转换为灰度图
将彩色图像转换为灰度图,简化后续处理。
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
应用高斯滤波
使用高斯滤波来平滑图像并减少噪声干扰。
gray = cv2.GaussianBlur(gray, (5, 5), 0)
边缘检测
使用Canny边缘检测算法来找出图像中的边缘,这是轮廓提取的基础。
edged = cv2.Canny(gray, 75, 200)
代码解释:cv2.Canny函数需要两个阈值参数,用于控制边缘检测的灵敏度。
第四步:轮廓检测与筛选
上一节我们通过边缘检测得到了图像的边缘信息,本节中我们来看看如何从这些边缘中找出我们需要的文档轮廓。
查找轮廓
使用OpenCV的findContours函数从边缘检测的结果中找出所有轮廓。
cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
筛选轮廓
图像中可能检测到多个轮廓,我们需要找到代表文档外部边界的那一个。通常,文档轮廓是面积最大或周长最长的轮廓。
以下是筛选轮廓的步骤:
- 对所有检测到的轮廓进行排序:我们可以根据轮廓面积或外接矩形大小进行降序排序。
- 遍历排序后的轮廓:检查每个轮廓,寻找符合我们条件的轮廓(例如,近似后为四边形)。
- 轮廓近似:使用
cv2.approxPolyDP函数对轮廓进行多边形近似。这可以将一个复杂的轮廓简化为由更少顶点组成的多边形。 - 判断是否为四边形:如果近似后的轮廓恰好有4个顶点,那么它很可能就是我们寻找的文档矩形边界。
# 按轮廓面积降序排序,取前5个可能的轮廓
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:5]
# 遍历轮廓
for c in cnts:
# 计算轮廓周长
peri = cv2.arcLength(c, True)
# 进行多边形近似,epsilon是近似精度,通常取周长的百分比
approx = cv2.approxPolyDP(c, 0.02 * peri, True)
# 如果近似后有4个点,则认为找到了文档轮廓
if len(approx) == 4:
screenCnt = approx
break
公式解释:在cv2.approxPolyDP函数中,epsilon参数是关键。我们使用公式 epsilon = 0.02 * 轮廓周长 来动态确定近似精度。值越小,近似轮廓越接近原始形状;值越大,轮廓越规则(趋向于矩形)。
第五步:绘制并展示结果
找到目标轮廓后,我们可以在图像上将其绘制出来以进行可视化验证。

# 在原始图像(缩放后的)上绘制找到的轮廓
cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 2)
cv2.imshow("Outline", image)
cv2.waitKey(0)
总结

本节课中我们一起学习了文档轮廓提取的完整流程:
- 读取图像并计算缩放比例以备坐标还原。
- 进行图像预处理,包括灰度转换、高斯滤波和边缘检测。
- 执行轮廓检测,并从所有轮廓中通过排序和多边形近似的方法筛选出代表文档边界的四边形轮廓。
- 最后绘制并展示提取到的轮廓。

通过这一系列步骤,我们能够从一张可能倾斜、包含背景杂物的图片中,准确地定位出文档的主体区域,为后续的透视变换等操作奠定了基础。

课程P37:原始与变换坐标计算 📐


在本节课中,我们将学习如何通过透视变换,将图像中检测到的倾斜或扭曲的文档区域,转换为规规矩矩、类似PDF的矩形格式。核心在于计算原始轮廓坐标与目标矩形坐标,并利用它们完成变换。

上一节我们介绍了如何通过轮廓检测和近似来定位文档的四个角点。本节中,我们来看看如何利用这些角点坐标,计算透视变换所需的两组关键坐标:原始坐标和目标坐标。
透视变换的目的是将图像中一个任意形状的四边形区域,校正为一个标准的矩形。这需要两组点:
- 原始坐标:从图像中检测到的文档轮廓的四个角点(A, B, C, D)。
- 目标坐标:我们希望将文档校正到的标准矩形的四个角点(通常设为 (0,0), (W,0), (W,H), (0,H))。

因此,关键步骤是计算出目标矩形的宽度 W 和高度 H。





以下是计算目标矩形宽度 W 和高度 H 的逻辑:

由于检测到的轮廓可能并非完美矩形(例如是梯形或不规则四边形),我们需要分别计算其上边和下边的宽度,以及左边和右边的高度,然后取最大值,以确保变换后的图像能完整包含所有内容。


宽度计算:
- 上边宽度 W1 = 点A(左上)与点B(右上)之间的欧氏距离。
- 下边宽度 W2 = 点C(右下)与点D(左下)之间的欧氏距离。
- 最终宽度 maxWidth =
max(W1, W2)。


用公式表示,两点 (x1, y1) 与 (x2, y2) 间的距离为:
distance = sqrt( (x2 - x1)^2 + (y2 - y1)^2 )



高度计算:
- 左边高度 H1 = 点A(左上)与点D(左下)之间的欧氏距离。
- 右边高度 H2 = 点B(右上)与点C(右下)之间的欧氏距离。
- 最终高度 maxHeight =
max(H1, H2)。

在代码实现中,有一个细节需要注意:坐标缩放。我们检测轮廓时,可能是在缩放后的图像上进行的(例如为了加速处理)。但透视变换需要应用在原始图像上。因此,必须将检测到的轮廓坐标按比例还原到原始图像的尺寸。


假设缩放比例为 ratio,原始图像上某点的坐标 (x_original, y_original) 与缩放后图像上对应点坐标 (x_resized, y_resized) 的关系为:
(x_original, y_original) = (x_resized * ratio, y_resized * ratio)



计算出 maxWidth 和 maxHeight 后,我们就可以定义目标坐标了。通常,我们将目标矩形的左上角设为原点 (0, 0)。


以下是目标坐标点的定义顺序(需与原始轮廓点的顺序一致):
- 目标点 1 (左上):
(0, 0) - 目标点 2 (右上):
(maxWidth - 1, 0)(减1是为了避免索引越界) - 目标点 3 (右下):
(maxWidth - 1, maxHeight - 1) - 目标点 4 (左下):
(0, maxHeight - 1)



至此,我们就得到了进行透视变换所必需的两组坐标:经过缩放还原的原始轮廓坐标和计算得到的目标矩形坐标。



本节课中我们一起学习了透视变换前最关键的一步:坐标计算。我们明确了需要原始坐标和目标坐标两组参数,并详细讲解了如何从检测到的轮廓中计算出目标矩形的宽高,以及如何处理坐标缩放问题。下一步,我们就可以利用这些坐标,调用透视变换函数,最终得到校正后的规整文档图像了。
课程P38:透视变换原理与实现 📐
在本节课中,我们将学习计算机视觉中的透视变换。我们将了解其数学原理,并学习如何使用OpenCV库来实现一个文档扫描效果。
透视变换的核心在于找到一个变换矩阵,它能将图像从一个视角(如倾斜的)转换到另一个视角(如正面的)。这需要两组对应的坐标点:原始图像中的点,以及我们希望它们变换后到达的目标点。
从坐标到变换矩阵
上一节我们介绍了如何获取图像的轮廓和角点。本节中我们来看看如何利用这些点进行透视变换。
想要进行变换,我们需要两组点:输入的四个源点,以及输出的四个目标点。有了这两组点,我们就能计算当前的变换矩阵了。这个变换涉及投影,因此它是一个3×3的矩阵。
原始坐标和目标坐标都已具备,但我们不知道原始坐标如何通过平移、旋转和翻转等操作综合变成目标坐标。这需要一个变换矩阵。我们需要求解这个变换矩阵,这要求提供两组值:第一组是输入的坐标,第二组是期望输出的坐标。
透视变换的数学原理
透视变换的过程可以这样理解:一开始我们得到的是二维坐标(X, Y)。但变换矩阵会先将它转换到三维空间,再从三维空间映射回二维,从而完成透视变换。这需要一个3×3的矩阵。
因为涉及三维,我们需要先将坐标转换为齐次坐标。齐次坐标就是往二维坐标里增加一个维度,通常添加一个1。例如,二维点(X, Y)的齐次坐标表示为(X, Y, 1)。这样做是为了方便进行矩阵运算,而不改变坐标的本质。
变换过程可以用以下公式描述:
设输入点的齐次坐标为 [x, y, 1]^T,变换矩阵为 M,输出点的齐次坐标为 [x', y', w']^T。那么变换关系为:
[x', y', w']^T = M * [x, y, 1]^T
最终输出的二维坐标 (u, v) 需要通过齐次坐标归一化得到:
u = x' / w'
v = y' / w'
其中,变换矩阵 M 是一个3x3的矩阵,包含8个未知参数(因为通常将 M[2,2] 设为1或某个常数来固定尺度):
M = [[H11, H12, H13],
[H21, H22, H23],
[H31, H32, 1]]
求解变换矩阵的条件
以下是求解变换矩阵 M 所需的条件:
一组坐标点 (x, y) 通过上述公式可以建立两个方程(分别对应 u 和 v)。矩阵 M 有8个未知数,因此需要8个独立的方程来求解。
这意味着我们需要4组对应的坐标点(因为4组点 * 2个方程/点 = 8个方程)。就像我们用一个矩形框选区域,正好提供四个角点。
此外,这些点必须满足一个前提条件:其中任意三个点不能共线。例如,如果四个点中有三个点在一条直线上,就无法构成有效的四边形,也就无法求解出正确的透视变换矩阵。因此,最好选择能构成一个规则四边形的点。
使用OpenCV实现变换
OpenCV库为我们封装了上述复杂的计算过程。我们只需要提供源坐标点和目标坐标点,它会自动联立方程,帮我们计算出变换矩阵 M。
以下是实现的关键步骤代码示例:
import cv2
import numpy as np

# 假设我们已经获得了源点 src_points 和目标点 dst_points
# 它们都是包含4个(x,y)坐标的NumPy数组,形状为(4,2)
# 计算透视变换矩阵 M
M = cv2.getPerspectiveTransform(src_points, dst_points)


# 对原始图像应用透视变换
# image 是输入的原始图像
# (width, height) 是输出图像的期望尺寸
transformed_image = cv2.warpPerspective(image, M, (width, height))
有了变换矩阵 M 之后,就可以对输入的原始图像进行变换,最终返回变换后的结果图像。
为了使结果更清晰,在显示前通常还会对图像进行一些后处理,例如灰度化和二值化,以突出有价值的信息。
文档扫描应用实例

结合边缘检测和轮廓查找,透视变换可以用于实现文档扫描功能。整个过程可以总结为三步:
- 边缘检测:识别图像中的边缘。
- 轮廓查找:找到文档的轮廓并获取其四个角点。
- 透视变换:利用角点进行变换,得到“摆正”后的文档图像。

我们知道为什么需要四个坐标点:每个点提供X和Y两个值,相当于两个方程。求解3x3变换矩阵中的8个未知参数需要8个方程,因此正好需要4个点。虽然求解过程复杂,但我们无需关心,交给OpenCV即可。

最终,利用求得的矩阵 M 对原始图像进行变换,就能得到扫描后的规整结果。

本节课中我们一起学习了透视变换的核心原理。我们了解到,透视变换的本质是求解一个3x3的投影变换矩阵,这需要至少四组不共线的对应点。OpenCV的 getPerspectiveTransform 和 warpPerspective 函数极大地简化了这一过程的实现。掌握这个工具,我们就能实现诸如文档扫描、图像校正等多种有趣的计算机视觉应用。
课程P39:Tesseract-OCR安装与配置教程 🛠️
在本节课中,我们将学习如何安装和配置Tesseract-OCR工具,并利用它进行基本的字符识别操作。Tesseract是一款开源的OCR识别工具,功能丰富,默认支持英文,也可通过训练支持中文。
概述
上一节我们介绍了图像处理的基本概念,本节中我们来看看如何将扫描后的图像转换为可编辑的文本。我们将从Tesseract的安装开始,逐步完成环境配置,并通过实例演示其使用方法。
安装Tesseract-OCR
以下是安装Tesseract-OCR的步骤。

- 访问下载页面:首先,需要访问Tesseract的GitHub页面或Windows版本的下载地址。建议下载最新的4.0版本,该版本集成了递归神经网络,识别效果更佳。
- 运行安装程序:下载完成后,双击
.exe文件进行安装。安装过程中,请记住选择的安装路径。 - 配置环境变量:安装完成后,需要将Tesseract的安装目录添加到系统的环境变量中。
- 打开“系统属性” -> “高级” -> “环境变量”。
- 在“系统变量”或“用户变量”中找到并编辑
Path变量。 - 点击“新建”,将Tesseract的安装路径(例如
C:\Program Files\Tesseract-OCR)添加进去。
- 验证安装:打开命令行,输入以下命令验证是否安装成功。
如果命令行显示出版本信息(例如tesseract -vtesseract 4.0.0),则表明安装和环境变量配置成功。

使用Tesseract进行OCR识别


配置好环境后,我们可以通过命令行直接使用Tesseract进行文字识别。


以下是基本的使用命令格式。
tesseract [图片路径] [输出文本文件路径(不含后缀)]
例如,要对E:\opencv.png图片进行识别,并将结果保存到E:\result.txt,命令如下。
tesseract E:\opencv.png E:\result
执行后,Tesseract会生成result.txt文件,其中包含了识别出的文本内容。

在Python中调用Tesseract


在实际项目中,我们更倾向于在Python程序中集成OCR功能。这需要安装Python的Tesseract封装库。
以下是安装和使用的步骤。



- 安装Python库:在命令行中使用pip安装
pytesseract。pip install pytesseract注意:如果你有多个Python环境,请确保在目标环境的
Scripts目录下执行此命令,或使用conda进行安装。

- 在Python代码中使用:安装完成后,即可在Python脚本中调用Tesseract。
这段代码会打开名为import pytesseract from PIL import Image # 指定Tesseract可执行文件的路径(如果环境变量已配置,通常可省略) # pytesseract.pytesseract.tesseract_cmd = r‘C:\Program Files\Tesseract-OCR\tesseract.exe’ # 打开图片 image = Image.open(‘opencv.png’) # 进行OCR识别 text = pytesseract.image_to_string(image) print(text)opencv.png的图片,并打印出识别出的文字。

总结

本节课中我们一起学习了Tesseract-OCR工具的安装、环境配置以及基本使用方法。我们首先在系统中安装并配置了Tesseract,然后通过命令行测试了其OCR功能,最后学习了如何在Python项目中通过pytesseract库集成该功能,为后续的自动化文本处理任务奠定了基础。
课程P4-1:计算机眼中的图像 👁️💻
在本节课中,我们将要学习OpenCV中图像最基本的操作。首先,我们需要理解在计算机眼中,图像究竟是什么样子。
图像的本质:像素矩阵
我们观察一张名为“LINA”的图片。这张图片被分成了许多小方格。实际上,这些方格还可以被进一步细分。我们取出其中一个方格进行观察,会发现它由更多的小块组成。
其中每一个小格被称为一个像素点。在计算机中,图像就是由这些像素点构成的。
那么,像素点是什么呢?本质上,它是一个数值。观察右边的图示(暂时忽略RGB部分),可以看到其中包含81、116、133、201等数值。这些数值构成了像素点。
这些数值的大小意味着什么?在计算机中,一个像素点的值在0到255之间浮动。最小值0代表黑色,最大值255代表白色。中间的不同数值则表示不同的亮度级别。
颜色通道:RGB
接下来,我们观察RGB部分。RGB代表图像的颜色通道。通常我们看到的彩色图像都是RGB三通道的。
例如,一个区域可能对应着R(红色)、G(绿色)、B(蓝色)三个通道的值。数值201表示在红色通道上的亮度,155表示在绿色通道上的亮度,165表示在蓝色通道上的亮度。

我们通常获取的图像都是这种三通道的彩色图。那么,黑白图像呢?黑白图像,或称灰度图,只有一个通道,仅表示亮度就足够了,不需要RGB三个通道。


总结图像结构
综上所述,在计算机眼中:
- 图像由众多像素点组成。
- 这些像素点排列成一个矩阵,这个矩阵代表了图像的大小。例如,一个500x500的图像,其高度(H)和宽度(W)都是500。
- 对于彩色(RGB)图像,每个颜色通道(R、G、B)都有一个独立的500x500矩阵。因此,整个图像的维度(shape)是
(500, 500, 3)。 - 对于灰度图像,则只有一个通道,其维度为
(500, 500, 1)或简化为(500, 500)。
公式表示彩色图像结构:图像形状 = (高度 H, 宽度 W, 通道数 C),其中C通常为3(RGB)。


上一节我们介绍了计算机眼中图像的本质,本节中我们来看看如何在OpenCV中进行最基本的图像操作。
第一步:读取图像
无论后续要做什么处理,第一步总是需要将图像加载到计算机中。我们需要将图像转换成像素矩阵,以便计算机进行分析和识别工作。

以下是读取图像所需的步骤:

首先,导入必要的工具包。

import cv2 # OpenCV库
import matplotlib.pyplot as plt # 绘图库
import numpy as np # 数值计算库
%matplotlib inline # 在Notebook中内嵌显示图像
%matplotlib inline 是一个“魔法指令”,它使得在Jupyter Notebook中绘图后能直接显示图像,无需额外调用 plt.show() 函数。这个指令通常在Notebook环境中使用。
读取图像非常简单,使用OpenCV的 cv2.imread() 函数,并指定图像文件的路径即可。

img = cv2.imread(‘cat.jpg’) # 读取名为‘cat.jpg’的图像
执行上述代码后,图像数据被存储在变量 img 中。让我们查看一下这个变量。

print(img)
print(img.dtype)
print(img.shape)

输出结果分析:
img是一个NumPy的ndarray(多维数组)结构。dtype是uint8,这表示数组中每个值(像素值)的数据类型是8位无符号整数,取值范围正是之前提到的 0到255。shape显示了数组的维度,例如(414, 500, 3),这对应着 (高度H, 宽度W, 通道数C)。
这样,我们就成功地将图像读取到程序中。
第二步:显示图像

在对图像进行处理的过程中,我们经常需要观察图像的变化。这里介绍如何使用OpenCV自带的函数来显示图像。

注意:OpenCV默认读取图像的格式是 BGR,而非常见的RGB。这与Matplotlib等库的默认格式存在冲突。因此,使用OpenCV自带的显示函数可以避免颜色通道转换的麻烦。

显示图像的方法如下:

cv2.imshow(‘window_name‘, img) # 创建一个窗口并显示图像
cv2.waitKey(0) # 等待键盘输入
cv2.destroyAllWindows() # 关闭所有窗口
cv2.imshow(‘window_name‘, img):第一个参数是窗口的名称,可以任意指定;第二个参数是要显示的图像变量。cv2.waitKey(0):参数0表示程序将无限期等待,直到用户在键盘上按下任意键后,程序才会继续执行。cv2.destroyAllWindows():关闭所有由OpenCV创建的窗口。

waitKey 函数的参数含义:
- 参数为0:等待任意键盘按键。
- 参数为1000:等待1000毫秒(即1秒),时间到后自动继续执行。
- 参数为10000:等待10000毫秒(即10秒)。

通常,在调试和观察时,建议使用 waitKey(0),这样可以有充足的时间查看图像结果,并在查看完毕后通过按键关闭窗口。

本节课中我们一起学习了:
- 图像的本质:计算机眼中的图像是由像素点组成的矩阵,像素值在0-255之间表示亮度。彩色图像包含RGB三个颜色通道,而灰度图只有一个通道。
- 图像的基本操作:我们学会了如何使用OpenCV读取图像(
cv2.imread),并理解了读取后图像的NumPy数组结构(shape,dtype)。 - 图像的显示:我们掌握了使用OpenCV显示图像(
cv2.imshow)和控制显示窗口的方法(cv2.waitKey),并注意到了OpenCV默认使用BGR格式这一重要细节。

理解这些基础概念是进行后续所有图像处理和分析的基石。

🚀 课程P4:YOLOv7核心原理与创新点详解

在本节课中,我们将学习YOLOv7的核心原理与主要创新点。我们将跳过从V1到V6的详细历史,直接聚焦于V7版本带来的关键改进,特别是其在推理加速和正样本分配策略上的新思路。课程结束后,我们会简要回顾V1到V5的发展脉络。

📖 概述:YOLOv7的“怀旧”与创新
YOLOv7带来的一大核心改变,不仅适用于自身,也普遍适用于所有卷积相关的任务。这启发我们回顾经典网络结构,例如VGG。尽管VGG在2014年后被ResNet等后起之秀超越,但YOLOv7的论文提出了一种思路:在推理阶段,使用一个极其简单、无分支的主干网络(类似VGG)来达到甚至超越复杂网络的效果。这就像为经典游戏开设“怀旧服”,让经典结构重新焕发生机。
这篇论文的摘要指出,他们提出了一种仅由卷积层组成的简单有效网络,在ImageNet数据集上取得了超过80%的top-1准确率,其特点是在训练时可以设计复杂结构,但在推理(测试)时会合并为一个无分支的流线型网络,从而实现速度快、准确率高的目标。
⚙️ 核心创新一:推理阶段的模型重参数化
上一节我们介绍了YOLOv7“怀旧服”的核心思想。本节中,我们来看看它是如何通过模型重参数化在推理阶段实现加速的。
核心目标是将训练时复杂的多分支结构,在推理时等价地合并为一个简单的单分支卷积层。这主要涉及两个步骤:
1. 卷积层(Conv)与批归一化层(BN)的合并
在训练时,卷积后通常会连接BN层。在推理时,我们可以将这两层的操作合并为一个新的卷积操作。
BN层的计算公式如下:
对于输入特征图的第 i 个通道 x_i,BN操作是:
y_i = γ_i * ( (x_i - μ_i) / √(σ_i² + ε) ) + β_i
其中,γ_i 和 β_i 是可学习的缩放和偏移参数,μ_i 和 σ_i 是该通道数据的均值和标准差,ε 是一个极小常数防止除零。
合并过程:
- 将BN的公式展开,可以重写为
y_i = W_bn_i * x_i + B_bn_i的形式,其中W_bn_i和B_bn_i是由γ_i, β_i, μ_i, σ_i, ε计算得到的固定值。 - 假设卷积操作为
z_i = W_conv * x + b_conv。 - 将卷积和BN串联:
y_i = W_bn_i * (W_conv * x + b_conv) + B_bn_i。 - 展开并整理后,可以得到一个新的等效卷积操作:
y_i = (W_bn_i · W_conv) * x + (W_bn_i * b_conv + B_bn_i)。 - 因此,我们可以定义一组新的卷积核权重
W_new = W_bn · W_conv和偏置b_new = W_bn * b_conv + B_bn_i。在推理时,直接用这组新参数进行卷积,等价于先后进行卷积和BN操作。
作用: 减少推理时的计算层数,加快速度。
2. 多分支卷积层的合并(例如1x1卷积、恒等映射)
训练时,网络可能包含并行分支,如1x1卷积、3x3卷积或直接恒等映射(Identity)。YOLOv7在推理时会将它们合并到主干的3x3卷积中。
以下是具体方法:
-
将1x1卷积变为3x3卷积: 通过对1x1的卷积核进行零填充(Padding),将其扩展为3x3的卷积核。同时,对输入特征图也进行相应的填充,以保证输出结果与原始的1x1卷积等价。
- 原因1: NVIDIA对3x3卷积的底层优化最好,计算效率最高。
- 原因2: 只有都变成3x3卷积,才能进行后续的合并操作。
-
将恒等映射变为3x3卷积: 恒等映射可以看作一个特殊的“1x1卷积”,其卷积核是一个单位矩阵。例如,对于两个输入通道,可以构造一个3x3卷积核,其中心点参数为
[[1,0], [0,1]],周围填充0。这样,该卷积操作就能原封不动地输出输入特征图。 -
合并所有3x3卷积: 当所有分支都被转换为3x3卷积后,将它们对应的卷积核权重和偏置直接相加,融合成一个单一的3x3卷积层。
最终效果: 在推理阶段,一个复杂的多分支模块被等价地替换为一个单一的3x3卷积层,显著提升了运行速度,且不损失精度。

🎯 核心创新二:改进的正样本分配策略
上一节我们了解了YOLOv7如何通过结构优化来加速。本节中,我们来看看另一个关键改进:正样本分配策略,它直接影响了模型的检测精度,尤其是召回率。

在YOLO系列中,特征图上的每个点(或称网格)会预设多个不同大小和长宽比的先验框(Anchor)。训练时,需要确定哪些Anchor负责学习哪个真实标注框(Ground Truth, GT),这些被选中的Anchor就是“正样本”。
传统的分配策略(以YOLOv3/v4为例)
- 计算每个Anchor与所有GT的IoU(交并比)。
- 对于每个GT,选择与其IoU最大的那个Anchor作为正样本。
- 同时,设定一个阈值(如0.5),IoU大于该阈值的Anchor也可能被选为正样本。
- 一个Anchor最多只能分配给一个GT。



YOLOv7的改进:动态软匹配与跨网格预测
YOLOv7的分配策略更精细,分为初筛和复筛两个阶段,并增加了正样本的数量。


1. 初筛(生成候选):
对于每个GT,初步筛选出一批Anchor候选。筛选条件相对宽松:
- Anchor与GT的长宽比在
[0.25, 4]之间。 - Anchor与GT的IoU大于一个阈值(如0.5)。
- Anchor预测的类别与GT类别一致(通过计算分类损失判断)。
综合这些条件,为每个候选Anchor计算一个加权损失。
2. 复筛(动态选择):
- 对每个GT,将其所有候选Anchor按加权损失从小到大排序。
- 不是简单地选择前N个(如Top 10),而是计算前N个候选损失的累加和。
- 根据累加和动态决定为该GT分配多少个正样本Anchor(例如,累加和小的可能只分配5个,累加和大的可能分配7个)。这避免了在损失分布出现“断崖式下降”时,将质量差的Anchor也纳入正样本。
3. 跨网格预测(增加正样本数):
这是YOLOv7的一个关键细节。对于GT中心点所在的网格,不仅该网格本身的Anchor参与竞争,还会将GT中心点向周围偏移0.5个单位(例如向左上、右下等方向)。偏移后的点落在哪个网格,就额外允许那个网格的Anchor也参与该GT的正样本竞争。
- 目的: 增加每个GT匹配到的正样本Anchor数量,让更多网格有机会学习到物体特征,从而提升模型对物体的召回能力,减少漏检。
4. 辅助头(Auxiliary Head)中的强化:
YOLOv7的一些版本引入了辅助头。在辅助头的训练中,跨网格预测的偏移量从0.5增加到了1.0。这意味着一个GT会在更多网格(最多5个)中寻找正样本,进一步强化了召回率的训练。但注意,在最终推理时,只使用主头的预测结果。


🏗️ 网络结构概览







YOLOv7的整体网络结构主干与YOLOv5相比,没有革命性变化,其创新更多体现在上述的工程优化和训练策略上。结构上的主要特点包括:








- 多路径聚合: 大量使用了类似
Concat的操作,将不同深度、不同尺度的特征图进行拼接,以融合浅层细节信息和深层语义信息。 - 下采样模块多样化: 下采样时,并非单一使用最大池化或步长为2的卷积,而是可能将两者并行,然后将结果拼接,以保留更多信息。
- 重参数化模块: 如前所述,训练时模块可能包含多分支,但会在导出推理模型时进行合并。
- 输出层: 通常包含四个不同尺度的检测头(用于检测不同大小的物体),在某些配置中还会增加辅助头。



📊 其他细节与总结
1. 边界框预测公式的改进:
YOLOv7的边界框偏移量预测公式继承了YOLOv5的设计,并与其正样本分配策略遥相呼应。
b_x = (2 * σ(t_x) - 0.5) + c_x
b_y = (2 * σ(t_y) - 0.5) + c_y
b_w = p_w * (2 * σ(t_w))²
b_h = p_h * (2 * σ(t_h))²
其中,σ是Sigmoid函数。这个设计巧妙地将网络预测值t_*的范围限制在合理的物理范围内:
(2 * σ(t_x) - 0.5)将中心偏移量限制在[-0.5, 1.5]之间,正好对应了“跨网格预测”中中心点偏移0.5个单位的需求。(2 * σ(t_w))²将宽度缩放比例限制在[0, 4]之间,这与初筛时长宽比小于4的条件相吻合。
2. 总结:
本节课我们一起学习了YOLOv7的核心精髓:
- 推理加速: 通过卷积层与BN层的合并以及多分支卷积层的重参数化,将训练模型转化为高效的单分支推理模型,在不损失精度的情况下提升速度。
- 训练优化: 通过改进的正样本分配策略(动态软匹配、跨网格预测),增加了正样本的数量和多样性,旨在提升模型的召回率。
- 工程实现: 网络结构通过多路径拼接丰富特征,边界框预测公式与分配策略紧密耦合,体现了细致的工程考量。

YOLOv7可以看作是YOLOv5系列思路的延续和深化,它没有提出全新的网络结构,而是在训练策略和模型部署优化上做出了扎实的改进,使其在精度和速度的平衡上达到了新的高度。要深入理解这些细节,最佳方式是结合其开源代码进行学习。
课程P40:文档扫描与OCR识别实践 📄➡️📝
在本节课中,我们将学习如何完成文档扫描,并使用OCR(光学字符识别)工具包将扫描图像中的文字提取出来。整个过程包括环境配置、扫描结果处理以及文字识别。

环境配置与问题解决 🔧
安装完必要的工具包后,需要将其导入到项目中。然而,仅完成此步骤可能无法在Python中直接调用。以下是一个在Windows系统中常见的配置问题及其解决方法。
上一节我们介绍了工具包的安装,本节中我们来看看如何解决调用时可能遇到的路径问题。

你需要进入Python的安装目录。例如,在Anaconda环境中,路径可能类似于 .../Anaconda3/。请按照以下步骤操作:
以下是具体的修改步骤:
- 进入
Lib文件夹。 - 进入
site-packages文件夹。 - 找到你安装的工具包对应的文件夹(例如
pytesseract)。 - 在该文件夹中找到
__init__.py文件并用编辑器打开。 - 在文件中找到指定
tesseract命令的代码行。默认可能为tesseract_cmd = ‘tesseract’。
关键修改点在于,由于Windows系统路径中反斜杠 \ 可能引发歧义,导致IDE无法正确识别全局变量,因此建议将路径改为绝对路径。
修改示例:
# 将默认的相对路径命令
tesseract_cmd = ‘tesseract‘
# 修改为你的tesseract.exe的绝对路径
tesseract_cmd = r‘C:\Program Files\Tesseract-OCR\tesseract.exe‘
指定绝对路径后,可以避免大多数因路径问题导致的“命令未找到”错误。


文档扫描与结果保存 📸

完成环境配置后,我们可以开始进行文档扫描。以下步骤将演示如何执行扫描并将结果保存为图像文件。

以下是核心代码逻辑:
# 执行扫描操作
scanned_image = scanner.scan(document_image)
# 将扫描结果保存为文件
cv2.imwrite(‘scanned_result.jpg‘, scanned_image)
执行上述代码后,扫描结果将保存为 scanned_result.jpg 文件。


如图所示,scanned_result.jpg 即为生成的扫描图像结果。

OCR文字识别 🔍

获得扫描图像后,下一步是提取其中的文字信息。我们将使用OCR工具包对图像进行处理和识别。

上一节我们得到了扫描图像,本节中我们来看看如何从中提取文字。

首先,需要读取图像并进行基本的预处理。
以下是图像预处理与识别的步骤:
- 读取图像:使用OpenCV读取扫描得到的文件。
- 预处理:将图像转换为灰度图,并可选择进行滤波或二值化操作以提升识别效果。不同预处理方法的效果可以对比选择。
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 可选:二值化 _, binary_image = cv2.threshold(gray_image, 127, 255, cv2.THRESH_BINARY) - 文字识别:调用OCR工具包的
image_to_string函数,将处理后的图像转换为文本。text = pytesseract.image_to_string(processed_image) print(text)

运行脚本,稍等片刻即可获得识别出的文本内容。


识别完成后,可以将输出的文本与原始扫描图像进行对比验证。例如,识别出的数字和字符序列应与图像中的内容一致。

通过对比可以确认,OCR工具包成功地将图像中的文字准确提取了出来。


总结 📚
本节课中我们一起学习了完整的文档扫描与OCR识别流程。
我们首先解决了Windows系统下Python调用OCR引擎的路径配置问题,然后演示了如何对文档进行扫描并将结果保存为图像。最后,我们使用OCR工具包对扫描图像进行预处理和文字识别,成功提取出其中的文本内容。
与命令行操作相比,在Python环境中完成这些步骤更便于集成和后续处理,例如将识别结果直接用于程序逻辑或展示。希望本教程能帮助你掌握使用Python进行文档扫描和文字识别的基本方法。



课程P41:1-角点检测基本原理 🧐
在本节课中,我们将要学习图像特征提取中的一个重要概念——HARRIS角点检测。我们将从理解什么是角点开始,逐步深入到其背后的数学原理,并学习如何让计算机识别出图像中的角点。
什么是角点? 🎯
首先,我们需要明确“角点”的定义。观察左边的图片,我们的注意力很可能会被蓝天与楼房之间的交界处所吸引。这些交界处,尤其是那些凸出的部分,就是典型的角点。
角点具有一个关键特性:无论你沿着水平方向还是竖直方向移动一个小窗口,窗口内图像的灰度值都会发生显著的变化。相比之下,在平坦区域(如一片蓝天)移动窗口,灰度值变化很小;在边缘区域(如楼房的垂直边界)移动窗口,灰度值可能只在一个方向(如竖直方向)变化剧烈,而在另一个方向(如水平方向)变化平缓。
因此,我们可以将图像区域分为三类:
- 平坦区域:在任意方向移动,灰度值变化都很小。
- 边缘区域:在某个特定方向移动,灰度值变化很小;在与之垂直的方向移动,灰度值变化剧烈。
- 角点区域:在任意方向移动,灰度值变化都很剧烈。
角点之所以重要,是因为它们包含了比平坦区域和边缘区域更丰富的特征信息,在图像匹配、目标跟踪等任务中非常有用。
上一节我们介绍了角点的直观概念,本节中我们来看看如何用数学语言来描述它。
角点的数学描述 📐
我们刚才的判断是基于人眼的观察。接下来,我们要让计算机也能识别角点,这就需要为它提供一个可计算的数学公式。
核心思路是:量化图像窗口移动时,其内部灰度值的变化程度。
假设我们有一个图像窗口(比如一个3x3的小区域),其中心位于 (x, y)。当这个窗口在图像上发生一个微小位移 (u, v) 后,窗口内像素灰度值的变化量 E(u, v) 可以用以下公式近似表示:
E(u, v) ≈ Σ [I(x+u, y+v) - I(x, y)]²
其中,I(x, y) 是图像在点 (x, y) 处的灰度值。这个公式计算了位移前后窗口内所有像素灰度差的平方和。
为了便于计算和分析,我们通常使用泰勒展开对其进行简化。最终,E(u, v) 可以表示为以下矩阵形式:
E(u, v) ≈ [u, v] M [u, v]ᵀ

这里的 M 是一个2x2的矩阵,称为结构张量,它由图像在x和y方向上的梯度(即导数 I_x 和 I_y)计算得出:
M = Σ [ I_x² I_x I_y; I_x I_y I_y² ]

窗口内每个像素的梯度都会贡献到这个求和当中。这个矩阵 M 捕获了窗口内灰度变化的本质信息。
角点的判断准则 🔍

我们已经得到了描述灰度变化的矩阵 M。如何根据 M 来判断一个区域是平坦区域、边缘还是角点呢?答案在于分析矩阵 M 的特征值 λ1 和 λ2。
以下是判断准则:
- 如果两个特征值
λ1和λ2都很小,说明该区域在任何方向上移动灰度变化都小,这是平坦区域。 - 如果其中一个特征值很大,另一个很小(即一个特征值远大于另一个),说明灰度只在一个方向变化剧烈,这是边缘区域。
- 如果两个特征值
λ1和λ2都很大,说明该区域在任何方向上移动灰度变化都剧烈,这就是我们要找的角点。
在实际的HARRIS角点检测算法中,为了避免直接计算耗时的特征值,定义了一个角点响应函数 R:

R = det(M) - k * (trace(M))²

其中,det(M) 是矩阵 M 的行列式(约等于 λ1 * λ2),trace(M) 是矩阵 **M的迹(等于λ1 + λ2),k` 是一个经验常数(通常取0.04~0.06)。
根据响应值 R 进行判断:
- R 很大:该区域是角点。
- R 为绝对值较大的负值:该区域是边缘。
- R 的绝对值很小:该区域是平坦区域。
算法会计算图像中每个像素点的 R 值,并通过设定阈值和非极大值抑制来最终确定角点的位置。
总结 📝
本节课中我们一起学习了HARRIS角点检测的基本原理。
- 我们首先从视觉上理解了角点的定义:即在所有方向上灰度变化都剧烈的图像点。
- 然后,我们探讨了其背后的数学原理,核心是使用结构张量M来量化窗口移动带来的灰度变化。
- 最后,我们了解到通过分析矩阵 M 的特征值或计算角点响应函数R,可以有效地将图像区域分类为平坦区域、边缘和角点。

理解这些基本原理是掌握更复杂图像特征提取技术的重要基础。

课程P42:2-基本数学原理 🧮
在本节课中,我们将要学习图像处理中一个核心概念的基本数学原理。我们将从一个直观的图像区域平移问题出发,逐步推导出用于衡量图像局部结构变化的数学公式,并学习如何通过泰勒展开等方法对其进行化简和矩阵化表示。
图像区域的平移与灰度变化
对于一个图像,我们用 I 来表示其灰度级。当我们关注图像的一个小区域时,例如一个 3×3 的区域,它包含九个像素点。
在图像上滑动这个窗口时,每次框住的像素点集合是不同的。例如,第一次框住的点可以表示为 X1, X2, ..., X9,而滑动后框住的点则变为 Z1, Z2, ..., Z9。
我们需要比较的是这两个区域之间灰度的变化情况。这本质上是一个对应位置像素的减法操作。
数学公式的建立
上一节我们介绍了通过滑动窗口比较灰度变化的想法,本节中我们来看看如何将其总结成一个数学公式。
对于图像中的一个区域 XY,当我们将其平移 (Δx, Δy) 后,需要判断平移前后区域的自相似性,即灰度变化情况。
我们用 C 来表示最终的灰度变化情况。由于是对区域中每一个点进行判断,因此需要进行求和计算。公式的核心部分如下:
C(Δx, Δy) = Σ [I(u, v) - I(u+Δx, v+Δy)]²
其中:
- u, v 属于当前窗口 W 中的每一个像素坐标。
- I(u, v) 是平移前点 (u, v) 的灰度值。
- I(u+Δx, v+Δy) 是平移后对应点的灰度值。
- 进行平方操作是为了消除灰度值上升或下降带来的正负号影响,只关注变化的幅度。
引入权重窗口
在计算窗口内所有像素的贡献时,我们并非同等对待每一个像素。通常,窗口中心的像素比边缘的像素更能代表该区域的特征,因此影响应该更大。
为此,我们引入一个权重窗口 W。W(u, v) 的大小与图像窗口 I 完全相同,它定义了窗口中每个像素点的权重。
此时,完整的公式变为:
C(Δx, Δy) = Σ W(u, v) * [I(u, v) - I(u+Δx, v+Δy)]²

关于权重窗口 W 的选择,有以下常见方式:
以下是权重窗口的两种常见形式:

- 常数值窗口:窗口内所有权重值均为1,表示所有像素同等重要。
# 例如一个3x3的常数值窗口 W_constant = [[1, 1, 1], [1, 1, 1], [1, 1, 1]] - 高斯窗口:权重从中心向四周按高斯分布递减,中心像素权重最高。
# 例如一个近似的高斯窗口 W_gaussian = [[1, 2, 1], [2, 4, 2], [1, 2, 1]]

通常情况下,我们更倾向于使用高斯加权窗口。
公式的化简:泰勒展开
上一节我们建立了完整的灰度变化公式,但公式中的 I(u+Δx, v+Δy) 项直接计算并不方便。本节中我们来看看如何利用数学工具对其进行化简。
当平移量 Δx 和 Δy 相对较小时(在图像处理中通常是成立的),我们可以对 I(u+Δx, v+Δy) 进行一阶泰勒展开。

根据泰勒展开公式,有:
I(u+Δx, v+Δy) ≈ I(u, v) + Iₓ(u, v)Δx + Iᵧ(u, v)Δy
其中:
- Iₓ(u, v) 是图像 I 在点 (u, v) 处对 x 方向的偏导数(可理解为水平方向的变化率)。
- Iᵧ(u, v) 是图像 I 在点 (u, v) 处对 y 方向的偏导数(可理解为垂直方向的变化率)。
- 高阶无穷小项被忽略。
进行泰勒展开的目的是为了化简。将展开式代入原公式的平方项内部:
[I(u, v) - I(u+Δx, v+Δy)]² ≈ [I(u, v) - (I(u, v) + IₓΔx + IᵧΔy)]² = [ - (IₓΔx + IᵧΔy) ]²
由于外面有平方运算,负号会消失,因此上式等价于:
[Iₓ(u, v)Δx + Iᵧ(u, v)Δy]²

矩阵化表示
经过泰勒展开近似后,我们的灰度变化公式 C(Δx, Δy) 简化为:
C(Δx, Δy) ≈ Σ W(u, v) * [Iₓ(u, v)Δx + Iᵧ(u, v)Δy]²
为了表达和后续分析的方便,我们将其写成矩阵形式。将平方项展开:
[IₓΔx + IᵧΔy]² = (IₓΔx + IᵧΔy) * (IₓΔx + IᵧΔy) = Iₓ²Δx² + 2IₓIᵧΔxΔy + Iᵧ²Δy²

这可以看作是向量 [Δx, Δy] 与一个矩阵相乘的二次型形式。具体地,令向量 d = [Δx, Δy]ᵀ,我们可以构造一个 2×2 的矩阵 M(在考虑权重 W 的求和后):
M = Σ W(u, v) * [ [Iₓ², IₓIᵧ], [IₓIᵧ, Iᵧ²] ]

那么,灰度变化函数可以优雅地表示为:

C(Δx, Δy) ≈ dᵀ * M * d

这个矩阵 M 被称为结构张量或二阶矩矩阵,它捕获了图像窗口内在 (u, v) 点附近沿 x 和 y 方向的梯度强度信息及其相关性,是后续判断角点、边缘等图像特征的基础。

总结

本节课中我们一起学习了图像局部结构分析的数学基础推导。
- 我们从滑动窗口比较灰度变化的直观概念出发,建立了初始的数学公式。
- 然后,我们引入了权重窗口 W 来区分窗口内不同像素的重要性。
- 接着,为了解决公式计算难点,我们利用一阶泰勒展开对平移后的灰度值进行了线性近似,极大地简化了公式。
- 最后,我们将简化后的公式整理成简洁的矩阵二次型 dᵀ M d,并引出了核心的结构张量矩阵 M。

这个推导过程是理解许多经典图像特征检测算法(如Harris角点检测)的关键第一步。矩阵 M 的特征值将直接告诉我们该图像区域是平坦的、存在边缘还是角点。
课程P43:3-求解化简 🧮
在本节课中,我们将学习如何将Harris角点检测中的核心表达式进行化简和转换。我们将通过矩阵运算,将复杂的二次型表达式转化为更易于分析和理解的椭圆方程形式,并探讨其几何意义。
上一节我们介绍了角点响应函数的基本形式。本节中,我们来看看如何通过矩阵变换来化简这个表达式。
首先,我们需要将原始表达式中的Δx和Δy项提取出来。观察原始式子,在相减之后,除了Δx和Δy,还剩下Ix²、Iy²以及IxIy项。因此,我们的目标是将表达式转换为以Δx、Δy为一组,以Ix、Iy为另一组的形式。
为了证明转换的可行性,我们定义一个矩阵M。让我们验证通过左乘一个由Δx和Δy组成的行向量,能否还原出最初的表达式。
我们进行如下矩阵乘法运算:
设行向量为 [Δx, Δy],矩阵M为2x2矩阵。计算过程如下:
第一行乘第一列得到:Δx * Ix²
第一行乘第二列得到:Δy * IxIy
接着,计算第二列的结果:
Δx * IxIy + Δy * Iy²
至此,我们得到了一个1x2的行向量结果:[Δx*Ix² + Δy*IxIy, Δx*IxIy + Δy*Iy²]。
然后,将这个行向量右乘一个列向量 [Δx, Δy]^T,得到一个标量值:
(Δx*Ix² + Δy*IxIy) * Δx + (Δx*IxIy + Δy*Iy²) * Δy

展开后得到:
Δx² * Ix² + ΔxΔy * IxIy + ΔxΔy * IxIy + Δy² * Iy²
合并同类项后,结果为:
Δx² * Ix² + 2 * ΔxΔy * IxIy + Δy² * Iy²
可以看到,这个结果与原始展开的表达式完全一致。因此,我们使用矩阵M进行的变换是恒等变换,而非近似。
在证明了矩阵变换的有效性后,我们可以用更简洁的符号来重新定义矩阵M,以便观察其特性。
令:
A = Ix²
B = Iy²
C = IxIy
则矩阵M可以写为:
M = [ A C ]
[ C B ]
这是一个实对称矩阵。在线性代数中,实对称矩阵可以进行对角化操作。对角化可以理解为一种“标准化”操作,使得矩阵只有主对角线上有非零值,这些值就是矩阵的特征值(记为λ1, λ2)。
上一节我们将表达式转换成了矩阵形式。本节中,我们利用这个矩阵形式来进一步探索其几何含义。
将矩阵 M = [[A, C], [C, B]] 代入二次型表达式 E(Δx, Δy) = [Δx, Δy] * M * [Δx, Δy]^T 并展开,得到:
E(Δx, Δy) = A * Δx² + 2C * ΔxΔy + B * Δy²
这个形式看起来像什么?它非常类似于一个二次曲线方程。回想高中解析几何,椭圆的标准方程为 x²/a² + y²/b² = 1。
虽然我们的表达式右边不是1,而是灰度变化值E,但我们可以暂时考虑其等高线,即假设 E(Δx, Δy) = 常数(例如设为1)。在这个假设下,方程 A * Δx² + 2C * ΔxΔy + B * Δy² = 1 描述的就是一个椭圆。
这个椭圆可能不是标准的(即其主轴可能与坐标轴不平行),这是因为方程中存在交叉项 2C * ΔxΔy。如果C=0,椭圆方程就变为 A * Δx² + B * Δy² = 1,这将是一个主轴与坐标轴对齐的标准椭圆。

基于角点的一个重要性质——旋转不变性(即图像旋转后,角点依然是角点),我们可以对这个“歪斜”的椭圆进行标准化处理。
以下是核心思路:
- 表达式
A * Δx² + 2C * ΔxΔy + B * Δy² = 1定义了一个椭圆。 - 该椭圆的“歪斜”是由交叉项系数C引起的。
- 通过对实对称矩阵M进行对角化,我们可以找到一个新的坐标系(由特征向量定义),在这个新坐标系下,椭圆方程将不再包含交叉项,从而变为标准形式。
- 这个变换相当于将图像窗口(或我们观察的局部区域)旋转到与椭圆主轴对齐,这并不改变该点是否为角点的本质属性。

这个过程为我们提供了一种强大的分析工具:通过研究矩阵M的特征值,我们可以直接判断当前窗口内是否包含角点。

本节课中我们一起学习了Harris角点检测中核心表达式的化简过程。我们首先通过矩阵运算将表达式转换为二次型形式,然后将其几何意义解释为一个椭圆方程。最后,我们引出了通过对实对称矩阵对角化来消除交叉项、分析椭圆主轴(即特征向量)和尺度(即特征值)的思想,这为下一节理解最终的Harris角点响应判据奠定了坚实的基础。
课程P44:4-特征归属划分 🎯
在本节课中,我们将深入探讨Harris角点检测算法的核心数学原理。我们将学习如何通过分析图像局部区域的灰度变化,来区分角点、边缘和平坦区域。理解这一过程是掌握经典特征检测方法的关键。
对角化与椭圆标准化 🔄
上一节我们介绍了自相关函数与矩阵M。本节中我们来看看如何通过对角化操作来简化分析。
这是一个两行两列的实对称矩阵。根据线性代数知识,对于N行N列的实对称阵,一定能找到N个特征向量,使其可对角化。在这里,我们同样可以进行这样的对角化操作。

对角化完成后,我们得到特征值λ₁和λ₂。这相当于对矩阵进行了一个标准化操作。如果不熟悉对角化的深层含义,可以通俗地理解为:我们得到了一个“歪的”椭圆,现在要通过标准化操作把它“正过来”。
完成标准化操作后,矩阵中只剩下λ₁和λ₂。此时,我们的式子变成了:

λ₁ * Δx² + λ₂ * Δy² = C
这里C是一个常数。为了更标准地表示椭圆方程,我们通常会写成 x²/a² + y²/b² = 1 的形式。对比可知,1/a² 对应 λ₁,1/b² 对应 λ₂。

因此,我们可以得到:
a = 1 / √λ₁
b = 1 / √λ₂

这里,a和b分别代表椭圆的长轴和短轴。
特征值的几何意义 📐
解释完对角化过程后,我们来看看特征值λ₁和λ₂的几何意义。
λ₁和λ₂分别对应椭圆的两个轴(长轴和短轴)。当椭圆发生变化时,其大小由特征值决定。椭圆越大,意味着λ₁和λ₂的值越大。

让我们再梳理一遍:原始表达式是 AΔx² + BΔy²。通过对角化,我们消去了交叉项,并用λ₁和λ₂代替了A和B。当λ₁和λ₂变大时,椭圆变大,这意味着函数值E变大。


函数值E变大,意味着图像在该窗口内的灰度变化剧烈;反之,E变化小,则意味着灰度变化平缓。

区域类型判断 🧭
解释完函数E的意义后,我们现在可以对比不同情况,来判断一个区域是边缘、平坦区域还是角点。


以下是基于特征值λ₁和λ₂大小的判断逻辑:

- 边缘:当λ₁远大于λ₂,或λ₂远大于λ₁时,表示在一个方向上变化剧烈,而在垂直方向上变化平缓,这对应图像的边缘。
- 平坦区域:当λ₁和λ₂都较小且近似相等时,表示自相关函数在各个方向的变化都很小,整体灰度变化平缓,这对应平坦区域。
- 角点:当λ₁和λ₂都比较大时,表示无论向哪个方向移动,灰度都会发生剧烈变化,这对应图像的角点。


角点响应函数R 📊
到目前为止,我们知道了如何通过比较特征值的大小来定性判断区域类型。但光说“远大于”或“比较小”不够直接,科学家们定义了一个量化的“角点响应函数R”来解决这个问题。
R的定义如下:
R = λ₁ * λ₂ - k * (λ₁ + λ₂)²
其中k是一个较小的经验常数,在OpenCV中通常取0.04。
我们可以通过计算出的R值来更精确地判断:
- 平坦区域:λ₁和λ₂都很小,R值接近于0。
- 边缘:λ₁和λ₂一个大一个小,R值为负数。
- 角点:λ₁和λ₂都比较大,R值为正数。
因此,基本的判断方法是:R > 0 大致为角点,R ≈ 0 为平坦区域,R < 0 为边缘。


Harris算法流程总结 📝

最后,我们来总结一下Harris角点检测算法的完整流程。


以下是算法的主要步骤:
- 计算梯度:对输入图像,分别计算x方向和y方向的梯度(如一阶导数),得到I_x和I_y。
- 构建与计算矩阵M:根据梯度计算矩阵M中的各个元素(I_x², I_y², I_x*I_y),并通常进行高斯加权,然后计算该矩阵的特征值λ₁和λ₂。
- 计算响应与判断:根据特征值计算角点响应函数R,并根据R值的大小判断每个像素位置属于角点、边缘还是平坦区域。
- 非极大值抑制:在角点检测结果中,一个角点周围区域的响应值可能都很高。我们需要应用非极大值抑制,只保留局部区域内的最大值点,从而得到精确的、单像素级别的角点位置。

本节课中我们一起学习了Harris角点检测算法的数学原理。我们从自相关矩阵的对角化入手,理解了特征值对应于灰度变化椭圆的轴长,进而学会了通过比较特征值大小或计算响应值R来区分角点、边缘和平坦区域,并掌握了算法的完整实施流程。
课程P45:OpenCV角点检测实战 🎯
在本节课中,我们将学习如何在OpenCV中使用Harris角点检测算法。我们将通过调用一个简单的函数接口,对图像进行角点检测,并学习如何调整参数以优化检测结果。
概述
角点检测是计算机视觉中的一项基础任务,用于识别图像中两条或多条边缘相交的点。OpenCV提供了便捷的函数来实现Harris角点检测。本节教程将详细介绍该函数的使用方法、参数含义以及结果的可视化过程。
使用OpenCV进行角点检测
上一节我们介绍了角点检测的基本原理,本节中我们来看看如何在OpenCV中具体实现。
OpenCV中用于Harris角点检测的核心函数是 cv2.cornerHarris。该函数有四个主要参数,其调用格式如下:
dst = cv2.cornerHarris(src, blockSize, ksize, k)
以下是各个参数的具体说明:
- src:输入图像。要求数据类型为
np.float32。如果读取的图像格式不符,需要进行转换。 - blockSize:指定用于角点检测的邻域窗口大小。
- ksize:Sobel算子的孔径参数,用于计算图像梯度。通常设置为3。
- k:Harris检测器方程中的自由参数,用于调整角点检测的敏感度。OpenCV推荐值为0.04,取值范围通常在0.04到0.06之间。
实战步骤
接下来,我们通过一个国际象棋棋盘的例子,演示完整的角点检测流程。
首先,导入必要的工具包并读取图像。
import cv2
import numpy as np
# 读取图像(国际象棋棋盘)
img = cv2.imread(‘chessboard.jpg’)
print(‘原始图像形状:‘, img.shape) # 例如 (512, 512, 3)
然后,将彩色图像转换为灰度图,因为角点检测通常在灰度图上进行。

# 转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

接着,对灰度图执行Harris角点检测。
# 执行Harris角点检测
gray = np.float32(gray)
dst = cv2.cornerHarris(gray, 2, 3, 0.04)
print(‘检测结果形状:‘, dst.shape) # 例如 (512, 512)
检测结果 dst 是一个与输入图像同尺寸的数组,其中的每个值代表了对应像素点的角点响应强度(即公式中的C值)。值越大,该点是角点的可能性越高。
结果可视化
得到角点响应图后,我们需要设定一个阈值来筛选出真正的角点。通常,我们不使用固定阈值,而是与响应图中的最大值进行比较。
以下是筛选并标记角点的过程:
# 将检测结果进行膨胀,使角点标记更明显(非必需步骤)
dst = cv2.dilate(dst, None)

# 设定一个阈值(例如最大值的1%),筛选角点
img[dst > 0.01 * dst.max()] = [0, 0, 255] # 将角点位置标记为红色(BGR格式)

# 显示结果
cv2.imshow(‘Harris Corners‘, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
在这段代码中,dst > 0.01 * dst.max() 会生成一个布尔型数组,其中 True 表示该位置的响应值超过了最大值的1%。我们将原图中这些位置像素的BGR值设置为 [0, 0, 255],即用红色圆点标记出角点。
参数调整与效果对比

如果觉得检测到的角点过多或过少,可以调整阈值比例。例如,将阈值提高到最大值的10%或50%:
# 使用更严格的阈值(10%)
img[dst > 0.1 * dst.max()] = [0, 0, 255]

# 使用非常严格的阈值(50%)
img[dst > 0.5 * dst.max()] = [0, 0, 255]
提高阈值后,只有响应更强的点会被标记为角点,从而减少检测数量。反之,降低阈值会增加检测到的角点数量。最佳阈值需要根据具体图像和应用场景进行调整。

在其他图像上的应用
Harris角点检测同样适用于其他类型的图像,例如建筑场景。通过更换输入图像,我们可以检测建筑物、树木等物体的角点。
# 对另一张图像(如房屋)进行角点检测
img_building = cv2.imread(‘building.jpg’)
gray_building = cv2.cvtColor(img_building, cv2.COLOR_BGR2GRAY)
gray_building = np.float32(gray_building)
dst_building = cv2.cornerHarris(gray_building, 2, 3, 0.04)
# ... 后续可视化步骤同上
在实际应用中,最好的学习方法是使用自己的图片进行测试和实验,观察不同参数下的检测效果。
总结

本节课中我们一起学习了OpenCV中Harris角点检测的实战应用。我们掌握了 cv2.cornerHarris 函数的使用方法,理解了其关键参数的含义,并完成了从图像读取、灰度转换、角点检测到结果可视化的完整流程。最后,我们还探讨了如何通过调整阈值来优化角点检测的效果。掌握这一工具,是进行更复杂图像特征提取和匹配的重要基础。
课程P46:SIFT算法(1)- 尺度空间定义 🏞️
在本节课中,我们将要学习图像特征匹配中的SIFT算法。SIFT全称为尺度不变特征变换,是一种具有平移不变性的图像特征匹配算法。这个算法是计算机视觉领域最常用的方法之一。虽然原始的SIFT算法在2004年完善,后续出现了许多改进版本,但其核心思想基本保持不变。由于SIFT算法涉及的数学概念和整体流程较多,我们将分模块进行解读。首先,我们来介绍第一个核心模块:图像的尺度空间。

图像的尺度空间

上一节我们介绍了SIFT算法的背景,本节中我们来看看什么是图像的尺度空间。

人类视觉系统具备一个关键能力:无论物体离我们是远是近、看起来是清晰还是模糊,我们通常都能识别出来。例如,在远处就能认出熟悉的人。我们希望计算机视觉系统也能具备类似的能力,即不仅能在图像清晰时提取特征,在图像模糊或尺度(大小)变化时也能提取出稳定的特征。这就是构建“尺度空间”的目的。
为了实现这个目标,我们需要对图像进行一系列变换,模拟人在不同观察条件下看到的图像。以下是构建尺度空间的核心步骤。
高斯滤波:实现尺度变换的工具
我们需要一种工具来系统地改变图像的清晰度(尺度)。这里使用的方法是高斯滤波。

高斯滤波的核心是一个高斯函数,它定义了一个滤波核(或称卷积核)。这个核的特点是其数值分布呈“钟形”,中心点的权重最大,向四周逐渐减小。滤波过程就是用这个核在原始图像上进行卷积操作。
用公式表示,二维高斯函数如下:
G(x, y, σ) = (1 / (2πσ²)) * exp(-(x² + y²) / (2σ²))
其中,(x, y)是像素点的坐标,σ(西格玛)是高斯分布的标准差。

标准差σ的作用
参数σ控制着高斯滤波的模糊程度。σ值越大,高斯核的权重分布越平缓,对原始图像的平滑(模糊)效果就越强;σ值越小,高斯核的权重越集中在中心,对图像的改变就越小,图像越接近原始状态。
因此,通过选择不同的σ值进行高斯滤波,我们可以得到同一图像在不同模糊程度(即不同尺度)下的版本。这就像从远处(模糊、大尺度)和近处(清晰、小尺度)观察同一个场景。

构建尺度空间的过程

具体操作是:从原始图像开始,使用一系列逐渐增大的σ值对其进行高斯滤波。这样就生成了一个图像序列,序列中的图像从清晰逐渐变得模糊。这个图像序列就构成了该图像的一个“尺度空间”。


本节课中我们一起学习了SIFT算法的第一个核心概念——尺度空间。我们了解到,为了让计算机像人眼一样在不同观察条件下稳定地识别特征,需要构建图像的尺度空间。这主要通过使用不同标准差σ的高斯滤波器对图像进行模糊处理来实现。σ值控制模糊程度,从而模拟出图像在不同尺度下的表现形式。理解尺度空间是掌握后续SIFT算法关键步骤的基础。

课程P47:2-高斯差分金字塔 🔍
在本节课中,我们将要学习尺度空间理论中的一个核心概念——高斯差分金字塔。我们将了解为什么需要构建图像金字塔,以及如何通过高斯差分来有效地识别图像中的关键特征点。

上一节我们介绍了高斯模糊在构建尺度空间中的作用。本节中我们来看看如何结合图像金字塔来构建一个更完整的尺度空间。

光进行高斯模糊是不够的。就像从远处就能认出班主任一样,计算机也需要在不同距离(尺度)下识别特征。因此,我们需要构建一个图像金字塔。
图像金字塔之前解释过。例如,最底层是一张400×400的图像。往上一层变为200×200,再上一层变为100×100。这就是一个图像金字塔。在特征提取过程中,无论目标较大(近处)还是较小(远处),计算机都需要能提取出其特征。

在图像尺度空间的构建中,我们需要做两件事:
- 在同一个尺寸的层面上(例如都是400×400),进行多次不同参数的高斯模糊。
- 在不同尺寸的金字塔层上,重复上述操作。

以下是构建过程的详细说明:
- 在400×400这一层,我们可以进行六种不同参数的高斯滤波变换。
- 同理,在200×200这一层,也需要进行六种不同的高斯滤波变换。
- 在100×100这一层,同样需要进行六种不同的高斯滤波变换。
因此,我们的尺度空间有两个层面:第一个层面是不同分辨率的图像金字塔;第二个层面是金字塔每一层内部,都需要生成多个经过不同高斯模糊的结果。

理解了尺度空间的构建后,接下来我们要介绍一个核心概念:高斯差分。

直接比较原始特征可能难以区分关键信息。例如,想区分A和B两个学生:
- A:身高180cm,体重60kg,成绩100分。
- B:身高180cm,体重59kg,成绩5分。
如果用身高比较,差异为0,无法区分。用体重比较,差异为1,也很小。但用成绩比较,差异为95分,这就能清晰地区分好坏。我们认为,呈现显著差异的地方才是有价值的特征。
在图像处理中,我们使用高斯差分金字塔来寻找这种差异性。其核心思想是:对同一金字塔层内、相邻的两个高斯模糊图像进行相减。
以下是高斯差分的具体做法:
- 假设在400×400这一层,我们得到了5张不同模糊程度的图像(记为1,2,3,4,5)。
- 由于它们尺寸相同,我们可以进行差分计算:图像2减去图像1,图像3减去图像2,图像4减去图像3,图像5减去图像4。
- 这样,5张输入图像最终会得到4个差分结果图。
- 在200×200层,也进行同样的操作,得到4个差分结果图。
这些差分结果图用于观察图像中的每个点。SIFT算法的目标就是寻找特征点。那么什么样的点能成为特征点呢?在差分结果中,数值较大(无论是正极大值还是负极小值)的极值点,通常被认为是重要的、有区分度的特征。

最后,我们从数学上定义一下高斯差分。一个高斯差分结果 D(x, y, σ) 需要三个参数来确定:
x, y:表示该点在图像中的具体位置坐标。σ:表示生成该差分结果所使用的高斯模糊核的参数(尺度)。


其计算公式可以表示为:
D(x, y, σ) = (G(x, y, kσ) - G(x, y, σ)) * I(x, y) = L(x, y, kσ) - L(x, y, σ)
其中:
G是高斯核函数。I是原始图像。L是图像经过高斯模糊后的结果。k是一个常数乘子,用于控制相邻模糊尺度之间的比例。

本节课中我们一起学习了高斯差分金字塔的构建原理与作用。我们了解到,通过构建图像金字塔并在每一层内计算相邻高斯模糊图像的差分,可以有效地凸显出图像在不同尺度下的显著特征区域,为后续定位关键特征点奠定了基础。核心在于,差分结果中的极值点对应了图像中稳定性高、区分度强的潜在特征位置。


课程P48:3-特征关键点定位 🔍
在本节课中,我们将学习SIFT算法中特征关键点定位的核心步骤。我们将重点探讨如何从高斯差分金字塔中检测极值点,以及如何对这些离散的极值点进行精确定位,使其更接近真实的连续极值位置。

极值点检测
上一节我们介绍了高斯差分金字塔的构建。本节中我们来看看如何从中找出极值点。
我们的目标是在高斯差分金字塔中寻找局部极值点(极大值或极小值)。在SIFT算法中,寻找极值点并非仅在一个二维图像平面内进行,而是在三维尺度空间中进行比较。


以下是具体的比较方法:
- 对于一个候选点,需要将其与同尺度图像中周围8个相邻像素进行比较。
- 同时,还需要与上一层尺度和下一层尺度图像中,对应位置3x3窗口内的9个像素点进行比较。
- 因此,每个候选点总共需要与 8 + 9 + 9 = 26 个点进行比较。



只有当一个点的值比这26个邻居的值都大(极大值)或都小(极小值)时,它才会被初步标记为关键点。


需要注意的边界情况:
- 尺度空间的最底层和最顶层的图像无法进行上下层比较,因此无法检测极值点。
- 若要在
S个尺度中检测极值点,则需要构建S+2层高斯差分金字塔,因为首尾两层无法使用。 - 相应地,构建高斯差分金字塔需要的高斯模糊图像层数
G应等于S+3。

关键点的精确定位
通过极值检测我们得到了一系列离散的关键点。然而,这些离散的点并不一定是连续尺度空间中准确的极值点位置。
我们需要对这些离散的极值点进行“微调”或“拟合”,以找到更精确的连续极值点位置。这个过程称为关键点的精确定位。
其核心思想是:利用离散点及其邻域的信息,通过数学方法拟合出连续的极值函数,从而找到更精确的极值位置。
一维情况下的拟合原理
为了便于理解,我们先看一个一维函数的简化例子。假设我们通过离散采样检测到一个极值点位于x=0处,其函数值为D(0)。但真实的连续极值点可能位于x=0附近。
我们可以利用泰勒展开式,在x=0处对连续函数D(x)进行近似:
D(x) ≈ D(0) + (∂D/∂x)*x + 0.5*(∂²D/∂x²)*x²
其中:
D(0)是离散点x=0处的函数值。∂D/∂x是函数在x=0处的一阶导数(梯度)。∂²D/∂x²是函数在x=0处的二阶导数。
对于图像这样的离散数据,导数无法直接计算,但可以用差分来近似。例如,一阶导数可以近似为:
∂D/∂x ≈ [D(1) - D(-1)] / 2
拟合出连续函数D(x)的近似表达式后,为了找到其真实的极值点,我们令其一阶导数为零并求解x:
∂D(x)/∂x = (∂D/∂x) + (∂²D/∂x²)*x = 0
解出偏移量x̂后,将其代回原泰勒展开式,即可得到修正后更精确的极值D(x̂)。
SIFT中的三维精确定位
在实际的SIFT算法中,关键点存在于三维空间:图像坐标(x, y)和尺度σ。因此,精确定位是在三维空间中进行的。



其原理与一维情况相同,但使用了向量和矩阵的形式来表示。对离散检测到的关键点(x, y, σ),其修正量X̂ = (Δx, Δy, Δσ)^T可以通过求解以下方程得到:


X̂ = - (∂²D⁻¹/∂X²) * (∂D/∂X)


其中:
∂D/∂X是三维梯度向量。∂²D/∂X²是Hessian矩阵(二阶偏导数矩阵)。
将计算出的偏移量X̂加回到原离散坐标上,就得到了精确定位后的关键点位置。同时,将X̂代入展开式也能得到该极值点更准确的响应值D(X̂),这个值可用于后续筛选低对比度的不稳定关键点。




总结


本节课中我们一起学习了SIFT特征关键点定位的两个核心步骤:
- 极值点检测:在高斯差分金字塔的三维尺度空间中,将每个像素点与其周围的26个邻域点进行比较,初步找出局部极值点。
- 关键点精确定位:由于初步检测到的极值点是离散的,我们利用泰勒展开式对离散点进行拟合,通过求导数为零的方法,在连续空间中计算出更精确的极值点位置和响应值。这个过程将关键点从离散的像素坐标修正到了亚像素级别的精度。



理解这两个步骤,特别是精确定位的数学思想,对于掌握SIFT算法的精髓至关重要。虽然其中涉及一些数学推导,但核心目标始终是:让找到的特征点位置尽可能稳定和准确。
课程P49:SIFT特征描述生成教程 🧭
在本节课中,我们将学习SIFT算法中生成特征描述的关键步骤。上一节我们介绍了关键点的定位与过滤,本节中我们来看看如何为这些关键点生成具有旋转不变性的特征描述符。
消除边界响应
当我们得到极值点的具体位置之后,还需要对这些位置进行过滤,判断其重要性。论文中提到一个步骤叫做“消除边界响应”。因为我们之前使用高斯差分函数进行滤波操作,可能会增强图像边缘的响应,因此需要消除这些响应。


这里的修正方法与之前讲解的Harris角点检测方法基本一致。在讲解角点检测时,我们提到了特征值λ1和λ2。当一个特征值大、另一个特征值小时,该点通常位于边界上。SIFT论文中采用了相同的思路,定义了α(较大的特征值)和β(较小的特征值),它们构成了Hessian矩阵:

H = [Dxx, Dxy;
Dxy, Dyy]


其计算方法与Harris角点检测完全相同。核心判断条件是特征值的比值。我们让α代表较大的特征值,β代表较小的特征值。如果它们的比值大于一个阈值(论文中设定γ=10),即一个特征值远大于另一个,那么该点大概率位于边界上。对于这类边界点,我们应当进行过滤操作。
以下是边界响应的消除步骤:
- 计算关键点处的Hessian矩阵。
- 求解该矩阵的特征值α和β。
- 判断比值
r = α / β是否大于阈值(例如10)。 - 若大于阈值,则判定为边缘响应点,将其剔除。
关键点方向分配

在得到稳定的关键点后,我们需要将其转换为计算机能够识别的数值向量。在生成描述向量之前,必须先为每个关键点分配一个主方向,以确保描述符具有旋转不变性。

方向定义方法很简单:对于当前关键点,我们计算其梯度幅值和方向。这与Harris角点检测中计算梯度方向的方法一致。梯度方向通过反正切函数计算,梯度幅值通过各方向梯度平方和的平方根计算。
因此,当我们得到一个关键点后,可以获得以下三个核心信息:
- 位置:由坐标 (x, y) 表示。
- 尺度:由检测到该关键点的高斯差分金字塔的尺度σ决定。
- 方向:由该点邻域内像素的梯度方向统计决定。
基于梯度方向直方图的主方向确定
有了位置、尺度和方向信息后,我们来看如何具体确定主方向。这需要借助梯度方向直方图。

观察关键点的一个邻域区域(论文对邻域半径有具体选择策略,此处我们理解核心思想即可)。在这个邻域内,每个像素都有其梯度方向和幅值,如同许多带有方向和长度的“小箭头”。
为了统计这些方向信息,我们使用方向直方图。直方图的X轴代表方向区间,Y轴代表落入该区间的梯度幅值累加和(而非简单的点数)。为了简化,通常将360度的方向范围划分为8个区间(每45度一个区间),例如0-44度、45-89度等。

以下是构建方向直方图的步骤:
- 在关键点的尺度空间邻域内,计算所有像素的梯度幅值和方向。
- 将方向范围量化为8个区间(bin)。
- 将每个像素的梯度幅值累加到其对应的方向区间中。
- 直方图的峰值代表了该关键点邻域梯度的主方向。
通常情况下,我们会得到一个明显的主方向峰值。但有时,可能存在一个与主峰值高度接近(例如达到主峰值80%以上)的次峰值。此时,论文建议将此关键点复制一份,并分别以主方向和次方向生成两个特征描述符。这样,一个关键点可能对应多个不同方向的特征向量,增强了匹配的鲁棒性。
总结

本节课中我们一起学习了SIFT特征描述生成的核心步骤。我们首先了解了如何通过Hessian矩阵特征值比值来消除不稳定的边缘响应点。接着,探讨了为关键点分配方向的重要性,这是实现旋转不变性的基础。最后,详细讲解了如何通过梯度方向直方图来确定关键点的主方向,并处理多峰值情况。这些步骤为下一步将关键点转换为独特的特征描述向量奠定了坚实基础。
图像处理基础课程 P5:图像平滑处理 📸
在本节课中,我们将要学习图像处理中的一项基础技术——图像平滑处理。平滑处理的核心是对图像数据进行各种滤波操作,其目的是去除图像中的噪声,使图像看起来更柔和。如果你了解过卷积的概念,那么理解平滑处理将会非常简单。

概述

我们首先来看一下本次处理的输入图像。这张图像经过了一些特殊处理,上面存在明显的椒盐噪声点。我们的目标就是通过滤波操作,尽可能去除这些噪声。


如上图所示,输入图像中包含了许多不规则的亮点和暗点,这些就是我们需要处理的噪声。接下来,我们将介绍几种不同的滤波方法来平滑图像。
均值滤波
上一节我们看到了带有噪声的图像,本节中我们来看看第一种平滑处理方法——均值滤波。对于不熟悉滤波概念的同学,可以将其理解为一种简单的平均卷积操作。
其核心思想是:对于图像中的每一个像素点,将其值替换为以其为中心的邻域内所有像素值的平均值。这能有效平滑局部区域的突变。
以下是均值滤波的工作原理:
- 定义一个固定大小的卷积核(例如 3x3)。
- 将这个核在图像上滑动。
- 对于核覆盖的每一个区域,计算区域内所有像素值的平均值。
- 用这个平均值替换中心像素点的原始值。
用公式描述,对于一个 3x3 的核,中心点的新值计算如下:
新像素值 = (P1 + P2 + P3 + P4 + P5 + P6 + P7 + P8 + P9) / 9
其中 P1 到 P9 是核覆盖的 9 个像素点的值。

在代码实现中,我们通常使用 OpenCV 库的 cv2.blur() 函数。
import cv2
# 读取图像
img = cv2.imread(‘noisy_image.jpg’)
# 应用均值滤波,使用 5x5 的核
blurred = cv2.blur(img, (5, 5))
让我们看一下均值滤波的效果。原始图像中的椒盐噪声非常明显。



经过均值滤波处理后,图像中的噪声点变得模糊,整体观感更为平滑。



可以看到,噪声虽然被削弱,但图像的边缘也同时变得模糊,这是均值滤波的一个特点。

方框滤波
了解了基础的均值滤波后,我们来看一个与之非常相似的操作——方框滤波。你可以将方框滤波视为均值滤波的一个更通用的版本。

方框滤波的核心操作同样是卷积求和,但它多了一个是否进行归一化的选项。以下是方框滤波的关键参数:
- 输入图像:需要处理的原始图像。
- 目标图像深度:通常设为
-1,表示输出图像与输入图像深度相同。 - 核大小:例如
(3,3)或(5,5),定义参与计算的邻域范围。 - 归一化标志:这是一个布尔值,决定是否在求和后除以核内元素总数。
当 normalize=True 时,方框滤波的效果与均值滤波完全一致,计算公式为:
新像素值 = (核内像素值之和) / (核宽 * 核高)
当 normalize=False 时,方框滤波直接输出核内像素值之和,而不进行平均。这可能导致像素值超过255(对于8位图像),此时OpenCV会将这些越界值截断为255。
以下是使用 OpenCV 的 cv2.boxFilter() 函数的示例:
import cv2
img = cv2.imread(‘noisy_image.jpg’)
# 应用方框滤波,并进行归一化(效果同均值滤波)
box_blur_norm = cv2.boxFilter(img, -1, (5,5), normalize=True)
# 应用方框滤波,不进行归一化
box_blur_no_norm = cv2.boxFilter(img, -1, (5,5), normalize=False)
让我们对比一下效果。当 normalize=True 时,结果与均值滤波相同。


当 normalize=False 时,由于大量像素值在求和后超过255并被置为255,图像中会出现大面积的白色区域。


因此,在使用方框滤波时,通常建议保持归一化选项开启,除非有特殊需求。
总结
本节课中我们一起学习了图像平滑处理的两种基础滤波方法。
- 均值滤波:通过计算邻域像素的平均值来平滑图像,能有效抑制噪声,但会使图像边缘模糊。
- 方框滤波:均值滤波的泛化形式,通过
normalize参数控制是否进行归一化。归一化时与均值滤波等价;不归一化时直接求和,可能导致像素值越界。

这两种方法是图像去噪和预处理中非常实用的工具,为后续更复杂的图像分析任务奠定了基础。

课程P50:5-特征向量生成 📊

在本节课中,我们将学习SIFT(尺度不变特征变换)算法中,如何从一个检测到的关键点生成一个具有旋转不变性的特征描述符。我们将详细讲解从关键点到128维向量的完整构造过程。





概述


上一节我们介绍了如何检测和定位关键点。本节中,我们来看看如何为这些关键点生成一个稳定且具有不变性的数学描述,即特征向量。这个过程是SIFT算法的核心,它确保了特征在不同图像变换下(如旋转)仍能被正确匹配。
1. 旋转不变性的保证 🔄
为了保证特征在图像旋转时保持不变,SIFT算法引入了一个关键步骤:将坐标轴旋转到关键点的主方向上。
具体做法是:以特征点为中心,计算其邻域内像素的梯度方向和大小,并确定一个主方向。无论原始图像如何旋转,在生成特征描述符之前,都会将邻域旋转到这个主方向上来。这确保了特征具有旋转不变性。

旋转操作本质上是一个坐标变换。给定一个旋转角度θ,新的坐标可以通过以下公式计算:

\begin{bmatrix} x' \\ y' \end{bmatrix} = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix}


其中,θ就是通过梯度计算得到的主方向角度。这样,所有后续操作都在一个标准化的方向上进行。


2. 构建特征描述符 🧱



旋转操作完成后,接下来需要围绕关键点构建一个特征描述符。这个描述符将关键点周围的图像信息编码成一个固定长度的向量。

首先,在旋转后的主方向上,以关键点为中心,选择一个邻域区域(例如一个16×16像素的窗口)。然后,将这个窗口进一步划分为更小的子区域。



以下是构建描述符的核心步骤:



- 划分邻域:将选定的16×16邻域窗口,划分为4×4共16个更小的子块。
- 计算梯度直方图:对于每一个4×4的子块,计算其内部所有像素的梯度方向。梯度方向被量化为8个方向区间(例如0-45度,45-90度等)。
- 生成子向量:统计每个子块内8个方向上的梯度幅值累加和,这样就为每个子块生成了一个8维的向量。
- 组合成描述符:将16个子块的8维向量按顺序连接起来,最终形成一个16 × 8 = 128维的特征向量。


3. 高斯加权与最终向量 ✨

为了增强特征的鲁棒性,在计算每个像素对梯度直方图的贡献时,SIFT算法还使用了高斯加权。


具体来说,离关键点中心越近的像素,其梯度对描述符的贡献越大;离得越远的像素,贡献越小。这通过一个以关键点为中心的高斯窗口函数来实现,使得描述符对特征点位置的微小变化不敏感。


最终,对于一个关键点,我们得到的是一个128维的向量,通常表示为 (4, 4, 8),其含义是:由4×4个子块构成,每个子块由8个方向的梯度信息描述。


总结



本节课中我们一起学习了SIFT特征描述符的生成过程。我们从保证旋转不变性开始,通过旋转坐标轴到主方向来实现。接着,我们详细讲解了如何以关键点为中心,通过划分邻域、计算梯度方向直方图,最终构造出一个128维的特征向量。这个过程还包括了高斯加权等细节,以确保生成的特征具有高度的区分度和稳定性。理解这一步,是掌握SIFT算法如何实现稳健图像匹配的关键。
课程P51:OpenCV中SIFT函数使用教程 🛠️
在本节课中,我们将学习如何在OpenCV中使用SIFT算法,包括检测图像关键点并将其构建为特征向量。我们将从环境配置开始,逐步讲解函数的具体使用方法。
环境配置与版本降级
上一节我们介绍了SIFT算法的基本原理,本节中我们来看看如何在OpenCV中实际应用它。首先需要解决一个常见问题:由于SIFT算法在OpenCV 3.4.3及以上版本中受专利保护,我们需要将OpenCV版本降级到3.4.1.15才能使用相关函数。
以下是降级OpenCV版本的具体步骤:

- 首先,需要卸载当前已安装的高版本OpenCV包。
- 然后,重新安装指定版本的
opencv-python和opencv-contrib-python包。

具体操作命令如下。请根据你的Python环境,在相应的命令行或终端中执行:

# 1. 卸载已安装的OpenCV包
pip uninstall opencv-python opencv-contrib-python
# 2. 安装指定版本的OpenCV包
pip install opencv-python==3.4.1.15
pip install opencv-contrib-python==3.4.1.15
安装完成后,可以通过以下代码验证版本:
import cv2
print(cv2.__version__) # 应输出 3.4.1.15
SIFT关键点检测
环境配置完成后,我们就可以开始使用SIFT算法了。第一步是实例化SIFT检测器,并对图像进行关键点检测。
以下是使用SIFT检测关键点的完整代码流程:

- 导入OpenCV库并读取图像。
- 将图像转换为灰度图,这是SIFT算法要求的输入格式。
- 创建SIFT检测器实例。
- 使用
detect方法检测图像中的关键点。

import cv2

# 读取图像
img = cv2.imread('lena.jpg')
# 转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 实例化SIFT检测器
sift = cv2.xfeatures2d.SIFT_create()
# 检测关键点
keypoints = sift.detect(gray, None)

检测到的关键点存储在keypoints变量中,它是一个由OpenCV封装的对象列表,包含了每个关键点的位置、尺度和方向等信息。
绘制关键点
得到关键点后,我们可以将其可视化在图像上。OpenCV提供了drawKeypoints函数来完成这个任务,这比自己手动绘制每个点要方便得多。
以下是绘制关键点的代码:
# 在原始图像上绘制关键点
img_with_keypoints = cv2.drawKeypoints(img, keypoints, None)
# 显示结果
cv2.imshow('SIFT Keypoints', img_with_keypoints)
cv2.waitKey(0)
cv2.destroyAllWindows()
执行上述代码后,你将在图像上看到许多被圆圈标记的关键点。这些关键点通常位于图像的角点、边缘等具有显著纹理变化的区域。
计算特征描述符
关键点本身只提供了位置信息。为了进行图像匹配或识别,我们需要为每个关键点计算一个特征向量,即描述符。SIFT描述符是一个128维的向量,它对关键点周围的梯度信息进行了统计。
以下是计算特征描述符的步骤:
- 使用之前实例化的SIFT对象的
compute方法。 - 该方法需要传入图像和检测到的关键点。
- 它会返回更新后的关键点列表和对应的描述符矩阵。
# 计算关键点的描述符
keypoints, descriptors = sift.compute(gray, keypoints)
# 查看描述符的维度
print(descriptors.shape) # 输出类似 (6827, 128)
# 查看第一个关键点的描述符向量
print(descriptors[0])
输出结果(6827, 128)表示检测到了6827个关键点,每个关键点的描述符是一个128维的向量。这个描述符矩阵就是后续进行图像匹配、目标识别等高级任务的基础数据。
完整代码示例
为了更清晰地展示整个流程,这里提供一个从读取图像到获取描述符的完整代码示例。你可以用你自己的图像路径替换'test_image.jpg'。
import cv2
# 1. 读取并预处理图像
img = cv2.imread('test_image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2. 创建SIFT检测器并检测关键点
sift = cv2.xfeatures2d.SIFT_create()
keypoints = sift.detect(gray, None)
# 3. 绘制关键点(可视化)
img_kp = cv2.drawKeypoints(img, keypoints, None)
cv2.imshow('Detected Keypoints', img_kp)
cv2.waitKey(0)
# 4. 计算特征描述符
keypoints, descriptors = sift.compute(gray, keypoints)
print(f"找到 {len(keypoints)} 个关键点。")
print(f"描述符矩阵形状: {descriptors.shape}") # (关键点数量, 128)
cv2.destroyAllWindows()
总结

本节课中我们一起学习了在OpenCV中应用SIFT算法的完整流程。我们首先解决了因专利问题导致的版本兼容性,将OpenCV降级到3.4.1.15。接着,我们分步实现了SIFT检测器的实例化、关键点检测、关键点可视化以及最重要的128维特征描述符计算。这些步骤是许多计算机视觉任务(如图像匹配、三维重建、目标识别)的基础。现在你已经掌握了将图像转换为一系列具有鲁棒性的特征向量的方法,可以尝试将其应用于你自己的项目中。
课程P52:1-检测任务中阶段的意义 🎯
在本节课中,我们将要学习目标检测领域中的两种经典范式:两阶段(Two-Stage) 与 单阶段(One-Stage) 方法。我们将通过对比,理解YOLO系列算法的核心思想及其优缺点,为后续深入学习YOLO V1奠定基础。
两种检测范式的介绍

上一节我们介绍了课程的整体安排,本节中我们来看看目标检测任务中的两种经典做法。
在深度学习中,目标检测算法主要分为两类:两阶段(Two-Stage) 和 单阶段(One-Stage)。

单阶段(One-Stage)方法


我们先来看单阶段方法。单阶段方法的核心思想非常直接:将目标检测任务视为一个回归问题。
现在我们要做一个物体检测任务。输入一张图像,例如一张包含猫的图像,最终输出一个框住猫的边界框坐标。从本质上说,我们只需要得到四个预测值:
- x1, y1: 边界框左上角的坐标。
- x2, y2: 边界框右下角的坐标。
这看起来就是一个标准的回归任务。因此,在YOLO系列算法中,我们提到的 One-Stage 就是指:一个卷积神经网络(CNN)直接完成从图像到边界框坐标的回归。


其流程可以概括为:
输入图像 -> CNN网络 -> 回归输出 (x1, y1, x2, y2)

这意味着,从输入到输出,中间不需要任何额外的、复杂的中间步骤或补充模块,一个CNN网络就能完成所有工作。这是YOLO系列算法最核心的出发点。

两阶段(Two-Stage)方法

除了YOLO这类单阶段算法,目标检测领域还有另一类著名的算法,称为两阶段方法。


一个典型的代表是2015年底提出的 Faster R-CNN 算法,它常被认为是目标检测领域的里程碑式工作。我们通过下图来简单了解其思路:

对于两阶段方法,我们同样输入一张猫的图像,最终目标也是输出猫的位置坐标。但在这个过程中,算法增加了一个额外的步骤。
在论文中,这个额外的步骤被称为 区域建议网络(RPN, Region Proposal Network)。观察流程图可以发现,在得到最终结果前,系统会先产生一些 预选框(Proposals),如图中的绿色和黄色虚线框。
这给人的感觉是:为了完成检测任务,算法先进行了一次“预选”或“初选”,从整张图像中筛选出一些可能包含物体的候选区域,然后再对这些候选区域进行精细的分类和位置回归。
我们可以用一个比喻来理解两者的区别:
- 单阶段(One-Stage):好比要从全国人口中直接选出1000人参加比赛。
- 两阶段(Two-Stage):先在全国人口中进行一轮初选,挑出一批有特长的人,然后再从这批人中选出最终的1000人参加比赛。
显然,两阶段方法流程更复杂一些。但这种复杂性带来了潜在的好处:由于经过了预筛选,最终预测的结果可能更准确。

为何要了解这两种范式

大家如果对Faster R-CNN等两阶段算法不熟悉,没有关系,只需要记住它的核心是多了一步 预选操作 即可。


而我们今天开始要讲的 YOLO系列算法,是没有这个预选步骤的,它直接用一个网络完成预测。

我之所以要给大家讲解Two-Stage和One-Stage的概念,主要是为了让大家在学习YOLO算法时,能够清晰地认识到它的优点与缺点。学完一个算法后,我们需要知道在实际应用场景中如何选择:是选择Faster R-CNN,还是选择YOLO? 理解它们根本的设计差异,是做出正确选择的基础。



本节课中我们一起学习了目标检测中单阶段与两阶段方法的根本区别。单阶段方法(如YOLO)将检测视为端到端的回归问题,追求速度;而两阶段方法(如Faster R-CNN)通过增加区域建议步骤来提升精度。理解这一对比,有助于我们把握YOLO系列算法的设计精髓与适用场景。接下来,我们将正式进入YOLO V1的详细讲解。

课程P53:不同阶段算法优缺点分析 🧐
在本节课中,我们将一起学习目标检测领域中单阶段(One-Stage)与双阶段(Two-Stage)算法的核心区别,并重点分析以YOLO为代表的单阶段算法的优缺点。我们将通过对比,理解不同算法在速度与精度上的权衡。
单阶段算法(如YOLO)的核心优势
上一节我们介绍了目标检测算法的两种主要类型。本节中,我们来看看单阶段算法的核心优势。
单阶段算法,例如YOLO,其最大的特点是速度非常快。这是因为它的网络结构非常直接:输入一张图片,网络直接进行回归,一次性输出目标的边界框和类别。中间省去了像RPN(区域建议网络)这样的复杂预处理或补充步骤。
核心流程可以简化为:
输入图像 -> 单一神经网络 -> 回归得到检测结果
这种“一条龙”式的处理流程,使得YOLO在处理速度上具有巨大优势。在基于视频流的实时检测任务中,高帧率(FPS)是至关重要的,而YOLO系列算法正是为此类任务设计的。
以下是单阶段算法YOLO的主要优点:
- 速度极快:能够满足实时检测的需求,例如视频监控、自动驾驶等场景。
- 流程简洁:网络结构端到端,易于理解和实现。

因此,如果你的任务核心需求是实时性,那么YOLO是一个非常适合的选择。

单阶段算法的局限性

然而,单阶段算法并非完美。在享受速度优势的同时,我们也需要了解其付出的代价。

由于缺少了像双阶段算法中“候选区域筛选”这一精细步骤,YOLO等单阶段算法在检测精度上通常会做出一些妥协。其预测可能相对“粗糙”一些。
以下是单阶段算法的主要缺点:
- 精度相对较低:与顶尖的双阶段算法(如Faster R-CNN)相比,在同等条件下,其检测精度(通常用mAP值衡量)通常会稍逊一筹。
- 对小目标检测效果欠佳:密集或小目标场景下的检测能力可能较弱。
简而言之,选择YOLO意味着在速度与精度之间进行权衡:我们获得了飞快的处理速度,但检测效果可能不如一些更复杂的双阶段算法。
双阶段算法的特点
作为对比,我们简要了解一下双阶段算法的特点。
双阶段算法,如Faster R-CNN,其流程分为两步:首先由RPN网络生成可能包含目标的候选区域(Region Proposals),然后对这些候选区域进行精细的分类和边界框回归。
核心流程可以简化为:
输入图像 -> RPN生成候选区域 -> 对候选区域进行分类与回归
这种设计的优点是:
- 精度通常更高:因为经过了两轮筛选和优化,对目标的定位和识别更为精准。
- 缺点也很明显:流程复杂,速度较慢,难以达到实时检测的要求(例如Faster R-CNN的典型速度可能只有5 FPS)。
所以,如果你的任务对检测精度要求极高,且对实时性不敏感(如离线分析图像),双阶段算法是更优的选择。
如何衡量算法性能:FPS与mAP
在目标检测中,我们主要用两个指标来综合评价一个算法:

- FPS:每秒帧数。它直接衡量算法的速度。FPS值越高,代表处理速度越快,越能满足实时性要求。在YOLO中,你可以通过调整网络结构的复杂度来权衡FPS。
- mAP:平均精度均值。它综合了精度和召回率,是衡量算法检测精度的核心指标。mAP值越高,代表算法的检测效果越好。
一个基本规律是:在现有技术下,速度(FPS)和精度(mAP)往往是矛盾的。 追求更高的速度通常会导致精度下降,反之亦然。没有算法能同时在两个方面都达到极致。
总结
本节课中,我们一起学习了目标检测中单阶段与双阶段算法的核心优缺点。
- 单阶段算法(以YOLO为例):速度快,流程简洁,非常适合实时检测任务,但在检测精度上通常不如双阶段算法。
- 双阶段算法(以Faster R-CNN为例):精度高,检测效果更优,但速度慢,难以实现实时处理。

选择算法时,关键在于根据你的任务需求,在速度(FPS) 与精度(mAP) 之间做出合适的权衡。对于后续的YOLO系列课程,我们将聚焦于其网络结构的具体实现与优化,而无需深入双阶段算法的复杂细节。
课程P54:IOU与mAP指标计算详解 🎯

在本节课中,我们将学习目标检测任务中的两个核心评估指标:IOU和mAP。理解这些指标对于衡量和比较不同检测模型的性能至关重要。

上一节我们介绍了目标检测的基本任务,本节中我们来看看如何量化评估一个检测模型的好坏。


综合评估指标:mAP

mAP(mean Average Precision)是一个综合衡量检测效果的指标。对于检测效果,我们可以从精度(Precision)和召回率(Recall)两个角度分析。


- 精度:指检测到的物体框与实际标注框的吻合程度。
- 召回率:指所有实际存在的物体中,有多少被成功检测出来。

在机器学习中,精度和召回率通常是矛盾的:一个指标升高,另一个往往会降低。因此,单独看其中任何一个指标都难以全面评价模型性能。mAP正是为了解决这个问题而提出的,它综合了精度和召回率的信息。


在介绍mAP的计算方法之前,我们需要先理解另一个基础且重要的概念。


基础概念:IOU(交并比)


IOU是“Intersection over Union”的缩写,中文称为交并比。它衡量的是预测框与真实标注框之间的重叠程度。

以下是IOU的计算原理:




- 分子:是交集(Intersection),即预测框与真实框重叠的区域(图中红色部分)。
- 分母:是并集(Union),即预测框与真实框所覆盖的总区域。


其计算公式可以表示为:
IOU = 交集面积 / 并集面积


在目标检测中:
- 真实值(Ground Truth):数据集中人工标注的、物体实际所在位置的边界框(图中蓝色框)。
- 预测值(Prediction):模型预测出的物体边界框(图中黄色框)。


我们希望预测框能尽可能与真实框重合,即IOU值越高越好。IOU越高,说明预测的位置越准确;IOU越低,则说明预测偏差越大。

理解了IOU这个衡量“位置吻合度”的指标后,我们再回到对整体“检测效果”的评估。

精度与召回率的矛盾


为了更直观地理解mAP的必要性,我们来看看精度和召回率为何会矛盾。

- 精度关注的是“检测得准不准”,即每一个预测框是否都对应着一个真实的物体(高IOU)。
- 召回率关注的是“检测得全不全”,即是否把所有真实的物体都找出来了。


举个例子:如果一个模型为了不漏掉任何物体(提高召回率),它可能会输出非常多的预测框,其中必然包含大量错误或重叠的框,这会导致精度下降。反之,如果模型只输出把握最大的几个框(提高精度),就可能漏掉一些不明显的物体,导致召回率降低。


因此,我们需要一个像mAP这样的单一指标,来平衡精度和召回率,从而对模型性能给出一个综合评分。



本节课总结

本节课中我们一起学习了目标检测的核心评估指标:
- IOU(交并比):用于计算预测框与真实框的重叠程度,是衡量定位精度的基础指标,其值越高越好。
- mAP(平均精度均值):一个综合了精度(Precision)和召回率(Recall)的指标,用于全面评估检测模型的整体性能。它是比较和优化模型的关键依据。

理解IOU和mAP,是分析后续YOLO等目标检测算法改进与性能对比的基础。
课程P55:4-评估所需参数计算 📊

在本节课中,我们将学习目标检测任务中两个至关重要的评估指标:精度(Precision)与召回率(Recall)。理解这两个指标是计算mAP(平均精度均值)的基础,也是阅读论文和进行技术交流的核心。

核心概念:TP、FP、FN、TN
上一节我们介绍了评估指标的重要性,本节中我们来看看计算这些指标所需的基础参数。为了理解精度和召回率的公式,我们必须先明确几个核心概念:TP、FP、FN和TN。
这些术语通常会让初学者感到困惑。我们通过一个简单的例子来理解它们。
假设有一个班级,共有100人,其中男生80人,女生20人。我们的任务是“找出所有女生”。现在,我们从这个班级中挑选了50人进行预测,结果挑出了20名女生和30名男生。
以下是各个参数的含义及其在该例子中的计算:
- TP (True Positive):做对了,并且正确地将其判断为正例。在我们的任务中,正例是“女生”。因此,TP代表“该学生本是女生,且我们正确地将其预测为女生”。例子中,我们挑出了20名女生,所以 TP = 20。
- FP (False Positive):做错了,并且错误地将其判断为正例。这代表“该学生本是男生(负例),但我们错误地将其预测为女生(正例)”。例子中,我们错误地将30名男生当作女生挑出,所以 FP = 30。
- FN (False Negative):做错了,并且错误地将其判断为负例。这代表“该学生本是女生(正例),但我们错误地将其预测为男生(负例)或背景(即漏检)”。例子中,我们目标是找所有女生,且挑出的20名女生都已找到,没有遗漏,所以 FN = 0。
- TN (True Negative):做对了,并且正确地将其判断为负例。这代表“该学生本是男生(负例),我们也正确地将其预测为男生(负例)”。例子中,未被挑选的50人全是男生,且我们都将其视为背景(非女生),所以 TN = 50。
理解这些参数的关键在于拆分单词:T/F 代表预测正确/错误,P/N 代表预测结果为正例/负例。结合真实情况,就能推导出具体含义。
精度 (Precision) 详解
理解了基础参数后,我们首先来看精度指标。精度关注的是“在你所有认为是正例的预测中,有多少是真正正确的”。
精度的计算公式为:
Precision = TP / (TP + FP)
- 分子 TP:你正确检测到的目标数量。
- 分母 (TP + FP):你做出的所有正例预测的总数(包括正确的和错误的)。
代入我们例子中的数值:Precision = 20 / (20 + 30) = 0.4。
这意味着,在我们所有“认为是女生”的预测中,只有40%是真正的女生。精度越高,说明模型的误报(将背景误认为目标)越少。
召回率 (Recall) 详解
接下来我们看看召回率,它也被称为查全率。召回率关注的是“在所有真实的正例中,你成功找出了多少”。
召回率的计算公式为:
Recall = TP / (TP + FN)
- 分子 TP:你正确检测到的目标数量。
- 分母 (TP + FN):数据中所有真实正例的总数(包括你检测到的和漏掉的)。
代入我们例子中的数值:Recall = 20 / (20 + 0) = 1.0。
这意味着,所有真实的女生都被我们找了出来,没有遗漏。召回率越高,说明模型的漏检越少。
精度与召回率的关系
我们已经分别介绍了精度和召回率。它们各自从不同角度评估模型性能:精度衡量“准不准”,召回率衡量“全不全”。
然而,在绝大多数情况下,精度和召回率是相互矛盾的。提高检测阈值(更确信时才判定为目标)可以提升精度(减少FP),但会导致更多漏检(FN增加),从而降低召回率。反之,降低阈值以找到更多目标(提升召回率),往往会引入更多误报(FP增加),导致精度下降。
因此,单独使用任何一个指标都不足以全面评估模型。我们需要一个能综合权衡二者的指标,这就是下节课将重点介绍的mAP (mean Average Precision)。

本节课中我们一起学习了目标检测评估的基础。我们明确了TP、FP、FN、TN四个核心参数的定义与计算方法,并深入探讨了精度(Precision)和召回率(Recall)这两个关键指标的含义、公式及其内在的权衡关系。理解这些内容是掌握更高级评估标准mAP的必经之路。
课程P56:5-map指标计算 📊
在本节课中,我们将学习目标检测任务中一个非常重要的综合评价指标——mAP(平均精度均值)。我们将从基础概念入手,解释精度、召回率、置信度等术语,并通过一个具体的例子,一步步演示如何计算这些指标,最终理解mAP的含义。
概述
在目标检测任务中,我们需要一个综合指标来评价模型的性能。mAP就是这样一个指标,它综合考虑了模型在不同置信度阈值下的精度和召回率表现。理解mAP的计算过程,有助于我们更全面地评估和比较不同模型的优劣。
精度与召回率的概念

上一节我们介绍了目标检测的基本任务,本节中我们来看看如何评价检测结果的好坏。这通常涉及两个核心指标:精度和召回率。

- 精度:衡量模型检测出的结果中有多少是正确的。它关注的是检测框与真实框的匹配程度,希望预测框与真实框越接近越好。
- 召回率:衡量模型找出了多少本该被找出的目标。它关注的是是否存在漏检的问题。

例如,在下图中,左图的精度较高,因为检测框(蓝色)与真实框(蓝色)重合度高。右图的召回率较低,因为存在预测框与真实值IOU较小或未被检测到的情况,即出现了漏检。



置信度与阈值
在开始计算前,我们需要理解一个关键概念:置信度。
置信度描述了模型对其检测出的框包含目标(在本例中为人脸)的确信程度。它是一个介于0到1之间的值,值越高表示模型越确信。

在模型的输出中,每个预测框都会附带一个置信度分数,如下图所示中的0.9、0.8、0.7。


由于一张图片可能产生大量重叠的预测框,我们需要设定一个置信度阈值来进行筛选。高于此阈值的预测框将被保留,低于的则被过滤掉,以避免图像中出现过多无效的框。
指标计算示例
让我们通过一个具体例子,来计算在不同置信度阈值下的精度和召回率。假设我们有三张测试图片的检测结果,其置信度分别为0.9、0.8和0.7。
首先,我们需要定义几个基础变量:
- TP:真正例。模型正确检测到的人脸。
- FP:假正例。模型误将背景检测为人脸。
- FN:假反例。模型未能检测到实际存在的人脸。
情况一:阈值 = 0.9
当我们将置信度阈值设置为0.9时:
- 只有置信度为0.9的第一个预测框被保留。
- 置信度为0.8和0.7的预测框被过滤掉。

以下是各指标的计算过程:
- TP = 1(第一个框正确检测到人脸)
- FP = 0(没有错误的检测被保留)
- FN = 2(第二、三个实际存在的人脸未被检测到,属于漏检)


根据公式计算:
- 精度 = TP / (TP + FP) = 1 / (1 + 0) = 1
- 召回率 = TP / (TP + FN) = 1 / (1 + 2) = 1/3
在这个高阈值下,模型精度很高(100%),但召回率很低(33.3%),因为很多目标被漏掉了。
情况二:阈值变化的影响

显然,我们可以将阈值设置为0.8、0.7等其他值。每一个阈值都会对应计算出一组精度和召回率。
PR曲线与mAP
如果我们为从0到1的每一个可能阈值(或一系列阈值)都计算出对应的精度和召回率,并将这些点绘制在图上,就能得到一条精度-召回率曲线,即PR曲线。

在PR曲线中,通常可以看到一种趋势:当精度较高时,召回率往往较低;反之,当召回率较高时,精度则会降低。
mAP 的核心思想就源于此。对于一条PR曲线,我们计算其下方所围成的面积,这个面积值就是平均精度。对于多类别检测,对所有类别的平均精度再取平均,即得到mAP。


计算面积时,通常需要对PR曲线进行平滑处理,即对于每个召回率点,取其右侧精度的最大值,然后计算曲线下的面积,如上图阴影部分所示。

mAP值越接近1,表示模型的综合性能越好。

总结
本节课我们一起学习了目标检测中的关键评价指标mAP。
- 我们首先理解了精度和召回率分别衡量了检测结果的准确性和完整性。
- 然后,我们认识了置信度和阈值的概念,它们用于筛选预测结果。
- 接着,我们通过一个实例,演示了如何计算TP、FP、FN,并由此算出特定阈值下的精度和召回率。
- 最后,我们了解到通过遍历不同阈值可以得到PR曲线,而mAP正是这条曲线下面积的体现,它是一个综合考量精度与召回率的稳健指标。

掌握这些基础概念和计算逻辑,对于后续理解和分析目标检测模型的性能至关重要。
课程P57:YOLO算法整体思路解读 🎯
在本节课中,我们将从细节角度学习YOLO系列的第一代版本——YOLO V1。这是一个经典的单阶段(one-stage) 目标检测方法,其核心思想是将检测问题简化为一个回归任务,通过一个网络架构直接输出预测结果。
上一节我们介绍了目标检测的背景,本节中我们来看看YOLO V1是如何实现“只看一次(You Only Look Once)”的。
YOLO V1的设计动机
YOLO V1于2016年提出,其主要任务是实现视频的实时检测。在应用层面,它的前景非常广阔。
在2016年,虽然以Faster R-CNN为代表的两阶段(two-stage)检测算法已经出现,并且mAP(平均精度均值) 很高,但其FPS(每秒帧数) 非常低,难以满足实时检测的需求。相比之下,YOLO V1虽然在许多问题上的mAP值比Faster R-CNN低了约10个百分点,但其FPS非常高,对于检测简单物体(如人或常见物品)的场景非常实用和高效。因此,YOLO系列算法从V1开始便迅速流行起来。
YOLO V1的核心思想

YOLO V1的核心思想非常直接。作者的想法是:预测一张图像中有哪些物体。
以下是其核心步骤的分解:
首先,将输入图像划分为一个 S × S 的网格。例如,假设S=7,我们就得到了一个7x7的网格。每个网格单元负责预测中心点落在该单元内的物体。
例如,在一张包含狗和自行车的图片中,狗的中心点(用一个红点表示)落在了某个网格内。那么,这个网格单元就需要负责预测“狗”这个物体。
预测机制:从先验框到回归
那么,每个网格单元如何进行预测呢?我们可以观察到,在每个网格单元中,通常会预设两个先验框(Prior Boxes),在图中用黄色框表示。
这些先验框不是最终结果,而是基于经验设定的、常见物体的长宽比例。例如,一个可能是长方形(H1, W1),另一个可能是正方形(H2, W2)。
对于要检测的狗,我们比较这两个先验框。直观上,长方形的框(H1, W1)可能更贴合狗的形状。然而,这个先验框与实际边界框仍有差距。
因此,网络需要对这个看起来“更靠谱”的先验框进行微调。微调的本质是调整框的中心点坐标(x, y)、高度(h) 和宽度(w),使其与真实框匹配。
这个过程就是一个回归任务。网络需要预测出中心点坐标的偏移量以及长宽的缩放比例。归根结底,YOLO V1将目标检测问题转化为了对一个 7x7网格 x (B个框 x 5个值 + C个类别) 的张量的回归预测。
其核心预测值可以用以下公式表示:
每个网格预测值 = [P_obj, x, y, w, h, class_1, class_2, ..., class_C]
其中,P_obj是存在物体的置信度,x, y是边界框中心相对于网格单元的偏移,w, h是相对于图像尺寸的宽高,class是类别概率。
总结
本节课中,我们一起学习了YOLO V1算法的整体思路。

我们了解到YOLO V1是一个单阶段检测器,它将输入图像划分为SxS的网格,每个网格负责预测中心点落在其中的物体。算法通过预设先验框,并利用回归网络对这些框的位置和大小进行微调,从而直接输出检测结果。虽然其精度在初期略低于两阶段方法,但其极高的速度使其在实时检测领域获得了巨大成功,为后续YOLO系列的发展奠定了基础。
YOLO目标检测算法详解(P58)🚀
在本节课中,我们将要学习YOLO V1目标检测算法的核心思想与具体实现步骤。我们将从输入数据的处理开始,逐步讲解候选框的生成、筛选、微调以及最终检测框的获取过程。
输入与候选框生成
上一节我们介绍了目标检测的基本概念,本节中我们来看看YOLO V1如何处理输入数据。
YOLO V1将输入图像划分为一个 S x S 的网格。每个网格单元负责预测其中心点落入该单元的物体。
在每个网格单元中,算法会预先设定生成两种候选框(Bounding Box)。在V1版本的论文中,这个数量B被设定为2。
公式:
B = 2
这意味着每个格子会生成两个不同尺寸或比例的初始预测框,作为检测的起点。
候选框的筛选与微调
生成了候选框后,我们需要决定哪个框更有可能匹配真实的物体。这就引入了筛选机制。
对于每个网格单元,算法会计算其生成的两个候选框与真实标注框(Ground Truth)之间的重合度。这个重合度通过交并比(IOU)来衡量。
公式:
IOU = (Area of Overlap) / (Area of Union)
计算过程如下:
- 获取当前网格对应的真实物体标注框。
- 分别计算两个候选框与该真实框的IOU值。
- 选择IOU值更大的那个候选框作为“优胜者”。
这个“优胜”的候选框将被用于后续的微调(即位置和尺寸的修正),而另一个候选框在此步骤中暂时不被考虑。这类似于从两个替补队员中选择状态更好的一个上场比赛。
置信度(Confidence)预测
仅仅预测框的位置是不够的。网络还需要判断这个框内“是否有物体”。
因此,对于每个网格单元,网络除了预测B个候选框的坐标偏移量(Δx, Δy, Δw, Δh)外,还必须预测一个至关重要的值:置信度(Confidence)。
置信度反映了模型认为当前网格单元包含物体的概率,以及预测框与真实框的吻合程度。一个高的置信度(例如0.9)意味着模型高度确信该位置存在一个物体,且预测框较为准确。一个低的置信度(例如0.2)则意味着该位置很可能是背景或预测框质量很差。
以下是每个网格单元需要预测的全部内容:
- B个边界框的预测:每个框包含4个坐标值(中心点偏移和宽高缩放)。
- 1个物体类别概率:表示该网格内物体属于各个类别的概率(C个类别)。
- B个置信度分数:每个候选框对应一个置信度。
在V1中,B=2,所以每个网格最终输出一个长度为 (B * 5 + C) 的张量。例如,在PASCAL VOC数据集(20个类别)上,输出维度为 (2*5 + 20) = 30。
代码表示(概念性):
# 每个网格的输出向量示例 [x1, y1, w1, h1, conf1, x2, y2, w2, h2, conf2, class_prob1, class_prob2, ...]
grid_cell_output = [bbox1_coords, bbox1_confidence, bbox2_coords, bbox2_confidence, class_probabilities]
最终检测结果的生成
经过网络前向传播,我们得到了所有S x S个网格的预测值,其中包含大量候选框和其置信度。
为了得到最终简洁、准确的检测结果,需要进行以下后处理步骤:
- 置信度阈值过滤:设定一个阈值(如0.5)。将所有置信度低于该阈值的预测框直接过滤掉。这一步去除了大量被认为是背景的无效预测。
- 非极大值抑制(NMS):经过阈值过滤后,对于同一个物体,可能仍有多个重叠度很高的框(来自相邻网格或多个候选框)。NMS用于解决这个问题,其步骤是:
- 将所有框按置信度从高到低排序。
- 选取置信度最高的框,将其加入最终输出列表。
- 计算该框与剩余所有框的IOU。
- 移除那些IOU超过设定阈值(如0.45)的框(因为它们很可能检测的是同一个物体)。
- 重复上述过程,直到处理完所有框。
经过“置信度过滤”和“非极大值抑制”两步处理后,剩下的预测框就是算法最终输出的、简洁明了的目标检测结果。
核心流程总结
本节课中我们一起学习了YOLO V1目标检测算法的核心流程,让我们再回顾一下整个过程:

- 划分网格:将输入图像划分为S x S的网格。
- 生成候选框:每个网格预设生成B个(V1中B=2)初始候选框。
- 预测与计算:每个网格预测B个框的坐标、B个置信度以及物体类别概率。通过计算IOU筛选出与真实物体最匹配的候选框进行重点优化。
- 输出与过滤:网络输出所有网格的预测值,首先通过置信度阈值过滤掉低置信度的预测,然后通过非极大值抑制(NMS) 去除对同一物体的冗余检测框。
- 得到结果:最终剩下的预测框即为检测到的物体,其位置由框坐标确定,类别由类别概率确定。

这个过程实现了“只看一眼(You Only Look Once)”就能完成图像中多个物体的定位与分类,是YOLO系列算法高效性的基石。
YOLO V1 整体网络架构解读 🧠 - 课程 P59
在本节课中,我们将学习YOLO V1版本的整体网络架构。我们将从输入图像开始,一步步解析网络如何处理数据,并最终输出检测结果。理解这个流程是掌握YOLO工作原理的关键。
输入图像尺寸的限制 📏
首先,第一步是获取输入图像。在V1版本中,网络指定的输入大小是448×448×3。这是一个固定值。
你可能会认为,固定输入大小意味着只能检测固定尺寸的物体。其实并非如此。固定值只是意味着我们将所有输入图像都缩放(resize)到这个固定尺寸。图像中物体的坐标会相应地进行变换,最终结果可以映射回原始输入图像的大小。
然而,由于在V1版本中,网络只训练448×448×3尺寸的图像,这确实限制了我们的输入。输入图像的大小被固定了。在后续的改进版本中,这个尺寸可以被改变,但在当前V1版本中无法改变。
为何输入尺寸必须固定?🔗
上一节我们介绍了输入尺寸的限制,本节中我们来看看其根本原因。大家可能会想,一个卷积神经网络的输入大小为什么不能改变?原因是什么?
一个卷积神经网络通常包含卷积层和全连接层。例如:
卷积层1 -> 卷积层2 -> 卷积层3 -> 全连接层1 (FC1) -> 全连接层2 (FC2)
那么,最终网络大小不能改变的主要原因在于卷积层还是全连接层?
卷积层对输入大小有严格要求吗?实际上,只要我们设置好卷积核,卷积操作可以在任何尺寸的原始图像上提取特征。无论是400×400还是800×800的图像,卷积都能进行,只是得到的特征图尺寸会不同。
但是,全连接层可以吗?可以这么说,全连接层的结构是“定死”的。例如,假设卷积层输出的特征图被拉平(flatten)后得到2048个特征。然后,FC1层要将其转换为1024个特征。这需要一个权重参数矩阵 W1 和一个偏置参数矩阵 B1。
- W1 的尺寸必须是 2048 × 1024。
- B1 的尺寸取决于输出,即 1024。
这里有一个关键问题:在训练网络时,W1 和 B1 的维度能动态改变吗?例如,W1 这次是2048×1024,下次变成1024×1024?这似乎从未见过。也就是说,全连接层要求前一层的特征数量必须是固定的。如果前面的特征数量不固定,全连接层就无法工作。
因此,由于YOLO V1版本中包含了全连接层,我们必须限制输入数据的大小,必须是 448×448×3。
特征提取网络 🔍
中间过程,也就是图中蓝色框起来的部分,是特征提取网络。在V1版本中,其特征提取方法相对简单(例如使用了类似GoogLeNet的结构,但现在已较少使用)。后续版本会有重大改进。
对于V1,你不需要深入理解这个具体网络。你只需将其视为一个标准的卷积神经网络即可。它对输入图像进行了多次卷积操作,最终得到一个特征图。
例如,这里我们得到了一个 7×7×1024 的特征图。你可能会问,这个卷积网络重要吗?实际上,相对于更先进的V3版本,V1的这个网络结构比较简单,学习价值有限。我们将在讲解V3版本时详细讨论网络细节,因为V3的改进更有实际价值。在这里,你只需将其理解为一个完成特征提取的卷积模块即可。
全连接层与最终输出 🎯
卷积完成后,我们得到了一个 7×7×1024 的特征图。那么,我们最终要从这个特征图中得到什么呢?这需要我们关注全连接层的结果。
以下是全连接层的结构:
- 第一个全连接层将特征转换为 4096 个特征。
- 第二个全连接层输出 1470 个特征。
1470这个数字看起来有些奇怪。为了理解它,我们需要将其 reshape(重塑) 一下。将其重塑为 7×7×30。这个值在V1版本中至关重要。
我们需要理解 7×7 和 30 分别代表什么。
7×7 的含义:
它表示将最终的预测空间划分为一个 7×7 的网格。如下图所示,可以将其想象成一个7×7的棋盘格。每个格子负责预测其中心区域可能存在的物体。
30 的含义:
30表示每个格子需要预测的30个值。可以理解为:一个7×7的网格,每个格子带有30个值。
那么,这30个值具体是什么?让我们来拆解一下。之前我们提到,每个格子需要预测2个边界框(Bounding Box)。
以下是每个格子30个值的组成:
-
第一个边界框 (B1) 的预测值 (5个):
- 边界框的中心坐标 (x1, y1)
- 边界框的宽度和高度 (w1, h1)
- 边界框的置信度 (C1),表示该框内包含物体的概率以及预测的准确度。
- 注意: 这里的坐标值 (x, y, w, h) 不是原始图像的绝对像素坐标,而是相对于整个图像尺寸的归一化值(范围在0到1之间)。在代码实现中,还会涉及一些对数变换等操作,我们将在代码讲解部分详细说明。
-
第二个边界框 (B2) 的预测值 (5个):
- 与B1相同,包含 (x2, y2, w2, h2, C2)。
-
类别概率 (20个):
- 这对应于数据集的类别数。以PASCAL VOC数据集为例,它有20个类别(如:狗、汽车、自行车等)。
- 这20个值表示当前格子所预测的物体属于每个类别的条件概率(即在该格子包含物体的前提下,属于各个类别的概率)。

现在,我们汇总一下:5 (B1) + 5 (B2) + 20 (类别) = 30。
因此,最终网络输出的 7×7×30 张量的含义就是:
- 7×7: 空间网格划分。
- 30: 每个网格预测的2个边界框信息(共10个值)和20个类别的概率。
网络如何学习这种结构?🤖
之前讲解时,很多同学会问:老师,你说前5个值是B1,接下来5个是B2,最后20个是类别概率。我认可这个解释,但计算机凭什么按照这个顺序来理解和输出呢?
计算机之所以能这样做,是因为我们为其指定了损失函数(Loss Function)。在损失函数中,我们明确告诉网络什么样的输出是“好”的(即损失值小)。网络在训练过程中,通过反向传播不断调整内部参数,其目标就是最小化这个损失函数。
为了最小化损失,网络会逐渐“学会”如何组织它的输出,以符合我们的预期:即前10个值最好能准确描述边界框,后20个值最好能准确分类。整个神经网络的魅力之一就在于此:你只需要设计合适的目标(损失函数),它就能通过数据驱动的方式,自动学习到完成复杂任务的方法。
许多论文的核心思想正是如此:作者提出一种创新的输出表示和对应的损失函数,最终网络真的能训练出令人惊叹的结果。
总结 📝
本节课中,我们一起学习了YOLO V1的整体网络架构:
- 固定输入: 由于全连接层的存在,V1要求固定的 448×448×3 输入尺寸。
- 特征提取: 通过一个卷积神经网络(主干网络)将输入图像转换为 7×7×1024 的特征图。
- 全连接与重塑: 特征图经过全连接层后输出1470维向量,并重塑为关键的 7×7×30 张量。
- 输出解析: 7×7 代表空间网格,30 代表每个网格的预测值,包括:
- 2个边界框的坐标 (x, y, w, h) 和置信度 C。
- 20个类别的条件概率。
- 学习机制: 网络通过我们设计的损失函数来学习如何正确组织这30个输出值,从而完成物体定位和分类的任务。
理解这个 7×7×30 的输出结构,是掌握YOLO V1工作原理的基石。在后续课程中,我们将深入探讨损失函数的具体构成以及代码实现细节。




课程P6:2-Notebook与IDE环境配置教程 📚💻
在本节课中,我们将学习如何配置OpenCV以及两种主要的代码编写环境:Jupyter Notebook和集成开发环境(IDE)。掌握这些工具是顺利进行后续课程的基础。

概述
课程内容主要分为两部分:首先介绍Python工具包(以OpenCV为例)的安装方法;其次,讲解我们将使用的两种编程环境——Jupyter Notebook和IDE(如Eclipse或PyCharm)的配置与用途。
第一部分:Python工具包安装方法 🔧


安装Python工具包时,首选方法是使用pip install命令。
标准安装方法
以下是标准安装步骤:
- 打开命令行工具。
- 输入命令
pip install 工具包名称并执行。 - 如果安装成功,则配置完成。

备用安装方案

如果使用pip install命令安装失败(可能由于版本冲突或网络问题),可以采用备用方案。
备用方案是访问一个收录了大量Python工具包的非官方Windows平台网站。在该网站可以搜索并下载特定版本的.whl文件进行安装。
操作步骤如下:
- 在网站中使用
Ctrl+F搜索所需工具包(例如opencv)。 - 在搜索结果中,选择符合要求的文件。文件名通常包含以下信息:
- 工具包名称:例如
opencv-python。 - 版本号:例如
3.4.5。 - Python版本:
cp35、cp36、cp37分别对应Python 3.5、3.6、3.7。 - 操作系统:
win32或win_amd64对应32位或64位系统。
- 工具包名称:例如
- 下载对应的
.whl文件。 - 将下载的
.whl文件复制到Python安装目录下的Scripts文件夹中。 - 在命令行中进入
Scripts目录,执行安装命令:pip install 文件名.whl


版本选择建议:对于OpenCV,建议选择较为稳定的版本(如3.4.1),避免使用最新的4.x版本,以减少可能遇到的兼容性问题。如果课程或项目需要安装其他老版本工具包,可以通过搜索引擎查找对应的资源。


第二部分:代码编写环境配置 🖥️
我们将使用两种环境进行学习和开发:Jupyter Notebook用于理论学习和笔记整理,IDE用于项目实战和调试。


Jupyter Notebook环境


Anaconda已内置Jupyter Notebook。启动方法如下:
- 从开始菜单打开
Anaconda3,然后启动Jupyter Notebook。 - 启动后,会先弹出一个命令行窗口,随后在默认浏览器中打开Jupyter的Web界面(地址通常是
localhost:8888)。 - 如果浏览器没有自动打开,可以将命令行中显示的地址(如
http://localhost:8888/?token=...)手动复制到浏览器地址栏访问。


在Jupyter Notebook中,可以新建Python笔记本进行测试:
import cv2
print(cv2.__version__)
执行代码(按Shift+Enter)后,若能正常输出版本号,则环境配置成功。
Jupyter Notebook的优势

Jupyter Notebook将代码、文档和可视化结果结合在一个文件中,非常适合教学和笔记。
- 文档功能:支持使用Markdown语法编写文本、插入图片和公式,形成结构化的“教案”或博客。
- 交互式编程:代码以“单元格”(Cell)为单位执行,可以随时查看中间变量的值,便于分步理解和调试。
- 结果展示:代码的输出(包括图表、图像)直接显示在单元格下方,使学习过程连贯直观。


课程中所有理论部分的讲解和算法演示都将基于Jupyter Notebook进行,方便大家将知识点和代码实践紧密结合,复习时也无需在PPT和代码编辑器间频繁切换。


集成开发环境(IDE)


对于代码量较大的项目实战,我们推荐使用功能更全面的IDE。
IDE的必要性
- 项目管理:IDE更适合管理包含多个模块和文件的大型项目结构。
- 调试功能:IDE提供强大的调试(Debug)功能,可以设置断点、逐行执行代码、实时观察变量值的变化,这对于理解复杂代码的逻辑至关重要。
- 代码提示与导航:IDE通常具备智能代码补全、函数定义跳转等功能,能提升开发效率。
IDE的选择
课程项目实战将使用Eclipse进行演示,因为它支持多种语言(Java, C++, Python等),且作者使用习惯。
- 其他选择:大家完全可以根据自己的喜好选择其他IDE,例如在Python开发中非常流行的PyCharm。
- 核心要求:只要所选的IDE具备代码执行和调试(Debug) 功能即可,我们的代码在所有主流IDE中都是通用的。


环境配置地址汇总 📌
为了方便大家,以下是课程中提到的关键资源地址:
- Anaconda下载地址:
https://www.anaconda.com/products/individual - Python
.whl文件下载地址:https://www.lfd.uci.edu/~gohlke/pythonlibs/ - IDE选择:Eclipse (
https://www.eclipse.org)、PyCharm (https://www.jetbrains.com/pycharm/) 或其他具备Debug功能的IDE。
总结 🎯
本节课我们一起学习了OpenCV等Python工具包的两种安装方法,并配置了两种编程环境。
- 工具包安装:优先使用
pip install,失败时可从指定网站下载对应版本的.whl文件进行安装。 - Jupyter Notebook:用于课程理论学习和笔记记录,它集代码、文档、结果于一体,交互性强。
- 集成开发环境(IDE):用于项目实战开发,其强大的调试功能是理解和编写复杂代码的利器。

请确保按照建议的版本(如OpenCV 3.4.1)配置好环境,这样能最大程度避免后续课程中出现意外的版本兼容性问题。准备好这些工具后,我们就可以正式开始精彩的OpenCV学习之旅了。
课程P60:YOLOv1位置损失计算详解 🎯
在本节课中,我们将深入探讨YOLOv1算法中一个核心组成部分——位置损失的计算方法。我们将学习如何量化预测边界框与真实边界框之间的差异,并理解损失函数的设计思路。
网络输出与损失函数概述
上一节我们介绍了YOLOv1的网络结构和最终输出值的含义。本节中我们来看看如何定义损失函数来指导网络学习。

一个完整的算法通常需要关注两点:网络结构如何得到输出值,以及如何定义损失函数来衡量输出值与真实值之间的差距。解决了这两个问题,我们就掌握了算法的核心。
位置损失函数解析
我们的预测任务包括边界框的中心坐标(X, Y)和尺寸(宽W,高H)。预测值与真实值之间必然存在差异,我们希望这个差异越小越好。因此,需要定义一个损失函数来最小化这些位置参数的误差。
以下是位置损失函数的核心公式及其组成部分的详细解释:
公式解读:
S^2: 表示网格总数。YOLOv1将图像划分为S x S个网格(原文中为7x7),因此需要对每个网格进行计算。B: 表示每个网格预测的边界框数量。在YOLOv1中,B=2,即每个网格预测两个不同尺寸的候选框。1_{ij}^{obj}: 这是一个指示函数。对于每个网格i和其中的边界框j,只有当该边界框对某个真实物体的预测“负责”(即与该真实物体的IOU最大)时,其值才为1,否则为0。这确保了只有最匹配的预测框才参与位置损失的计算。(x_i - \hat{x}_i)^2和(y_i - \hat{y}_i)^2: 计算预测的中心坐标(x, y)与真实中心坐标(\hat{x}, \hat{y})之间的平方误差。\lambda_{\text{coord}}: 这是一个权重系数,用于调整位置损失在总损失中的重要性。通常在论文中会给出一个具体值(如5),以强调定位准确性的重要性。
宽高损失的特殊处理:开根号的原因
观察公式可以发现,对于宽(W)和高(H)的误差计算,我们对其进行了开平方根处理:(\sqrt{w} - \sqrt{\hat{w}})^2。这与直接计算坐标误差不同。
为什么要这样做?
这主要是为了解决物体尺度不同带来的问题。考虑以下场景:
- 大物体: 假设一个物体的宽为100像素,预测误差为10像素。这个误差相对于物体本身尺寸(100)来说,比例较小。
- 小物体: 假设另一个物体的宽仅为10像素,同样有10像素的误差。这个误差相对于物体尺寸(10)来说,比例就非常大,是致命的。
如果直接使用 (w - \hat{w})^2,损失函数对大小物体的绝对误差敏感度相同。但我们更希望网络能更精细地处理小物体的定位,因为小物体本身容错率低,几个像素的偏差就可能导致完全漏检。
开根号的作用:
函数 y = \sqrt{x} 的特性是,当 x 较小时,函数值 y 的变化率(导数)较大;当 x 较大时,变化率较小。将宽高开根号后再计算误差,相当于:
- 对于较小的宽高值(小物体),给予其误差变化更高的权重,使网络对其更敏感。
- 对于较大的宽高值(大物体),给予其误差变化相对较低的权重。
这是一种让损失函数对不同尺度物体定位误差具有不同敏感度的工程技巧。虽然在YOLOv1中这只是一个初步的改进,但它指明了优化方向,后续版本(如YOLOv2/v3)使用了更完善的尺度处理方法。

本节课中我们一起学习了:
- YOLOv1损失函数的核心组成部分之一——位置损失。
- 位置损失函数的具体数学形式,包括对每个网格(
S^2)、每个预测框(B)的计算,以及使用指示函数(1_{ij}^{obj})筛选负责预测的边界框。 - 对边界框中心坐标(X, Y)直接计算平方误差。
- 对边界框宽高(W, H)采用先开根号再计算平方误差的特殊处理,其目的是让网络在训练时更关注小物体的定位精度。这是YOLOv1针对多尺度物体检测问题的一个重要设计考量。
YOLO系列课程 P61:置信度误差与优缺点分析 📊
在本节课中,我们将学习YOLO v1目标检测算法中置信度误差的计算方法,并分析其整体架构的优缺点。我们将深入理解损失函数的构成,特别是如何处理前景与背景的置信度预测,并探讨YOLO v1的局限性。
置信度误差计算 🧮
上一节我们介绍了边界框位置误差的计算。本节中,我们来看看如何计算置信度误差。
在图像中,有些区域是前景(有物体),有些区域是背景。通常,背景区域远多于前景区域。因此,在计算置信度时,需要分类讨论:预测为前景和预测为背景的情况。
- 对于背景,置信度的真实值应为
0。 - 对于前景,置信度的真实值应为
1。
如何确定一个候选框的置信度真实值?
以下是确定置信度真实值的步骤:
- 计算候选框与所有真实框的交并比(IOU)。
- 如果候选框与某个真实框的IOU大于阈值(例如0.5),则认为该候选框负责预测前景。
- 此时,置信度的真实值并非简单地设为
1,而是设为该最大IOU值(例如0.7)。我们希望模型预测的置信度接近这个IOU值。 - 如果一个真实框有多个候选框与之重叠(IOU>0.5),则只选择IOU最大的那个候选框负责预测,其余候选框视为背景。
- 对于所有IOU小于阈值(或与任何真实框都不重叠)的候选框,其置信度真实值设为
0,代表背景。
置信度损失函数 📉
在YOLO论文中,置信度损失分为两部分:含有物体的部分和不含物体的部分。
含有物体的置信度损失:
模型预测的置信度应接近真实IOU值(当IOU>0.5时)。其损失计算如下:
λ_coord * Σ_i^S² Σ_j^B 1_{ij}^{obj} (C_i - Ĉ_i)²
其中,1_{ij}^{obj} 是指示函数,当第 i 个网格的第 j 个预测框负责预测物体时为1,否则为0。C_i 是预测值,Ĉ_i 是真实IOU值。
不含物体的置信度损失:
对于背景,我们希望预测的置信度接近 0。其损失计算如下:
λ_noobj * Σ_i^S² Σ_j^B 1_{ij}^{noobj} (C_i - Ĉ_i)²
其中,1_{ij}^{noobj} 是背景的指示函数。
这里引入了权重参数 λ_coord(例如5)和 λ_noobj(例如0.5)。这是因为背景样本数量远多于前景样本,存在样本不均衡问题。增加前景损失的权重,降低背景损失的权重,可以防止模型被大量的背景样本主导,从而更好地学习识别前景物体。
分类误差与总损失函数 ➕
接下来,我们看最后一个误差项:分类误差。
分类误差处理的是物体类别的识别问题(例如20分类或80分类)。这本质上是一个多分类问题,通常使用交叉熵损失函数来计算预测类别概率与真实类别之间的差异。
分类损失:
Σ_i^S² 1_i^{obj} Σ_c∈classes (p_i(c) - p̂_i(c))²
其中,p_i(c) 是预测的类别概率,p̂_i(c) 是真实的类别标签(one-hot编码)。
总损失函数


YOLO v1的总损失函数是上述所有误差项的加权和:
Loss = λ_coord * 位置误差 + 置信度误差(含物体) + λ_noobj * 置信度误差(不含物体) + 分类误差
这个框架在YOLO后续版本中也基本保持不变,仅在细节上有所优化。

YOLO v1 网络架构与流程回顾 🔄


现在,让我们回顾一下YOLO v1的整体流程。

网络架构非常简单,最终输出一个 7x7x30 的张量。我们需要理解 30 的含义,并在构建损失函数时描述位置、置信度和分类这三类问题的损失。将这两部分结合起来,就能完成模型的训练。
在预测(测试)阶段,经常会遇到同一个物体被多个重叠的边界框检测到的情况。此时,需要使用非极大值抑制(NMS) 来筛选最终结果。
NMS的基本步骤:
- 将所有预测框按置信度从高到低排序。
- 选择置信度最高的框,将其加入最终输出列表。
- 计算该框与剩余所有框的IOU。
- 移除IOU超过设定阈值(如0.5)的所有框(因为它们很可能检测的是同一个物体)。
- 重复步骤2-4,直到处理完所有框。
NMS确保了对于每个物体,我们只保留一个最可信的预测框。


YOLO v1 优缺点分析 ⚖️
最后,我们来分析YOLO v1的优点与局限性。
优点 👍
- 速度快:单次前向传播即可完成检测,流程简单,速度远超当时的双阶段检测器(如R-CNN系列)。
- 全局推理:对整张图像进行卷积操作,能更好地利用上下文信息,减少背景误检。
缺点 👎
- 空间限制:每个网格(
7x7)只能预测一个主要物体类别。当多个物体中心落入同一个网格时(尤其是小物体或重叠物体),模型难以检测。 - 先验框单一:每个网格只预设两个边界框(
B=2),且形状固定。这难以适应数据集中所有物体多变的尺度和长宽比,特别是对于非常规形状或小物体的检测效果不佳。 - 损失函数问题:均方误差损失对大小边界框的误差同等对待,但实际上大框的小偏差影响不如小框的同等级偏差敏感。此外,样本不均衡(背景远多于前景)虽有权重调整,但仍是一个挑战。
- 多标签问题:使用Softmax进行单标签分类,无法处理一个物体属于多个类别(例如“狗”和“哈士奇”)的情况。

总结 📝

本节课中,我们一起学习了YOLO v1目标检测算法的核心部分。

我们详细剖析了其损失函数,特别是置信度误差如何区分前景与背景,并通过权重参数解决样本不均衡问题。我们还回顾了网络输出 7x7x30 的含义以及训练、预测(包括NMS)的整体流程。

最后,我们系统地分析了YOLO v1速度快、结构简单的优点,以及其难以检测重叠与小物体、先验框设计单一等主要缺点。理解YOLO v1的这些核心思想和局限性,是学习后续YOLO v2、v3等改进版本的重要基础。
课程P62:YOLO V3版本改进概述 🚀
在本节课中,我们将学习YOLO V3版本的核心改进内容。与V2版本包含众多细节优化不同,V3版本主要围绕一个核心目标:升级整体网络架构以提升特征提取能力。其出发点是设计一个能更好地检测大、中、小各类目标的网络结构,使检测结果更进一步。
上一节我们介绍了YOLO V2的改进,本节中我们来看看YOLO V3做了哪些关键升级。
性能表现与背景
首先,我们来看一张有趣的性能对比图。这张图来自原论文,其绘制方式颇具深意。

X轴表示预测一张图像所需的时间(速度),Y轴表示mAP值(精度)。值得注意的是,图的原点并非(0,0),而是(50,0)。作者故意将YOLO V3的数据点绘制在了第二象限。
这种绘制方式意在强调,YOLO V3在速度和精度上,都远超当年的其他代表性算法,仿佛“跑到了另一个象限”。这直观地展示了YOLO V3的强大性能。
此外,关于YOLO系列的后续发展有一个小插曲。在2020年初,YOLO的作者宣布将不再继续更新该系列,并退出计算机视觉研究领域。原因是他不希望自己的研究成果被过度应用于军事打击等用途。虽然官方系列可能止步于V3,但其高实用价值促使社区在其基础上进行了大量延伸和改进。
YOLO V3的实用性极高,许多企业级项目,如实时检测和目标追踪任务,都直接参考或套用其框架。对于工程师而言,掌握YOLO意味着可以直接利用现成的论文、源码和预训练模型来解决实际问题。
核心改进点
进入正式内容,我们来详细看看YOLO V3的具体改进。其最大的改进集中于网络结构本身。
YOLO V1和V2曾因检测精度(尤其是对小目标)而受到质疑。V3的主要出发点不再是单纯追求速度,而是显著提升检测效果,特别是针对小目标。既然YOLO本质上是单阶段(one-stage)检测器,基于一个大CNN网络,那么提升效果就只能从优化这个主干特征提取网络入手。
以下是YOLO V3的几个核心改进方向:

1. 主干网络升级:Darknet-53
YOLO V3引入了一个全新的主干特征提取网络,称为Darknet-53。

Darknet-53借鉴了ResNet的残差思想,通过堆叠残差块来构建更深的网络,从而提取更丰富的特征。其设计在深度与效率之间取得了良好平衡。
2. 多尺度预测与更丰富的先验框
为了提升对不同尺寸目标的检测能力,YOLO V3采用了多尺度预测机制。
- 多尺度特征图:网络会在三个不同尺度的特征图上进行预测(例如13x13, 26x26, 52x52),分别负责检测大、中、小目标。
- 先验框(Anchor Boxes):YOLO V2通过聚类得到了5种先验框。V3在此基础上,针对上述三个预测尺度,通过聚类得到了9种不同尺寸的先验框(例如,每个尺度分配3种尺寸)。这为模型提供了更丰富的初始框选择。
3. 分类器改进:逻辑回归替代Softmax
在目标分类环节,YOLO V3用多个独立的逻辑回归(Logistic)分类器替代了传统的Softmax分类器。
- Softmax的局限:Softmax输出一个概率分布,强制模型为每个目标只预测一个最可能的类别。这适用于互斥的单标签分类任务。
- 逻辑回归的优势:YOLO V3为每个类别独立使用一个二分类器(逻辑回归),判断目标“是”或“不是”该类别。这种方式支持多标签分类(Multi-label Classification)。例如,一张图片中可能同时包含“人”和“自行车”,使用独立的逻辑回归可以预测出这两个标签,而Softmax只能输出其中一个。
其核心公式可以简化为对每个类别 c 进行独立的二分类预测:
P(class=c) = σ(t_c)
其中,σ 是Sigmoid函数,t_c 是网络对该类别的输出值。

本节课中我们一起学习了YOLO V3版本的核心改进。主要包括:1)引入了更强大的主干网络Darknet-53以提升特征提取能力;2)采用多尺度预测和更多先验框来增强对不同尺寸目标(尤其是小目标)的检测;3)将分类器从Softmax改为多个逻辑回归,以支持多标签分类任务。这些改进使得YOLO V3在保持实时性的同时,大幅提升了检测精度,成为当时极具实用价值的检测框架。
课程P63:多尺度方法改进与特征融合 🎯
在本节课中,我们将学习YOLOv3中一个核心的改进思想:多尺度检测与特征融合。我们将理解为何需要为不同大小的物体设计专门的检测层,以及如何通过特征融合来提升检测性能。
多尺度检测的核心思想
上一节我们介绍了YOLO的基本检测流程。本节中我们来看看YOLOv3如何改进以更好地检测不同大小的物体。
物体在图像中是有大小之分的。在YOLOv2中,我们曾尝试将最后一层的特征图与前一层特征图融合,形成一个大的特征向量。但这种方法存在潜在问题。
举个例子,假设有两位专家,一位精通电器维修,另一位精通水电焊接。如果将他们强行融合成一个团队去执行综合任务,可能反而会淹没各自最擅长的技能。电器专家的专长在水电任务中可能无法完全发挥。
因此,YOLOv3的设计理念是“术业有专攻”。既然我们的目标包含大、中、小三种不同尺寸的物体,那就让网络的不同部分专门负责预测特定尺寸的目标。
三种特征图与感受野
以下是YOLOv3中三种不同尺度的特征图及其分工:
- 13×13的特征图:感受野最大,专门负责预测大目标。
- 26×26的特征图:感受野中等,专门负责预测中目标。
- 52×52的特征图:感受野较小,专门负责预测小目标。
网络输入图像后,经过一系列卷积层,会自然生成由深到浅、尺寸由小到大的特征图。YOLOv3的做法是让每种尺寸的特征图“各司其职”,完成自己最擅长的检测任务。

先验框(Anchor Box)的设置

上一节我们了解了特征图的分工,本节中我们来看看每种特征图上如何生成预测框。
在YOLOv3中,为了平衡检测效果与速度,每一种尺度的特征图上都预设了3个不同大小比例的先验框(Anchor Box)。
因此,对于52×52、26×26、13×13这三种特征图,每种都会产生3个预测框。最终,整个模型一共使用了 3种尺度 × 3个先验框 = 9个 先验框。这使得模型能够更灵活地适应不同形状和尺寸的物体。
特征融合的必要性


仅仅从网络的不同深度提取52、26、13的特征图直接进行预测就足够了吗?答案是否定的。我们需要进行特征融合。
我们可以用一个比喻来理解:
- 13×13特征图 好比一位百岁老人,阅历丰富,眼界宽广,能看清事物本质,适合预测大目标。
- 26×26特征图 好比一位中年人,人生走到一半,对前路可能感到迷茫。它在预测中目标时,需要向“见过世面”的13×13特征图借鉴思想。
- 52×52特征图 好比一位年轻人,眼界尚浅。它在预测小目标时,需要向“经验更多”的26×26特征图取经,以避开可能的“坑”。
因此,YOLOv3的网络结构并不是简单地提取三层特征。它通过特征融合,将深层特征图的语义信息(知道“是什么”)与浅层特征图的位置细节信息(知道“在哪里”)结合起来。
具体来说:
- 26×26的特征图会与更深层(例如13×13上采样后)的特征图进行融合。
- 52×52的特征图会与中层(例如26×26上采样后)的特征图进行融合。
这个过程通常通过上采样(Upsampling)和拼接(Concatenation) 操作来实现。
# 特征融合的简化示意代码(非完整YOLOv3结构)
# 假设 deep_feat 是13x13的深层特征, mid_feat 是26x26的中层特征
upsampled_deep = upsample(deep_feat, scale_factor=2) # 上采样到26x26
fused_mid_feat = concatenate([mid_feat, upsampled_deep]) # 通道维度拼接
# fused_mid_feat 将同时包含细节和语义信息,用于预测中目标
总结
本节课中我们一起学习了YOLOv3的多尺度检测与特征融合机制。核心要点包括:
- 分而治之:使用13×13、26×26、52×52三种不同尺度的特征图,分别负责检测大、中、小目标。
- 先验框设计:每种尺度特征图配备3个先验框,共9个,以提升模型灵活性。
- 特征融合:通过上采样与拼接,将深层特征的强语义信息与浅层特征的精细位置信息相结合,使每一层的预测都更加准确。

这种设计显著提升了YOLOv3对不同尺寸物体的检测能力,尤其是在复杂场景中小目标的检测效果。
课程P64:经典变换方法对比分析 🎯
在本节课中,我们将学习目标检测中几种经典的尺度变换方法,并分析它们在YOLO算法中的适用性。我们将重点理解不同方法的原理、优缺点,并最终明确YOLOv3所采用的核心策略。
概述
目标检测需要处理不同尺度的物体。为了同时检测大、中、小目标,我们需要设计有效的特征提取与融合策略。本节将对比分析图像金字塔、单尺度预测以及特征金字塔网络(FPN)等方法的思路,并解释YOLOv3为何选择特定的路径。
方法一:图像金字塔 🏔️
第一种做法是构建图像金字塔。其核心思想是通过直接调整输入图像的分辨率来获得不同尺度的特征图。
具体做法是:将原始图像数据通过resize操作生成多个不同分辨率的版本(例如,原图、缩小一半、缩小四分之一)。将这些不同尺度的图像分别输入到同一个网络中,自然就能得到对应尺度的特征图输出。
以下是该方法的简要步骤:
- 对原始图像进行多次下采样,生成一个图像金字塔。
- 将金字塔中的每一层图像分别输入检测网络。
- 网络为每一层输入输出对应尺度的检测结果。

然而,这种方法在YOLO算法中并不合适。YOLO的核心优势之一是速度。图像金字塔要求对同一张图片进行多次前向传播(例如三次),这会使处理速度大打折扣。虽然该方法在其他不特别追求速度的算法中可行,但违背了YOLO的设计初衷。
方法二:单尺度预测 🔍
第二种方法是单尺度预测,这也是YOLOv1采用的方式。
其过程非常简单:无论输入图像内容如何,只通过一次卷积神经网络(CNN)前向传播,最终在网络的末端输出单一的预测结果(例如一个7x7的网格)。这种方法速度最快,但难以有效检测尺度变化极大的物体,尤其是小目标。
方法三:特征金字塔网络(FPN)与融合 🧩
上一节我们介绍了两种较为简单的方法,本节中我们来看看YOLOv3所采用的核心思想——特征金字塔网络(FPN)与跨尺度特征融合。
下图展示了三种不同的特征利用策略:

- 左图(分别预测):网络在不同深度自然产生了13x13、26x26、52x52等不同尺度的特征图。这种方法让各个尺度的特征“各自为战”,只利用自身信息进行预测。问题在于,浅层的特征图(如52x52)感受野小,适合看局部细节(小目标),但缺乏全局语义信息;深层的特征图(如13x13)感受野大,语义信息强,适合看大目标,但空间细节丢失严重。让它们“自己玩自己的”,效果可能不理想。
- 中图(图像金字塔):即我们第一部分分析过的方法,因速度慢而不被YOLO采用。
- 右图(特征融合):这是YOLOv3的核心。它认为,深层特征(如13x13)拥有强大的语义信息(“眼界广”),应该用来帮助提升浅层特征(如26x26、52x52)的检测能力。
那么,不同尺度的特征图如何融合呢?关键在于上采样(Upsampling)。
以预测中目标(26x26尺度)为例,融合过程如下:
- 将深层、语义信息丰富的13x13特征图进行上采样(例如使用最近邻插值或转置卷积),使其尺寸变为26x26。
- 将这个上采样后的特征图,与网络中间层原生的26x26特征图进行融合(通常是通道维度上的拼接(Concatenation)或相加(Addition))。
- 融合后的新特征图同时具备了深层的强语义信息和浅层的精细空间信息,用于预测中尺度目标。
同理,对于预测小目标(52x52尺度):
- 将用于预测中目标的、已经融合过的特征图(26x26)再进行一次上采样,得到52x52的特征图。
- 将其与网络最浅层原生的52x52特征图进行融合。
- 用融合后的特征预测小目标。
这个过程如下图所示,清晰地展示了特征自上而下传递并融合的路径:


这种设计的优势在于,它只用了一次前向传播,就通过巧妙的特征融合,让不同尺度的输出都同时具备了高分辨率的细节和丰富的语义信息,从而实现了速度与精度的良好平衡。
总结

本节课中我们一起学习了目标检测中三种经典的尺度变换方法。
- 图像金字塔:通过多尺度输入实现多尺度检测,但速度慢,不适合YOLO。
- 单尺度预测:YOLOv1的做法,速度快但多尺度检测能力弱。
- 特征金字塔网络(FPN):YOLOv3采用的核心方法。它通过上采样将深层特征的语义信息与浅层特征的空间细节进行融合,公式化地看,融合过程可表示为:
Fused_Feature[l] = Concat( Upsample( Feature[l+1] ), Feature[l] )
其中l代表特征图层级。这种方法在单次前向传播中高效地实现了对不同尺度目标的鲁棒检测。

课程P65:4-残差连接方法解读 🧠

在本节课中,我们将要学习深度学习中一个至关重要的概念——残差连接。我们将从它被提出的背景开始,解释其核心思想,并说明它为何能成为现代主流神经网络架构的基石。
背景:深度网络的困境
上一节我们介绍了特征融合与尺度变换,本节中我们来看看网络深度带来的挑战。
在2014年,VGG网络通过堆叠卷积层取得了巨大成功,但人们发现,当网络层数继续增加时(例如超过19层),模型的性能反而会下降。无论是训练误差还是测试误差,更深的网络表现得更差。这引发了深度学习的瓶颈问题:网络越深,学习效果越“回旋”。
残差连接的提出 🚀
为了解决上述问题,ResNet(深度残差网络)在2016年横空出世,它提出的残差连接思想几乎挽救了深度学习的“深度”探索。
其核心思想是:让网络至少不比原来的浅层网络差。它通过一种巧妙的结构,确保增加的网络层不会损害模型的性能,甚至可能带来提升。
残差连接的核心思想
以下是残差连接的基本工作原理:
假设我们有一个输入 X,它经过了一些网络层(例如第19层)。现在,我们想在此基础上增加新的层(例如第20层和第21层)。传统做法是让 X 连续通过这些新层,得到输出 F(X)。
ResNet的做法不同。它构建了两条路径:
- 主路径:输入
X经过新的卷积层等操作,得到变换后的输出F(X)。 - 捷径(Shortcut)连接:输入
X本身通过一条“捷径”被原封不动地(或经过简单的线性变换如1x1卷积)传递过来。
最后,将这两条路径的结果相加:
输出 = F(X) + X
这个加法操作就是残差连接。
残差连接为何有效?💡
这种结构赋予了网络强大的自选择能力。
- 如果新增的层(
F(X))学习到了有用的特征,那么F(X)将是一个有效的补充,最终输出会优于原始的X。 - 如果新增的层学习效果很差,甚至产生了有害的变换,网络可以通过训练将
F(X)的权重参数学习为接近零。此时,输出≈ 0 + X = X,即网络自动“跳过”了这些无效层,退化回原始输入的状态。
因此,残差连接保证了网络的性能下限:增加层数后,效果最差也不过是和浅层网络一样(即 F(X)=0),而只要新增的层有一点点贡献,整体效果就会提升。这就像团队合作,新成员如果表现好就加分,表现不好也不会拖累原有团队的平均分。
残差块(Residual Block)
在实际应用中,上述结构被封装成一个可重复堆叠的基本模块,称为残差块。
一个基础的残差块可以用以下伪代码描述:
# 假设 identity 是捷径连接,主路是两层卷积
def residual_block(x):
identity = x # 保存输入
out = conv1(x)
out = conv2(out)
out = out + identity # 残差连接:相加操作
out = activation(out) # 激活函数
return out
你可以将数十甚至数百个这样的残差块堆叠起来,构建极深的网络(如ResNet-50, ResNet-101)。即使其中只有部分块是有效的,整体网络也能从中受益。
总结
本节课中我们一起学习了残差连接这一深度学习中的核心思想。
- 背景:我们了解到单纯增加网络深度会导致性能下降。
- 解决方案:ResNet提出了残差连接,通过将输入与经过变换的输出相加(
输出 = F(X) + X),构建了“捷径”。 - 核心优势:这种结构确保了深度网络至少不差于其对应的浅层网络,并让网络能自动选择是否利用新增的层,从而使得训练成百上千层的超深网络成为可能。
- 广泛应用:正如YOLOv3等众多先进算法所示,残差连接已成为现代神经网络设计中不可或缺的标准组件。

理解残差连接,是理解当今主流深度学习模型架构的关键一步。
课程P66:YOLOv3整体网络模型架构分析 🧠
在本节课中,我们将要学习YOLOv3的整体网络模型架构。我们将深入解析其核心设计思想、网络结构特点以及多尺度特征融合的策略,帮助你理解这个经典目标检测模型的工作原理。
概述
YOLOv3的网络结构被称为Darknet-53。你可以将其理解为与ResNet类似的残差网络,其核心设计理念是高效和简洁。
网络结构特点
上一节我们介绍了YOLOv3的基本概念,本节中我们来看看其网络结构的主要特点。
最大的特点是:该网络中没有池化层和全连接层。在之前的YOLO版本中,我们已经解释过全连接层在实际应用中效率不高,因此被移除。而池化层会对特征图进行压缩,可能导致信息丢失,影响效果。因此,YOLOv3决定完全舍弃池化层。
那么,一个随之而来的问题是:如果全部使用卷积,如何将特征图的尺寸缩小为原来的1/2(如下采样)?
这里我们来看卷积操作中的一个关键参数:步长(stride)。
- 当卷积的
stride=1时,特征图大小保持不变。 - 当我们将卷积的
stride设置为2时,卷积核每次滑动两个单元格,这样就能让特征图的高(H)和宽(W)都变为原来的1/2。
因此,在YOLOv3中,凡是需要进行下采样的地方,就使用 stride=2 的卷积层;不需要改变尺寸的地方,则使用 stride=1 的卷积层。这是一种趋势,因为卷积操作省时省力、速度快且效果好,所以能简化的结构都尽量简化,全部使用卷积来完成。
多尺度预测与特征融合
了解了网络的基础构成后,我们进一步探讨YOLOv3实现多尺度目标检测的核心机制。
在网络末端,我们得到了三种不同尺度的特征图:13×13、26×26和52×52。其中,13×13的深层特征图适合预测大目标。
其具体操作流程如下:
- 首先,网络会生成一个13×13×1024的特征图(红色部分)。
- 接着,将该特征图进行上采样操作,并经过卷积,变换为26×26×256的特征图。
- 然后,将这个上采样得到的特征图与网络中较早层生成的26×26×512的特征图进行拼接(融合)。拼接后的特征图尺寸为26×26×(256+512)= 26×26×768。
- 最后,对这个融合后的特征进行进一步的卷积以提取信息,并完成最终的预测。
对于52×52的小目标预测分支,原理是相同的:将26×26的特征图上采样后,与更早层的52×52特征图进行拼接融合,再经过卷积得到最终的输出。

以下是整个特征融合过程的简要总结:
- 13×13特征图:用于预测大目标。
- 26×26特征图:由13×13特征图上采样后,与中层特征拼接融合得到,用于预测中目标。
- 52×52特征图:由26×26特征图上采样后,与浅层特征拼接融合得到,用于预测小目标。
从整体思想来看,其核心并不复杂。它基于残差网络得到了三种不同尺度的输出特征图。为了使特征图包含更丰富的信息,它不仅利用了当前层的信息,还通过上采样融合了深层特征的信息,从而使检测效果更加完善。
总结


本节课中我们一起学习了YOLOv3的网络架构。我们了解到其主干网络Darknet-53完全由卷积层构成,利用stride=2的卷积进行下采样。同时,我们重点分析了其多尺度预测与特征融合机制,即通过上采样和拼接操作,将深层语义信息与浅层细节信息相结合,从而实现对不同大小目标的有效检测。
课程P67:先验框设计改进 🎯
在本节课中,我们将学习YOLO V3模型在目标检测任务中,对先验框(Anchor Boxes)设计所做的关键改进。我们将详细解释其设计思路、具体实现以及与之前版本(如YOLO V2)的区别。
网络输出结构解析
上一节我们介绍了YOLO V3的网络架构,本节中我们来看看其具体的输出结构。网络通过不同步长的卷积层进行下采样,最终会得到多个尺度的特征图,例如32倍、16倍和8倍下采样的结果。
最终预测结果的典型维度是 13×13×3×85。这个值可能因数据集而异,但其结构是固定的。我们来分解这个维度的含义:
以下是每个维度的具体解释:
- 13×13:这是特征图的网格大小。它类似于YOLO V1中的7×7网格,或YOLO V2中的13×13网格。在YOLO V3中,我们会有三种不同尺度的网格:13×13、26×26和52×52。
- ×3:这代表每个网格单元(Grid Cell)会预测3个不同尺寸的先验框。这对应了图中标注的box1、box2、box3。
- ×85:这代表每个预测框需要输出的信息量。它由三部分组成:
- 4:边界框的偏移量(
x, y, w, h)。 - 1:置信度(
confidence),表示该框内包含物体的可能性。 - 80:在COCO等数据集上,对应80个类别的分类概率(通过Softmax转换得到)。
- 4:边界框的偏移量(
因此,当看到类似 13×13×3×85 的输出时,应理解其每个数字的含义。不同数据集的类别数(如80)可能变化,但网格、先验框数量及4+1的基本结构是固定的。YOLO V3的网络结构核心与V2相似,主要是在细节上进行了优化。

先验框的聚类与分配策略

理解了输出结构后,我们聚焦于核心改进之一:先验框的设计。在YOLO V2中,我们使用K-means聚类在训练集上得到5个先验框尺寸。YOLO V3延续了聚类方法,但做了更科学的分配。
YOLO V3通过聚类得到了9种不同尺寸的先验框。关键改进在于,它没有让每个尺度的特征图都预测全部9种框,而是根据特征图的感受野大小,将9种框分配给了三个不同的输出层,实现了“术业有专攻”。
以下是具体的分配逻辑:
- 13×13 特征图:分配3个较大尺寸的先验框。因为该层感受野最大,适合检测图像中的大目标。
- 26×26 特征图:分配3个中等尺寸的先验框。该层感受野适中,适合检测中等尺寸的目标。
- 52×52 特征图:分配3个较小尺寸的先验框。该层感受野最小,保留了更多细节信息,因此擅长检测小目标。
这与YOLO V2有显著不同。V2中每个位置预测相同的一组先验框,而V3则根据特征图的能力进行了针对性分配,使不同层专注于检测不同尺度的物体。

改进效果可视化
为了更直观地理解这种分配策略的效果,我们来看一张示意图。

图中展示了相同输入图像在不同尺度特征图上的先验框情况。其中,黄色框代表真实标签(Ground Truth),蓝色框代表该层分配的候选先验框。

可以清晰地看到:
- 在 13×13 的特征图上,蓝色的先验框尺寸最大。
- 在 26×26 的特征图上,蓝色的先验框尺寸中等。
- 在 52×52 的特征图上,蓝色的先验框尺寸最小。

这种设计使得每个尺度的特征图都使用与其感受野相匹配的先验框进行初始预测,网络只需在此基础上进行微调,从而提升了多尺度目标检测的精度和效率。
总结
本节课中我们一起学习了YOLO V3在先验框设计上的重要改进。核心在于将聚类得到的9种先验框,根据尺寸大小科学地分配给三个不同尺度的输出特征层(13×13, 26×26, 52×52),让大感受野的层检测大目标,小感受野的层检测小目标。这种“分而治之”的策略相比YOLO V2的均匀分配更为合理,是提升模型多尺度检测性能的关键之一。
YOLOv3 课程 P68:Softmax层改进 🧠
在本节课中,我们将学习 YOLOv3 模型在输出层的一个重要改进。上一节我们介绍了 YOLOv3 的整体网络架构,本节中我们来看看其如何通过改进 Softmax 层来处理更复杂的多标签分类任务。
概述
YOLOv3 在输出预测时,面临一个实际问题:一个物体可能同时属于多个类别。例如,一张图片中的对象可能既是“狗”,又是“哺乳动物”,还是“哈士奇”。传统的 Softmax 层和交叉熵损失函数是为单标签分类设计的,无法直接处理这种情况。因此,YOLOv3 对此进行了关键性改进。
传统 Softmax 与交叉熵的局限
在标准的单标签分类任务中,Softmax 层输出一个概率分布,交叉熵损失函数只关注正确类别的预测概率。
其损失函数公式为:
Loss = -log(P(correct_class))
其中,P(correct_class) 是模型预测物体属于真实类别的概率。
- 当预测正确概率很高(接近1)时,
-log(1) = 0,损失值接近零。 - 当预测正确概率很低(接近0)时,
-log(0)趋近于无穷大,损失值很大。
这种方法只计算一个“正确”标签的损失,忽略了物体可能具有的其它有效标签。
YOLOv3 的改进方案
为了处理多标签任务,YOLOv3 放弃了使用单一的 Softmax 层进行分类。取而代之的是,它为每一个类别都独立地执行一次二分类。
以下是具体的实现思路:


- 独立二分类:网络不再输出一个 N 类的概率分布,而是为 N 个类别中的每一个都输出一个独立的概率值。这个概率值表示对象属于该类别的置信度。
- 设定阈值:为这些概率值设定一个阈值(例如 0.5 或 0.7)。
- 收集标签:将所有概率值超过该阈值的类别,都作为该对象的预测标签。
例如,模型对一个对象进行预测,可能得到如下结果:
- 是猫的概率:0.9
- 是狗的概率:0.05
- 是哺乳动物的概率:0.85
- 是汽车的概率:0.01

如果设定阈值为 0.7,那么该对象的最终预测标签就是“猫”和“哺乳动物”。

核心网络架构回顾

这个改进是集成在 YOLOv3 的“大内 53”核心网络架构之中的。整个架构的基础是残差块,它有效地解决了深层网络中的梯度消失问题,使网络可以构建得非常深。同时,通过上采样操作融合不同尺度的特征图,使得模型既能检测大物体,也能检测小物体。
总结
本节课中我们一起学习了 YOLOv3 模型在输出层的核心改进。主要内容总结如下:
- 问题识别:传统 Softmax 层和交叉熵损失函数无法处理一个对象对应多个标签的现实任务。
- 解决方案:YOLOv3 采用为每个类别独立进行二分类的策略,替代了全局的 Softmax。
- 预测流程:对每个类别预测一个置信度分数,并通过设定阈值来筛选出所有符合条件的标签,从而支持多标签预测。
- 架构整合:此改进被无缝整合进基于残差块和特征金字塔的 YOLOv3 主干网络中。

正是这些扎实的改进,使得 YOLOv3 在其发布时取得了“逆天”的性能,并在很长一段时间内成为目标检测领域最经典、最常用的模型之一。尽管已有更先进的模型出现,但 YOLOv3 因其出色的平衡性(速度、精度、易用性)至今仍被广泛使用。
🚀 YOLOv3实战课程:P69 - 数据与环境配置
在本节课中,我们将从实战角度出发,学习YOLOv3版本的整体网络架构与代码实现细节。我们的核心任务是理解每一行代码的含义,并掌握如何获得最终的预测结果。在开始之前,首先需要配置好开发环境与准备数据集。

🛠️ 环境配置

上一节我们介绍了YOLO各版本的改进,本节中我们来看看运行代码所需的环境配置。以下是两个必须准备的核心组件:
1. 集成开发环境 (IDE)
你需要一个Python编程工具,用于编写代码和进入Debug模式。我使用的是Eclipse,因为它支持多种语言。你也可以选择PyCharm或其他你喜欢的IDE。如果之前没有使用过IDE,建议跟随教程配置Eclipse,以便后续能同步进入Debug模式逐行追踪代码执行流程。

2. 深度学习框架
我们将使用当下最流行的PyTorch框架。如果你对PyTorch不熟悉,本课程会介绍其基本用法。你也可以参考专门的PyTorch实战课程进行深入学习。你需要自行安装好PyTorch框架。
除了以上两点,NumPy、Pandas等常用Python包也是必备的。
📁 数据准备
环境配置好后,接下来我们需要准备训练数据。我们将使用与论文一致的COCO数据集。


数据集结构
COCO数据集主要包含两个部分:
images文件夹:存放实际的图像文件。例如train2014和val2014分别对应训练集和验证集的图片。labels文件夹:存放对应的标签文件。标签文件是.txt格式,其文件名与images文件夹中的图片名一一对应。




标签文件内容
每个标签文件(.txt)内包含多行数据,每行代表图像中的一个物体标注,格式通常为:[类别ID, 中心点x坐标, 中心点y坐标, 框宽度, 框高度]。这些坐标是归一化后的值。



路径索引文件
在实际读取数据时,代码并非直接扫描文件夹,而是通过读取一个 .txt 文件来获取所有图像的完整路径。因此,你还需要准备:
train.txt:包含所有训练图像路径的列表。val.txt或val5k.txt:包含所有验证图像路径的列表。

总结一下,你需要准备好的数据相关文件包括:
- 实际的图像文件夹(如
train2014/,val2014/)。 - 对应的标签文件夹(如
labels/train2014/,labels/val2014/)。 - 训练集路径文件
train.txt。 - 验证集路径文件
val5k.txt。
⚙️ 配置文件



除了数据和环境,另一个核心是网络配置文件。YOLOv3的网络结构定义在一个 .cfg 配置文件中(例如 yolov3.cfg)。
配置文件的作用
该文件按顺序定义了网络的每一层,包括:
- 卷积层:指定了卷积核大小、步长(stride)、填充(padding)和输出通道数。
[convolutional] batch_normalize=1 filters=32 size=3 stride=1 pad=1 activation=leaky - 快捷连接层:用于实现残差连接,通过
from=-3这样的参数指定与前面哪一层进行相加。[shortcut] from=-3 activation=linear - YOLO检测层:定义了锚框(anchor boxes)和最终的检测输出。
在代码中,我们会读取这个配置文件,并根据其中的定义逐层构建出完整的Darknet-53网络模型。对于COCO数据集,通常使用预设的9种锚框,并在三个不同尺度的特征图(13x13, 26x26, 52x52)上进行检测。
📝 本节总结
本节课中,我们一起学习了开始YOLOv3代码实战前必须完成的准备工作:
- 环境配置:安装合适的IDE(如Eclipse或PyCharm)以及PyTorch深度学习框架。
- 数据准备:下载并组织COCO数据集,确保图像、标签文件以及路径索引文件正确对应。
- 配置文件:了解网络结构配置文件(
.cfg)的格式与作用,它是构建模型的蓝图。

确保以上三点都已就绪,我们就能在接下来的课程中,深入代码内部,一步步理解YOLOv3是如何工作的了。
图像处理基础教程 P7:高斯与中值滤波 📊

在本节课中,我们将要学习两种重要的图像平滑滤波方法:高斯滤波和中值滤波。我们将了解它们的基本原理、核心思想以及如何应用它们来处理图像中的噪声。
高斯滤波 🌀
上一节我们介绍了均值滤波,它平等地对待邻域内的所有像素。本节中我们来看看高斯滤波,它引入了一种更符合直觉的加权思想。
高斯函数是一个大家应该都了解的数学函数。假设均值为零,并指定一个标准差,我们可以得到高斯函数的图像。高斯函数的核心含义是:越接近均值点,其函数值(可能性)越大。这代表在坐标轴上,越接近X等于零的位置,Y的取值相对较大。
我们可以这样理解:在图像处理中,离中心像素越近的像素点,对中心像素最终值的影响应该越大。这与均值滤波中所有像素权重相等的做法不同。

例如,以一个像素值为204的点作为中心点。其邻域内,像素值75离204的距离较近,78离204的距离稍远,113比较近,235比较远,104比较近,154可能比较远。按照高斯函数的思想,离中心点越近的像素,我们应该更重视它。

因此,在使用高斯滤波时,我们使用的滤波器(filter)会发生改变。我们可以这样设置权重:最中心的像素权重设为1,表示其权重比较重要;距离中心较近的像素点,可以设置成0.8等较大的值;距离相对较远的像素点,如121、174、235、154,在滤波器中的数值可以设置得稍小一点,表示其重要程度较低。这体现了像素间基于距离的权重关系。


核心思想公式:滤波器权重矩阵的构造基于二维高斯函数:
G(x, y) = (1/(2πσ²)) * exp(-(x² + y²)/(2σ²))
其中,(x, y)是相对于中心点的坐标,σ是标准差。

这个道理很简单:离中心点越近的像素,在计算中发挥的效果应当越强;离得越远的像素,其作用就没有那么大了。这相当于我们构造了一个权重矩阵来进行计算,而不是简单地使用均值。
以下是高斯滤波效果的观察:
从整体上看,图像仍然存在一些噪声点,但给人的感觉是噪声点没有使用均值滤波后那么严重了。这就是高斯滤波的效果。
高斯滤波比较简单,核心是回想高斯分布的形状,理解了这一点,再理解高斯滤波就很简单了。

中值滤波 🔢
接下来我们介绍中值滤波。中值滤波的核心是“中值”这个概念。

中值是指将一组数值按大小顺序排列后,位于中间位置的那个值。在中值滤波中,我们处理一个像素时,会查看以其为中心的某个区域(例如3x3的方框)内所有像素的值。
例如,对于中心点及其8邻域的像素值:204, 75, 78, 113, 24, 154, 104, 235, 121。我们首先将这些值进行排序(从小到大或从大到小均可)。排序后为:24, 75, 78, 104, 113, 121, 154, 204, 235。排序完成后,我们寻找中间的那个值。前面有4个值,后面有4个值,中间的值是113。那么,经过中值滤波处理后,当前中心像素点的值就变为113。我们使用这个中间值作为平滑处理后的结果。

中值滤波的使用比较简单:指定滤波器的大小(例如是5x5还是3x3)。滤波器会框选住对应区域的像素,例如5x5就是25个像素,对这25个值排序后,取第13个值(中间值)作为处理结果。
以下是中值滤波效果的观察:
当使用中值滤波执行后,图像中所有的椒盐噪声点看起来几乎都消失了。因为它是用中间值来替代原值,而噪声点(极大或极小的值)在排序后通常不会成为中值,从而不会被“拷贝”到结果中。因此,当我们的图像数据中存在一些噪声点,特别是类似椒盐噪声的孤立噪点时,使用中值滤波通常能非常有效地处理掉这个问题。

结果对比与展示 🖼️

最后,介绍一个展示多个结果的方法。当我们使用cv2.imshow时,可以将多种滤波的结果组合在一起展示。例如,我们可以将均值滤波、高斯滤波和中值滤波的结果通过np.hstack或np.vstack函数水平或垂直拼接在一起。

这样,我们可以一次性观察不同滤波方法处理后的效果对比。例如,从左到右依次展示:原始图像(或均值滤波结果)、高斯滤波结果、中值滤波结果。通过对比可以清晰地看到,中值滤波在去除椒盐噪声方面效果最为显著,而高斯滤波则在平滑图像的同时更好地保留了边缘信息。
本节课中我们一起学习了高斯滤波和中值滤波。高斯滤波基于距离赋予像素不同的权重,实现平滑;中值滤波则通过取邻域像素值的中位数来有效滤除孤立噪声点。它们是图像预处理中用于去噪和平滑的两种基本且强大的工具。
课程P70:训练参数设置详解 🛠️

在本节课中,我们将学习如何为YOLO模型训练配置必要的参数。我们将一步步讲解如何传入数据路径和预训练模型,并概述代码执行的整体流程。
参数配置步骤
接下来演示这段代码的使用方法。第一步需要传入所有必需的参数。

无需担心这些参数是否需要逐一编写。它们都是常见参数,例如迭代次数、批次大小、模型定义文件以及数据位置。

这些参数均已预先配置好,无需额外设置,例如输入尺寸等。只需要额外写入两个参数即可。
配置方法
如果你使用与我相同的IDE,可以按照以下步骤操作:

- 在代码文件上点击右键。
- 在运行选项中选择
Run As。 - 选择最后一项以配置当前代码。
点击之后会弹出一个对话框。在对话框中找到参数配置部分。

我已经将两个所需参数写入。如果你初次使用,IDE中不会有这些参数,需要你自行指定。
以下是需要配置的两个参数:
- 第一个参数
data configure:即coco.data文件的路径。这个文件描述了训练数据集的所有必要信息,例如这是一个80分类任务、训练数据路径(train)、验证数据路径(valid)等。我们只需确保数据已准备妥当,路径信息已正确写入coco.data文件。 - 第二个参数
pretrain model:即预训练权重模型的路径。我们通常采用迁移学习的方式进行训练,而非从零开始。迁移学习是指加载一个在大型数据集上预训练好的模型,即使其原始任务与我们的任务不同(例如,原模型是800分类,而我们是200分类),其底层的卷积层特征提取能力也是通用的。这相当于有了一个更好的起点。这里使用的是yolov3.conv.74文件。


配置完成后,点击 Apply,然后点击 Run 即可开始训练。
为了方便大家使用,可以将配置好的参数字符串复制出来,以便在其他项目中直接粘贴。如果你使用其他IDE,方法也是类似的,所有IDE都支持设置运行参数。
代码执行流程概述
在讲解代码时,我会设置断点,逐行分析。为了更清晰地理解,这里先调整一下顺序,概述整体执行流程:
- 加载配置参数:第一步是加载所有输入的配置参数,这一步比较简单直接。
- 构建模型:第二步是构建模型。这涉及到定义Darknet的网络结构,并编写前向传播的逻辑。这些内容都已在
model中实现。你需要完成两件事:定义网络所使用的组件,并指定这些组件如何进行前向传播。反向传播由框架自动处理,无需手动编写。 - 加载数据与模型并训练:第三步是将数据(
data)和预训练模型(weights)加载进来,然后正式开始训练过程。

至此,整个训练流程就完成了。
总结

本节课中,我们一起学习了YOLO模型训练前的参数配置。关键点在于正确设置 data configure(数据配置文件路径)和 pretrain model(预训练模型路径)这两个参数。我们还概述了代码执行的三个主要阶段:参数加载、模型构建以及最终的训练循环。掌握这些步骤是成功启动模型训练的基础。
课程P71:3-数据与标签读取 📂➡️🔢
在本节课中,我们将要学习在深度学习项目中,如何高效地读取大规模数据集和对应的标签。我们将重点理解“生成器”的概念,并了解数据读取、预处理以及与标签匹配的完整流程。
上一节我们介绍了项目的基本结构,本节中我们来看看数据是如何被读取和处理的。
我们通常的想法可能是先将所有数据读入内存,再传入模型。其实不是这样的。这里引入一个核心概念:生成器。
生成器是这样工作的:假设我们有一个模型,它需要输入数据。但数据量很大(例如COCO数据集有18G),无法一次性全部加载到内存或显存中。生成器就像一个供应商,它不会一次性购入所有原材料(数据)。当模型在训练中提出需求(例如,一次需要64个数据),生成器就实时读取64个数据,打包好传给模型。下一次迭代时,再读取下一个64个数据。

因此,数据不是在训练开始前读取的,而是在训练过程中实时读取的。我们来看一下代码中的关键部分:
for epoch in range(num_epochs):
for batch_i, (images, targets) in enumerate(dataloader):
# 训练步骤...
在 dataloader 中,数据是在训练循环中被实时读入的。所以,我们的构建顺序通常是:先构建模型和配置优化器,数据读取则在训练时进行。
为了让大家更清晰地理解代码逻辑,我们先讲解数据是如何读取的。
在 dataset 类中,核心函数是 __getitem__。这个函数负责读取单张图像及其对应的标签。
在调试过程中,我们可以看到生成器是如何一张一张图像读取的。当模型需要一个包含64个数据的批次时,生成器会依次读取第1个、第2个数据,直到读满64个。
以下是读取和处理单条数据的关键步骤:

-
读取图像路径:首先,从存储了路径的TXT文件中,读取某一张训练图像的路径。
- 提示:建议使用绝对路径来指定数据位置,这可以避免80%因路径配置错误导致的问题(如空值报错)。
-
加载并转换图像:使用图像处理包打开该路径的图像。无论原始格式是PNG还是JPG,都必须统一转换为RGB格式,并进一步转换为PyTorch所需的
tensor格式。 -
图像预处理(填充):原始图像的尺寸可能各不相同(例如391x640的长方形)。但模型输入通常需要固定尺寸的正方形。因此,我们需要对图像进行填充操作。
- 例如,一个391x640的图像,经过填充后,会变成一个640x640的正方形图像。缺失的部分用特定值(如0)填充。
-
读取标签:根据标签文件的路径,读取对应的标签数据。标签的格式(如相对坐标或绝对坐标)取决于具体数据集的规定,需要参照数据集的官方说明。
- 关键点:必须确保图像和标签是一一对应的。常见错误是数据和标签不匹配,导致训练结果无效。编写代码后,务必通过调试验证图像和标签的对应关系。
本节课中我们一起学习了深度学习中的数据读取机制。我们明白了使用生成器可以高效处理大规模数据,避免了内存不足的问题。我们还了解了数据读取的完整流程:从路径读取、图像加载与格式转换、尺寸标准化(填充),到标签的读取与匹配。记住,确保数据与标签正确对应是成功训练模型的重要前提。
课程P72:标签文件读取与处理 📄➡️🔢
在本节课中,我们将学习如何从标签文件中读取目标检测任务所需的边界框数据,并将其转换为模型训练所需的格式。整个过程包括读取原始数据、进行坐标转换以及处理数据增强等步骤。
数据读取与格式转换
上一节我们介绍了图像数据的加载与预处理,本节中我们来看看如何读取和处理对应的标签文件。
标签文件通常以 .txt 格式存储。我们首先使用 numpy.loadtxt 函数将其读入内存。然而,在深度学习框架中,我们通常需要将数据转换为张量格式。
import numpy as np
import torch
# 读取标签文件
boxes = np.loadtxt('label.txt')
# 将NumPy数组转换为PyTorch张量
boxes_tensor = torch.from_numpy(boxes)
转换完成后,我们得到一个张量。其中,每一行代表一个边界框。第一个值(例如14)表示该框所属物体在数据集类别(如COCO数据集的80个类别)中的ID编号。后续的四个值则代表该边界框的相对坐标 [x, y, w, h]。
坐标变换:从原始图像到填充后图像
我们之前对图像数据进行了填充(padding),使其变为正方形。因此,标签中的坐标也必须进行相应的调整,以匹配填充后的图像。
以下是坐标变换的步骤:
- 还原实际坐标:首先,将标签中的相对坐标乘以图像原始的高度(H)和宽度(W),得到在原始图像上的实际像素坐标
(x1, y1, x2, y2)。 - 应用填充偏移:根据之前计算出的填充方式(上下填充或左右填充),将对应的偏移量加到坐标上。这样,我们就得到了在填充后正方形图像上的坐标。
经过上述步骤,我们得到了填充后图像上的边界框坐标 (x1, y1, x2, y2)。
坐标格式转换:从角点式到中心式
在目标检测模型中,我们通常预测边界框的中心点坐标和宽高,而不是两个角点的坐标。因此,我们需要进行第二次坐标转换。
将角点坐标 (x1, y1, x2, y2) 转换为相对的中心点坐标 (cx, cy) 和相对宽高 (w, h) 的公式如下:

# 假设 img_size 是填充后正方形的边长
cx = (x1 + x2) / 2 / img_size
cy = (y1 + y2) / 2 / img_size
w = (x2 - x1) / img_size
h = (y2 - y1) / img_size

请注意,这里除以 img_size 是为了将坐标值归一化到 [0, 1] 区间,得到相对于整个图像尺寸的相对值。这是模型训练所期望的格式。
最终,我们的 targets 张量中存储了每个边界框的类别ID以及转换后的相对坐标 (cx, cy, w, h)。
可选步骤:数据增强

在训练过程中,我们有时会对图像进行数据增强,例如水平翻转、随机裁剪等,以提高模型的泛化能力。
以下是进行数据增强时需要注意的事项:
- 同步变换:对图像进行任何几何变换时,必须同步地对标签中的边界框坐标进行完全相同的变换。
- 可选性:对于数据量已经足够大的任务,数据增强可能带来的提升有限,可以根据实际情况选择是否使用。
如果决定实施数据增强,需要在数据加载的循环中,对每一张图像及其对应的标签同步应用选定的增强操作。
数据加载循环
在训练时,数据加载器会循环遍历数据集。每次迭代,它都会执行上述所有步骤,为一批(batch)数据生成对应的图像张量 imgs 和标签张量 targets。
这个过程确保了模型在训练时接收到的每一对图像和标签都是正确对齐且格式统一的。

本节课中我们一起学习了目标检测任务中标签文件的完整处理流程。我们从读取原始的 .txt 文件开始,经历了转换为张量、根据图像填充调整坐标、将坐标格式从角点式转换为模型所需的中心式,并简要探讨了数据增强的注意事项。理解这个过程对于构建和调试目标检测模型的数据管道至关重要。
课程 P73:YOLOv3模型架构与计算流程详解 🧠
在本节课中,我们将学习YOLOv3模型的核心架构及其前向传播的计算流程。我们将深入代码,了解如何根据配置文件构建网络,以及数据是如何在网络中流动并最终产生预测结果的。
模型架构与计算流程概述
说完了数据之后,接下来就是模型整体的架构。在这里会说明YOLOv3模型该如何进行构造,以及在前向传播过程中实际是怎么计算的。这相当于要讲两部分内容。
第一部分是模型,或者说Darknet-53网络,它是怎样组成的。第二部分是实际数据输入后,从前到后一步一步是怎么计算的。因此,讲解模型要讲两部分:一个是它的架构,一个是它的计算方法。
所有核心的代码都在一个名为 models.py 的文件中。接下来会进入代码进行讲解。首先,需要了解在PyTorch框架中,我们的结构该怎么去写。
PyTorch网络结构编写逻辑

这里定义了一个名为 Darknet 的类,它其实就是论文中的Darknet-53。因为我们的框架是用PyTorch去做的,所以在这里需要写两块逻辑。

第一块逻辑是构造函数。在这个构造函数中,需要指定好接下来这个网络模型都用到了哪些模块。

在我们的配置文件中,有一个 yolov3.cfg 文件。它就是我们整体要使用哪些结构的全部定义,包括一些参数和网络层的名字。
以下是配置文件的示例内容:

[net]
# 超参数

[convolutional]
# 第一个卷积层

[convolutional]
# 第二个卷积层
[shortcut]
# 残差连接层

[yolo]
# YOLO输出层
配置文件是按顺序写的,先有第一个层是什么层,然后第二层是什么层。每一个层它做了什么事,这里全有。因此,在代码中,我们要做的第一步就是把这个配置文件读进来。
读进来之后,按照配置文件当中写的顺序,逐层把我们的结构定义好。把结构定义好,就是说现在要定义一个卷积层,那卷积里边有些参数需要设置,比如Batch Normalization层可能有些参数,还有一些YOLO层。每一层都有参数,我们需要从上到下指定好都有哪些层,以及这些层之间的参数。这是我们要做的第一件事,即在构造函数中把需要的东西先都指定好。
前向传播计算过程
下一个部分就是 forward 函数。这个函数的作用是:刚才你已经定义了第一层、第二层、第三层、第四层分别是什么。接下来,就要实际去走一遍计算流程。
实际计算流程是这样的:真正来数据的时候,输入 x 要进来走一步。比如这是一个卷积层,好,马上就走。然后,如果它是一个 shortcut 层(即残差连接层),我们就做一个残差连接。接下来再往下走,可能又是一些卷积层,然后可能还有一个比较复杂的YOLO层。YOLO层相当于要得到最终的一个输出结果以及损失值计算。
因此,在 forward 函数中,我们会说明一个输入 x 来了之后,怎么样一步步去走,最终是如何得出预测结果的。大家自己写网络结构时,逻辑也是一样的。只要使用PyTorch框架或其他框架,其实也大同小异。
第一步都需要去写构造函数,来写一写里边用到什么东西。然后接下来就是一个 forward 函数。forward 函数中的代码虽然看起来不多,但大部分核心内容都在这里,我们要讲怎么样去实际做计算。这一步都是需要大家自己去写的,这些是比较核心的东西。
接下来,我们会进入到Debug模式中,为大家逐一讲解。
总结
本节课中,我们一起学习了YOLOv3模型的两大核心部分:架构定义与前向传播计算。
- 架构定义:我们了解了如何通过读取
yolov3.cfg配置文件,在PyTorch的类构造函数中按顺序定义网络所需的各个层(如卷积层、残差连接层、YOLO层)及其参数。 - 前向传播:我们明确了在
forward函数中,输入数据x是如何按照定义好的层顺序,一步步进行计算,并最终通过YOLO层得到预测框和类别信息的。
理解这两部分是将理论模型转化为可运行代码的关键。在接下来的课程中,我们将进入代码的Debug模式,详细查看每一部分的实现细节。
课程P74:基于配置文件构建网络模型 🧠
在本节课中,我们将学习如何根据一个配置文件来构建一个复杂的神经网络模型。我们将深入代码,一步步解析模型是如何从配置文件中读取信息,并组装成最终的神经网络结构的。
概述
我们将通过分析一段代码,了解如何从一个YOLOv3的配置文件(.cfg)中读取网络结构定义和超参数,并据此动态地构建出对应的神经网络模型。这个过程涉及读取文件、解析模块、创建层(如卷积、批归一化、激活函数)并按顺序组合它们。
第一步:读取配置文件
首先,我们需要从指定的路径读取配置文件。配置文件包含了网络的所有结构信息和训练所需的超参数。
以下是读取配置文件的核心代码:
# 假设配置文件路径为 'yolov3.cfg'
with open('yolov3.cfg', 'r') as f:
lines = f.read().split('\n')
读取后,lines 变量包含了配置文件的所有行。配置文件通常分为两部分:顶部的超参数部分和下面的网络结构定义部分。
第二步:解析网络结构

上一节我们介绍了如何读取配置文件,本节中我们来看看如何解析其中的网络结构定义。
网络结构由多个“模块”按顺序组成。每个模块在配置文件中以一个 [type] 开头(例如 [convolutional]),后面跟着该模块的参数(如 filters=32, size=3)。
以下是解析过程的关键步骤:

- 初始化一个空的模块列表
module_list,用于按顺序存放构建好的网络层。 - 遍历配置文件的每一行。
- 当遇到以
[开头的行时,表示一个新的模块开始了。 - 根据模块类型(如
convolutional,upsample),读取其后的参数行,并创建对应的PyTorch层。 - 将创建好的层添加到
module_list中。
第三步:构建“三合一”卷积模块
在YOLOv3的配置中,一个 [convolutional] 模块实际上代表了一个“三合一”的组合:卷积层 (Conv2D) + 批归一化层 (BatchNorm) + 激活函数层 (LeakyReLU)。

以下是构建这个组合模块的代码逻辑:
if module_type == '[convolutional]':
# 1. 解析参数,例如 filters, size, stride, pad
filters = int(module_def['filters'])
size = int(module_def['size'])
stride = int(module_def['stride'])
pad = int(module_def['pad'])
# 2. 创建卷积层
# 注意:如果后面接了BatchNorm,则卷积层通常不设置偏置 (bias=False)
conv = nn.Conv2d(in_channels=prev_filters,
out_channels=filters,
kernel_size=size,
stride=stride,
padding=pad,
bias=False)
# 3. 创建批归一化层
bn = nn.BatchNorm2d(filters)
# 4. 创建激活函数层 (LeakyReLU)
# 参数0.1表示负半轴的斜率
activation = nn.LeakyReLU(0.1)
# 5. 将这三个层按顺序组合成一个序列(一个模块)
module = nn.Sequential(conv, bn, activation)
# 6. 将这个“三合一”模块添加到总的 module_list 中
module_list.append(module)
# 7. 更新 prev_filters,作为下一层的输入通道数
prev_filters = filters
通过这种方式,配置文件中的一个 [convolutional] 行就被转换成了一个功能完整的神经网络模块。
第四步:处理其他类型的层
除了最常见的卷积模块,配置文件中还定义了其他类型的层,例如上采样层 ([upsample]) 和快捷连接(用于构建残差块)。
以下是处理上采样层的示例:
elif module_type == '[upsample]':
# 解析上采样因子
stride = int(module_def['stride'])
# 创建一个上采样层。在构造函数中可能只做记录,实际操作在前向传播中定义。
# 例如使用 nn.Upsample 或 F.interpolate
module = nn.Upsample(scale_factor=stride, mode='nearest')
module_list.append(module)
对于快捷连接 ([shortcut]) 或路由层 ([route]),它们不直接创建新的参数层,而是在前向传播过程中定义张量的运算逻辑(如相加或拼接)。在模型构建阶段,它们通常被添加为一个自定义的、空的占位层,其具体逻辑在模型的 forward 函数中实现。

总结
本节课中我们一起学习了基于配置文件构建神经网络模型的完整流程:
- 读取与解析:从
.cfg文件中读取所有行,并区分超参数与网络结构定义。 - 模块化构建:遍历结构定义,根据每个模块的类型(如
[convolutional])解析其参数。 - 组合层:重点掌握了如何将卷积、批归一化和激活函数组合成一个高效的“三合一”模块。
- 组装模型:将所有创建好的模块按顺序添加到一个
nn.ModuleList中,形成完整的模型骨架。
这种方法极大地提高了模型的灵活性和可配置性,只需修改文本配置文件即可改变网络结构,无需重写大量代码。理解这一过程对于阅读和修改现代目标检测框架(如YOLO系列)的源码至关重要。
课程P75:7-路由层与shortcut层的作用 🧭
在本节课中,我们将学习YOLO网络结构中的两个关键层:路由层和shortcut层。我们将详细解释它们各自的功能、区别以及在配置文件中的表示方式,帮助你清晰理解这两个容易混淆的概念。
路由层的作用
上一节我们介绍了卷积层等基础结构,本节中我们来看看路由层。路由层在YOLO网络中的核心作用是进行特征图的拼接操作。
具体来说,当网络进行到某一层(例如一个上采样层)时,路由层会将当前层的输出特征图与网络中更早某一层的特征图在通道维度上进行拼接。

例如,假设我们有一个上采样后的特征图,尺寸为 26×26×256。同时,网络中更早存在一个尺寸为 26×26×512 的特征图。路由层会将这两个特征图拼接起来,最终得到一个尺寸为 26×26×768 的新特征图。其操作可以表示为:
公式: output = Concat([feature_map_A, feature_map_B], dim=channel)

在配置文件中,路由层通过 layers 参数来指定与前面哪一层进行拼接。
以下是路由层在配置文件中的一个示例:
[route]
layers = -4
这里的 layers = -4 表示当前层需要与前面倒数第4层的输出进行拼接操作。
Shortcut层的作用
了解了进行拼接的路由层后,我们再来看看 shortcut层。Shortcut层是实现残差连接的关键,它的核心操作是加法,而非拼接。
Shortcut层会将当前层的输出与网络中某一更早层的输出进行逐元素相加。这个操作不会改变特征图的尺寸(高度、宽度和通道数),它只是将两部分的特征信息在数值上融合。
例如,一个 13×13×128 的特征图经过一个残差块后,输出仍然是 13×13×128。Shortcut连接会将这个输出与该残差块的输入(另一个 13×13×128 的特征图)直接相加。其操作可以表示为:
公式: output = feature_map_A + feature_map_B



在配置文件中,shortcut层通过 from 参数来指定与前面哪一层进行相加。
以下是shortcut层在配置文件中的一个示例:
[shortcut]
from=-3
activation=linear
这里的 from = -3 表示当前层的输出要与前面倒数第3层的输出进行相加操作。

核心区别总结
为了更清晰地对比,以下是路由层与shortcut层的核心区别:
- 操作不同:路由层进行拼接,会增加特征图的通道数;shortcut层进行加法,保持特征图尺寸不变。
- 目的不同:路由层旨在融合不同尺度或阶段的特征,增加特征的丰富性;shortcut层旨在建立残差连接,缓解深层网络中的梯度消失问题,使网络更容易训练。
- 输出维度:路由层输出维度改变;shortcut层输出维度不变。


在代码的构造函数中,这两层通常只进行基本的定义和“占位”,具体的拼接或加法计算会在网络的前向传播过程中执行。

过渡至YOLO层

到目前为止,我们一起学习了卷积层、路由层和shortcut层。接下来,我们将进入YOLOv3网络中最核心、最复杂的部分——YOLO层。
YOLO层负责最终的目标检测输出。在网络中,通常会有三个YOLO层,分别对应大、中、小三种尺度的预测,用于检测不同大小的物体。

在YOLO层的构造函数中,需要完成多项重要工作,例如:
- 指定该层对应的先验框的ID。
- 获取这些先验框的实际宽高尺寸。
- 定义网络输入的图像尺寸和目标的类别数量(例如COCO数据集的80类)。
这些准备工作是为后续前向传播中计算边界框坐标、置信度和类别概率奠定基础。

本节课总结
在本节课中,我们一起学习了YOLO网络中的两个重要层:
- 路由层:执行特征拼接操作,用于融合来自网络不同深度的特征图,增加通道维度的信息量。
- Shortcut层:执行特征加法操作,是实现残差连接的核心,通过恒等映射帮助梯度流动,稳定深层网络的训练。
理解这两者的区别对于读懂YOLO网络结构图和配置文件至关重要。路由层扩展了特征的“宽度”,而shortcut层保障了网络训练的“深度”。下一节课,我们将深入剖析最复杂的YOLO层,完成整个网络结构的解读。
课程P76:YOLO层定义解析 🧩
在本节课中,我们将深入解析YOLO(You Only Look Once)目标检测模型中YOLO层的定义与构建过程。我们将从模型的构造函数开始,逐步深入到前向传播的核心逻辑,并重点关注YOLO层如何进行计算。通过本教程,你将理解YOLO模型在代码层面是如何组织并处理数据的。
模型构建与初始化 🏗️
上一节我们介绍了模型的基本结构,本节中我们来看看YOLO层的具体构建过程。模型构建的第一步是初始化,这通常在构造函数中完成。

在构造函数中,我们根据配置文件读取并定义模型所需的所有层及其参数。这个过程相对简单,主要是读取配置并设置好各个模块的基本属性。
以下是YOLO层在构造函数中定义的一些核心参数:
- 先验框数量:
num_anchors = 3 - 分类类别数:
num_classes = 80 - 阈值:一个用于后续处理的阈值参数。
- 损失函数:为后续计算损失函数预先定义好所需的组件。
# 示例:YOLO层初始化参数示意
self.num_anchors = 3
self.num_classes = 80
self.threshold = 0.5
# 损失函数相关定义...
当前,我们只是完成了模型的基本定义,将配置文件中的结构转化为代码中的层(ModuleList)。特别是,我们会构建三个不同尺度的YOLO层,分别用于预测大、中、小目标。
前向传播流程 🔄

模型构建完成后,真正的计算发生在前向传播(forward)过程中。我们需要将输入数据依次通过每一层,并得到最终的输出。
首先,我们拿到输入数据 x。这个 x 是一个张量(Tensor),包含了经过预处理(如填充padding)的图像数据。我们的目标是将 x 通过网络定义的每一层。
以下是前向传播中针对不同类型层的处理逻辑:
卷积层、上采样层等标准操作
对于PyTorch中已实现的标准操作(如卷积Conv2d、上采样Upsample),处理非常简单。我们直接调用该层模块,输入数据 x,即可得到输出结果。
x = module(x) # module 可以是卷积、上采样等层
Shortcut层(跳跃连接)
Shortcut层实现了类似ResNet中的跳跃连接,其核心操作是加法。它会将当前层的输出与前面某一指定层的输出相加。
# layer_i 指定了与前面第几层进行相加
x = x + layer_outputs[-layer_i]
这里的 layer_outputs 列表保存了之前每一层的输出结果,方便进行这种跨层连接。
Route层(路由层)
Route层的作用是进行张量拼接(Concatenation)。它可以将当前层的输出与之前一层或多层的输出在通道维度上进行拼接。
# 将指定层的输出拼接在一起
x = torch.cat([layer_outputs[i] for i in route_layers], dim=1)
在我们的任务中,通常是将两个特征图拼接在一起。
深入YOLO层 ⚙️
在遍历所有层的过程中,最复杂且核心的部分是YOLO层的前向传播。YOLO层接收特征图,并输出最终的检测结果(边界框、置信度、类别概率)。

YOLO层的计算是代码最多的部分。它会将输入的特征图转换为最终我们需要的预测格式。三个YOLO层会依次执行,通常的顺序是:先预测大目标,然后将其特征图上采样,与中层特征拼接后预测中目标,最后再上采样并与浅层特征拼接预测小目标。
这个过程涉及到将网络输出的密集预测张量,根据先验框(anchors)解码成在原始图像坐标下的边界框,并应用阈值过滤等操作。其输出包含了目标检测所需的所有信息。

总结 📝

本节课中我们一起学习了YOLO模型层的定义与执行流程。
我们首先了解了模型如何在构造函数中根据配置文件初始化,定义了包括三个YOLO层在内的所有网络层。接着,我们深入探讨了前向传播的过程:数据如何依次通过卷积、Shortcut(加法)、Route(拼接)等标准层。最后,我们指出了整个流程中最关键的环节——YOLO层的前向计算,它将特征图转换为最终的检测预测结果。
理解这一从构建到执行的完整流程,是掌握YOLO模型代码实现的基础。在后续的课程中,我们将进一步剖析YOLO层内部具体的计算细节。

课程P77:9-预测结果计算 📊

在本节课中,我们将深入YOLO层的前向传播过程,学习如何将网络输出的特征图转换为具体的边界框预测。我们将重点关注数据的维度变换、预测值的分解以及核心计算步骤。
进入YOLO层

上一节我们介绍了模型的前向传播流程,本节中我们来看看核心的YOLO层内部是如何工作的。我们通过设置断点,跳入到YOLO层的前向传播函数中。
在YOLO层中,我们需要处理输入数据,进行坐标变换和损失计算等所有关键操作。
输入数据解析
以下是输入数据的构成:
x:这不是原始的输入图像数据,而是网络前一层输出的结果。在YOLO层中,x被当作本层的输入。targets:这是从数据中读取的标签信息,包含了边界框的类别以及坐标信息(如x, y, w, h)。img_dim:这是输入图像的尺寸。
特征图形状分析
为了理解代码在做什么,一个有效的方法是打印并分析数据的形状(shape)。我们打印了输入 x 的形状:
print(x.shape)
假设输出为 torch.Size([4, 255, 15, 15]),其含义如下:
4:批处理大小(batch_size),表示一次处理4张图像。为了调试演示,我们设置得较小,实际训练时应根据显存尽可能调大。255:特征图的通道数。这个数字的由来稍后会解释,它代表了每个网格点预测的信息总量。15, 15:特征图的高度和宽度,即网格的尺寸。在YOLOv3中,输入图像尺寸会随机变化(但需能被32整除),因此网格大小也会相应改变,例如可能是13x13或15x15。PyTorch采用 通道优先(channel-first) 的格式。
设备与环境设置
在训练时,代码需要兼容GPU和CPU环境。通过判断 is_cuda 标志,将张量(tensor)设置为对应的设备格式。PyTorch中使用 .cuda() 方法将张量移至GPU。
维度重塑与预测值分解
接下来是核心操作:将特征图转换为具体的预测值。我们通过 view 方法(类似于NumPy的 reshape)对数据进行维度变换。
prediction = x.view(num_samples, num_anchors, num_classes + 5, grid_size, grid_size)
prediction = prediction.permute(0, 1, 3, 4, 2).contiguous()
变换后的形状为:(4, 3, 15, 15, 85)。
其含义解析如下:
4:batch_size,4张图片。3:每个网格对应的先验框(anchor)数量。15, 15:网格尺寸。85:每个预测框需要预测的数值总数。其构成是:4个坐标值(x, y, w, h) + 1个置信度(confidence) + 80个类别概率。
现在,我们从这85个值中分解出各部分预测结果:
- 中心点坐标与宽高:取前4个值,即
(x, y, w, h),代表预测边界框的中心点坐标和尺寸。 - 置信度:取第5个值,代表该框包含物体的可能性,范围应在0到1之间。
- 类别概率:取后80个值,代表属于80个类别中每一个的概率。
类别预测的激活函数
对于80个类别概率的预测,YOLOv3使用了 Sigmoid函数 而非Softmax。

# 伪代码示意
class_pred = torch.sigmoid(prediction[..., 5:])
为什么使用Sigmoid?
- Sigmoid函数将任意输入映射到(0, 1)区间,可以独立地表示每个类别的存在概率。
- 这与YOLOv3的设计一致:它对每个类别执行独立的二分类判断,允许一个物体属于多个类别(多标签分类)。这是YOLOv3与早期版本(使用Softmax进行单标签多分类)的一个重要区别。


本节课中我们一起学习了YOLO层如何将特征图输出转换为具体的边界框预测。我们分析了输入数据的形状,理解了维度重塑操作的意义,并分解了预测值中的坐标、置信度和类别概率。关键点在于,YOLOv3通过Sigmoid函数为每个类别进行独立的二分类预测,这是其多标签识别能力的基础。

课程P78:11-模型要计算的损失概述 📊
在本节课中,我们将要学习如何计算YOLO模型训练过程中的损失值。我们已经了解了前向传播的整体流程,现在只剩下最后一步:根据模型的预测值和真实的标签值来计算损失。本节课将详细解释损失计算的组成部分,以及如何将标签数据转换为与预测值相匹配的格式,以便进行有效的损失计算。
损失计算概述
上一节我们介绍了模型的前向传播过程,本节中我们来看看如何计算损失值。要计算损失,我们需要模型的预测值和真实的标签值。然而,预测值和标签值的表示格式并不相同,因此在进行计算之前,必须先将标签值转换为与预测值相对应的“相对格式”。
标签格式转换的必要性
现在,我们手里有模型的预测结果,例如边界框的坐标(XYWH)和类别预测。同时,我们也有真实的标签数据(targets)。直接使用它们进行计算似乎可行,但存在一个问题:预测值中的坐标(如XY)是相对于其所在网格单元的“相对位置”,而标签中的坐标通常是相对于整张图像的“绝对位置”。两者的表示层面不同,无法直接比较。
因此,在计算损失之前,我们需要一个辅助函数来处理标签数据。这个函数的核心任务是将标签中所有用于损失计算的值(如位置、置信度、类别)全部转换为与预测值格式一致的“相对格式”。
预测值与标签的维度对齐
首先,我们需要理解预测值的张量形状。在预测时,我们得到一个形状为 (batch_size, num_anchors, grid_size, grid_size, 5 + num_classes) 的张量。以示例说明:
batch_size = 4:表示批次中有4个数据样本。num_anchors = 3:表示每个网格单元有3种先验框(anchor)。grid_size = 13:表示特征图被划分为13x13的网格(具体大小因YOLO层和输入图像尺寸而异)。5 + num_classes:其中5代表边界框的4个坐标(x, y, w, h)和1个置信度(confidence),num_classes=80代表类别数量。
为了使标签能与预测值进行计算,我们必须将标签数据也处理成完全相同的格式:(batch_size, num_anchors, grid_size, grid_size, ...)。这是转换函数要完成的第一步工作。

损失函数的组成部分
在将标签格式对齐后,我们就可以计算损失了。YOLO的损失函数通常由几个关键部分组成。让我们回顾一下:

以下是损失函数的主要构成部分:
-
位置误差(Bounding Box Coordinate Loss):衡量预测边界框(XYWH)与真实边界框之间的差异。计算这个损失需要将标签中的XYWH坐标转换为与预测值相同的相对格式。
-
置信度误差(Confidence Loss):衡量模型预测的“框中包含物体的置信度”的准确性。它又细分为两部分:
- 包含物体的置信度误差:针对那些确实有物体的网格单元,我们希望模型预测的置信度接近1。
- 不包含物体的置信度误差:针对那些没有物体的网格单元,我们希望模型预测的置信度接近0。
因此,在准备标签时,我们需要生成两种不同的“置信度标签”来匹配这两种计算:
- 对于“包含物体”的损失,在真实物体中心的网格单元对应位置标为1,其余位置标为0。
- 对于“不包含物体”的损失,其标签逻辑与前者相反(通常在代码中通过掩码实现),目的是让模型降低对背景区域的置信度预测。这一点在后续代码中会特别强调。
-
分类误差(Classification Loss):衡量预测的物体类别是否正确。这是一个标准的多类别分类问题。在标签中,对于有物体的位置,其对应的真实类别索引处标为1(one-hot编码),其他类别标为0。

分类误差的处理相对简单直接。
总结

本节课中我们一起学习了YOLO模型损失计算前的准备工作。核心在于理解预测值与真实标签在格式上的差异,并需要通过一个转换函数将标签数据(包括位置、两种置信度和类别信息)全部处理成与模型预测输出维度完全一致的“相对格式”。只有这样,后续才能正确计算位置误差、置信度误差(含物体与不含物体)以及分类误差这三部分损失,从而有效地训练模型。下一节,我们将深入代码,具体查看这个转换过程是如何实现的。
课程P79:12-标签值格式修改 📝
在本节课中,我们将学习如何将原始的标签数据转换为与YOLO模型预测值格式相匹配的格式。这一过程是构建损失函数、训练模型的关键步骤。我们将详细讲解转换的逻辑和具体实现。
概述
原始标签数据中的边界框坐标是相对于整个原始图像的归一化值(0到1之间)。然而,模型的预测值是基于特征图网格的相对位置。因此,我们需要将标签数据转换为与预测值一致的格式,以便计算损失。本节将分步讲解这一转换过程。
标签初始化
首先,我们需要初始化一个与预测输出维度匹配的标签张量。这个张量将用于存储转换后的标签信息。
以下是初始化过程中涉及的几个核心部分:
- 物体存在掩码(obj_mask):用于标记哪些网格位置存在物体。初始化时全部填充为0,表示初始状态下认为所有位置都没有物体。后续会根据真实标签,将存在物体的位置标记为1。
- 公式:
obj_mask = zeros([batch_size, grid_size, grid_size, num_anchors])
- 公式:
- 无物体掩码(noobj_mask):与
obj_mask相反,用于标记哪些网格位置没有物体。初始化时全部填充为1,表示初始状态下认为所有位置都没有物体。后续会将存在物体的位置标记为0。- 公式:
noobj_mask = ones([batch_size, grid_size, grid_size, num_anchors])
- 公式:
- 分类标签(class_mask):用于标记物体所属的类别。初始化时全部填充为0。后续会根据真实标签,在正确的类别索引位置标记为1。
- 公式:
class_mask = zeros([batch_size, grid_size, grid_size, num_anchors, num_classes])
- 公式:
- IoU分数(iou_score):用于存储预测框与真实框的交并比(IoU)值。初始化时填充为0,后续进行计算和填充。
- 边界框偏移量(tx, ty, tw, th):用于存储真实框相对于其所属网格的偏移量。初始化时全部填充为0,后续进行计算和赋值。
- 代码:
tx = ty = tw = th = zeros_like(obj_mask)
- 代码:
通过以上初始化,我们创建了一个结构化的容器,接下来就是将原始标签中的真实值填充到这个容器的对应位置。


坐标格式转换
上一节我们介绍了标签张量的初始化,本节中我们来看看如何将原始标签坐标转换为模型所需的格式。原始标签坐标是归一化的图像坐标,而模型预测的是基于特征图网格的坐标。

转换过程分为两步:
- 转换为特征图绝对坐标:将归一化坐标乘以特征图的尺寸(例如13),得到边界框在特征图上的绝对坐标位置。
- 公式:
target_boxes_abs = target_boxes_normalized * grid_size
- 公式:
- 转换为网格相对坐标:获取边界框中心点所在的网格索引(
gx,gy),并计算中心点相对于该网格左上角的偏移量(tx,ty)。同时,计算边界框的宽高相对于先验框(anchor)的缩放比例(tw,th)。- 代码示例(获取网格索引):
gx = target_boxes_abs[..., 0].long() # 中心点x坐标的整数部分 gy = target_boxes_abs[..., 1].long() # 中心点y坐标的整数部分 - 代码示例(计算相对偏移):
tx = target_boxes_abs[..., 0] - gx.float() # 中心点在网格内的x偏移(0~1) ty = target_boxes_abs[..., 1] - gy.float() # 中心点在网格内的y偏移(0~1)
- 代码示例(获取网格索引):
经过这两步转换,我们得到的tx, ty, tw, th就与模型预测值的格式完全一致,可以用于后续的损失计算。


总结

本节课中我们一起学习了YOLO标签格式转换的核心步骤。

我们首先初始化了一个结构化的标签张量,用于存放物体掩码、类别标签和边界框偏移等信息。然后,我们重点讲解了坐标转换:将原始图像上的归一化坐标,先转换为特征图上的绝对坐标,再进一步转换为相对于所属网格和先验框的相对坐标。这个过程确保了标签数据与模型预测值在同一个坐标系下,是计算回归损失的基础。

理解并完成标签格式的转换,是正确实现YOLO损失函数、成功训练模型的关键前提。

OpenCV入门课程 P8:图像的读取与处理 📸

在本节课中,我们将学习如何使用OpenCV进行图像和视频的基本操作,包括读取、显示、属性获取、格式转换以及保存。这些是计算机视觉任务中最基础也是最重要的步骤。

图像读取与显示


上一节我们介绍了OpenCV的环境配置,本节中我们来看看如何读取和显示一张图像。
首先,我们使用cv2.imread()函数读取一张图像。读取后,为了展示图像,我们需要执行三个步骤:创建窗口、显示图像、等待用户操作。
import cv2


# 读取图像
img = cv2.imread('cat.jpg')
# 创建窗口并显示图像
cv2.namedWindow('image', cv2.WINDOW_NORMAL)
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

如果每次显示图像都要写这三行代码,会显得比较繁琐。我们可以将这些绘图操作封装到一个函数中。

以下是定义一个名为cv_show的显示函数的方法:

def cv_show(name, img):
cv2.namedWindow(name, cv2.WINDOW_NORMAL)
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()

定义好函数后,只需传入窗口名称和图像对象,即可完成图像的显示。
图像属性

在读取图像后,我们常常需要了解其基本属性,例如尺寸和通道数。


最常用的属性是shape属性,通过它可以获取图像的高度(H)、宽度(W)和通道数(C)。

# 获取图像尺寸和通道信息
h, w, c = img.shape
print(f'高度: {h}, 宽度: {w}, 通道数: {c}')

对于使用cv2.imread()默认方式读取的彩色图像,其通道顺序是BGR,而不是常见的RGB。

读取灰度图像
在许多检测或识别任务中,我们通常需要先将彩色图像转换为灰度图进行预处理。


在OpenCV中,有两种主要的图像读取模式:
- 彩色图像 (
cv2.IMREAD_COLOR) - 灰度图像 (
cv2.IMREAD_GRAYSCALE)

我们可以在读取图像时通过指定第二个参数来直接读取为灰度图。

# 以灰度模式读取图像
img_gray = cv2.imread('cat.jpg', cv2.IMREAD_GRAYSCALE)
cv_show('Gray Image', img_gray)
读取后,查看其shape属性,会发现它只有高度和宽度两个维度,表示这是一个单通道的灰度图像。
除了在读取时转换,我们也可以在任意阶段将彩色图和灰度图进行相互转换,这在后续的实际应用中会经常遇到。
# 将彩色图像转换为灰度图像
img_gray_converted = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv_show('Converted Gray Image', img_gray_converted)
转换后,图像中的彩色信息消失,只保留了亮度(灰度)信息。
图像保存
图像保存操作相对简单,使用cv2.imwrite()函数即可。
# 保存图像
cv2.imwrite('saved_cat.jpg', img)
执行后,就会在指定路径生成图像文件。
此外,OpenCV读取的图像在底层是NumPy数组格式。我们可以查看其size(像素点总数)和dtype(数据类型)等属性。
print(f'像素总数: {img.size}')
print(f'数据类型: {img.dtype}')

视频处理基础


了解了静态图像的处理后,我们来看看如何处理动态的视频。

视频本质上是由一系列连续的静态图像(称为“帧”)组成的。当这些帧以一定速度(如每秒30帧,即30fps)连续播放时,人眼就会感觉到动态画面。帧率越低,视频看起来就越卡顿。
因此,处理视频的核心就是将其拆分成一帧一帧的图像,然后对每一帧图像进行处理(例如人脸检测),最后再将处理结果组合或展示出来。
以下是读取和处理视频的基本步骤:
首先,使用cv2.VideoCapture()创建视频捕获对象。
# 创建视频捕获对象
vc = cv2.VideoCapture('test.mp4')
创建后,需要检查视频是否能被成功打开。
# 检查视频是否成功打开
if vc.isOpened():
print("视频打开成功")
else:
print("无法打开视频")
如果能成功打开,我们就可以在一个循环中逐帧读取和处理视频。
以下是遍历视频每一帧并进行处理的代码框架:


while True:
# 读取一帧,ret为是否读取成功的标志,frame为当前帧图像
ret, frame = vc.read()
# 如果读取失败(如视频已到结尾),则退出循环
if not ret:
break
# 在此处对当前帧 `frame` 进行处理
# 例如,将彩色帧转换为灰度帧
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 显示处理后的帧
cv2.imshow('Video Processing', gray_frame)
# 等待一定时间,并检测按键
# 参数1:等待的毫秒数,控制播放速度。值越小播放越快。
# 参数27:代表ESC键,按下ESC可退出循环
if cv2.waitKey(10) & 0xFF == 27:
break
# 释放视频捕获对象并关闭所有窗口
vc.release()
cv2.destroyAllWindows()
在上面的循环中:
vc.read()每次调用都会返回下一帧。cv2.waitKey(10)中的参数10表示每帧显示后等待10毫秒。这个值可以控制视频播放的速度。值越小,帧与帧之间的间隔越短,视频播放越快;值越大,则播放越慢,看起来可能卡顿。- 按
ESC键(键码27)可以随时中断视频播放并退出程序。
通过这种方式,我们就实现了对视频的逐帧读取、处理(本例中转换为灰度图)和实时显示。


本节课中我们一起学习了OpenCV处理图像和视频的核心基础操作。我们掌握了如何读取、显示和保存图像,了解了图像的基本属性,并学会了在彩色与灰度图之间进行转换。同时,我们也理解了视频是由帧构成的原理,并实践了如何读取视频流、逐帧处理并将其动态展示出来。这些技能是进行更复杂计算机视觉任务的重要基石。


课程 P80:13-坐标相对位置计算 📐
在本节课中,我们将学习如何计算目标检测中预测框与真实框之间的相对位置关系。这是构建YOLO模型训练标签的关键步骤,涉及候选框匹配、IOU计算以及坐标转换。
概述
上一节我们介绍了如何在特征图中定位目标的实际位置。本节中,我们来看看如何为每个真实框(ground truth)匹配最合适的候选框(anchor),并计算它们之间的相对偏移量,为后续的回归训练做准备。

候选框匹配逻辑

在YOLO模型中,特征图的每个格子会预设三种不同尺寸的候选框。对于一个给定的真实目标,我们需要从这三种候选框中选出与其重叠度最高的一个,作为后续微调的基础。


以下是匹配过程的步骤:

- 计算IOU:对于每个真实框,分别计算它与当前格子对应的三种候选框的交并比(IOU)。
- 选择最佳匹配:比较三个IOU值,选择数值最大的那个候选框作为该真实框的“最佳匹配”。
- 记录匹配信息:记录下最佳匹配候选框的编号(0, 1, 2)以及对应的最大IOU值。
这个过程确保了每个真实目标都由一个最合适的预设框来负责预测。
代码实现解析
接下来,我们通过代码来具体理解上述匹配过程。代码的核心是遍历所有真实框,并为每个真实框找到最匹配的候选框规格。
# 假设 `anchors` 是三种候选框的尺寸,`gt_boxes` 是所有真实框的坐标
# 计算每个候选框与每个真实框的IOU
ious = compute_iou(anchors, gt_boxes) # 返回形状为 [3, num_gts] 的矩阵

# 找出每个真实框对应的最佳候选框及其IOU
best_ious, best_n = ious.max(dim=0) # best_n 记录了最佳候选框的索引 (0, 1, 2)

变量 best_n 是一个列表,其长度等于真实框的数量。列表中的每个值(0, 1或2)指明了对应真实框最适合由哪种规格的候选框来预测。
坐标分离与处理
在匹配完成后,我们需要进一步处理真实框的坐标信息,以便将其转换为模型训练所需的格式。
以下是需要分离和计算的坐标信息:
- 批次索引 (batch index):标识当前真实框属于哪一张输入图像。
- 类别标签 (class label):真实框所属物体的类别ID(0~80之间)。
- 中心点坐标 (GX, GY):真实框在特征图尺度上的中心点坐标。
- 宽高 (GW, GH):真实框在特征图尺度上的宽度和高度。
- 格子索引 (I, J):真实框中心点所在格子的左上角坐标。通过向下取整计算得出:
I = floor(GX)J = floor(GY)
例如,如果真实框中心点 GX = 5.51, GY = 8.45,那么它所在的格子索引就是 I = 5, J = 8。这个 (I, J) 对应对应公式中的 (c_x, c_y),即格子本身的坐标位置。
总结


本节课中我们一起学习了目标检测中坐标相对位置计算的核心步骤。我们首先理解了为何以及如何为每个真实框匹配最合适的预设候选框,然后通过代码演示了IOU计算和最佳匹配选择的过程。最后,我们分解了真实框的各项坐标信息,特别是计算了其所在的格子索引 (I, J),为下一节构建完整的训练标签打下了基础。
课程P81:14-完成所有损失函数所需计算指标 📊
在本节课中,我们将学习如何为YOLO模型的损失函数计算准备所有必需的指标。核心任务是将真实标签(targets)转换为与模型预测值(predictions)相匹配的格式,以便后续计算损失。
上一节我们介绍了如何匹配预测框与真实框,并计算了IOU。本节中,我们来看看如何填充掩码、计算位置偏移量以及准备类别标签。
填充物体与背景掩码
首先,我们需要填充之前构建的 obj_mask(物体掩码)和 noobj_mask(非物体掩码)。
obj_mask 初始化为全零,代表背景。对于真实标签中存在的物体,我们需要将其对应位置填充为1,表示该位置存在物体。
以下是具体步骤:
- 确定真实物体在特征图网格中的坐标
(i, j)。 - 将
obj_mask中这些(i, j)坐标位置的值设置为1。
noobj_mask 的填充逻辑与 obj_mask 相反。它初始化为全1,代表背景。对于真实物体所在的网格位置,我们需要将其设置为0,表示该位置不是背景。
以下是具体步骤:
- 将
noobj_mask中真实物体坐标(i, j)位置的值设置为0。

基于IOU阈值调整背景掩码

在计算损失时,需要考虑一种情况:某些预测框虽然与真实框的中心点不完全重合,但它们的交并比(IOU)很高。这些预测框不应被简单地视为背景。
因此,我们设定一个IOU阈值(例如0.5)。对于所有预测框,如果其与任一真实框的IOU大于此阈值,即使在 noobj_mask 中,我们也应将其视为“可能包含物体”,并将其对应位置设置为0。
以下是具体步骤:
- 遍历所有计算出的IOU值。
- 若某个IOU值大于阈值(如0.5),则在
noobj_mask中将该预测框对应的位置设置为0。
计算边界框偏移量(tx, ty, tw, th)
预测值输出的是边界框相对于其所在网格单元的偏移量。我们的真实标签也需要转换成相同的格式。


tx 和 ty 的计算:
它们表示物体中心点相对于所属网格左上角的偏移量。公式如下:
tx = gx - cx
ty = gy - cy
其中,(gx, gy) 是物体在特征图上的绝对坐标,(cx, cy) 是物体中心点所在网格的左上角坐标。

tw 和 th 的计算:
它们表示边界框的宽和高相对于先验框(anchor)尺寸的对数偏移量。这与模型预测时使用 exp() 函数还原宽高的操作相对应。因此,在准备真实标签时,我们需要对宽高进行对数变换。
tw = log(gw / anchor_w)
th = log(gh / anchor_h)
其中,(gw, gh) 是真实框的宽高(相对于特征图尺寸),(anchor_w, anchor_h) 是匹配到的先验框的宽高。

准备类别标签(tcls)与置信度标签(tconf)
类别标签 tcls:
这是一个one-hot编码向量,表示物体所属的类别。例如,如果物体属于第3类,则向量的第3个位置为1,其余为0。
置信度标签 tconf:
它表示一个边界框是否包含物体以及定位的准确性。对于与真实框匹配的预测框,其置信度标签为1;对于背景或不匹配的预测框,其置信度标签为0。实际上,我们之前构建的 obj_mask 就可以直接作为置信度的真实标签。


计算辅助指标

此外,我们还需要计算一些辅助指标,用于后续的损失计算或评估:
- 预测框与匹配的真实框之间的IOU:这个值在计算置信度损失时可能会用到。
- 真实框的置信度:如前所述,即为1。
总结

本节课中我们一起学习了为YOLO损失函数准备计算指标的全过程。我们完成了以下关键步骤:
- 填充了物体掩码(
obj_mask)和背景掩码(noobj_mask)。 - 根据IOU阈值对
noobj_mask进行了精细化调整。 - 将真实框的坐标转换成了模型预测所需的偏移量格式(
tx,ty,tw,th)。 - 准备了类别标签(
tcls)和置信度标签(tconf)。 - 计算了预测框与真实框的IOU等辅助指标。

至此,我们已经将真实标签成功转换为与模型预测值维度、格式完全一致的数据,为下一步计算损失函数做好了所有准备。
🎯 课程P82:YOLOv3模型训练与总结

在本节课中,我们将学习YOLOv3模型训练的核心流程,包括损失计算、反向传播以及如何利用开源代码进行实践。我们将详细解析训练代码中的关键步骤,并探讨如何将论文原理与源码实践相结合,以提升深度学习项目的实战能力。
📊 损失计算详解
上一节我们介绍了前向传播中预测值与标签的构建。本节中我们来看看如何计算模型的损失。
损失计算的核心是将网络前向传播得到的预测值(XYWH2)与通过build_targets函数处理好的标签(T_x, T_y, T_w, T_h2)进行比较。但在计算之前,我们需要明确一个前提:并非所有预测位置都包含目标物体。


以下是计算各项损失的具体步骤:


- 边界框回归损失:我们只对包含目标物体的位置计算边界框(XYWH)的损失。这通过一个名为
obj_mask的掩码来实现,该掩码中值为1的位置表示存在目标。- 计算公式:
loss_xy = obj_mask * MSE_loss(pred_xy, target_xy),loss_wh同理。
- 计算公式:
- 置信度损失:置信度预测目标框内包含物体的概率。它分为两部分:
- 前景损失:针对
obj_mask指示的有目标位置,其真实标签为1。 - 背景损失:针对无目标位置,其真实标签为0。
- 由于这是一个二分类问题(是物体/不是物体),我们使用二元交叉熵损失(BCE Loss)进行计算。
- 公式:
BCE_loss = -1/N * Σ [y_i * log(x_i) + (1 - y_i) * log(1 - x_i)],其中y_i是真实标签(0或1),x_i是预测值(0~1之间)。
- 公式:
- 最终置信度损失是前景损失与背景损失的加权和。
- 前景损失:针对
- 分类损失:计算目标所属类别的损失,同样使用BCE Loss,因为YOLOv3允许一个目标属于多个类别(多标签分类)。



最终,模型的总损失是边界框回归损失、置信度损失和分类损失三项的加权和。这与我们在原理部分讲解的完全一致。
🔄 反向传播与模型更新
理解了前向传播和损失计算后,本节我们来看看模型如何通过学习来优化。
在PyTorch等现代深度学习框架中,反向传播是自动完成的。我们只需要定义好前向传播过程并计算出损失,框架会自动计算网络中所有参数的梯度。
- 反向传播:调用
loss.backward()一行代码即可完成,无需手动计算梯度。 - 参数更新:使用优化器(如SGD或Adam)执行
optimizer.step()来更新模型参数。 - 梯度清零:在下一轮迭代前,需要调用
optimizer.zero_grad()将梯度归零,防止梯度累积。
因此,训练循环的核心代码结构非常清晰:
# 前向传播
predictions = model(images)
loss = compute_loss(predictions, targets) # 包含我们上面详解的各项损失
# 反向传播与更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
训练过程中的其余部分,如日志打印、评估指标计算(如mAP、Recall、IoU等)以及模型保存,都是相对通用的操作,可以根据项目需求灵活添加。


🌐 源码使用与进阶学习



我们已经完整分析了YOLOv3的训练代码。在实际项目中,我们通常直接使用开源实现。本节将介绍如何有效利用这些资源并规划后续学习路径。



以下是高效使用开源代码和深度学习资源的建议:



- 寻找开源实现:在GitHub等平台,搜索“YOLOv3 PyTorch”、“YOLOv3 TensorFlow”等关键词,可以找到不同框架的官方或高质量复现版本。
- 阅读项目说明:仔细阅读项目的
README.md文件,按照指引配置环境、下载数据和权重、运行训练或推理脚本。 - 调试与理解:对于重要的模块,像本课程一样,使用调试工具逐行跟踪代码执行流程,将论文中的理论与代码中的实现细节对应起来。
对于希望进阶的学者或工程师,最好的方法是结合论文阅读与源码分析。


- 深入研读论文:反复阅读原始论文,理解算法的核心思想、网络架构和创新点。论文提供了宏观的蓝图。
- 精读与分析源码:源码包含了所有论文中未提及的实现细节,如数据预处理、损失函数的具体实现、训练技巧等。这是将理论落地的关键。
- 应用于实际项目:尝试将学习的算法应用到自己的任务中,或者对现有代码进行修改以适应新需求。这个过程能极大提升工程能力。
这种“论文+代码”的学习模式,不仅是学术研究的必备技能,也是工业界算法工程师的核心能力。它锻炼了你快速理解、复现和应用前沿算法的能力,这在技术面试和解决实际业务问题时至关重要。

本节课中我们一起学习了YOLOv3模型训练的完整流程,从损失函数的具体计算、反向传播的自动化机制,到如何利用开源代码以及规划未来的进阶学习路线。YOLOv3作为一个经典的检测算法,其代码结构清晰,是理解目标检测训练过程的优秀范例。掌握“原理-代码”相结合的学习方法,将为你打开更广阔的深度学习应用之门。

课程P83:YOLOv3模型预测与效果展示 🚀

在本节课中,我们将学习如何加载训练好的YOLOv3模型,并使用它对新的图像进行目标检测。我们将从指定测试图像开始,逐步完成模型加载、前向传播、非极大值抑制以及结果可视化的完整流程。


1. 准备工作与参数配置

上一节我们介绍了模型的训练过程,本节中我们来看看如何使用训练好的模型进行预测。首先,我们需要指定测试图像所在的文件夹路径。


在代码中,我们打开了一个名为 detect.py 的文件,这是我们的检测函数。第一步是配置参数,指定待检测图像的路径。

# 示例:在配置文件中指定图像文件夹路径
image_folder = './data/simple/'
这里,./data/simple/ 文件夹中包含了一系列待测试的图像,例如狗、自行车、卡车、鸟等不同大小和内容的图片。我们将把这些图像输入到网络中进行检测。


2. 模型加载与设置
接下来,我们需要构建并加载训练好的YOLOv3模型。预测时使用的网络结构与训练时完全相同。

以下是核心步骤:
- 选择运行设备(CPU或GPU)。
- 使用
Darknet类构建模型。 - 加载预训练的权重文件(例如
darknet53.conv.74)。 - 将模型设置为评估模式(
eval()),这意味着在预测过程中不会更新模型参数。

# 构建模型并加载权重
model = Darknet(config_path).to(device)
model.load_state_dict(torch.load(weights_path))
model.eval() # 设置为评估模式
设置评估模式后,模型只进行前向传播计算,不进行反向传播和参数更新。


3. 数据加载与预处理
我们需要创建一个数据加载器(DataLoader)来读取指定文件夹中的图像数据。
以下是数据加载的关键步骤:
- 根据配置的
image_folder路径创建数据集。 - 设置批处理大小(
batch_size)等参数。 - 同时,需要加载一个将类别ID映射为类别名称的文件(如
coco.names),以便将预测的数字结果转换为“狗”、“自行车”等可读标签。

# 创建数据加载器
dataset = ImageFolder(image_folder, transform=preprocess_transform)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)

# 加载类别名称映射
names = load_classes(names_path) # 例如:{0: 'person', 1: 'bicycle', ...}



4. 执行预测与后处理

现在,我们可以遍历数据加载器,对每一批图像进行预测。

以下是预测循环中的核心操作:
- 前向传播:将图像数据输入模型,得到原始预测输出。
- 非极大值抑制(NMS):模型会预测出大量候选框。我们需要使用NMS算法来筛选出最可能代表真实物体的、互不重叠的最佳边界框。其核心公式是依据置信度和IoU(交并比)进行筛选。
- 结果转换:将筛选后的边界框坐标从网络输出格式还原到原始图像尺寸。

for batch_i, (img_paths, input_imgs) in enumerate(dataloader):
input_imgs = input_imgs.to(device)
with torch.no_grad(): # 不计算梯度,加速推理
detections = model(input_imgs) # 前向传播
detections = non_max_suppression(detections, conf_thres, nms_thres) # 执行NMS



5. 结果可视化

最后一步是将检测结果绘制在原始图像上并保存。

以下是可视化的主要过程:
- 遍历NMS处理后的检测结果。
- 对于每个检测到的物体,获取其边界框坐标、置信度和类别ID。
- 根据坐标在图像上绘制矩形框。
- 将类别名称和置信度作为标签标注在框旁。
- 将带标注的图像保存到输出文件夹(如
./output/)中。


执行完代码后,会在输出文件夹生成带检测框的图像。例如,一张包含狗和自行车的图片会被正确标记出“dog”和“bicycle”,并显示相应的置信度。


总结


本节课中我们一起学习了YOLOv3模型预测的完整流程。我们首先配置了测试图像路径,然后加载了训练好的模型权重并将其设置为评估模式。接着,我们使用数据加载器读取图像,通过模型进行前向传播得到预测结果,并利用非极大值抑制(NMS)算法对预测框进行筛选。最后,我们将筛选后的边界框和类别标签可视化在原始图像上。整个过程清晰地展示了如何将一个训练好的目标检测模型应用于实际场景。
课程P84:Labelme工具安装指南 🛠️
在本节课中,我们将学习如何安装Labelme工具。Labelme是一个用于图像标注的图形界面工具,在训练自定义数据集(如用于Mask R-CNN)时,手动为图像创建标签是必不可少的步骤。我们将从安装开始,为后续的数据标注工作做好准备。
工具安装步骤
上一节我们介绍了Labelme工具的作用,本节中我们来看看具体的安装流程。安装过程主要涉及三个Python包的安装。

以下是详细的安装步骤列表:
- 安装Labelme核心包
在命令行或终端中,使用pip命令安装labelme包。pip install labelme


- 安装图形界面支持包
Labelme工具依赖PyQt5来提供图形用户界面,因此需要安装此包。pip install pyqt5


- 安装指定版本的图像处理库
根据官方要求,需要安装特定版本的Pillow库以确保兼容性。pip install pillow==4.0.0

启动Labelme工具


完成上述三个包的安装后,环境便配置好了。启动工具的方法非常简单。


只需在命令行(例如Anaconda Prompt)中直接输入命令 labelme 并回车,系统便会自动运行并打开Labelme的图形界面程序,之后即可开始进行图像标注工作。

本节课中我们一起学习了Labelme标注工具的完整安装流程。我们通过三个简单的pip安装命令,配置好了运行环境,并掌握了启动工具的方法。安装好Labelme是创建自定义数据集标签的第一步,为后续的模型训练奠定了基础。
课程P85:数据信息标注教程 🖼️📝

在本节课中,我们将学习如何使用LabelMe工具对图像数据进行标注,为后续的计算机视觉任务(如目标检测)准备训练数据。

打开标注工具

首先,打开命令提示符(CMD),输入命令 labelme 来启动标注工具。


输入命令后,LabelMe工具界面会立即弹出。

加载图像数据
工具界面提供两个选项来加载数据:“Open”用于打开单张图像,“Open Dir”用于打开包含多张图像的文件夹。


这里我们选择“Open Dir”来打开一个文件夹。你可以使用任何你想练习的数据,例如包含猫或狗的图片。如果后续有特定任务,再使用对应的专门数据。

例如,本教程将使用一个包含现场施工画面的数据集。我们的任务是进行行人检测,即标注出图像中每个人的位置。
打开文件夹后,工具会加载其中的图像。

开始标注
上一节我们加载了数据,本节中我们来看看如何进行具体的标注操作。
选择一张人员较多的图像开始标注。初始状态下,标注模式默认是“多边形”,即通过连接多个点来形成区域,这类似于实例分割的标注方式。

但对于目标检测任务,我们通常使用矩形框。右键点击图像区域,可以在菜单中选择不同的标注形状,如点、矩形框、圆形、线等。
选择第二个选项“Create Rectangle”来创建矩形框。
以下是标注一个目标的具体步骤:
- 选择矩形框工具。
- 在目标左上角点击并按住鼠标。
- 拖动鼠标至目标右下角,形成一个包围目标的矩形框。
- 松开鼠标后,会弹出一个对话框。
- 在对话框中输入该目标的类别标签(例如
person)。 - 点击“OK”完成当前目标的标注。
重复以上步骤,将图像中所有需要检测的目标(本例中的所有行人)都用矩形框标注出来。
保存标注结果
标注完成后,点击工具栏上的“Save”按钮保存标注信息。
系统会提示你选择保存位置。你可以新建一个文件夹(例如 label_test)来存放这些标注文件。
保存后,LabelMe会为当前图像生成一个同名的JSON文件,其中包含了所有矩形框的位置坐标和类别信息。

本节课中我们一起学习了使用LabelMe工具进行图像数据标注的完整流程,包括启动工具、加载数据、使用矩形框标注目标以及保存标注结果。这是构建自定义计算机视觉模型数据集的关键第一步。
课程P86:3-完成标签制作 📝
在本节课中,我们将学习如何完成目标检测任务的数据标注工作。我们将使用LabelMe工具标注多张图像,并了解标注后生成的JSON文件格式。最后,我们会将标注好的数据整合到项目中,为后续的模型训练做好准备。
标注剩余图像
上一节我们介绍了如何使用LabelMe标注第一张图像。本节中,我们来看看如何继续标注文件夹中的其他图像。
文件夹中包含多张图像,我们需要从中再选择几张进行标注。为了演示,我们选择一张与之前略有不同的图像。
以下是标注第二张图像的步骤:
- 在图像中,使用矩形框工具框选一个人形目标。
- 在弹出的标签对话框中,输入类别“person”。
- 重复此过程,将图像中可见的其他人形目标都标注为“person”。
我们计划总共标注五张图像用于后续的迁移学习训练。标注完成后,记得保存为JSON文件,例如 test_2.json。
接下来,我们继续标注第三张、第四张图像,过程与之前相同,将图像中所有“person”目标框选出来。
在标注过程中,你会发现一个事实:数据标注工作并不轻松。虽然我们只标注“人”这一类别,看起来比较简单,但在实际任务中,可能需要检测的类别多达上百种。这要求标注者先在图像中识别出各类目标,再进行框选,过程会繁琐许多。
此外,LabelMe工具还支持更精细的标注方式。之前提到的“点一个点一个点”的操作,是用于制作“mask”,即图像分割任务,其复杂程度远高于拉一个矩形框。
现在我们已经标注了四张图像,最后再选择一张人像较多、目标稍大的图像进行标注。
以下是标注最后一张图像的要点:
- 这张图像中人物较多,需耐心框选每一个“person”目标。
- 在实际个人项目中,你可以标注多个类别。
- 需要指出的是,仅靠少量标注数据训练模型,效果通常有限。要达到较好的效果,通常需要数千张标注图像。
至此,五张图像的标注工作全部完成。我们可以关闭LabelMe工具了。
关于标注工具
市面上存在多种数据标注工具。我们本次使用的LabelMe是其中流行度较高的一款。还有其他如“YOLO-MARK”等专门为YOLO系列算法设计的标注工具。
对于初学者,掌握一个像LabelMe这样用户基数大、功能全面的工具即可,无需安装过多工具。
理解标注文件格式
接下来,我们看看标注完成后生成的数据。在 label_test 文件夹中,生成了以 .json 为后缀的标注文件。
我们打开其中一个JSON文件,查看其内部结构。核心信息如下:
label:表示标注的类别,例如"person"。points:表示标注框的具体坐标,其格式为[[x1, y1], [x2, y2]],分别对应矩形框左上角和右下角的坐标。shape_type:表示标注的形状类型,例如"rectangle"代表矩形框。如果使用其他形状(如多边形)标注,此处信息会不同。- 文件还包含图像路径(
imagePath)、图像高度(imageHeight)和宽度(imageWidth)等信息。
每个JSON文件对应一张图像,其中包含该图像所有标注目标的列表。你标注了几个目标,列表中就有几个条目。
需要注意的是,我们示例中的图像尺寸较大(超过1000像素)。在实际训练时,通常需要对图像进行压缩或缩放,以适配模型输入并提升训练效率。
整合数据到项目
现在,我们已经完成了数据标注,得到了关键的JSON文件。
接下来,我们需要将这些数据整合到PyTorch YOLOv3项目目录中。以下是需要进行的操作:
- 将标注好的JSON文件(位于
label_test文件夹)和对应的原始图像,组织到项目指定的数据目录下。 - 项目源码中通常包含数据加载和预处理脚本,我们需要根据JSON格式编写或调整代码,以正确读取我们的标注信息。
为了方便后续查看,此处展示了数据标注环节的成果示意图:

(标注后生成的JSON文件列表)

(JSON文件内部结构示例)


(项目目录结构示意图)


本节课中,我们一起学习了使用LabelMe完成多张图像的目标标注,理解了标注文件(JSON)的数据结构,并明确了将标注数据导入训练项目的后续步骤。数据准备是深度学习项目的重要基础,准确的标注是模型获得良好性能的前提。
课程P87:4-生成模型所需配置文件 📄
在本节课中,我们将学习如何为自定义的目标检测任务生成和修改YOLOv3模型所需的配置文件。我们将从COCO数据集的默认配置出发,根据自己数据集的类别数量进行调整。


概述
上一节我们完成了数据的标注与整理。本节中,我们来看看如何根据自定义的数据集(例如,一个包含“人”和“吊车”两个类别的数据集)来生成和修改YOLOv3模型的配置文件。核心在于调整模型输出层的类别数,使其匹配我们的任务。
第一步:定位配置文件目录

首先,需要进入YOLOv3项目中的配置文件夹。

以下是具体步骤:
- 进入当前YOLOv3项目目录。
- 找到并进入
config文件夹。

第二步:使用脚本生成自定义配置文件

在 config 文件夹中,存在一个名为 create_custom_model.sh 的脚本文件。该脚本的作用是根据用户指定的类别数量,生成一个对应的自定义模型配置文件。因为我们的任务(例如,检测“人”和“吊车”)与COCO数据集(80个类别)的类别数不同,所以需要此步骤。
在Windows系统下运行.sh脚本
.sh 文件通常在Linux系统下直接运行。在Windows系统中,需要借助Git工具来执行。


以下是获取和安装Git的步骤:
- 访问Git官网或通过搜索引擎下载Windows版本的Git安装程序。
- 下载完成后,按照提示完成安装。
安装Git后,即可在项目目录中运行.sh脚本。

执行生成命令
安装Git后,进入YOLOv3项目的 config 目录。

以下是运行脚本的步骤:
- 在
config文件夹内右键,选择Git Bash Here,打开命令行窗口。 - 输入命令
bash create_custom_model.sh [类别数量]并执行。其中,[类别数量]应替换为你的数据集中目标类别的总数。例如,我们的任务包含“人”和“吊车”,则类别数量为2。
执行该命令后,系统会自动生成一个新的配置文件(例如 yolov3-custom.cfg)。该文件基于原始模板创建,主要修改了网络最终输出层(YOLO层)中的 classes 参数,将其设置为用户指定的类别数量(本例中为2)。网络的其他架构(如特征提取部分)基本保持不变。
第三步:理解与进一步调整(可选)
新生成的配置文件已经适配了我们的类别数量。对于大多数任务,使用默认的锚框(Anchor Boxes)设置即可。
以下是可以根据任务需求进行的可选调整:
- 修改锚框:如果检测的目标尺寸与COCO数据集差异很大,可以重新聚类(K-means)自己的数据集,生成更合适的锚框尺寸,并更新配置文件中的对应参数。
- 调整网络结构:通常,特征提取等主体网络架构无需改动,保持默认即可。
总结

本节课中我们一起学习了为自定义目标检测任务生成YOLOv3配置文件的全过程。核心步骤是使用项目提供的 create_custom_model.sh 脚本,并传入自己数据集的类别数量,从而自动生成一个将最终输出类别数修改正确的配置文件。这为后续使用我们自己的数据训练模型做好了准备。
课程P88:JSON格式转换为YOLO-v3所需输入 🛠️
在本节课中,我们将学习如何将LabelMe标注工具生成的JSON格式标签,转换为YOLO-v3模型训练所需的特定格式。这是准备自定义数据集的关键一步。
概述:为何需要转换标签格式?

上一节我们介绍了配置文件的编写。本节中,我们来看看数据准备的核心环节——标签格式转换。

在LabelMe中,我们得到的标注结果是边界框的绝对坐标,即左上角点(x1, y1)和右下角点(x2, y2)的像素值。


然而,YOLO-v3模型所需的输入格式与此不同。它需要的是边界框中心点的相对坐标以及相对的长宽。具体来说,其格式为:
(class_id, cx, cy, w, h)
其中:
class_id是目标的类别索引(从0开始)。cx和cy是边界框中心点的x、y坐标,其值是相对于图片宽度和高度的比例,取值范围在0到1之间。w和h是边界框的宽度和高度,同样是相对于图片宽度和高度的比例。

因此,我们必须将LabelMe的绝对坐标格式转换为YOLO所需的相对坐标格式。

标签转换实战

以下是进行标签格式转换的具体步骤和代码说明。


首先,我们需要明确类别及其对应的索引。这通过一个字典来定义。
# 定义类别名称与索引的映射关系
# 索引从0开始,顺序需与后续的names文件保持一致
classes = {
"person": 0,
"crane": 1 # 示例类别“吊车”
}


接下来,我们读取JSON文件并进行坐标转换。核心转换公式如下:
# 假设图片宽度为 img_w,高度为 img_h
# LabelMe中的坐标:x1, y1, x2, y2
# YOLO格式转换:
cx = (x1 + x2) / 2.0 / img_w
cy = (y1 + y2) / 2.0 / img_h
w = abs(x2 - x1) / img_w
h = abs(y2 - y1) / img_h
为了方便大家使用,我已经编写好了一个完整的转换脚本 json_to_yolo.py。你需要根据你的项目结构修改两个关键路径:

- JSON文件所在路径:指向你通过LabelMe标注生成的
.json文件。 - 输出文件路径:建议将转换后的
.txt标签文件保存到项目目录的data/custom/labels/文件夹下,以便与后续的训练代码配置保持一致。

运行此脚本后,每个JSON文件都会生成一个同名的TXT文件,其中包含转换后的YOLO格式标签。
总结

本节课中我们一起学习了将LabelMe的JSON标注格式转换为YOLO-v3训练格式的方法。我们理解了两种格式的核心差异:绝对坐标与相对坐标。通过使用提供的转换脚本并正确配置路径,你可以轻松完成自定义数据集的标签准备工作,为接下来的模型训练奠定基础。
课程P89:6-完成输入数据准备工作 📝
在本节课中,我们将学习如何完成目标检测任务中输入数据的准备工作。具体内容包括将LabelMe标注的JSON文件转换为YOLO格式的标签文件,以及将对应的图像文件整理到正确的目录结构中。
指定JSON文件路径
上一节我们介绍了数据标注的概念,本节中我们来看看如何读取这些标注文件。
首先,需要在代码中指定存放LabelMe生成的JSON标签文件的文件夹路径。这里使用绝对路径以避免路径问题。
json_dir = "C:\\Users\\YourName\\Desktop\\label_test"
注意:在Windows系统中,路径分隔符应使用双反斜杠 \\ 或正斜杠 /,以防止被Python解释为转义字符。
读取并转换JSON文件
指定路径后,程序将遍历该文件夹中的所有JSON文件。对于每一个文件,都需要读取并进行格式转换。
在上述代码中,程序会打开一个输出文件用于写入转换后的标签。然后,读取当前的JSON文件内容。
JSON文件中包含了图像的宽度W和高度H,以及每一个标注对象(称为shape)的信息。我们的目标是获取每个对象的类别和边界框坐标,并将其转换为YOLO所需的中心点坐标和相对宽高。
以下是转换过程的核心步骤:
- 计算图像尺寸:从JSON中获取图像的原始宽度
W和高度H。 - 遍历每个标注对象:对于JSON中的每一个
shape(即一个标注框):- 获取其类别标签
label_name(例如“person”)。 - 获取其矩形框的左上角
(x1, y1)和右下角(x2, y2)坐标。
- 获取其类别标签
- 执行坐标转换:通过一个
convert操作,将绝对坐标转换为相对坐标(0到1之间)。- 中心点x坐标:
cx = (x1 + x2) / 2.0 / W - 中心点y坐标:
cy = (y1 + y2) / 2.0 / H - 框的相对宽度:
w = (x2 - x1) / W - 框的相对高度:
h = (y2 - y1) / H
- 中心点x坐标:
转换完成后,每一行标签数据的格式为:类别索引 cx cy w h。
注意:类别索引需要根据之前定义的类别映射字典进行转换,例如“person”可能对应索引0。
生成转换后的标签文件
完成转换逻辑后,执行代码。程序会快速处理所有JSON文件,并在指定的输出目录(例如data/custom/labels/)下生成对应的.txt标签文件。
每个.txt文件与原始的JSON文件同名。文件内容如下所示:
0 0.5 0.5 0.3 0.4
0 0.2 0.7 0.1 0.2

这表示该图像中有两个标注框,类别都是0(例如“person”),后面四列分别是中心点坐标和宽高的相对值。



至此,标签文件的转换工作就完成了。

整理图像数据文件
标签准备完毕后,接下来需要整理对应的图像文件。
我们需要将训练和验证所用的图像文件放入指定的目录中(例如data/custom/images/)。
以下是操作要点:
- 手动或代码复制:可以将图像文件手动复制到目标文件夹。在数据量较大时,建议编写简单的脚本自动完成。
- 保持文件名一致:至关重要的一点是,图像文件的名称(不含后缀)必须与对应的标签文件名称完全一致。
- 例如:图像
image_001.jpg对应标签image_001.txt。
- 例如:图像

通过以上步骤,我们就完成了输入数据的全部准备工作,得到了结构清晰、格式正确的图像和标签文件对。


课程总结
本节课中我们一起学习了目标检测数据准备的最后步骤。
我们首先指定了LabelMe标注文件的路径,然后编写代码读取JSON文件,并将其中以(x1, y1, x2, y2)格式存储的边界框坐标,转换为YOLO系列模型所需的(cx, cy, w, h)相对坐标格式,并生成了对应的文本标签文件。

最后,我们将图像文件整理到相应目录,并强调了图像文件名与标签文件名必须保持一致的关键要求。至此,数据已准备就绪,可用于后续的模型训练。
课程P9:ROI区域操作与通道分离 🖼️

在本节课中,我们将学习OpenCV中两个核心的图像处理操作:ROI(感兴趣区域)截取和BGR颜色通道的分离与合并。这些操作是后续进行更复杂图像处理和分析的基础。


上一节我们介绍了图像的基本读取与显示,本节中我们来看看如何从图像中提取特定的部分。

ROI(感兴趣区域)截取
ROI是“Region of Interest”的缩写,意为感兴趣区域。它指的是从一张完整的图像中,截取出我们想要观察或处理的特定部分。
例如,给定一张小猫的图像,我们可能只对图像中间的某个区域感兴趣,而不是整张图。
以下是实现ROI截取的步骤:
- 首先,将图像数据读入。在OpenCV中,图像数据本质上是一个多维数组(NumPy数组)。
- 利用数组的切片(Slicing)操作,可以指定我们想要截取的区域范围。
具体操作如下面的代码所示:
# 假设 `img` 是已经读取的图像
roi = img[50:200, 50:200] # 截取纵坐标50到200,横坐标50到200的区域
在这行代码中,img[50:200, 50:200] 就是一个切片操作。它从原始图像中截取了一个高度为150像素(200-50)、宽度为150像素的矩形区域。这样,我们就得到了一个只包含感兴趣部分的新图像 roi。

上一节我们学会了如何截取图像的局部区域,本节中我们来看看如何对彩色图像的各个颜色通道进行操作。
BGR通道的分离与合并

在OpenCV中,彩色图像默认以BGR(蓝、绿、红)顺序存储。有时我们需要分别分析或处理这三个颜色通道。
通道分离
我们可以使用 cv2.split() 函数将一张彩色图像分离成独立的B、G、R三个通道。
b, g, r = cv2.split(img)
分离后得到的 b、g、r 分别是代表蓝色、绿色、红色通道的二维数组。它们的大小(shape)与原始图像的高度和宽度一致,只是失去了颜色维度。
通道合并
处理完单个通道后,我们可以使用 cv2.merge() 函数将它们重新合并成一张彩色图像。合并时需要按照B、G、R的顺序传入通道。

img_merged = cv2.merge([b, g, r])
合并后的图像 img_merged 将恢复为原始的三通道BGR格式。
查看单一通道效果
为了直观理解每个通道的作用,我们可以通过将其他两个通道的值置为零,来观察仅保留一个通道时的图像效果。

以下是分别仅保留R、G、B通道的代码:

# 仅保留红色通道
img_r = img.copy()
img_r[:, :, 0] = 0 # 将B通道置零
img_r[:, :, 1] = 0 # 将G通道置零
# R通道保持不变
# 仅保留绿色通道
img_g = img.copy()
img_g[:, :, 0] = 0 # 将B通道置零
img_g[:, :, 2] = 0 # 将R通道置零

# 仅保留蓝色通道
img_b = img.copy()
img_b[:, :, 1] = 0 # 将G通道置零
img_b[:, :, 2] = 0 # 将R通道置零

通过这种方式,我们可以看到:
- 仅保留R通道时,图像呈现偏红的色调。
- 仅保留G通道时,图像呈现偏绿的色调。
- 仅保留B通道时,图像呈现偏蓝的色调。
这三个单通道图像叠加在一起,才构成了我们看到的完整彩色图像。


本节课中我们一起学习了两个重要的图像基础操作。首先,我们掌握了如何使用数组切片来截取图像的ROI区域,这能帮助我们将注意力集中在关键部分。接着,我们深入了解了彩色图像的BGR通道结构,学会了如何分离、合并通道,以及如何可视化单个通道的效果。这些技能是进行图像处理、特征提取等高级任务的基石。
课程P90:7-训练代码与参数配置更改 🛠️
在本节课中,我们将学习如何修改YOLOv5项目的训练代码与参数配置文件,以适配我们自定义的数据集。我们将一步步讲解需要修改的关键位置,确保模型能够正确识别我们自己的类别。
概述
我们将从修改配置文件中的类别名称开始,然后处理训练和验证数据集的路径文件,最后调整训练脚本中的关键参数。整个过程旨在将通用模型框架应用于我们的特定任务。
第一步:修改类别名称文件
上一节我们准备好了数据集,本节中我们来看看如何让模型认识我们的数据类别。首先需要修改 class.names 文件,该文件定义了模型需要识别的所有类别名称。
以下是具体操作步骤:
- 打开项目中的
class.names文件。 - 删除文件中所有与本任务无关的默认类别名称。
- 按照顺序,逐行写入我们自定义的类别名称。例如,本任务有两个类别:
person和吊钩。 - 注意,在写完最后一个类别后,需要额外按一次回车键,确保光标移动到新的一行,以避免文件读取错误。
- 保存文件。
核心概念:类别名称的顺序必须与数据标注文件中构建的类别ID字典顺序完全一致。例如,在标注字典中 0 对应 person,那么在 class.names 文件中,第一行也必须是 person。
第二步:配置数据路径文件
接下来,我们需要创建或修改训练集和验证集的路径文件,即 train.txt 和 validation.txt。这两个文件包含了所有用于训练和验证的图片的绝对路径。
以下是具体操作步骤:
- 创建
train.txt:遍历你的训练集图片文件夹(例如data/custom/images/train/),将每张图片的完整文件路径逐行写入train.txt。 - 创建
validation.txt:用同样的方法,为验证集图片创建路径文件。 - 你可以编写一个简单的脚本来自动完成此过程,也可以手动复制粘贴路径。对于初学者,建议先手动操作以理解流程。
- 确保文件中的路径在你的计算机上是有效的。如果你的项目目录结构不同,需要相应调整路径。
代码示例(生成路径文件的Python脚本思路):
import os
# 假设图片存放在此文件夹
image_dir = ‘data/custom/images/train/‘
# 获取所有图片文件的路径
image_paths = [os.path.join(image_dir, img) for img in os.listdir(image_dir) if img.endswith(('.jpg', '.png'))]
# 写入 train.txt
with open(‘train.txt’, ‘w’) as f:
for path in image_paths:
f.write(path + ‘\n’)

第三步:修改配置文件中的数据集设置

现在,我们需要修改YOLOv5的配置文件,以指向我们刚刚设置好的数据和类别。关键文件是 custom.data(或类似名称的配置文件)。
以下是具体操作步骤:
- 在项目的
config目录下找到custom.data文件并打开。 - 修改其中的关键参数:
classes:将其值改为我们自定义的类别数量,例如2。train:指向我们创建的train.txt文件的路径。val:指向我们创建的validation.txt文件的路径。names:指向我们修改好的class.names文件的路径。
- 保存修改。
第四步:调整训练脚本参数
最后,我们进入训练代码 train.py,对启动训练所需的参数进行配置。这是启动训练前的最后一步。
以下是 train.py 中需要关注和修改的主要参数:
--model:指定我们自定义的模型配置文件路径(即上一步修改的custom.yaml文件路径)。例如:--model ./config/custom.yaml。--data:指定我们修改好的数据配置文件路径。例如:--data ./config/custom.data。--pretrained:是否加载预训练权重。强烈建议设置为True,以便在预训练模型的基础上进行微调(迁移学习),这对于数据量较小的任务至关重要。除非你有数千张以上的图片,否则不要从头开始训练。--epochs:训练的总轮数。可以根据任务复杂度调整,例如设为100。--batch-size:每次输入模型的图片数量。根据你的显卡显存大小调整,显存小则设置较小的值,如2或4。--save-period:每隔多少轮保存一次模型权重。例如设为50,则会在第50轮和第100轮结束时保存模型。--device:指定训练设备,如0代表使用第一块GPU。
参数配置示例:
python train.py --model ./config/custom.yaml --data ./config/custom.data --pretrained --epochs 100 --batch-size 2 --save-period 50 --device 0
完成这些参数的设置后,运行 train.py 脚本即可开始针对自定义数据集的模型训练。
总结

本节课中我们一起学习了将YOLOv5应用于自定义数据集的关键配置步骤。我们首先修改了 class.names 文件来定义新类别,然后创建了 train.txt 和 validation.txt 来指定数据路径,接着在 custom.data 配置文件中关联了这些设置,最后在 train.py 训练脚本中配置了模型、数据、预训练权重等核心参数。遵循这些步骤,你就可以成功地用自己的数据训练目标检测模型了。

课程P91:8-训练模型并测试效果 🚀
在本节课中,我们将学习如何训练一个YOLOv3模型,并使用训练好的模型对图像进行目标检测预测。我们将涵盖从配置训练参数、运行训练过程到执行预测并查看结果的完整流程。
训练模型
上一节我们介绍了数据准备和配置文件,本节中我们来看看如何启动模型训练。
将参数配置好后,直接运行训练脚本即可。运行过程中,程序会打印出每一个epoch的损失值。由于我们提供的数据量较少,训练过程看起来会很快。
实际上,当数据量较大时,一个epoch的运行时间可能会很长。因此,不要误以为所有训练任务都很快,这只是因为当前数据量小,检测速度快。
以下是训练过程中的关键观察点:
- 当前损失值会持续下降,我们主要关注最终的损失值结果。
- 训练过程大约会运行100个epoch。
- 训练完成后,会得到一个最终的模型。
训练结束后,我们将实际查看模型在行人检测任务上的效果。我们仅使用了六张图像进行训练,需要验证模型是否能完成检测任务。
额外注意事项
在训练前,有一个重要的操作步骤需要强调。
当执行 create_model.sh 脚本时,不能重复执行。每次执行前,需要先删除旧的配置文件。
以下是具体操作步骤:
- 先删除现有的配置文件(例如
cfg文件)。 - 然后再执行
create_model.sh脚本。
如果不这样做,新配置会在旧配置基础上叠加,导致模型配置文件混乱。因此,在使用 create_model.sh 时,务必先清理旧的 cfg 文件。
加载训练好的模型
当模型训练完成后,在项目目录中会有一个 train_weights 文件夹用于保存模型权重。
在训练代码 train.py 的最后部分,设置了每50个epoch将模型保存到 train_weights 中。这个保存路径可以根据需要自行修改。
我们可以在 train_weights 文件夹中找到训练好的模型,例如 yolov3_custom_100,这就是我们训练了100个epoch的最终模型。
接下来进行预测时,我们将使用这个训练好的模型。
执行预测操作
现在,我们进入预测阶段,使用训练好的模型对图像进行检测。
我们打开 detect.py 文件,首先需要配置预测所需的参数。
以下是预测操作的核心参数配置:
- 预测文件路径:将所有需要预测的图像文件放在一个文件夹中。例如,我们将训练集中的图像放入
data/samples目录。 - 模型权重路径:指定训练好的模型文件路径,例如
train_weights/yolov3_custom_100。 - 类别名称文件:用于在预测框上显示物体类别名称(如“person”),将模型输出的索引值转换为实际类别名。
参数配置完成后,直接运行 detect.py 脚本。第一次执行时速度可能较慢,后续会变快。
程序开始检测,完成后会得到结果。预测结果图像保存在 output 文件夹中。
我们查看 output 文件夹,里面保存了所有带检测框的结果图像。可以看到,模型成功检测出了图像中标注的行人。
需要说明的是,我们直接使用训练集进行预测属于“作弊”行为,因为数据量太少,模型在未见过的测试集上表现可能不佳。但在训练集上,它展示了良好的检测效果。
这个示例演示了完整的流程。建议大家在实际操作时,先从少量数据开始尝试,熟悉整个流程后,再应用到大型实际项目中。
总结
本节课中我们一起学习了YOLOv3模型训练与测试的完整流程。我们从配置参数、运行训练、注意配置清理,到加载训练好的模型并进行图像预测,逐步完成了目标检测任务。建议大家在课后亲自动手实践一遍,这是学习和熟练掌握的关键步骤,这些配置工作在实际项目中是常见任务。

整个YOLO系列课程到此结束。


浙公网安备 33010602011771号