
这几年在参与中华白海豚相关影像资料整理时最大的感触是视频素材动辄几十个小时但真正拍到白海豚的片段可能只占几分钟。人工逐帧去找既费眼睛又容易漏掉关键画面。后来我们逐步把“目标检测 视频拆帧 结果过滤”这套流程落地才发现“没有白海豚就没有这个视频”这个判断其实可以交给算法来做。这篇文章就把这套从海量视频中自动截取白海豚片段的完整方案整理出来包含环境搭建、模型训练、视频推理和常见报错排查希望给正在做野生动物影像分析、海洋监测或视频自动剪辑的朋友一些参考。1. 背景与核心概念1.1 为什么要用算法识别视频中的白海豚中华白海豚是国家一级保护动物通常栖息在河口和近岸海域。野外监测和纪录片拍摄过程中常用无人机、船载摄像头或水下摄影机长时间连续录制。这样会产生大量视频素材而白海豚的出现往往没有固定时间规律可能一整天的素材里只有几段有效画面。人工处理这些素材的工作量非常大。比如一段 8 小时的 4K 视频如果按每秒 25 帧计算大约有 72 万帧画面逐帧浏览几乎不现实。即使用快进方式预览也很容易错过白海豚浮出水面的瞬间。于是“自动找出包含白海豚的视频片段”就成了一个很现实的工程问题。1.2 从视频中提取目标片段的核心思路这个问题的本质是“视频中的目标检测与时间片段定位”。整体流程可以拆成三个阶段视频拆帧把连续视频转换成帧图片并记录每帧对应的时间点。目标检测用训练好的目标检测模型判断每一帧中是否出现白海豚并给出置信度分数。片段合并把连续出现白海豚的帧合并成时间片段再从原始视频中截取出对应的视频段落。目标检测部分采用 YOLO 系列模型。YOLO 的核心思想是把目标检测当成回归问题一次前向推理同时预测目标类别和边界框位置。相比两阶段检测器YOLO 在视频场景下速度优势明显更适合处理大规模视频数据。1.3 适用场景这套方案不局限于白海豚同样适用于江豚、鲸类、鸟类、大型海洋生物等野生动物影像分析。任何需要“从长视频中找出特定目标出现片段”的需求都可以复用同一套技术框架。2. 环境准备与版本说明2.1 开发环境清单先列出本次实战使用的运行环境。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。类别推荐环境操作系统Ubuntu 20.04 / Windows 10 以上编程语言Python 3.9 或 3.10深度学习框架PyTorch 2.0 以上目标检测库Ultralytics YOLO建议 8.0 以上视频处理OpenCV 4.5 以上硬件建议 NVIDIA GPU显存 8G 以上构建工具pip / conda2.2 安装依赖建议先创建独立的 Python 虚拟环境避免和系统环境或其他项目冲突。conda create -n dolphin python3.10 conda activate dolphin安装 PyTorch 时需要根据你的 CUDA 版本选择合适的安装命令。如果暂时没有 GPU也可以安装 CPU 版本用于流程验证但视频处理速度会明显偏慢。# CPU 版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # GPU 版本请先查看本机 CUDA 版本示例以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118然后安装目标检测库和视频处理相关依赖。pip install ultralytics opencv-python安装完成后可以输入下面命令验证 Ultralytics 是否正常。python -c from ultralytics import YOLO; print(YOLO OK)如果输出YOLO OK说明环境已经就绪。2.3 推荐项目结构建议按照下面的目录结构组织代码和数据。dolphin_video/ ├── data/ │ ├── images/ │ │ ├── train/ │ │ └── val/ │ ├── labels/ │ │ ├── train/ │ │ └── val/ │ └── dolphin.yaml ├── models/ │ └── best.pt ├── input/ │ └── raw_video.mp4 ├── output/ │ ├── clips/ │ └── frames/ ├── scripts/ │ ├── extract_frames.py │ ├── train.py │ ├── detect_video.py │ └── merge_clips.py └── requirements.txtdata/images存放训练图片data/labels存放对应的 YOLO 格式标注文件models保存训练好的权重input放待处理的视频output放检测结果和截取片段。3. 核心语法、配置与原理拆解3.1 视频拆帧与时间戳记录视频处理的第一步是拆帧。为什么不能直接拿视频文件去做检测因为目标检测模型通常处理的是单张图片需要把视频流逐帧暴露给模型。同时每个帧必须对应一个时间点后续合并片段时才能准确还原到原始视频上。下面是一个拆帧脚本的核心片段工作逻辑是用 OpenCV 读取视频循环读取每一帧保存为 JPG 图片并记录帧序号和时间戳。# 文件路径scripts/extract_frames.py import cv2 import os import time def extract_frames(video_path, output_dir, frame_step1): os.makedirs(output_dir, exist_okTrue) cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) total_frames int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) print(f视频帧率: {fps:.2f}, 总帧数: {total_frames}) frame_idx 0 saved_idx 0 timestamp_file os.path.join(output_dir, timestamps.txt) with open(timestamp_file, w, encodingutf-8) as f: while True: ret, frame cap.read() if not ret: break if frame_idx % frame_step 0: timestamp frame_idx / fps frame_path os.path.join(output_dir, fframe_{saved_idx:06d}.jpg) cv2.imwrite(frame_path, frame) f.write(fframe_{saved_idx:06d}.jpg {timestamp:.3f}\n) saved_idx 1 frame_idx 1 cap.release() print(f共提取 {saved_idx} 帧时间戳已保存到 {timestamp_file}) if __name__ __main__: extract_frames(input/raw_video.mp4, output/frames)几个关键点frame_step表示每隔多少帧保存一次。如果视频帧率是 30frame_step1表示每秒保存 30 帧检测压力较大frame_step3表示每秒保存 10 帧速度更快但可能漏掉快速游动的目标。时间戳frame_idx / fps是这一帧在视频中的绝对时间位置单位是秒。后续合并片段和截取视频都依赖这个值。timestamps.txt是帧文件名和时间戳的映射关系建议保留下来方便回溯。3.2 YOLO 数据集标注与配置目标检测模型需要带标注的图片数据集。每张图片的标注文件是 TXT 格式每一行表示一个目标类别编号 中心点x 中心点y 宽度 高度坐标值都是相对于图片宽高的归一化数值取值范围在 0 到 1 之间。比如一张 1920x1080 的图片上白海豚边界框中心点在 (960, 540)宽 500高 300那么标注内容就是0 0.5 0.5 0.2604 0.2778其中0.2604 500 / 19200.2778 300 / 1080。然后需要写一个 YOLO 数据配置文件告诉模型训练时去哪里找图片和标注文件。文件路径如下# 文件路径data/dolphin.yaml train: data/images/train val: data/images/val nc: 1 names: [dolphin]train和val分别指定训练集和验证集图片所在的目录。nc是类别数量这里只有白海豚一个类别。names是类别名称列表。3.3 模型训练核心参数在 Ultralytics YOLO 中训练入口非常简单但参数的含义需要理解。# 文件路径scripts/train.py from ultralytics import YOLO # 加载预训练模型这里以 YOLOv8s 为例 model YOLO(yolov8s.pt) # 开始训练 model.train( datadata/dolphin.yaml, epochs100, imgsz640, batch8, device0, workers4, patience20, projectruns/dolphin, nameexp01 )参数说明data指定数据配置文件路径。epochs训练轮数。数据集小的时候 100 轮左右通常足够。imgsz输入图片尺寸。640 是速度与精度比较平衡的默认值。batch批大小。根据显存调整显存不足时降低到 4 或 2。device0表示使用第一块 GPUcpu表示使用 CPU。workers数据加载线程数。patience早停策略连续 N 轮验证集指标没有提升就停止训练。project和name控制训练结果保存目录。训练结束后会在runs/dolphin/exp01/weights/目录下生成best.pt和last.pt。best.pt是验证集表现最好的权重后续检测和截取都使用这个文件。3.4 目标检测与置信度过滤训练完成后可以用 YOLO 模型对图片做推理。推理阶段的重点不只是“检测到有没有目标”而是要拿到目标的类别、置信度和边界框坐标然后按照置信度阈值过滤低质量结果。from ultralytics import YOLO model YOLO(models/best.pt) results model.predict(output/frames/frame_000000.jpg, conf0.5, verboseFalse) for result in results: boxes result.boxes if boxes is not None and len(boxes) 0: for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf float(box.conf[0]) cls int(box.cls[0]) print(f坐标: ({x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f}), 置信度: {conf:.2f})conf0.5表示只保留置信度大于 0.5 的检测结果。置信度阈值直接影响漏检和误检的平衡如果是文档片段的粗筛0.3 到 0.5 都可以如果是正式科研统计建议调高到 0.6 以上并且配合人工复核。3.5 连续帧合并成视频片段单帧检测结果只是一堆“哪一帧有白海豚”的布尔值。我们需要的是一段连续的时间区间比如“从 00:12:35 到 00:12:48”。这就要做连续帧合并。思路是遍历所有帧的检测结果记录有目标出现的帧序号。如果连续出现的帧之间没有断开就认为是同一个片段。允许一定帧数的断档避免因为模型偶尔漏检导致片段被切断。设置最短片段长度过滤掉只有一两帧的误检片段。下面是一个简化版合并逻辑def merge_detections(detected_frames, max_gap15, min_clip_len30): detected_frames: 已按时间排序的有目标帧序号列表 max_gap: 允许断开的帧数 min_clip_len: 片段最少帧数 clips [] start detected_frames[0] prev detected_frames[0] for idx in detected_frames[1:]: if idx - prev max_gap: if prev - start min_clip_len: clips.append((start, prev)) start idx prev idx if prev - start min_clip_len: clips.append((start, prev)) return clips这里的detected_frames是“帧序号”列表不是时间戳。合并后再根据帧率转换回时间从原始视频中截取片段。4. 完整实战案例4.1 创建项目结构先在项目根目录下创建必要的文件夹。mkdir -p data/images/train data/images/val data/labels/train data/labels/val mkdir -p models input output/clips output/frames scripts runs然后把需要处理的视频放到input目录下把标注好的图片和标注文件按目录放好。4.2 统一文件名规范图片和标注文件必须保持文件名一致只是扩展名不同。比如dolphin_001.jpg对应的标注文件是dolphin_001.txt。这是 YOLO 数据加载的基本要求。建议用下面脚本检查数据集是否对齐import os image_dir data/images/train label_dir data/labels/train images set(os.path.splitext(f)[0] for f in os.listdir(image_dir) if f.endswith(.jpg)) labels set(os.path.splitext(f)[0] for f in os.listdir(label_dir) if f.endswith(.txt)) missing_labels images - labels missing_images labels - images print(f缺标注的图片: {missing_labels if missing_labels else 无}) print(f缺图片的标注: {missing_images if missing_images else 无})4.3 训练目标检测模型直接运行训练脚本。如果你的数据集较小建议使用预训练权重yolov8s.pt做迁移学习这样收敛更快泛化能力也更好。python scripts/train.py训练过程中可以观察损失变化和验证集指标。训练完成后查看runs/dolphin/exp01/目录里面会有results.png、confusion_matrix.png、weights/best.pt等文件。把best.pt复制到models/目录下备用。cp runs/dolphin/exp01/weights/best.pt models/best.pt4.4 编写视频检测与片段截取脚本现在进入最关键的环节输入一段原始视频输出多个包含白海豚的片段。下面是一个完整可运行的脚本。# 文件路径scripts/detect_video.py import cv2 import os import subprocess from ultralytics import YOLO # 配置区 VIDEO_PATH input/raw_video.mp4 OUTPUT_DIR output/clips FRAME_OUTPUT_DIR output/frames MODEL_PATH models/best.pt CONF_THRESHOLD 0.5 MAX_GAP 15 # 允许断档帧数 MIN_CLIP_FRAMES 30 # 片段最短帧数 FRAME_STEP 1 # 每隔多少帧检测一次 # def extract_frames_with_ts(video_path, output_dir, frame_step): os.makedirs(output_dir, exist_okTrue) cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) frame_idx 0 saved_idx 0 frame_list [] while True: ret, frame cap.read() if not ret: break if frame_idx % frame_step 0: frame_path os.path.join(output_dir, fframe_{saved_idx:06d}.jpg) cv2.imwrite(frame_path, frame) frame_list.append(frame_path) saved_idx 1 frame_idx 1 cap.release() return frame_list, fps def detect_frames(model, frame_list, conf_threshold): detected [] for i, frame_path in enumerate(frame_list): results model.predict(frame_path, confconf_threshold, verboseFalse) for result in results: boxes result.boxes if boxes is not None and len(boxes) 0: detected.append(i) break # 只要这一帧有目标就记录 if i % 500 0: print(f已检测 {i}/{len(frame_list)} 帧) return detected def merge_detections(detected, max_gap, min_clip_frames): if not detected: return [] clips [] start detected[0] prev detected[0] for idx in detected[1:]: if idx - prev max_gap: if prev - start min_clip_frames: clips.append((start, prev)) start idx prev idx if prev - start min_clip_frames: clips.append((start, prev)) return clips def cut_clips(video_path, clips, frame_list, fps, output_dir): os.makedirs(output_dir, exist_okTrue) cap cv2.VideoCapture(video_path) original_fps cap.get(cv2.CAP_PROP_FPS) for clip_idx, (start, end) in enumerate(clips): start_time start / fps end_time end / fps start_path frame_list[start] end_path frame_list[end] output_path os.path.join(output_dir, fclip_{clip_idx:03d}.mp4) print(f片段 {clip_idx}: {start_time:.2f}s - {end_time:.2f}s) # 使用 ffmpeg 从原始视频中截取时间片段 cmd [ ffmpeg, -y, -i, video_path, -ss, f{start_time:.3f}, -t, f{end_time - start_time:.3f}, -c:v, libx264, -c:a, aac, output_path ] subprocess.run(cmd, capture_outputTrue) cap.release() if __name__ __main__: print(步骤1: 拆帧...) frame_list, fps extract_frames_with_ts(VIDEO_PATH, FRAME_OUTPUT_DIR, FRAME_STEP) print(f完成共 {len(frame_list)} 帧帧率 {fps:.2f}) print(步骤2: 加载模型...) model YOLO(MODEL_PATH) print(步骤3: 逐帧检测...) detected detect_frames(model, frame_list, CONF_THRESHOLD) print(f检测到有白海豚的帧数: {len(detected)}) print(步骤4: 合并片段...) clips merge_detections(detected, MAX_GAP, MIN_CLIP_FRAMES) print(f合并出 {len(clips)} 个片段) print(步骤5: 截取视频...) cut_clips(VIDEO_PATH, clips, frame_list, fps, OUTPUT_DIR) print(f完成片段保存在 {OUTPUT_DIR})脚本内部逻辑分成五步拆帧并保存帧图片得到有序的帧路径列表。加载训练好的 YOLO 模型。逐帧检测只记录“出现白海豚”的帧索引。对检测结果做合并形成连续片段。调用 ffmpeg 从原始视频中截取对应时间段生成独立的视频文件。4.5 运行与验证在运行截取脚本之前确保系统已经安装 ffmpeg。如果没有安装可以用系统包管理器安装# Ubuntu sudo apt install ffmpeg # macOS brew install ffmpeg # Windows 请到 ffmpeg 官网下载并添加到 PATH然后运行脚本python scripts/detect_video.py正常情况下会输出类似下面的日志步骤1: 拆帧... 视频帧率: 30.00, 总帧数: 216000 完成共 216000 帧帧率 30.00 步骤2: 加载模型... 步骤3: 逐帧检测... 已检测 0/216000 帧 已检测 500/216000 帧 ... 检测到有白海豚的帧数: 248 步骤4: 合并片段... 合并出 4 个片段 步骤5: 截取视频... 片段 0: 1234.56s - 1246.89s 片段 1: 5678.12s - 5690.33s ... 完成片段保存在 output/clips到output/clips目录查看生成的片段用播放器确认每个片段确实有白海豚出现。如果某些片段里目标不明显可以调整CONF_THRESHOLD和MIN_CLIP_FRAMES重新检测。4.6 结果说明这个脚本输出两类结果output/frames/下是所有拆帧图片和对应的检测依据。output/clips/下是截取好的视频片段。帧图片的价值在于可以配合人工标注工具做二次筛选比如把置信度较低但视觉上确实是白海豚的帧挑出来重新加入训练集迭代优化模型。视频片段则是最终交付成果可以直接用于科研记录、档案整理或纪录片粗剪。5. 常见问题与排查思路5.1 CUDA 不可用导致推理速度极慢问题现象调用model.predict()时明显感觉速度很慢日志显示使用 CPU 推理。常见原因PyTorch 安装的 CUDA 版本和本机显卡驱动不匹配或者安装的是 CPU 版本 PyTorch。排查步骤python -c import torch; print(torch.cuda.is_available())输出False说明 CUDA 不可用。需要重新安装对应 CUDA 版本的 PyTorch。可以先查看驱动支持的 CUDA 版本nvidia-smi右上角有 “CUDA Version” 信息然后到 PyTorch 官网选择对应的安装命令。5.2 检测效果差经常漏检问题现象视频中白海豚非常明显但模型没有检测到。常见原因训练数据太少模型没见过足够多角度和光照条件下的白海豚。训练切片尺寸太小白海豚在画面中占比过大导致边界框被截断。视频分辨率高模型对缩小后的目标不敏感。解决思路增加训练数据尽量覆盖清晨、傍晚、逆光、远距离、近距离等不同场景。把imgsz从 640 提升到 960 或 1280但要注意显存占用。降低置信度阈值到 0.3 做初步筛选再用人工方式确认。5.3 片段断裂严重同一次出现被切成多段问题现象白海豚明明连续出现在画面里好几分钟结果被切成了好几个短片段。常见原因模型在中间某几帧漏检导致连续帧之间超过MAX_GAP。解决思路增大MAX_GAP允许更多的帧数断档。调低CONF_THRESHOLD减少因为阈值过高导致的漏检。对检测结果做插值修复比如如果第 100 帧和第 110 帧有目标而中间几帧没有可以认为是连续片段。5.4 视频处理太慢问题现象一段 1 小时的视频跑了 20 分钟还没完成。常见原因视频分辨率过高比如 4K 视频单帧检测耗时明显。FRAME_STEP1每一帧都检测没有利用时间连续性。解决思路将视频先缩放再检测比如把长边缩到 1280检测速度会快很多。增大FRAME_STEP比如改成 2 或 3间隔几帧检测一次减少计算量。使用批量推理把多张帧图片放到一个 batch 里预测而不是一次一张。Ultralytics 支持批量列表输入results model.predict(frame_batch, conf0.5, verboseFalse)可以一次传入数十张图片吞吐量会提升很多。5.5 训练时显存不足问题现象训练开始时报CUDA out of memory。常见原因imgsz和batch设置过大超过了显卡显存。解决思路降低batch从 8 改成 4 或 2。降低imgsz从 640 改成 512。清理 GPU 上其他进程占用的显存用nvidia-smi查看。5.6 常见问题速查表问题现象常见原因解决思路CUDA 不可用驱动或 PyTorch 版本不匹配检查 nvidia-smi 并重新安装 PyTorch漏检率高数据集不足 / 目标过小扩充数据集、提高输入分辨率、降低置信度片段断裂中间帧漏检增大 MAX_GAP、调低阈值、做帧间插值处理速度慢高分辨率逐帧检测缩放视频、增大帧间隔、批量预测显存不足batch 或 imgsz 过大降低 batch 和 imgsz截取片段没有声音ffmpeg 命令漏掉音频编码添加-c:a aac参数6. 最佳实践与工程建议6.1 数据管理建议数据质量直接决定模型效果。做野生动物目标检测时数据往往是最消耗精力的一环。建议从这几个方面入手视频素材按日期、地点、拍摄设备归档避免混合目录。标注时尽量框住完整目标不要只框背鳍或尾巴除非你的目标本来就是局部特征。每个场景至少标注 200 到 500 张有效图片类别越多每类图片数量也越多。训练集和验证集不要有同一视频连续帧否则验证集指标会虚高。建议按视频文件划分而不是按图片划分。标注图片保留原始分辨率不要在标注前压得太小。6.2 模型训练与选型建议YOLO 系列有不同的尺寸yolov8n、yolov8s、yolov8m、yolov8l、yolov8x速度依次变慢精度通常依次提升。第一次千万不要直接上最大的模型先用yolov8s跑通流程再按需升级。训练轮数不是越多越好。建议开启patience早停否则验证集指标可能下降产生过拟合。观察runs/.../results.png中的验证集指标曲线如果训练集损失持续下降但验证集损失开始上升说明过拟合应停止训练。6.3 后处理与片段截取建议截取片段时不要只截“刚好有目标”的区间建议前后各扩展 2 到 3 秒让片段有上下文便于人工查看。输出片段生成后抽样检查即可不必所有帧都人工复核。如果片段数量很多可以生成一个 CSV 索引文件记录片段名 | 开始时间 | 结束时间 | 包含目标帧数 | 最大置信度方便后续检索。import csv records [] for idx, (start, end) in enumerate(clips): records.append([fclip_{idx:03d}.mp4, start / fps, end / fps, end - start 1]) with open(output/clips/summary.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([文件名, 开始时间(秒), 结束时间(秒), 帧数]) writer.writerows(records)6.4 合规与安全注意事项中华白海豚是国家一级保护动物相关影像数据可能涉及保护栖息地和个体识别信息在项目开发中应该注意未经许可不要使用敏感地理坐标做公开标注。涉及保护动物影像时不要使用可能干扰动物正常活动的无人机或拍摄设备。模型和数据集的分享要遵循相关法律法规和科研伦理要求。如果是科研用途建议与相关保护研究机构合作确保数据来源合法、使用合理。6.5 工程化部署建议视频检测脚本在开发环境跑通后如果需要长期处理大量视频建议考虑以下工程化改造用队列任务管理工具调度视频处理任务比如 Celery 加 Redis或者简单的 Shell 脚本批量调度。把配置文件从脚本中抽离为 YAML避免改参数就要改代码。对处理结果写日志包括视频路径、检测帧数、片段数、处理耗时方便统计和排错。如果视频量很大优先用 GPU 服务器批量跑批处理避免频繁模型加载。7. 总结与学习路线这篇文章围绕“判断一个视频里是否包含白海豚并截取对应片段”这个需求完整梳理了从环境准备、数据集标注、模型训练、视频拆帧、目标检测到片段合并的完整流程。核心收获有几个把视频问题拆成“拆帧 图像目标检测 时间片段合并”三个子问题每一步都有明确的输入输出。YOLO 系列模型在视频目标检测场景下速度快、部署简单配合预训练权重做迁移学习即使数据量不大也能得到可用模型。置信度阈值、最大断档帧数、最短片段长度这三个参数是平衡漏检、误检和片段完整性的关键需要在真实数据上反复调优。工程上要重视数据目录规范、日志记录和结果索引文件这能让后续模型迭代和视频排查省下大量时间。如果你是第一次接触目标检测建议按下面的顺序继续学习先跑通一个公开数据集的目标检测流程比如 COCO 上的 YOLO 推理理解检测结果的数据结构。自己标注几百张图片训练一个单类别检测模型体会数据质量对结果的影响。将模型接入视频流完成视频拆帧、检测、片段输出的完整闭环。再深入优化比如模型剪枝量化、基于时序信息的跟踪和插值、边缘设备部署。如果你已有目标检测基础可以把重心放在视频后处理和工程化上比如如何降低漏检、如何合并时序片段、如何批量处理大规模视频数据。这套方案里的代码和调优思路可以直接复用到类似的野生动物监测场景。最后给一个实用提示第一次跑完整流程时不要拿一整天的长视频做测试先剪出 5 到 10 分钟包含白海豚的视频片段跑通后再扩展到全量数据。这样能快速验证模型效果和参数设置避免等几个小时才发现某个环节出了问题。如果这篇文章对你有帮助可以收藏备用后续基于 YOLO 的视频目标检测项目都能参照这套流程来实施。