Python 解析大疆无人机 SRT 字幕日志

一、前言

日常使用大疆无人机航拍时,视频配套的.srt字幕文件内置了每一帧的拍摄元数据:GPS经纬度、相对高度、绝对海拔、ISO、快门、光圈、焦距等信息。在无人机视频地理配准、摄影测量、视频GIS可视化、帧位姿匹配的工程场景中,我们需要把SRT日志结构化读取为程序可用的数据字典。

大疆新版SRT字幕存在两个特殊格式问题:

  1. 字幕内容带有 <font size="28"> HTML格式化标签;
  2. 相对高度rel_alt和绝对海拔abs_alt合并写在同一个方括号内 [rel_alt: xxx abs_alt: xxx],普通单值正则无法正常解析两个高程字段,这也是网上很多开源SRT解析代码失效的核心原因。

本文提供一份工程化、注释完整、修复高程读取BUG的Python解析代码,支持构建帧号‑元数据快速索引,方便后续OpenCV逐帧匹配姿态参数,也可以扩展导出CSV用于数据分析。

二、原始SRT文件格式样例

大疆导出的字幕格式如下,可以看到高度字段在同一个[]中:

复制代码
1
00:00:00,000 --> 00:00:00,016
<font size="28">FrameCnt: 1, DiffTime: 16ms
2025-06-04 16:34:44.560
[iso: 100] [shutter: 1/800.0] [fnum: 2.8] [ev: 0] [color_md: default] [focal_len: 24.00] [latitude: 30.75053] [longitude: 121.40925] [rel_alt: 106.100 abs_alt: 145.399] [ct: 5683] </font>

三、原有方案存在的问题

  1. 未过滤<font>标签,正则匹配容易受HTML标签干扰;
  2. 正则规则只适配一个括号一个参数 ,遇到rel_altabs_alt共存的格式捕获失败,高程数据读取为None
  3. 缺少类型自动转换(int/float/str),拿到的数据全部是字符串,后续还要手动处理;
  4. 缺少帧号快速索引结构,逐帧视频匹配效率低下;
  5. 代码注释简陋,工程复用性差,不适合二次开发(摄影测量、视频投影、轨迹绘图)。

四、本代码实现功能

✅ 自动清除大疆SRT自带的<font>格式化标签

✅ SRT时间字符串转为视频相对秒数,保留毫秒精度

✅ 特殊正则规则,兼容同一个方括号内同时存在rel_alt与abs_alt

✅ 自动完成数据类型转换:整型、浮点型、字符串区分处理

✅ 结构化字典存储全部字段,字段附带中文业务含义

✅ 提供帧号‑元数据映射字典,O(1)时间复杂度快速查询单帧参数

✅ 完整详细中文注释,Docstring规范,适合二次开发

✅ 支持后续扩展:导出CSV、姿态插值、视频帧地理匹配

解析得到全部字段清单(附带中文释义)

字段名 中文含义
frame_cnt 帧编号
datetime 拍摄绝对时间(年‑月‑日 时:分:秒.毫秒)
iso ISO感光度
shutter 快门速度
fnum 光圈F值
ev 曝光补偿
color_md 相机色彩模式
focal_len 镜头等效焦距(mm)
latitude WGS84地理纬度
longitude WGS84地理经度
rel_alt 相对起飞点高度(m)
abs_alt 海平面绝对海拔高度(m)
ct 相机内部计数字段

五、依赖环境说明

纯Python标准库实现,无需安装第三方包

复制代码
import re
from datetime import timedelta
from typing import List, Dict

运行环境:Python3.8+

六、核心调用示例

1.完整读取SRT全部字幕块数据

复制代码
srt_full_data = read_dji_srt("test.srt")

返回列表,每一条包含字幕序号、起止时间、原始文本、清洗后文本、完整元数据字典。

2.构建帧索引,通过帧号快速查询GPS与高程

复制代码
frame_meta_lookup = build_frame_metadata_map(srt_full_data)
frame_info = frame_meta_lookup.get(1)
lat = frame_info["latitude"]
lon = frame_info["longitude"]
rel_h = frame_info["rel_alt"]
abs_h = frame_info["abs_alt"]

适合OpenCV读取视频时,帧号一一对应地理位置与拍摄参数

七、完整源码

