第三部分:特征提取与目标检测

像边缘、角点、特定的纹理模式等都是图像的特征。提取这些特征是许多计算机视觉任务的关键第一步,例如图像匹配、对象识别、图像拼接等。目标检测则是在图像中找到特定对象(如人脸、汽车等)的位置。

本部分将涵盖以下关键主题:

  1. 特征点提取
    • 角点检测(Harris 角点)
    • SIFT/SURF 特征(概念介绍)
    • ORB 特征提取与匹配
  2. 轮廓检测与分析
    • 轮廓查找
    • 轮廓属性(面积、周长、形状)
    • 实战:简单形状识别
  3. 目标检测入门
    • 模板匹配
    • 级联分类器
    • 实战:人脸检测

我们将提供详细的解释和代码示例。公式部分将使用 LaTeX 格式方便复制和学习。


OpenCV 特征提取与目标检测 (第三部分)

欢迎来到 OpenCV 教程的第三部分!在前两部分,我们学习了图像的基础操作、像素访问以及基本的图像增强和滤波技术。现在,我们将提升一个层次,学习如何从图像中提取更抽象、更有意义的信息:特征。

为什么需要特征?

想象一下,你要识别一张照片里的某个人。你不会逐个比较两个图像中所有像素点是否完全一致,因为光照、角度、距离等变化都会导致像素值改变。你会寻找这个人的眼睛、鼻子、嘴巴等关键部位的形状、相对位置等信息,这些就是特征

特征提取就是寻找这些有区分度、相对稳定(对光照、旋转、缩放不敏感)的图像区域或点。提取到特征后,我们可以用它们来:

  • 匹配不同图像中的同一个物体或场景(如图像拼接、三维重建)。
  • 识别图像中的特定对象(如人脸、汽车)。
  • 跟踪视频中移动的物体。
  • 分析图像的结构和内容。

让我们开始学习这些强大的技术!

1. 特征点提取

特征点是图像中具有独特性、易于识别和跟踪的点,例如角点、图像纹理变化剧烈的点等。

1.1 角点检测 (Corner Detection)

角点是两条或多条边相交的点,它在各个方向上的像素强度变化都非常明显。因此,角点是图像中非常重要的特征点。

Harris 角点检测

Harris 角点检测是一种经典的角点检测算法。其基本思想是:如果在图像中的一个窗口沿任何方向移动,窗口内的像素灰度都发生显著变化,那么这个窗口所在的区域可能是一个角点。

OpenCV 提供了 cv2.cornerHarris() 函数来实现 Harris 角点检测。

Python

复制代码
import cv2
import numpy as np

# --- 练习 1.1: Harris 角点检测 ---

# 1. 加载图像并转换为灰度图 (角点检测通常在灰度图上进行)
# Harris 角点检测的输入图像需要是 float32 类型
image_path = 'your_image.jpg'
try:
    image_color = cv2.imread(image_path)
    if image_color is None:
        raise FileNotFoundError(f"图片文件未找到: {image_path}")
    image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)
