1. 环境搭建与基础认知:OpenCV与YOLO的安装配置,理解检测流程
好,咱们直接开始。这一章说白了就是「把家伙事儿备齐,然后搞清楚这玩意儿到底怎么跑的」。我见过太多人一上来就调模型、改参数,结果环境没配好,折腾半天跑不起来——嗯,这种事我自己也干过。
1.1 为什么选OpenCV + YOLO?
你可能会问:检测框架那么多,为什么非要用OpenCV来跑YOLO?
我个人习惯是这么看的:YOLO负责「认出东西」,OpenCV负责「画出来」。两者配合,就像你有了一个眼神好的侦察兵,再加上一个手稳的绘图员。实际项目中,OpenCV的dnn模块可以直接加载YOLO的模型文件,不需要装PyTorch或TensorFlow那一整套。这对部署来说太友好了——你想想看,客户服务器上可能连GPU都没有,但OpenCV几乎哪都有。
1.2 环境搭建:一步步来
咱们分两步走:先装OpenCV,再准备YOLO的模型文件。
1.2.1 安装OpenCV
Python环境下,最省事的办法就是pip。但我建议你创建一个虚拟环境,别把系统搞乱了。
# 创建虚拟环境(我习惯用venv)
python -m venv yolo_env
# 激活(Windows)
yolo_env\Scripts\activate
# 激活(Mac/Linux)
source yolo_env/bin/activate
# 安装OpenCV(包含contrib模块)
pip install opencv-python opencv-contrib-python
# 验证安装
python -c "import cv2; print(cv2.__version__)"
看到版本号输出来,就说明装好了。如果报错,多半是网络问题——换个国内镜像试试:
pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple
opencv-python-headless,那个没有GUI功能,后面你连显示图片都做不到。我曾经踩过这个坑,调试了半天才发现。
1.2.2 准备YOLO模型文件
YOLO的模型文件有三样东西:权重文件(.weights)、配置文件(.cfg)、类别名称文件(.names)。你可以从官方GitHub下载,也可以用我下面这个脚本自动拉取:
import urllib.request
import os
# 创建模型目录
os.makedirs('yolo_files', exist_ok=True)
# 下载YOLOv4的权重和配置文件
urls = {
'yolov4.weights': 'https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights',
'yolov4.cfg': 'https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4.cfg',
'coco.names': 'https://raw.githubusercontent.com/pjreddie/darknet/master/data/coco.names'
}
for name, url in urls.items():
print(f'正在下载 {name}...')
urllib.request.urlretrieve(url, f'yolo_files/{name}')
print('下载完成!')
这里我用的是YOLOv4,为什么?因为v4在速度和精度之间平衡得不错,而且OpenCV的dnn模块对它的支持最成熟。v5、v8那些虽然更新,但有时候反而兼容性会出问题。
1.3 理解检测流程:从输入到输出
好,环境搭好了。现在咱们聊聊YOLO到底是怎么工作的。说白了就三步:
- 图像预处理:把图片缩放到模型需要的尺寸(通常是416x416或608x608)
- 前向推理:把缩放后的图片扔进神经网络,得到一堆原始输出
- 后处理:从原始输出中解析出边界框、置信度、类别,再用NMS(非极大值抑制)去掉重复的框
你想想看,整个过程就像流水线——原料进去,成品出来。中间每个环节都可能出问题,所以咱们得把每一步都搞清楚。
1.3.1 图像预处理
OpenCV读进来的图片是BGR格式,而YOLO训练时用的是RGB。这个坑我一开始就踩过——检测结果全是错的,后来才发现颜色通道搞反了。
import cv2
import numpy as np
# 读取图片
img = cv2.imread('test.jpg') # BGR格式
# 获取图片尺寸
height, width = img.shape[:2]
# 创建blob(OpenCV的预处理函数)
blob = cv2.dnn.blobFromImage(
img,
scalefactor=1/255.0, # 归一化到[0,1]
size=(416, 416), # 缩放到416x416
mean=(0, 0, 0), # 不减去均值
swapRB=True, # BGR -> RGB(就是这里!)
crop=False
)
print(f'blob的形状: {blob.shape}') # (1, 3, 416, 416)
blobFromImage这个函数,我建议你记住它的参数顺序。每次写代码时我都会默念一遍:图片、缩放因子、尺寸、均值、是否交换通道、是否裁剪。
1.3.2 前向推理
加载模型,然后把blob喂进去:
# 加载模型
net = cv2.dnn.readNet(
'yolo_files/yolov4.weights',
'yolo_files/yolov4.cfg'
)
# 设置blob为输入
net.setInput(blob)
# 前向推理
# 获取所有输出层的名称
layer_names = net.getLayerNames()
output_layers = [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]
# 推理
outputs = net.forward(output_layers)
print(f'输出层数量: {len(outputs)}')
print(f'每个输出的形状: {[o.shape for o in outputs]}')
这里有个细节:getUnconnectedOutLayers()返回的是输出层的索引,需要减1才能对应到layer_names。这个逻辑我第一次看的时候也懵了一下。
1.3.3 后处理:从原始输出到检测框
原始输出长什么样?每个输出层是一个三维数组,形状是 (batch, num_boxes, 85)。其中85的含义是:
| 索引范围 | 含义 |
|---|---|
| 0-3 | 边界框坐标 (cx, cy, w, h) —— 注意是相对于网格的偏移 |
| 4 | 目标置信度(有没有物体) |
| 5-84 | 80个类别的置信度(COCO数据集) |
后处理的核心就是解析这些数据,然后做NMS:
def post_process(outputs, img_width, img_height, conf_threshold=0.5, nms_threshold=0.4):
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 > conf_threshold:
# 将中心点坐标和宽高转换为像素坐标
center_x = int(detection[0] * img_width)
center_y = int(detection[1] * img_height)
w = int(detection[2] * img_width)
h = int(detection[3] * img_height)
# 转换为左上角坐标
x = int(center_x - w / 2)
y = int(center_y - h / 2)
boxes.append([x, y, w, h])
confidences.append(float(confidence))
class_ids.append(class_id)
# NMS去重
indices = cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, nms_threshold)
result_boxes = []
result_confidences = []
result_class_ids = []
if len(indices) > 0:
for i in indices.flatten():
result_boxes.append(boxes[i])
result_confidences.append(confidences[i])
result_class_ids.append(class_ids[i])
return result_boxes, result_confidences, result_class_ids
1.4 完整检测流程:把一切串起来
好了,现在咱们把上面所有步骤拼成一个完整的函数。这也是我实际项目中用的模板:
def detect_objects(image_path, model_path='yolo_files'):
# 1. 读取图片
img = cv2.imread(image_path)
if img is None:
print(f'无法读取图片: {image_path}')
return None
height, width = img.shape[:2]
# 2. 加载模型(只加载一次,实际项目中会缓存)
net = cv2.dnn.readNet(
f'{model_path}/yolov4.weights',
f'{model_path}/yolov4.cfg'
)
# 3. 预处理
blob = cv2.dnn.blobFromImage(img, 1/255.0, (416, 416), swapRB=True)
net.setInput(blob)
# 4. 推理
layer_names = net.getLayerNames()
output_layers = [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]
outputs = net.forward(output_layers)
# 5. 后处理
boxes, confidences, class_ids = post_process(outputs, width, height)
# 6. 加载类别名称
with open(f'{model_path}/coco.names', 'r') as f:
classes = [line.strip() for line in f.readlines()]
# 7. 绘制结果
for i in range(len(boxes)):
x, y, w, h = boxes[i]
label = f'{classes[class_ids[i]]}: {confidences[i]:.2f}'
# 画框
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
# 写标签
cv2.putText(img, label, (x, y - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
return img
# 测试
result_img = detect_objects('test.jpg')
if result_img is not None:
cv2.imshow('Detection Result', result_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
1.5 常见问题与避坑指南
最后,分享几个我实际踩过的坑:
- 模型文件版本不匹配:.weights和.cfg必须来自同一个YOLO版本。我曾经混用过v3的cfg和v4的weights,结果检测结果全是乱的。
- 输入尺寸不一致:训练时用的416x416,推理时也必须是416x416。别想着用608x608能提高精度——模型没训练过那个尺寸,反而会变差。
- 置信度阈值设得太低:新手总想把阈值设低一点,生怕漏检。结果画出来的框比目标还多。我一般从0.5开始调,根据实际效果上下浮动。
- 忘记swapRB:这个前面说过了,BGR和RGB的坑,几乎每个人都会踩一次。
嗯,这一章的内容就到这儿。环境搭好了,流程也捋清楚了。下一章咱们会深入聊聊如何调试检测结果——比如怎么可视化中间层的输出、怎么分析漏检和误检的原因。到时候见。