技术研究之无人机图片定位

标签: 无 分类: 未分类 创建时间:2026-01-08 07:09:01 更新时间:2026-05-27 11:42:29

1.前言

这里的目的就是,如何确定一个无人机照片中某个物体的地址位置。这就需要进行坐标变换了,将像素坐标,转为无人机镜头坐标,再转为世界坐标,这样就可以推算出一个图片中的某一个物体到底位于哪里了。

2.代码

位置:经度、纬度、高度
姿态:偏航角(yaw)、俯仰角(pitch)、横滚角(roll)
相机标定参数:内参矩阵、畸变系数

(1) 安装依赖

1
pip install scipy

(2) 测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import cv2
import numpy as np
from scipy.spatial.transform import Rotation as R

class PreciseGeoCalculator:
def __init__(self, camera_matrix, dist_coeffs):
self.camera_matrix = camera_matrix
self.dist_coeffs = dist_coeffs

def pixel_to_geo(self, pixel_x, pixel_y, drone_pos, drone_attitude):
"""
精确的像素坐标到地理坐标的转换
drone_pos: [longitude, latitude, altitude]
drone_attitude: [yaw, pitch, roll] (度)
"""
# 1. 畸变校正
undistorted_x, undistorted_y = self.undistort_pixel(pixel_x, pixel_y)

# 2. 像素坐标转相机坐标(归一化平面)
x_cam, y_cam = self.pixel_to_camera(undistorted_x, undistorted_y)

# 3. 相机坐标转无人机坐标
x_body, y_body, z_body = self.camera_to_body(x_cam, y_cam, drone_attitude)

# 4. 无人机坐标转世界坐标(ENU坐标系)
x_enu, y_enu, z_enu = self.body_to_enu(x_body, y_body, z_body, drone_attitude)

# 5. ENU坐标转地理坐标
target_lat, target_lon = self.enu_to_geo(
x_enu, y_enu,
drone_pos[1], drone_pos[0] # drone_lat, drone_lon
)

return target_lat, target_lon

def undistort_pixel(self, pixel_x, pixel_y):
"""畸变校正"""
point = np.array([[pixel_x, pixel_y]], dtype=np.float32)
undistorted = cv2.undistortPoints(
point, self.camera_matrix, self.dist_coeffs, P=self.camera_matrix
)
return undistorted[0][0]

def pixel_to_camera(self, x, y):
"""像素坐标转相机坐标系"""
fx, fy = self.camera_matrix[0, 0], self.camera_matrix[1, 1]
cx, cy = self.camera_matrix[0, 2], self.camera_matrix[1, 2]

x_cam = (x - cx) / fx
y_cam = (y - cy) / fy

return x_cam, y_cam

def camera_to_body(self, x_cam, y_cam, attitude):
"""
相机坐标系 → 无人机机体坐标系
考虑相机安装角度(假设相机与机体坐标系对齐)
"""
# 将归一化坐标转换为3D射线(假设深度为1)
z_depth = 1.0 # 单位深度

# 相机坐标系下的3D点
x_cam_3d = x_cam * z_depth
y_cam_3d = y_cam * z_depth
z_cam_3d = z_depth

# 如果相机有安装角度偏移,需要额外变换
# 这里假设相机坐标系与机体坐标系对齐
x_body = x_cam_3d
y_body = y_cam_3d
z_body = z_cam_3d

return x_body, y_body, z_body

def body_to_enu(self, x_body, y_body, z_body, attitude):
"""
机体坐标系 → ENU坐标系 (East-North-Up)
使用完整的旋转矩阵变换
"""
yaw, pitch, roll = np.radians(attitude)

# 构建旋转矩阵(ZYX顺序,即偏航-俯仰-横滚)
# 从机体坐标系到ENU坐标系的变换
cos_yaw, sin_yaw = np.cos(yaw), np.sin(yaw)
cos_pitch, sin_pitch = np.cos(pitch), np.sin(pitch)
cos_roll, sin_roll = np.cos(roll), np.sin(roll)

# 旋转矩阵 R_enu_body
R = np.array([
[cos_yaw*cos_pitch, cos_yaw*sin_pitch*sin_roll - sin_yaw*cos_roll, cos_yaw*sin_pitch*cos_roll + sin_yaw*sin_roll],
[sin_yaw*cos_pitch, sin_yaw*sin_pitch*sin_roll + cos_yaw*cos_roll, sin_yaw*sin_pitch*cos_roll - cos_yaw*sin_roll],
[-sin_pitch, cos_pitch*sin_roll, cos_pitch*cos_roll]
])

# 坐标变换
body_vec = np.array([x_body, y_body, z_body])
enu_vec = R @ body_vec

return enu_vec[0], enu_vec[1], enu_vec[2]

def enu_to_geo(self, x_enu, y_enu, drone_lat, drone_lon):
"""
ENU坐标转换为地理坐标
"""
# 地球半径(米)
R_earth = 6378137.0

# 转换为经纬度偏移
lat_offset = y_enu / (R_earth * np.pi / 180.0)
lon_offset = x_enu / (R_earth * np.pi / 180.0 * np.cos(np.radians(drone_lat)))

target_lat = drone_lat + lat_offset
target_lon = drone_lon + lon_offset

return target_lat, target_lon

# 使用示例
if __name__ == "__main__":
calibration_data = np.load('camera_calibration.npz')
camera_matrix = calibration_data['camera_matrix']
dist_coeffs = calibration_data['dist_coeffs']
# 使用示例
calculator = PreciseGeoCalculator(camera_matrix, dist_coeffs)