except FileNotFoundError as e:
    print(e)
    print("请确保你的图片文件存在并位于正确路径。")
    image_color = np.zeros((400, 600, 3), dtype=np.uint8)
    cv2.putText(image_color, "Placeholder Image", (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)

# 将灰度图像转换为 float32
image_float = np.float32(image)

# 2. 进行 Harris 角点检测
# 参数: src (输入图像, float32), blockSize (用于角点检测的邻域大小),
#       ksize (Sobel算子的大小,用于计算梯度), k (Harris检测器参数,通常在0.04-0.06)
harris_corners = cv2.cornerHarris(image_float, blockSize=2, ksize=3, k=0.04)

# 3. 标记检测到的角点
# 结果 harris_corners 是一个浮点图像,值越大表示越可能是角点
# 我们对其进行阈值处理,并将角点位置在原始彩色图像上用圆圈标记出来
# 阈值可以根据 harris_corners 的最大值来设置,例如最大值的 1%
threshold = 0.01 * harris_corners.max()

# 复制原始彩色图像,用于标记
output_image = image_color.copy()

# 遍历结果图像,在R值大于阈值的位置绘制圆圈
# 注意: harris_corners 的维度与输入图像相同
# np.where(condition) 会返回满足条件的元素的索引 (y_coords, x_coords)
strong_corners_indices = np.where(harris_corners > threshold)

# strong_corners_indices 是一个元组,第一个元素是所有满足条件的行的索引,第二个是所有满足条件的列的索引
y_coords, x_coords = strong_corners_indices

# 在原彩色图上绘制圆圈
for i in range(len(x_coords)):
    center = (x_coords[i], y_coords[i])
    cv2.circle(output_image, center, 5, (0, 255, 0), 2) # 绿色圆圈,半径5,线宽2

# 4. 显示结果
cv2.imshow('Original Image', image_color)
cv2.imshow('Harris Corners', output_image)

cv2.waitKey(0)
cv2.destroyAllWindows()

print("\n--- 练习 1.1 完成 ---")

练习提示:

  • 尝试调整 blockSizeksizek 参数,观察检测到的角点数量和位置的变化。
  • 调整阈值 (threshold) 也会显著影响结果。较高的阈值会检测到更少的、更明显的角点。
1.2 SIFT/SURF 特征 (概念介绍)

SIFT (Scale-Invariant Feature Transform)SURF (Speeded Up Robust Features) 是两种非常著名的、对图像缩放和旋转具有不变性的局部特征点检测和描述算法。

  • 它们都能在图像中找到具有独特性的特征点(称为关键点)。
  • 对于每个关键点,它们都会计算一个描述符(一串数字),用来描述关键点周围区域的纹理信息。这个描述符对于光照、视角、缩放、旋转都有一定的鲁棒性。
  • SIFT 和 SURF 算法相对复杂且计算量较大。

重要提示: SIFT 和 SURF 算法在某些国家是受专利保护的。OpenCV 的主模块 (opencv-python) 可能不包含这些算法。你可能需要安装 opencv-contrib-python 库来使用它们。但是,由于专利问题和后续出现的更优秀的免费算法,SIFT 和 SURF 在学术界和工业界的使用受到一定限制。因此,本教程仅概念性介绍它们,我们将把重点放在一个免费且高效的替代品:ORB 特征。

1.3 ORB 特征提取与匹配

ORB (Oriented FAST and Rotated BRIEF) 是一种高效且免费的特征点检测和描述算法。它结合了 FAST 算法来检测关键点,并对 BRIEF 描述符进行了改进,使其具有方向和一定的尺度不变性。

ORB 的核心是两个主要部分:

  1. 检测器: 使用改进的 FAST 算法来寻找关键点。FAST 算法速度非常快,通过比较中心像素与周围圆环像素的灰度值来判断是否为关键点。ORB 在此基础上增加了多尺度检测和方向信息。
  2. 描述符: 使用改进的 BRIEF 描述符来描述关键点周围的区域。BRIEF 描述符通过比较关键点周围随机选取的像素对的灰度值来生成一个二进制串。ORB 对 BRIEF 进行了旋转使其具有旋转不变性。

特征匹配

提取到图像的特征点及其描述符后,我们可以进行特征匹配。特征匹配的目的是在两幅图像中找到对应于同一物理点的特征点对。常用的匹配方法是比较描述符之间的相似度(例如,对于 ORB 的二进制描述符,可以使用汉明距离)。

BFMatcher (Brute-Force Matcher)

BFMatcher 是一种简单的特征匹配器。它对第一组描述符中的每个描述符,计算与第二组描述符中所有描述符的距离,然后找到最近的一个或几个。

通常,我们会使用 KNN (K-Nearest Neighbors) 匹配,找到每个描述符的 k 个最近邻,然后应用 Lowe's Ratio Test 进行过滤:如果最佳匹配的距离与次佳匹配的距离之比小于一个阈值(例如 0.75),则认为这是一个好的匹配点。

Python

复制代码
import cv2
import numpy as np

# --- 练习 1.3: ORB 特征提取与匹配 ---

# 1. 加载两张图像 (最好包含一些相同的物体或场景)
# 请替换成你自己的图片路径
image_path1 = 'your_image1.jpg'
image_path2 = 'your_image2.jpg'
try:
    image1 = cv2.imread(image_path1, cv2.IMREAD_GRAYSCALE) # ORB通常在灰度图上工作
    image2 = cv2.imread(image_path2, cv2.IMREAD_GRAYSCALE)
    if image1 is None:
        raise FileNotFoundError(f"图片文件未找到: {image_path1}")
    if image2 is None:
        raise FileNotFoundError(f"图片文件未找到: {image_path2}")
    print("成功加载两张图像。")
except FileNotFoundError as e:
    print(e)
    print("请确保你的图片文件存在并位于正确路径。")
    print("将使用模拟图像代替,无法演示实际匹配效果。")
    # 如果图片加载失败,创建两个简单的模拟图像
    image1 = np.zeros((300, 400), dtype=np.uint8)
    cv2.putText(image1, "Image 1 Placeholder", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    image2 = np.zeros((300, 400), dtype=np.uint8)
    cv2.putText(image2, "Image 2 Placeholder", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)


# 2. 创建 ORB 特征检测器对象
orb = cv2.ORB_create(
    nfeatures=5000, # 最多检测的特征点数量
    scaleFactor=1.2, # 图像金字塔的缩放因子
    nlevels=8, # 图像金字塔的层数
    edgeThreshold=31, # 忽略边缘区域的像素数量
    firstLevel=0, # 金字塔的第一层
    WTA_K=2, # 描述符生成算法
    scoreType=cv2.ORB_HARRIS_SCORE, # 使用Harris角点响应或FAST角点响应
    patchSize=31 # 描述符计算的区域大小
)

# 3. 检测关键点和计算描述符
# kp: 关键点列表
# des: 描述符 (numpy数组)
kp1, des1 = orb.detectAndCompute(image1, None)
kp2, des2 = orb.detectAndCompute(image2, None)

# 确保描述符是 float32 类型,有些匹配器可能需要
# ORB 描述符是二进制的,这里不需要转换,但对于SIFT/SURF可能需要

print(f"图像1检测到 {len(kp1)} 个关键点")
print(f"图像2检测到 {len(kp2)} 个关键点")

# 4. 创建 Brute-Force 匹配器
# 对于ORB的二进制描述符,使用 cv2.NORM_HAMMING
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False) # crossCheck=True 会进行双向匹配过滤

# 5. 进行特征匹配 (使用 KNN 匹配)
# k=2 表示为每个关键点找到 2 个最近邻
matches = bf.knnMatch(des1, des2, k=2)

# 6. 应用 Lowe's Ratio Test 过滤好的匹配点
good_matches = []
ratio_threshold = 0.75 # 比例阈值

for m, n in matches:
    # m是最佳匹配,n是次佳匹配
    if m.distance < ratio_threshold * n.distance:
        good_matches.append(m)

print(f"经过 Lowe's Ratio Test 过滤后,找到 {len(good_matches)} 个好的匹配点")

# 7. 绘制匹配结果
# 参数: img1, kp1, img2, kp2, matches, outImg, flags
# flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS 表示不绘制没有匹配点的关键点
match_image = cv2.drawMatchesKnn(image1, kp1, image2, kp2, [good_matches], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)

# 8. 显示结果
cv2.imshow('ORB Feature Matches', match_image)

cv2.waitKey(0)
cv2.destroyAllWindows()

print("\n--- 练习 1.3 完成 ---")

练习提示:

  • 尝试使用包含相同物体但拍摄角度、距离略有差异的两张图片。
  • 调整 ORB_create() 函数中的参数,例如 nfeatures,观察检测到的关键点数量和匹配结果的变化。
  • 调整 Lowe's Ratio Test 中的 ratio_threshold,较低的阈值会得到更严格(更少但更可靠)的匹配点。

2. 轮廓检测与分析

轮廓是连接所有连续的、具有相同颜色或亮度的点的曲线。它们代表了物体的边界。轮廓在物体检测、形状分析和识别、图像分割等领域非常有用。

查找轮廓通常在二值图像(只有黑白两种颜色)上进行。

2.1 轮廓查找

OpenCV 提供了 cv2.findContours() 函数来查找图像中的轮廓。

Python

复制代码
import cv2
import numpy as np

# --- 练习 2.1: 轮廓查找 ---

# 1. 加载图像并转换为灰度图
image_path = 'your_image.jpg'
try:
    image_color = cv2.imread(image_path)
    if image_color is None:
        raise FileNotFoundError(f"图片文件未找到: {image_path}")
    image_gray = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)
