Python 机器视觉进阶:第11-13章完整学习指南

恭喜你完成了OpenCV基础部分的学习!现在你已经掌握了图像处理、特征提取和轮廓分析等核心技能。接下来的三章将带你进入机器视觉的更高级领域:相机标定与三维视觉实时视频处理以及深度学习与DNN模块


第11章:相机标定与三维视觉

相机标定是机器视觉中的基础但至关重要的步骤。现实中的相机镜头会引入畸变,而标定的目的就是获取相机内部参数和畸变系数,从而校正图像,并为三维重建打下基础。

11.1 相机模型与畸变原理

重点:真实相机使用透镜成像,这会导致两种主要畸变:

  1. 径向畸变:光线在透镜边缘弯曲程度更大,导致图像边缘的直线变弯曲。用参数 (k_1, k_2, k_3) 表示。
  2. 切向畸变:透镜与成像平面不平行导致,用参数 (p_1, p_2) 表示。

相机标定的目的:获取相机内参矩阵(焦距、主点坐标)和畸变系数,用于图像校正和三维信息恢复。

11.2 使用棋盘格进行相机标定

重点:标定需要拍摄多张不同角度的棋盘格图案,OpenCV会自动检测角点并计算参数。

import cv2
import numpy as np
import glob

# 1. 准备标定板参数
# 棋盘格内角点数(例如9x6的棋盘,内角点为8x5,这里以9x6为例)
chessboard_size = (9, 6)  # 内角点数量
square_size = 25  # 每个方格的实际尺寸(毫米),用于后续三维重建

# 2. 准备对象点(世界坐标系中的三维点)
# 假设棋盘格在Z=0平面上,生成(0,0,0), (1,0,0), ... 坐标
objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
objp = objp * square_size  # 乘以实际尺寸

# 存储所有图像的对象点和图像点
objpoints = []  # 三维点
imgpoints = []  # 二维点

# 3. 读取标定图像
images = glob.glob('calibration_images/*.jpg')

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 4. 查找棋盘格角点
    ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
    
    if ret:
        objpoints.append(objp)
        
        # 5. 亚像素级角点精细化
        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
        corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        imgpoints.append(corners2)
        
        # 6. 绘制角点并显示
        cv2.drawChessboardCorners(img, chessboard_size, corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(100)

cv2.destroyAllWindows()

# 7. 执行相机标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None
)

print(f"重投影误差: {ret}")
print(f"相机内参矩阵:\n{mtx}")
print(f"畸变系数: {dist.ravel()}")

重点

  • cv2.findChessboardCorners():检测棋盘格角点,返回角点坐标
  • cv2.cornerSubPix():亚像素级角点精细化,提高标定精度
  • cv2.calibrateCamera():执行标定,返回内参矩阵和畸变系数

11.3 畸变校正(去畸变)

获取相机参数后,就可以对图像进行畸变校正。

# 读取一张待校正的图像
img = cv2.imread('distorted_image.jpg')
h, w = img.shape[:2]

# 1. 获取优化后的相机矩阵(可以调整alpha参数)
# alpha=0 时,返回的图像裁剪掉黑色边框,保留有效像素
# alpha=1 时,保留所有原始像素,但有黑色边框
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))

# 2. 方法一:使用 undistort 直接校正
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

# 3. 裁剪图像(去除黑色边框)
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]

# 4. 方法二:使用 remap 进行校正(适合视频流,可预先计算映射)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), 5)
dst2 = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

