Python+FFmpeg提取大疆无人机MP4内嵌mov_text遥测SRT字幕并做完整性校验

作者:RS迷途小书童

博客地址:https://rsran.blog.csdn.net/

标签:大疆 DJI Python ffmpeg SRT字幕 无人机遥测 M3TD 航测

文章目录

  1. 前言
  2. 原理说明
  3. 环境与依赖准备
  4. 完整Python源码
  5. 代码运行输出示例
  6. nb_frames元数据字段说明
  7. [流索引说明,‑map 0:3含义](#流索引说明,‑map 0:3含义)
  8. 适用范围与局限性
  9. 总结

1. 前言

大疆M3TD、M300等工业无人机录制MP4视频时,文件内部封装多路数据流:H.264视频流、djmd元数据流、dbgi二进制遥测流、mov_text(tx3g)内嵌字幕流。

mov_text字幕轨道存储可读SRT格式遥测信息,包含GPS经纬度、相对/绝对海拔、云台姿态、焦距、变焦系数等传感器数据。在航测、摄影测量、无人机航迹解算、视频‑POS匹配场景中经常需要导出该字幕。

直接使用ffmpeg可以导出SRT,但很多使用者无法判断导出后的SRT文件是否完整。本套Python脚本基于ffprobe+ffmpeg实现三件事:

  1. 读取MP4容器、视频流元信息:时长、平均帧率;
  2. 获取元数据中标注的视频总帧数、字幕轨道总帧数nb_frames;
  3. 导出SRT字幕,统计磁盘上SRT真实字幕条目,与元数据标注帧数做比对,完成完整性校验。

校验逻辑:当元数据字幕轨道帧数 = SRT实际条目数,代表字幕完整导出。

2. 原理说明

  1. 调用ffprobe(ffmpeg自带工具)读取MP4元数据,输出JSON结构化结果,解析得到容器时长、视频流时长、帧率、nb_frames;
  2. 调用ffmpeg,通过‑map指定mov_text字幕流,直接输出标准SRT字幕文件;
  3. 遍历生成的SRT文件,统计字幕序号行数量,得到真实字幕条目;
  4. 将元数据标注帧数与实际SRT条目对比,完成完整性校验。

nb_frames:Number of frames,存储在MP4 moov盒子内的元数据字段,仅读取文件头部即可获取,不需要解码整个视频。

3. 环境与依赖准备

  1. Python 3.7+(Conda虚拟环境同样支持,仅使用标准库json、subprocess,无需pip安装第三方包)
  2. FFmpeg完整构建包,必须同时包含ffmpeg.exe与ffprobe.exe

下载地址:https://www.gyan.dev/ffmpeg/builds/

解压后使用bin目录下可执行文件,填写完整绝对路径到代码。
测试机型:DJI M3TD,其他支持内嵌mov_text字幕的大疆工业机型均可适配。

4. 完整Python源码

python 复制代码
# -*- coding: utf-8 -*-
"""
@Time:2026/10/07 20:02
@Auth:RS迷途小书童
@File:DJ SRT Extracting.py
@IDE:PyCharm
@Purpose:大疆无人机视频 SRT字幕日志提取
@Web:博客地址: [https://rsran.blog.csdn.net/](https://rsran.blog.csdn.net/)
@Attention:
    1.依赖ffmpeg.exe,下载地址:[https://www.gyan.dev/ffmpeg/builds/](https://www.gyan.dev/ffmpeg/builds/)
    2.注意核对视频帧数和SRT字幕轨道帧数是否一致
"""
import json
import subprocess

def parse_fraction(s: str):
    if s is None or str(s).strip() in ("", "N/A"):
        return None
    s = str(s).strip()
    if "/" in s:
        a, b = s.split("/")
        return float(a) / float(b)
    else:
        return float(s)


def get_video_info(ffmpeg_exe: str, video_path: str):
    ffprobe_exe = ffmpeg_exe.replace("ffmpeg.exe", "ffprobe.exe")
    cmd = [
        ffprobe_exe,
        "-v", "error",
        "-print_format", "json",
        "-show_format",
        "-show_streams",
        video_path
    ]
    proc = subprocess.run(cmd, capture_output=True, encoding="utf-8", errors="ignore")
    if proc.returncode != 0:
        print("⚠️获取视频元信息失败:", proc.stderr)
        return None
    try:
        raw = json.loads(proc.stdout)
    except json.JSONDecodeError:
        print("⚠️ffprobe JSON解析失败")
        return None

    streams = raw.get("streams", [])
    video_stream = None
    subtitle_stream = None
    for st in streams:
        idx = st.get("index")
        codec_type = st.get("codec_type")
        tag_string = st.get("codec_tag_string", "")
        if idx == 0 and codec_type == "video":
            video_stream = st
        if codec_type == "subtitle" and tag_string == "tx3g":
            subtitle_stream = st

    if video_stream is None:
        return None

    container_dur = parse_fraction(raw["format"].get("duration"))
    video_dur = parse_fraction(video_stream.get("duration"))
    avg_fps = parse_fraction(video_stream.get("avg_frame_rate"))

    video_meta_frames = None
    sub_meta_frames = None
    nbf_vid = video_stream.get("nb_frames")
    if nbf_vid and nbf_vid != "N/A":
        video_meta_frames = int(nbf_vid)

    if subtitle_stream is not None:
        nbf_sub = subtitle_stream.get("nb_frames")
        if nbf_sub and nbf_sub != "N/A":
            sub_meta_frames = int(nbf_sub)

    return {
        "container_duration_sec": round(container_dur, 3) if container_dur else None,
        "video_stream_duration_sec": round(video_dur, 3) if video_dur else None,
        "avg_fps": round(avg_fps, 3) if avg_fps else None,
        "video_meta_total_frames": video_meta_frames,
        "subtitle_meta_nb_frames": sub_meta_frames
    }


def count_srt_entries(srt_file_path: str) -> int:
    count = 0
    try:
        with open(srt_file_path, "r", encoding="utf-8") as f:
            for line in f:
                line_strip = line.strip()
                if line_strip.isdigit():
                    count += 1
    except FileNotFoundError:
        return 0
    return count


# SRT提取主函数:视频信息、提取、验证
def extract_dji_mov_text_srt(ffmpeg_exe: str, video_path: str, out_srt: str):
    vid_info = get_video_info(ffmpeg_exe, video_path)
    if vid_info is not None:
        print("-----------------视频元数据信息【ffprobe读取】-----------------")
        print(f"MP4容器总时长(sec): {vid_info['container_duration_sec']}")
        print(f"视频流实际时长(sec): {vid_info['video_stream_duration_sec']}")
        print(f"平均帧率(fps): {vid_info['avg_fps']}")
        print(f"元数据标注视频总帧数: {vid_info['video_meta_total_frames']}")
        print(f"元数据标注字幕轨道帧数: {vid_info['subtitle_meta_nb_frames']}")
        print("-----------------视频元数据信息【ffprobe读取】-----------------\n")

        cmd = [
            ffmpeg_exe,
            "-i", video_path,
            "-map", "0:3",
            "-y",
            out_srt
        ]
        proc = subprocess.run(cmd, capture_output=True)
        if proc.returncode == 0:
            print(f"✅ 执行完毕,输出文件路径:{out_srt}")
            real_srt_count = count_srt_entries(out_srt)
            print(f"📄 磁盘上srt文件实际字幕条目数:{real_srt_count} 条")
        else:
            print("❌ ffmpeg执行出错")
            print(proc.stderr.decode("utf8", errors="ignore"))


if __name__ == "__main__":
    # 修改为你本机实际路径
    FFMPEG_EXE = r"D:\ffmpeg-9.0-full_build\bin\ffmpeg.exe"
    INPUT_VIDEO = r"D:\Work Data\DJI_video.mp4"
    OUTPUT_SRT = r"D:\Work Data\output.srt"
    extract_dji_mov_text_srt(FFMPEG_EXE, INPUT_VIDEO, OUTPUT_SRT)

5. 代码运行输出示例

复制代码
-----------------视频元数据信息【ffprobe读取】-----------------
MP4容器总时长(sec): 343.777
视频流实际时长(sec): 343.777
平均帧率(fps): 29.97
元数据标注视频总帧数: 10303
元数据标注字幕轨道帧数: 10303
-----------------视频元数据信息【ffprobe读取】-----------------

✅ 执行完毕,输出文件路径:D:\Work Data\无人机视频三维可视化\示例数据\ffmpeg_python_out.srt
📄 磁盘上srt文件实际字幕条目数:10303 条

当元数据字幕轨道帧数与SRT实际条目相等,代表字幕完整导出。

6. nb_frames元数据字段说明

nb_frames是MP4文件moov盒子内记录的帧计数字段:

  1. ffprobe仅读取文件头部元数据,不需要解码视频即可拿到该数值;
  2. 视频流nb_frames代表元数据记录的视频总帧;
  3. 字幕流nb_frames代表元数据记录的mov_text字幕总帧;
  4. 本脚本把元数据值和磁盘SRT真实条目做比对,作为完整性校验依据。

注意:部分固件、部分视频,元数据中nb_frames字段可能为空,此时无法依靠该字段校验。

7. 流索引说明,‑map 0:3含义

DJI M3TD生成MP4内部流排布:

索引 流类型 说明
0:0 video H.264主视频流
0:1 data djmd大疆元数据
0:2 data dbgi完整二进制遥测流
0:3 subtitle mov_text(tx3g)内嵌字幕流,提取目标
0:4 video MJPEG缩略图流

‑map 0:3:选取输入文件0的索引为3的字幕流输出。

⚠️不同机型、固件版本流索引可能发生变化。如果报错找不到字幕流,执行下面命令查看全部流信息,确认tx3g字幕流真实index。

cmd 复制代码
ffprobe -v error -print_format json -show_streams "你的视频.mp4"

8. 适用范围与局限性

✅ 适用场景

  • 大疆工业无人机录制、开启内嵌字幕的MP4视频;
  • 需要导出mov_text内嵌SRT遥测,用于航迹分析、视频POS匹配、摄影测量预处理;
  • 需要对导出SRT做自动化完整性校验。

❌ 局限性

  1. mov_text字幕的采样率受机型固件设置决定;
  2. ffmpeg无法解析dbgi私有二进制遥测流;
  3. 个别固件版本MP4,字幕流索引不是0:3,需要手动修改map参数;
  4. 部分视频元数据不存在nb_frames字段,无法做帧数比对校验。

9. 总结

  1. 脚本完成:读取MP4元信息、ffmpeg导出mov_text字幕、统计SRT真实条目、元数据‑实际条目比对校验;
  2. 完整性判断标准:字幕轨道元数据帧数等于SRT实际条目数,说明字幕完整;
  3. mov_text字幕提供可读的GPS、姿态、焦距信息,适合快速解析;如需更高频率原始遥测,可使用SD卡原始日志或研究dbgi流解析。

如果文章对你有帮助,欢迎点赞收藏,问题可在评论区交流。

博客地址:https://rsran.blog.csdn.net/

CSDN标签:

大疆 DJI Python ffmpeg SRT字幕 无人机遥测 M3TD 航测

相关推荐
开开心心就好1 小时前
视频压缩工具推荐,画质效果很能打
智能手机·ffmpeg·ocr·word·排序算法·音视频·散列表
幻影123!2 小时前
AlphaZero 五子棋实战(番外二):白棋专项训练的一种方案
python·机器学习·强化学习·五子棋·alpha zero
头发够用的程序员2 小时前
TensorRT 自定义算子插件实战(三):手搓 2×2 最大池化 customMaxpool
人工智能·pytorch·python·深度学习·神经网络·边缘计算·jetson
鬼手点金2 小时前
opencode-crawl4ai使用建议
java·开发语言·人工智能·python·机器学习
海天一色y2 小时前
深入理解对偶单纯形法:原理推导与 Python/MATLAB 编程实现
python·matlab·最优化算法·对偶单纯形法
专业程序开发源2 小时前
springboot校园志愿者管理系统28562-计算机课程设计、毕业设计
java·spring boot·后端·python·django·php·课程设计
计算机毕业编程指导师2 小时前
计算机毕设怎么做?Python+Hadoop的跨平台消费者评论情感分析与数据可视化系统实战 源码 毕业设计 选题推荐 毕设选题 数据分析
大数据·hadoop·python·计算机·spark·课程设计·消费者评论
余槐i2 小时前
WAL 下写并发仍是 1:SQLite 单写者模型实测与绕开方案
数据库·python·性能优化·sqlite·wal
怕浪猫11 小时前
RAG 面试 6 连问,从原理到优化全部覆盖
python·算法·面试