alexhagiopol / orthomosaic

Rectify and stitch images together using multiview geometry.
95 stars 37 forks source link

Example dataset link broken #4

Open islandmonkey opened 5 years ago

islandmonkey commented 5 years ago

@alexhagiopol,

unfortunately your example dataset link is broken. Is there any chance you could upload it again?

Cheers

cmbasnett commented 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:

Screen Shot 2019-07-04 at 11 19 44 AM

OpenCV4

I also had to do a few tweaks to get the script working with OpenCV4:

cv.SURF is deprecated

The 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 deprecated

The 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).

alexhagiopol commented 5 years ago

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.

selva221724 commented 4 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:

Screen Shot 2019-07-04 at 11 19 44 AM

OpenCV4

I also had to do a few tweaks to get the script working with OpenCV4:

cv.SURF is deprecated

The 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 deprecated

The 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

cmbasnett commented 4 years ago

@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.

Sidx369 commented 3 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:

Screen Shot 2019-07-04 at 11 19 44 AM

OpenCV4

I also had to do a few tweaks to get the script working with OpenCV4:

cv.SURF is deprecated

The 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 deprecated

The 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?

islandmonkey commented 3 years ago

@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.

Sidx369 commented 3 years ago

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'

atharva-ak commented 3 years ago

@Sidx369 use marker, body = content.split(b'\x00',1) for the error, b'\x00' this refers '\x00' as bytes-like object.

AshutoshStark commented 10 months ago

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?