复制代码
# -*- coding: utf-8 -*-
"""
@Time:2026/09/07 17:15
@Auth:RS迷途小书童
@File:DJ SRT Parsing.py
@IDE:PyCharm
@Purpose:大疆无人机SRT字幕日志解析
@Web:博客地址: https://rsran.blog.csdn.net/
@Attention:
    frame_cnt(帧编号), datetime(拍摄时间), iso(感光度), shutter(快门速度),
    fnum(光圈值), ev(曝光补偿), color_md(色彩模式), focal_len(等效焦距 mm),
    latitude(纬度 WGS84), longitude(经度 WGS84), rel_alt(相对起飞点高度 m),
    abs_alt(海拔高度 m), ct(相机内部计数)
"""
import re
from datetime import timedelta
from typing import List, Dict


def parse_srt_time(time_str: str) -> float:
    """
    将SRT字幕时间字符串转为浮点型总秒数
    :param time_str: SRT原始时间字符串,格式 hh:mm:ss,mmm
    :return: float,从视频起始到该时刻的总秒数
    """
    # 按冒号拆分时:分,再拆分秒与毫秒
    h_part, m_part, s_ms = time_str.strip().split(":")
    s_part, ms_part = s_ms.split(",")
    # 使用timedelta计算时间长度,保证毫秒精度
    t_delta = timedelta(
        hours=int(h_part),
        minutes=int(m_part),
        seconds=int(s_part),
        milliseconds=int(ms_part)
    )
    return t_delta.total_seconds()


def clean_font_tag(raw_text: str) -> str:
    """
    清除大疆SRT自带的HTML字体标签 <font size="xx"> 和闭合标签 </font>
    :param raw_text: 原始带标签的字幕文本
    :return: 去除格式化标签后的干净纯文本
    """
    # 匹配并删除起始font标签
    text = re.sub(r'<font\s+size="\d+">', "", raw_text)
    # 匹配并删除闭合font标签
    text = re.sub(r'</font>', "", text)
    return text.strip()


def extract_metadata(srt_body: str) -> Dict:
    """
    从清洗完成的字幕正文,正则提取全部无人机与相机元数据
    :param srt_body: 去除HTML标签后的多行字幕文本
    :return: 参数字典,不存在的字段赋值为None,每个字段含义:
        frame_cnt(int)        帧编号
        datetime(str)         拍摄绝对时间(年-月-日 时:分:秒.毫秒)
        iso(int)              ISO感光度
        shutter(str)          快门速度
        fnum(float)           光圈F值
        ev(float)             曝光补偿
        color_md(str)         相机色彩模式
        focal_len(float)      镜头等效焦距,单位mm
        latitude(float)       WGS84地理纬度
        longitude(float)      WGS84地理经度
        rel_alt(float)        相对起飞点海拔高度,单位m
        abs_alt(float)        海平面绝对海拔高度,单位m
        ct(int)               相机内部计数字段
    """
    # 初始化所有字段,默认值None
    data = {
        "frame_cnt": None,      # 帧编号
        "datetime": None,       # 拍摄绝对时间
        "iso": None,            # ISO感光度
        "shutter": None,        # 快门速度
        "fnum": None,           # 光圈F值
        "ev": None,             # 曝光补偿
        "color_md": None,       # 相机色彩模式
        "focal_len": None,      # 镜头等效焦距(mm)
        "latitude": None,       # WGS84纬度
        "longitude": None,      # WGS84经度
        "rel_alt": None,        # 相对起飞点高度(m)
        "abs_alt": None,        # 海平面绝对海拔(m)
        "ct": None              # 相机内部计数
    }

    # 正则匹配帧序号 FrameCnt: X
    m_frame = re.search(r"FrameCnt\s*:\s*(\d+)", srt_body)
    if m_frame:
        data["frame_cnt"] = int(m_frame.group(1))

    # 正则匹配拍摄时间 格式:2025-06-04 16:34:44.560
    m_dt = re.search(r"\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}", srt_body)
    if m_dt:
        data["datetime"] = m_dt.group(0)

    # ----------------------独立中括号字段正则配置列表----------------------
    # 格式:(正则表达式, 字典key),适用于单独一个[]只包含一组参数
    single_pattern_list = [
        (r"\[iso:\s*(\d+)\]", "iso"),
        (r"\[shutter:\s*([\d\/\.]+)\]", "shutter"),
        (r"\[fnum:\s*([\d\.]+)\]", "fnum"),
        (r"\[ev:\s*([\-\d\.]+)\]", "ev"),
        (r"\[color_md:\s*(\w+)\]", "color_md"),
        (r"\[focal_len:\s*([\d\.]+)\]", "focal_len"),
        (r"\[latitude:\s*([\-\d\.]+)\]", "latitude"),
        (r"\[longitude:\s*([\-\d\.]+)\]", "longitude"),
        (r"\[ct:\s*(\d+)\]", "ct"),
    ]

    # 循环遍历独立规则,捕获数值并做类型转换
    for reg_exp, dict_key in single_pattern_list:
        match_res = re.search(reg_exp, srt_body)
        if match_res:
            value_str = match_res.group(1)
            # 整型字段转换
            if dict_key in ["iso", "ct"]:
                data[dict_key] = int(value_str)
            # 浮点型字段转换
            elif dict_key in ["fnum", "focal_len", "latitude", "longitude", "ev"]:
                data[dict_key] = float(value_str)
            # 字符串字段直接赋值(快门、色彩模式)
            else:
                data[dict_key] = value_str

    # ----------------------特殊规则:rel_alt与abs_alt共用同一个[]----------------------
    # 匹配格式 [rel_alt: 106.100 abs_alt: 145.399],一次捕获两个高度值
    alt_group_match = re.search(r"\[rel_alt:\s*([\-\d\.]+)\s+abs_alt:\s*([\-\d\.]+)\]", srt_body)
    if alt_group_match:
        data["rel_alt"] = float(alt_group_match.group(1))
        data["abs_alt"] = float(alt_group_match.group(2))

    return data


