def get_depthmap_resolution(args, photos): if 'depthmap_resolution_is_set' in args: # Legacy log.ODM_WARNING( "Legacy option --depthmap-resolution (this might be removed in a future version). Use --pc-quality instead." ) return int(args.depthmap_resolution) else: max_dim = find_largest_photo_dim(photos) min_dim = 320 # Never go lower than this pc_quality_scale = { 'ultra': 1, 'high': 0.5, 'medium': 0.25, 'low': 0.125, 'lowest': 0.0675 } if max_dim > 0: return max(min_dim, int(max_dim * pc_quality_scale[args.pc_quality])) else: log.ODM_WARNING( "Cannot compute max image dimensions, going with default depthmap_resolution of 640" ) return 640 # Sensible default
def process(self, args, outputs): tree = outputs['tree'] reconstruction = outputs['reconstruction'] max_dim = find_largest_photo_dim(reconstruction.photos) max_texture_size = 8 * 1024 # default if max_dim > 8000: log.ODM_INFO("Large input images (%s pixels), increasing maximum texture size." % max_dim) max_texture_size *= 3 class nonloc: runs = [] def add_run(nvm_file, primary=True, band=None): subdir = "" if not primary and band is not None: subdir = band if not args.skip_3dmodel and (primary or args.use_3dmesh): nonloc.runs += [{ 'out_dir': os.path.join(tree.odm_texturing, subdir), 'model': tree.odm_mesh, 'nadir': False, 'primary': primary, 'nvm_file': nvm_file, 'labeling_file': os.path.join(tree.odm_texturing, "odm_textured_model_geo_labeling.vec") if subdir else None }] if not args.use_3dmesh: nonloc.runs += [{ 'out_dir': os.path.join(tree.odm_25dtexturing, subdir), 'model': tree.odm_25dmesh, 'nadir': True, 'primary': primary, 'nvm_file': nvm_file, 'labeling_file': os.path.join(tree.odm_25dtexturing, "odm_textured_model_geo_labeling.vec") if subdir else None }] if reconstruction.multi_camera: for band in reconstruction.multi_camera: primary = band['name'] == get_primary_band_name(reconstruction.multi_camera, args.primary_band) nvm_file = os.path.join(tree.opensfm, "undistorted", "reconstruction_%s.nvm" % band['name'].lower()) add_run(nvm_file, primary, band['name'].lower()) # Sort to make sure primary band is processed first nonloc.runs.sort(key=lambda r: r['primary'], reverse=True) else: add_run(tree.opensfm_reconstruction_nvm) progress_per_run = 100.0 / len(nonloc.runs) progress = 0.0 for r in nonloc.runs: if not io.dir_exists(r['out_dir']): system.mkdir_p(r['out_dir']) odm_textured_model_obj = os.path.join(r['out_dir'], tree.odm_textured_model_obj) if not io.file_exists(odm_textured_model_obj) or self.rerun(): log.ODM_INFO('Writing MVS Textured file in: %s' % odm_textured_model_obj) # Format arguments to fit Mvs-Texturing app skipGlobalSeamLeveling = "" skipLocalSeamLeveling = "" keepUnseenFaces = "" nadir = "" if args.texturing_skip_global_seam_leveling: skipGlobalSeamLeveling = "--skip_global_seam_leveling" if args.texturing_skip_local_seam_leveling: skipLocalSeamLeveling = "--skip_local_seam_leveling" if args.texturing_keep_unseen_faces: keepUnseenFaces = "--keep_unseen_faces" if (r['nadir']): nadir = '--nadir_mode' # mvstex definitions kwargs = { 'bin': context.mvstex_path, 'out_dir': os.path.join(r['out_dir'], "odm_textured_model_geo"), 'model': r['model'], 'dataTerm': args.texturing_data_term, 'outlierRemovalType': args.texturing_outlier_removal_type, 'skipGlobalSeamLeveling': skipGlobalSeamLeveling, 'skipLocalSeamLeveling': skipLocalSeamLeveling, 'keepUnseenFaces': keepUnseenFaces, 'toneMapping': args.texturing_tone_mapping, 'nadirMode': nadir, 'maxTextureSize': '--max_texture_size=%s' % max_texture_size, 'nvm_file': r['nvm_file'], 'intermediate': '--no_intermediate_results' if (r['labeling_file'] or not reconstruction.multi_camera) else '', 'labelingFile': '-L "%s"' % r['labeling_file'] if r['labeling_file'] else '' } mvs_tmp_dir = os.path.join(r['out_dir'], 'tmp') # Make sure tmp directory is empty if io.dir_exists(mvs_tmp_dir): log.ODM_INFO("Removing old tmp directory {}".format(mvs_tmp_dir)) shutil.rmtree(mvs_tmp_dir) # run texturing binary system.run('"{bin}" "{nvm_file}" "{model}" "{out_dir}" ' '-d {dataTerm} -o {outlierRemovalType} ' '-t {toneMapping} ' '{intermediate} ' '{skipGlobalSeamLeveling} ' '{skipLocalSeamLeveling} ' '{keepUnseenFaces} ' '{nadirMode} ' '{labelingFile} ' '{maxTextureSize} '.format(**kwargs)) # Backward compatibility: copy odm_textured_model_geo.mtl to odm_textured_model.mtl # for certain older WebODM clients which expect a odm_textured_model.mtl # to be present for visualization # We should remove this at some point in the future geo_mtl = os.path.join(r['out_dir'], 'odm_textured_model_geo.mtl') if io.file_exists(geo_mtl): nongeo_mtl = os.path.join(r['out_dir'], 'odm_textured_model.mtl') shutil.copy(geo_mtl, nongeo_mtl) progress += progress_per_run self.update_progress(progress) else: log.ODM_WARNING('Found a valid ODM Texture file in: %s' % odm_textured_model_obj) if args.optimize_disk_space: for r in nonloc.runs: if io.file_exists(r['model']): os.remove(r['model']) undistorted_images_path = os.path.join(tree.opensfm, "undistorted", "images") if io.dir_exists(undistorted_images_path): shutil.rmtree(undistorted_images_path)
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 has_alt = True has_gps = False with open(list_path, 'w') as fout: for photo in photos: if not photo.altitude: has_alt = False 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 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)) use_bow = args.matcher_type == "bow" feature_type = "SIFT" # GPSDOP override if we have GPS accuracy information (such as RTK) if 'gps_accuracy_is_set' in args: log.ODM_INFO("Forcing GPS DOP to %s for all images" % args.gps_accuracy) log.ODM_INFO("Writing exif overrides") exif_overrides = {} for p in photos: if 'gps_accuracy_is_set' in args: dop = args.gps_accuracy elif p.get_gps_dop() is not None: dop = p.get_gps_dop() else: dop = args.gps_accuracy # default value if p.latitude is not None and p.longitude is not None: exif_overrides[p.filename] = { 'gps': { 'latitude': p.latitude, 'longitude': p.longitude, 'altitude': p.altitude if p.altitude is not None else 0, 'dop': dop, } } with open( os.path.join(self.opensfm_project_path, "exif_overrides.json"), 'w') as f: f.write(json.dumps(exif_overrides)) # 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 if 'resize_to_is_set' in args: # Legacy log.ODM_WARNING( "Legacy option --resize-to (this might be removed in a future version). Use --feature-quality instead." ) feature_process_size = int(args.resize_to) else: feature_quality_scale = { 'ultra': 1, 'high': 0.5, 'medium': 0.25, 'low': 0.125, 'lowest': 0.0675, } max_dim = find_largest_photo_dim(photos) if max_dim > 0: log.ODM_INFO("Maximum photo dimensions: %spx" % str(max_dim)) feature_process_size = int( max_dim * feature_quality_scale[args.feature_quality]) else: log.ODM_WARNING( "Cannot compute max image dimensions, going with defaults" ) depthmap_resolution = get_depthmap_resolution(args, photos) # create config file for OpenSfM 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" % args.matcher_neighbors, "matching_gps_distance: %s" % args.matcher_distance, "optimize_camera_parameters: %s" % ('no' if args.use_fixed_camera_params or args.cameras else 'yes'), "undistorted_image_format: tif", "bundle_outlier_filtering_type: AUTO", "align_orientation_prior: vertical", "triangulation_type: ROBUST", "retriangulation_ratio: 2", ] if args.camera_lens != 'auto': config.append("camera_projection_type: %s" % args.camera_lens.upper()) if not has_gps: log.ODM_INFO("No GPS information, using BOW matching") use_bow = True feature_type = args.feature_type.upper() if use_bow: config.append("matcher_type: WORDS") # Cannot use SIFT with BOW if feature_type == "SIFT": log.ODM_WARNING( "Using BOW matching, will use HAHOG feature type, not SIFT" ) feature_type = "HAHOG" # GPU acceleration? if has_gpus() and feature_type == "SIFT": log.ODM_INFO("Using GPU for extracting SIFT features") log.ODM_INFO("--min-num-features will be ignored") feature_type = "SIFT_GPU" 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") 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)) else: log.ODM_WARNING("%s already exists, not rerunning OpenSfM setup" % list_path)
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 has_alt = True has_gps = False with open(list_path, 'w') as fout: for photo in photos: if not photo.altitude: has_alt = False 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 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 if ('resize_to_is_set' in args) and args.resize_to > 0: # Legacy log.ODM_WARNING( "Legacy option --resize-to (this might be removed in a future version). Use --feature-quality instead." ) feature_process_size = int(args.resize_to) else: feature_quality_scale = { 'ultra': 1, 'high': 0.5, 'medium': 0.25, 'low': 0.125, 'lowest': 0.0675, } max_dim = find_largest_photo_dim(photos) if max_dim > 0: log.ODM_INFO("Maximum photo dimensions: %spx" % str(max_dim)) feature_process_size = int( max_dim * feature_quality_scale[args.feature_quality]) log.ODM_INFO( "Photo dimensions for feature extraction: %ipx" % feature_process_size) else: log.ODM_WARNING( "Cannot compute max image dimensions, going with defaults" ) depthmap_resolution = get_depthmap_resolution(args, photos) # create config file for OpenSfM 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" % args.matcher_neighbors, "matching_gps_distance: 0", "matching_graph_rounds: 50", "optimize_camera_parameters: %s" % ('no' if args.use_fixed_camera_params or args.cameras 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.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(): max_photo = find_largest_photo(photos) w, h = max_photo.width, max_photo.height 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" 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") 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)