# 显示结果对比
cv2.imshow('Original', img)
cv2.imshow('Undistorted', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()

重点

  • cv2.getOptimalNewCameraMatrix():根据alpha参数优化相机矩阵,决定是否保留黑色边框
  • cv2.undistort():直接对单张图像进行畸变校正
  • cv2.initUndistortRectifyMap() + cv2.remap():预先计算映射表,适合批量处理或视频流

11.4 立体视觉基础(双目相机)

重点:双目视觉通过两个相机从不同角度拍摄同一场景,计算视差来恢复深度信息。

11.4.1 立体标定
# 假设已经对左右相机分别进行了单目标定
# 需要额外标定两个相机之间的相对位置关系(旋转矩阵R和平移向量T)

# 立体标定
ret, K1, D1, K2, D2, R, T, E, F = cv2.stereoCalibrate(
    objpoints,           # 世界坐标系中的三维点
    imgpoints_left,      # 左图像角点
    imgpoints_right,     # 右图像角点
    cameraMatrix1,       # 左相机内参
    distCoeffs1,         # 左相机畸变系数
    cameraMatrix2,       # 右相机内参
    distCoeffs2,         # 右相机畸变系数
    image_size,          # 图像尺寸
    criteria, flags
)
11.4.2 立体校正

立体校正是为了消除左右图像的行不对齐问题,使得对应点在同一水平线上,便于计算视差。

# 立体校正
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(
    K1, D1, K2, D2, image_size, R, T,
    alpha=0, flags=cv2.CALIB_ZERO_DISPARITY
)

# 计算校正映射
map1x, map1y = cv2.initUndistortRectifyMap(K1, D1, R1, P1, image_size, cv2.CV_32FC1)
map2x, map2y = cv2.initUndistortRectifyMap(K2, D2, R2, P2, image_size, cv2.CV_32FC1)

# 应用校正
rectified_left = cv2.remap(left_img, map1x, map1y, cv2.INTER_LINEAR)
rectified_right = cv2.remap(right_img, map2x, map2y, cv2.INTER_LINEAR)
11.4.3 视差图与深度计算
# 1. 创建立体匹配对象(使用SGBM算法)
stereo = cv2.StereoSGBM_create(
    minDisparity=0,
    numDisparities=16*5,      # 视差范围,必须是16的倍数
    blockSize=21,              # 匹配块大小
    P1=8*3*21**2,              # 视差平滑度参数
    P2=32*3*21**2,
    disp12MaxDiff=1,
    uniquenessRatio=10,
    speckleWindowSize=100,
    speckleRange=32
)

# 2. 计算视差图
disparity = stereo.compute(rectified_left, rectified_right).astype(np.float32) / 16.0

# 3. 将视差图转换为深度图(需要Q矩阵)
depth_map = cv2.reprojectImageTo3D(disparity, Q)

# 4. 显示视差图
cv2.imshow('Disparity', (disparity - disparity.min()) / (disparity.max() - disparity.min()))

重点

  • 视差:左右图像中对应点的水平位置差异,与深度成反比
  • cv2.StereoSGBM_create():半全局块匹配算法,比BM更准确
  • cv2.reprojectImageTo3D():将视差图转换为三维点云

11.5 综合案例:简单三维重建

import cv2
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def simple_3d_reconstruction(left_img, right_img, K1, D1, K2, D2, R, T):
    """简单的双目三维重建流程"""
    
    # 1. 立体校正
    image_size = left_img.shape[:2][::-1]
    R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(
        K1, D1, K2, D2, image_size, R, T, alpha=0
    )
    
    # 2. 计算校正映射
    map1x, map1y = cv2.initUndistortRectifyMap(K1, D1, R1, P1, image_size, cv2.CV_32FC1)
    map2x, map2y = cv2.initUndistortRectifyMap(K2, D2, R2, P2, image_size, cv2.CV_32FC1)
    
    # 3. 校正图像
    rect_left = cv2.remap(left_img, map1x, map1y, cv2.INTER_LINEAR)
    rect_right = cv2.remap(right_img, map2x, map2y, cv2.INTER_LINEAR)
    
    # 4. 计算视差
    stereo = cv2.StereoSGBM_create(
        minDisparity=0,
        numDisparities=16*5,
        blockSize=15,
        P1=8*3*15**2,
        P2=32*3*15**2
    )
    disparity = stereo.compute(rect_left, rect_right).astype(np.float32) / 16.0
    
    # 5. 计算三维点云
    points_3d = cv2.reprojectImageTo3D(disparity, Q)
    
    # 6. 简单点云可视化
    mask = disparity > disparity.min()
    x = points_3d[:, :, 0][mask]
    y = points_3d[:, :, 1][mask]
    z = points_3d[:, :, 2][mask]
    
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111, projection='3d')
    ax.scatter(x[::10], y[::10], z[::10], c=z[::10], cmap='viridis', s=1)
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    plt.title('3D Reconstruction')
    plt.show()
    
    return disparity, points_3d

