GCP with OpenSfM, refactoring

pull/997/head
Piero Toffanin 2019-06-20 15:39:49 -04:00
rodzic a15256943d
commit 7a8502f585
11 zmienionych plików z 195 dodań i 138 usunięć

Wyświetl plik

@ -1,12 +1,15 @@
import glob import glob
import os import os
from opendm import log from opendm import log
from opendm import location
from pyproj import CRS
class GCPFile: class GCPFile:
def __init__(self, gcp_path): def __init__(self, gcp_path):
self.gcp_path = gcp_path self.gcp_path = gcp_path
self.entries = [] self.entries = []
self.srs = "" self.raw_srs = ""
self.srs = None
self.read() self.read()
def read(self): def read(self):
@ -16,7 +19,8 @@ class GCPFile:
lines = map(str.strip, contents.split('\n')) lines = map(str.strip, contents.split('\n'))
if lines: if lines:
self.srs = lines[0] # SRS self.raw_srs = lines[0] # SRS
self.srs = location.parse_srs_header(self.raw_srs)
for line in lines[1:]: for line in lines[1:]:
if line != "" and line[0] != "#": if line != "" and line[0] != "#":
@ -47,6 +51,18 @@ class GCPFile:
def exists(self): def exists(self):
return self.gcp_path and os.path.exists(self.gcp_path) return self.gcp_path and os.path.exists(self.gcp_path)
def wgs84_utm_zone(self):
"""
Finds the UTM zone where the first point of the GCP falls into
:return utm zone string valid for a coordinates header
"""
if len(self.entries) > 0:
point = list(self.entries_dict())[0]
longlat = CRS.from_epsg("4326")
lat, lon = location.transform(self.srs, longlat, point['x'], point['y'])
utm_zone, hemisphere = location.get_utm_zone_and_hemisphere_from(lon, lat)
return "WGS84 UTM %s%s" % (utm_zone, hemisphere)
def make_filtered_copy(self, gcp_file_output, images_dir, min_images=3): def make_filtered_copy(self, gcp_file_output, images_dir, min_images=3):
""" """
Creates a new GCP file from an existing GCP file includes Creates a new GCP file from an existing GCP file includes
@ -62,7 +78,7 @@ class GCPFile:
files = map(os.path.basename, glob.glob(os.path.join(images_dir, "*"))) files = map(os.path.basename, glob.glob(os.path.join(images_dir, "*")))
output = [self.srs] output = [self.raw_srs]
files_found = 0 files_found = 0
for entry in self.entries_dict(): for entry in self.entries_dict():

Wyświetl plik

@ -1,6 +1,6 @@
import math import math
from opendm import log from opendm import log
from pyproj import Proj from pyproj import Proj, Transformer, CRS
def extract_utm_coords(photos, images_path, output_coords_file): def extract_utm_coords(photos, images_path, output_coords_file):
""" """
@ -54,6 +54,9 @@ def extract_utm_coords(photos, images_path, output_coords_file):
for coord in coords: for coord in coords:
f.write("%s %s %s\n" % (coord[0] - dx, coord[1] - dy, coord[2])) f.write("%s %s %s\n" % (coord[0] - dx, coord[1] - dy, coord[2]))
def transform(from_srs, to_srs, x, y):
transformer = Transformer.from_crs(from_srs, to_srs)
return transformer.transform(x, y)
def get_utm_zone_and_hemisphere_from(lon, lat): def get_utm_zone_and_hemisphere_from(lon, lat):
""" """
@ -84,3 +87,45 @@ def convert_to_utm(lon, lat, alt, utm_zone, hemisphere):
x,y = p(lon, lat) x,y = p(lon, lat)
return [x, y, alt] return [x, y, alt]
def parse_srs_header(header):
"""
Parse a header coming from GCP or coordinate file
:param header (str) line
:return Proj object
"""
log.ODM_DEBUG('Parsing SRS header: %s' % header)
header = header.strip()
ref = header.split(' ')
try:
if ref[0] == 'WGS84' and ref[1] == 'UTM':
datum = ref[0]
utm_pole = (ref[2][len(ref[2]) - 1]).upper()
utm_zone = int(ref[2][:len(ref[2]) - 1])
proj_args = {
'zone': utm_zone,
'datum': datum
}
proj4 = '+proj=utm +zone={zone} +datum={datum} +no_defs=True'
if utm_pole == 'S':
proj4 += ' +south=True'
srs = CRS.from_proj4(proj4.format(**proj_args))
elif '+proj' in header:
srs = CRS.from_proj4(header.strip('\''))
elif header.lower().startswith("epsg:"):
srs = CRS.from_epsg(header.lower()[5:])
else:
log.ODM_ERROR('Could not parse coordinates. Bad SRS supplied: %s' % header)
except RuntimeError as e:
log.ODM_ERROR('Uh oh! There seems to be a problem with your coordinates/GCP file.\n\n'
'The line: %s\n\n'
'Is not valid. Projections that are valid include:\n'
' - EPSG:*****\n'
' - WGS84 UTM **(N|S)\n'
' - Any valid proj4 string (for example, +proj=utm +zone=32 +north +ellps=WGS84 +datum=WGS84 +units=m +no_defs)\n\n'
'Modify your input and try again.' % header)
raise RuntimeError(e)
return srs

