
最近在做一个视频理解相关的需求时我产生了一个非常强烈的感受让大模型“看”视频真正的难点并不在模型本身而在“喂给它什么”。你可能会觉得多模态大模型已经这么强了能看图、能读文档那给它一段视频让它总结、问答、找关键信息应该也不难吧但实际做下来你会发现把整个视频直接丢给模型是完全不现实的——上下文窗口装不下计算成本扛不住而且视频里大量重复、无关的帧反而会稀释模型对关键信息的注意力。所以业界的通行做法是先从视频里抽出一批帧再把帧交给多模态大模型去理解和推理。问题就出在“抽哪些帧”这一步。抽得太密预算爆炸费钱费时抽得太稀会漏掉关键动作无脑均匀抽样可能正好错过那个转瞬即逝的异常事件。换句话说你选择的帧决定了模型能看到什么模型能看到什么决定了它最后输出什么。这篇文章我会从“帧选择”这个看似小、实则决定成败的环节出发聊聊让 LLM 理解视频时的核心难点、常见帧选择策略、一套可直接运行的代码实现以及工程落地时的最佳实践和坑。1. 为什么说“帧选择”决定了 LLM 看视频的上限先建立一个大致的量级概念。一段 1 分钟的视频按常见的 24 FPS每秒帧数算一共有 1440 帧。一段 1 小时的视频就是 86400 帧。即使我们缩到 5 秒抽一帧1 小时视频也有 720 帧。对多模态大模型来说每一帧都会转换成几百甚至上千个视觉 token再加上文本提示词很快就能把几十万 token 的上下文窗口撑爆。所以在实际工程中通常不是“能不能塞下”的问题而是“在有限预算里怎么塞得最有价值”。帧选择本质上是在解决一个信息瓶颈问题视频是连续的信息流但模型的输入是离散的、有限的。抽帧是强制降采样必然造成信息丢失。好的帧选择是让“丢失的信息”尽量远离关键内容。如果抽取的帧没覆盖关键信息后面不管用多强的模型、多精巧的提示词都不可能输出正确答案。这也是为什么说 frame selection is the whole game——它是在输入侧决定模型性能上限的操作。举个具体例子。假设视频里有一场篮球比赛一个关键投篮动作只持续了 0.5 秒。如果采用“每 10 秒抽一帧”的均匀抽样很可能完全没拍到投篮瞬间。模型看到的就是一群球员在跑位然后比分变了。这时候模型再强也只能 “猜” 这个分是怎么得到的。但如果你用“按场景切换检测”去抽帧就可能在投篮瞬间附近多采几帧模型就能准确回答“谁投进了这个球”。这个区别不是模型能力的差距而是输入质量的差距。2. LLM 理解视频的基本流程与常见误区要让 LLM 理解视频标准流程一般是这样视频解码把视频文件按指定格式解码得到连续帧序列。帧选择/采样从帧序列中选择一个子集这是核心环节。帧编码把选出的帧转换成适合多模态模型的输入格式通常是图像编码器输出的视觉特征或直接压缩后的图像。模型推理将帧序列与文本提示词一起送入多模态 LLM得到描述、答案或分析结果。这个流程里大部分人容易陷入三个误区。第一个误区把抽帧当成“预处理”随便均匀抽就行。均匀抽帧确实是最简单的方案也是很多开源项目默认的做法。但均匀抽帧有一个天然缺陷它假设视频里的信息在时间轴上均匀分布而真实视频几乎不可能满足这个假设。一段长镜头访谈视频画面 10 分钟不变均匀抽帧会得到大量重复帧一段快剪宣传片可能每 2 秒切换一个场景均匀抽帧又可能漏掉其中一半场景。第二个误区帧越多越好。有些同学认为既然怕漏信息那就多抽帧甚至把视频里所有帧都喂给模型。这不仅成本高还未必有效。大量高度相似的连续帧会引入冗余信息模型可能反而被重复的视觉内容干扰在长上下文中丢失对关键帧的注意力。额外的 token 成本更是实打实的开销。第三个误区只看视觉帧忽略其他模态。人类理解视频不只靠画面还有声音、字幕、说话内容。一段教程视频里关键信息很大概率存在于旁白或字幕中而不是画面。很多帧选择方案只做视觉分析等于丢掉了半个信息源。实际工程里把音频转写文本与视觉抽帧结合往往能大幅提升下游任务准确率。3. 主流的帧选择策略与适用场景目前业界和学术界常用的帧选择策略大致可以分为四类。3.1 均匀抽样Uniform Sampling最简单、最通用也最稳妥。设定一个固定间隔每隔 N 帧取一帧。或者设定目标帧数把视频等分成若干段每段取一帧。优点实现简单解码器按时间戳 seek 即可性能开销小。不依赖任何模型和计算可以提前预计算。对于画面变化均匀、节奏稳定的视频如慢速教学录屏、固定机位讲座效果足够好。缺点对动态变化剧烈的视频容易漏掉短时关键事件。对长时间静止的视频会产生大量冗余帧。适用场景预算有限、视频类型单一、对精度要求不高的快速验证。3.2 场景切换检测Scene Cut Detection基于视频内容的变化程度来决定是否抽帧。核心逻辑是计算相邻帧之间的差异颜色直方图、像素差、特征距离等当差异超过阈值时认为发生了场景切换此时取一帧作为关键帧。优点自适应视频内容动态场景抽得多静态场景抽得少。对视频剪辑类、广告类、多镜头切换类内容非常有效。缺点阈值需要针对具体视频内容调整否则要么切分过细要么切分过粗。只关注帧间突变对同一场景内的重要物体移动、表情变化不敏感。适用场景多镜头、多场景切换的短视频、电影片段、广告分析。3.3 基于内容重要性的关键帧选择通过一个“重要性评分模型”给每一帧打分然后选择得分最高的 Top-K 帧。这个评分模型可以是CLIP计算帧与用户问题之间的语义相似度比如问“视频里有没有穿红色衣服的人”就选与这句描述最相似的帧。专门的视觉显著性模型模拟人眼关注位置。目标检测模型根据检测到的物体数量/类别做启发式打分比如检测到“人”的帧权重更高。优点可以选择性地关注“和任务相关”的帧效果通常最好。可以融合多路信号文本相关性、运动度、人脸清晰度等。缺点需要额外运行一个模型计算开销较大。对评分模型的依赖较强如果评分模型选得不好可能引入偏差。适用场景有明确任务目标的场景如视频问答、特定事件检索、安防监控异常检测。3.4 镜头分段 代表帧组合实际工程中最常用的一种组合策略先用场景切换检测把视频切成多个镜头段Shot。对每个镜头段再用均匀抽样或内容评分选出一个或多个代表帧。汇总所有代表帧统一送 LLM。这样做的好处是既保证了对每个镜头都有覆盖又限制了总帧数兼顾召回与效率。很多视频摘要、视频检索系统采用的就是这个思路。4. 完整示例用 OpenCV CLIP LLM 构建帧选择流水线讲了这么多策略我们用一个最小可运行的示例把它们串起来。这里我用的是 Python OpenCV 做视频解码和场景检测用 CLIP 做文本相关性评分最后选出的关键帧交给一个多模态 LLM示例中用 OpenAI 的兼容接口风格演示实际请替换成你自己的模型接入方式。环境依赖pip install opencv-python pillow transformers torch示例代码以通用思路为主版本细节以实际环境为准。4.1 视频解码与均匀抽样# 文件路径frame_sampler.py import cv2 import os def extract_frames_uniform(video_path, output_dir, interval_seconds5): 按时间间隔均匀抽取视频帧。 :param video_path: 视频文件路径 :param output_dir: 输出目录 :param interval_seconds: 每隔多少秒抽一帧 :return: 保存的帧文件路径列表 os.makedirs(output_dir, exist_okTrue) cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) print(f视频帧率: {fps:.2f} FPS) frame_interval int(fps * interval_seconds) frame_idx 0 saved_paths [] saved_count 0 while True: ret, frame cap.read() if not ret: break # 每隔 interval 帧保存一帧 if frame_idx % frame_interval 0: out_path os.path.join(output_dir, funiform_{saved_count:04d}.jpg) cv2.imwrite(out_path, frame) saved_paths.append(out_path) saved_count 1 frame_idx 1 cap.release() print(f均匀抽样完成共保存 {saved_count} 帧) return saved_paths if __name__ __main__: # 使用示例python frame_sampler.py demo.mp4 frames/ import sys video_path sys.argv[1] if len(sys.argv) 1 else demo.mp4 output_dir sys.argv[2] if len(sys.argv) 2 else frames extract_frames_uniform(video_path, output_dir, interval_seconds2)4.2 基于直方图差异的场景切换检测# 文件路径scene_detector.py import cv2 import numpy as np import os def detect_scene_frames(video_path, output_dir, threshold30.0): 基于 HSV 直方图差异检测场景切换并在切换点保存关键帧。 差异值越大说明前后两帧变化越剧烈。 os.makedirs(output_dir, exist_okTrue) cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) print(f视频帧率: {fps:.2f} FPS) prev_hist None frame_idx 0 saved_paths [] saved_count 0 while True: ret, frame cap.read() if not ret: break # 转换为 HSV计算颜色直方图 hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) hist cv2.calcHist([hsv], [0, 1], None, [50, 60], [0, 180, 0, 256]) cv2.normalize(hist, hist) if prev_hist is not None: # 用巴氏距离衡量直方图差异值在 0~1 之间 diff cv2.compareHist(prev_hist, hist, cv2.HISTCMP_BHATTACHARYYA) # 差异超过阈值说明发生了镜头切换 if diff * 100 threshold: out_path os.path.join(output_dir, fscene_{saved_count:04d}_frame{frame_idx}.jpg) cv2.imwrite(out_path, frame) saved_paths.append(out_path) saved_count 1 print(f检测到场景切换: 第 {frame_idx} 帧, 差异值 {diff * 100:.2f}) prev_hist hist frame_idx 1 cap.release() print(f场景切换检测完成共保存 {saved_count} 帧) return saved_paths if __name__ __main__: import sys video_path sys.argv[1] if len(sys.argv) 1 else demo.mp4 output_dir sys.argv[2] if len(sys.argv) 2 else scene_frames detect_scene_frames(video_path, output_dir, threshold40.0)这里的关键是cv2.compareHist计算的巴氏距离。距离越大说明两个画面在颜色分布上差异越大大概率发生了镜头切换。实际使用时阈值需要根据你的视频类型调整——动画片切换频繁阈值可以调低固定机位的监控视频阈值要调高避免把光照变化误判为切换。4.3 用 CLIP 做文本相关性评分筛选任务相关帧如果我们的任务是“找到视频里运动员投篮的瞬间”均匀抽样可能漏掉关键动作场景检测也不理解语义。这时候可以用 CLIP 计算每帧与问题描述的相关性保留相关性最高的几帧。# 文件路径clip_selector.py import cv2 import torch from PIL import Image from transformers import CLIPProcessor, CLIPModel import os def select_frames_by_text(video_path, query_text, top_k10, sample_fps2): 使用 CLIP 模型计算每一帧与查询文本的相似度 返回相似度最高的 top_k 帧。 model CLIPModel.from_pretrained(openai/clip-vit-base-patch32) processor CLIPProcessor.from_pretrained(openai/clip-vit-base-patch32) cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) global_frame_idx 0 sampled_frames [] # 先按需求进行采样降低需要评分的帧数 while True: ret, frame cap.read() if not ret: break if global_frame_idx % int(fps / sample_fps) 0: sampled_frames.append((global_frame_idx, frame)) global_frame_idx 1 cap.release() print(f共采样 {len(sampled_frames)} 帧开始计算 CLIP 相似度...) scored [] for idx, frame in sampled_frames: # 转换为 PIL 图像 pil_img Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) inputs processor(text[query_text], images[pil_img], return_tensorspt, paddingTrue) with torch.no_grad(): outputs model(**inputs) # 计算文本-图像相似度 logits logits_per_image outputs.logits_per_image score logits_per_image.item() scored.append((idx, frame, score)) # 按相似度排序取前 top_k scored.sort(keylambda x: x[2], reverseTrue) top_scored scored[:top_k] os.makedirs(clip_frames, exist_okTrue) saved_paths [] for rank, (idx, frame, score) in enumerate(top_scored): out_path fclip_frames/rank_{rank:02d}_frame{idx}_score{score:.4f}.jpg cv2.imwrite(out_path, frame) saved_paths.append(out_path) print(f选出帧: 视频第 {idx} 帧, 相似度 {score:.4f}) return saved_paths if __name__ __main__: import sys video_path sys.argv[1] if len(sys.argv) 1 else demo.mp4 query_text sys.argv[2] if len(sys.argv) 2 else a person is shooting a basketball select_frames_by_text(video_path, query_text, top_k5, sample_fps2)这段代码的思路是先用较低的采样率对视频做一次粗采样保证候选帧数量可控然后对每个候选帧用 CLIP 计算与用户问题的语义相似度最后只保留分数最高的 K 帧。实际项目中可以把这个思路扩展到“按镜头分组后再做相关性排序”效果更稳。4.4 把关键帧交给多模态 LLM帧选好之后就是把它交给多模态模型了。这里我用一个兼容 OpenAI 接口风格的演示代码实际项目中请替换成你自己可用的模型接入方式。# 文件路径video_question_answering.py import base64 import os import httpx from openai import OpenAI # 初始化客户端endpoint 换成你的模型服务地址 client OpenAI( api_keyyour-api-key, base_urlhttp://localhost:8000/v1 ) def encode_image(image_path): with open(image_path, rb) as f: return base64.b64encode(f.read()).decode(utf-8) def ask_llm_with_frames(frame_paths, question): content [ {type: text, text: f请根据提供的视频关键帧回答下面的问题。 帧按时间顺序排列如果某帧画面模糊或无关键信息可以忽略。 问题{question} 请用简洁的语句回答并指出你是依据哪些帧得出的结论。} ] for path in frame_paths: image_base64 encode_image(path) content.append({ type: image_url, image_url: { url: fdata:image/jpeg;base64,{image_base64} } }) response client.chat.completions.create( modelyour-vlm-model, messages[ {role: user, content: content} ], max_tokens512, temperature0.2, ) return response.choices[0].message.content if __name__ __main__: # 假设你已经选出了关键帧 frame_paths [ clip_frames/rank_00_frame120_score0.5340.jpg, clip_frames/rank_01_frame240_score0.5120.jpg, clip_frames/rank_02_frame360_score0.4980.jpg, ] question 视频里的人正在做什么运动 answer ask_llm_with_frames(frame_paths, question) print(模型回答) print(answer)注意多模态模型对多张图的输入顺序敏感。如果帧之间有明确的时间先后关系建议你在文本提示词里显式说明“帧按时间顺序排列”并保证传入顺序正确。有些模型对图像数量有限制需要去查对应模型的输入规范。5. 如何评估帧选择策略的好坏帧选择做得好不好最终要看下游效果但我们可以拆出几个可量化的评估维度。第一个维度答案准确率Accuracy这是最直接的评估指标。准备一批带标准答案的视频问答数据对比不同帧选择策略下LLM 回答的准确率。比如一段视频里标准答案是“有一辆红色卡车在画面中驶过”。如果 A 策略选出的帧让模型答对了B 策略没答对那在这个样本上 A 优于 B。多个样本汇总后就能得到总体准确率。第二个维度关键信息覆盖率Recall这一项用于评估“选出的帧里是否包含了解答问题所需的视觉证据”。做法是先人工标记视频中每一个关键事件出现的帧区间。然后检查你的帧选择结果是否至少有一帧落在关键区间内。这个指标适合排查“漏帧”问题——如果覆盖率低说明你的抽样策略在时间分布上存在盲区。第三个维度预算效率Cost Efficiency每一帧都对应模型输入的 token。在相同准确率的前提下选帧数越少成本越低。评估时可以固定一组任务对比不同策略的“准确率/帧数”比值。有的策略虽然准确率高但用了 50 帧有的策略用了 15 帧就达到接近的准确率后者在工程上可能更划算。第四个维度帧冗余度Redundancy计算选出的帧之间的平均视觉相似度。相似度太高意味着有很多重复帧白白消耗预算。可以用 CLIP 图片向量之间的余弦相似度来量化。综合这四个维度你基本能判断一个帧选择方案在不同项目里是否适用。6. 常见问题与排查思路帧选择看似只是一个抽帧操作实际落地时坑不少。我把常见的问题整理成表格方便快速定位。问题现象可能原因排查方式解决方案模型回答“看不清”或“无法判断”次数多抽帧间隔过大关键事件被跳过检查抽帧结果中是否有覆盖关键事件时间点的帧降低抽帧间隔或改用场景切换检测抽出的帧大量重复画面几乎一样视频中存在长时间静止镜头计算相邻帧之间的直方图差异观察变化值对抽帧结果做去重或使用基于内容变化的动态采样场景检测帧数过多/过少直方图差异阈值设置不合理输出检测到的 diff 值分布观察波动范围根据视频类型调阈值或者对 diff 做归一化用 CLIP 筛选出的帧与文本不相关视频类型与 CLIP 训练分布差异较大抽几帧人工查看确认文本描述是否符合视频内容更换更大的 CLIP 模型或改用专门训练的检索模型多图输入时模型回答混乱帧顺序信息丢失或帧数超出模型限制查看模型文档确认图像输入上限确认提示词中是否声明顺序在提示词中标注顺序必要时分多个 batch 输入视频解码速度极慢视频码率高或逐帧读取查看后性能瓶颈在解码还是评分使用 cv2.CAP_PROP_POS_MSEC 按时间戳跳读或抽取关键帧快速解码内存溢出一次性载入所有帧的 CLIP 特征检查内存监控曲线分批处理帧或使用生成器逐帧评分这里真正经常出问题的是很多人把“抽帧”和“解码”混在一起导致性能开销翻倍。建议在工程中用cap.set(cv2.CAP_PROP_POS_MSEC, timestamp_ms)按时间戳直接 seek 到目标位置而不是逐帧读取再丢弃速度会快很多。7. 工程落地的最佳实践如果你要把帧选择真正集成到生产系统里下面这几个实践点值得留意。7.1 采用“抽样-评分-精选”三级流水线不要指望一种算法一步到位。我推荐用三级结构第一级用均匀抽样或场景检测以较低成本从视频中选出候选帧池比如 20~50 帧。第二级用 CLIP 或其他评分模型对候选帧做任务相关性排序。第三级按预算从高到低取 Top-K 帧送 LLM。这样既控制了总帧数又能保证关键镜头不被漏掉。7.2 结合音频模态别只靠画面对很多视频类型来说音频转写文本的可用性非常高。像访谈、课堂、新闻类视频说话内容包含大量关键信息。工程上常见的做法是对视频做语音转写ASR得到带时间戳的文本。在帧选择时把 ASR 文本片段嵌入到上下文里与视觉帧一起送 LLM。根据 ASR 内容定位关键时间点在这些时间点附近重点抽帧。比如用户问“讲师提到了哪几个关键概念”纯粹的视觉抽帧很难回答但如果有 ASR 文本答案就非常直接。7.3 用时间戳对齐保留下游可追溯性每一帧被选中时一定要记录它在原视频中的时间戳或帧号。这在下游非常有用LLM 输出答案后可以根据关键帧时间戳快速回跳到原视频对应位置。方便出问题时的数据回溯比如判断是抽帧问题还是模型问题。方便构建人工评估数据集。存储时建议用一个可解析的 JSON 或数据库表把帧路径、时间戳、评分、来源策略全部记录下来。7.4 针对不同任务设计不同的帧选择策略帧选择不是“找一把万能钥匙”而是根据任务类型做匹配任务类型推荐策略视频摘要/概览场景切换检测 每个场景抽代表帧视频问答基于画面CLIP 文本相关性评分动作识别/事件检测运动程度评分 高动态区间加密采样长视频检索/归档均匀抽样 去重保证覆盖率监控安防基于目标检测的启发式评分7.5 控制帧数与模型上下文的匹配关系在把帧送入 LLM 之前先查一下目标模型的图像输入限制和 token 换算规则。不同模型的视觉 token 计算方式差别很大。比如有的模型每张图固定占用固定数量 token有的模型会根据分辨率动态变化。如果你需要输入较多帧可以在提示词中要求模型“先整体浏览再结合具体帧回答”。将多帧按逻辑分组分开调用模型最后汇总。对分辨率做统一压缩不要送原始高清图。7.6 异常处理与幂等重试视频文件格式混乱、解码失败、某一帧损坏都是生产环境的常态。帧选择模块要做的不是“失败就让下游全挂”而是解码失败时记录错误信息跳过该视频或走降级策略。选帧结果为空时回退到均匀抽样兜底。对损坏的帧文件重新抽帧或从相邻时间点补采。对 LLM 调用做超时重试避免偶发的网络问题拖垮任务。7.7 建立帧选择效果回归测试集帧选择策略改完后怎么保证效果不会变差答案是沉淀一个回归测试集。挑 20~50 个有代表性的视频覆盖不同时长、不同画质、不同内容类型。每个视频都标注了关键事件帧区间和对应的标准答案。每次改动抽帧策略、评分模型或参数时都跑一遍回归测试对比准确率和覆盖率的变化。这能帮你避免“调顺了这个视频搞砸了另一个视频”的情况。8. 更进一步从“抽帧”走向“关键帧理解”如果你已经掌握了基本的帧选择下一步可以往三个方向深入。方向一自适应的动态帧率选择。不再固定“几秒抽一帧”而是根据视频的运动信息、场景复杂度、下游任务置信度动态调整采样密度。比如模型对当前帧确定性很低时自动在附近时间区间补充更多帧。这个思路在视频问答中已经被证明能有效提升长尾事件召回。方向二多模态联合检索。不止用文本查帧还可以用音频、用前一帧的运动向量、用物体检测结果一起做联合评分。把多种模态的“投票”聚合起来选出最符合当前任务的帧集合。方向三端到端训练关键帧选择模型。在具体业务中如果你的任务类型足够固定、数据量足够大可以微调一个专门做帧选择的轻量模型。它既知道业务目标又了解视频结构比通用的 CLIP 更贴合你的场景。从我的实践经验来看大多数团队其实不需要一步到位做最复杂的方案。先落地“场景切换 CLIP 相关性评分”的组合已经能覆盖绝大多数视频理解需求。等到业务量上来、问题变明确再针对性投入资源优化。9. 总结给 LLM 喂什么比 LLM 本身更能决定结果回到开头那句话Frame selection is the whole game。很多人做视频理解项目第一反应是“选一个更强的多模态模型”。但模型再强也只能基于输入帧做推理。如果你的帧选择策略保证不了关键信息被采到那模型的能力优势根本无从发挥。这篇文章梳理了帧选择的核心逻辑、常见策略和可运行的代码示例。当你下一次做 LLM 视频理解任务时建议先停下来想一想我的视频里关键信息通常出现在哪些时刻均匀抽样会不会漏掉这些时刻我有没有把音频转写文本一起利用起来选出来的帧是否真的覆盖了我需要回答的问题把这些问题想清楚再去调模型和提示词你会发现整体的效果提升是系统性的而不是碰运气。