第12章:视频处理与实时采集

将图像处理算法应用到视频流或实时摄像头,是实现工业检测系统的基础。

12.1 从摄像头捕获实时视频

import cv2

# 1. 创建 VideoCapture 对象
# 参数0表示第一个摄像头(通常是内置摄像头),1表示第二个,依此类推
cap = cv2.VideoCapture(0)

# 检查摄像头是否成功打开
if not cap.isOpened():
    print("无法打开摄像头")
    exit()

# 2. 设置摄像头属性(可选)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)   # 设置宽度
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)   # 设置高度
cap.set(cv2.CAP_PROP_FPS, 30)              # 设置帧率

while True:
    # 3. 逐帧捕获
    ret, frame = cap.read()
    
    # 如果帧读取正确,ret为True
    if not ret:
        print("无法接收帧,退出...")
        break
    
    # 4. 在这里对帧进行处理
    # 例如转换为灰度图
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    # 5. 显示结果
    cv2.imshow('Camera Feed', gray)
    
    # 6. 按'q'键退出
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 7. 释放资源
cap.release()
cv2.destroyAllWindows()

重点

  • cv2.VideoCapture(0):打开摄像头,参数为设备索引
  • cap.read():读取一帧,返回布尔值和图像数据
  • cap.set(propId, value):设置摄像头属性,如分辨率、帧率
  • cv2.waitKey(1):等待1毫秒,用于检测按键和维持窗口显示

12.2 从视频文件读取

import cv2

# 从视频文件读取
cap = cv2.VideoCapture('test_video.mp4')

# 获取视频属性
fps = cap.get(cv2.CAP_PROP_FPS)           # 帧率
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))  # 总帧数
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))        # 宽度
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))      # 高度

print(f"视频信息: {width}x{height}, {fps}fps, 总帧数: {frame_count}")

# 计算每帧的显示延迟(毫秒)
delay = int(1000 / fps)

while cap.isOpened():
    ret, frame = cap.read()
    
    if not ret:
        print("视频播放完毕")
        break
    
    # 处理帧
    processed = cv2.Canny(frame, 100, 200)  # 边缘检测
    
    cv2.imshow('Video', processed)
    
    # 使用基于帧率的延迟
    if cv2.waitKey(delay) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

重点

  • cap.get(propId):获取视频属性,如帧率、尺寸
  • 使用视频的实际帧率计算 waitKey 延迟,实现正常速度播放

12.3 保存处理后的视频

import cv2

# 打开摄像头
cap = cv2.VideoCapture(0)

# 定义视频编码器和输出文件
fourcc = cv2.VideoWriter_fourcc(*'XVID')  # 编码器
out = cv2.VideoWriter('output.avi', fourcc, 20.0, (640, 480))

while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break
    
    # 处理帧(例如水平翻转)
    flipped = cv2.flip(frame, 1)
    
    # 写入输出视频
    out.write(flipped)
    
    cv2.imshow('Recording', flipped)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
out.release()  # 记得释放 VideoWriter
cv2.destroyAllWindows()

重点

  • cv2.VideoWriter_fourcc():定义视频编码格式
  • cv2.VideoWriter():创建视频写入对象,指定输出文件名、编码器、帧率和尺寸
  • out.write():写入处理后的帧

12.4 实时视频处理性能优化

实时视频处理需要保证处理速度不低于帧率,否则会出现卡顿。

import cv2
import time

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

# FPS计算变量
fps_counter = 0
fps_start_time = time.time()
fps = 0