# 无人机6自由度信息
drone_position = [116.4074, 39.9042, 100.0] # [经度, 纬度, 高度]
drone_attitude = [45.0, -10.0, 2.0] # [偏航角, 俯仰角, 横滚角] (度)

# 目标在图像中的像素坐标
target_pixel = (960, 540) # 图像中心

# 计算精确的地理坐标
target_lat, target_lon = calculator.pixel_to_geo(
target_pixel[0], target_pixel[1],
drone_position,
drone_attitude
)

print(f"目标精确位置: 纬度 {target_lat:.8f}, 经度 {target_lon:.8f}")

5.多帧融合

借鉴于空三解算中的共线方程的算法。

参考文章:
【1】.三角测量计算点的三维坐标 在双目视觉中,分左右相机,同时可以获得左视图和右视图,根据匹配的同名点和左右相机的投影矩阵,就可以计算出匹配同名点的世界坐标。
【2】.DJI无人机影像地理坐标系校正 代码的主要功能是将无人机拍摄的图像中的每个像素位置转换为地理坐标(经纬度),并计算图像的实际物理尺寸。
【3】.坐标转换:从图像坐标到世界坐标的旅程
【4】.基于无人机正摄JPG图像实现像素GPS定位
【5】.无人机影像的像素坐标转大地坐标
【6】.无人机像素坐标转地理坐标——实操(附数据+代码) 读取照片,获取地物点的像素坐标
【7】.【Python&RS】基于GDAL给无人机图片定义坐标系 有了这个想法后就要行动起来,定义一个坐标系,一般需要两个点。一是需要投影坐标系的参数,二就是仿射地理变换参数。投影坐标系非常容易得到,投影坐标系的参数GDAL有内置的一部分,当然也可以读取已有数据的坐标系。但仿射地理变换参数就比较麻烦了,我印象中定义仿射地理变换参数时只能通过图片左上角的点就是设置,不像ENVI可以任意选点。ENVI方便很多,有兴趣可以查看【RS】基于ENVI给图片/影像定义坐标系。所以我们只要能解决仿射地理变换参数就可以解决图片定义坐标系的问题。
【8】.在给定像素在照片中的像素坐标和相机校准值的情况下,是否可以计算像素的世界坐标?
【9】.大疆智图重建任务导出常见问题 由于策略改变,大疆智图 V3.0.0版本三维重建的无畸变影像 undistort 文件夹存放在 temp 文件夹里,该文件夹存储计算过程文件,导出任务压缩包时 temp 文件不会跟随导出;如果您需要使用无畸变影像,直接拷贝备份 undistort 文件夹即可;
【10】.C++无人机摄影测量——OMEGA、Phi、kappa转换为YAW,Pitch,Roll

6.自由网平差

从约束网平差的结果文件(如你之前的ba_calib_params.json)中提取以下关键参数,这些是 “通用模型” 的核心,无需重复平差:
参数类型 具体内容 来源 作用
相机内参 fx/fy/cx/cy/k1/k2/k3/p1/p2 约束网平差的camera字段 像素畸变校正、投影计算
系统校准参数 姿态偏移(yaw/pitch/roll_offset)、安装角(mount_yaw/pitch/roll) 约束网平差的system_errors字段 修正新照片的姿态角,消除系统误差
测区基准 ENU 参考原点(ref_lat/ref_lon/ref_alt)、缩放因子(scale=cos (ref_lat)*R_earth) 约束网平差的 ENU 转换参数 将计算出的 ENU 坐标转为大地经纬度
全局偏移 global_dx/global_dy/global_dz 约束网平差的global_offset字段 修正新照片的位置坐标,匹配大地基准

(1)相机模型可以在空三报告中得到
(2)系统校准,主要通过 BlocksExchangeUndistortAT.xml 中的 Rotation 以及原始图片中 姿态教得到,相机安装角误差、姿态测量误差
(3)测区基准,通过 sfm_geo_desc.json 文件得到
(4)全家偏移量,主要通过 BlocksExchangeUndistortAT.xml 中的 Center 以及原始图片中保存的 pos 信息进行系统校准得到。

这是豆包给的思路,看起来没有问题,一切都没有问题,但是我用了很多时间,去思考和尝试这个东西,最后我发现,这事不能成功的,因为无法通过一次平差,获取一个统一的公式,如果可以的话,那就不用每次都进行空三解算平差了。

参考文章:
【1】.Omega (X)-Phi (Y)-Kappa (Z)
【2】. 基于BA的改进视觉/惯性融合定位算法 这是一篇基于 光束平差法(bundle adjustment)进行 局部优化的计算目标位置的方法。
【3】.一种基于深度图像的实时三维重建方法及系统
【4】.局部BA优化
小额赞助
本人提供免费与付费咨询服务,感谢您的支持!赞助请发邮件(ititchuan@gmail.com)通知,方便公布您的善意!
**光 3.01 元
Sun 3.00 元
ititchuan 3.00 元
微信公众号
广告位
诚心邀请广大金主爸爸洽谈合作
每日一省
isNaN 和 Number.isNaN 函数的区别?

1.函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。

2.函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

每日二省
为什么0.1+0.2 ! == 0.3,如何让其相等?

一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3。

每日三省
== 操作符的强制类型转换规则?

1.首先会判断两者类型是否**相同,**相同的话就比较两者的大小。

2.类型不相同的话,就会进行类型转换。

3.会先判断是否在对比 null 和 undefined,是的话就会返回 true。

4.判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number。

5.判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断。

6.判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断。

每日英语
Happiness is time precipitation, smile is the lonely sad.
幸福是年华的沉淀,微笑是寂寞的悲伤。