2019-03-01 17:35:53 +00:00
|
|
|
import sys
|
2019-03-06 02:45:08 +00:00
|
|
|
import os
|
2020-03-30 14:32:21 +00:00
|
|
|
import shutil
|
|
|
|
import glob
|
2015-11-26 12:15:02 +00:00
|
|
|
|
2015-11-20 10:00:43 +00:00
|
|
|
from opendm import log
|
2015-11-26 12:15:02 +00:00
|
|
|
from opendm import io
|
2015-11-20 10:00:43 +00:00
|
|
|
from opendm import system
|
|
|
|
from opendm import context
|
2018-08-11 16:45:21 +00:00
|
|
|
from opendm import gsd
|
2019-03-06 00:38:46 +00:00
|
|
|
from opendm import point_cloud
|
2019-04-22 19:14:39 +00:00
|
|
|
from opendm import types
|
2020-11-12 15:23:02 +00:00
|
|
|
from opendm.utils import get_depthmap_resolution
|
2019-04-25 15:40:45 +00:00
|
|
|
from opendm.osfm import OSFMContext
|
2020-03-06 18:04:34 +00:00
|
|
|
from opendm import multispectral
|
2021-03-23 19:15:47 +00:00
|
|
|
from opendm import thermal
|
2020-12-06 16:56:58 +00:00
|
|
|
from opendm import nvm
|
2021-03-23 19:15:47 +00:00
|
|
|
from opendm.photo import find_largest_photo
|
2016-02-26 18:50:12 +00:00
|
|
|
|
2021-06-18 20:35:11 +00:00
|
|
|
from opensfm.undistort import add_image_format_extension
|
|
|
|
|
2019-04-22 19:14:39 +00:00
|
|
|
class ODMOpenSfMStage(types.ODM_Stage):
|
|
|
|
def process(self, args, outputs):
|
|
|
|
tree = outputs['tree']
|
|
|
|
reconstruction = outputs['reconstruction']
|
2018-01-26 19:38:26 +00:00
|
|
|
photos = reconstruction.photos
|
2015-11-26 12:15:02 +00:00
|
|
|
|
2015-11-27 16:51:21 +00:00
|
|
|
if not photos:
|
2021-06-09 15:46:56 +00:00
|
|
|
raise system.ExitException('Not enough photos in photos array to start OpenSfM')
|
2015-11-27 16:51:21 +00:00
|
|
|
|
2019-04-25 15:40:45 +00:00
|
|
|
octx = OSFMContext(tree.opensfm)
|
2020-12-02 18:04:12 +00:00
|
|
|
octx.setup(args, tree.dataset_raw, reconstruction=reconstruction, rerun=self.rerun())
|
2021-12-14 19:47:48 +00:00
|
|
|
octx.photos_to_metadata(photos, self.rerun())
|
2020-03-06 18:04:34 +00:00
|
|
|
self.update_progress(20)
|
|
|
|
octx.feature_matching(self.rerun())
|
|
|
|
self.update_progress(30)
|
|
|
|
octx.reconstruct(self.rerun())
|
|
|
|
octx.extract_cameras(tree.path("cameras.json"), self.rerun())
|
|
|
|
self.update_progress(70)
|
2019-04-25 15:40:45 +00:00
|
|
|
|
2021-01-15 20:39:29 +00:00
|
|
|
def cleanup_disk_space():
|
|
|
|
if args.optimize_disk_space:
|
2021-08-04 21:59:38 +00:00
|
|
|
for folder in ["features", "matches", "reports"]:
|
2021-01-15 20:39:29 +00:00
|
|
|
folder_path = octx.path(folder)
|
2021-03-24 18:21:25 +00:00
|
|
|
if os.path.exists(folder_path):
|
|
|
|
if os.path.islink(folder_path):
|
|
|
|
os.unlink(folder_path)
|
|
|
|
else:
|
|
|
|
shutil.rmtree(folder_path)
|
2020-03-30 14:32:21 +00:00
|
|
|
|
2020-03-06 18:04:34 +00:00
|
|
|
# If we find a special flag file for split/merge we stop right here
|
|
|
|
if os.path.exists(octx.path("split_merge_stop_at_reconstruction.txt")):
|
|
|
|
log.ODM_INFO("Stopping OpenSfM early because we found: %s" % octx.path("split_merge_stop_at_reconstruction.txt"))
|
|
|
|
self.next_stage = None
|
2021-01-15 20:39:29 +00:00
|
|
|
cleanup_disk_space()
|
2020-03-06 18:04:34 +00:00
|
|
|
return
|
2019-05-08 16:00:07 +00:00
|
|
|
|
2021-07-30 20:07:34 +00:00
|
|
|
# Stats are computed in the local CRS (before geoprojection)
|
|
|
|
if not args.skip_report:
|
|
|
|
|
|
|
|
# TODO: this will fail to compute proper statistics if
|
|
|
|
# the pipeline is run with --skip-report and is subsequently
|
|
|
|
# rerun without --skip-report a --rerun-* parameter (due to the reconstruction.json file)
|
|
|
|
# being replaced below. It's an isolated use case.
|
|
|
|
|
|
|
|
octx.export_stats(self.rerun())
|
|
|
|
|
|
|
|
self.update_progress(75)
|
|
|
|
|
2021-02-24 20:11:16 +00:00
|
|
|
# We now switch to a geographic CRS
|
2021-09-24 15:06:40 +00:00
|
|
|
if reconstruction.is_georeferenced() and (not io.file_exists(tree.opensfm_topocentric_reconstruction) or self.rerun()):
|
2021-05-04 17:04:13 +00:00
|
|
|
octx.run('export_geocoords --reconstruction --proj "%s" --offset-x %s --offset-y %s' %
|
2021-02-24 20:11:16 +00:00
|
|
|
(reconstruction.georef.proj4(), reconstruction.georef.utm_east_offset, reconstruction.georef.utm_north_offset))
|
2021-09-24 15:06:40 +00:00
|
|
|
shutil.move(tree.opensfm_reconstruction, tree.opensfm_topocentric_reconstruction)
|
2021-02-24 20:11:16 +00:00
|
|
|
shutil.move(tree.opensfm_geocoords_reconstruction, tree.opensfm_reconstruction)
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING("Will skip exporting %s" % tree.opensfm_geocoords_reconstruction)
|
|
|
|
|
|
|
|
self.update_progress(80)
|
|
|
|
|
2020-03-06 18:04:34 +00:00
|
|
|
updated_config_flag_file = octx.path('updated_config.txt')
|
2019-08-15 21:01:11 +00:00
|
|
|
|
2020-03-06 18:04:34 +00:00
|
|
|
# Make sure it's capped by the depthmap-resolution arg,
|
|
|
|
# since the undistorted images are used for MVS
|
|
|
|
outputs['undist_image_max_size'] = max(
|
|
|
|
gsd.image_max_size(photos, args.orthophoto_resolution, tree.opensfm_reconstruction, ignore_gsd=args.ignore_gsd, has_gcp=reconstruction.has_gcp()),
|
2020-11-12 15:23:02 +00:00
|
|
|
get_depthmap_resolution(args, photos)
|
2020-03-06 18:04:34 +00:00
|
|
|
)
|
2019-08-15 21:01:11 +00:00
|
|
|
|
2020-03-06 18:04:34 +00:00
|
|
|
if not io.file_exists(updated_config_flag_file) or self.rerun():
|
|
|
|
octx.update_config({'undistorted_image_max_size': outputs['undist_image_max_size']})
|
|
|
|
octx.touch(updated_config_flag_file)
|
2017-04-04 16:54:40 +00:00
|
|
|
|
2020-12-02 18:04:12 +00:00
|
|
|
# Undistorted images will be used for texturing / MVS
|
2020-12-02 22:17:51 +00:00
|
|
|
|
2020-12-05 03:48:21 +00:00
|
|
|
alignment_info = None
|
|
|
|
primary_band_name = None
|
2021-03-23 19:15:47 +00:00
|
|
|
largest_photo = None
|
2020-12-02 18:04:12 +00:00
|
|
|
undistort_pipeline = []
|
|
|
|
|
|
|
|
def undistort_callback(shot_id, image):
|
|
|
|
for func in undistort_pipeline:
|
|
|
|
image = func(shot_id, image)
|
|
|
|
return image
|
2020-02-26 22:33:08 +00:00
|
|
|
|
2021-03-23 19:15:47 +00:00
|
|
|
def resize_thermal_images(shot_id, image):
|
|
|
|
photo = reconstruction.get_photo(shot_id)
|
|
|
|
if photo.is_thermal():
|
|
|
|
return thermal.resize_to_match(image, largest_photo)
|
|
|
|
else:
|
|
|
|
return image
|
|
|
|
|
2020-12-02 18:04:12 +00:00
|
|
|
def radiometric_calibrate(shot_id, image):
|
|
|
|
photo = reconstruction.get_photo(shot_id)
|
2021-03-23 19:15:47 +00:00
|
|
|
if photo.is_thermal():
|
2021-12-29 15:11:09 +00:00
|
|
|
return thermal.dn_to_temperature(photo, image, tree.dataset_raw)
|
2021-03-23 19:15:47 +00:00
|
|
|
else:
|
|
|
|
return multispectral.dn_to_reflectance(photo, image, use_sun_sensor=args.radiometric_calibration=="camera+sun")
|
2020-12-02 18:04:12 +00:00
|
|
|
|
2020-12-02 22:17:51 +00:00
|
|
|
|
2020-12-02 18:04:12 +00:00
|
|
|
def align_to_primary_band(shot_id, image):
|
2020-12-05 03:48:21 +00:00
|
|
|
photo = reconstruction.get_photo(shot_id)
|
|
|
|
|
2020-12-15 01:49:20 +00:00
|
|
|
# No need to align if requested by user
|
|
|
|
if args.skip_band_alignment:
|
|
|
|
return image
|
|
|
|
|
2020-12-05 03:48:21 +00:00
|
|
|
# No need to align primary
|
|
|
|
if photo.band_name == primary_band_name:
|
|
|
|
return image
|
|
|
|
|
|
|
|
ainfo = alignment_info.get(photo.band_name)
|
|
|
|
if ainfo is not None:
|
|
|
|
return multispectral.align_image(image, ainfo['warp_matrix'], ainfo['dimension'])
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING("Cannot align %s, no alignment matrix could be computed. Band alignment quality might be affected." % (shot_id))
|
|
|
|
return image
|
2020-12-02 22:17:51 +00:00
|
|
|
|
2021-03-23 19:15:47 +00:00
|
|
|
if reconstruction.multi_camera:
|
|
|
|
largest_photo = find_largest_photo(photos)
|
|
|
|
undistort_pipeline.append(resize_thermal_images)
|
|
|
|
|
2020-12-02 22:17:51 +00:00
|
|
|
if args.radiometric_calibration != "none":
|
2020-12-02 18:04:12 +00:00
|
|
|
undistort_pipeline.append(radiometric_calibrate)
|
|
|
|
|
2020-12-05 03:48:21 +00:00
|
|
|
image_list_override = None
|
|
|
|
|
2020-12-02 18:04:12 +00:00
|
|
|
if reconstruction.multi_camera:
|
2020-12-02 22:17:51 +00:00
|
|
|
|
2020-12-05 03:48:21 +00:00
|
|
|
# Undistort only secondary bands
|
|
|
|
image_list_override = [os.path.join(tree.dataset_raw, p.filename) for p in photos] # if p.band_name.lower() != primary_band_name.lower()
|
|
|
|
|
|
|
|
# We backup the original reconstruction.json, tracks.csv
|
|
|
|
# then we augment them by duplicating the primary band
|
|
|
|
# camera shots with each band, so that exports, undistortion,
|
|
|
|
# etc. include all bands
|
|
|
|
# We finally restore the original files later
|
|
|
|
|
|
|
|
added_shots_file = octx.path('added_shots_done.txt')
|
2021-06-15 15:25:23 +00:00
|
|
|
s2p, p2s = None, None
|
2020-12-15 01:49:20 +00:00
|
|
|
|
2020-12-05 03:48:21 +00:00
|
|
|
if not io.file_exists(added_shots_file) or self.rerun():
|
|
|
|
primary_band_name = multispectral.get_primary_band_name(reconstruction.multi_camera, args.primary_band)
|
|
|
|
s2p, p2s = multispectral.compute_band_maps(reconstruction.multi_camera, primary_band_name)
|
2020-12-15 01:49:20 +00:00
|
|
|
|
|
|
|
if not args.skip_band_alignment:
|
|
|
|
alignment_info = multispectral.compute_alignment_matrices(reconstruction.multi_camera, primary_band_name, tree.dataset_raw, s2p, p2s, max_concurrency=args.max_concurrency)
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING("Skipping band alignment")
|
|
|
|
alignment_info = {}
|
2020-12-15 16:43:56 +00:00
|
|
|
|
2020-12-05 20:15:07 +00:00
|
|
|
log.ODM_INFO("Adding shots to reconstruction")
|
|
|
|
|
|
|
|
octx.backup_reconstruction()
|
|
|
|
octx.add_shots_to_reconstruction(p2s)
|
2020-12-05 03:48:21 +00:00
|
|
|
octx.touch(added_shots_file)
|
2020-12-02 22:17:51 +00:00
|
|
|
|
2020-12-02 18:04:12 +00:00
|
|
|
undistort_pipeline.append(align_to_primary_band)
|
|
|
|
|
2020-12-05 03:48:21 +00:00
|
|
|
octx.convert_and_undistort(self.rerun(), undistort_callback, image_list_override)
|
2018-08-11 16:45:21 +00:00
|
|
|
|
2021-02-24 20:11:16 +00:00
|
|
|
self.update_progress(95)
|
2019-05-15 22:01:46 +00:00
|
|
|
|
2019-12-16 16:52:27 +00:00
|
|
|
if reconstruction.multi_camera:
|
2020-12-05 20:15:07 +00:00
|
|
|
octx.restore_reconstruction_backup()
|
2020-12-06 16:56:58 +00:00
|
|
|
|
|
|
|
# Undistort primary band and write undistorted
|
|
|
|
# reconstruction.json, tracks.csv
|
2020-12-05 20:15:07 +00:00
|
|
|
octx.convert_and_undistort(self.rerun(), undistort_callback, runId='primary')
|
2020-12-05 03:48:21 +00:00
|
|
|
|
2019-12-16 16:52:27 +00:00
|
|
|
if not io.file_exists(tree.opensfm_reconstruction_nvm) or self.rerun():
|
|
|
|
octx.run('export_visualsfm --points')
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING('Found a valid OpenSfM NVM reconstruction file in: %s' %
|
|
|
|
tree.opensfm_reconstruction_nvm)
|
2020-12-06 16:56:58 +00:00
|
|
|
|
|
|
|
if reconstruction.multi_camera:
|
|
|
|
log.ODM_INFO("Multiple bands found")
|
|
|
|
|
|
|
|
# Write NVM files for the various bands
|
|
|
|
for band in reconstruction.multi_camera:
|
|
|
|
nvm_file = octx.path("undistorted", "reconstruction_%s.nvm" % band['name'].lower())
|
|
|
|
|
2020-12-08 16:37:25 +00:00
|
|
|
if not io.file_exists(nvm_file) or self.rerun():
|
|
|
|
img_map = {}
|
|
|
|
|
|
|
|
if primary_band_name is None:
|
|
|
|
primary_band_name = multispectral.get_primary_band_name(reconstruction.multi_camera, args.primary_band)
|
|
|
|
if p2s is None:
|
|
|
|
s2p, p2s = multispectral.compute_band_maps(reconstruction.multi_camera, primary_band_name)
|
2020-12-06 16:56:58 +00:00
|
|
|
|
2020-12-08 16:37:25 +00:00
|
|
|
for fname in p2s:
|
|
|
|
|
|
|
|
# Primary band maps to itself
|
|
|
|
if band['name'] == primary_band_name:
|
2021-06-18 20:35:11 +00:00
|
|
|
img_map[add_image_format_extension(fname, 'tif')] = add_image_format_extension(fname, 'tif')
|
2020-12-06 16:56:58 +00:00
|
|
|
else:
|
2020-12-08 16:37:25 +00:00
|
|
|
band_filename = next((p.filename for p in p2s[fname] if p.band_name == band['name']), None)
|
2020-12-06 16:56:58 +00:00
|
|
|
|
2020-12-08 16:37:25 +00:00
|
|
|
if band_filename is not None:
|
2021-06-18 20:35:11 +00:00
|
|
|
img_map[add_image_format_extension(fname, 'tif')] = add_image_format_extension(band_filename, 'tif')
|
2020-12-08 16:37:25 +00:00
|
|
|
else:
|
|
|
|
log.ODM_WARNING("Cannot find %s band equivalent for %s" % (band, fname))
|
2019-12-16 16:52:27 +00:00
|
|
|
|
2020-12-08 16:37:25 +00:00
|
|
|
nvm.replace_nvm_images(tree.opensfm_reconstruction_nvm, img_map, nvm_file)
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING("Found existing NVM file %s" % nvm_file)
|
|
|
|
|
2019-04-28 16:20:03 +00:00
|
|
|
# Skip dense reconstruction if necessary and export
|
|
|
|
# sparse reconstruction instead
|
|
|
|
if args.fast_orthophoto:
|
2020-12-02 18:04:12 +00:00
|
|
|
output_file = octx.path('reconstruction.ply')
|
|
|
|
|
2019-04-28 16:20:03 +00:00
|
|
|
if not io.file_exists(output_file) or self.rerun():
|
2021-01-13 16:34:19 +00:00
|
|
|
octx.run('export_ply --no-cameras --point-num-views')
|
2019-04-28 16:20:03 +00:00
|
|
|
else:
|
|
|
|
log.ODM_WARNING("Found a valid PLY reconstruction in %s" % output_file)
|
2019-04-27 19:51:13 +00:00
|
|
|
|
2021-01-15 20:39:29 +00:00
|
|
|
cleanup_disk_space()
|
|
|
|
|
2020-03-30 14:32:21 +00:00
|
|
|
if args.optimize_disk_space:
|
|
|
|
os.remove(octx.path("tracks.csv"))
|
2020-12-05 20:15:07 +00:00
|
|
|
if io.file_exists(octx.recon_backup_file()):
|
|
|
|
os.remove(octx.recon_backup_file())
|
|
|
|
|
2020-03-30 14:32:21 +00:00
|
|
|
if io.dir_exists(octx.path("undistorted", "depthmaps")):
|
|
|
|
files = glob.glob(octx.path("undistorted", "depthmaps", "*.npz"))
|
|
|
|
for f in files:
|
2020-11-21 18:21:10 +00:00
|
|
|
os.remove(f)
|
|
|
|
|
|
|
|
# Keep these if using OpenMVS
|
2021-01-13 22:22:44 +00:00
|
|
|
if args.fast_orthophoto:
|
2020-11-21 18:21:10 +00:00
|
|
|
files = [octx.path("undistorted", "tracks.csv"),
|
|
|
|
octx.path("undistorted", "reconstruction.json")
|
|
|
|
]
|
|
|
|
for f in files:
|
|
|
|
if os.path.exists(f):
|
|
|
|
os.remove(f)
|