ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

无人机航拍目标定位:用Python实现像素坐标到经纬度的转换

无人机航拍目标定位:用Python实现像素坐标到经纬度的转换 无人机航拍这些年越来越普及但真正玩出花的人往往卡在一个问题上——画面里的目标到底在地图上的哪个位置以前我的做法是发现目标就记下无人机当时的经纬度再估个偏移量发给同事。可这种估算法误差随飞行高度直线上升100米误差三五米200米误差七八米用来当粗参考还行想精确到米级根本指望不上。后来我把这个需求彻底梳理了一遍发现用Python写个坐标投影工具整个过程其实就是像素坐标 → 机身坐标 → 地面坐标 → 经纬度的四步走而且整个流程依赖的条件绝大多数无人机航拍任务本身就已经满足机载GPS记录了位置飞控和云台记录了姿态角相机规格是公开参数。这篇文章我就把这个计算链路完整拆给大家附带一整套可以直接复制运行的代码针对的就是有航拍测绘、巡检定位、搜救标记这类需求的朋友。1. 目标坐标计算的底层逻辑1.1 从一张航拍照里抠出目标位置本质上在算什么无人机在空中拍摄时对地面目标来说你的相机其实就是一个针孔投影仪只不过投影方向反过来了——现实世界的地面点通过镜头映射到了感光芯片上形成照片里的一个像素。我们要做的就是把这个投影过程逆推回去从照片上选中的那个像素点反向计算出它在地面上的位置。这个过程放到数学里就是一条完整的光路追溯。首先照片上的像素坐标是二维的我们必须知道相机在拍摄那一瞬间的内参和外参。内参描述的是光心位置、焦距和感光芯片尺寸外参描述的是相机三维空间中的位置朝向。知道了内外参我就能从像素坐标得到一个三维视线方向。再从无人机的位置出发沿着这个视线方向往前延伸找到它与地面的交点这个交点就是目标点的空间坐标。最后再把这个空间坐标从直角坐标系折算成经纬度任务就闭环了。这里有个关键点很多人一开始会搞混无人机的GPS坐标是天线或飞控的位置不等于相机的光心位置。两边的安装偏差虽然只有几厘米到十几厘米但在某些追求精度的场景里必须考虑。不过对大多数应用来说这个偏差可以压缩到忽略不计的范围后面我会讲怎么处理。1.2 为什么用普通消费级无人机也能做高精度测量我以前跟朋友聊这个思路对方第一反应是——这不得用RTK或者专业测绘无人机才能干吗这话对也不对。RTK的作用是给无人机本身的位置精度做个大幅提升从米级变成厘米级。但如果你已经有了一个静态目标无人机飞过去在很多反光、遮挡条件下RTK的固定解都未必稳。说白了RTK解决的是平台自身定位的问题而不是目标投影计算的问题。普通消费级无人机的GPS精度确实一般水平误差大约在1到3米但这已经是裸GPS的水平了。我们在计算目标坐标时无人机和应用目标之间往往是同一架次内的相对关系只要悬停或匀速飞行时定位没有明显漂移靠单帧照片推算目标的相对位置再叠加到无人机自身的经纬度上最终能做到3到5米甚至更好的绝对精度。如果同一个目标有多张不同角度、不同航线的照片还可以把多个结果取平均把系统随机误差进一步压缩。真正决定精度的天花板其实是相机姿态角的准确性。飞控里的姿态数据通常来自惯性测量单元加融合算法俯仰横滚角精度通常在0.1到0.5度之间偏航角在带有磁力计辅助时也能做到1度左右。在100米高度上0.5度的角度误差对应到地面大约是0.87米的横向偏移1度就是1.7米左右。所以思路是完全可行的——真正要研究的是怎么把这些误差控制住。2. 关键参数与数据准备2.1 无人机的位置、高度、姿态数据从哪来要做坐标计算第一步就是拿到无人机拍摄瞬间的完整状态信息。这部分数据有三个常见来源照片EXIF、飞控日志、以及大疆特有的XMP元数据。照片EXIF里最常用的字段是GPS经纬度和海拔。用Python的exifread或Pillow都能直接读出来。但要注意照片里记录的GPS经纬度通常是度分秒格式处理前必须先转换。海拔这一项要格外留心消费级无人机气压计给出的海拔是大气压换算的几何海拔误差在十米级别很正常如果直接用会影响比例尺的计算精度。所以我在实际项目中高度那项都优先用相对起飞点的离地高度或者直接从飞控日志里取相对高度而不是用绝对海拔。飞控日志里则有更完整的姿态、高度和速度数据Pix4Dmapper、大疆的DJI FlightHub、或者自己解析DAT/CSV日志都能提取。对一般开发来说更省事的办法是直接读照片里的XMP元数据大疆相机会在每张照片的XMP区域写入FlightYawDegree、FlightPitchDegree、FlightRollDegree以及GimbalYawDegree、GimbalPitchDegree、GimbalRollDegree这些字段。飞控姿态和云台姿态都有分开记录这也是后面做投影计算时必须区分的。2.2 相机内参怎么换算焦距为什么单位是像素相机内参里最核心的变量有两个焦距和像主点。像主点通常就取图像正中心这对大多数消费级无人机是合理的近似。不少人看到相机参数表上写的24mm就以为是内参里的焦距其实那通常是等效全画幅焦距。做坐标计算必须用物理焦距单位是毫米再结合传感器尺寸换算成像素单位。换算公式不复杂焦距像素 物理焦距毫米 / 传感器宽度毫米 × 图像宽度像素。垂直方向同理。我举例说明。大疆Mavic 3广角相机物理焦距12.29mmCMOS尺寸17.3×13mm照片分辨率是5280×3960。代入计算fx12.29/17.3×5280≈3752像素fy12.29/13×3960≈3743像素。这意味着在100米高度单个像素对应地面大约2.7厘米。这个数字越往下算越能体现为什么消费级无人机在目标坐标计算上其实潜力很大。焦距像素和传感器宽度、分辨率之间的换算关系是我每次写代码都会放在最前面检查的。很多人拿错了焦距后面所有计算都白费。建议在拿到新机型或者换云台相机时先做一次简单的标定或对照官方规格书校准参数。2.3 姿态角与坐标系约定把方向搞清楚再动手坐标系约定是这类程序最容易出错的地方。无人机的机头朝向、偏航角、云台朝向以及图像坐标的正方向每一处都有不同的习惯和约定。拿偏航角来说很多飞控以正北为0度顺时针增大90度表示机头朝正东。而某些惯性导航算法输出的是弧度制有些坐标系里负角度又是另一种含义。同一个角度数据从不同日志源读出来的语义可能完全不同。图像坐标也有约定。常见的照片都是从左上角开始x轴向右为正y轴向下为正。这一点跟数学里y轴通常向上的习惯不同容易出符号错误。在后面的代码里我会把图像右方向和机身右方向一一对应、图像下方向对应机尾方向这些关系明确写出来这样理解代码的时候就不会被绕晕。做坐标计算时我习惯把手机和笔记本里的所有角度数据统一转换成弧度制参与运算最后输出经纬度时才转回角度。混用度数和弧度的bug我亲眼见过团队里排查了一个下午。提前统一约定是给未来的自己省时间。3. 实操过程与核心代码实现3.1 最小可用环境依赖几分钟跑起来代码本身不需要重型依赖只要你的电脑能跑Python 3.8以上就行。平时我习惯先建一个干净的虚拟环境再装exifread和Pillow这两个库一个用来读照片元数据一个用来做图像像素坐标的读取和处理。如果你后续要处理大量照片、做批量计算可以再装上pandas、numpy这些但跑通核心逻辑并不需要它们。安装命令也一起列出来python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install exifread Pillow有些朋友读EXIF会卡在依赖装不好其实还有一个更轻量的替代方案——直接用Pillow读_getexif()也能拿到GPS和姿态字段只是字段结构比较繁琐。我推荐exifread因为它对各类相机的兼容性更好读取方式也直接。3.2 核心代码从像素点到经纬度的完整实现下面这段代码就是整个流程的主干。为了便于理解我把参数定义、坐标转换和主流程拆成几个函数。代码中用到的是正下视拍摄模型也就是云台基本垂直朝下这是绝大多数巡检和测绘航拍的实际状态。# -*- coding: utf-8 -*- 无人机航拍目标地理坐标计算工具 原理利用无人机机载GPS/姿态角与相机内参将图像中的像素点投影到地面 解析出目标点的WGS84经纬度坐标。 作者一个经常跑外业的航测程序员 import math from dataclasses import dataclass dataclass class CameraParams: 相机内参相关参数 focal_mm: float # 物理焦距毫米 sensor_width_mm: float # CMOS宽度毫米 sensor_height_mm: float # CMOS高度毫米 img_width: int # 图像宽像素 img_height: int # 图像高像素 dataclass class DroneState: 无人机拍摄瞬间的状态 lat: float # 无人机纬度度 lon: float # 无人机经度度 height: float # 无人机相对地面高度米 yaw: float # 机头偏航角度正北为0顺时针增大 def calc_pixel_focal(cam: CameraParams): 将物理焦距转换为像素焦距并计算像主点坐标 焦距像素 物理焦距 / 传感器宽度 * 图像像素宽度 像主点近似取图像中心 fx cam.focal_mm / cam.sensor_width_mm * cam.img_width fy cam.focal_mm / cam.sensor_height_mm * cam.img_height cx cam.img_width / 2.0 cy cam.img_height / 2.0 return fx, fy, cx, cy def pixel_to_target_lla(u: float, v: float, drone: DroneState, cam: CameraParams): 将目标点在照片中的像素坐标(u, v)转换为WGS84经纬度 参数 u: 目标点x坐标像素从左往右增大 v: 目标点y坐标像素从上往下增大 返回 (target_lat, target_lon) 目标点纬度、经度单位度 # 1. 换算像素焦距和像主点 fx, fy, cx, cy calc_pixel_focal(cam) # 2. 目标点相对图像中心的偏移像素 dx_px u - cx dy_px v - cy # 3. 比例尺每个像素对应多少米的地面距离 # 正下视前提下比例尺 高度 / 像素焦距 focal_px (fx fy) / 2.0 scale drone.height / focal_px # 4. 把像素偏移换算成机体系下的地面偏移 # 图像右侧方向 - 机身右侧图像下方方向 - 机尾方向 right_offset dx_px * scale fwd_offset -dy_px * scale # 5. 从机体系旋转到ENU东-北-天 yaw_rad math.radians(drone.yaw) east right_offset * math.cos(yaw_rad) fwd_offset * math.sin(yaw_rad) north -right_offset * math.sin(yaw_rad) fwd_offset * math.cos(yaw_rad) # 6. 把ENU偏移折算成经纬度增量 # 纬度方向每度约111.32公里经度方向每度距离随纬度变化 meters_per_deg_lat 111_320.0 meters_per_deg_lon 111_320.0 * math.cos(math.radians(drone.lat)) target_lat drone.lat north / meters_per_deg_lat target_lon drone.lon east / meters_per_deg_lon return target_lat, target_lon if __name__ __main__: # 以大疆 Mavic 3 广角相机为例 cam CameraParams( focal_mm12.29, # Mavic 3 广角物理焦距 sensor_width_mm17.3, # 4/3 CMOS sensor_height_mm13.0, img_width5280, img_height3960, ) # 假设无人机拍摄瞬间的参数 drone DroneState( lat30.123456, lon120.654321, height100.0, yaw45.0, ) # 目标点在照片正中心时结果应该约等于无人机坐标 u, v 2640.0, 1980.0 lat, lon pixel_to_target_lla(u, v, drone, cam) print(f目标坐标: {lat:.6f}, {lon:.6f}) # 目标点在画面右侧200像素处 u, v 2840.0, 1980.0 lat, lon pixel_to_target_lla(u, v, drone, cam) print(f目标坐标: {lat:.6f}, {lon:.6f})直接运行这段代码你会看到两行坐标输出。第一行基本就是无人机的坐标因为目标点在图像正中心。第二行是在右侧200像素处经度会有一个微小的偏移。这个偏移量在100米高度约等于200×0.0267≈5.3米符合实际预期。3.3 读懂代码里的三步走哪里能省哪里不能省这段代码虽然只有几十行但每一步都有讲究。从像素偏移到地面距离用的是最简单的等比例换算。正下视时地面上一个物体在照片里占多少个像素跟它的实际尺寸、飞行高度、镜头焦距之间存在等比关系地面距离/高度 像素偏移/焦距。把公式变换一下就是代码里的scale height / focal_px这一步是整个计算的核心。飞行高度必须使用相对地面的高度不是海拔高度。很多小伙伴直接用GPS海拔算结果在起伏地形区域目标坐标偏差能被拉得很大。代码里注释已经写明白了实操时务必先确认高度类型。从机体系到ENU的旋转这里用到了偏航角的旋转矩阵。机头朝向定义为正北0度、顺时针增大那么机身右侧的方向就落在方位角yaw90度的位置。图像右方向对应机身右方向图像下方向对应机尾方向。理解了这一步代码里的正负号就都清楚了。很多人是自己画一张坐标系草图然后照着草图把旋转矩阵写出来这个方法比死记代码里的公式靠谱得多。最后从ENU偏移折算经纬度用的是球面近似模型。纬度方向每度对应的距离在地球表面近似恒定约111.32公里经度方向则要乘上当前纬度的余弦。这个近似在几公里范围内足够用了如果目标距离无人机几十公里远那就该换用更严谨的大地主题求解算法了。3.4 从照片自动读取GPS和姿态参数上面的例子是手动输入无人机参数实际跑项目时最好还是自动读取。这里我给出一个读取EXIF的小函数可以把它和前面的主程序拼在一起用import exifread def read_gps_from_jpg(file_path): 从JPEG文件中读取GPS经纬度和海拔简易版 with open(file_path, rb) as f: tags exifread.process_file(f) def _to_degrees(value): d float(value.values[0].num) / float(value.values[0].den) m float(value.values[1].num) / float(value.values[1].den) s float(value.values[2].num) / float(value.values[2].den) return d m / 60.0 s / 3600.0 lat_tag tags.get(GPS GPSLatitude) lat_ref str(tags.get(GPS GPSLatitudeRef, N)) lon_tag tags.get(GPS GPSLongitude) lon_ref str(tags.get(GPS GPSLongitudeRef, E)) alt_tag tags.get(GPS GPSAltitude) if lat_tag and lon_tag: lat _to_degrees(lat_tag) lon _to_degrees(lon_tag) if lat_ref S: lat -lat if lon_ref W: lon -lon alt None if alt_tag: alt float(alt_tag.values[0].num) / float(alt_tag.values[0].den) return lat, lon, alt return None, None, None大疆的XMP里还有更完整的姿态角字段用exifread处理XMP时会以Image FlightYawDegree这类标签出现可以直接读取。如果读不到就需要去飞控日志里找。这里我不展开全部细节核心是提醒你有自动读取能力后批量处理几百张照片就变成一件很轻松的事了。4. 精度评估与误差来源哪些坑会吃掉你的坐标精度4.1 影响精度的前三名高度、姿态角、GPS漂移我做过一组对照实验在同一位置、同一高度用同一个相机连续拍摄10张照片分别对同一个地面目标做坐标解算。结果10个点的离散程度大约在2米左右。这个离散主要来自于无人机GPS在悬停时的随机漂移以及姿态角读数的小幅波动。高度误差的影响最为直接。比例尺和高度是线性关系高度错了5%地面距离计算就跟着错5%。100米高度时5%就是5米这个量级很可观。所以要尽量使用相对起飞点的离地高度有条件的话用激光测距模块或者视觉辅助定高设备。姿态角的误差在正下视场景主要影响目标偏离画面中心时的精度。画面中心附近光轴接近垂直姿态角的小误差对投影点位置影响较小画面边缘同样的姿态角误差会造成更大的地面偏移。这也是为什么拍摄目标时我总建议把目标尽量放在画面中央再拍照。4.2 镜头畸变要不要处理什么时候必须处理消费级无人机为了小型化镜头都带有一定程度的桶形畸变尤其是广角端。这种畸变在画面边缘尤其明显会让目标点的实际成像位置发生偏移。如果你用大疆自带相机直出的JPEG厂家其实在相机内部已经做了一定程度的畸变校正边缘残余量通常在1到2个像素量级这在100米高度上对应几厘米对大多数应用不构成问题。但如果你用的是导出RAW再自己处理或者使用了其他第三方相机畸变校正就不能忽略。处理方式是先用棋盘格标定出相机畸变系数再用OpenCV的undistortPoints函数把像素坐标校正到理想针孔模型。这一步做下来边缘精度能明显改善。我之前在某款微单配合无人机云台的方案上就是靠这一步把总体误差从米级压到了半米级内。4.3 实测校准如何验证你的代码算得准代码写出来最忐忑的就是结果到底准不准。我常用的验证方案是找一个地面有明显特征点的区域比如标准足球场、带斑马线的道路口或者自己用石灰在地上画几个十字标记然后用RTK设备或手机差分GPS测出这些标记点的实际坐标。无人机飞上去照片一拍代码一跑跑出来的坐标和实测坐标直接对比。对比时建议算一下每个点的误差矢量而不是只看距离。比如5个测试点都偏向东南方向偏1米那往往说明某个系统误差没有补偿到位比如镜头光心偏移、姿态角常值偏差、相机安装角偏差等。如果误差方向随机那多半是GPS噪声主导这种通过多帧平均就能缓解。下表是我在某次测试中记录的误差分布条件Mavic 3100米高度正下视目标位于画面中心附近。测试点计算坐标偏移量米方向10.8东偏南21.2北偏西30.5东41.5南偏东50.9西北这组数据说明主要的误差来源是随机噪声通过取多次解算的平均值最终定位误差可以稳定在1米上下。对绝大多数巡检、搜救类需求来说这个精度已经完全够用了。5. 常见问题与避坑指南5.1 我遇到过的五个典型问题第一个坑是偏航角定义不一致。大疆飞控的偏航角以正北为0度顺时针为正和地理方位角一致。但某些开源自研飞控用的却是逆时针为正或者以机体坐标系X轴为0度。数据源一变坐标就偏得离谱。我的解决办法是在代码里做一层数据适配把不同源的姿态数据先统一成一个约定再进入核心算法。第二个坑是图像坐标原点。像素坐标的原点在左上角很多人习惯用数学坐标系直接算结果图像右侧和下方偏移符号全反了。建议每次拿到一张新图先在图上随便点一个目标用代码算一遍预期的偏移方向和图上实际位置对比一下确认无误再批量处理。第三个坑是镜头的实际焦距并不是标称值。厂家给的焦距是标称值制造公差会让实际焦距有1%到3%的偏差这在长焦上更明显。做高精度测量前建议先做一次焦距标定简单方法是飞一段已知长度的线段换算实际焦距。第四个坑是照片里GPS时间与实际拍摄时刻不对齐。无人机航拍时GPS位置是持续更新的但快门按下的瞬间照片记录的GPS是飞控当时的解算值与真正曝光时刻之间有几毫秒到几十毫秒的延迟。在快速飞行状态下这个延迟会引入明显的位置偏差。所以无人机一定要设置为开始悬停或低速移动后再拍照。第五个坑是地面有起伏时不适用平面地面假设。代码默认目标地面是一个平面但实际地形可能有几米高差。处理山地、台阶、楼房区域时必须引入DEM或DSM高程数据。这也是我曾多次在项目中被地形打脸的地方——看起来算法没问题跑到山地实测就发现坐标整体偏移到山坡上去了。5.2 进阶思路当云台不垂直时怎么算有些任务需要斜拍目标比如勘测陡峭山体或建筑立面时。正下视模型会失准这时就得用完整的光线求交法先把目标点的像素坐标转成相机坐标系下的视线方向向量再通过云台姿态和机头姿态把视线向量旋转到世界坐标系再从无人机位置出发沿该方向与地平面求交。原理上和正下视一样只是多了两级旋转矩阵的运算。两级旋转分别是云台相对机身的旋转矩阵、机身相对地面的旋转矩阵。第一级用云台姿态角第二级用飞控姿态角。旋转矩阵的构造可以查欧拉角转矩阵公式也可以用scipy的Rotation类直接生成。我自己用Rotation.from_euler比较多因为角度顺序可显式指定不容易踩坑。5.3 从单点推演到整个项目如果只是算一两个点手动输入参数完全够用。但当你手上拿着几千张照片时就会发现单点计算的思路可以扩展成一套完整的作业流程读照片元数据、批量解算、把结果落到带地理坐标的图片或KML文件里再用GIS工具叠加显示。这一套流程我在后续项目里已经按这个思路成型了目前稳定性很好。过程中我还有个习惯把每一次运行的输入参数、照片ID、输出坐标都记录下来方便事后回溯和精度分析。数据不落地出了问题就没法定位。这些看起来不起眼的操作反而比算法本身更能提升最终交付质量。做航测坐标计算这件事我最大的体会是——算法模型并不复杂真正决定成败的是对传感器误差和坐标约定的理解深度。每次踩坑后回到物理本质去分析比盲目调参要有效得多。这套代码和数据准备的思路希望能帮你少走一些我当年走过的弯路。
返回列表