Open islandmonkey opened 5 years ago
I have made a quick-and-dirty script that can generate an example data set from a set of DJI drone images (it extracts the EXIF/XMP data to get the XYZYPR values needed).
import os
from bs4 import BeautifulSoup
from PIL import Image, ExifTags
from pymap3d import ecef2enu, geodetic2ecef
import numpy as np
def dms_to_decimal(d, m, s):
return d + (m / 60.0) + (s / 3600.0)
def get_gps_coords(im):
"""
Gets latitude and longitude values from image EXIF data.
:param im:
:return:
"""
exif = im.getexif()
exif_data = dict()
for tag, value in exif.items():
decoded_tag = ExifTags.TAGS.get(tag, tag)
exif_data[decoded_tag] = value
gps_info = exif_data['GPSInfo']
lat_dms = map(lambda x: x[0] / float(x[1]), gps_info[2])
lat = dms_to_decimal(*lat_dms)
if gps_info[1] == 'S':
lat *= -1
lng_dms = map(lambda x: x[0] / float(x[1]), gps_info[4])
lng = dms_to_decimal(*lng_dms)
if gps_info[3] == 'W':
lng *= -1
return lat, lng
def get_data(path):
lat0 = None
lon0 = None
h0 = 0
for root, dirs, files in os.walk(path):
for filename in sorted(filter(lambda x: os.path.splitext(x)[1].lower() == '.jpg', files)):
filepath = os.path.join(root, filename)
with Image.open(filepath) as im:
for segment, content in im.applist:
marker, body = content.split('\x00', 1)
if segment == 'APP1' and marker == 'http://ns.adobe.com/xap/1.0/':
soup = BeautifulSoup(body, features='html.parser')
description = soup.find('x:xmpmeta').find('rdf:rdf').find('rdf:description')
pitch = float(description['drone-dji:gimbalpitchdegree']) + 90
yaw = float(description['drone-dji:gimbalyawdegree'])
roll = float(description['drone-dji:gimbalrolldegree'])
alt = float(description['drone-dji:relativealtitude'])
lat, lon = get_gps_coords(im)
if lat0 is None:
lat0 = lat
lon0 = lon
x, y, z = geodetic2ecef(lat, lon, alt)
x, y, z = ecef2enu(x, y, z, lat0, lon0, h0)
yield filename, '{:f}'.format(x), '{:f}'.format(y), '{:f}'.format(z), yaw, pitch, roll
def main():
data = [d for d in get_data('datasets/images')]
data = sorted(data, key=lambda x: x[0])
x = np.array(map(lambda d: d[1], data))
y = np.array(map(lambda d: d[2], data))
with open('datasets/imageData.txt', 'w+') as f:
for datum in data:
f.write(','.join([str(d) for d in datum]) + '\n')
if __name__ == '__main__':
main()
To get the XYZ values expected by the dataset, the script uses GPS coordinates, converts them from WGS84->ECEF->ENU, with the "origin" of the ENU space being the first image.
If you put images in the datasets/images
folder and run this script, you'll get a file called imageData.txt
written out in a CSV format like this:
DJI_0023.JPG,0.000000,-0.000000,58.600000,56.2,0.0,0.0
DJI_0024.JPG,6.390588,4.473257,58.599995,56.1,0.0,0.0
DJI_0025.JPG,23.154435,15.434010,58.599939,55.5,0.0,0.0
DJI_0026.JPG,26.520979,17.642848,58.599921,55.7,0.1,0.0
An interesting note, the pitch
value treats nadir (straight-down) as 0 degrees pitch, not -90.
Hope this helps some folks getting this working, here's some preliminary results from a few sets of photos:
I also had to do a few tweaks to get the script working with OpenCV4:
cv.SURF
is deprecatedThe call to cv.SURF(500)
was deprecated, and is apparently a patented algorithm that they can't include anymore in modern versions of OpenCV. I simply changed it to use ORB: cv2.ORB_create()
.
estimateRigidTransform
is deprecatedThe new docs lay out that there are two new functions that do the same thing. In this case, we want to replace the call with A, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts)
.
Hi @cmbasnett and @island-monkey . I apologize for neglecting this issue for so long. For some reason, I never got an email notification about problem this until Colin posted his message above.
It's true that the Dropbox account where I stored my example data 4 years ago no longer exists. I will try to locate the data, but it's possible it could be lost forever 😔.
Colin's code looks very useful though I have not had the chance to review in detail, and - without the data - I would not be able to test the results myself. I will post here if I am able to come up with a solution.
I have made a quick-and-dirty script that can generate an example data set from a set of DJI drone images (it extracts the EXIF/XMP data to get the XYZYPR values needed).
import os from bs4 import BeautifulSoup from PIL import Image, ExifTags from pymap3d import ecef2enu, geodetic2ecef import numpy as np def dms_to_decimal(d, m, s): return d + (m / 60.0) + (s / 3600.0) def get_gps_coords(im): """ Gets latitude and longitude values from image EXIF data. :param im: :return: """ exif = im.getexif() exif_data = dict() for tag, value in exif.items(): decoded_tag = ExifTags.TAGS.get(tag, tag) exif_data[decoded_tag] = value gps_info = exif_data['GPSInfo'] lat_dms = map(lambda x: x[0] / float(x[1]), gps_info[2]) lat = dms_to_decimal(*lat_dms) if gps_info[1] == 'S': lat *= -1 lng_dms = map(lambda x: x[0] / float(x[1]), gps_info[4]) lng = dms_to_decimal(*lng_dms) if gps_info[3] == 'W': lng *= -1 return lat, lng def get_data(path): lat0 = None lon0 = None h0 = 0 for root, dirs, files in os.walk(path): for filename in sorted(filter(lambda x: os.path.splitext(x)[1].lower() == '.jpg', files)): filepath = os.path.join(root, filename) with Image.open(filepath) as im: for segment, content in im.applist: marker, body = content.split('\x00', 1) if segment == 'APP1' and marker == 'http://ns.adobe.com/xap/1.0/': soup = BeautifulSoup(body, features='html.parser') description = soup.find('x:xmpmeta').find('rdf:rdf').find('rdf:description') pitch = float(description['drone-dji:gimbalpitchdegree']) + 90 yaw = float(description['drone-dji:gimbalyawdegree']) roll = float(description['drone-dji:gimbalrolldegree']) alt = float(description['drone-dji:relativealtitude']) lat, lon = get_gps_coords(im) if lat0 is None: lat0 = lat lon0 = lon x, y, z = geodetic2ecef(lat, lon, alt) x, y, z = ecef2enu(x, y, z, lat0, lon0, h0) yield filename, '{:f}'.format(x), '{:f}'.format(y), '{:f}'.format(z), yaw, pitch, roll def main(): data = [d for d in get_data('datasets/images')] data = sorted(data, key=lambda x: x[0]) x = np.array(map(lambda d: d[1], data)) y = np.array(map(lambda d: d[2], data)) with open('datasets/imageData.txt', 'w+') as f: for datum in data: f.write(','.join([str(d) for d in datum]) + '\n') if __name__ == '__main__': main()
To get the XYZ values expected by the dataset, the script uses GPS coordinates, converts them from WGS84->ECEF->ENU, with the "origin" of the ENU space being the first image.
If you put images in the
datasets/images
folder and run this script, you'll get a file calledimageData.txt
written out in a CSV format like this:DJI_0023.JPG,0.000000,-0.000000,58.600000,56.2,0.0,0.0 DJI_0024.JPG,6.390588,4.473257,58.599995,56.1,0.0,0.0 DJI_0025.JPG,23.154435,15.434010,58.599939,55.5,0.0,0.0 DJI_0026.JPG,26.520979,17.642848,58.599921,55.7,0.1,0.0
An interesting note, the
pitch
value treats nadir (straight-down) as 0 degrees pitch, not -90.Hope this helps some folks getting this working, here's some preliminary results from a few sets of photos:
OpenCV4
I also had to do a few tweaks to get the script working with OpenCV4:
cv.SURF
is deprecatedThe call to
cv.SURF(500)
was deprecated, and is apparently a patented algorithm that they can't include anymore in modern versions of OpenCV. I simply changed it to use ORB:cv2.ORB_create()
.
estimateRigidTransform
is deprecatedThe new docs lay out that there are two new functions that do the same thing. In this case, we want to replace the call with
A, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts)
.
HI mate, could you please share your Combiner.py script here... i am not able to come to a solution here, thanks
@selva221724 I don't have the script on my machine anymore. The Combiner.py script I used is the same as the one that's in the repository except with the changes I laid out at the bottom of my post.
I have made a quick-and-dirty script that can generate an example data set from a set of DJI drone images (it extracts the EXIF/XMP data to get the XYZYPR values needed).
import os from bs4 import BeautifulSoup from PIL import Image, ExifTags from pymap3d import ecef2enu, geodetic2ecef import numpy as np def dms_to_decimal(d, m, s): return d + (m / 60.0) + (s / 3600.0) def get_gps_coords(im): """ Gets latitude and longitude values from image EXIF data. :param im: :return: """ exif = im.getexif() exif_data = dict() for tag, value in exif.items(): decoded_tag = ExifTags.TAGS.get(tag, tag) exif_data[decoded_tag] = value gps_info = exif_data['GPSInfo'] lat_dms = map(lambda x: x[0] / float(x[1]), gps_info[2]) lat = dms_to_decimal(*lat_dms) if gps_info[1] == 'S': lat *= -1 lng_dms = map(lambda x: x[0] / float(x[1]), gps_info[4]) lng = dms_to_decimal(*lng_dms) if gps_info[3] == 'W': lng *= -1 return lat, lng def get_data(path): lat0 = None lon0 = None h0 = 0 for root, dirs, files in os.walk(path): for filename in sorted(filter(lambda x: os.path.splitext(x)[1].lower() == '.jpg', files)): filepath = os.path.join(root, filename) with Image.open(filepath) as im: for segment, content in im.applist: marker, body = content.split('\x00', 1) if segment == 'APP1' and marker == 'http://ns.adobe.com/xap/1.0/': soup = BeautifulSoup(body, features='html.parser') description = soup.find('x:xmpmeta').find('rdf:rdf').find('rdf:description') pitch = float(description['drone-dji:gimbalpitchdegree']) + 90 yaw = float(description['drone-dji:gimbalyawdegree']) roll = float(description['drone-dji:gimbalrolldegree']) alt = float(description['drone-dji:relativealtitude']) lat, lon = get_gps_coords(im) if lat0 is None: lat0 = lat lon0 = lon x, y, z = geodetic2ecef(lat, lon, alt) x, y, z = ecef2enu(x, y, z, lat0, lon0, h0) yield filename, '{:f}'.format(x), '{:f}'.format(y), '{:f}'.format(z), yaw, pitch, roll def main(): data = [d for d in get_data('datasets/images')] data = sorted(data, key=lambda x: x[0]) x = np.array(map(lambda d: d[1], data)) y = np.array(map(lambda d: d[2], data)) with open('datasets/imageData.txt', 'w+') as f: for datum in data: f.write(','.join([str(d) for d in datum]) + '\n') if __name__ == '__main__': main()
To get the XYZ values expected by the dataset, the script uses GPS coordinates, converts them from WGS84->ECEF->ENU, with the "origin" of the ENU space being the first image.
If you put images in the
datasets/images
folder and run this script, you'll get a file calledimageData.txt
written out in a CSV format like this:DJI_0023.JPG,0.000000,-0.000000,58.600000,56.2,0.0,0.0 DJI_0024.JPG,6.390588,4.473257,58.599995,56.1,0.0,0.0 DJI_0025.JPG,23.154435,15.434010,58.599939,55.5,0.0,0.0 DJI_0026.JPG,26.520979,17.642848,58.599921,55.7,0.1,0.0
An interesting note, the
pitch
value treats nadir (straight-down) as 0 degrees pitch, not -90.Hope this helps some folks getting this working, here's some preliminary results from a few sets of photos:
OpenCV4
I also had to do a few tweaks to get the script working with OpenCV4:
cv.SURF
is deprecatedThe call to
cv.SURF(500)
was deprecated, and is apparently a patented algorithm that they can't include anymore in modern versions of OpenCV. I simply changed it to use ORB:cv2.ORB_create()
.
estimateRigidTransform
is deprecatedThe new docs lay out that there are two new functions that do the same thing. In this case, we want to replace the call with
A, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts)
.
@cmbasnett Can you explain what these numbers separated by comma eg. DJI_0026.JPG,26.520979,17.642848,58.599921,55.7,0.1,0.0 means?
@Sidx369
from the code:
'{:f}'.format(x), '{:f}'.format(y), '{:f}'.format(z), yaw, pitch, roll
latitude, longitude, altitude, yaw, pitch, roll
where, in the case of the DJI Phantom, yaw, pitch and roll are the values of the gimbal. If you don't have a gimbal (i.e. your camera is fixed to the drone frame) just use the yaw, pitch, roll of the drone.
Thanks @islandmonkey,
I have an error on running the above script, can anyone tell me how to correct it?
File "C:\Users\Downloads\Aerial repo\orthomosaic\generate_dataset.py", line 44, in get_data marker, body = content.split('\x00', 1) TypeError: a bytes-like object is required, not 'str'
@Sidx369 use marker, body = content.split(b'\x00',1) for the error, b'\x00' this refers '\x00' as bytes-like object.
the is making the file but it's printing anything can anyone help
code:
import os from bs4 import BeautifulSoup from PIL import Image, ExifTags from pymap3d import ecef2enu, geodetic2ecef import numpy as np
def dms_to_decimal(d, m, s): return d + (m / 60.0) + (s / 3600.0)
def get_gps_coords(im): """ Gets latitude and longitude values from image EXIF data. :param im: :return: """ exif = im.getexif() exif_data = dict() for tag, value in exif.items(): decoded_tag = ExifTags.TAGS.get(tag, tag) exif_data[decoded_tag] = value gps_info = exif_data['GPSInfo'] lat_dms = map(lambda x: x[0] / float(x[1]), gps_info[2]) lat = dms_to_decimal(lat_dms) if gps_info[1] == 'S': lat = -1 lng_dms = map(lambda x: x[0] / float(x[1]), gps_info[4]) lng = dms_to_decimal(lng_dms) if gps_info[3] == 'W': lng = -1 return lat, lng
def get_data(path): lat0 = None lon0 = None h0 = 0 for root, dirs, files in os.walk(path): for filename in sorted(filter(lambda x: os.path.splitext(x)[1].lower() == '.jpg', files)): filepath = os.path.join(root, filename) with Image.open(filepath) as im: for segment, content in im.applist: marker, body = content.split(b'\x00',1) if segment == 'APP1' and marker == 'http://ns.adobe.com/xap/1.0/': soup = BeautifulSoup(body, features='html.parser') description = soup.find('x:xmpmeta').find('rdf:rdf').find('rdf:description') pitch = float(description['drone-dji:gimbalpitchdegree']) + 90 yaw = float(description['drone-dji:gimbalyawdegree']) roll = float(description['drone-dji:gimbalrolldegree']) alt = float(description['drone-dji:relativealtitude']) lat, lon = get_gps_coords(im) if lat0 is None: lat0 = lat lon0 = lon x, y, z = geodetic2ecef(lat, lon, alt) x, y, z = ecef2enu(x, y, z, lat0, lon0, h0) yield filename, '{:f}'.format(x), '{:f}'.format(y), '{:f}'.format(z), yaw, pitch, roll
def main(): data = [d for d in get_data("images")] data = sorted(data, key=lambda x: x[0]) x = np.array(map(lambda d: d[1], data)) y = np.array(map(lambda d: d[2], data)) with open('imageData.txt', 'w+') as f: for datum in data: f.write(','.join([str(d) for d in datum]) + '\n')
if name == 'main': main() print(main())
Question:. the is making the file but it's printing anything can anyone help?
@alexhagiopol,
unfortunately your example dataset link is broken. Is there any chance you could upload it again?
Cheers