跳转至

图像预处理

机器人相机采集的原始图像很少能直接用于感知算法。光照变化、传感器噪声、无关的背景细节以及不一致的图像尺寸都会降低目标检测、特征匹配和位姿估计等下游任务的性能。**图像预处理**将原始相机数据转换为更干净、更一致的表示,从而使上层算法能够可靠地运行。

本教程涵盖每位机器人专业学生都应掌握的核心预处理技术——从色彩空间转换和滤波,到形态学操作和完整流水线。


学习目标

完成本教程后,你将能够:

  • 在常见色彩空间(BGR、HSV、灰度)之间转换图像
  • 应用空间滤波(高斯、中值、双边)并理解其权衡
  • 使用直方图均衡化和 CLAHE 增强图像对比度
  • 使用形态学操作清理二值掩码
  • 应用几何变换(缩放、仿射、透视)
  • 为机器人视觉任务构建完整的预处理流水线

前置要求

要求 详情
Python 3.8+
opencv-pythonnumpymatplotlib
先修知识 Python 基础、NumPy 数组索引

如需安装依赖:

pip install opencv-python numpy matplotlib

1. 色彩空间

**色彩空间**定义了像素颜色的数值表示方式。为特定任务选择合适的色彩空间可以显著简化后续处理。

1.1 RGB / BGR

大多数库(PIL、matplotlib、scikit-image)使用 RGB 排序,但 OpenCV 默认使用 BGR。这一历史选择源于 OpenCV 早期开发时视频采集硬件常用的 BGR 字节序。

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 加载图像(以 BGR 顺序返回)
img_bgr = cv2.imread("robot_workspace.jpg")

# 将 BGR 转换为 RGB,以便 matplotlib 正确显示
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(img_bgr)          # 颜色错误——通道被交换
axes[0].set_title("原始 BGR(以 RGB 方式显示)")
axes[1].imshow(img_rgb)          # 颜色正确
axes[1].set_title("转换后的 RGB")
for ax in axes:
    ax.axis("off")
plt.tight_layout()
plt.show()

常见错误

在调用 plt.imshow() 之前忘记进行 BGR→RGB 转换是 OpenCV + matplotlib 工作流中最常见的新手错误。图像会呈现偏蓝/偏红的色调。

1.2 HSV / HSL

HSV(色相、饱和度、明度)将颜色(色相)与亮度(明度)分离,非常适合基于**颜色的分割**——例如,检测彩色球体或车道线标记,不受光照影响。

通道 范围(OpenCV) 含义
H(色相) 0–179 色轮上的角度
S(饱和度) 0–255 颜色纯度
V(明度) 0–255 亮度

交互式 HSV 滑条演示

import cv2
import numpy as np

def nothing(x):
    pass

img = cv2.imread("robot_workspace.jpg")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

cv2.namedWindow("Trackbars")
cv2.createTrackbar("L-H", "Trackbars", 0, 179, nothing)
cv2.createTrackbar("L-S", "Trackbars", 0, 255, nothing)
cv2.createTrackbar("L-V", "Trackbars", 0, 255, nothing)
cv2.createTrackbar("U-H", "Trackbars", 179, 179, nothing)
cv2.createTrackbar("U-S", "Trackbars", 255, 255, nothing)
cv2.createTrackbar("U-V", "Trackbars", 255, 255, nothing)

while True:
    l_h = cv2.getTrackbarPos("L-H", "Trackbars")
    l_s = cv2.getTrackbarPos("L-S", "Trackbars")
    l_v = cv2.getTrackbarPos("L-V", "Trackbars")
    u_h = cv2.getTrackbarPos("U-H", "Trackbars")
    u_s = cv2.getTrackbarPos("U-S", "Trackbars")
    u_v = cv2.getTrackbarPos("U-V", "Trackbars")

    lower = np.array([l_h, l_s, l_v])
    upper = np.array([u_h, u_s, u_v])
    mask = cv2.inRange(hsv, lower, upper)
    result = cv2.bitwise_and(img, img, mask=mask)

    cv2.imshow("Original", img)
    cv2.imshow("Mask", mask)
    cv2.imshow("Result", result)

    if cv2.waitKey(1) & 0xFF == 27:  # 按 ESC 退出
        break

cv2.destroyAllWindows()

静态 HSV 颜色检测示例

import cv2
import numpy as np
import matplotlib.pyplot as plt

img_bgr = cv2.imread("robot_workspace.jpg")
hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)