Wyświetl plik

@ -5,7 +5,9 @@ import os
from fractions import Fraction from fractions import Fraction
from opensfm.exif import sensor_string from opensfm.exif import sensor_string
from opendm import get_image_size from opendm import get_image_size
from pyproj import Proj from opendm import location
from opendm.gcp import GCPFile
from pyproj import CRS
import log import log
import io import io
@ -87,121 +89,103 @@ class ODM_Photo:
class ODM_Reconstruction(object): class ODM_Reconstruction(object):
"""docstring for ODMReconstruction""" def __init__(self, photos):
self.photos = photos
def __init__(self, photos, projstring = None, coords_file = None):
self.photos = photos # list of ODM_Photos
self.projection = None # Projection system the whole project will be in
self.georef = None self.georef = None
if projstring:
self.projection = self.set_projection(projstring)
self.georef = ODM_GeoRef(self.projection)
else:
self.projection = self.parse_coordinate_system(coords_file)
if self.projection:
self.georef = ODM_GeoRef(self.projection)
def parse_coordinate_system(self, _file): def is_georeferenced(self):
"""Write attributes to jobOptions from coord file""" return self.georef is not None
def georeference_with_gcp(self, gcp_file, output_coords_file, reload_coords=False):
if not io.file_exists(output_coords_file) or reload_coords:
gcp = GCPFile(gcp_file)
if gcp.exists():
# Create coords file
with open(output_coords_file, 'w') as f:
coords_header = gcp.wgs84_utm_zone()
f.write(coords_header + "\n")
log.ODM_DEBUG("Generated coords file from GCP: %s" % coords_header)
else:
log.ODM_WARNING("GCP file does not exist: %s" % gcp_file)
return
else:
log.ODM_INFO("Coordinates file already exist: %s" % output_coords_file)
self.georef = ODM_GeoRef.FromCoordsFile(output_coords_file)
return self.georef
def georeference_with_gps(self, images_path, output_coords_file, reload_coords=False):
try:
if not io.file_exists(output_coords_file) or reload_coords:
location.extract_utm_coords(photos, tree.dataset_raw, output_coords_file)
else:
log.ODM_INFO("Coordinates file already exist: %s" % output_coords_file)
self.georef = ODM_GeoRef.FromCoordsFile(output_coords_file)
except:
log.ODM_WARNING('Could not generate coordinates file. An orthophoto will not be generated.')
return self.georef
def save_proj_srs(self, file):
# Save proj to file for future use (unless this
# dataset is not georeferenced)
if self.is_georeferenced():
with open(file, 'w') as f:
f.write(self.georef.proj4())
class ODM_GeoRef(object):
@staticmethod
def FromProj(projstring):
return ODM_GeoRef(CRS.from_proj4(projstring))
@staticmethod
def FromCoordsFile(coords_file):
# check for coordinate file existence # check for coordinate file existence
if not io.file_exists(_file): if not io.file_exists(coords_file):
log.ODM_WARNING('Could not find file %s' % _file) log.ODM_WARNING('Could not find file %s' % coords_file)
return return
with open(_file) as f: srs = None
with open(coords_file) as f:
# extract reference system and utm zone from first line. # extract reference system and utm zone from first line.
# We will assume the following format: # We will assume the following format:
# 'WGS84 UTM 17N' or 'WGS84 UTM 17N \n' # 'WGS84 UTM 17N' or 'WGS84 UTM 17N \n'
line = f.readline().rstrip() line = f.readline().rstrip()
log.ODM_DEBUG('Line: %s' % line) srs = location.parse_srs_header(line)
ref = line.split(' ')
# match_wgs_utm = re.search('WGS84 UTM (\d{1,2})(N|S)', line, re.I)
try:
if ref[0] == 'WGS84' and ref[1] == 'UTM': # match_wgs_utm:
datum = ref[0]
utm_pole = (ref[2][len(ref[2]) - 1]).upper()
utm_zone = int(ref[2][:len(ref[2]) - 1])
proj_args = { return ODM_GeoRef(srs)
'proj': "utm",
'zone': utm_zone,
'datum': datum,
'no_defs': True
}
if utm_pole == 'S':
proj_args['south'] = True
return Proj(**proj_args) def __init__(self, srs):
elif '+proj' in line: self.srs = srs
return Proj(line.strip('\''))
elif 'epsg' in line.lower():
return Proj(init=line)
else:
log.ODM_ERROR('Could not parse coordinates. Bad CRS supplied: %s' % line)
except RuntimeError as e:
log.ODM_ERROR('Uh oh! There seems to be a problem with your GCP file.\n\n'
'The line: %s\n\n'
'Is not valid. Projections that are valid include:\n'
' - EPSG:*****\n'
' - WGS84 UTM **(N|S)\n'
' - Any valid proj4 string (for example, +proj=utm +zone=32 +north +ellps=WGS84 +datum=WGS84 +units=m +no_defs)\n\n'
'Modify your GCP file and try again.' % line)
raise RuntimeError(e)
def set_projection(self, projstring):
try:
return Proj(projstring)
except RuntimeError:
log.ODM_EXCEPTION('Could not set projection. Please use a proj4 string')
class ODM_GeoRef(object):
"""docstring for ODMUtmZone"""
def __init__(self, projection):
self.projection = projection
self.epsg = None
self.utm_east_offset = 0 self.utm_east_offset = 0
self.utm_north_offset = 0 self.utm_north_offset = 0
self.transform = [] self.transform = []
self.gcps = []
def coord_to_fractions(self, coord, refs): def proj4(self):
deg_dec = abs(float(coord)) return self.srs.to_proj4()
deg = int(deg_dec)
minute_dec = (deg_dec - deg) * 60 def valid_utm_offsets(self):
minute = int(minute_dec) return self.utm_east_offset and self.utm_north_offset
sec_dec = (minute_dec - minute) * 60 def extract_offsets(self, geo_sys_file):
sec_dec = round(sec_dec, 3) if not io.file_exists(geo_sys_file):
sec_denominator = 1000 log.ODM_ERROR('Could not find file %s' % geo_sys_file)
sec_numerator = int(sec_dec * sec_denominator)
if float(coord) >= 0:
latRef = refs[0]
else:
latRef = refs[1]
output = str(deg) + '/1 ' + str(minute) + '/1 ' + str(sec_numerator) + '/' + str(sec_denominator)
return output, latRef
def extract_offsets(self, _file):
if not io.file_exists(_file):
log.ODM_ERROR('Could not find file %s' % _file)
return return
with open(_file) as f: with open(geo_sys_file) as f:
offsets = f.readlines()[1].split(' ') offsets = f.readlines()[1].split(' ')
self.utm_east_offset = float(offsets[0]) self.utm_east_offset = float(offsets[0])
self.utm_north_offset = float(offsets[1]) self.utm_north_offset = float(offsets[1])
def parse_transformation_matrix(self, _file): def parse_transformation_matrix(self, matrix_file):
if not io.file_exists(_file): if not io.file_exists(matrix_file):
log.ODM_ERROR('Could not find file %s' % _file) log.ODM_ERROR('Could not find file %s' % matrix_file)
return return
# Create a nested list for the transformation matrix # Create a nested list for the transformation matrix
with open(_file) as f: with open(matrix_file) as f:
for line in f: for line in f:
# Handle matrix formats that either # Handle matrix formats that either
# have leading or trailing brakets or just plain numbers. # have leading or trailing brakets or just plain numbers.