def read_dji_srt(srt_file_path: str) -> List[Dict]:
    """
    SRT主读取解析函数
    :param srt_file_path: SRT日志文件路径(绝对路径 / 相对路径)
    :return: list[dict],每一项代表一个字幕时间块
        item["index"]:字幕序列号
        item["start_sec"]:字幕起始时刻(单位:秒)
        item["end_sec"]:字幕结束时刻(单位:秒)
        item["raw_text"]:未清洗的原始字幕文本
        item["clean_text"]:去除HTML标签后的干净文本
        item["meta"]:相机与GPS元数据字典(字段含义见extract_metadata函数注释)
    """
    # 使用utf‑8编码读取整个SRT文件
    with open(srt_file_path, "r", encoding="utf-8") as f:
        file_content = f.read()

    # 使用连续换行符分割每一条独立字幕块
    srt_block_list = re.split(r"\n\s*\n", file_content.strip())
    parse_result = []

    # 逐个字幕块处理
    for block_content in srt_block_list:
        # 过滤空行,保留有效文本行
        line_list = [line.rstrip("\n") for line in block_content.splitlines() if line.strip()]
        # 标准SRT单块最少包含3行:序号|时间|内容,不足直接跳过
        if len(line_list) < 3:
            continue

        seq_number = line_list[0].strip()
        time_segment_line = line_list[1].strip()
        content_lines = line_list[2:]

        # 拼接原始多行字幕文本
        raw_subtitle_body = "\n".join(content_lines)
        # 清除font标签
        clean_subtitle_body = clean_font_tag(raw_subtitle_body)

        # 拆分起始时间与结束时间字符串
        start_time_str, end_time_str = time_segment_line.split(" --> ")
        start_second = parse_srt_time(start_time_str)
        end_second = parse_srt_time(end_time_str)

        # 提取所有拍摄定位元数据
        metadata_dict = extract_metadata(clean_subtitle_body)

        # 组装单条字幕结构化数据
        single_srt_item = {
            "index": seq_number,
            "start_sec": round(start_second, 3),
            "end_sec": round(end_second, 3),
            "raw_text": raw_subtitle_body,
            "clean_text": clean_subtitle_body,
            "meta": metadata_dict
        }
        parse_result.append(single_srt_item)

    return parse_result


def build_frame_metadata_map(srt_parse_list: List[Dict]) -> Dict[int, Dict]:
    """
    构建【帧编号 → 完整元数据】的哈希映射字典
    作用:视频逐帧渲染时,通过帧号O(1)快速查询当前帧GPS与相机参数
    :param srt_parse_list: read_dji_srt()返回的完整解析结果列表
    :return: dict {frame_number:int : metadata_dict:dict}
    """
    frame_index_dict = {}
    for srt_item in srt_parse_list:
        meta_data = srt_item["meta"]
        frame_id = meta_data.get("frame_cnt")
        # 仅保存帧号不为空的有效记录
        if frame_id is not None:
            frame_index_dict[frame_id] = meta_data
    return frame_index_dict


