4. 单线程检测实现
好,咱们正式开始写代码了。
这一章的目标很明确:用单线程把整个检测流程跑通。说白了,就是让摄像头一帧一帧地读,YOLO 模型一帧一帧地检测,然后把结果画在画面上,最后把 FPS 也怼上去。
你可能会想:“单线程?那多慢啊。” 别急,咱们先把这个基础版本做扎实了,后面再上多线程优化。我个人的习惯是,先把功能跑通,再谈性能。否则你一开始就搞多线程,出了问题都不知道是模型的问题还是线程同步的问题。
4.1 整体流程概览
先看看单线程检测的核心流程,我画了一张图,你一看就明白:
这个流程看起来简单,但每个环节都有坑。我一个个拆开讲。
4.2 打开摄像头与循环读取帧
第一步,打开摄像头。OpenCV 里用 cv2.VideoCapture(0) 就行,参数 0 表示默认摄像头。
读取帧用 cap.read(),它会返回两个值:ret(布尔值,表示是否成功)和 frame(图像数据)。
import cv2
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("无法打开摄像头")
exit()
while True:
ret, frame = cap.read()
if not ret:
print("读取帧失败")
break
# 后续处理...
这里有个小细节:cap.read() 默认返回的帧是 BGR 格式,而 YOLO 模型通常需要 RGB 格式。所以后面做检测前要记得转换颜色空间。
4.3 调用模型进行检测
我用的是 YOLOv8,加载模型就一行代码:
from ultralytics import YOLO
model = YOLO('yolov8n.pt') # 加载预训练模型
然后对每一帧调用检测:
results = model(frame) # 直接传 BGR 帧也行,模型内部会处理
results 是一个列表,每个元素对应一张图的检测结果。因为我们一次只传一帧,所以取 results[0] 就行。
检测结果里包含:
- boxes:边界框坐标(xyxy 格式)
- conf:置信度
- cls:类别 ID
model(frame, verbose=False) 关掉控制台输出,不然每帧都会打印一堆信息,刷屏刷得你怀疑人生。
4.4 绘制边界框
绘制边界框我习惯自己写,不用 YOLO 自带的 plot() 方法。为什么?因为自带的 plot 会画一堆额外信息,有时候画面太乱了。我自己画,想怎么控制就怎么控制。
def draw_boxes(frame, results):
boxes = results[0].boxes
for box in boxes:
x1, y1, x2, y2 = map(int, box.xyxy[0])
conf = box.conf[0].item()
cls_id = int(box.cls[0].item())
label = f"{model.names[cls_id]} {conf:.2f}"
# 画矩形框
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
# 画标签背景
cv2.rectangle(frame, (x1, y1 - 25), (x1 + len(label) * 15, y1), (0, 255, 0), -1)
# 写标签文字
cv2.putText(frame, label, (x1, y1 - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
return frame
这里我用了绿色框,你也可以换成其他颜色。注意标签背景的宽度我是粗略估算的,如果你要精确控制,可以用 cv2.getTextSize() 计算文字宽度。
cv2.rectangle 报错,排查了半天才发现是 float 类型的问题。YOLO 返回的坐标是 tensor 类型,记得用 map(int, ...) 转换。
4.5 计算并显示 FPS
FPS 的计算其实很简单:记录每一帧的时间戳,然后算差值。
import time
prev_time = time.time()
while True:
# ... 读取帧、检测、画框 ...
current_time = time.time()
fps = 1 / (current_time - prev_time)
prev_time = current_time
cv2.putText(frame, f"FPS: {fps:.1f}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
这个 FPS 是瞬时值,波动会比较大。如果你想看平滑后的 FPS,可以用滑动平均:
fps_list = []
while True:
# ...
fps_list.append(fps)
if len(fps_list) > 30:
fps_list.pop(0)
avg_fps = sum(fps_list) / len(fps_list)
我个人习惯在画面上同时显示瞬时 FPS 和平均 FPS,这样既能看出当前性能,又能了解整体趋势。
4.6 完整代码整合
把上面所有代码拼起来,就是完整的单线程检测程序:
import cv2
import time
from ultralytics import YOLO
# 加载模型
model = YOLO('yolov8n.pt')
# 打开摄像头
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("无法打开摄像头")
exit()
prev_time = time.time()
while True:
ret, frame = cap.read()
if not ret:
break
# 检测
results = model(frame, verbose=False)
# 绘制边界框
boxes = results[0].boxes
for box in boxes:
x1, y1, x2, y2 = map(int, box.xyxy[0])
conf = box.conf[0].item()
cls_id = int(box.cls[0].item())
label = f"{model.names[cls_id]} {conf:.2f}"
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.rectangle(frame, (x1, y1 - 25), (x1 + len(label) * 15, y1), (0, 255, 0), -1)
cv2.putText(frame, label, (x1, y1 - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
# 计算并显示 FPS
current_time = time.time()
fps = 1 / (current_time - prev_time)
prev_time = current_time
cv2.putText(frame, f"FPS: {fps:.1f}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# 显示画面
cv2.imshow("YOLO Detection", frame)
# 按 'q' 退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
这段代码大概 40 行,跑起来你就能看到实时检测效果了。
4.7 性能分析:瓶颈在哪?
跑起来之后,你可能会发现 FPS 不高。我用一张表总结一下各环节的耗时占比:
| 环节 | 耗时占比 | 说明 |
|---|---|---|
| 读取帧 | ~5% | 摄像头读取很快,基本不耗时 |
| 模型检测 | ~85% | 这是最大的瓶颈,尤其是大模型 |
| 绘制边界框 | ~5% | 框多了会稍微慢一点 |
| 显示画面 | ~5% | imshow 本身有开销 |
看到了吧?模型检测占了绝大部分时间。这就是为什么后面我们要用多线程——把检测放到单独的线程里,让主线程专心读取和显示。
嗯,这一章的内容就到这。代码你可以直接复制跑跑看,感受一下单线程的“卡顿感”。有了这个基础,后面优化起来你才知道到底优化了什么。