Wyświetl plik

@ -6,7 +6,6 @@ from opendm import io
from opendm import types from opendm import types
from opendm import log from opendm import log
from opendm import system from opendm import system
from opendm import location
from shutil import copyfile from shutil import copyfile
from opendm import progress from opendm import progress
@ -100,27 +99,13 @@ class ODMLoadDatasetStage(types.ODM_Stage):
log.ODM_INFO('Found %s usable images' % len(photos)) log.ODM_INFO('Found %s usable images' % len(photos))
# append photos to cell output # Create reconstruction object
if not self.params.get('proj'): reconstruction = types.ODM_Reconstruction(photos)
if tree.odm_georeferencing_gcp:
outputs['reconstruction'] = types.ODM_Reconstruction(photos, coords_file=tree.odm_georeferencing_gcp)
else:
# Generate UTM from images
try:
if not io.file_exists(tree.odm_georeferencing_coords) or self.rerun():
location.extract_utm_coords(photos, tree.dataset_raw, tree.odm_georeferencing_coords)
else:
log.ODM_INFO("Coordinates file already exist: %s" % tree.odm_georeferencing_coords)
except:
log.ODM_WARNING('Could not generate coordinates file. '
'Ignore if there is a GCP file')
outputs['reconstruction'] = types.ODM_Reconstruction(photos, coords_file=tree.odm_georeferencing_coords) if tree.odm_georeferencing_gcp:
reconstruction.georeference_with_gcp(tree.odm_georeferencing_gcp, tree.odm_georeferencing_coords)
else: else:
outputs['reconstruction'] = types.ODM_Reconstruction(photos, projstring=self.params.get('proj')) reconstruction.georeference_with_gps(tree.dataset_raw, tree.odm_georeferencing_coords, reload_coords=self.rerun())
# Save proj to file for future use (unless this reconstruction.save_proj_srs(io.join_paths(tree.odm_georeferencing, tree.odm_georeferencing_proj))
# dataset is not georeferenced) outputs['reconstruction'] = reconstruction
if outputs['reconstruction'].projection:
with open(io.join_paths(tree.odm_georeferencing, tree.odm_georeferencing_proj), 'w') as f:
f.write(outputs['reconstruction'].projection.srs)