if __name__ == "__main__":
    # ====================== 测试运行入口 ======================
    # 修改为你的srt文件路径
    srt_file_path = r"test.srt"

    # 1.执行完整解析
    srt_full_data = read_dji_srt(srt_file_path)
    # 2.构建帧快速查询字典
    frame_meta_lookup = build_frame_metadata_map(srt_full_data)

    print(f"✅ 总共解析字幕块数量:{len(srt_full_data)}")
    print(f"✅ 成功建立帧索引的记录数量:{len(frame_meta_lookup)}")

    # 打印前3帧全部字段用于校验
    for block_index, srt_entry in enumerate(srt_full_data[:3]):
        print(f"\n==================== 字幕块 {block_index + 1} ====================")
        print(f"时间区间(start_sec): {srt_entry['start_sec']:.3f} s")
        print(f"时间区间(end_sec):   {srt_entry['end_sec']:.3f} s")
        meta = srt_entry["meta"]
        print(f"frame_cnt(帧编号)      : {meta['frame_cnt']}")
        print(f"datetime(拍摄时间)     : {meta['datetime']}")
        print(f"iso(感光度)            : {meta['iso']}")
        print(f"shutter(快门速度)      : {meta['shutter']}")
        print(f"fnum(光圈F值)          : {meta['fnum']}")
        print(f"ev(曝光补偿)           : {meta['ev']}")
        print(f"color_md(色彩模式)     : {meta['color_md']}")
        print(f"focal_len(焦距mm)      : {meta['focal_len']}")
        print(f"latitude(WGS84纬度)    : {meta['latitude']}")
        print(f"longitude(WGS84经度)   : {meta['longitude']}")
        print(f"rel_alt(相对起飞高度m): {meta['rel_alt']}")
        print(f"abs_alt(绝对海拔m)     : {meta['abs_alt']}")
        print(f"ct(相机内部计数)       : {meta['ct']}")

八、运行输出效果示例

控制台输出校验结果,可以看到高程字段成功读取:

复制代码
✅ 总共解析字幕块数量:9
✅ 成功建立帧索引的记录数量:9

==================== 字幕块 1 ====================
时间区间(start_sec): 0.000 s
时间区间(end_sec):   0.016 s
frame_cnt(帧编号)      : 1
datetime(拍摄时间)     : 2025-06-04 16:34:44.560
iso(感光度)            : 100
shutter(快门速度)      : 1/800.0
fnum(光圈F值)          : 2.8
ev(曝光补偿)           : 0.0
color_md(色彩模式)     : default
focal_len(焦距mm)      : 24.0
latitude(WGS84纬度)    : 30.75053
longitude(WGS84经度)   : 121.40925
rel_alt(相对起飞高度m): 106.1
abs_alt(绝对海拔m)     : 145.399
ct(相机内部计数)       : 5683

九、后续扩展方向

  1. 增加导出CSV工具函数,把每一帧轨迹保存成表格,用于GIS绘图;
  2. 如果SRT包含云台姿态yaw/pitch/roll,补充对应的正则捕获规则;
  3. 增加时间插值算法,补全缺失帧的GPS高程数据;
  4. 对接OpenCV读取航拍视频,实现视频帧‑地理坐标实时匹配,用于正射影像、视频地理配准;
  5. 结合摄影测量共线方程,实现航拍视频像素反算地面坐标。

十、总结

网上很多大疆SRT解析脚本没有考虑新版高程字段合并在同一个中括号的格式,导致高程读取失败。本份代码专门针对该坑做了正则兼容,代码注释规范,适合测绘、遥感、无人机视频处理相关开发人员直接使用与二次开发。

如果对你有用,欢迎点赞收藏,后续我会更新CSV导出与视频地理配准完整工程代码。

相关推荐
我不会起名字3221 小时前
一天一道算法题(29):单调栈
java·数据结构·python·算法·leetcode·golang·单调栈
萧鼎1 小时前
2026新库实测:sbxloop 1.5.24 让 AI Agent 在 Docker 沙箱中安全自治,告别环境混乱
人工智能·python·开源·开发工具·ai agent
点心的游戏开发世界1 小时前
GDScript 入门笔记(十六):文件读写与数据持久化
开发语言·笔记·游戏引擎·godot
栀椩1 小时前
CODrone 无人机航拍车辆检测
pytorch·python·yolo
亥时科技1 小时前
跨区域无人机作业,如何做到近乎零延迟?
开源·无人机·ai巡检
会飞的拖把2 小时前
Python序列详解:列表、元组、字符串的通用操作(超详细版)
开发语言·windows·python
2601_962283882 小时前
如何通过python控制nbiot
python·串口通信·数据传输·nb-iot·at命令
reasonsummer2 小时前
【办公类-115-01】20260906育儿知识(家园小报)批量制作(2026年9月-2027年6月)
开发语言·数据库·c#
君顾12 小时前
智慧场馆解决方案小程序系统实战:从架构设计到上线指南
java·开发语言·智慧场馆