CLIP+YOLO多模态目标检测:开放词汇视频监控检索实战
发布时间:2026/9/27 23:07:05来源:尧图网络
简介这份资源面向计算机视觉与智能安防方向的学习者提供一套融合CLIP与YOLO的智能视频监控系统实现方案重点解决实时目标检测与自然语言检索监控画面两大问题。包内共11个文件以Python脚本、zbak备份文件、zip压缩包为主另含txt依赖说明、md说明文档、png预览图与gitignore配置整体约3.83MB结构轻量便于快速上手。已有47人学习下载属于小众但方向明确的实战型资料。读者可从中获取文本化检索、并行视频流处理、中英文双语查询适配、反例样本生成等核心模块的代码实现并借助README与预览图理解系统架构与运行效果适合作为安防监控、视频内容解析及多模态检索方向的课程设计或自学参考帮助建立从模型调用到系统集成的完整认知。1. 当监控画面能“听懂人话”CLIP 加 YOLO 到底在解决什么凌晨两点保安盯着十六路监控画面想找“一个穿红衣服、背着黑色双肩包的人”只能一路路回放、一帧帧扫。传统视频监控的痛点从来不是“拍不到”而是“找不到”——检索维度只有时间轴没有语义。基于 CLIP 与 YOLO 的智能视频监控系统要解决的就是这件事让 YOLO 负责“看见”画面里有什么让 CLIP 负责“听懂”你想找什么两者拼成一条从自然语言到目标框的检索链路。它适合做安防二次开发、边缘盒子部署、校园或园区行为分析的从业者也适合想入门多模态目标检测的个人学习者。这套架构的核心价值在于你不需要为每个新检索词重新训练检测器改一句文本描述就能换一个查询目标这在真实项目里省下的是标注和重训的整条流水线。2. 拆开这套架构YOLO 管检测CLIP 管语义对齐2.1 两个模型各干什么为什么不能只留一个YOLO 是单阶段目标检测器输入一帧图像输出一组边界框加类别标签比如 person、car、backpack。它的强项是快和准在 GPU 上跑 1080p 视频轻松过 30 FPS边缘设备上量化后也能实时。但它有个硬边界类别是训练时定死的。COCO 的 80 类里没有“红色外套的人”你也没法用一句话让它去找“左手拿伞的人”。CLIP 是图文对比学习模型由文本编码器和图像编码器组成把图片和文字映射到同一个向量空间靠余弦相似度判断“这段文字像不像这张图”。它天生支持开放词汇你写什么它就能比什么但它不做定位——它告诉你“这张图整体像不像这句话”不告诉你目标在哪。所以单用谁都不行。只留 YOLO检索词被锁死在类别表里只留 CLIP你拿不到精确坐标做不了框选和告警。常见做法是让 YOLO 先出候选框把每个框裁出来送进 CLIP 图像编码器再和文本向量比相似度用阈值筛掉不相关的框。这样既保留了 YOLO 的定位精度又拿到了 CLIP 的开放词汇能力。2.2 多模态查询链路的四个环节整条链路拆开是四步抽帧、检测、编码、匹配。第一步抽帧。视频不是逐帧都有用常见做法是按固定间隔抽比如每 5 帧取 1 帧或者用场景变化检测只在画面大幅变动时抽。抽帧率直接决定后面算力开销25 FPS 的视频每秒抽 5 帧一小时就是 18000 张图这个量级要先算清楚。第二步检测。YOLO 对每张抽出来的帧做推理输出框坐标、置信度、类别。这里要设一个检测置信度阈值太低会引入大量噪声框太高会漏掉小目标。我一般从 0.25 起步按实际漏检情况调。第三步编码。把每个检测框从原图裁出来缩放到 CLIP 图像编码器要求的输入尺寸常见是 224×224送进图像编码器得到图像向量。同时把用户的查询文本送进文本编码器得到文本向量。文本向量可以预先算好缓存图像向量必须每帧现算。第四步匹配。对每个框算图像向量和文本向量的余弦相似度设一个相似度阈值比如 0.25高于阈值的框保留低于的丢弃。最终输出的是“满足这句文本描述的目标框”。提示文本向量只跟查询词有关同一句查询在整个视频里只需要编码一次务必缓存否则每帧都跑文本编码器是纯浪费。2.3 最小可跑通的原型代码下面这段是链路的最小实现用 ultralytics 的 YOLO 和 open_clip 搭跑通之后再谈优化。import cv2 import torch import open_clip from ultralytics import YOLO # 加载模型YOLO 负责检测CLIP 负责图文对齐 yolo YOLO(yolov8n.pt) # 轻量版边缘设备友好 clip_model, _, preprocess open_clip.create_model_and_transforms( ViT-B-32, pretrainedlaion2b_s34b_b79k ) tokenizer open_clip.get_tokenizer(ViT-B-32) clip_model.eval() device cuda if torch.cuda.is_available() else cpu yolo.to(device) clip_model.to(device) def encode_text(query): # 文本向量只算一次缓存复用 tokens tokenizer([query]).to(device) with torch.no_grad(): feat clip_model.encode_text(tokens) return feat / feat.norm(dim-1, keepdimTrue) def search_frame(frame, text_feat, det_conf0.25, sim_thresh0.25): results yolo(frame, confdet_conf, verboseFalse)[0] hits [] for box in results.boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) crop frame[y1:y2, x1:x2] if crop.size 0: continue # 裁剪图转 CLIP 输入张量 img preprocess(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)).unsqueeze(0).to(device) with torch.no_grad(): img_feat clip_model.encode_image(img) img_feat img_feat / img_feat.norm(dim-1, keepdimTrue) sim (img_feat text_feat.T).item() if sim sim_thresh: hits.append((box, sim)) return hits # 使用示例 text_feat encode_text(a person wearing a red jacket) cap cv2.VideoCapture(test.mp4) while True: ret, frame cap.read() if not ret: break for box, sim in search_frame(frame, text_feat): x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f{sim:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(result, frame) if cv2.waitKey(1) 0xFF 27: break逻辑说明encode_text把查询词编码成归一化文本向量只调一次。search_frame对单帧跑 YOLO逐个裁框、编码、算相似度。归一化之后点积就是余弦相似度省一次除法。参数上det_conf控制 YOLO 出框的松紧sim_thresh控制 CLIP 匹配的松紧这两个阈值要分开调别混在一起。参数说明yolov8n.pt是最小模型换成yolov8s或yolov8m精度更高但更慢ViT-B-32是 CLIP 里较轻的骨干显存吃紧可以换ViT-B-16以下laion2b_s34b_b79k是开放预训练权重中文场景下英文查询词效果通常更稳。3. 从单帧到实时流抽帧、批处理与阈值调参3.1 抽帧策略决定系统吞吐上限上一章的代码是逐帧处理真实监控流是 25 FPS逐帧跑 CLIP 编码根本扛不住。假设 YOLO 出 10 个框每帧就要跑 10 次 CLIP 图像编码25 FPS 下每秒 250 次编码一张 224×224 的图在 V100 上单次约 5ms光编码就 1.25 秒直接崩。常见做法是抽帧加跳帧。抽帧按时间间隔比如每 200ms 处理一帧等于 5 FPS 的有效处理率跳帧按帧号取模if frame_id % 5 ! 0: continue。两者效果类似抽帧更好控节奏。如果场景里目标移动慢比如停车场1 FPS 都够如果是快速跑动至少 5 FPS。另一个优化是批处理。把一帧里的多个裁剪框拼成一个 batch 送进 CLIP比逐个送快得多GPU 利用率能从 30% 拉到 80% 以上。下面是把编码改成批量的写法。def search_frame_batch(frame, text_feat, det_conf0.25, sim_thresh0.25, batch_size16): results yolo(frame, confdet_conf, verboseFalse)[0] boxes results.boxes if len(boxes) 0: return [] crops, valid_boxes [], [] for box in boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) crop frame[y1:y2, x1:x2] if crop.size 0: continue crops.append(preprocess(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))) valid_boxes.append(box) hits [] # 分批编码避免一次性占满显存 for i in range(0, len(crops), batch_size): batch torch.stack(crops[i:i batch_size]).to(device) with torch.no_grad(): feat clip_model.encode_image(batch) feat feat / feat.norm(dim-1, keepdimTrue) sims (feat text_feat.T).squeeze(-1) for j, sim in enumerate(sims.tolist()): if sim sim_thresh: hits.append((valid_boxes[i j], sim)) return hits逻辑说明先把所有裁剪图预处理成张量存列表再按batch_size分批送 GPU。torch.stack把同尺寸张量拼成 batchCLIP 图像编码器支持批量输入。相似度算完用.tolist()转回 Python 浮点方便逐框判断。参数说明batch_size按显存调8G 显存跑 ViT-B-32 可以到 32跑 ViT-L-14 建议 8 以下。批处理不改变结果只改变速度阈值逻辑完全一致。3.2 两个阈值怎么调才不玄学检测置信度和相似度阈值是这套系统里最容易被调崩的两个参数。调参不是拍脑袋要有观察指标。检测置信度低0.1~0.2框多召回高但 CLIP 要编码的框也多算力涨误检概率涨。检测置信度高0.5 以上框少快但小目标和遮挡目标容易漏。我一般先用 0.25 跑一段看漏检情况漏了就降到 0.15误检多了就升到 0.35。相似度阈值低0.15~0.2宽松能召回更多相关框但会把“有点像”的框也放进来。相似度高0.3 以上严格只留高置信匹配但可能一个框都不剩。CLIP 的余弦相似度分布跟模型和查询词都有关没有万能值。稳妥做法是先跑一批样本把相似度打出来看分布取能分开正负样本的那个点。注意别拿检测置信度当相似度阈值用两者量纲和含义完全不同混用是新手最常见的翻车点。3.3 边缘部署时的模型选型对照边缘盒子算力有限模型选型直接决定能不能实时。下面这张表是我在 RK3588 和 Jetson 上实测后的经验区间具体数字随固件和量化方式浮动。组件轻量方案均衡方案高精度方案YOLOyolov8nyolov8syolov8mCLIP 图像编码ViT-B-32ViT-B-16ViT-L-14显存占用约 1.5G约 3G约 6G单帧检测编码30~50ms60~100ms150ms适用场景单路 5FPS双路 5FPS服务器端选型原则是先保实时再保精度。边缘设备上 yolov8n 加 ViT-B-32 是能跑起来的底线组合再往上就要看具体芯片的 NPU 支持情况。YOLO 导出成 ONNX 或 RKNN 后能吃到硬件加速CLIP 的图像编码器在多数边缘 NPU 上支持有限往往还是回落到 GPU 或 CPU这是部署时最容易低估的瓶颈。4. 避坑与排查那些让系统“看起来能跑”的暗坑4.1 现象框对了但相似度全是 0.1 上下检索不出东西原因CLIP 预训练权重和输入预处理不匹配。open_clip 里不同权重对应的归一化均值方差不一样用错preprocess会让图像向量整体偏移相似度被压平。另一个常见原因是查询词写得太抽象比如“可疑人员”CLIP 对抽象词区分度低。解决确认create_model_and_transforms返回的preprocess和pretrained是配套的别自己手写归一化。查询词尽量具体用“a person in a black hoodie”而不是“suspicious person”。如果必须用抽象词先跑一批样本看相似度分布再定阈值。4.2 现象YOLO 检测正常但 CLIP 编码报显存溢出原因裁剪框数量多且没分批一次性 stack 成大批次送 GPU。或者裁剪框尺寸差异大torch.stack要求同尺寸预处理后虽然统一到 224但如果中间有异常框没过滤会拼出超大张量。解决加batch_size分批并在预处理前过滤掉宽高小于 8 像素的框。显存监控用torch.cuda.memory_allocated()打点定位是哪一步涨上去的。4.3 现象同一段视频两次跑出来的框数量不一样原因YOLO 推理有随机性尤其是用了数据增强或非确定性算子时。另外抽帧如果按时间戳而不是帧号视频解码的浮点误差会导致抽到的帧不同。解决固定随机种子torch.manual_seed(42)YOLO 推理时关掉增强。抽帧统一按帧号取模别按时间戳。如果要做结果对比把抽到的帧存下来复用别每次重新解码。4.4 现象中文查询词效果明显差于英文原因多数开放 CLIP 权重以英文语料为主中文文本编码器没对齐好直接输中文相似度区分度低。解决常见做法是把中文查询词先翻成英文再编码或者换用支持中文的 CLIP 变体。如果项目必须中文输入可以在文本编码前加一层轻量翻译或者用中文 CLIP 权重但要重新验证相似度阈值。4.5 现象边缘设备上跑几分钟后帧率断崖下跌原因内存泄漏或显存碎片。常见于每帧都新建 tensor 却没释放或者cv2.VideoCapture缓冲堆积。解决推理包在torch.no_grad()里循环内避免保留大对象引用。cap.grab()跳帧比cap.read()更省不需要的帧直接 grab 掉不解码。定期调torch.cuda.empty_cache()但别每帧都调会拖慢。5. 进阶把检索结果做成可回查的事件流跑通实时检索只是第一步真实项目要的是“事后能查”。我一般会在检索命中时落一条事件记录包含时间戳、帧号、框坐标、相似度、查询词写进 SQLite 或轻量时序库。这样前端可以按时间回查也能统计某类目标出现的频次。import sqlite3, time conn sqlite3.connect(events.db) conn.execute(CREATE TABLE IF NOT EXISTS hits ( ts REAL, frame_id INTEGER, query TEXT, x1 INT, y1 INT, x2 INT, y2 INT, sim REAL)) def log_hit(frame_id, query, box, sim): x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) conn.execute(INSERT INTO hits VALUES (?,?,?,?,?,?,?,?), (time.time(), frame_id, query, x1, y1, x2, y2, sim)) conn.commit()逻辑说明命中即写库字段覆盖回查所需的最小集合。query存原始查询词方便按语义分组统计。sim存下来是为了后续调阈值时能离线重筛不用重跑视频。参数说明SQLite 适合单机轻量场景写入频率高时改成批量提交比如每 50 条 commit 一次避免频繁 IO。多路视频并发时换 PostgreSQL 或时序库。验证方法上我习惯拿一段已知答案的视频做回归人工标出目标出现的帧号区间跑系统看命中区间是否吻合算召回和误报。这套离线验证比在线盯屏靠谱得多也是调阈值时唯一能给出量化依据的手段。从那以后我每次上新查询词都强制先跑一遍离线回归确认召回和误报在可接受范围再上线。这套流程帮我挡掉过好几次“看着能跑、实际漏一半”的翻车。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网