# 检测蓝色物体(OpenCV HSV 中蓝色的典型范围)
lower_blue = np.array([100, 50, 50])
upper_blue = np.array([130, 255, 255])
mask = cv2.inRange(hsv, lower_blue, upper_blue)
result = cv2.bitwise_and(img_bgr, img_bgr, mask=mask)

fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
axes[0].set_title("原始图像")
axes[1].imshow(mask, cmap="gray")
axes[1].set_title("蓝色掩码")
axes[2].imshow(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
axes[2].set_title("检测到的蓝色区域")
for ax in axes:
    ax.axis("off")
plt.tight_layout()
plt.show()

1.3 灰度转换

许多算法(边缘检测、模板匹配、特征提取)在单通道灰度图像上运行。转换为灰度可以减少 3 倍数据量并简化计算。

import cv2
import matplotlib.pyplot as plt

img_bgr = cv2.imread("robot_workspace.jpg")
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)

print(f"原始图像形状: {img_bgr.shape}")   # (H, W, 3)
print(f"灰度图像形状: {gray.shape}")     # (H, W)

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
axes[0].set_title("彩色 (RGB)")
axes[1].imshow(gray, cmap="gray")
axes[1].set_title("灰度")
for ax in axes:
    ax.axis("off")
plt.tight_layout()
plt.show()

2. 图像滤波

滤波(也称为*平滑*或*模糊*)用于抑制噪声和去除细节。滤波器的选择取决于噪声类型以及是否需要保留边缘。

2.1 高斯滤波

高斯滤波器使用钟形曲线(高斯)核将每个像素替换为其邻域的加权平均值。它对**高斯噪声**有效,是最常用的低通滤波器。

import cv2
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)

# 使用递增的核大小应用高斯模糊
blur_3 = cv2.GaussianBlur(img, (3, 3), 0)
blur_7 = cv2.GaussianBlur(img, (7, 7), 0)
blur_15 = cv2.GaussianBlur(img, (15, 15), 0)

fig, axes = plt.subplots(1, 4, figsize=(16, 4))
for ax, im, title in zip(axes,
    [img, blur_3, blur_7, blur_15],
    ["原始", "3×3", "7×7", "15×15"]):
    ax.imshow(im, cmap="gray")
    ax.set_title(f"高斯 {title}")
    ax.axis("off")
plt.tight_layout()
plt.show()

核大小规则

  • 必须是正奇数:3、5、7、……
  • 核越大 → 模糊越强,计算越慢
  • 第三个参数 0 表示让 OpenCV 根据核大小自动计算 σ

2.2 中值滤波

中值滤波器**将每个像素替换为其邻域的中值。它特别擅长去除**椒盐噪声(随机黑白像素),同时比高斯滤波器更好地保留边缘。

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)

# 模拟椒盐噪声
def add_salt_pepper(image, amount=0.02):
    noisy = image.copy()
    num_salt = int(amount * image.size / 2)
    # 盐噪声
    coords = tuple(np.random.randint(0, d, num_salt) for d in image.shape)
    noisy[coords] = 255
    # 胡椒噪声
    coords = tuple(np.random.randint(0, d, num_salt) for d in image.shape)
    noisy[coords] = 0
    return noisy

noisy = add_salt_pepper(img, amount=0.03)
median_3 = cv2.medianBlur(noisy, 3)
median_7 = cv2.medianBlur(noisy, 7)

fig, axes = plt.subplots(1, 3, figsize=(12, 4))
for ax, im, title in zip(axes,
    [noisy, median_3, median_7],
    ["含噪图像", "中值 3×3", "中值 7×7"]):
    ax.imshow(im, cmap="gray")
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.show()

2.3 双边滤波

双边滤波器**在平滑图像的同时**保留边缘。它在对邻域像素加权时同时考虑空间距离和强度差异——跨越边缘的像素贡献非常小。

import cv2
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)

# cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace)
bilateral = cv2.bilateralFilter(img, d=9, sigmaColor=75, sigmaSpace=75)
gaussian = cv2.GaussianBlur(img, (9, 9), 0)

fig, axes = plt.subplots(1, 3, figsize=(14, 4))
for ax, im, title in zip(axes,
    [img, gaussian, bilateral],
    ["原始", "高斯 9×9", "双边 d=9"]):
    ax.imshow(im, cmap="gray")
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.show()
参数 含义
d 像素邻域直径(使用 -1 可从 sigmaSpace 自动计算)
sigmaColor 值越大 → 邻域中混合的颜色越多
sigmaSpace 值越大 → 距离更远的像素相互影响越大

