2019-04-29 20:18:08 +00:00
import os
2021-02-24 20:11:16 +00:00
import shutil
2020-11-09 03:59:24 +00:00
import warnings
2021-02-24 20:11:16 +00:00
import numpy as np
2019-03-07 17:36:05 +00:00
from opendm import get_image_size
2019-06-20 19:39:49 +00:00
from opendm import location
from opendm . gcp import GCPFile
from pyproj import CRS
2019-12-07 22:05:59 +00:00
import xmltodict as x2d
2020-02-01 19:19:35 +00:00
from six import string_types
2015-11-17 17:17:56 +00:00
2020-09-08 17:08:57 +00:00
from opendm import log
from opendm import io
from opendm import system
from opendm import context
2019-05-15 21:04:09 +00:00
from opendm . progress import progressbc
2020-02-26 21:06:39 +00:00
from opendm . photo import ODM_Photo
2015-11-27 10:00:08 +00:00
2020-11-09 03:59:24 +00:00
# Ignore warnings about proj information being lost
warnings . filterwarnings ( " ignore " )
2019-03-07 17:36:05 +00:00
2015-12-10 17:17:39 +00:00
class ODM_Reconstruction ( object ) :
2019-06-20 19:39:49 +00:00
def __init__ ( self , photos ) :
self . photos = photos
2018-04-19 02:03:54 +00:00
self . georef = None
2019-06-21 18:47:00 +00:00
self . gcp = None
2019-12-05 20:28:06 +00:00
self . multi_camera = self . detect_multi_camera ( )
def detect_multi_camera ( self ) :
"""
Looks at the reconstruction photos and determines if this
is a single or multi - camera setup .
"""
2019-12-13 15:58:37 +00:00
band_photos = { }
band_indexes = { }
2019-12-07 22:05:59 +00:00
for p in self . photos :
2019-12-13 15:58:37 +00:00
if not p . band_name in band_photos :
band_photos [ p . band_name ] = [ ]
if not p . band_name in band_indexes :
2021-01-05 18:37:47 +00:00
band_indexes [ p . band_name ] = str ( p . band_index )
2019-12-13 15:58:37 +00:00
band_photos [ p . band_name ] . append ( p )
2019-12-07 22:05:59 +00:00
2019-12-13 15:58:37 +00:00
bands_count = len ( band_photos )
2019-12-07 22:05:59 +00:00
if bands_count > = 2 and bands_count < = 8 :
# Validate that all bands have the same number of images,
# otherwise this is not a multi-camera setup
2019-12-13 15:58:37 +00:00
img_per_band = len ( band_photos [ p . band_name ] )
for band in band_photos :
if len ( band_photos [ band ] ) != img_per_band :
log . ODM_ERROR ( " Multi-camera setup detected, but band \" %s \" (identified from \" %s \" ) has only %s images (instead of %s ), perhaps images are missing or are corrupted. Please include all necessary files to process all bands and try again. " % ( band , band_photos [ band ] [ 0 ] . filename , len ( band_photos [ band ] ) , img_per_band ) )
2019-12-07 22:05:59 +00:00
raise RuntimeError ( " Invalid multi-camera images " )
2019-12-05 20:28:06 +00:00
2019-12-13 15:58:37 +00:00
mc = [ ]
for band_name in band_indexes :
mc . append ( { ' name ' : band_name , ' photos ' : band_photos [ band_name ] } )
# Sort by band index
mc . sort ( key = lambda x : band_indexes [ x [ ' name ' ] ] )
2019-12-07 22:05:59 +00:00
return mc
2019-12-13 15:58:37 +00:00
2019-12-05 20:28:06 +00:00
return None
2019-06-20 19:39:49 +00:00
def is_georeferenced ( self ) :
return self . georef is not None
2020-02-04 17:56:20 +00:00
def has_gcp ( self ) :
2021-10-12 18:05:07 +00:00
return self . is_georeferenced ( ) and self . gcp is not None and self . gcp . exists ( )
2020-02-04 17:56:20 +00:00
2021-02-24 20:11:16 +00:00
def georeference_with_gcp ( self , gcp_file , output_coords_file , output_gcp_file , output_model_txt_geo , rerun = False ) :
2019-06-21 18:47:00 +00:00
if not io . file_exists ( output_coords_file ) or not io . file_exists ( output_gcp_file ) or rerun :
2019-06-20 19:39:49 +00:00
gcp = GCPFile ( gcp_file )
if gcp . exists ( ) :
2020-11-02 16:17:44 +00:00
if gcp . entries_count ( ) == 0 :
raise RuntimeError ( " This GCP file does not have any entries. Are the entries entered in the proper format? " )
2020-11-03 17:07:20 +00:00
2019-06-21 18:47:00 +00:00
# Convert GCP file to a UTM projection since the rest of the pipeline
# does not handle other SRS well.
rejected_entries = [ ]
2019-06-24 12:28:44 +00:00
utm_gcp = GCPFile ( gcp . create_utm_copy ( output_gcp_file , filenames = [ p . filename for p in self . photos ] , rejected_entries = rejected_entries , include_extras = False ) )
2019-06-21 18:47:00 +00:00
if not utm_gcp . exists ( ) :
raise RuntimeError ( " Could not project GCP file to UTM. Please double check your GCP file for mistakes. " )
for re in rejected_entries :
log . ODM_WARNING ( " GCP line ignored (image not found): %s " % str ( re ) )
if utm_gcp . entries_count ( ) > 0 :
log . ODM_INFO ( " %s GCP points will be used for georeferencing " % utm_gcp . entries_count ( ) )
else :
raise RuntimeError ( " A GCP file was provided, but no valid GCP entries could be used. Note that the GCP file is case sensitive ( \" .JPG \" is not the same as \" .jpg \" ). " )
2021-02-24 20:11:16 +00:00
2019-06-21 18:47:00 +00:00
self . gcp = utm_gcp
2021-02-24 20:11:16 +00:00
# Compute RTC offsets from GCP points
2021-02-25 18:35:43 +00:00
x_pos = [ p . x for p in utm_gcp . iter_entries ( ) ]
y_pos = [ p . y for p in utm_gcp . iter_entries ( ) ]
2021-02-24 20:11:16 +00:00
x_off , y_off = int ( np . round ( np . mean ( x_pos ) ) ) , int ( np . round ( np . mean ( y_pos ) ) )
# Create coords file, we'll be using this later
# during georeferencing
with open ( output_coords_file , ' w ' ) as f :
coords_header = gcp . wgs84_utm_zone ( )
f . write ( coords_header + " \n " )
f . write ( " {} {} \n " . format ( x_off , y_off ) )
log . ODM_INFO ( " Generated coords file from GCP: %s " % coords_header )
# Deprecated: This is mostly for backward compatibility and should be
# be removed at some point
shutil . copyfile ( output_coords_file , output_model_txt_geo )
log . ODM_INFO ( " Wrote %s " % output_model_txt_geo )
2019-06-20 19:39:49 +00:00
else :
log . ODM_WARNING ( " GCP file does not exist: %s " % gcp_file )
return
2018-01-26 19:38:26 +00:00
else :
2019-06-20 19:39:49 +00:00
log . ODM_INFO ( " Coordinates file already exist: %s " % output_coords_file )
2019-06-21 18:47:00 +00:00
log . ODM_INFO ( " GCP file already exist: %s " % output_gcp_file )
self . gcp = GCPFile ( output_gcp_file )
2019-06-20 19:39:49 +00:00
2020-01-21 20:32:24 +00:00
self . georef = ODM_GeoRef . FromCoordsFile ( output_coords_file )
2019-06-20 19:39:49 +00:00
return self . georef
2021-02-24 20:11:16 +00:00
def georeference_with_gps ( self , images_path , output_coords_file , output_model_txt_geo , rerun = False ) :
2019-06-20 19:39:49 +00:00
try :
2019-06-21 18:47:00 +00:00
if not io . file_exists ( output_coords_file ) or rerun :
2019-06-21 19:51:27 +00:00
location . extract_utm_coords ( self . photos , images_path , output_coords_file )
2019-06-20 19:39:49 +00:00
else :
log . ODM_INFO ( " Coordinates file already exist: %s " % output_coords_file )
2021-02-24 20:11:16 +00:00
# Deprecated: This is mostly for backward compatibility and should be
# be removed at some point
if not io . file_exists ( output_model_txt_geo ) or rerun :
with open ( output_coords_file , ' r ' ) as f :
with open ( output_model_txt_geo , ' w+ ' ) as w :
w . write ( f . readline ( ) ) # CRS
w . write ( f . readline ( ) ) # Offset
else :
log . ODM_INFO ( " Model geo file already exist: %s " % output_model_txt_geo )
2020-01-21 20:32:24 +00:00
self . georef = ODM_GeoRef . FromCoordsFile ( output_coords_file )
2019-06-20 19:39:49 +00:00
except :
2020-02-04 20:47:00 +00:00
log . ODM_WARNING ( ' Could not generate coordinates file. The orthophoto will not be georeferenced. ' )
2019-06-20 19:39:49 +00:00
2019-06-21 19:33:10 +00:00
self . gcp = GCPFile ( None )
2019-06-20 19:39:49 +00:00
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 :
2020-05-15 17:51:46 +00:00
f . write ( self . get_proj_srs ( ) )
def get_proj_srs ( self ) :
if self . is_georeferenced ( ) :
return self . georef . proj4 ( )
2021-10-12 20:43:42 +00:00
def get_proj_offset ( self ) :
if self . is_georeferenced ( ) :
return ( self . georef . utm_east_offset , self . georef . utm_north_offset )
else :
return ( None , None )
2019-06-20 19:39:49 +00:00
2020-02-26 22:33:08 +00:00
def get_photo ( self , filename ) :
2020-02-26 21:06:39 +00:00
for p in self . photos :
if p . filename == filename :
return p
2020-02-26 22:33:08 +00:00
2020-02-26 21:06:39 +00:00
2019-06-20 19:39:49 +00:00
class ODM_GeoRef ( object ) :
@staticmethod
2020-01-21 20:32:24 +00:00
def FromCoordsFile ( coords_file ) :
2018-01-26 19:38:26 +00:00
# check for coordinate file existence
2019-06-20 19:39:49 +00:00
if not io . file_exists ( coords_file ) :
log . ODM_WARNING ( ' Could not find file %s ' % coords_file )
2018-01-26 19:38:26 +00:00
return
2019-06-20 19:39:49 +00:00
srs = None
2021-02-24 20:11:16 +00:00
utm_east_offset = None
utm_north_offset = None
2019-06-20 19:39:49 +00:00
with open ( coords_file ) as f :
2018-01-26 19:38:26 +00:00
# extract reference system and utm zone from first line.
# We will assume the following format:
# 'WGS84 UTM 17N' or 'WGS84 UTM 17N \n'
line = f . readline ( ) . rstrip ( )
2019-06-20 19:39:49 +00:00
srs = location . parse_srs_header ( line )
2015-12-10 17:17:39 +00:00
2021-02-24 20:11:16 +00:00
# second line is a northing/easting offset
line = f . readline ( ) . rstrip ( )
utm_east_offset , utm_north_offset = map ( float , line . split ( " " ) )
2015-12-10 17:17:39 +00:00
2021-02-24 20:11:16 +00:00
return ODM_GeoRef ( srs , utm_east_offset , utm_north_offset )
def __init__ ( self , srs , utm_east_offset , utm_north_offset ) :
2019-06-20 19:39:49 +00:00
self . srs = srs
2021-02-24 20:11:16 +00:00
self . utm_east_offset = utm_east_offset
self . utm_north_offset = utm_north_offset
2018-01-26 19:38:26 +00:00
self . transform = [ ]
2016-04-05 20:10:02 +00:00
2019-06-20 19:39:49 +00:00
def proj4 ( self ) :
return self . srs . to_proj4 ( )
2021-02-24 20:11:16 +00:00
def utm_offset ( self ) :
return ( self . utm_east_offset , self . utm_north_offset )
2015-12-10 17:17:39 +00:00
class ODM_Tree ( object ) :
2020-09-14 18:33:39 +00:00
def __init__ ( self , root_path , gcp_file = None , geo_file = None ) :
2016-02-26 18:50:12 +00:00
# root path to the project
2015-12-02 14:24:38 +00:00
self . root_path = io . absolute_path_file ( root_path )
2020-10-28 14:05:01 +00:00
self . input_images = os . path . join ( self . root_path , ' images ' )
2015-12-02 14:24:38 +00:00
2016-02-26 18:50:12 +00:00
# modules paths
2015-12-02 14:24:38 +00:00
# here are defined where all modules should be located in
# order to keep track all files al directories during the
# whole reconstruction process.
2020-10-28 14:05:01 +00:00
self . dataset_raw = os . path . join ( self . root_path , ' images ' )
self . opensfm = os . path . join ( self . root_path , ' opensfm ' )
2020-10-28 18:48:18 +00:00
self . openmvs = os . path . join ( self . opensfm , ' undistorted ' , ' openmvs ' )
2020-10-28 14:05:01 +00:00
self . odm_meshing = os . path . join ( self . root_path , ' odm_meshing ' )
self . odm_texturing = os . path . join ( self . root_path , ' odm_texturing ' )
self . odm_25dtexturing = os . path . join ( self . root_path , ' odm_texturing_25d ' )
self . odm_georeferencing = os . path . join ( self . root_path , ' odm_georeferencing ' )
self . odm_filterpoints = os . path . join ( self . root_path , ' odm_filterpoints ' )
self . odm_orthophoto = os . path . join ( self . root_path , ' odm_orthophoto ' )
self . odm_report = os . path . join ( self . root_path , ' odm_report ' )
2015-12-02 14:24:38 +00:00
2016-02-26 18:50:12 +00:00
# important files paths
2016-02-29 14:45:00 +00:00
# benchmarking
2020-10-28 14:05:01 +00:00
self . benchmarking = os . path . join ( self . root_path , ' benchmark.txt ' )
self . dataset_list = os . path . join ( self . root_path , ' img_list.txt ' )
2016-02-29 14:45:00 +00:00
2015-12-02 14:24:38 +00:00
# opensfm
2020-10-28 14:05:01 +00:00
self . opensfm_image_list = os . path . join ( self . opensfm , ' image_list.txt ' )
self . opensfm_reconstruction = os . path . join ( self . opensfm , ' reconstruction.json ' )
self . opensfm_reconstruction_nvm = os . path . join ( self . opensfm , ' undistorted/reconstruction.nvm ' )
2021-02-24 20:11:16 +00:00
self . opensfm_geocoords_reconstruction = os . path . join ( self . opensfm , ' reconstruction.geocoords.json ' )
2021-09-24 15:06:40 +00:00
self . opensfm_topocentric_reconstruction = os . path . join ( self . opensfm , ' reconstruction.topocentric.json ' )
2015-12-10 11:01:41 +00:00
2020-10-27 21:10:10 +00:00
# OpenMVS
2020-11-03 17:07:20 +00:00
self . openmvs_model = os . path . join ( self . openmvs , ' scene_dense_dense_filtered.ply ' )
2020-10-27 21:10:10 +00:00
2019-04-03 18:47:06 +00:00
# filter points
2020-10-28 14:05:01 +00:00
self . filtered_point_cloud = os . path . join ( self . odm_filterpoints , " point_cloud.ply " )
2019-04-03 18:47:06 +00:00
2015-12-02 14:24:38 +00:00
# odm_meshing
2020-10-28 14:05:01 +00:00
self . odm_mesh = os . path . join ( self . odm_meshing , ' odm_mesh.ply ' )
self . odm_meshing_log = os . path . join ( self . odm_meshing , ' odm_meshing_log.txt ' )
self . odm_25dmesh = os . path . join ( self . odm_meshing , ' odm_25dmesh.ply ' )
self . odm_25dmeshing_log = os . path . join ( self . odm_meshing , ' odm_25dmeshing_log.txt ' )
2016-02-26 18:50:12 +00:00
2016-03-24 17:35:29 +00:00
# texturing
2021-02-24 20:11:16 +00:00
self . odm_textured_model_obj = ' odm_textured_model_geo.obj '
2015-12-10 11:01:41 +00:00
# odm_georeferencing
2020-10-28 14:05:01 +00:00
self . odm_georeferencing_coords = os . path . join (
2018-03-03 16:48:43 +00:00
self . odm_georeferencing , ' coords.txt ' )
2018-02-01 16:10:32 +00:00
self . odm_georeferencing_gcp = gcp_file or io . find ( ' gcp_list.txt ' , self . root_path )
2020-10-28 14:05:01 +00:00
self . odm_georeferencing_gcp_utm = os . path . join ( self . odm_georeferencing , ' gcp_list_utm.txt ' )
2020-09-14 18:33:39 +00:00
self . odm_geo_file = geo_file or io . find ( ' geo.txt ' , self . root_path )
2018-03-03 16:48:43 +00:00
self . odm_georeferencing_proj = ' proj.txt '
2021-02-24 21:14:59 +00:00
self . odm_georeferencing_model_txt_geo = os . path . join (
self . odm_georeferencing , ' odm_georeferencing_model_geo.txt ' )
2020-10-28 14:05:01 +00:00
self . odm_georeferencing_xyz_file = os . path . join (
2016-02-25 20:02:48 +00:00
self . odm_georeferencing , ' odm_georeferenced_model.csv ' )
2020-10-28 14:05:01 +00:00
self . odm_georeferencing_model_laz = os . path . join (
2018-06-17 13:51:37 +00:00
self . odm_georeferencing , ' odm_georeferenced_model.laz ' )
2020-10-28 14:05:01 +00:00
self . odm_georeferencing_model_las = os . path . join (
2019-02-22 20:28:37 +00:00
self . odm_georeferencing , ' odm_georeferenced_model.las ' )
2015-12-10 11:01:41 +00:00
2015-12-02 14:24:38 +00:00
# odm_orthophoto
2020-10-28 14:05:01 +00:00
self . odm_orthophoto_render = os . path . join ( self . odm_orthophoto , ' odm_orthophoto_render.tif ' )
self . odm_orthophoto_tif = os . path . join ( self . odm_orthophoto , ' odm_orthophoto.tif ' )
self . odm_orthophoto_corners = os . path . join ( self . odm_orthophoto , ' odm_orthophoto_corners.txt ' )
self . odm_orthophoto_log = os . path . join ( self . odm_orthophoto , ' odm_orthophoto_log.txt ' )
self . odm_orthophoto_tif_log = os . path . join ( self . odm_orthophoto , ' gdal_translate_log.txt ' )
2017-06-23 15:20:46 +00:00
2020-09-17 15:28:03 +00:00
# tiles
2020-10-28 14:05:01 +00:00
self . orthophoto_tiles = os . path . join ( self . root_path , " orthophoto_tiles " )
2020-09-17 15:28:03 +00:00
2019-04-23 01:42:32 +00:00
# Split-merge
2020-10-28 14:05:01 +00:00
self . submodels_path = os . path . join ( self . root_path , ' submodels ' )
2019-04-23 01:42:32 +00:00
2019-05-31 16:12:59 +00:00
# Tiles
self . entwine_pointcloud = self . path ( " entwine_pointcloud " )
2017-06-23 20:15:13 +00:00
def path ( self , * args ) :
2019-04-29 20:18:08 +00:00
return os . path . join ( self . root_path , * args )
2019-04-22 19:14:39 +00:00
class ODM_Stage :
2019-05-15 21:04:09 +00:00
def __init__ ( self , name , args , progress = 0.0 , * * params ) :
2019-04-22 19:14:39 +00:00
self . name = name
self . args = args
2019-05-15 21:04:09 +00:00
self . progress = progress
2019-04-22 19:14:39 +00:00
self . params = params
if self . params is None :
self . params = { }
self . next_stage = None
2019-05-15 21:04:09 +00:00
self . prev_stage = None
2019-04-22 19:14:39 +00:00
def connect ( self , stage ) :
self . next_stage = stage
2019-05-15 21:04:09 +00:00
stage . prev_stage = self
2019-04-22 19:14:39 +00:00
return stage
def rerun ( self ) :
"""
Does this stage need to be rerun ?
"""
2019-04-23 22:01:14 +00:00
return ( self . args . rerun is not None and self . args . rerun == self . name ) or \
2019-04-22 19:14:39 +00:00
( self . args . rerun_all ) or \
2019-04-23 22:01:14 +00:00
( self . args . rerun_from is not None and self . name in self . args . rerun_from )
2019-04-22 19:14:39 +00:00
def run ( self , outputs = { } ) :
start_time = system . now_raw ( )
2021-06-09 15:46:56 +00:00
log . logger . log_json_stage_run ( self . name , start_time )
2019-04-22 19:14:39 +00:00
2021-06-09 15:46:56 +00:00
log . ODM_INFO ( ' Running %s stage ' % self . name )
2019-04-22 19:14:39 +00:00
self . process ( self . args , outputs )
# The tree variable should always be populated at this point
if outputs . get ( ' tree ' ) is None :
raise Exception ( " Assert violation: tree variable is missing from outputs dictionary. " )
if self . args . time :
system . benchmark ( start_time , outputs [ ' tree ' ] . benchmarking , self . name )
log . ODM_INFO ( ' Finished %s stage ' % self . name )
2019-05-15 21:04:09 +00:00
self . update_progress_end ( )
2019-04-22 19:14:39 +00:00
# Last stage?
2019-04-29 02:48:49 +00:00
if self . args . end_with == self . name or self . args . rerun == self . name :
2019-04-22 19:23:02 +00:00
log . ODM_INFO ( " No more stages to run " )
return
2019-04-22 19:14:39 +00:00
# Run next stage?
elif self . next_stage is not None :
self . next_stage . run ( outputs )
2019-05-15 21:04:09 +00:00
def delta_progress ( self ) :
if self . prev_stage :
return max ( 0.0 , self . progress - self . prev_stage . progress )
else :
return max ( 0.0 , self . progress )
def previous_stages_progress ( self ) :
2019-05-21 16:38:30 +00:00
if self . prev_stage :
return max ( 0.0 , self . prev_stage . progress )
else :
return 0.0
2019-05-15 21:04:09 +00:00
def update_progress_end ( self ) :
self . update_progress ( 100.0 )
def update_progress ( self , progress ) :
2019-05-15 22:01:46 +00:00
progress = max ( 0.0 , min ( 100.0 , progress ) )
2019-05-15 21:04:09 +00:00
progressbc . send_update ( self . previous_stages_progress ( ) +
2019-05-20 20:29:51 +00:00
( self . delta_progress ( ) / 100.0 ) * float ( progress ) )
2019-05-15 21:04:09 +00:00
2021-07-30 20:07:34 +00:00
def last_stage ( self ) :
if self . next_stage :
return self . next_stage . last_stage ( )
else :
return self
2019-04-22 19:14:39 +00:00
def process ( self , args , outputs ) :
raise NotImplementedError