2019-04-23 17:59:54 +00:00
|
|
|
"""
|
|
|
|
OpenSfM related utils
|
|
|
|
"""
|
|
|
|
|
2019-04-24 18:28:44 +00:00
|
|
|
import os, shutil, sys
|
2019-04-23 17:59:54 +00:00
|
|
|
from opendm import io
|
|
|
|
from opendm import log
|
|
|
|
from opendm import system
|
|
|
|
from opendm import context
|
|
|
|
|
2019-04-25 15:40:45 +00:00
|
|
|
class OSFMContext:
|
|
|
|
def __init__(self, opensfm_project_path):
|
|
|
|
self.opensfm_project_path = opensfm_project_path
|
|
|
|
|
|
|
|
def run(self, command):
|
|
|
|
system.run('%s/bin/opensfm %s %s' %
|
|
|
|
(context.opensfm_path, command, self.opensfm_project_path))
|
|
|
|
|
|
|
|
def export_bundler(self, destination_bundle_file, rerun=False):
|
|
|
|
if not io.file_exists(destination_bundle_file) or rerun:
|
|
|
|
# convert back to bundler's format
|
|
|
|
system.run('%s/bin/export_bundler %s' %
|
|
|
|
(context.opensfm_path, self.opensfm_project_path))
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING('Found a valid Bundler file in: %s' % destination_bundle_file)
|
2019-04-25 01:09:23 +00:00
|
|
|
|
2019-04-25 15:40:45 +00:00
|
|
|
def reconstruct(self, rerun=False):
|
|
|
|
tracks_file = os.path.join(self.opensfm_project_path, 'tracks.csv')
|
|
|
|
reconstruction_file = os.path.join(self.opensfm_project_path, 'reconstruction.json')
|
2019-04-23 17:59:54 +00:00
|
|
|
|
2019-04-25 15:40:45 +00:00
|
|
|
if not io.file_exists(tracks_file) or rerun:
|
|
|
|
self.run('create_tracks')
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING('Found a valid OpenSfM tracks file in: %s' % tracks_file)
|
2019-04-23 18:45:47 +00:00
|
|
|
|
2019-04-25 15:40:45 +00:00
|
|
|
if not io.file_exists(reconstruction_file) or rerun:
|
|
|
|
self.run('reconstruct')
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING('Found a valid OpenSfM reconstruction file in: %s' % reconstruction_file)
|
|
|
|
|
|
|
|
# Check that a reconstruction file has been created
|
|
|
|
if not io.file_exists(reconstruction_file):
|
|
|
|
log.ODM_ERROR("The program could not process this dataset using the current settings. "
|
|
|
|
"Check that the images have enough overlap, "
|
|
|
|
"that there are enough recognizable features "
|
|
|
|
"and that the images are in focus. "
|
|
|
|
"You could also try to increase the --min-num-features parameter."
|
|
|
|
"The program will now exit.")
|
|
|
|
raise Exception("Reconstruction could not be generated")
|
|
|
|
|
|
|
|
|
|
|
|
def setup(self, args, images_path, photos, gcp_path=None, append_config = [], rerun=False):
|
|
|
|
"""
|
|
|
|
Setup a OpenSfM project
|
|
|
|
"""
|
|
|
|
if rerun and io.dir_exists(self.opensfm_project_path):
|
|
|
|
shutil.rmtree(self.opensfm_project_path)
|
|
|
|
|
|
|
|
if not io.dir_exists(self.opensfm_project_path):
|
|
|
|
system.mkdir_p(self.opensfm_project_path)
|
|
|
|
|
|
|
|
list_path = io.join_paths(self.opensfm_project_path, 'image_list.txt')
|
|
|
|
if not io.file_exists(list_path) or rerun:
|
|
|
|
|
|
|
|
# create file list
|
|
|
|
has_alt = True
|
|
|
|
with open(list_path, 'w') as fout:
|
|
|
|
for photo in photos:
|
|
|
|
if not photo.altitude:
|
|
|
|
has_alt = False
|
|
|
|
fout.write('%s\n' % io.join_paths(images_path, photo.filename))
|
|
|
|
|
|
|
|
# TODO: does this need to be a relative path?
|
|
|
|
|
|
|
|
# create config file for OpenSfM
|
|
|
|
config = [
|
|
|
|
"use_exif_size: no",
|
|
|
|
"feature_process_size: %s" % args.resize_to,
|
|
|
|
"feature_min_frames: %s" % args.min_num_features,
|
|
|
|
"processes: %s" % args.max_concurrency,
|
|
|
|
"matching_gps_neighbors: %s" % args.matcher_neighbors,
|
|
|
|
"depthmap_method: %s" % args.opensfm_depthmap_method,
|
|
|
|
"depthmap_resolution: %s" % args.depthmap_resolution,
|
|
|
|
"depthmap_min_patch_sd: %s" % args.opensfm_depthmap_min_patch_sd,
|
|
|
|
"depthmap_min_consistent_views: %s" % args.opensfm_depthmap_min_consistent_views,
|
|
|
|
"optimize_camera_parameters: %s" % ('no' if args.use_fixed_camera_params else 'yes')
|
|
|
|
]
|
|
|
|
|
|
|
|
if has_alt:
|
|
|
|
log.ODM_DEBUG("Altitude data detected, enabling it for GPS alignment")
|
|
|
|
config.append("use_altitude_tag: yes")
|
|
|
|
config.append("align_method: naive")
|
|
|
|
else:
|
|
|
|
config.append("align_method: orientation_prior")
|
|
|
|
config.append("align_orientation_prior: vertical")
|
|
|
|
|
|
|
|
if args.use_hybrid_bundle_adjustment:
|
|
|
|
log.ODM_DEBUG("Enabling hybrid bundle adjustment")
|
|
|
|
config.append("bundle_interval: 100") # Bundle after adding 'bundle_interval' cameras
|
|
|
|
config.append("bundle_new_points_ratio: 1.2") # Bundle when (new points) / (bundled points) > bundle_new_points_ratio
|
|
|
|
config.append("local_bundle_radius: 1") # Max image graph distance for images to be included in local bundle adjustment
|
|
|
|
|
|
|
|
if args.matcher_distance > 0:
|
|
|
|
config.append("matching_gps_distance: %s" % args.matcher_distance)
|
|
|
|
|
|
|
|
if gcp_path:
|
|
|
|
config.append("bundle_use_gcp: yes")
|
|
|
|
io.copy(gcp_path, self.opensfm_project_path)
|
|
|
|
|
|
|
|
config = config + append_config
|
|
|
|
|
|
|
|
# write config file
|
|
|
|
log.ODM_DEBUG(config)
|
|
|
|
config_filename = io.join_paths(self.opensfm_project_path, 'config.yaml')
|
|
|
|
with open(config_filename, 'w') as fout:
|
|
|
|
fout.write("\n".join(config))
|
|
|
|
|
|
|
|
# check for image_groups.txt (split-merge)
|
|
|
|
image_groups_file = os.path.join(args.project_path, "image_groups.txt")
|
|
|
|
if io.file_exists(image_groups_file):
|
|
|
|
log.ODM_DEBUG("Copied image_groups.txt to OpenSfM directory")
|
|
|
|
io.copy(image_groups_file, os.path.join(self.opensfm_project_path, "image_groups.txt"))
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING("%s already exists, not rerunning OpenSfM setup" % list_path)
|
2019-04-23 18:45:47 +00:00
|
|
|
|
|
|
|
|
2019-04-25 15:40:45 +00:00
|
|
|
def feature_matching(self, rerun=False):
|
|
|
|
if not io.file_exists(self.feature_matching_done_file()) or rerun:
|
|
|
|
# TODO: put extract metadata into its own function
|
|
|
|
self.run('extract_metadata')
|
2019-04-23 22:01:14 +00:00
|
|
|
|
2019-04-25 15:40:45 +00:00
|
|
|
# TODO: distributed workflow should do these two steps independently
|
|
|
|
self.run('detect_features')
|
|
|
|
self.run('match_features')
|
2019-04-23 22:01:14 +00:00
|
|
|
|
2019-04-25 15:40:45 +00:00
|
|
|
self.mark_feature_matching_done()
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING('Found a feature matching done progress file in: %s' % self.feature_matching_done_file())
|
2019-04-23 22:01:14 +00:00
|
|
|
|
2019-04-25 15:40:45 +00:00
|
|
|
def feature_matching_done_file(self):
|
|
|
|
return io.join_paths(self.opensfm_project_path, 'matching_done.txt')
|
2019-04-23 22:01:14 +00:00
|
|
|
|
2019-04-25 15:40:45 +00:00
|
|
|
def mark_feature_matching_done(self):
|
|
|
|
with open(self.feature_matching_done_file(), 'w') as fout:
|
|
|
|
fout.write("Matching done!\n")
|
|
|
|
|
|
|
|
def path(self, *paths):
|
|
|
|
return os.path.join(self.opensfm_project_path, *paths)
|
2019-04-23 22:01:14 +00:00
|
|
|
|
2019-04-29 02:48:49 +00:00
|
|
|
def set_image_list_absolute(self):
|
|
|
|
"""
|
|
|
|
Checks the image_list.txt file and makes sure that all paths
|
|
|
|
written in it are absolute paths and not relative paths.
|
|
|
|
If there are relative paths, they are changed to absolute paths.
|
|
|
|
"""
|
|
|
|
image_list_file = self.path("image_list.txt")
|
|
|
|
tmp_list_file = self.path("image_list.txt.tmp")
|
|
|
|
|
|
|
|
if io.file_exists(image_list_file):
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
with open(image_list_file, 'r') as f:
|
|
|
|
content = f.read()
|
|
|
|
|
|
|
|
lines = []
|
|
|
|
for line in map(str.strip, content.split('\n')):
|
|
|
|
if line and not line.startswith("/"):
|
|
|
|
changed = True
|
|
|
|
line = os.path.abspath(os.path.join(self.opensfm_project_path, line))
|
|
|
|
lines.append(line)
|
|
|
|
|
|
|
|
if changed:
|
|
|
|
with open(tmp_list_file, 'w') as f:
|
|
|
|
f.write("\n".join(lines))
|
|
|
|
|
|
|
|
os.remove(image_list_file)
|
|
|
|
os.rename(tmp_list_file, image_list_file)
|
|
|
|
|
|
|
|
log.ODM_DEBUG("%s now contains absolute paths" % image_list_file)
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING("No %s found, cannot check for absolute paths." % image_list_file)
|
2019-04-24 18:28:44 +00:00
|
|
|
|
|
|
|
def get_submodel_argv(args, submodels_path, submodel_name):
|
|
|
|
"""
|
|
|
|
:return the same as argv, but removing references to --split,
|
|
|
|
setting/replacing --project-path and name
|
2019-04-25 01:09:23 +00:00
|
|
|
removing --rerun-from, --rerun, --rerun-all
|
2019-04-30 20:24:08 +00:00
|
|
|
adding --orthophoto-cutline
|
|
|
|
adding --dem-euclidean-map
|
2019-04-30 20:09:03 +00:00
|
|
|
adding --skip-3dmodel (split-merge does not support 3D model merging)
|
2019-04-24 18:28:44 +00:00
|
|
|
"""
|
|
|
|
argv = sys.argv
|
|
|
|
|
|
|
|
result = [argv[0]]
|
|
|
|
i = 1
|
|
|
|
project_path_found = False
|
|
|
|
project_name_added = False
|
2019-04-29 21:35:12 +00:00
|
|
|
orthophoto_cutline_found = False
|
2019-04-30 20:24:08 +00:00
|
|
|
dem_euclidean_map_found = False
|
|
|
|
|
2019-04-30 20:09:03 +00:00
|
|
|
skip_3dmodel_found = False
|
2019-04-24 18:28:44 +00:00
|
|
|
|
2019-04-24 19:15:22 +00:00
|
|
|
# TODO: what about GCP paths?
|
|
|
|
|
2019-04-24 18:28:44 +00:00
|
|
|
while i < len(argv):
|
|
|
|
arg = argv[i]
|
|
|
|
|
|
|
|
# Last?
|
|
|
|
if i == len(argv) - 1:
|
|
|
|
# Project name?
|
|
|
|
if arg == args.name:
|
|
|
|
result.append(submodel_name)
|
|
|
|
project_name_added = True
|
|
|
|
else:
|
|
|
|
result.append(arg)
|
|
|
|
i += 1
|
|
|
|
elif arg == '--project-path':
|
|
|
|
result.append(arg)
|
|
|
|
result.append(submodels_path)
|
|
|
|
project_path_found = True
|
|
|
|
i += 2
|
2019-04-29 21:35:12 +00:00
|
|
|
elif arg == '--orthophoto-cutline':
|
2019-04-27 22:37:07 +00:00
|
|
|
result.append(arg)
|
2019-04-29 21:35:12 +00:00
|
|
|
orthophoto_cutline_found = True
|
2019-04-27 22:37:07 +00:00
|
|
|
i += 1
|
2019-04-30 20:24:08 +00:00
|
|
|
elif arg == '--dem-euclidean-map':
|
|
|
|
result.append(arg)
|
|
|
|
dem_euclidean_map_found = True
|
|
|
|
i += 1
|
2019-04-30 20:09:03 +00:00
|
|
|
elif arg == '--skip-3dmodel':
|
|
|
|
result.append(arg)
|
|
|
|
skip_3dmodel_found = True
|
|
|
|
i += 1
|
2019-04-24 18:28:44 +00:00
|
|
|
elif arg == '--split':
|
|
|
|
i += 2
|
2019-04-25 01:09:23 +00:00
|
|
|
elif arg == '--rerun-from':
|
|
|
|
i += 2
|
|
|
|
elif arg == '--rerun':
|
|
|
|
i += 2
|
|
|
|
elif arg == '--rerun-all':
|
|
|
|
i += 1
|
2019-04-24 18:28:44 +00:00
|
|
|
else:
|
|
|
|
result.append(arg)
|
|
|
|
i += 1
|
|
|
|
|
|
|
|
if not project_path_found:
|
|
|
|
result.append('--project-path')
|
|
|
|
result.append(submodel_project_path)
|
2019-04-24 21:36:45 +00:00
|
|
|
|
2019-04-24 18:28:44 +00:00
|
|
|
if not project_name_added:
|
|
|
|
result.append(submodel_name)
|
2019-04-27 22:37:07 +00:00
|
|
|
|
2019-04-29 21:35:12 +00:00
|
|
|
if not orthophoto_cutline_found:
|
|
|
|
result.append("--orthophoto-cutline")
|
2019-04-24 18:28:44 +00:00
|
|
|
|
2019-04-30 20:24:08 +00:00
|
|
|
if not dem_euclidean_map_found:
|
|
|
|
result.append("--dem-euclidean-map")
|
|
|
|
|
2019-04-30 20:09:03 +00:00
|
|
|
if not skip_3dmodel_found:
|
|
|
|
result.append("--skip-3dmodel")
|
|
|
|
|
2019-04-24 18:28:44 +00:00
|
|
|
return result
|
2019-04-24 21:36:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_submodel_paths(submodels_path, *paths):
|
|
|
|
"""
|
|
|
|
:return Existing paths for all submodels
|
|
|
|
"""
|
|
|
|
result = []
|
|
|
|
for f in os.listdir(submodels_path):
|
|
|
|
if f.startswith('submodel'):
|
|
|
|
p = os.path.join(submodels_path, f, *paths)
|
|
|
|
if os.path.exists(p):
|
|
|
|
result.append(p)
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING("Missing %s from submodel %s" % (p, f))
|
|
|
|
|
2019-04-27 22:37:07 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
def get_all_submodel_paths(submodels_path, *all_paths):
|
|
|
|
"""
|
|
|
|
:return Existing, multiple paths for all submodels as a nested list (all or nothing for each submodel)
|
|
|
|
if a single file is missing from the submodule, no files are returned for that submodel.
|
|
|
|
|
|
|
|
(i.e. get_multi_submodel_paths("path/", "odm_orthophoto.tif", "dem.tif")) -->
|
|
|
|
[["path/submodel_0000/odm_orthophoto.tif", "path/submodel_0000/dem.tif"],
|
|
|
|
["path/submodel_0001/odm_orthophoto.tif", "path/submodel_0001/dem.tif"]]
|
|
|
|
"""
|
|
|
|
result = []
|
|
|
|
for f in os.listdir(submodels_path):
|
|
|
|
if f.startswith('submodel'):
|
|
|
|
all_found = True
|
|
|
|
|
|
|
|
for ap in all_paths:
|
|
|
|
p = os.path.join(submodels_path, f, ap)
|
|
|
|
if not os.path.exists(p):
|
|
|
|
log.ODM_WARNING("Missing %s from submodel %s" % (p, f))
|
|
|
|
all_found = False
|
|
|
|
|
|
|
|
if all_found:
|
|
|
|
result.append([os.path.join(submodels_path, f, ap) for ap in all_paths])
|
|
|
|
|
2019-04-24 21:36:45 +00:00
|
|
|
return result
|