☰
目标检测与人流统计:OpenCV+YOLO+FastAPI部署实践
2026/10/5 2:39:53 网站建设 项目流程

“你都看到了谁?”

这句话放在监控画面、活动照片或者一段视频素材里,本质上是在问:画面里有哪些人、分别在什么位置、总共有多少人、能不能继续做轨迹或行为分析。放到计算机视觉里,对应一条很标准的工程链路——目标检测、人影识别、人数统计、批量处理和接口服务化。

这篇文章不绑定某个具体开源仓库,而是把这条链路从模型选择、环境配置、服务部署、功能测试到 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\activate

4.2 安装依赖

pip install -r requirements.txt

requirements.txt 内容示例:

opencv-python numpy onnxruntime fastapi uvicorn python-multipart requests

4.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 8000

6.2 curl 调用示例

curl -X POST http://127.0.0.1:8000/detect \ -F "file=@inputs/test.jpg" \ -o outputs/api_result.jpg

Python 端调用也一样:

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-smi

Windows 下可以使用任务管理器中的 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. 总结与下一步

“你都看到了谁”听起来像一个哲学问题,在工程里它就是“目标检测模型输出的一组带类别和坐标的框”。这套链路最值得尝试的地方在于模块清晰、替换成本低:模型可以换,后处理可以改,接口可以自行定义,从零到能跑通一张图片,可能只需要半天时间。

最先应该验证的不是高级功能,而是三个最小闭环:

  1. 单张图片能否正确输出带框结果;
  2. 连续图片处理是否稳定;
  3. 接口能否在局域网内被其他程序调用。

最容易踩的坑集中在后处理和路径管理:输出格式不对,再强的模型也画不出正确位置;路径写死,换个目录就要改代码。

后续可以继续扩展的方向也很明确:加入跟踪算法做多目标轨迹分析,增加人群密度估计,把检测结果输出成结构化数据供报表使用,或者在摄像头画面上做人脸脱敏处理后再存储。每一步都是在现有检测能力上叠加模块,而不是另起炉灶。先把“识别”这件事跑稳,后面的路就好走了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询