except FileNotFoundError as e:
    print(e)
    print("请确保你的图片文件存在并位于正确路径。")
    image_color = np.zeros((400, 600, 3), dtype=np.uint8)
    cv2.putText(image_color, "Placeholder Image", (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    image_gray = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)


# 2. 对灰度图进行二值化处理 (阈值分割)
# findContours 函数通常在二值图像上操作
# cv2.THRESH_BINARY: 大于阈值的设为最大值 (255), 否则设为0
# cv2.THRESH_OTSU: 使用Otsu算法自动寻找最佳阈值
ret, binary_image = cv2.threshold(image_gray, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# 3. 查找轮廓
# cv2.findContours() 函数会返回轮廓列表和对应的层级关系
# 参数: image (输入图像, 8位单通道,通常是二值图), mode (轮廓检索模式), method (轮廓近似方法)
# RETR_TREE: 检索所有轮廓并建立完整的层级树
# CHAIN_APPROX_SIMPLE: 压缩水平、垂直和对角线段,只保留端点
# 注意: OpenCV 4.x 版本 findContours 返回值顺序变了,前面是image,后面是轮廓和层级
contours, hierarchy = cv2.findContours(binary_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

print(f"检测到 {len(contours)} 个轮廓")

# 4. 在原始彩色图像上绘制所有轮廓
# 参数: image (绘制目标图像), contours (轮廓列表), contourIdx (要绘制的轮廓索引,-1表示所有),
#       color (颜色), thickness (线宽)
# 复制原始彩色图像,以免修改原图
image_with_contours = image_color.copy()
cv2.drawContours(image_with_contours, contours, -1, (0, 255, 0), 2) # 绿色,线宽2

# 5. 显示结果
cv2.imshow('Original Image', image_color)
cv2.imshow('Binary Image', binary_image)
cv2.imshow('Contours', image_with_contours)

cv2.waitKey(0)
cv2.destroyAllWindows()

print("\n--- 练习 2.1 完成 ---")

练习提示:

  • cv2.findContours() 函数会改变输入图像,所以在查找轮廓时通常传入二值图像的副本或查找后不再使用该二值图像。
  • 尝试不同的 mode (如 cv2.RETR_LIST 不建立层级关系) 和 method (如 cv2.CHAIN_APPROX_NONE 存储所有轮廓点) 参数,观察它们对结果的影响。
2.2 轮廓属性

一旦找到轮廓,我们可以计算它们的各种属性来描述轮廓的形状、大小、位置等。

Python

复制代码
import cv2
import numpy as np

# --- 练习 2.2: 轮廓属性 ---

# (沿用上一个练习的轮廓查找代码)
image_path = 'your_image.jpg'
try:
    image_color = cv2.imread(image_path)
    if image_color is None:
        raise FileNotFoundError(f"图片文件未找到: {image_path}")
    image_gray = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)
except FileNotFoundError as e:
    print(e)
    print("请确保你的图片文件存在并位于正确路径。")
    image_color = np.zeros((400, 600, 3), dtype=np.uint8)
    cv2.putText(image_color, "Placeholder Image", (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    image_gray = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)

ret, binary_image = cv2.threshold(image_gray, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# 注意: findContours 修改了 binary_image,所以通常传递副本
contours, hierarchy = cv2.findContours(binary_image.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 复制原始彩色图像用于可视化属性
image_with_properties = image_color.copy()

print("\n--- 轮廓属性 ---")
# 遍历所有找到的轮廓
for i, contour in enumerate(contours):
    # 忽略非常小的轮廓(可能是噪声)
    if cv2.contourArea(contour) < 100:
        continue

    print(f"\n轮廓 {i+1}:")

    # 面积 (Area)
    area = cv2.contourArea(contour)
    print(f"  面积: {area:.2f}")

    # 周长 (Perimeter / Arc Length)
    # 参数: curve (轮廓), closed (是否闭合)
    perimeter = cv2.arcLength(contour, closed=True)
    print(f"  周长: {perimeter:.2f}")

    # 矩 (Moments) - 用于计算中心、面积等
    # M['m00'] 就是面积
    M = cv2.moments(contour)
    # 计算中心点 (Centroid)
    if M['m00'] != 0:
        cx = int(M['m10'] / M['m00'])
        cy = int(M['m01'] / M['m00'])
        print(f"  中心点 (cx, cy): ({cx}, {cy})")
        # 在中心点绘制圆圈
        cv2.circle(image_with_properties, (cx, cy), 5, (0, 0, 255), -1) # 红色实心圆

    # 外接矩形 (Bounding Rectangle) - 直立矩形
    # 参数: points (轮廓点)
    x, y, w, h = cv2.boundingRect(contour)
    print(f"  外接矩形 (x, y, w, h): ({x}, {y}, {w}, {h})")
    # 绘制外接矩形
    cv2.rectangle(image_with_properties, (x, y), (x+w, y+h), (255, 0, 0), 2) # 蓝色

    # 最小外接圆 (Minimum Enclosing Circle)
    # 参数: points (轮廓点)
    (center_x, center_y), radius = cv2.minEnclosingCircle(contour)
    center_circle = (int(center_x), int(center_y))
    radius_int = int(radius)
    print(f"  最小外接圆 (中心: {center_circle}, 半径: {radius_int})")
    # 绘制最小外接圆
    cv2.circle(image_with_properties, center_circle, radius_int, (0, 255, 255), 2) # 黄色

    # 轮廓近似 (Contour Approximation) - 简化轮廓
    # 使用 Ramer-Douglas-Peucker 算法
    # 参数: curve (轮廓), epsilon (近似精度,原始轮廓与近似轮廓之间的最大距离), closed (是否闭合)
    epsilon = 0.02 * perimeter # 精度通常设为周长的百分比
    approx = cv2.approxPolyDP(contour, epsilon, closed=True)
    print(f"  近似轮廓点数量: {len(approx)}")
    # 绘制近似轮廓 (可选,可以用 drawContours)

# 6. 显示结果
cv2.imshow('Original Image', image_color)
cv2.imshow('Contours with Properties', image_with_properties)

cv2.waitKey(0)
cv2.destroyAllWindows()

print("\n--- 练习 2.2 完成 ---")

练习提示:

  • 理解不同属性的含义及其计算方法。
  • 注意如何使用这些属性来描述轮廓的特征。例如,可以通过比较轮廓面积和其外接矩形面积的比例来判断轮廓的"紧凑"程度。
2.3 实战:简单形状识别

结合轮廓查找和轮廓属性,我们可以尝试识别一些简单的几何形状,如矩形、圆形。

思路:


3. 目标检测入门

目标检测比简单的形状识别更进一步,它旨在图像中找到特定类别的对象(如人脸、汽车、猫等)的位置,并通常用一个边界框标记出来。

3.1 模板匹配 (Template Matching)

模板匹配是一种简单直观的目标检测方法。它需要一个已知的模板图像 (你要寻找的对象的小图像),然后在源图像中滑动这个模板,计算模板与源图像中对应区域的相似度。相似度最高的区域就被认为是找到了模板。

3.2 级联分类器 (Cascade Classifiers)

级联分类器是一种基于机器学习的目标检测方法。它使用一系列经过训练的分类器(弱分类器)组成一个级联结构。在检测时,图像区域会依次通过这些分类器。如果在任何一个阶段被判定为非目标,就会被立即丢弃,大大提高了检测速度。只有通过所有分类器的区域才会被认为是目标。

OpenCV 提供了一个使用 Haar-like 特征(类似于简单的边缘、线条特征)训练的级联分类器框架。尽管现代深度学习方法在许多目标检测任务上取得了更好的效果,但级联分类器(尤其是用于人脸检测)因为其速度快、实时性好,在一些场景下仍然有应用价值。

使用级联分类器进行目标检测需要:

Python

复制代码
import cv2
import numpy as np

# --- 练习 3.2: 级联分类器入门 ---

# 1. 加载预训练的级联分类器模型
# 你需要找到 OpenCV 安装目录下的 haarcascades 文件夹,或者从网上下载
# 例如: '/usr/local/share/opencv4/haarcascades/haarcascade_frontalface_default.xml'
# 或者在项目目录下放置下载好的xml文件
cascade_path = 'haarcascade_frontalface_default.xml' # 替换成你的xml文件路径

# 创建级联分类器对象
face_cascade = cv2.CascadeClassifier()

# 加载分类器模型
if not face_cascade.load(cv2.samples.findFile(cascade_path)):
    print(f"错误: 无法加载级联分类器文件: {cascade_path}")
    print("请检查文件路径是否正确,或从以下位置下载:")
    print("https://github.com/opencv/opencv/tree/master/data/haarcascades")
    # 如果加载失败,创建一个空的分类器,后续检测会失败
    face_cascade = None

# 2. 加载图像并转换为灰度图 (级联分类器通常在灰度图上工作)
image_path = 'your_image_with_faces.jpg' # 最好找一张包含人脸的图片
try:
    image_color = cv2.imread(image_path)
    if image_color is None:
         raise FileNotFoundError(f"图片文件未找到: {image_path}")
    print(f"成功加载图像: {image_path}")
    image_gray = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)
    image_gray = cv2.equalizeHist(image_gray) # 可选: 直方图均衡化,有时能提高检测效果

except FileNotFoundError as e:
    print(e)
    print("请确保你的图片文件存在并位于正确路径。")
    print("将使用模拟图像代替,无法演示实际检测效果。")
    # 创建模拟图像,无法包含真实人脸
    image_color = np.zeros((400, 600, 3), dtype=np.uint8)
    cv2.putText(image_color, "Placeholder Image", (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    image_gray = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)


# 3. 进行目标检测 (人脸检测)
# 参数: image (灰度图), scaleFactor (图像缩放因子), minNeighbors (每个矩形应该具有的邻近个数),
#       minSize (最小可能对象大小), maxSize (最大可能对象大小)
if face_cascade: # 确保分类器加载成功
    faces = face_cascade.detectMultiScale(
        image_gray,
        scaleFactor=1.1, # 每次图像尺寸减小的比例
        minNeighbors=5, # 每个目标至少被检测到5次才被确认为目标
        minSize=(30, 30) # 最小目标尺寸 (宽, 高)
        # maxSize=(200, 200) # 最大目标尺寸
    )
else:
    faces = [] # 如果分类器加载失败,检测结果为空列表

print(f"检测到 {len(faces)} 个人脸")

# 4. 在原始彩色图像上绘制检测到的目标 (矩形框)
output_image = image_color.copy()
for (x, y, w, h) in faces:
    cv2.rectangle(output_image, (x, y), (x+w, y+h), (255, 0, 0), 2) # 蓝色矩形框

# 5. 显示结果
cv2.imshow('Original Image', image_color)
cv2.imshow('Detected Faces', output_image)

cv2.waitKey(0)
cv2.destroyAllWindows()

print("\n--- 练习 3.2 完成 ---")

练习提示:

3.3 实战:人脸检测

这个实战练习就是将上一节学习的级联分类器应用到人脸检测任务上。你只需要确保有包含人脸的图片以及正确加载人脸分类器的 XML 文件。

  1. 找到轮廓。

  2. 过滤掉过小或过大的轮廓。

  3. 对轮廓进行近似。

  4. 根据近似轮廓的顶点数量来判断形状:

    • 如果顶点数量接近 4 且是凸多边形,可能是矩形/正方形。
    • 如果顶点数量很多(近似后变化不大)且是凸多边形,可能是圆形。
  5. 进一步细化:对于顶点数为 4 的,可以检查长宽比来区分正方形和矩形。对于圆形,可以检查面积与最小外接圆面积的比例。Python

    复制代码
    import cv2
    import numpy as np
    
    # --- 实战练习: 简单形状识别 ---
    
    # 1. 加载图像并转换为灰度图
    image_path = 'your_image.jpg' # 最好找一张有明显几何图形的图片
    try:
        image_color = cv2.imread(image_path)
        if image_color is None:
            raise FileNotFoundError(f"图片文件未找到: {image_path}")
        image_gray = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)
    except FileNotFoundError as e:
        print(e)
        print("请确保你的图片文件存在并位于正确路径。")
        # 创建一个包含简单形状的模拟图像
        image_color = np.zeros((400, 400, 3), dtype=np.uint8)
        cv2.rectangle(image_color, (50, 50), (150, 150), (255, 0, 0), -1) # 蓝色方块
        cv2.circle(image_color, (300, 100), 50, (0, 255, 0), -1) # 绿色圆圈
        cv2.rectangle(image_color, (200, 200), (350, 300), (0, 0, 255), -1) # 红色矩形
        image_gray = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)
    
    
    # 2. 二值化图像
    ret, binary_image = cv2.threshold(image_gray, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    
    # 3. 查找轮廓 (注意使用副本)
    contours, hierarchy = cv2.findContours(binary_image.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    
    # 复制原始彩色图像用于绘制结果
    output_image = image_color.copy()
    
    print("\n--- 形状识别结果 ---")
    
    # 4. 遍历轮廓并进行识别
    for contour in contours:
        # 过滤小轮廓
        area = cv2.contourArea(contour)
        if area < 1000: # 过滤面积小于1000的轮廓
            continue
    
        # 计算周长
        perimeter = cv2.arcLength(contour, closed=True)
    
        # 轮廓近似
        epsilon = 0.04 * perimeter # 提高 epsilon 可以减少近似点数量,有助于识别多边形
        approx = cv2.approxPolyDP(contour, epsilon, closed=True)
    
        # 获取外接矩形属性
        x, y, w, h = cv2.boundingRect(approx)
        aspect_ratio = float(w) / h
        bbox_area = w * h
    
        # 识别形状
        shape = "未知"
        M = cv2.moments(contour)
        if M['m00'] != 0:
             cx = int(M['m10'] / M['m00'])
             cy = int(M['m01'] / M['m00'])
             center = (cx, cy)
        else:
             center = (x + w // 2, y + h // 2) # 使用外接矩形中心代替
    
        # 绘制轮廓并标注形状
        cv2.drawContours(output_image, [contour], -1, (0, 255, 0), 2) # 绘制当前轮廓
    
        num_vertices = len(approx)
    
        if num_vertices == 3:
            shape = "三角形"
        elif num_vertices == 4:
            # 判断是正方形还是矩形
            if 0.9 <= aspect_ratio <= 1.1 and 0.85 <= area / bbox_area <= 1.15: # 检查长宽比和面积比例
                 shape = "正方形"
            else:
                 shape = "矩形"
        elif num_vertices == 5:
            shape = "五边形"
        else:
            # 对于顶点数量多的轮廓,可能是圆形或其它复杂形状
            # 可以通过检查面积与最小外接圆面积的比例来判断是否接近圆形
            (center_circle_x, center_circle_y), radius = cv2.minEnclosingCircle(contour)
            circle_area = np.pi * radius**2
            if area / circle_area > 0.8: # 如果轮廓面积占最小外接圆面积的大部分
                shape = "圆形"
            else:
                 shape = "其他" # 或者 "复杂形状"
    
        print(f"  轮廓点数量: {len(contour)}, 近似点数量: {num_vertices}, 识别为: {shape}")
    
        # 在中心点附近显示形状名称
        cv2.putText(output_image, shape, (center[0] - 20, center[1]), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
    
    
    # 5. 显示结果
    cv2.imshow('Original Image', image_color)
    cv2.imshow('Simple Shape Recognition', output_image)
    
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    
    print("\n--- 实战练习: 简单形状识别 完成 ---")

    实战提示:

  6. 这个简单的形状识别方法对于理想的几何图形效果较好,对于真实照片中形状会受光照、遮挡、变形等影响,效果会下降。

  7. 调整 epsilon 参数会影响近似轮廓的顶点数量,从而影响识别结果。

  8. 圆形识别可以通过检查面积与最小外接圆面积的比例、或者周长与半径的关系等多种方法来实现。

  9. Python

    复制代码
    import cv2
    import numpy as np
    
    # --- 练习 3.1: 模板匹配 ---
    
    # 1. 加载源图像和模板图像
    # 请替换成你自己的图片路径,确保 template_image.jpg 是 source_image.jpg 的一部分
    source_image_path = 'your_source_image.jpg'
    template_image_path = 'your_template_image.jpg' # 模板图像要小于源图像
    
    try:
        source_image = cv2.imread(source_image_path)
        template_image = cv2.imread(template_image_path)
        if source_image is None:
             raise FileNotFoundError(f"源图片文件未找到: {source_image_path}")
        if template_image is None:
             raise FileNotFoundError(f"模板图片文件未找到: {template_image_path}")
        print("成功加载源图像和模板图像。")
    except FileNotFoundError as e:
        print(e)
        print("请确保你的图片文件存在并位于正确路径。")
        print("将使用模拟图像代替,无法演示实际匹配效果。")
        # 创建模拟图像
        source_image = np.zeros((300, 400, 3), dtype=np.uint8)
        cv2.circle(source_image, (200, 150), 50, (0, 255, 0), -1)
        cv2.putText(source_image, "Source (Green Circle)", (50, 250), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
        template_image = np.zeros((100, 100, 3), dtype=np.uint8)
        cv2.circle(template_image, (50, 50), 40, (0, 255, 0), -1)
        cv2.putText(template_image, "Template", (10, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
    
    
    # 确保两张图像都是单通道或三通道,且类型一致
    # 模板匹配也可以在灰度图上进行,通常更快
    source_gray = cv2.cvtColor(source_image, cv2.COLOR_BGR2GRAY)
    template_gray = cv2.cvtColor(template_image, cv2.COLOR_BGR2GRAY)
    
    # 获取模板图像的宽度和高度
    w, h = template_gray.shape[::-1] # shape返回(h, w),[::-1]反转得到(w, h)
    
    # 2. 进行模板匹配
    # 参数: image (源图像), templ (模板图像), method (匹配方法)
    # cv2.TM_CCOEFF_NORMED: 归一化相关系数匹配,值越接近1越好
    # cv2.TM_SQDIFF_NORMED: 归一化平方差匹配,值越接近0越好
    method = cv2.TM_CCOEFF_NORMED
    result = cv2.matchTemplate(source_gray, template_gray, method)
    
    # 3. 查找最佳匹配位置
    # cv2.minMaxLoc() 找到匹配结果矩阵中的最小值、最大值及其位置
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
    
    # 根据匹配方法,最佳匹配位置可能是 min_loc 或 max_loc
    # 对于 TM_CCOEFF_NORMED, TM_CCORR_NORMED, TM_CCOEFF,最大值表示最佳匹配
    # 对于 TM_SQDIFF, TM_SQDIFF_NORMED, TM_CCORR,最小值表示最佳匹配
    if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
        top_left = min_loc # 最佳匹配的左上角坐标
    else:
        top_left = max_loc
    
    # 计算最佳匹配区域的右下角坐标
    bottom_right = (top_left[0] + w, top_left[1] + h)
    
    # 4. 在源图像上绘制匹配结果 (矩形框)
    # 复制源图像以免修改原图
    output_image = source_image.copy()
    cv2.rectangle(output_image, top_left, bottom_right, (0, 0, 255), 2) # 红色矩形框
    
    # 5. 显示结果
    cv2.imshow('Source Image', source_image)
    cv2.imshow('Template Image', template_image)
    cv2.imshow('Template Matching Result', output_image)
    # 匹配结果矩阵通常数值范围较大,直接显示可能不直观,可以先归一化再显示
    # cv2.imshow('Match Result Matrix', cv2.normalize(result, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U))
    
    
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    
    print("\n--- 练习 3.1 完成 ---")

    练习提示:

  10. 模板匹配对模板图像的大小、旋转、光照变化非常敏感。如果模板和目标在源图像中的外观有较大差异,匹配可能会失败。

  11. 尝试不同的 method 参数,比较它们的效果。

  12. cv2.matchTemplate 也可以返回多个匹配区域(例如,使用阈值过滤匹配结果矩阵)。

  13. 一个预先训练好的分类器模型文件(通常是 XML 格式)。OpenCV 库自带了一些常用的分类器模型,如人脸、眼睛等。你可以在 OpenCV 的 GitHub 仓库中找到它们(opencv/data/haarcascadesopencv_extra/testdata/cv/face/)。

  14. 使用 cv2.CascadeClassifier 类加载模型。

  15. 使用 cascade.detectMultiScale() 方法在图像中进行检测。

  16. 找到并替换 cascade_path 为你环境中正确的 XML 文件路径。

  17. 尝试不同的 scaleFactorminNeighbors 参数。减小 scaleFactor 可以检测到更小的目标,但速度变慢;增大 minNeighbors 可以减少误检,但可能漏掉一些目标。

  18. 尝试使用不同的图像进行测试。

  19. 除了人脸,OpenCV 还提供了眼睛、身体等其他预训练的级联分类器。

  20. Python

    复制代码
    import cv2
    import numpy as np
    
    # --- 实战练习: 人脸检测 ---
    
    # 1. 指定人脸分类器模型文件路径
    # 这是 OpenCV 官方提供的模型,你需要确保你的OpenCV安装目录有这个文件,或者手动下载放在项目目录
    # 例如: '/usr/local/share/opencv4/haarcascades/haarcascade_frontalface_default.xml'
    # 或者直接放在当前脚本同一目录下
    # 你可以从这里下载: https://github.com/opencv/opencv/blob/master/data/haarcascades/haarcascade_frontalface_default.xml
    cascade_filename = 'haarcascade_frontalface_default.xml'
    
    # 创建人脸分类器对象
    face_cascade = cv2.CascadeClassifier()
    
    # 尝试加载模型文件
    if not face_cascade.load(cv2.samples.findFile(cascade_filename)):
        print(f"错误: 无法加载人脸分类器文件: {cascade_filename}")
        print("请确保文件存在,并可能需要手动下载或找到OpenCV安装路径下的haarcascades文件夹。")
        print("使用一个空的分类器,后续检测将不会找到任何目标。")
        face_cascade = None # 设置为 None 表示加载失败
    
    # 2. 加载你要检测人脸的图像
    image_path = 'your_image_to_detect_faces.jpg' # 请替换为你想要检测人脸的图片路径
    try:
        image_color = cv2.imread(image_path)
        if image_color is None:
            raise FileNotFoundError(f"图片文件未找到: {image_path}")
        print(f"成功加载图像: {image_path}")
    except FileNotFoundError as e:
        print(e)
        print("请确保你的图片文件存在并位于正确路径。")
        # 创建一个空白图像作为备用
        image_color = np.zeros((400, 600, 3), dtype=np.uint8)
        cv2.putText(image_color, "Placeholder Image", (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    
    
    # 3. 将图像转换为灰度图 (级联分类器工作在灰度图上)
    gray_image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)
    # 可以选择性地进行直方图均衡化以增强对比度
    # gray_image = cv2.equalizeHist(gray_image)
    
    
    # 4. 进行人脸检测
    # 使用 detectMultiScale 方法
    # 参数: scaleFactor, minNeighbors, minSize, maxSize (这些参数会影响检测精度和速度)
    if face_cascade: # 只有在分类器加载成功时才执行检测
        faces = face_cascade.detectMultiScale(
            gray_image,
            scaleFactor=1.1, # 图像缩小比例
            minNeighbors=5,  # 构成检测目标的最小邻近矩形数量
            minSize=(30, 30) # 最小检测窗口大小
        )
    else:
        faces = [] # 如果分类器加载失败,结果为空
    
    print(f"检测到 {len(faces)} 个人脸")
    
    # 5. 在原始图像上绘制检测到的人脸矩形框
    output_image = image_color.copy()
    # faces 变量是一个包含检测到的人脸矩形信息的列表,每个元素是 (x, y, w, h)
    for (x, y, w, h) in faces:
        cv2.rectangle(output_image, (x, y), (x+w, y+h), (0, 255, 0), 2) # 绿色矩形框
    
    # 6. 显示结果
    cv2.imshow('Original Image', image_color)
    cv2.imshow('Face Detection Result', output_image)
    
    # 7. 等待按键,然后关闭窗口
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    
    print("\n--- 实战练习: 人脸检测 完成 ---")

    实战提示:

  21. 最关键的一步是正确指定 cascade_filename 的路径。

  22. 尝试使用不同光照、角度、表情的人脸图片进行测试,观察检测效果。

  23. 调整 detectMultiScale 的参数,尤其是 scaleFactorminNeighbors,会显著影响检测结果。

相关推荐
诸葛务农36 分钟前
DeepSeek眼中的文明印记:金刚经
人工智能
AlexandrMisko2 小时前
从0搭建Transformer
人工智能·pytorch·transformer
搏博3 小时前
机器学习之五:基于解释的学习
人工智能·深度学习·学习·算法·机器学习
望获linux3 小时前
北京亦庄机器人马拉松:人机共跑背后的技术突破与产业启示
linux·人工智能·机器人·操作系统·开源软件·rtos·具身智能
Xiaoxiaoxiao02097 小时前
GAEA商业前景和生态系统扩展
人工智能·架构·web3·去中心化·区块链
乌恩大侠7 小时前
【东枫科技】AMD / Xilinx Alveo™ V80计算加速器卡
人工智能·科技·5g·nvidia·6g·usrp
LIUDAN'S WORLD7 小时前
OpenCV实战教程:从零开始的计算机视觉之旅
人工智能·opencv·计算机视觉
中科岩创7 小时前
某建筑石料用灰岩矿自动化监测
大数据·人工智能
shao9185167 小时前
Gradio全解20——Streaming:流式传输的多媒体应用(3)——实时语音识别技术
人工智能·ffmpeg·语音识别·transformers·gradio·asr
高效匠人8 小时前
Python10天冲刺-设计模型之策略模式
开发语言·人工智能·python·策略模式