2.4 滤波器对比

滤波器 最佳用途 保留边缘? 速度
高斯 高斯/一般噪声
中值 椒盐噪声 一般
双边 保边平滑
import cv2
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)

filters = {
    "高斯 (5×5)": cv2.GaussianBlur(img, (5, 5), 0),
    "中值 (5)":   cv2.medianBlur(img, 5),
    "双边":       cv2.bilateralFilter(img, 5, 50, 50),
}

fig, axes = plt.subplots(1, 4, figsize=(16, 4))
axes[0].imshow(img, cmap="gray")
axes[0].set_title("原始")
for ax, (name, im) in zip(axes[1:], filters.items()):
    ax.imshow(im, cmap="gray")
    ax.set_title(name)
for ax in axes:
    ax.axis("off")
plt.tight_layout()
plt.show()

3. 直方图均衡化

直方图均衡化将像素强度重新分布到完整的动态范围内,改善光照不足场景中的对比度——这在室内机器人环境中很常见。

3.1 基本直方图均衡化

import cv2
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)
equ = cv2.equalizeHist(img)

fig, axes = plt.subplots(2, 2, figsize=(10, 8))
axes[0, 0].imshow(img, cmap="gray")
axes[0, 0].set_title("原始")
axes[0, 1].imshow(equ, cmap="gray")
axes[0, 1].set_title("均衡化后")
axes[1, 0].hist(img.ravel(), bins=256, range=(0, 256))
axes[1, 0].set_title("原始直方图")
axes[1, 1].hist(equ.ravel(), bins=256, range=(0, 256))
axes[1, 1].set_title("均衡化直方图")
for ax in axes.flat:
    if ax in axes[0]:
        ax.axis("off")
plt.tight_layout()
plt.show()

3.2 CLAHE(对比度受限自适应直方图均衡化)

基本均衡化使用全局直方图,可能在均匀区域过度放大噪声。CLAHE 将图像分成小块,独立均衡化每个小块,并通过裁剪限制来防止噪声放大。

import cv2
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg", cv2.IMREAD_GRAYSCALE)

# 全局均衡化
equ = cv2.equalizeHist(img)

# CLAHE
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
clahe_img = clahe.apply(img)

fig, axes = plt.subplots(1, 3, figsize=(14, 4))
for ax, im, title in zip(axes,
    [img, equ, clahe_img],
    ["原始", "全局均衡化", "CLAHE (clip=2.0)"]):
    ax.imshow(im, cmap="gray")
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.show()

何时使用 CLAHE

CLAHE 是机器人领域的首选方案,因为场景通常同时包含明亮和黑暗区域(例如,机器人从走廊进入阳光照射的房间)。它避免了全局均衡化的泛白效果。


4. 形态学操作

形态学操作基于形状处理二值(或灰度)图像。它们对于清理阈值化后的掩码至关重要——去除小噪声块、填充小孔以及分离接触的物体。

4.1 腐蚀与膨胀

**腐蚀**缩小亮区域:只有当核下的*所有*像素都为 1 时,该像素才变为 1。

**膨胀**扩大亮区域:只要核下的*任意*像素为 1,该像素就变为 1。

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 创建合成二值图像用于演示
img = np.zeros((200, 200), dtype=np.uint8)
cv2.circle(img, (60, 60), 30, 255, -1)
cv2.circle(img, (140, 60), 30, 255, -1)
cv2.circle(img, (100, 140), 40, 255, -1)
cv2.rectangle(img, (20, 120), (50, 180), 255, -1)

# 添加噪声
noise = np.random.randint(0, 2, img.shape, dtype=np.uint8) * 255
img_noisy = cv2.bitwise_or(img, noise)

# 定义核
kernel = np.ones((5, 5), np.uint8)

eroded = cv2.erode(img_noisy, kernel, iterations=1)
dilated = cv2.dilate(img_noisy, kernel, iterations=1)

fig, axes = plt.subplots(1, 3, figsize=(12, 4))
for ax, im, title in zip(axes,
    [img_noisy, eroded, dilated],
    ["含噪二值图", "腐蚀 (5×5)", "膨胀 (5×5)"]):
    ax.imshow(im, cmap="gray")
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.show()

核的变化形式

import cv2
import numpy as np

