YOLOv8 级联检测:在人脸 ROI 内检测眼镜(零改源码方案)
在复杂背景中,直接对整图进行精细检测可能导致精度不高,尤其是当目标的尺寸较小或背景较为复杂时。通过限制检测范围,仅在人脸区域进行进一步的目标检测(如眼镜检测),可以避免背景干扰,提高检测的准确性。
为了说明二次目标检测的实际应用,以下以人脸识别和眼镜检测为例,展示具体的流程。
已拥有两个 YOLOv8 目标检测模型(face.pt
用于人脸;glasses.pt
用于眼镜),希望不改动 Ultralytics 源码,通过脚本实现“先做人脸检测,再在人脸 ROI 内做人眼镜检测”的两级推理。
该方案通过调用 YOLOv8 的官方 Python API 进行串联,而不需要修改 Ultralytics 的源代码。不仅保证了代码的兼容性,还能够利用 YOLOv8 现有的优化和功能。
关键步骤:
加载人脸检测模型(一级检测):
首先,我们加载一个用于人脸检测的YOLOv8模型(face.pt
)。该模型能够快速准确地识别图像中的人脸区域。对输入图像进行人脸检测:
将输入图像传入人脸检测模型,获取图像中的人脸框(bounding boxes)。这些框标识了图像中所有检测到的人脸的位置。裁剪出人脸区域(ROI):
基于检测到的人脸框,将每个框内的区域裁剪出来,得到单独的人脸子图。每个子图即为接下来进行眼镜检测的区域。加载眼镜检测模型(二级检测):
在提取出的人脸区域内,我们使用另一个YOLOv8模型(glasses.pt
)进行眼镜检测。该模型专门用于识别眼镜,并能高效地检测出每个人脸中的眼镜框。回填坐标并绘制结果:
由于眼镜检测得到的坐标是相对于人脸区域的,我们需要将这些坐标映射回原图的坐标系。通过回填坐标,将眼镜框准确地绘制在人脸区域上。显示或保存检测结果:
最后,根据需求,展示实时检测结果,或者将检测后的图像保存到指定目录中。
代码如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Cascade inference: face ➜ glasses(两级推理脚本)
- 不修改 Ultralytics 源码,仅用官方 Python API 串联两次推理
- 支持图片/文件夹/视频文件/摄像头/网络流(与 YOLO 原生接口保持一致)
- 图片会保存到 runs/cascade/;视频或摄像头默认仅显示(可按需扩展为视频写出)
"""import argparse
from pathlib import Path
import itertools
import cv2
import torch
from ultralytics import YOLOdef clamp(v, lo, hi):"""将坐标值限制在 [lo, hi] 区间内,避免越界。"""return max(lo, min(hi, v))def is_image_path(p: str) -> bool:"""简单判断是否为图片路径(用于保存命名)。"""if not isinstance(p, str):return Falsesuffix = Path(p).suffix.lower()return suffix in {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp"}def run(face_w, # 人脸检测权重路径,如 "face.pt"glass_w, # 眼镜检测权重路径,如 "glasses.pt"source, # 输入源:图片/文件夹/视频/摄像头 ID/URLimgsz=640, # 一级(人脸)输入尺寸g_imgsz=256, # 二级(眼镜)输入尺寸(ROI 更小,建议取 224~320)conf_face=0.25, # 人脸检测置信度阈值conf_glass=0.50, # 眼镜检测置信度阈值(可适当更严)iou_face=0.45, # 人脸 NMS IoU 阈值iou_glass=0.45, # 眼镜 NMS IoU 阈值save=False, # 是否保存图片结果(视频/摄像头默认逐帧图片保存,可按需扩展为视频写出)device='' # 计算设备:''(自动)、'cpu'、'cuda:0' 等
):"""两级推理主流程:1) 先对整帧做人脸检测,得到 face boxes;2) 对每个 face ROI 批量运行眼镜检测;3) 将眼镜框坐标平移回原图坐标系后叠加绘制与保存/显示。"""# ① 加载两个模型,可自动选择设备或手动指定face_model = YOLO(face_w).to(device)glass_model = YOLO(glass_w).to(device)# 计数器:用于对无文件名来源(如摄像头)生成递增帧名frame_counter = itertools.count(start=0, step=1)# ② 遍历输入流:支持文件夹/视频/摄像头/URL(与 YOLO 保持一致)# 使用 stream=True 逐帧生成 Result,避免一次性占用过多内存with torch.inference_mode():for res in face_model(source,stream=True,imgsz=imgsz,conf=conf_face,iou=iou_face,verbose=False):# 原始 BGR 图像(numpy.ndarray)frame = res.orig_imgH, W = frame.shape[:2]# 收集“眼镜”的最终框 [(x1,y1,x2,y2), score]eyeglass_dets = []# ③ 批量裁剪所有人脸 ROI,并记录偏移量以便回填坐标crops, offsets = [], []for b in res.boxes:# b.xyxy[0] 是 tensor,需转为 Python 数值(.tolist() 会拷到 CPU)x1, y1, x2, y2 = map(int, b.xyxy[0].tolist())# 安全裁剪:坐标边界约束,避免越界报错x1 = clamp(x1, 0, W - 1)y1 = clamp(y1, 0, H - 1)x2 = clamp(x2, 0, W - 1)y2 = clamp(y2, 0, H - 1)# 跳过异常或过小的框if x2 - x1 <= 1 or y2 - y1 <= 1:continue# 取 ROI(注意 copy 避免引用同一内存导致画图时污染)roi = frame[y1:y2, x1:x2].copy()crops.append(roi)offsets.append((x1, y1))# ④ 在所有 ROI 上批量跑眼镜检测(比逐个调用更快)if crops:g_results = glass_model(crops,imgsz=g_imgsz,conf=conf_glass,iou=iou_glass,verbose=False)# 将每个 ROI 的眼镜框坐标平移回原图坐标for g_res, (ox, oy) in zip(g_results, offsets):for g in g_res.boxes:gx1, gy1, gx2, gy2 = map(int, g.xyxy[0].tolist())# 平移至原图坐标系X1, Y1 = gx1 + ox, gy1 + oyX2, Y2 = gx2 + ox, gy2 + oy# 再次边界约束(理论上已在人脸框内,无需,但稳妥起见)X1 = clamp(X1, 0, W - 1)Y1 = clamp(Y1, 0, H - 1)X2 = clamp(X2, 0, W - 1)Y2 = clamp(Y2, 0, H - 1)eyeglass_dets.append(((X1, Y1, X2, Y2), float(g.conf)))# ⑤ 在原图上绘制眼镜框与置信度for (x1, y1, x2, y2), score in eyeglass_dets:cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) # 绿色框cv2.putText(frame, f'{score:.2f}', (x1, max(0, y1 - 6)),cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)if save:save_dir = Path('runs/cascade')save_dir.mkdir(parents=True, exist_ok=True)# 有文件名的(图片/视频帧)尽量沿用原文件名;否则用递增帧号if is_image_path(res.path):out_name = Path(res.path).name # 保留原图名else:idx = next(frame_counter)stem = Path(res.path).stem if isinstance(res.path, str) else "frame"out_name = f"{stem}_{idx:06d}.jpg"cv2.imwrite(str(save_dir / out_name), frame)cv2.destroyAllWindows()def parse_args():"""命令行参数解析。"""p = argparse.ArgumentParser(description="YOLOv8 级联检测脚本(face ➜ glasses)· 零改源码")p.add_argument('--face', help='人脸检测权重,如 face.pt')p.add_argument('--glasses', help='眼镜检测权重,如 glasses.pt')p.add_argument('--source', help='输入源:图片/文件夹/视频/摄像头 ID/网络流 URL(如 0/rtsp://...)')p.add_argument('--imgsz', type=int, default=640, help='一级检测(人脸)输入尺寸')p.add_argument('--g-imgsz', type=int, default=256, help='二级检测(眼镜)输入尺寸')p.add_argument('--conf-face', type=float, default=0.25, help='人脸检测置信度阈值')p.add_argument('--conf-glass', type=float, default=0.4, help='眼镜检测置信度阈值')p.add_argument('--iou-face', type=float, default=0.45, help='人脸 NMS IoU 阈值')p.add_argument('--iou-glass', type=float, default=0.45, help='眼镜 NMS IoU 阈值')p.add_argument('--save', default=True, help='将结果保存到 runs/cascade/')p.add_argument('--device', default=0, help='计算设备:""(自动)、"cpu"、"cuda:0"、"cuda:1" 等')return p.parse_args()if __name__ == "__main__":args = parse_args()run(face_w=args.face,glass_w=args.glasses,source=args.source,imgsz=args.imgsz,g_imgsz=args.g_imgsz,conf_face=args.conf_face,conf_glass=args.conf_glass,iou_face=args.iou_face,iou_glass=args.iou_glass,save=args.save,device=args.device)
经过实验,代码逻辑没问题,能跑通。