ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大疆无人机Python数据玩法:从照片GPS提取到正射影像生成

大疆无人机Python数据玩法:从照片GPS提取到正射影像生成 很多人买回大疆无人机之后使用最多的场景还是“起飞-拍视频-降落后把素材导进手机里简单剪辑”。但如果你把无人机只看成一台会飞的相机其实错过了它最有价值的一部分能力飞机在空中记录的 GPS、姿态、海拔、拍摄时间以及这些数据组合之后形成的地理信息工作流。这篇文章准备从“数据玩法”的角度出发重新打开大疆无人机的新玩法。我们会用 Python 读取航拍照片中的 GPS 信息把几百张素材按日期自动归档生成可在地图软件里查看的曝光点地图再自己写一个扫描航线生成脚本最后把照片丢进开源建图工作流生成正射影像。整个过程不需要破解、不需要刷第三方固件也不需要动飞控完全基于官方拍摄产物和合法软件开发工具。文章内容对以下读者会比较友好手里有大疆消费级或行业级无人机、想学 Python 的开发者、航测或 GIS 入门爱好者以及准备用 DJI SDK 做二次开发但还没有明确方向的人。全文会包含可直接运行的 Python 脚本和命令行示例同时也会说明每个步骤背后的原理和可能踩到的坑。1. 重新理解大疆玩法从“飞起来”到“把数据用起来”1.1 传统玩法与数据玩法的差别传统的大疆无人机玩法核心是“影像”。飞手关心的是画面是否稳定、构图是否好看、色彩是否通透。这个方向本身没有错但最终产出的是一段视频或一组照片处理链路相对短。数据玩法不太一样。它把无人机看作一个“可移动的传感器平台”。飞机在空中的每一个位置、每一条飞行轨迹、每一张照片的拍摄点都可以被记录下来并参与计算。举个例子你用无人机拍了一块农田的 200 张照片。传统玩法下这 200 张照片只是素材你可能挑出 10 张好看的导出来。但在数据玩法下这 200 张照片可以变成一张带每一个拍照点坐标的 CSV 表格一张可以交互缩放的地图一条自动生成的航测扫描航线一张完整的正射影像图一组可用于测量距离、面积甚至作物长势的多光谱数据。这些能力不是只有行业级无人机才能用。只要你的无人机带 GPS、云台相机能记录 EXIF 信息就具备了玩数据的基本条件。1.2 本文涉及的玩法边界在很多社区里关于大疆无人机“玩法”的讨论经常走上误区。例如通过修改固件解除限高限远、绕过禁飞区限制、使用非官方地面站刷写第三方飞控固件等。这一类行为风险极高轻则导致设备失去保修甚至损坏重则违反当地航空管理规定本文不会涉及也建议读者不要尝试。我们接下来要做的所有操作都符合以下几个边界照片和数据全部来自无人机正常起飞后在存储卡中产生的文件不修改无人机固件、不破解任何官方限制航线生成只是用于数据规划和可视化实际飞行仍使用官方 App 或合规的开放 SDK 能力所有数据后处理工具都是通用开发库或开源软件。在这个边界内你能玩的东西已经非常多了。2. 环境准备与项目结构2.1 需要的硬件与软件硬件方面本文对无人机机型没有严格限制只要拍摄出的照片带 GPS EXIF 信息即可。大疆大部分机型默认都会在照片 EXIF 中写入经纬度部分机型还会写入海拔高度。测试前建议先用官方 App 拍几张 JPG 照片然后导出到电脑。软件方面只需要准备基础 Python 环境和几个常见库工具用途Python 3.9运行所有数据处理脚本piexif读取 JPG 照片的 EXIF 信息Pillow辅助 piexif 处理图片元数据folium生成交互式 HTML 曝光点地图Docker运行开源建图工具 ODM/WebODMexiftool排查照片元数据读取失败问题这里补充一句版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。不建议一上来就追求最新 Python 版本稳定版本更省心。2.2 创建项目目录建议新建一个专门的工程目录把所有代码和素材统一管理。目录结构可以参考这样drone-play/ ├── photos/ # 从无人机存储卡拷贝出来的 JPG │ ├── DJI_0001.JPG │ ├── DJI_0002.JPG │ └── ... ├── archive/ # 按日期整理后的归档目录 ├── output/ # KML、HTML、CSV 等输出文件 ├── 01_read_gps.py # 读取 GPS 并导出 CSV ├── 02_organize_files.py # 按拍摄日期归档 ├── 03_generate_waypoints.py # 生成扫描航线 KML └── 04_photo_map.py # 生成 HTML 曝光点地图在项目目录中创建虚拟环境并安装 Python 依赖python -m venv venv # Windows 下激活 venv\Scripts\activate # macOS / Linux 下激活 source venv/bin/activate pip install piexif pillow folium后续所有 Python 命令都需要在激活虚拟环境的状态下执行。3. 玩法一用 Python 把航拍照片里的定位信息挖出来3.1 为什么能读取到 GPS无人机在拍照时会同步记录拍摄瞬间飞机的姿态、位置、云台角度等参数并把一部分信息写入照片文件。最重要的是 EXIF可交换图像文件格式和 XMP 元数据。在 EXIF 中GPS 信息一般放在GPS IFD里例如GPSLatitude纬度度分秒格式GPSLongitude经度度分秒格式GPSAltitude海拔高度GPSLatitudeRef/GPSLongitudeRef南北纬、东西经标识。Python 的piexif库能直接读取 JPG 文件中的 EXIF 数据。下面这个脚本会遍历指定目录中的.JPG文件提取拍摄时间和经纬度生成drone_metadata.csv。3.2 读取 GPS 的 Python 脚本在项目根目录创建01_read_gps.py#!/usr/bin/env python3 读取大疆航拍 JPG 中的拍摄时间与 GPS 坐标并导出为 CSV。 用法python 01_read_gps.py --source photos import argparse import csv from datetime import datetime from pathlib import Path import piexif def dms_to_dd(value): 将 EXIF 中度分秒格式转换为十进制经纬度。 EXIF 中的度分秒结构一般是 ((度,1), (分,1), (秒,100)) if not value: return None try: degrees value[0][0] / value[0][1] minutes value[1][0] / value[1][1] seconds value[2][0] / value[2][1] return degrees minutes / 60.0 seconds / 3600.0 except (TypeError, ValueError, ZeroDivisionError): return None def rational_to_float(value): 将 EXIF 中的有理数转换为浮点数。 if not value: return None try: return value[0] / value[1] except (TypeError, ValueError, ZeroDivisionError): return None def read_photo_info(image_path: Path): 读取单张照片的拍摄时间、经纬度和海拔。 返回一个字典读取失败或字段缺失时值为 None。 exif_dict piexif.load(str(image_path)) exif_ifd exif_dict.get(Exif, {}) gps_ifd exif_dict.get(GPS, {}) # 拍摄时间通常在 ExifIFD.DateTimeOriginal 中 datetime_str None dt_raw exif_ifd.get(piexif.ExifIFD.DateTimeOriginal) if dt_raw: try: parsed datetime.strptime(dt_raw.decode(utf-8), %Y:%m:%d %H:%M:%S) datetime_str parsed.isoformat() except (ValueError, AttributeError): datetime_str None # 经纬度转换 lat_ref gps_ifd.get(piexif.GPSIFD.GPSLatitudeRef) lon_ref gps_ifd.get(piexif.GPSIFD.GPSLongitudeRef) lat dms_to_dd(gps_ifd.get(piexif.GPSIFD.GPSLatitude)) lon dms_to_dd(gps_ifd.get(piexif.GPSIFD.GPSLongitude)) if lat is not None and lat_ref and lat_ref.decode(utf-8).upper() S: lat -lat if lon is not None and lon_ref and lon_ref.decode(utf-8).upper() W: lon -lon altitude rational_to_float( gps_ifd.get(piexif.GPSIFD.GPSAltitude) ) return { filename: image_path.name, datetime: datetime_str, latitude: lat if lat is not None else , longitude: lon if lon is not None else , altitude: altitude if altitude is not None else , } def main(): parser argparse.ArgumentParser(description读取照片 GPS 信息) parser.add_argument(--source, defaultphotos, help存放 JPG 的目录) parser.add_argument(--output, defaultoutput/drone_metadata.csv, help输出 CSV 路径) args parser.parse_args() source_dir Path(args.source) output_path Path(args.output) output_path.parent.mkdir(parentsTrue, exist_okTrue) records [] no_gps_count 0 for image_path in sorted(source_dir.glob(*.JPG)) sorted(source_dir.glob(*.jpg)): info read_photo_info(image_path) if info[latitude] or info[longitude] : no_gps_count 1 print(f[跳过] {image_path.name} 没有 GPS 信息) continue records.append(info) print( f[成功] {image_path.name}: ftime{info[datetime]}, flat{info[latitude]}, flon{info[longitude]}, falt{info[altitude]} ) with open(output_path, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter( f, fieldnames[filename, datetime, latitude, longitude, altitude], ) writer.writeheader() writer.writerows(records) print(f\n共读取 {len(records)} 张含 GPS 照片{no_gps_count} 张无 GPS 信息。) print(f结果已写入{output_path}) if __name__ __main__: main()运行python 01_read_gps.py --source photos --output output/drone_metadata.csv如果照片目录里有两张来自同一航次的照片预期输出类似于下面这样[成功] DJI_0001.JPG: time2025-11-20T15:22:31, lat30.123456, lon120.654321, alt112.4 [成功] DJI_0002.JPG: time2025-11-20T15:22:40, lat30.123502, lon120.654410, alt112.7 共读取 200 张含 GPS 照片0 张无 GPS 信息。 结果已写入output/drone_metadata.csv3.3 为什么使用 utf-8-sig 编码代码中写 CSV 时使用了utf-8-sig编码。这个细节很重要utf-8-sig会在文件开头写入 BOM而 Excel 在打开普通utf-8文件时经常把中文列名或内容识别成乱码。加上 BOM 之后用 Excel 直接打开 CSV 就能正确显示中文。这里也提醒一个容易忽略的点脚本默认只查找.JPG和.jpg文件。如果你使用 DNG RAW 格式拍摄piexif 默认可能无法直接解析需要在后续先转为 JPG 或者使用 exiftool 做元数据读取。4. 玩法二使用 Python 批量整理航拍素材并按 GPS 落图4.1 为什么需要自动化归档一次航拍任务常常会产生几百张照片。如果全手动整理很容易出现目录混乱、日期重复、拍摄地点无法对应等问题。更麻烦的是当你想后续做正射拼图或再次查证某次飞行的拍摄范围时靠人工找文件几乎不可能。我们可以把第二步做成一个简单的归档脚本按拍摄年份和日期自动生成目录并把照片复制过去。这样后续建图和轨迹分析都会轻松很多。在项目根目录创建02_organize_files.py#!/usr/bin/env python3 读取第一步生成的 CSV并按拍摄日期把照片归档到 archive/ 目录。 用法python 02_organize_files.py --source photos --csv output/drone_metadata.csv import argparse import csv import shutil from datetime import datetime from pathlib import Path def parse_datetime(datetime_str: str): 将 ISO 格式时间转换为 date 对象。 if not datetime_str: return None try: return datetime.fromisoformat(datetime_str) except ValueError: return None def main(): parser argparse.ArgumentParser(description按日期归档照片) parser.add_argument(--source, defaultphotos, help原始 JPG 所在目录) parser.add_argument(--csv, defaultoutput/drone_metadata.csv, help元数据 CSV) parser.add_argument(--archive, defaultarchive, help归档根目录) args parser.parse_args() source_dir Path(args.source) archive_root Path(args.archive) archive_root.mkdir(parentsTrue, exist_okTrue) moved 0 unknown 0 with open(args.csv, r, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: src_path source_dir / row[filename] if not src_path.exists(): print(f[缺失] {src_path.name} 不存在跳过) continue shot_date parse_datetime(row[datetime]) if shot_date is None: target_dir archive_root / 未知日期 else: target_dir archive_root / str(shot_date.year) / shot_date.strftime(%Y%m%d) target_dir.mkdir(parentsTrue, exist_okTrue) target_path target_dir / src_path.name shutil.copy2(src_path, target_path) moved 1 print(f[归档] {src_path.name} - {target_path}) print(f\n共归档 {moved} 个文件{unknown} 个文件无法识别日期。) if __name__ __main__: main()运行python 02_organize_files.py --source photos --csv output/drone_metadata.csv --archive archive运行完成后你的素材就会形成类似下面的结构archive/ └── 2025/ └── 20251120/ ├── DJI_0001.JPG ├── DJI_0002.JPG └── ...4.2 把曝光点绘制到交互地图上有了 CSV就可以用 folium 生成一张交互式 HTML 地图。这样你能直接在浏览器中看到每张照片的拍摄位置也可以快速判断这次飞行是否覆盖了整个目标区域。创建04_photo_map.py#!/usr/bin/env python3 读取元数据 CSV在地图上绘制所有拍摄点。 用法python 04_photo_map.py --csv output/drone_metadata.csv import argparse import csv import folium def main(): parser argparse.ArgumentParser(description生成曝光点地图) parser.add_argument(--csv, defaultoutput/drone_metadata.csv, help元数据 CSV) parser.add_argument(--output, defaultoutput/photo_map.html, help输出 HTML) args parser.parse_args() points [] with open(args.csv, r, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: try: lat float(row[latitude]) lon float(row[longitude]) points.append((lat, lon, row[filename])) except ValueError: continue if not points: print(没有可用的坐标点请先运行 01_read_gps.py) return center_lat sum(p[0] for p in points) / len(points) center_lon sum(p[1] for p in points) / len(points) m folium.Map(location[center_lat, center_lon], zoom_start16) for lat, lon, name in points: folium.CircleMarker( location[lat, lon], radius4, popupname, color#1a73e8, fillTrue, fill_color#1a73e8, ).add_to(m) m.save(args.output) print(f共绘制 {len(points)} 个拍摄点地图已保存到{args.output}) if __name__ __main__: main()运行python 04_photo_map.py --csv output/drone_metadata.csv --output output/photo_map.html浏览器打开output/photo_map.html后你可以看到所有拍摄点的位置分布。如果这次飞行是按航线拍摄的曝光点通常呈现比较规则的蛇形排列如果点是乱序的说明拍摄时没有使用航点任务而是纯手动打点这对后续正射拼图的效果会有影响。5. 玩法三用 Python 自己生成扫描航线5.1 航线规划的核心参数大疆官方 App 中已经内置了航点飞行、建图航拍等任务一般情况下直接使用即可。但如果你想理解航线规划原理或者需要批量生成可复用的测区范围就可以用 Python 自己写一个简单的扫描航线生成器。扫描航线的核心思路是“蛇形覆盖”在目标区域的外包矩形内按固定间隔画一条一条平行线飞行器沿一条线从一端飞到另一端到达边界后拐弯进入下一条相邻线方向相反如此循环直到覆盖整个矩形区域。这里的“固定间隔”对应旁向重叠率。假设你要做正射建图相邻航线间距通常可以由公式换算间隔 地面幅宽 × (1 - 旁向重叠率)例如一架无人机在 100 米高度拍摄照片覆盖的地面宽度是 80 米希望旁向重叠率达到 70%那么相邻航线间隔大约为 24 米。下面的脚本会根据中心点经纬度、区域长宽和航线间距生成蛇形航点和 KML 文件。5.2 生成 KML 航线脚本创建03_generate_waypoints.py#!/usr/bin/env python3 根据中心点和扫描宽度/高度生成蛇形航线并导出 KML。 用法 python 03_generate_waypoints.py \ --lat 30.123456 \ --lon 120.654321 \ --width 120 \ --height 100 \ --spacing 25 import argparse import math OUTPUT_TEMPLATE ?xml version1.0 encodingUTF-8? kml xmlnshttp://www.opengis.net/kml/2.2 Document nameserpentine-waypoints/name Placemark namescan-route/name LineString tessellate1/tessellate coordinates {coords} /coordinates /LineString /Placemark /Document /kml def offset_meters(lat: float, lon: float, d_north: float, d_east: float): 根据起始经纬度向北偏移 d_north 米、向东偏移 d_east 米返回新经纬度。 该方法只在几公里范围内有较好精度超过 5 公里建议改用 UTM 投影。 d_lat d_north / 111320.0 d_lon d_east / (111320.0 * math.cos(math.radians(lat))) return lat d_lat, lon d_lon def generate_waypoints(center_lat, center_lon, width_m, height_m, spacing_m, altitude_m): 生成从西南角开始的蛇形航线航点。 每个航点是一个 (lat, lon, alt) 三元组。 start_lat, start_lon offset_meters(center_lat, center_lon, -height_m / 2, -width_m / 2) rows max(1, int(math.ceil(height_m / spacing_m))) waypoints [] for row in range(rows): row_lat, row_lon offset_meters(start_lat, start_lon, row * spacing_m, 0) west_point (row_lat, row_lon) east_point offset_meters(row_lat, row_lon, 0, width_m) if row % 2 0: waypoints.append((west_point[0], west_point[1], altitude_m)) waypoints.append((east_point[0], east_point[1], altitude_m)) else: waypoints.append((east_point[0], east_point[1], altitude_m)) waypoints.append((west_point[0], west_point[1], altitude_m)) return waypoints def save_kml(waypoints, output_path): coords_lines [] for lat, lon, alt in waypoints: coords_lines.append(f{lon:.7f},{lat:.7f},{alt:.1f}) coords \n .join(coords_lines) with open(output_path, w, encodingutf-8) as f: f.write(OUTPUT_TEMPLATE.format(coordscoords)) def main(): parser argparse.ArgumentParser(description生成蛇形扫描航线 KML) parser.add_argument(--lat, typefloat, requiredTrue, help中心点纬度) parser.add_argument(--lon, typefloat, requiredTrue, help中心点经度) parser.add_argument(--width, typefloat, default120, help测区宽度米) parser.add_argument(--height, typefloat, default100, help测区高度米) parser.add_argument(--spacing, typefloat, default25, help航线间距米) parser.add_argument(--altitude, typefloat, default100, help飞行高度米) parser.add_argument(--output, defaultoutput/waypoints.kml, help输出 KML 路径) args parser.parse_args() waypoints generate_waypoints( center_latargs.lat, center_lonargs.lon, width_margs.width, height_margs.height, spacing_margs.spacing, altitude_margs.altitude, ) Path(args.output).parent.mkdir(parentsTrue, exist_okTrue) save_kml(waypoints, args.output) print(f生成 {len(waypoints)} 个航点已保存到{args.output}) if __name__ __main__: main()注意这里也需要引入Path所以在脚本头部应补充from pathlib import Path然后运行python 03_generate_waypoints.py \ --lat 30.123456 \ --lon 120.654321 \ --width 120 \ --height 100 \ --spacing 25 \ --altitude 80输出文件中每一行坐标都是“经度,纬度,高度”的格式这是 KML 的标准顺序不要写反。5.3 航线脚本不是飞行指令需要特别强调一点这个脚本生成的是 KML 路线数据不是官方的飞行任务文件。它更适合用来做任务规划预览、在 QGIS 中叠加对比测区范围、或者作为你理解航线规划原理的学习工具。实际飞行仍然应该使用大疆官方 App 中的“航点飞行”“建图航拍”等功能。如果你想把自动化航线真正跑起来路线是通过 DJI Mobile SDK 或行业应用在合规授权条件下开发自己的执行程序而不是简单地把 KML 导入某个非官方地面站。6. 玩法四用开源
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进