Wyświetl plik

@ -20,7 +20,6 @@ class ODMGeoreferencingStage(types.ODM_Stage):
doPointCloudGeo = True doPointCloudGeo = True
transformPointCloud = True transformPointCloud = True
verbose = '-verbose' if self.params.get('verbose') else '' verbose = '-verbose' if self.params.get('verbose') else ''
geo_ref = reconstruction.georef
runs = [{ runs = [{
'georeferencing_dir': tree.odm_georeferencing, 'georeferencing_dir': tree.odm_georeferencing,
@ -73,8 +72,8 @@ class ODMGeoreferencingStage(types.ODM_Stage):
if transformPointCloud: if transformPointCloud:
kwargs['pc_params'] = '-inputPointCloudFile {input_pc_file} -outputPointCloudFile {output_pc_file}'.format(**kwargs) kwargs['pc_params'] = '-inputPointCloudFile {input_pc_file} -outputPointCloudFile {output_pc_file}'.format(**kwargs)
if geo_ref and geo_ref.projection and geo_ref.projection.srs: if reconstruction.is_georeferenced():
kwargs['pc_params'] += ' -outputPointCloudSrs %s' % pipes.quote(geo_ref.projection.srs) kwargs['pc_params'] += ' -outputPointCloudSrs %s' % pipes.quote(reconstruction.georef.proj4())
else: else:
log.ODM_WARNING('NO SRS: The output point cloud will not have a SRS.') log.ODM_WARNING('NO SRS: The output point cloud will not have a SRS.')
else: else:
@ -99,9 +98,7 @@ class ODMGeoreferencingStage(types.ODM_Stage):
doPointCloudGeo = False # skip the rest of the georeferencing doPointCloudGeo = False # skip the rest of the georeferencing
if doPointCloudGeo: if doPointCloudGeo:
# update images metadata reconstruction.georef.extract_offsets(odm_georeferencing_model_txt_geo_file)
geo_ref.extract_offsets(odm_georeferencing_model_txt_geo_file)
reconstruction.georef = geo_ref
# XYZ point cloud output # XYZ point cloud output
if args.pc_csv: if args.pc_csv:

Wyświetl plik

@ -38,19 +38,17 @@ class ODMOrthoPhotoStage(types.ODM_Stage):
# Check if the georef object is initialized # Check if the georef object is initialized
# (during a --rerun this might not be) # (during a --rerun this might not be)
# TODO: we should move this to a more central # TODO: this should be moved to a more central location?
# location (perhaps during the dataset initialization) if reconstruction.is_georeferenced() and not reconstruction.georef.valid_utm_offsets():
if georef and not georef.utm_east_offset:
georeferencing_dir = tree.odm_georeferencing if args.use_3dmesh and not args.skip_3dmodel else tree.odm_25dgeoreferencing georeferencing_dir = tree.odm_georeferencing if args.use_3dmesh and not args.skip_3dmodel else tree.odm_25dgeoreferencing
odm_georeferencing_model_txt_geo_file = os.path.join(georeferencing_dir, tree.odm_georeferencing_model_txt_geo) odm_georeferencing_model_txt_geo_file = os.path.join(georeferencing_dir, tree.odm_georeferencing_model_txt_geo)
if io.file_exists(odm_georeferencing_model_txt_geo_file): if io.file_exists(odm_georeferencing_model_txt_geo_file):
georef.extract_offsets(odm_georeferencing_model_txt_geo_file) reconstruction.georef.extract_offsets(odm_georeferencing_model_txt_geo_file)
else: else:
log.ODM_WARNING('Cannot read UTM offset from {}. An orthophoto will not be generated.'.format(odm_georeferencing_model_txt_geo_file)) log.ODM_WARNING('Cannot read UTM offset from {}. An orthophoto will not be generated.'.format(odm_georeferencing_model_txt_geo_file))
if reconstruction.is_georeferenced():
if georef:
if args.use_3dmesh: if args.use_3dmesh:
kwargs['model_geo'] = os.path.join(tree.odm_texturing, tree.odm_georeferencing_model_obj_geo) kwargs['model_geo'] = os.path.join(tree.odm_texturing, tree.odm_georeferencing_model_obj_geo)
else: else:
@ -69,7 +67,7 @@ class ODMOrthoPhotoStage(types.ODM_Stage):
# Create georeferenced GeoTiff # Create georeferenced GeoTiff
geotiffcreated = False geotiffcreated = False
if georef and georef.projection and georef.utm_east_offset and georef.utm_north_offset: if reconstruction.is_georeferenced() and reconstruction.georef.valid_utm_offsets():
ulx = uly = lrx = lry = 0.0 ulx = uly = lrx = lry = 0.0
with open(tree.odm_orthophoto_corners) as f: with open(tree.odm_orthophoto_corners) as f:
for lineNumber, line in enumerate(f): for lineNumber, line in enumerate(f):
@ -77,13 +75,13 @@ class ODMOrthoPhotoStage(types.ODM_Stage):
tokens = line.split(' ') tokens = line.split(' ')
if len(tokens) == 4: if len(tokens) == 4:
ulx = float(tokens[0]) + \ ulx = float(tokens[0]) + \
float(georef.utm_east_offset) float(reconstruction.georef.utm_east_offset)
lry = float(tokens[1]) + \ lry = float(tokens[1]) + \
float(georef.utm_north_offset) float(reconstruction.georef.utm_north_offset)
lrx = float(tokens[2]) + \ lrx = float(tokens[2]) + \
float(georef.utm_east_offset) float(reconstruction.georef.utm_east_offset)
uly = float(tokens[3]) + \ uly = float(tokens[3]) + \
float(georef.utm_north_offset) float(reconstruction.georef.utm_north_offset)
log.ODM_INFO('Creating GeoTIFF') log.ODM_INFO('Creating GeoTIFF')
orthophoto_vars = orthophoto.get_orthophoto_vars(args) orthophoto_vars = orthophoto.get_orthophoto_vars(args)
@ -94,7 +92,7 @@ class ODMOrthoPhotoStage(types.ODM_Stage):
'lrx': lrx, 'lrx': lrx,
'lry': lry, 'lry': lry,
'vars': ' '.join(['-co %s=%s' % (k, orthophoto_vars[k]) for k in orthophoto_vars]), 'vars': ' '.join(['-co %s=%s' % (k, orthophoto_vars[k]) for k in orthophoto_vars]),
'proj': georef.projection.srs, 'proj': reconstruction.georef.proj4(),
'png': tree.odm_orthophoto_file, 'png': tree.odm_orthophoto_file,
'tiff': tree.odm_orthophoto_tif, 'tiff': tree.odm_orthophoto_tif,
'log': tree.odm_orthophoto_tif_log, 'log': tree.odm_orthophoto_tif_log,

Wyświetl plik

@ -84,7 +84,7 @@ class ODMOpenSfMStage(types.ODM_Stage):
self.update_progress(90) self.update_progress(90)
if reconstruction.georef and (not io.file_exists(tree.opensfm_transformation) or self.rerun()): if reconstruction.is_georeferenced() and (not io.file_exists(tree.opensfm_transformation) or self.rerun()):
octx.run('export_geocoords --transformation --proj \'%s\'' % reconstruction.georef.projection.srs) octx.run('export_geocoords --transformation --proj \'%s\'' % reconstruction.georef.proj4())
else: else:
log.ODM_WARNING("Will skip exporting %s" % tree.opensfm_transformation) log.ODM_WARNING("Will skip exporting %s" % tree.opensfm_transformation)

Wyświetl plik

@ -1 +1,5 @@
python -m unittest discover tests "test_*.py" if [ ! -z "$1" ]; then
python -m unittest discover tests "test_$1.py"
else
python -m unittest discover tests "test_*.py"
fi

Wyświetl plik

@ -0,0 +1,3 @@
EPSG:4326
44.701151842605441 -85.61322729531112 150.5 1331 350 DJI_0002.JPG
44.701023803183617 -85.612848642726888 151.5 1331 350 DJI_0003.JPG

Wyświetl plik

@ -0,0 +1,4 @@
WGS84 UTM 16N
609925.8180680283 4950688.771811823 171.662982 1331 350 DJI_0002.JPG
609925.8180680283 4950688.771811823 171.662982 2028 87 DJI_0003.JPG
609925.8180680283 4950688.771811823 171.662982 2101 1181 DJI_0004.JPG

21
tests/test_gcp.py 100644
Wyświetl plik

@ -0,0 +1,21 @@
import time
import unittest
import os
from opendm.gcp import GCPFile
class TestRemote(unittest.TestCase):
def setUp(self):
pass
def test_utm_north(self):
gcp = GCPFile("tests/assets/gcp_utm_north_valid.txt")
self.assertTrue(gcp.exists())
self.assertEqual(gcp.wgs84_utm_zone(), "WGS84 UTM 16N")
def test_epsg(self):
gcp = GCPFile("tests/assets/gcp_epsg_valid.txt")
self.assertTrue(gcp.exists())
self.assertEqual(gcp.wgs84_utm_zone(), "WGS84 UTM 16N")
if __name__ == '__main__':
unittest.main()