# 矩形核
kernel_rect = np.ones((5, 5), np.uint8)

# 椭圆核
kernel_ellip = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))

# 十字形核
kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))

print("矩形核:\n", kernel_rect)
print("椭圆核:\n", kernel_ellip)
print("十字核:\n", kernel_cross)

4.2 开运算与闭运算

  • 开运算 = 先腐蚀后膨胀。去除小的亮噪声,同时保留较大的结构。
  • 闭运算 = 先膨胀后腐蚀。填充亮物体内部的小暗孔。
import cv2
import numpy as np
import matplotlib.pyplot as plt

img = np.zeros((200, 200), dtype=np.uint8)
cv2.circle(img, (100, 100), 50, 255, -1)

# 添加噪声和孔洞
noise = np.random.randint(0, 2, img.shape, dtype=np.uint8) * 255
img_dirty = cv2.bitwise_or(img, noise)

kernel = np.ones((7, 7), np.uint8)
opened = cv2.morphologyEx(img_dirty, cv2.MORPH_OPEN, kernel)
closed = cv2.morphologyEx(img_dirty, cv2.MORPH_CLOSE, kernel)

fig, axes = plt.subplots(1, 3, figsize=(12, 4))
for ax, im, title in zip(axes,
    [img_dirty, opened, closed],
    ["含噪二值图", "开运算(去噪)", "闭运算(填孔)"]):
    ax.imshow(im, cmap="gray")
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.show()

4.3 实际应用:噪声去除与孔洞填充

在机器人学中,对颜色检测到的物体进行阈值化后,掩码通常有噪声。典型的清理流程:

import cv2
import numpy as np

def clean_mask(mask, kernel_size=5, iterations=2):
    """使用形态学操作清理二值掩码。"""
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))

    # 步骤 1:去除小噪声(开运算)
    cleaned = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=iterations)

    # 步骤 2:填充小孔(闭运算)
    cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel, iterations=iterations)

    return cleaned

# 与颜色检测掩码配合使用:
# mask = cv2.inRange(hsv, lower_color, upper_color)
# clean = clean_mask(mask)

5. 图像变换

几何变换调整图像的位置、方向和大小。它们用于规范化神经网络输入、校正相机透视以及对齐拼接图像。

5.1 缩放与裁剪

import cv2
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg")
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# 缩放到精确尺寸
resized = cv2.resize(img_rgb, (320, 240))

# 按比例缩放
half = cv2.resize(img_rgb, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)

