“你都看到了谁?”
这句话放在监控画面、活动照片或者一段视频素材里,本质上是在问:画面里有哪些人、分别在什么位置、总共有多少人、能不能继续做轨迹或行为分析。放到计算机视觉里,对应一条很标准的工程链路——目标检测、人影识别、人数统计、批量处理和接口服务化。
这篇文章不绑定某个具体开源仓库,而是把这条链路从模型选择、环境配置、服务部署、功能测试到 API 封装完整走一遍。你拿到任何一个基于 OpenCV 或 YOLO 系模型的检测项目,都可以按这个思路快速跑通。
先说几个关键结论,方便判断值不值得看:
- 硬件门槛不算高。单张图片检测用 CPU 就能跑,视频流或摄像头实时处理建议有 NVIDIA 显卡。
- 部署难度低于很多 AIGC 项目。核心依赖基本是 Python 环境加 OpenCV、NumPy、ONNX Runtime。
- 可扩展性不错。检测结果既能输出到本地图片,也能打包成 HTTP 接口,还能挂到批量任务目录里跑。
下面按工程落地顺序拆解。
1. 核心能力速览
下面这组指标按常见开源检测模型的标准用法整理,不绑定某个具体仓库版本,具体参数以你选择的模型权重为准。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 计算机视觉目标检测 / 人影识别 / 人群分析 |
| 典型技术栈 | OpenCV、ONNX Runtime、PyTorch(可选)、FastAPI |
| 主要功能 | 图片与视频中的人体、人脸、常见物体检测,输出坐标、类别、置信度 |
| 推荐硬件 | CPU 可运行;视频流与摄像头场景建议 NVIDIA GPU |
| 显存占用 | 取决于模型规格、输入分辨率和批量大小,需按实际测试确定 |
| 支持平台 | Windows / Linux / macOS,部分算子依赖需单独验证 |
| 启动方式 | 命令行脚本、HTTP 服务、Docker 容器(可选) |
| API 能力 | 可通过 FastAPI 封装图片上传、检测结果返回 |
| 批量任务 | 支持图片目录批量处理、视频文件列表排队推理 |
| 适合场景 | 图片检索、素材审核、人流统计、体育视频分析、AIGC 人影合规检查 |
一句话总结:这不是某个“全家桶”软件,而是一条可组合的检测技术路线。模型负责“认人”,OpenCV 负责“画框”,FastAPI 负责“对外服务”,三部分解耦,方便按自己的场景替换其中任意一段。
2. 适用场景与使用边界
2.1 适合做什么
- 图片素材库人形检索:从大量照片里筛选包含人物的图片,按坐标裁出人脸或人体区域。
- 人流统计:对商场、园区、展厅的单目摄像头画面做人数统计,输出每个时间点的在场人数。
- 体育视频分析:识别运动员位置,配合跟踪算法生成跑动轨迹、热点区域。
- AIGC 审核:批量检查 AI 生成图片里是否出现未预期人物,判断人物数量是否超出合规范围。
2.2 不适合做什么
- 身份核验。通用目标检测模型只能回答“这里有人”,回答不了“这个人是谁”。人脸比对、证件照核验是另一条需要专门资质和合规渠道的技术路线。
- 高精度人数统计。单目视角会出现重叠遮挡,模型输出的是“检测框数量”,不是“真实人头数”。想要精确计数,需要设计跨帧去重或配合深度摄像头。
- 低算力嵌入式设备直接跑大模型。如果板子只有 2GB 内存,优先选择轻量模型并降低输入分辨率,而不是直接加载标准尺寸的 ONNX 模型。
2.3 隐私与合规边界
这一点必须前置。凡是使用含真实人物图像的素材,都要确认是否获得肖像授权;在公共区域部署摄像头采集和分析,需要符合当地隐私保护法规,并在显眼位置公示用途。检测结果如果保存了带坐标的图片或 JSON 日志,应限制访问权限,不应该把包含人脸位置的原始数据放到公网目录里。
3. 环境准备与前置条件
3.1 硬件要求
先看本机环境。CPU 可以使用 OpenCV DNN 或 ONNX Runtime 跑推理,单张图片通常几百毫秒到几秒不等;处理 1080p 视频流时,CPU 很难达到实时帧率。NVIDIA GPU 能明显提速,但先确认驱动能正常输出:
nvidia-smi能显示显卡型号和驱动版本说明 GPU 可用。显卡比较老也不要紧,很多检测模型对显卡架构不敏感,重点看显存容量和安装的 CUDA 版本是否匹配。显存大小直接决定输入分辨率和批大小,8GB 以上会从容很多,4GB 也可以运行轻量模型。
3.2 软件依赖
建议用 Python 3.8 或更高版本。核心依赖如下:
- opencv-python:负责图片读写、画框、NMS 后处理,以及 DNN 推理。
- numpy:数组操作和坐标计算。
- onnxruntime:ONNX 模型推理,相比 OpenCV DNN 在某些模型上兼容性更好。
- fastapi / uvicorn:把检测逻辑封装成 HTTP 接口。
- python-multipart:FastAPI 接收文件上传时使用。
4. 安装部署与启动方式
4.1 创建虚拟环境
推荐先创建独立虚拟环境,避免和系统 Python 包冲突。
python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate4.2 安装依赖
pip install -r requirements.txtrequirements.txt 内容示例:
opencv-python numpy onnxruntime fastapi uvicorn python-multipart requests4.3 准备模型文件
常见检测模型导出后会得到两个文件:
- 模型文件,通常是 .onnx 或 .pt,例如
yolo.onnx。 - 类别名文件,通常是
.names,例如coco.names,每行一个类别名,顺序必须和模型输出头一致。
建议把模型文件和类别文件放进models/目录,检测结果统一输出到outputs/目录。路径写死会让后续维护很痛苦,尽量用相对路径加配置常量。
4.4 单图检测脚本
下面脚本是一个通用模板,目标是把一张图片读进来、推理、画框、保存结果。这里以 YOLO 系常见的[1, N, 85]输出格式为例,如果你的模型输出是[1, 84, N]或者其他格式,需要按实际尺寸调整后处理。
import cv2 import numpy as np MODEL_PATH = "./models/yolo.onnx" NAMES_PATH = "./models/coco.names" CONF_THRESHOLD = 0.4 IOU_THRESHOLD = 0.5 INPUT_SIZE = (640, 640) def load_names(path): with open(path, "r", encoding="utf-8") as f: return [line.strip() for line in f.readlines()] def detect_image(image_path, output_path): names = load_names(NAMES_PATH) net = cv2.dnn.readNetFromONNX(MODEL_PATH) image = cv2.imread(image_path) if image is None: raise ValueError(f"读取图片失败: {image_path}") h, w = image.shape[:2] blob = cv2.dnn.blobFromImage( image, scalefactor=1.0 / 255.0, size=INPUT_SIZE, mean=(0, 0, 0), swapRB=True, crop=False, ) net.setInput(blob) output = net.forward() # 假设输出形状是 [1, N, 85],最后一维为 cx, cy, w, h + 80 个类别分数 output = output[0] boxes, confidences, class_ids = [], [], [] scale_x = w / INPUT_SIZE[0] scale_y = h / INPUT_SIZE[1] for row in output: scores = row[4:] class_id = int(np.argmax(scores)) confidence = float(scores[class_id]) if confidence < CONF_THRESHOLD: continue cx, cy, bw, bh = row[:4] * [scale_x, scale_y, scale_x, scale_y] x = int(cx - bw / 2) y = int(cy - bh / 2) boxes.append([x, y, int(bw), int(bh)]) confidences.append(confidence) class_ids.append(class_id) indexes = cv2.dnn.NMSBoxes(boxes, confidences, CONF_THRESHOLD, IOU_THRESHOLD) if len(indexes) > 0: indexes = indexes.flatten() for i in indexes: x, y, bw, bh = boxes[i] label = f"{names[class_ids[i]]} {confidences[i]:.2f}" cv2.rectangle(image, (x, y), (x + bw, y + bh), (0, 255, 0), 2) cv2.putText(image, label, (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imwrite(output_path, image) print(f"检测完成,共 {len(indexes) if len(indexes) > 0 else 0} 个目标,结果: {output_path}") if __name__ == "__main__": detect_image("inputs/test.jpg", "outputs/result.jpg")跑一行命令验证基础链路:
python detect.py如果当前目录缺少inputs/test.jpg或模型文件,先补齐再验证。
5. 功能测试与效果验证
项目能跑起来的第一个信号是:输入一张图片,输出一张带框的图片。不要直接上大分辨率视频,先用单图确认模型、类别文件、后处理代码是否对齐。
5.1 单张图片检测
准备一张至少包含一个人物的图片,放到inputs/目录。运行上面的脚本后,检查outputs/result.jpg:
- 人物位置是否被正确框出。
- 框是否明显偏移或过大过小。
- 置信度数值是否可信,如果所有目标都小于 0.25,可能模型或输入尺寸有问题。
5.2 视频文件检测
单图稳定后,再做视频检测。思路是逐帧读取、逐帧推理、合并写回视频:
import cv2 def detect_frame(frame, net, names): h, w = frame.shape[:2] blob = cv2.dnn.blobFromImage(frame, 1.0 / 255.0, (640, 640), swapRB=True, crop=False) net.setInput(blob) output = net.forward()[0] boxes, confidences, class_ids = [], [], [] scale_x = w / 640.0 scale_y = h / 640.0 for row in output: scores = row[4:] class_id = int(np.argmax(scores)) confidence = float(scores[class_id]) if confidence < 0.4: continue cx, cy, bw, bh = row[:4] * [scale_x, scale_y, scale_x, scale_y] boxes.append([int(cx - bw / 2), int(cy - bh / 2), int(bw), int(bh)]) confidences.append(confidence) class_ids.append(class_id) indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.4, 0.5) if len(indexes) > 0: indexes = indexes.flatten() for i in indexes: x, y, bw, bh = boxes[i] cv2.rectangle(frame, (x, y), (x + bw, y + bh), (0, 255, 0), 2) return frame def detect_video(input_path, output_path): net = cv2.dnn.readNetFromONNX("./models/yolo.onnx") cap = cv2.VideoCapture(input_path) fps = int(cap.get(cv2.CAP_PROP_FPS)) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height)) while True: ok, frame = cap.read() if not ok: break frame = detect_frame(frame, net, None) writer.write(frame) cap.release() writer.release() print(f"视频检测完成: {output_path}") if __name__ == "__main__": detect_video("inputs/test.mp4", "outputs/result.mp4")这里有个性能预期问题:视频推理耗时等于“逐帧推理耗时 x 帧数”,如果单帧要 80ms,25fps 的视频每秒实际只能处理 12 帧左右,生成的视频会明显比原视频慢。验证时先截取 10 秒片段测试,不要一上来处理整个长视频。
5.3 摄像头实时检测
服务器或本机有摄像头时,可以替换输入源:
cap = cv2.VideoCapture(0) while True: ok, frame = cap.read() if not ok: break frame = detect_frame(frame, net, None) cv2.imshow("detect", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()摄像头场景最容易出现的问题是分辨率过高导致推理延迟明显,建议先用cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)这类方式把输入分辨率限制住。
5.4 判断标准与常见失败原因
功能验证的通过标准可以划分为三层:
- 基础通过:单张图片能输出带框结果,类别名正确。
- 稳定通过:连续 100 张图片无崩溃、无内存持续上涨、漏检率可接受。
- 生产通过:视频或摄像头能稳定运行,单帧耗时稳定,显存不溢出。
常见失败原因如下:
| 现象 | 原因 |
|---|---|
| 完全没有框 | 模型路径错误、类别文件顺序错误、置信度阈值过高 |
| 框位置偏移 | 输入缩放比例计算错误、blob 的 scale 和 size 设置不一致 |
| 同一目标多个框 | NMS 阈值设置不合理、后处理没有正确调用 NMSBoxes |
| 推理速度很慢 | 输入分辨率过大、CPU 推理、模型文件过大 |
| 视频写出后无法播放 | fourcc 编码不支持、fps 与原视频不一致 |
6. 接口 API 与批量任务
检测脚本只能自己用,想要接到业务系统里,需要包一层 HTTP 接口。下面用 FastAPI 封装一个最简单的图片检测接口。
6.1 FastAPI 检测服务
import os import uuid import cv2 from fastapi import FastAPI, UploadFile, File from fastapi.responses import FileResponse app = FastAPI() TMP_INPUT_DIR = "./tmp_input" TMP_OUTPUT_DIR = "./tmp_output" os.makedirs(TMP_INPUT_DIR, exist_ok=True) os.makedirs(TMP_OUTPUT_DIR, exist_ok=True) @app.post("/detect") async def detect(file: UploadFile = File(...)): suffix = os.path.splitext(file.filename)[-1] task_id = uuid.uuid4().hex input_path = os.path.join(TMP_INPUT_DIR, f"{task_id}{suffix}") output_path = os.path.join(TMP_OUTPUT_DIR, f"{task_id}_result.jpg") with open(input_path, "wb") as f: f.write(await file.read()) # 这里复用 detect_image 的逻辑,可把模型加载放到 app 启动时完成 detect_image(input_path, output_path) return FileResponse(output_path, media_type="image/jpeg") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="127.0.0.1", port=8000)生产环境中模型初始化不应该放在每次请求里,建议把net和names放到全局变量,启动时加载一次,请求时只走推理。
启动服务:
uvicorn main:app --host 127.0.0.1 --port 80006.2 curl 调用示例
curl -X POST http://127.0.0.1:8000/detect \ -F "file=@inputs/test.jpg" \ -o outputs/api_result.jpgPython 端调用也一样:
import requests url = "http://127.0.0.1:8000/detect" files = {"file": open("inputs/test.jpg", "rb")} response = requests.post(url, files=files, timeout=30) if response.status_code == 200: with open("outputs/api_result.jpg", "wb") as f: f.write(response.content) print("接口调用成功") else: print("接口调用失败:", response.status_code)如果接口返回的不是图片而是 JSON,更适合对接业务系统。可以在接口里把检测框、类别、置信度序列化后返回:
{ "objects": [ { "class": "person", "confidence": 0.83, "box": [120, 45, 300, 520] } ] }具体返回结构取决于你自己的定义,关键是先跑通“上传图片 -> 获取结果”这个最小闭环。
6.3 批量任务设计与失败重试
批量任务的核心诉求是:给一批文件,程序自动逐个处理,跑完输出结果清单。最简单的目录遍历方式:
import os from pathlib import Path input_dir = Path("./inputs_batch") output_dir = Path("./outputs_batch") output_dir.mkdir(exist_ok=True) for image_path in input_dir.glob("*.jpg"): output_path = output_dir / f"{image_path.stem}_result.jpg" try: detect_image(str(image_path), str(output_path)) except Exception as e: print(f"处理失败: {image_path} -> {e}")更稳妥的批量任务要加进度记录和失败重试:
- 用一个
results.csv记录每个文件的成功状态。 - 失败文件单独写入
failed.txt。 - 重跑时跳过已经成功的结果,只处理失败列表。
- 每处理完一批,打印这轮的耗时、平均单张耗时、失败数量。
批量任务最常见的坑有两个:一是内存持续上涨,要确保每张图片处理后变量被回收;二是中途崩溃导致前面白跑,所以日志和断点续跑比单线程硬跑更值得投入时间。
7. 资源占用与性能观察
7.1 查看显存与 CPU 占用
Linux 下实时观察 GPU 显存:
watch -n 2 nvidia-smiWindows 下可以使用任务管理器中的 GPU 性能面板。推理过程中主要观察显存占用是否稳定、是否持续增长。如果显存持续增长,大概率是每帧生成的对象没有被回收或缓存没有清理。
CPU 占用可以通过top或任务管理器查看。ONNX Runtime 默认会启用多线程,小模型在 CPU 上可能只用到几十到几百 MB 内存,视频场景则主要吃 CPU 算力。
7.2 影响性能的主要因素
- 模型规格:输入分辨率越大,计算量越大;模型参数量越大,消耗越高。
- 帧率与分辨率:1080p 视频需要逐帧缩放,预处理时间不可忽略。
- 批大小:同一帧内同时检测多张图会显著增加显存和内存占用。
- 后处理复杂度:NMS、类别过滤、画框叠加,在目标数量很多时也会影响整体延迟。
7.3 降低资源占用的方法
优先从输入分辨率下手。很多模型设计输入为 640x640,但实际业务图更大,先缩放能明显减少耗时。其次可以导出更小的模型变体,例如将标准模型转成量化后的 ONNX INT8 模型。变换精度前需要先用一批真实数据做精度对比,确认漏检率可以接受。
如果显存不足,还可以把推理从 GPU 切回 CPU 测试,虽然变慢但至少不爆显存。视频场景启用帧抽帧策略,例如每秒只检测 5 帧,其余帧沿用上一帧结果,也能大幅降低资源占用。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 依赖安装失败 | Python 版本不匹配 | 查看 pip 报错信息 | 升级 Python 或换用对应版本 |
| 模型文件无法加载 | ONNX 算子版本不兼容 | 检查 OpenCV/ONNX Runtime 版本 | 重新导出模型或升级运行时 |
| CUDA 不可用 | 驱动版本过旧或 PyTorch/ONNX 与 CUDA 版本不匹配 | 运行nvidia-smi和推理日志 | 更新显卡驱动或切换为 CPU 运行 |
| 显存不足 | 输入分辨率过大、批大小过大 | 观察 nvidia-smi | 缩小输入尺寸、降批大小、换轻量模型 |
| 页面或接口无法访问 | 服务未启动或端口被占用 | 检查日志与netstat -ano | 换端口或重启服务 |
| API 返回 413 | 上传文件过大 | 查看 Web 服务配置 | 增大 body 限制或先压缩图片 |
| 批量任务卡住 | 单个文件阻塞或内存溢出 | 查看日志定位卡住文件 | 加超时控制和失败重试 |
| 检测结果飘忽不定 | 模型不适合场景、光照变化大 | 用多个样张测试 | 更换模型或补充后处理规则 |
遇到问题时先做隔离:把“加载模型 - 推理 - 后处理 - 写文件”四段分别打日志,定位问题发生在哪一段。不要一次性改多个参数,改完一项再验证。
9. 最佳实践与使用建议
- 第一次一定用小参数测试。不要一上来就处理 4K 长视频,先跑一张图、一小段视频、一个只有两张图片的目录。
- 保留一套最小可运行配置。模型文件、类别文件、测试图片、运行脚本整理进同一个项目目录,记录使用的依赖版本,避免换机器后环境对不上。
- 输入素材、输出结果、日志文件分开管理。目录结构可以按
inputs/ outputs/ logs/ models/组织,清理数据时不会误删模型。 - 检测接口要控制访问范围。FastAPI 默认监听 127.0.0.1,对外提供服务时应放到内网或加授权,不要直接把上传接口暴露到公网。
- 批量任务必须加日志和失败重试。一句
try except加一个失败列表,能省下大量排查时间。 - 涉及人脸、声音、版权素材时必须确认授权。目标检测本身只是框出物体,但把结果用于人员追踪、统计或商业化分析时,要遵守隐私和数据保护要求。
- 发布或商用前要做效果复核。特定场景下漏检率可能很高,例如背对镜头、遮挡严重、暗光环境。用至少几百张实际业务图做抽样验证,比单独跑一张好看的效果图更有说服力。
10. 总结与下一步
“你都看到了谁”听起来像一个哲学问题,在工程里它就是“目标检测模型输出的一组带类别和坐标的框”。这套链路最值得尝试的地方在于模块清晰、替换成本低:模型可以换,后处理可以改,接口可以自行定义,从零到能跑通一张图片,可能只需要半天时间。
最先应该验证的不是高级功能,而是三个最小闭环:
- 单张图片能否正确输出带框结果;
- 连续图片处理是否稳定;
- 接口能否在局域网内被其他程序调用。
最容易踩的坑集中在后处理和路径管理:输出格式不对,再强的模型也画不出正确位置;路径写死,换个目录就要改代码。
后续可以继续扩展的方向也很明确:加入跟踪算法做多目标轨迹分析,增加人群密度估计,把检测结果输出成结构化数据供报表使用,或者在摄像头画面上做人脸脱敏处理后再存储。每一步都是在现有检测能力上叠加模块,而不是另起炉灶。先把“识别”这件事跑稳,后面的路就好走了。