while True:
    # 计算帧率
    fps_counter += 1
    if time.time() - fps_start_time > 1.0:
        fps = fps_counter
        fps_counter = 0
        fps_start_time = time.time()
    
    ret, frame = cap.read()
    if not ret:
        break
    
    # 优化1:调整处理分辨率
    # 如果处理算法很耗时,可以先缩小图像
    small_frame = cv2.resize(frame, (320, 240))
    
    # 在这里进行耗时处理
    # 例如边缘检测
    edges = cv2.Canny(small_frame, 100, 200)
    
    # 优化2:跳过部分帧(例如每处理一帧,跳过两帧)
    # 但这会影响实时性,需根据需求权衡
    
    # 在原始帧上显示FPS
    cv2.putText(frame, f"FPS: {fps}", (10, 30), 
                cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
    
    cv2.imshow('Optimized Processing', frame)
    cv2.imshow('Edges', edges)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

重点

  • 降低分辨率处理:先缩小图像再进行算法处理,再映射回原图
  • 帧率计算:用于监控性能,确保处理速度达标
  • 跳帧处理:当算法非常耗时时,可以间隔处理,但会损失实时性

12.5 综合案例:实时运动检测

import cv2
import numpy as np

class MotionDetector:
    def __init__(self, threshold=25, min_area=500):
        self.threshold = threshold
        self.min_area = min_area
        self.first_frame = None
    
    def detect(self, frame):
        """检测运动区域"""
        # 转换为灰度并高斯模糊
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        gray = cv2.GaussianBlur(gray, (21, 21), 0)
        
        # 如果是第一帧,保存作为背景
        if self.first_frame is None:
            self.first_frame = gray
            return frame, []
        
        # 计算当前帧与背景的差异
        frame_delta = cv2.absdiff(self.first_frame, gray)
        thresh = cv2.threshold(frame_delta, self.threshold, 255, cv2.THRESH_BINARY)[1]
        
        # 形态学操作去除噪声
        thresh = cv2.dilate(thresh, None, iterations=2)
        thresh = cv2.erode(thresh, None, iterations=2)
        
        # 查找轮廓
        contours, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
        # 筛选符合条件的运动区域
        motion_areas = []
        for contour in contours:
            if cv2.contourArea(contour) < self.min_area:
                continue
            
            # 获取外接矩形
            (x, y, w, h) = cv2.boundingRect(contour)
            motion_areas.append((x, y, w, h))
        
        return thresh, motion_areas

# 主程序
cap = cv2.VideoCapture(0)
detector = MotionDetector(threshold=25, min_area=500)

while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    # 检测运动
    thresh, motion_areas = detector.detect(frame)
    
    # 在帧上绘制运动区域
    for (x, y, w, h) in motion_areas:
        cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
        cv2.putText(frame, "Motion", (x, y-10), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
    
    # 显示结果
    cv2.imshow('Motion Detection', frame)
    cv2.imshow('Threshold', thresh)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

工业应用场景:硅片检测设备中,可以在硅片进入检测区域时触发图像采集,代替外部传感器。


第13章:深度学习与OpenCV DNN模块

OpenCV的DNN(Deep Neural Network)模块可以直接加载预训练的深度学习模型进行推理,无需安装TensorFlow、PyTorch等深度学习框架。

13.1 DNN模块支持的模型格式

重点:OpenCV DNN模块支持多种深度学习框架的模型格式:

  • Caffe.caffemodel + .prototxt
  • TensorFlow.pb(冻结图)或SavedModel
  • Torch/PyTorch:需转换为ONNX格式
  • Darknet.weights + .cfg(YOLO系列)
  • ONNX:开放神经网络交换格式(推荐,兼容性好)

13.2 加载模型并进行图像分类

import cv2
import numpy as np

# 1. 加载预训练模型
# 以GoogleNet为例(Caffe格式)
net = cv2.dnn.readNetFromCaffe('bvlc_googlenet.prototxt', 'bvlc_googlenet.caffemodel')

# 或者加载ONNX格式
# net = cv2.dnn.readNetFromONNX('model.onnx')

# 2. 读取图像并预处理
image = cv2.imread('cat.jpg')
blob = cv2.dnn.blobFromImage(
    image, 
    scalefactor=1.0/255,      # 缩放因子
    size=(224, 224),           # 输入尺寸
    mean=(104, 117, 123),      # 均值(不同模型不同)
    swapRB=True,                # 交换RB通道(OpenCV是BGR,模型需要RGB)
    crop=False
)

# 3. 设置网络输入
net.setInput(blob)

# 4. 前向传播,获取输出
output = net.forward()

# 5. 解析结果
# 假设是1000类的分类任务
predictions = output[0]
class_id = np.argmax(predictions)
confidence = predictions[class_id]

# 加载类别标签
with open('imagenet_classes.txt', 'r') as f:
    classes = [line.strip() for line in f.readlines()]

print(f"预测类别: {classes[class_id]}, 置信度: {confidence:.4f}")

# 在图像上显示结果
cv2.putText(image, f"{classes[class_id]}: {confidence:.2f}", (10, 30),
            cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.imshow('Classification', image)
cv2.waitKey(0)
cv2.destroyAllWindows()

重点

  • cv2.dnn.blobFromImage():将图像转换为深度学习模型需要的blob格式,包括缩放、裁剪、均值归一化等
  • net.setInput():设置网络输入
  • net.forward():执行前向推理,获取输出

13.3 使用PyTorch模型(转换为ONNX)

重点:PyTorch模型需要通过ONNX格式才能在OpenCV中使用。

# 在PyTorch中将模型转换为ONNX
import torch
import torchvision.models as models

# 1. 加载预训练PyTorch模型
model = models.resnet50(pretrained=True)
model.eval()

# 2. 创建示例输入
dummy_input = torch.randn(1, 3, 224, 224)

# 3. 导出ONNX
torch.onnx.export(
    model,
    dummy_input,
    "resnet50.onnx",
    export_params=True,
    opset_version=11,
    input_names=['input'],
    output_names=['output']
)

print("模型已转换为ONNX格式")
# 在OpenCV中加载ONNX模型
import cv2
import numpy as np

# 加载ONNX模型
net = cv2.dnn.readNetFromONNX('resnet50.onnx')

# 图像预处理(需与PyTorch保持一致)
image = cv2.imread('image.jpg')
image = cv2.resize(image, (256, 256))
# 中心裁剪为224x224
h, w = image.shape[:2]
start_h = (h - 224) // 2
start_w = (w - 224) // 2
image = image[start_h:start_h+224, start_w:start_w+224]

# 转换为blob,注意PyTorch的预处理顺序:归一化到[0,1],减去均值,除以标准差
blob = cv2.dnn.blobFromImage(
    image,
    scalefactor=1.0/255,
    size=(224, 224),
    mean=(0.485, 0.456, 0.406),  # ImageNet均值
    swapRB=True
)
# 除以标准差
blob[0] /= np.array([0.229, 0.224, 0.225]).reshape(3, 1, 1)

net.setInput(blob)
output = net.forward()

13.4 目标检测(YOLO)实战

重点:YOLO(You Only Look Once)是最流行的实时目标检测算法。

import cv2
import numpy as np

class YOLODetector:
    def __init__(self, weights_path, config_path, classes_path, conf_threshold=0.5, nms_threshold=0.4):
        # 加载网络
        self.net = cv2.dnn.readNetFromDarknet(config_path, weights_path)
        self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
        self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
        
        # 加载类别
        with open(classes_path, 'r') as f:
            self.classes = [line.strip() for line in f.readlines()]
        
        # 获取输出层名称
        self.output_layers = self.get_output_layers()
        
        self.conf_threshold = conf_threshold
        self.nms_threshold = nms_threshold
    
    def get_output_layers(self):
        """获取YOLO的输出层名称"""
        layer_names = self.net.getLayerNames()
        output_layers = [layer_names[i - 1] for i in self.net.getUnconnectedOutLayers()]
        return output_layers
    
    def detect(self, image):
        """执行目标检测"""
        h, w = image.shape[:2]
        
        # 创建blob
        blob = cv2.dnn.blobFromImage(image, 1/255.0, (416, 416), swapRB=True, crop=False)
        
        # 前向传播
        self.net.setInput(blob)
        outputs = self.net.forward(self.output_layers)
        
        # 解析检测结果
        boxes = []
        confidences = []
        class_ids = []
        
        for output in outputs:
            for detection in output:
                scores = detection[5:]
                class_id = np.argmax(scores)
                confidence = scores[class_id]
                
                if confidence > self.conf_threshold:
                    # YOLO输出格式:center_x, center_y, width, height (归一化坐标)
                    center_x = int(detection[0] * w)
                    center_y = int(detection[1] * h)
                    width = int(detection[2] * w)
                    height = int(detection[3] * h)
                    
                    # 转换为左上角坐标
                    x = int(center_x - width / 2)
                    y = int(center_y - height / 2)
                    
                    boxes.append([x, y, width, height])
                    confidences.append(float(confidence))
                    class_ids.append(class_id)
        
        # 非极大值抑制
        indices = cv2.dnn.NMSBoxes(boxes, confidences, self.conf_threshold, self.nms_threshold)
        
        results = []
        if len(indices) > 0:
            for i in indices.flatten():
                x, y, w, h = boxes[i]
                results.append({
                    'box': (x, y, w, h),
                    'confidence': confidences[i],
                    'class_id': class_ids[i],
                    'class_name': self.classes[class_ids[i]]
                })
        
        return results
    
    def draw_detections(self, image, detections):
        """在图像上绘制检测结果"""
        for det in detections:
            x, y, w, h = det['box']
            label = f"{det['class_name']}: {det['confidence']:.2f}"
            
            # 绘制矩形
            cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), 2)
            
            # 绘制标签背景
            (label_w, label_h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)
            cv2.rectangle(image, (x, y-label_h-10), (x+label_w, y), (0, 255, 0), -1)
            
            # 绘制标签文字
            cv2.putText(image, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 2)
        
        return image

# 使用示例
detector = YOLODetector(
    weights_path='yolov3.weights',
    config_path='yolov3.cfg',
    classes_path='coco.names'
)

# 对图像进行检测
img = cv2.imread('scene.jpg')
detections = detector.detect(img)
result_img = detector.draw_detections(img, detections)

cv2.imshow('YOLO Detection', result_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

重点

  • cv2.dnn.readNetFromDarknet():加载YOLO的Darknet模型
  • net.getUnconnectedOutLayers():获取YOLO的输出层名称(YOLO有多个输出层)
  • 非极大值抑制(NMS):去除重叠的检测框,保留最佳结果

13.5 DNN模块在视频流中的应用

import cv2
import numpy as np

# 加载YOLO模型
net = cv2.dnn.readNetFromDarknet('yolov3-tiny.cfg', 'yolov3-tiny.weights')
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)

# 获取输出层
layer_names = net.getLayerNames()
output_layers = [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]

# 加载类别
with open('coco.names', 'r') as f:
    classes = [line.strip() for line in f.readlines()]

# 打开摄像头
cap = cv2.VideoCapture(0)

while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    h, w = frame.shape[:2]
    
    # 创建blob
    blob = cv2.dnn.blobFromImage(frame, 1/255.0, (416, 416), swapRB=True, crop=False)
    
    # 前向传播
    net.setInput(blob)
    outputs = net.forward(output_layers)
    
    # 解析结果
    boxes = []
    confidences = []
    class_ids = []
    
    for output in outputs:
        for detection in output:
            scores = detection[5:]
            class_id = np.argmax(scores)
            confidence = scores[class_id]
            
            if confidence > 0.5:
                center_x = int(detection[0] * w)
                center_y = int(detection[1] * h)
                width = int(detection[2] * w)
                height = int(detection[3] * h)
                
                x = int(center_x - width / 2)
                y = int(center_y - height / 2)
                
                boxes.append([x, y, width, height])
                confidences.append(float(confidence))
                class_ids.append(class_id)
    
    # NMS
    indices = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
    
    if len(indices) > 0:
        for i in indices.flatten():
            x, y, w, h = boxes[i]
            label = f"{classes[class_ids[i]]}: {confidences[i]:.2f}"
            
            cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
            cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
    
    # 显示FPS(简化计算)
    cv2.putText(frame, "YOLO Real-time Detection", (10, 30), 
                cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
    
    cv2.imshow('YOLO Live', frame)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

13.6 综合案例:硅片缺陷检测原型

结合本章所学,实现一个简单的硅片缺陷检测原型:

import cv2
import numpy as np

class SiliconWaferInspector:
    def __init__(self, model_path=None):
        """初始化检测器"""
        # 如果有深度学习模型,可以在这里加载
        if model_path:
            self.net = cv2.dnn.readNetFromONNX(model_path)
        else:
            self.net = None
        
        # 传统视觉参数
        self.defect_threshold = 30
    
    def preprocess(self, image):
        """图像预处理"""
        # 转为灰度
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        
        # 高斯滤波去噪
        blurred = cv2.GaussianBlur(gray, (5, 5), 1.5)
        
        return blurred
    
    def detect_defects_traditional(self, image):
        """传统视觉方法检测缺陷"""
        processed = self.preprocess(image)
        
        # 自适应阈值分割
        thresh = cv2.adaptiveThreshold(
            processed, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
            cv2.THRESH_BINARY_INV, 11, 2
        )
        
        # 形态学操作去除小噪点
        kernel = np.ones((3, 3), np.uint8)
        thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1)
        
        # 查找轮廓
        contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
        defects = []
        for contour in contours:
            area = cv2.contourArea(contour)
            if area > 50:  # 面积大于50像素认为是缺陷
                x, y, w, h = cv2.boundingRect(contour)
                defects.append({
                    'box': (x, y, w, h),
                    'area': area,
                    'type': 'scratch' if w > h * 2 else 'dot'  # 简单分类
                })
        
        return defects
    
    def detect_defects_deeplearning(self, image):
        """深度学习方法检测缺陷(需训练好的模型)"""
        if self.net is None:
            return []
        
        # 预处理
        blob = cv2.dnn.blobFromImage(image, 1/255.0, (224, 224), swapRB=True)
        self.net.setInput(blob)
        outputs = self.net.forward()
        
        # 解析输出(假设是缺陷分割或检测结果)
        # 这里简化处理,实际需要根据模型输出格式解析
        
        return outputs
    
    def inspect(self, image, method='traditional'):
        """执行检测"""
        if method == 'traditional':
            defects = self.detect_defects_traditional(image)
        else:
            defects = self.detect_defects_deeplearning(image)
        
        # 绘制结果
        result = image.copy()
        for defect in defects:
            x, y, w, h = defect['box']
            color = (0, 0, 255) if defect['type'] == 'scratch' else (0, 255, 255)
            cv2.rectangle(result, (x, y), (x+w, y+h), color, 2)
            cv2.putText(result, defect['type'], (x, y-5),
                       cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
        
        # 统计信息
        cv2.putText(result, f"Defects: {len(defects)}", (10, 30),
                   cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
        
        return result, defects

# 使用示例
inspector = SiliconWaferInspector()

# 读取硅片图像
wafer_img = cv2.imread('wafer_sample.jpg')

# 执行检测
result_img, defects = inspector.inspect(wafer_img)

# 显示结果
cv2.imshow('Wafer Inspection', result_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

print(f"检测到 {len(defects)} 处缺陷")

学习建议与下一阶段预告

恭喜你完成了第11-13章的学习!现在你已经掌握了机器视觉的高级技术:

  1. 相机标定与三维视觉:能够校正图像畸变,获取三维信息
  2. 视频处理与实时采集:能够处理实时视频流,构建实时检测系统
  3. 深度学习与DNN模块:能够将深度学习模型集成到OpenCV应用中

实战建议

  • 用你自己的相机拍摄棋盘格图像,完成一次完整的相机标定
  • 尝试用YOLO模型对实时摄像头流进行目标检测
  • 结合之前学的知识,尝试改进硅片检测原型

下一阶段展望
你已经完成了整个Python+OpenCV机器视觉的学习路线!接下来可以:

  • 深入学习深度学习框架:PyTorch或TensorFlow,训练自己的缺陷检测模型
  • 系统集成:将Python视觉算法封装为服务,供C#上位机调用
  • 项目实战:开发完整的硅片检测系统,集成相机控制、图像采集、缺陷检测、PLC通信、数据库存储

如果你在学习过程中遇到任何问题,或者想要深入某个专题的实战案例,随时可以提问!祝你在机器视觉领域不断精进!

更多推荐