# 裁剪感兴趣区域 (y1:y2, x1:x2)
h, w = img_rgb.shape[:2]
cropped = img_rgb[h//4:3*h//4, w//4:3*w//4]

fig, axes = plt.subplots(1, 3, figsize=(14, 4))
for ax, im, title in zip(axes,
    [resized, half, cropped],
    [f"缩放 {resized.shape[1]}×{resized.shape[0]}",
     f"缩放50% {half.shape[1]}×{half.shape[0]}",
     f"裁剪 {cropped.shape[1]}×{cropped.shape[0]}"]):
    ax.imshow(im)
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.show()

插值方法

  • cv2.INTER_AREA — 缩小图像时最佳(避免摩尔纹)
  • cv2.INTER_LINEAR — 放大时的良好默认(双线性)
  • cv2.INTER_CUBIC — 更高质量但更慢
  • cv2.INTER_NEAREST — 最快,最近邻(用于掩码)

5.2 仿射变换

**仿射变换**保持平行线不变(平移、旋转、缩放、剪切)。你需要指定三对对应点。

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg")
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
h, w = img.shape[:2]

# 源点(原始图像中)
pts_src = np.float32([[50, 50], [200, 50], [50, 200]])

# 目标点(应映射到的位置)
pts_dst = np.float32([[10, 100], [200, 50], [100, 250]])

# 计算仿射矩阵
M = cv2.getAffineTransform(pts_src, pts_dst)

# 应用变换
affine_img = cv2.warpAffine(img_rgb, M, (w, h))

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(img_rgb)
axes[0].set_title("原始图像")
for pt in pts_src:
    axes[0].plot(pt[0], pt[1], 'ro')
axes[1].imshow(affine_img)
axes[1].set_title("仿射变换")
for pt in pts_dst:
    axes[1].plot(pt[0], pt[1], 'ro')
for ax in axes:
    ax.axis("off")
plt.tight_layout()
plt.show()

围绕某点旋转

import cv2
import numpy as np

img = cv2.imread("robot_workspace.jpg")
h, w = img.shape[:2]

# 围绕中心旋转 30 度
center = (w // 2, h // 2)
angle = 30
scale = 1.0

M = cv2.getRotationMatrix2D(center, angle, scale)
rotated = cv2.warpAffine(img, M, (w, h))

5.3 透视变换

透视变换(单应性)将四个源点映射到四个目标点,校正透视畸变。这在文档扫描、AR 标记检测和鸟瞰图生成中非常有用。

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread("robot_workspace.jpg")
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
h, w = img.shape[:2]

# 图像中文档/标记的四个角点(可能是歪斜的)
pts_src = np.float32([
    [56, 65],
    [368, 52],
    [28, 387],
    [389, 390]
])

# 期望输出矩形
output_w, output_h = 300, 400
pts_dst = np.float32([
    [0, 0],
    [output_w, 0],
    [0, output_h],
    [output_w, output_h]
])

# 计算透视矩阵
M = cv2.getPerspectiveTransform(pts_src, pts_dst)

# 应用透视变换
warped = cv2.warpPerspective(img_rgb, M, (output_w, output_h))

fig, axes = plt.subplots(1, 2, figsize=(10, 5))
axes[0].imshow(img_rgb)
axes[0].set_title("原始(有畸变)")
# 绘制源四边形
src_quad = np.vstack([pts_src, pts_src[0]])
axes[0].plot(src_quad[:, 0], src_quad[:, 1], 'r-', linewidth=2)
for pt in pts_src:
    axes[0].plot(pt[0], pt[1], 'ro')
axes[1].imshow(warped)
axes[1].set_title("透视校正后")
for ax in axes:
    ax.axis("off")
plt.tight_layout()
plt.show()

6. 机器人视觉实用流水线

在实际机器人系统中,你需要将多个预处理步骤组合成一个流水线。下面是一个完整的示例,检测彩色物体并计算其中心位置。

流水线步骤

采集帧
    → 转换为 HSV
    → 应用颜色范围掩码
    → 清理掩码(形态学开运算 + 闭运算)
    → 查找轮廓
    → 计算边界框 / 质心
    → 绘制结果

完整流水线代码

import cv2
import numpy as np
import matplotlib.pyplot as plt


def preprocess_frame(frame):
    """应用去噪和增强。"""
    # 使用双边滤波去噪(保留边缘)
    denoised = cv2.bilateralFilter(frame, d=9, sigmaColor=75, sigmaSpace=75)
    return denoised


def create_color_mask(hsv_frame, lower, upper):
    """为目标颜色创建并清理二值掩码。"""
    mask = cv2.inRange(hsv_frame, lower, upper)

    # 形态学清理
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)

    return mask


def detect_object(frame, mask):
    """查找最大轮廓并计算其质心。"""
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    if not contours:
        return None, None, frame

    # 按面积查找最大轮廓
    largest = max(contours, key=cv2.contourArea)

    # 跳过微小轮廓(噪声)
    if cv2.contourArea(largest) < 500:
        return None, None, frame

    # 计算边界框和质心
    x, y, w, h = cv2.boundingRect(largest)
    M = cv2.moments(largest)
    if M["m00"] > 0:
        cx = int(M["m10"] / M["m00"])
        cy = int(M["m01"] / M["m00"])
    else:
        cx, cy = x + w // 2, y + h // 2

    # 绘制结果
    output = frame.copy()
    cv2.rectangle(output, (x, y), (x + w, y + h), (0, 255, 0), 2)
    cv2.circle(output, (cx, cy), 5, (0, 0, 255), -1)
    cv2.putText(output, f"({cx}, {cy})", (cx + 10, cy),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)

    return (cx, cy), (x, y, w, h), output


def run_pipeline(image_path):
    """完整的预处理和检测流水线。"""
    # --- 步骤 1:采集 / 加载 ---
    frame = cv2.imread(image_path)
    if frame is None:
        raise FileNotFoundError(f"无法加载图像: {image_path}")

    # --- 步骤 2:去噪 ---
    denoised = preprocess_frame(frame)

    # --- 步骤 3:转换为 HSV ---
    hsv = cv2.cvtColor(denoised, cv2.COLOR_BGR2HSV)

    # --- 步骤 4:创建颜色掩码(检测蓝色物体) ---
    lower_blue = np.array([100, 50, 50])
    upper_blue = np.array([130, 255, 255])
    mask = create_color_mask(hsv, lower_blue, upper_blue)

    # --- 步骤 5:检测物体 ---
    centroid, bbox, output = detect_object(denoised, mask)

    if centroid:
        print(f"检测到物体,质心: {centroid}, 边界框: {bbox}")
    else:
        print("未检测到物体。")

    # --- 步骤 6:可视化 ---
    fig, axes = plt.subplots(1, 4, figsize=(18, 4))
    axes[0].imshow(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
    axes[0].set_title("1. 原始")
    axes[1].imshow(cv2.cvtColor(denoised, cv2.COLOR_BGR2RGB))
    axes[1].set_title("2. 去噪")
    axes[2].imshow(mask, cmap="gray")
    axes[2].set_title("3. 清理后掩码")
    axes[3].imshow(cv2.cvtColor(output, cv2.COLOR_BGR2RGB))
    axes[3].set_title("4. 检测结果")
    for ax in axes:
        ax.axis("off")
    plt.suptitle("机器人视觉预处理流水线", fontsize=14)
    plt.tight_layout()
    plt.show()

    return centroid, bbox


# --- 运行流水线 ---
if __name__ == "__main__":
    centroid, bbox = run_pipeline("robot_workspace.jpg")

实时摄像头流水线(用于实际机器人)

import cv2
import numpy as np


def live_pipeline(camera_index=0):
    """在实时摄像头画面中运行预处理流水线。"""
    cap = cv2.VideoCapture(camera_index)

    if not cap.isOpened():
        print("错误:无法打开摄像头。")
        return

    # 定义颜色范围(根据目标物体调整)
    lower = np.array([100, 50, 50])
    upper = np.array([130, 255, 255])

    print("按 'q' 退出。")

    while True:
        ret, frame = cap.read()
        if not ret:
            break

        # 流水线
        denoised = cv2.bilateralFilter(frame, 9, 75, 75)
        hsv = cv2.cvtColor(denoised, cv2.COLOR_BGR2HSV)
        mask = cv2.inRange(hsv, lower, upper)

        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
        mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2)
        mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)

        contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
                                       cv2.CHAIN_APPROX_SIMPLE)
        for cnt in contours:
            if cv2.contourArea(cnt) > 500:
                x, y, w, h = cv2.boundingRect(cnt)
                cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

        # 显示结果
        cv2.imshow("Frame", frame)
        cv2.imshow("Mask", mask)

        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    live_pipeline()

