前段时间有过一个想法,就是如果可以给无人机拍摄的图片定义坐标系,再使用GADL库里的镶嵌拼接函数,是不是就可以实现快速拼接影像。虽然结果不是正射影像,但效率比无人机厂家的软件提高了很多很多,主要还是看用途。
有了这个想法后就要行动起来,定义一个坐标系,一般需要两个点。一是需要投影坐标系的参数,二就是仿射地理变换参数。投影坐标系非常容易得到,投影坐标系的参数GDAL有内置的一部分,当然也可以读取已有数据的坐标系。但仿射地理变换参数就比较麻烦了,我印象中定义仿射地理变换参数时只能通过图片左上角的点就是设置,不像ENVI可以任意选点。ENVI方便很多,有兴趣可以查看【RS】基于ENVI给图片/影像定义坐标系。所以我们只要能解决仿射地理变换参数就可以解决图片定义坐标系的问题。
大致思路是:通过之前计算图片某点的投影坐标的方法,得到图片左上角的投影坐标和偏转角度,以此建立仿射地理变换参数,再通过这些创建新的数据资源将原来的数据写入。
这一步之前的博文已经说过无数次了,这里就不解释。如果你只是娱乐一下,并不需要定义真实的坐标系,那么可以直接跳到最后一步,仿射地理变换参数直接设置0,1即可。
def Get_LatLon(path_image):
"""
:param path_image: 输入图片路径
:return: 返回图片中心点经纬度
"""
# 获取图片的经纬度信息
f = open(path_image, 'rb')
contents = exifread.process_file(f)
longitude = contents["GPS GPSLongitude"].values
longitude_f = longitude[0].num/longitude[0].den + (longitude[1].num/longitude[1].den/60) + (longitude[2].num/longitude[2].den/3600)
latitude = contents["GPS GPSLatitude"].values
latitude_f = latitude[0].num/latitude[0].den + (latitude[1].num/latitude[1].den/60) + (latitude[2].num/latitude[2].den/3600)
# print("经度:", longitude_f) # contents['GPS GPSLatitudeRef']
# print("纬度:", latitude_f) # contents['GPS GPSLongitudeRef']
f.close()
return latitude_f, longitude_f
无人机拍摄的影像一般都是GPS经纬度坐标(WGS84),我这里要将他定义为UTM/WGS84 51N,所以需要将它的地理坐标系先转换成投影坐标系。
def LonLat_Meter(lat, lon):
"""
:param lat: 图片中心点纬度
:param lon: 图片中心点经度
:return: 返回UTM投影坐标
"""
source = osr.SpatialReference()
# 初始化osr.SpatialReference对象以形成一个合法的坐标系统
source.ImportFromEPSG(4326)
# 向对象中写入Source_EPSG坐标系统
target = osr.SpatialReference()
target.ImportFromEPSG(32651)
# 这里是用内置的EPSG对应的坐标系作为转换参数
transform = osr.CoordinateTransformation(source, target)
point = ogr.CreateGeometryFromWkt("POINT (%s %s)" % (lat, lon))
# 报错的话,将经纬度翻过来
point.Transform(transform)
# print(point.GetX(), point.GetY())
return point.GetX(), point.GetY()
无人机航拍时不可能一直与正北方向一致,所以还需要获取其成像时的偏转角。这一步是为了计算图片左上角的仿射地理变换参数。我这里使用的是大疆的无人机,其他厂商的无人机成像参数需要自己查阅。
def Get_Image_Yaw_angle(file_path):
"""
:param file_path: 输入图片路径
:return: 图片的偏航角
"""
# 获取图片偏航角
b = b"\x3c\x2f\x72\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e"
a = b"\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20"
img = open(file_path, 'rb')
data = bytearray()
dj_data_dict = {}
flag = False
for line in img.readlines():
if a in line:
flag = True
if flag:
data += line
if b in line:
break
if len(data) > 0:
data = str(data.decode('ascii'))
lines = list(filter(lambda x: 'drone-dji:' in x, data.split("\n")))
for d in lines:
d = d.strip()[10:]
key, value = d.split("=")
dj_data_dict[key] = value
# print("Image_yaw",dj_data_dict["FlightYawDegree"][1:-1])
return float(dj_data_dict["FlightYawDegree"][1:-1])
这一步是获取左上角的投影坐标,以此建立仿射地理变换参数。
def Target_Meter(Image_Yaw_angle, width, height, meter_X, meter_Y, rat):
"""
:param Image_Yaw_angle: 图片与正北的偏转角
:param width: 图片的宽度的一半
:param height: 图片的高度的一半
:param meter_X: 图片中心点的投影坐标
:param meter_Y: 图片中心点的投影坐标
:param rat: 图片分辨率
:return: 返回左上角的投影坐标
"""
if Image_Yaw_angle < 0:
Image_Yaw_angle = 360 + Image_Yaw_angle
yaw_angle = 360 - math.degrees(math.atan(width / height))
# 左上角与图片正北的夹角
sum_angle = Image_Yaw_angle + yaw_angle
# 图片偏转角+左上角偏转角
if sum_angle >= 360:
sum_angle = sum_angle - 360
if sum_angle == 0:
meter_x = meter_X
meter_y = meter_Y - height*rat
elif sum_angle == 90:
meter_x = meter_X + (-width)*rat
meter_y = meter_Y
elif sum_angle == 180:
meter_x = meter_X
meter_y = meter_Y + (-height)*rat
elif sum_angle == 270:
meter_x = meter_X - width*rat
meter_y = meter_Y
elif sum_angle == 360:
meter_x = meter_X
meter_y = meter_Y - height*rat
elif 0 < sum_angle < 90:
meter_x = meter_X + math.sin(math.radians(sum_angle))*math.sqrt(math.pow(-width, 2)+math.pow(-height, 2))*rat
meter_y = meter_Y + math.cos(math.radians(sum_angle))*math.sqrt(math.pow(-width, 2)+math.pow(-height, 2))*rat
elif 90 < sum_angle < 180:
meter_x = meter_X + math.sin(math.radians(180-sum_angle)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
meter_y = meter_Y - math.cos(math.radians(180-sum_angle)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
elif 180 < sum_angle < 270:
meter_x = meter_X - math.sin(math.radians(sum_angle-180)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
meter_y = meter_Y - math.cos(math.radians(sum_angle-180)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
elif 270 < sum_angle < 360:
meter_x = meter_X - math.sin(math.radians(360-sum_angle)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
meter_y = meter_Y + math.cos(math.radians(360-sum_angle)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
return meter_x, meter_y
前面的工作完成后就得到了仿射地理变换参数,就可以定义坐标系了。如果大家只是想拼接手机拍摄的图片,或者娱乐学习,就可以不那么严谨。不需要求出真实的投影坐标,仿射地理变换参数直接设置为(1, 0, 0, 1, 0, 0)即可。关于仿射地理变换参数的含义,大家可以查看这篇博文【Python&RS】遥感影像的像素坐标转地理坐标(仿射变换)。
def Define_Proj(input_path, ds_geo, out_path):
"""
:param input_path: 需要定义投影的图片路径
:param ds_geo: 仿射地理变换参数,元组的形式
:param out_path: 输出路径
:return:
"""
ds = gdal.Open(input_path) # 打开数据集dataset,需要定义投影的图片
ds_width = ds.RasterXSize # 获取数据宽度
ds_height = ds.RasterYSize # 获取数据高度
ds_bands = ds.RasterCount # 获取波段数
driver = gdal.GetDriverByName('GTiff') # 载入数据驱动,用于存储内存中的数组
ds_result = driver.Create(out_path, ds_width, ds_height, bands=ds_bands, eType=gdal.GDT_Float64)
ds_result.SetGeoTransform(ds_geo) # 导入仿射地理变换参数
target = osr.SpatialReference()
target.ImportFromEPSG(32651)
# 输出投影坐标系为WGS84 UTM 51N
ds_result.SetProjection(str(target)) # 导入投影信息
for i in range(1, ds_bands+1):
band_data = ds.GetRasterBand(i).ReadAsArray(0, 0, ds_width, ds_height).astype(np.cfloat)
ds_result.GetRasterBand(i).WriteArray(band_data)
del ds_result
上面的代码都是分步的,直接复制无法运行,因为有一部分我是放在了主函数里面。运行代码要注意两点,一是无人机图片是否有中心点的经纬度,二是是否为大疆的无人机,如果不是第三步的代码就需要自己修改。
# -*- coding: utf-8 -*-
"""
@Time : 2023/5/29 15:28
@Auth : RS迷途小书童
@File :Define Coordinate System For Image.py
@IDE :PyCharm
@Purpose :给图片定义坐标系
"""
import exifread
from osgeo import gdal, ogr, osr
import math
import numpy as np
def Get_LatLon(path_image):
"""
:param path_image: 输入图片路径
:return: 返回图片中心点经纬度
"""
# 获取图片的经纬度信息
f = open(path_image, 'rb')
contents = exifread.process_file(f)
longitude = contents["GPS GPSLongitude"].values
longitude_f = longitude[0].num/longitude[0].den + (longitude[1].num/longitude[1].den/60) + (longitude[2].num/longitude[2].den/3600)
latitude = contents["GPS GPSLatitude"].values
latitude_f = latitude[0].num/latitude[0].den + (latitude[1].num/latitude[1].den/60) + (latitude[2].num/latitude[2].den/3600)
# print("经度:", longitude_f) # contents['GPS GPSLatitudeRef']
# print("纬度:", latitude_f) # contents['GPS GPSLongitudeRef']
f.close()
return latitude_f, longitude_f
def LonLat_Meter(lat, lon):
"""
:param lat: 图片中心点纬度
:param lon: 图片中心点经度
:return: 返回UTM投影坐标
"""
source = osr.SpatialReference()
# 初始化osr.SpatialReference对象以形成一个合法的坐标系统
source.ImportFromEPSG(4326)
# 向对象中写入Source_EPSG坐标系统
target = osr.SpatialReference()
target.ImportFromEPSG(32651)
# 这里是用内置的EPSG对应的坐标系作为转换参数
transform = osr.CoordinateTransformation(source, target)
point = ogr.CreateGeometryFromWkt("POINT (%s %s)" % (lat, lon))
# 报错的话,将经纬度翻过来
point.Transform(transform)
# print(point.GetX(), point.GetY())
return point.GetX(), point.GetY()
def Get_Image_Yaw_angle(file_path):
"""
:param file_path: 输入图片路径
:return: 图片的偏航角
"""
# 获取图片偏航角
b = b"\x3c\x2f\x72\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e"
a = b"\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20"
img = open(file_path, 'rb')
data = bytearray()
dj_data_dict = {}
flag = False
for line in img.readlines():
if a in line:
flag = True
if flag:
data += line
if b in line:
break
if len(data) > 0:
data = str(data.decode('ascii'))
lines = list(filter(lambda x: 'drone-dji:' in x, data.split("\n")))
for d in lines:
d = d.strip()[10:]
key, value = d.split("=")
dj_data_dict[key] = value
# print("Image_yaw",dj_data_dict["FlightYawDegree"][1:-1])
return float(dj_data_dict["FlightYawDegree"][1:-1])
def Target_Meter(Image_Yaw_angle, width, height, meter_X, meter_Y, rat):
"""
:param Image_Yaw_angle: 图片与正北的偏转角
:param width: 图片的宽度的一半
:param height: 图片的高度的一半
:param meter_X: 图片中心点的投影坐标
:param meter_Y: 图片中心点的投影坐标
:param rat: 图片分辨率
:return: 返回左上角的投影坐标
"""
if Image_Yaw_angle < 0:
Image_Yaw_angle = 360 + Image_Yaw_angle
yaw_angle = 360 - math.degrees(math.atan(width / height))
# 左上角与图片正北的夹角
sum_angle = Image_Yaw_angle + yaw_angle
# 图片偏转角+左上角偏转角
if sum_angle >= 360:
sum_angle = sum_angle - 360
if sum_angle == 0:
meter_x = meter_X
meter_y = meter_Y - height*rat
elif sum_angle == 90:
meter_x = meter_X + (-width)*rat
meter_y = meter_Y
elif sum_angle == 180:
meter_x = meter_X
meter_y = meter_Y + (-height)*rat
elif sum_angle == 270:
meter_x = meter_X - width*rat
meter_y = meter_Y
elif sum_angle == 360:
meter_x = meter_X
meter_y = meter_Y - height*rat
elif 0 < sum_angle < 90:
meter_x = meter_X + math.sin(math.radians(sum_angle))*math.sqrt(math.pow(-width, 2)+math.pow(-height, 2))*rat
meter_y = meter_Y + math.cos(math.radians(sum_angle))*math.sqrt(math.pow(-width, 2)+math.pow(-height, 2))*rat
elif 90 < sum_angle < 180:
meter_x = meter_X + math.sin(math.radians(180-sum_angle)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
meter_y = meter_Y - math.cos(math.radians(180-sum_angle)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
elif 180 < sum_angle < 270:
meter_x = meter_X - math.sin(math.radians(sum_angle-180)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
meter_y = meter_Y - math.cos(math.radians(sum_angle-180)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
elif 270 < sum_angle < 360:
meter_x = meter_X - math.sin(math.radians(360-sum_angle)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
meter_y = meter_Y + math.cos(math.radians(360-sum_angle)) * math.sqrt(math.pow(-width, 2) +
math.pow(-height, 2))*rat
return meter_x, meter_y
def Define_Proj(input_path, ds_geo, out_path):
"""
:param input_path: 需要定义投影的图片路径
:param ds_geo: 仿射地理变换参数,元组的形式
:param out_path: 输出路径
:return:
"""
ds = gdal.Open(input_path) # 打开数据集dataset,需要定义投影的图片
ds_width = ds.RasterXSize # 获取数据宽度
ds_height = ds.RasterYSize # 获取数据高度
ds_bands = ds.RasterCount # 获取波段数
driver = gdal.GetDriverByName('GTiff') # 载入数据驱动,用于存储内存中的数组
ds_result = driver.Create(out_path, ds_width, ds_height, bands=ds_bands, eType=gdal.GDT_Float64)
ds_result.SetGeoTransform(ds_geo) # 导入仿射地理变换参数
target = osr.SpatialReference()
target.ImportFromEPSG(32651)
# 输出投影坐标系为WGS84 UTM 51N
ds_result.SetProjection(str(target)) # 导入投影信息
for i in range(1, ds_bands+1):
band_data = ds.GetRasterBand(i).ReadAsArray(0, 0, ds_width, ds_height).astype(np.cfloat)
ds_result.GetRasterBand(i).WriteArray(band_data)
del ds_result
if __name__ == '__main__':
path_image1 = 'G:/A_01_DSC00005.JPG'
out_image = 'G:/try/3.tif'
latitude1, longitude1 = Get_LatLon(path_image1)
# 获取图片中心点的纬度、经度
X, Y = LonLat_Meter(latitude1, longitude1)
x, y = Target_Meter(135, 3000, 2000, X, Y, 0.04)
geo = tuple((x, 0.04, 0, y, 0, 0.04))
print(geo)
Define_Proj(path_image1, geo, out_image)
我这里只是实验一下,所以仿射地理变换参数里的旋转角度没有加进去,位置有一点偏移。总体代码没有问题,但这个逻辑应该还需要改进。大家有好的想法也可以和我一起探讨,后续应该会针对这个问题进行优化。
前端时间去出外业了,所以博文没有更新。后面应该会持续更新的,当然博主也会休息休息,偷偷懒,要一起加油鸭。