kopia lustrzana https://github.com/OpenDroneMap/ODM
797 wiersze
32 KiB
Python
797 wiersze
32 KiB
Python
"""
|
|
OpenSfM related utils
|
|
"""
|
|
|
|
import os, shutil, sys, json, argparse, copy
|
|
import yaml
|
|
import numpy as np
|
|
import pyproj
|
|
from pyproj import CRS
|
|
from opendm import io
|
|
from opendm import log
|
|
from opendm import system
|
|
from opendm import context
|
|
from opendm import camera
|
|
from opendm import location
|
|
from opendm.photo import find_largest_photo_dims, find_largest_photo
|
|
from opensfm.large import metadataset
|
|
from opensfm.large import tools
|
|
from opensfm.actions import undistort
|
|
from opensfm.dataset import DataSet
|
|
from opensfm.types import Reconstruction
|
|
from opensfm import report
|
|
from opendm.multispectral import get_photos_by_band
|
|
from opendm.gpu import has_popsift_and_can_handle_texsize, has_gpu
|
|
from opensfm import multiview, exif
|
|
from opensfm.actions.export_geocoords import _transform
|
|
|
|
class OSFMContext:
|
|
def __init__(self, opensfm_project_path):
|
|
self.opensfm_project_path = opensfm_project_path
|
|
|
|
def run(self, command):
|
|
osfm_bin = os.path.join(context.opensfm_path, 'bin', 'opensfm')
|
|
system.run('"%s" %s "%s"' %
|
|
(osfm_bin, command, self.opensfm_project_path))
|
|
|
|
def is_reconstruction_done(self):
|
|
tracks_file = os.path.join(self.opensfm_project_path, 'tracks.csv')
|
|
reconstruction_file = os.path.join(self.opensfm_project_path, 'reconstruction.json')
|
|
|
|
return io.file_exists(tracks_file) and io.file_exists(reconstruction_file)
|
|
|
|
def create_tracks(self, rerun=False):
|
|
tracks_file = os.path.join(self.opensfm_project_path, 'tracks.csv')
|
|
rs_file = self.path('rs_done.txt')
|
|
|
|
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)
|
|
|
|
def reconstruct(self, rolling_shutter_correct=False, merge_partial=False, rerun=False):
|
|
reconstruction_file = os.path.join(self.opensfm_project_path, 'reconstruction.json')
|
|
if not io.file_exists(reconstruction_file) or rerun:
|
|
self.run('reconstruct')
|
|
if merge_partial:
|
|
self.check_merge_partial_reconstructions()
|
|
else:
|
|
log.ODM_WARNING('Found a valid OpenSfM reconstruction file in: %s' % reconstruction_file)
|
|
|
|
# Check that a reconstruction file has been created
|
|
if not self.reconstructed():
|
|
raise system.ExitException("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.")
|
|
|
|
if rolling_shutter_correct:
|
|
rs_file = self.path('rs_done.txt')
|
|
|
|
if not io.file_exists(rs_file) or rerun:
|
|
self.run('rs_correct')
|
|
|
|
log.ODM_INFO("Re-running the reconstruction pipeline")
|
|
|
|
self.match_features(True)
|
|
self.create_tracks(True)
|
|
self.reconstruct(rolling_shutter_correct=False, merge_partial=merge_partial, rerun=True)
|
|
|
|
self.touch(rs_file)
|
|
else:
|
|
log.ODM_WARNING("Rolling shutter correction already applied")
|
|
|
|
def check_merge_partial_reconstructions(self):
|
|
if self.reconstructed():
|
|
data = DataSet(self.opensfm_project_path)
|
|
reconstructions = data.load_reconstruction()
|
|
tracks_manager = data.load_tracks_manager()
|
|
|
|
if len(reconstructions) > 1:
|
|
log.ODM_WARNING("Multiple reconstructions detected (%s), this might be an indicator that some areas did not have sufficient overlap" % len(reconstructions))
|
|
log.ODM_INFO("Attempting merge")
|
|
|
|
merged = Reconstruction()
|
|
merged.set_reference(reconstructions[0].reference)
|
|
|
|
for ix_r, rec in enumerate(reconstructions):
|
|
if merged.reference != rec.reference:
|
|
# Should never happen
|
|
continue
|
|
|
|
log.ODM_INFO("Merging reconstruction %s" % ix_r)
|
|
|
|
for camera in rec.cameras.values():
|
|
merged.add_camera(camera)
|
|
|
|
for point in rec.points.values():
|
|
try:
|
|
new_point = merged.create_point(point.id, point.coordinates)
|
|
new_point.color = point.color
|
|
except RuntimeError as e:
|
|
log.ODM_WARNING("Cannot merge shot id %s (%s)" % (shot.id, str(e)))
|
|
continue
|
|
|
|
for shot in rec.shots.values():
|
|
merged.add_shot(shot)
|
|
try:
|
|
obsdict = tracks_manager.get_shot_observations(shot.id)
|
|
except RuntimeError:
|
|
log.ODM_WARNING("Shot id %s missing from tracks_manager!" % shot.id)
|
|
continue
|
|
for track_id, obs in obsdict.items():
|
|
if track_id in merged.points:
|
|
merged.add_observation(shot.id, track_id, obs)
|
|
|
|
data.save_reconstruction([merged])
|
|
|
|
def setup(self, args, images_path, reconstruction, 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 = os.path.join(self.opensfm_project_path, 'image_list.txt')
|
|
if not io.file_exists(list_path) or rerun:
|
|
|
|
if reconstruction.multi_camera:
|
|
photos = get_photos_by_band(reconstruction.multi_camera, args.primary_band)
|
|
if len(photos) < 1:
|
|
raise Exception("Not enough images in selected band %s" % args.primary_band.lower())
|
|
log.ODM_INFO("Reconstruction will use %s images from %s band" % (len(photos), args.primary_band.lower()))
|
|
else:
|
|
photos = reconstruction.photos
|
|
|
|
# create file list
|
|
num_zero_alt = 0
|
|
has_alt = True
|
|
has_gps = False
|
|
with open(list_path, 'w') as fout:
|
|
for photo in photos:
|
|
if photo.altitude is None:
|
|
has_alt = False
|
|
elif photo.altitude == 0:
|
|
num_zero_alt += 1
|
|
if photo.latitude is not None and photo.longitude is not None:
|
|
has_gps = True
|
|
|
|
fout.write('%s\n' % os.path.join(images_path, photo.filename))
|
|
|
|
# check 0 altitude images percentage when has_alt is True
|
|
if has_alt and num_zero_alt / len(photos) > 0.05:
|
|
log.ODM_WARNING("More than 5% of images have zero altitude, this might be an indicator that the images have no altitude information")
|
|
has_alt = False
|
|
|
|
# check for image_groups.txt (split-merge)
|
|
image_groups_file = os.path.join(args.project_path, "image_groups.txt")
|
|
if 'split_image_groups_is_set' in args:
|
|
image_groups_file = os.path.abspath(args.split_image_groups)
|
|
|
|
if io.file_exists(image_groups_file):
|
|
dst_groups_file = os.path.join(self.opensfm_project_path, "image_groups.txt")
|
|
io.copy(image_groups_file, dst_groups_file)
|
|
log.ODM_INFO("Copied %s to %s" % (image_groups_file, dst_groups_file))
|
|
|
|
# check for cameras
|
|
if args.cameras:
|
|
try:
|
|
camera_overrides = camera.get_opensfm_camera_models(args.cameras)
|
|
with open(os.path.join(self.opensfm_project_path, "camera_models_overrides.json"), 'w') as f:
|
|
f.write(json.dumps(camera_overrides))
|
|
log.ODM_INFO("Wrote camera_models_overrides.json to OpenSfM directory")
|
|
except Exception as e:
|
|
log.ODM_WARNING("Cannot set camera_models_overrides.json: %s" % str(e))
|
|
|
|
# Check image masks
|
|
masks = []
|
|
for p in photos:
|
|
if p.mask is not None:
|
|
masks.append((p.filename, os.path.join(images_path, p.mask)))
|
|
|
|
if masks:
|
|
log.ODM_INFO("Found %s image masks" % len(masks))
|
|
with open(os.path.join(self.opensfm_project_path, "mask_list.txt"), 'w') as f:
|
|
for fname, mask in masks:
|
|
f.write("{} {}\n".format(fname, mask))
|
|
|
|
# Compute feature_process_size
|
|
feature_process_size = 2048 # default
|
|
|
|
feature_quality_scale = {
|
|
'ultra': 1,
|
|
'high': 0.5,
|
|
'medium': 0.25,
|
|
'low': 0.125,
|
|
'lowest': 0.0675,
|
|
}
|
|
|
|
max_dims = find_largest_photo_dims(photos)
|
|
|
|
if max_dims is not None:
|
|
w, h = max_dims
|
|
max_dim = max(w, h)
|
|
log.ODM_INFO("Maximum photo dimensions: %spx" % str(max_dim))
|
|
|
|
lower_limit = 320
|
|
upper_limit = 4480
|
|
megapixels = (w * h) / 1e6
|
|
multiplier = 1
|
|
|
|
if megapixels < 2:
|
|
multiplier = 2
|
|
elif megapixels > 42:
|
|
multiplier = 0.5
|
|
|
|
factor = min(1, feature_quality_scale[args.feature_quality] * multiplier)
|
|
feature_process_size = min(upper_limit, max(lower_limit, int(max_dim * factor)))
|
|
log.ODM_INFO("Photo dimensions for feature extraction: %ipx" % feature_process_size)
|
|
else:
|
|
log.ODM_WARNING("Cannot compute max image dimensions, going with defaults")
|
|
|
|
# create config file for OpenSfM
|
|
if args.matcher_neighbors > 0:
|
|
matcher_graph_rounds = 0
|
|
matcher_neighbors = args.matcher_neighbors
|
|
else:
|
|
matcher_graph_rounds = 50
|
|
matcher_neighbors = 0
|
|
|
|
# Always use matcher-neighbors if less than 4 pictures
|
|
if len(photos) <= 3:
|
|
matcher_graph_rounds = 0
|
|
matcher_neighbors = 3
|
|
|
|
config = [
|
|
"use_exif_size: no",
|
|
"flann_algorithm: KDTREE", # more stable, faster than KMEANS
|
|
"feature_process_size: %s" % feature_process_size,
|
|
"feature_min_frames: %s" % args.min_num_features,
|
|
"processes: %s" % args.max_concurrency,
|
|
"matching_gps_neighbors: %s" % matcher_neighbors,
|
|
"matching_gps_distance: 0",
|
|
"matching_graph_rounds: %s" % matcher_graph_rounds,
|
|
"optimize_camera_parameters: %s" % ('no' if args.use_fixed_camera_params else 'yes'),
|
|
"reconstruction_algorithm: %s" % (args.sfm_algorithm),
|
|
"undistorted_image_format: tif",
|
|
"bundle_outlier_filtering_type: AUTO",
|
|
"sift_peak_threshold: 0.066",
|
|
"align_orientation_prior: vertical",
|
|
"triangulation_type: ROBUST",
|
|
"retriangulation_ratio: 2",
|
|
]
|
|
|
|
if args.matcher_order > 0:
|
|
if not reconstruction.is_georeferenced():
|
|
config.append("matching_order_neighbors: %s" % args.matcher_order)
|
|
else:
|
|
log.ODM_WARNING("Georeferenced reconstruction, ignoring --matcher-order")
|
|
|
|
if args.camera_lens != 'auto':
|
|
config.append("camera_projection_type: %s" % args.camera_lens.upper())
|
|
|
|
matcher_type = args.matcher_type
|
|
feature_type = args.feature_type.upper()
|
|
|
|
osfm_matchers = {
|
|
"bow": "WORDS",
|
|
"flann": "FLANN",
|
|
"bruteforce": "BRUTEFORCE"
|
|
}
|
|
|
|
if not has_gps and not 'matcher_type_is_set' in args:
|
|
log.ODM_INFO("No GPS information, using BOW matching by default (you can override this by setting --matcher-type explicitly)")
|
|
matcher_type = "bow"
|
|
|
|
if matcher_type == "bow":
|
|
# Cannot use anything other than HAHOG with BOW
|
|
if feature_type != "HAHOG":
|
|
log.ODM_WARNING("Using BOW matching, will use HAHOG feature type, not SIFT")
|
|
feature_type = "HAHOG"
|
|
|
|
config.append("matcher_type: %s" % osfm_matchers[matcher_type])
|
|
|
|
# GPU acceleration?
|
|
if has_gpu(args) and max_dims is not None:
|
|
w, h = max_dims
|
|
if w > h:
|
|
h = int((h / w) * feature_process_size)
|
|
w = int(feature_process_size)
|
|
else:
|
|
w = int((w / h) * feature_process_size)
|
|
h = int(feature_process_size)
|
|
|
|
if has_popsift_and_can_handle_texsize(w, h) and feature_type == "SIFT":
|
|
log.ODM_INFO("Using GPU for extracting SIFT features")
|
|
feature_type = "SIFT_GPU"
|
|
self.gpu_sift_feature_extraction = True
|
|
|
|
config.append("feature_type: %s" % feature_type)
|
|
|
|
if has_alt:
|
|
log.ODM_INFO("Altitude data detected, enabling it for GPS alignment")
|
|
config.append("use_altitude_tag: yes")
|
|
|
|
gcp_path = reconstruction.gcp.gcp_path
|
|
if has_alt or gcp_path:
|
|
config.append("align_method: auto")
|
|
else:
|
|
config.append("align_method: orientation_prior")
|
|
|
|
if args.use_hybrid_bundle_adjustment:
|
|
log.ODM_INFO("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
|
|
else:
|
|
config.append("local_bundle_radius: 0")
|
|
|
|
if gcp_path:
|
|
config.append("bundle_use_gcp: yes")
|
|
if not args.force_gps:
|
|
config.append("bundle_use_gps: no")
|
|
else:
|
|
config.append("bundle_compensate_gps_bias: yes")
|
|
|
|
io.copy(gcp_path, self.path("gcp_list.txt"))
|
|
|
|
config = config + append_config
|
|
|
|
# write config file
|
|
log.ODM_INFO(config)
|
|
config_filename = self.get_config_file_path()
|
|
with open(config_filename, 'w') as fout:
|
|
fout.write("\n".join(config))
|
|
|
|
# We impose our own reference_lla
|
|
if reconstruction.is_georeferenced():
|
|
self.write_reference_lla(reconstruction.georef.utm_east_offset, reconstruction.georef.utm_north_offset, reconstruction.georef.proj4())
|
|
else:
|
|
log.ODM_WARNING("%s already exists, not rerunning OpenSfM setup" % list_path)
|
|
|
|
def get_config_file_path(self):
|
|
return os.path.join(self.opensfm_project_path, 'config.yaml')
|
|
|
|
def reconstructed(self):
|
|
if not io.file_exists(self.path("reconstruction.json")):
|
|
return False
|
|
|
|
with open(self.path("reconstruction.json"), 'r') as f:
|
|
return f.readline().strip() != "[]"
|
|
|
|
def extract_metadata(self, rerun=False):
|
|
metadata_dir = self.path("exif")
|
|
if not io.dir_exists(metadata_dir) or rerun:
|
|
self.run('extract_metadata')
|
|
|
|
def photos_to_metadata(self, photos, rolling_shutter, rolling_shutter_readout, rerun=False):
|
|
metadata_dir = self.path("exif")
|
|
|
|
if io.dir_exists(metadata_dir) and not rerun:
|
|
log.ODM_WARNING("%s already exists, not rerunning photo to metadata" % metadata_dir)
|
|
return
|
|
|
|
if io.dir_exists(metadata_dir):
|
|
shutil.rmtree(metadata_dir)
|
|
|
|
os.makedirs(metadata_dir, exist_ok=True)
|
|
|
|
camera_models = {}
|
|
data = DataSet(self.opensfm_project_path)
|
|
|
|
for p in photos:
|
|
d = p.to_opensfm_exif(rolling_shutter, rolling_shutter_readout)
|
|
with open(os.path.join(metadata_dir, "%s.exif" % p.filename), 'w') as f:
|
|
f.write(json.dumps(d, indent=4))
|
|
|
|
camera_id = p.camera_id()
|
|
if camera_id not in camera_models:
|
|
camera = exif.camera_from_exif_metadata(d, data)
|
|
camera_models[camera_id] = camera
|
|
|
|
# Override any camera specified in the camera models overrides file.
|
|
if data.camera_models_overrides_exists():
|
|
overrides = data.load_camera_models_overrides()
|
|
if "all" in overrides:
|
|
for key in camera_models:
|
|
camera_models[key] = copy.copy(overrides["all"])
|
|
camera_models[key].id = key
|
|
else:
|
|
for key, value in overrides.items():
|
|
camera_models[key] = value
|
|
data.save_camera_models(camera_models)
|
|
|
|
def is_feature_matching_done(self):
|
|
features_dir = self.path("features")
|
|
matches_dir = self.path("matches")
|
|
|
|
return io.dir_exists(features_dir) and io.dir_exists(matches_dir)
|
|
|
|
def feature_matching(self, rerun=False):
|
|
features_dir = self.path("features")
|
|
|
|
if not io.dir_exists(features_dir) or rerun:
|
|
try:
|
|
self.run('detect_features')
|
|
except system.SubprocessException as e:
|
|
# Sometimes feature extraction by GPU can fail
|
|
# for various reasons, so before giving up
|
|
# we try to fallback to CPU
|
|
if hasattr(self, 'gpu_sift_feature_extraction'):
|
|
log.ODM_WARNING("GPU SIFT extraction failed, maybe the graphics card is not supported? Attempting fallback to CPU")
|
|
self.update_config({'feature_type': "SIFT"})
|
|
if os.path.exists(features_dir):
|
|
shutil.rmtree(features_dir)
|
|
self.run('detect_features')
|
|
else:
|
|
raise e
|
|
else:
|
|
log.ODM_WARNING('Detect features already done: %s exists' % features_dir)
|
|
|
|
self.match_features(rerun)
|
|
|
|
def match_features(self, rerun=False):
|
|
matches_dir = self.path("matches")
|
|
if not io.dir_exists(matches_dir) or rerun:
|
|
self.run('match_features')
|
|
else:
|
|
log.ODM_WARNING('Match features already done: %s exists' % matches_dir)
|
|
|
|
def align_reconstructions(self, rerun):
|
|
alignment_file = self.path('alignment_done.txt')
|
|
if not io.file_exists(alignment_file) or rerun:
|
|
log.ODM_INFO("Aligning submodels...")
|
|
meta_data = metadataset.MetaDataSet(self.opensfm_project_path)
|
|
reconstruction_shots = tools.load_reconstruction_shots(meta_data)
|
|
transformations = tools.align_reconstructions(reconstruction_shots,
|
|
tools.partial_reconstruction_name,
|
|
False)
|
|
tools.apply_transformations(transformations)
|
|
|
|
self.touch(alignment_file)
|
|
else:
|
|
log.ODM_WARNING('Found a alignment done progress file in: %s' % alignment_file)
|
|
|
|
def touch(self, file):
|
|
with open(file, 'w') as fout:
|
|
fout.write("Done!\n")
|
|
|
|
def path(self, *paths):
|
|
return os.path.join(self.opensfm_project_path, *paths)
|
|
|
|
def extract_cameras(self, output, rerun=False):
|
|
if not os.path.exists(output) or rerun:
|
|
try:
|
|
reconstruction_file = self.path("reconstruction.json")
|
|
with open(output, 'w') as fout:
|
|
fout.write(json.dumps(camera.get_cameras_from_opensfm(reconstruction_file), indent=4))
|
|
except Exception as e:
|
|
log.ODM_WARNING("Cannot export cameras to %s. %s." % (output, str(e)))
|
|
else:
|
|
log.ODM_INFO("Already extracted cameras")
|
|
|
|
def convert_and_undistort(self, rerun=False, imageFilter=None, image_list=None, runId="nominal"):
|
|
log.ODM_INFO("Undistorting %s ..." % self.opensfm_project_path)
|
|
done_flag_file = self.path("undistorted", "%s_done.txt" % runId)
|
|
|
|
if not io.file_exists(done_flag_file) or rerun:
|
|
ds = DataSet(self.opensfm_project_path)
|
|
|
|
if image_list is not None:
|
|
ds._set_image_list(image_list)
|
|
|
|
undistort.run_dataset(ds, "reconstruction.json",
|
|
0, None, "undistorted", imageFilter)
|
|
|
|
self.touch(done_flag_file)
|
|
else:
|
|
log.ODM_WARNING("Already undistorted (%s)" % runId)
|
|
|
|
def restore_reconstruction_backup(self):
|
|
if os.path.exists(self.recon_backup_file()):
|
|
# This time export the actual reconstruction.json
|
|
# (containing only the primary band)
|
|
if os.path.exists(self.recon_file()):
|
|
os.remove(self.recon_file())
|
|
os.replace(self.recon_backup_file(), self.recon_file())
|
|
log.ODM_INFO("Restored reconstruction.json")
|
|
|
|
def backup_reconstruction(self):
|
|
if os.path.exists(self.recon_backup_file()):
|
|
os.remove(self.recon_backup_file())
|
|
|
|
log.ODM_INFO("Backing up reconstruction")
|
|
shutil.copyfile(self.recon_file(), self.recon_backup_file())
|
|
|
|
def recon_backup_file(self):
|
|
return self.path("reconstruction.backup.json")
|
|
|
|
def recon_file(self):
|
|
return self.path("reconstruction.json")
|
|
|
|
def add_shots_to_reconstruction(self, p2s):
|
|
with open(self.recon_file()) as f:
|
|
reconstruction = json.loads(f.read())
|
|
|
|
# Augment reconstruction.json
|
|
for recon in reconstruction:
|
|
shots = recon['shots']
|
|
sids = list(shots)
|
|
|
|
for shot_id in sids:
|
|
secondary_photos = p2s.get(shot_id)
|
|
if secondary_photos is None:
|
|
log.ODM_WARNING("Cannot find secondary photos for %s" % shot_id)
|
|
continue
|
|
|
|
for p in secondary_photos:
|
|
shots[p.filename] = shots[shot_id]
|
|
|
|
with open(self.recon_file(), 'w') as f:
|
|
f.write(json.dumps(reconstruction))
|
|
|
|
|
|
def update_config(self, cfg_dict):
|
|
cfg_file = self.get_config_file_path()
|
|
log.ODM_INFO("Updating %s" % cfg_file)
|
|
if os.path.exists(cfg_file):
|
|
try:
|
|
with open(cfg_file) as fin:
|
|
cfg = yaml.safe_load(fin)
|
|
for k, v in cfg_dict.items():
|
|
cfg[k] = v
|
|
log.ODM_INFO("%s: %s" % (k, v))
|
|
with open(cfg_file, 'w') as fout:
|
|
fout.write(yaml.dump(cfg, default_flow_style=False))
|
|
except Exception as e:
|
|
log.ODM_WARNING("Cannot update configuration file %s: %s" % (cfg_file, str(e)))
|
|
else:
|
|
log.ODM_WARNING("Tried to update configuration, but %s does not exist." % cfg_file)
|
|
|
|
def export_stats(self, rerun=False):
|
|
log.ODM_INFO("Export reconstruction stats")
|
|
stats_path = self.path("stats", "stats.json")
|
|
if not os.path.exists(stats_path) or rerun:
|
|
self.run("compute_statistics --diagram_max_points 100000")
|
|
else:
|
|
log.ODM_WARNING("Found existing reconstruction stats %s" % stats_path)
|
|
|
|
def export_report(self, report_path, odm_stats, rerun=False):
|
|
log.ODM_INFO("Exporting report to %s" % report_path)
|
|
|
|
osfm_report_path = self.path("stats", "report.pdf")
|
|
if not os.path.exists(report_path) or rerun:
|
|
data = DataSet(self.opensfm_project_path)
|
|
pdf_report = report.Report(data, odm_stats)
|
|
pdf_report.generate_report()
|
|
pdf_report.save_report("report.pdf")
|
|
|
|
if os.path.exists(osfm_report_path):
|
|
if os.path.exists(report_path):
|
|
os.unlink(report_path)
|
|
shutil.move(osfm_report_path, report_path)
|
|
else:
|
|
log.ODM_WARNING("Report could not be generated")
|
|
else:
|
|
log.ODM_WARNING("Report %s already exported" % report_path)
|
|
|
|
def write_reference_lla(self, offset_x, offset_y, proj4):
|
|
reference_lla = self.path("reference_lla.json")
|
|
|
|
longlat = CRS.from_epsg("4326")
|
|
lon, lat = location.transform2(CRS.from_proj4(proj4), longlat, offset_x, offset_y)
|
|
|
|
with open(reference_lla, 'w') as f:
|
|
f.write(json.dumps({
|
|
'latitude': lat,
|
|
'longitude': lon,
|
|
'altitude': 0.0
|
|
}, indent=4))
|
|
|
|
log.ODM_INFO("Wrote reference_lla.json")
|
|
|
|
def ground_control_points(self, proj4):
|
|
"""
|
|
Load ground control point information.
|
|
"""
|
|
gcp_stats_file = self.path("stats", "ground_control_points.json")
|
|
|
|
if not io.file_exists(gcp_stats_file):
|
|
return []
|
|
|
|
gcps_stats = {}
|
|
try:
|
|
with open(gcp_stats_file) as f:
|
|
gcps_stats = json.loads(f.read())
|
|
except:
|
|
log.ODM_INFO("Cannot parse %s" % gcp_stats_file)
|
|
|
|
if not gcps_stats:
|
|
return []
|
|
|
|
ds = DataSet(self.opensfm_project_path)
|
|
reference = ds.load_reference()
|
|
projection = pyproj.Proj(proj4)
|
|
|
|
result = []
|
|
for gcp in gcps_stats:
|
|
geocoords = _transform(gcp['coordinates'], reference, projection)
|
|
result.append({
|
|
'id': gcp['id'],
|
|
'observations': gcp['observations'],
|
|
'coordinates': geocoords,
|
|
'error': gcp['error']
|
|
})
|
|
|
|
return result
|
|
|
|
|
|
def name(self):
|
|
return os.path.basename(os.path.abspath(self.path("..")))
|
|
|
|
def get_submodel_argv(args, submodels_path = None, submodel_name = None):
|
|
"""
|
|
Gets argv for a submodel starting from the args passed to the application startup.
|
|
Additionally, if project_name, submodels_path and submodel_name are passed, the function
|
|
handles the <project name> value and --project-path detection / override.
|
|
When all arguments are set to None, --project-path and project name are always removed.
|
|
|
|
:return the same as argv, but removing references to --split,
|
|
setting/replacing --project-path and name
|
|
removing --rerun-from, --rerun, --rerun-all, --sm-cluster
|
|
removing --pc-las, --pc-csv, --pc-ept, --tiles flags (processing these is wasteful)
|
|
adding --orthophoto-cutline
|
|
adding --dem-euclidean-map
|
|
adding --skip-3dmodel (split-merge does not support 3D model merging)
|
|
tweaking --crop if necessary (DEM merging makes assumption about the area of DEMs and their euclidean maps that require cropping. If cropping is skipped, this leads to errors.)
|
|
removing --gcp (the GCP path if specified is always "gcp_list.txt")
|
|
reading the contents of --cameras
|
|
reading the contents of --boundary
|
|
"""
|
|
assure_always = ['orthophoto_cutline', 'dem_euclidean_map', 'skip_3dmodel', 'skip_report']
|
|
remove_always = ['split', 'split_overlap', 'rerun_from', 'rerun', 'gcp', 'end_with', 'sm_cluster', 'rerun_all', 'pc_csv', 'pc_las', 'pc_ept', 'tiles', 'copy-to', 'cog']
|
|
read_json_always = ['cameras', 'boundary']
|
|
|
|
argv = sys.argv
|
|
|
|
# Startup script (/path/to/run.py)
|
|
startup_script = argv[0]
|
|
|
|
# On Windows, make sure we always invoke the "run.bat" file
|
|
if sys.platform == 'win32':
|
|
startup_script_dir = os.path.dirname(startup_script)
|
|
startup_script = os.path.join(startup_script_dir, "run")
|
|
|
|
result = [startup_script]
|
|
|
|
args_dict = vars(args).copy()
|
|
set_keys = [k[:-len("_is_set")] for k in args_dict.keys() if k.endswith("_is_set")]
|
|
|
|
# Handle project name and project path (special case)
|
|
if "name" in set_keys:
|
|
del args_dict["name"]
|
|
set_keys.remove("name")
|
|
|
|
if "project_path" in set_keys:
|
|
del args_dict["project_path"]
|
|
set_keys.remove("project_path")
|
|
|
|
# Remove parameters
|
|
set_keys = [k for k in set_keys if k not in remove_always]
|
|
|
|
# Assure parameters
|
|
for k in assure_always:
|
|
if not k in set_keys:
|
|
set_keys.append(k)
|
|
args_dict[k] = True
|
|
|
|
# Read JSON always
|
|
for k in read_json_always:
|
|
if k in set_keys:
|
|
try:
|
|
if isinstance(args_dict[k], str):
|
|
args_dict[k] = io.path_or_json_string_to_dict(args_dict[k])
|
|
if isinstance(args_dict[k], dict):
|
|
args_dict[k] = json.dumps(args_dict[k])
|
|
except ValueError as e:
|
|
log.ODM_WARNING("Cannot parse/read JSON: {}".format(str(e)))
|
|
|
|
# Handle crop (cannot be zero for split/merge)
|
|
if "crop" in set_keys:
|
|
crop_value = float(args_dict["crop"])
|
|
if crop_value == 0:
|
|
crop_value = 0.015625
|
|
args_dict["crop"] = crop_value
|
|
|
|
# Populate result
|
|
for k in set_keys:
|
|
result.append("--%s" % k.replace("_", "-"))
|
|
|
|
# No second value for booleans
|
|
if isinstance(args_dict[k], bool) and args_dict[k] == True:
|
|
continue
|
|
|
|
result.append(str(args_dict[k]))
|
|
|
|
if submodels_path:
|
|
result.append("--project-path")
|
|
result.append(submodels_path)
|
|
|
|
if submodel_name:
|
|
result.append(submodel_name)
|
|
|
|
return result
|
|
|
|
def get_submodel_args_dict(args):
|
|
submodel_argv = get_submodel_argv(args)
|
|
result = {}
|
|
|
|
i = 0
|
|
while i < len(submodel_argv):
|
|
arg = submodel_argv[i]
|
|
next_arg = None if i == len(submodel_argv) - 1 else submodel_argv[i + 1]
|
|
|
|
if next_arg and arg.startswith("--"):
|
|
if next_arg.startswith("--"):
|
|
result[arg[2:]] = True
|
|
else:
|
|
result[arg[2:]] = next_arg
|
|
i += 1
|
|
elif arg.startswith("--"):
|
|
result[arg[2:]] = True
|
|
i += 1
|
|
|
|
return result
|
|
|
|
|
|
def get_submodel_paths(submodels_path, *paths):
|
|
"""
|
|
:return Existing paths for all submodels
|
|
"""
|
|
result = []
|
|
if not os.path.exists(submodels_path):
|
|
return 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))
|
|
|
|
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 = []
|
|
if not os.path.exists(submodels_path):
|
|
return 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])
|
|
|
|
return result
|