7. 练习

练习 1:色彩空间探索(简单)

加载一张包含至少两个不同颜色物体的图像。转换到 HSV 并使用滑条分离每个物体。报告你找到的 HSV 范围。

练习 2:噪声去除挑战(中等)

向干净图像添加高斯噪声(σ=25)和椒盐噪声(5%)。应用高斯、中值和双边滤波器。使用 PSNR(峰值信噪比)比较结果:

import numpy as np

def psnr(original, processed):
    mse = np.mean((original.astype(float) - processed.astype(float)) ** 2)
    if mse == 0:
        return float('inf')
    return 10 * np.log10(255.0 ** 2 / mse)

哪种滤波器对每种噪声类型效果最好?为什么?

练习 3:车道线检测器(困难)

使用道路/赛道的俯视摄像头画面:

  1. 转换到 HSV 并掩蔽车道线标记(通常是白色或黄色)
  2. 用形态学操作清理掩码
  3. 应用透视变换获得鸟瞰图
  4. 使用 cv2.HoughLinesP 检测车道线

练习 4:文档扫描器(困难)

编写一个函数,实现:

  1. 检测图像中最大的四边形轮廓
  2. 应用透视变换生成平坦的矩形输出
  3. 应用 CLAHE 增强可读性

练习 5:预处理调优(中等)

给定一张在光照不足条件下拍摄的图像(由助教提供),设计一个预处理流水线以最大化目标物体的可见性。记录每个步骤并解释你的选择。


参考资料

  1. OpenCV 文档docs.opencv.org
  2. OpenCV-Python 教程opencv-python-tutroals.readthedocs.io
  3. Szeliski, R. (2022). Computer Vision: Algorithms and Applications, 2nd ed. Springer. 第 3 章:图像处理。
  4. Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing, 4th ed. Pearson.
  5. Bradski, G. (2000). The OpenCV Library. Dr. Dobb's Journal of Software Tools.

本教程是港中深机器人课程的一部分。