def process_local(self): octx = OSFMContext(self.path("opensfm")) log.ODM_INFO("==================================") log.ODM_INFO("Local Reconstruction %s" % octx.name()) log.ODM_INFO("==================================") octx.feature_matching(self.params['rerun']) octx.create_tracks(self.params['rerun']) octx.reconstruct(self.params['rolling_shutter'], self.params['rerun'])
def process_remote(self, done): octx = OSFMContext(self.path("opensfm")) if not octx.is_feature_matching_done() or not octx.is_reconstruction_done() or self.params['rerun']: self.execute_remote_task(done, seed_files=["opensfm/exif", "opensfm/camera_models.json", "opensfm/reference_lla.json"], seed_touch_files=["opensfm/split_merge_stop_at_reconstruction.txt"], outputs=["opensfm/matches", "opensfm/features", "opensfm/reconstruction.json", "opensfm/tracks.csv", "cameras.json"]) else: log.ODM_INFO("Already processed feature matching and reconstruction for %s" % octx.name()) done()
def process(self, args, outputs): tree = outputs['tree'] reconstruction = outputs['reconstruction'] photos = reconstruction.photos outputs['large'] = len(photos) > args.split if outputs['large']: # If we have a cluster address, we'll use a distributed workflow local_workflow = not bool(args.sm_cluster) octx = OSFMContext(tree.opensfm) split_done_file = octx.path("split_done.txt") if not io.file_exists(split_done_file) or self.rerun(): orig_max_concurrency = args.max_concurrency if not local_workflow: args.max_concurrency = max(1, args.max_concurrency - 1) log.ODM_INFO("Setting max-concurrency to %s to better handle remote splits" % args.max_concurrency) log.ODM_INFO("Large dataset detected (%s photos) and split set at %s. Preparing split merge." % ( len(photos), args.split)) config = [ "submodels_relpath: ../submodels/opensfm", "submodel_relpath_template: ../submodels/submodel_%04d/opensfm", "submodel_images_relpath_template: ../submodels/submodel_%04d/images", "submodel_size: %s" % args.split, "submodel_overlap: %s" % args.split_overlap, ] octx.setup(args, tree.dataset_raw, reconstruction=reconstruction, append_config=config, rerun=self.rerun()) octx.extract_metadata(self.rerun()) self.update_progress(5) if local_workflow: octx.feature_matching(self.rerun()) self.update_progress(20) # Create submodels if not io.dir_exists(tree.submodels_path) or self.rerun(): if io.dir_exists(tree.submodels_path): log.ODM_WARNING("Removing existing submodels directory: %s" % tree.submodels_path) shutil.rmtree(tree.submodels_path) octx.run("create_submodels") else: log.ODM_WARNING("Submodels directory already exist at: %s" % tree.submodels_path) # Find paths of all submodels mds = metadataset.MetaDataSet(tree.opensfm) submodel_paths = [os.path.abspath(p) for p in mds.get_submodel_paths()] for sp in submodel_paths: sp_octx = OSFMContext(sp) # Copy filtered GCP file if needed # One in OpenSfM's directory, one in the submodel project directory if reconstruction.gcp and reconstruction.gcp.exists(): submodel_gcp_file = os.path.abspath(sp_octx.path("..", "gcp_list.txt")) submodel_images_dir = os.path.abspath(sp_octx.path("..", "images")) if reconstruction.gcp.make_filtered_copy(submodel_gcp_file, submodel_images_dir): log.ODM_INFO("Copied filtered GCP file to %s" % submodel_gcp_file) io.copy(submodel_gcp_file, os.path.abspath(sp_octx.path("gcp_list.txt"))) else: log.ODM_INFO( "No GCP will be copied for %s, not enough images in the submodel are referenced by the GCP" % sp_octx.name()) # Reconstruct each submodel log.ODM_INFO( "Dataset has been split into %s submodels. Reconstructing each submodel..." % len(submodel_paths)) self.update_progress(25) if local_workflow: for sp in submodel_paths: log.ODM_INFO("Reconstructing %s" % sp) OSFMContext(sp).reconstruct(self.rerun()) else: lre = LocalRemoteExecutor(args.sm_cluster, self.rerun()) lre.set_projects([os.path.abspath(os.path.join(p, "..")) for p in submodel_paths]) lre.run_reconstruction() self.update_progress(50) # TODO: this is currently not working and needs a champion to fix it # https://community.opendronemap.org/t/filenotfound-error-cameras-json/6047/2 # resplit_done_file = octx.path('resplit_done.txt') # if not io.file_exists(resplit_done_file) and bool(args.split_multitracks): # submodels = mds.get_submodel_paths() # i = 0 # for s in submodels: # template = octx.path("../aligned_submodels/submodel_%04d") # with open(s+"/reconstruction.json", "r") as f: # j = json.load(f) # for k in range(0, len(j)): # v = j[k] # path = template % i # #Create the submodel path up to opensfm # os.makedirs(path+"/opensfm") # os.makedirs(path+"/images") # #symlinks for common data # images = os.listdir(octx.path("../images")) # for image in images: # os.symlink("../../../images/"+image, path+"/images/"+image) # os.symlink("../../../opensfm/exif", path+"/opensfm/exif") # os.symlink("../../../opensfm/features", path+"/opensfm/features") # os.symlink("../../../opensfm/matches", path+"/opensfm/matches") # os.symlink("../../../opensfm/reference_lla.json", path+"/opensfm/reference_lla.json") # os.symlink("../../../opensfm/camera_models.json", path+"/opensfm/camera_models.json") # shutil.copy(s+"/../cameras.json", path+"/cameras.json") # shutil.copy(s+"/../images.json", path+"/images.json") # with open(octx.path("config.yaml")) as f: # doc = yaml.safe_load(f) # dmcv = "depthmap_min_consistent_views" # if dmcv in doc: # if len(v["shots"]) < doc[dmcv]: # doc[dmcv] = len(v["shots"]) # print("WARNING: Reduced "+dmcv+" to accommodate short track") # with open(path+"/opensfm/config.yaml", "w") as f: # yaml.dump(doc, f) # #We need the original tracks file for the visualsfm export, since # #there may still be point matches between the tracks # shutil.copy(s+"/tracks.csv", path+"/opensfm/tracks.csv") # #Create our new reconstruction file with only the relevant track # with open(path+"/opensfm/reconstruction.json", "w") as o: # json.dump([v], o) # #Create image lists # with open(path+"/opensfm/image_list.txt", "w") as o: # o.writelines(list(map(lambda x: "../images/"+x+'\n', v["shots"].keys()))) # with open(path+"/img_list.txt", "w") as o: # o.writelines(list(map(lambda x: x+'\n', v["shots"].keys()))) # i+=1 # os.rename(octx.path("../submodels"), octx.path("../unaligned_submodels")) # os.rename(octx.path("../aligned_submodels"), octx.path("../submodels")) # octx.touch(resplit_done_file) mds = metadataset.MetaDataSet(tree.opensfm) submodel_paths = [os.path.abspath(p) for p in mds.get_submodel_paths()] # Align octx.align_reconstructions(self.rerun()) self.update_progress(55) # Aligned reconstruction is in reconstruction.aligned.json # We need to rename it to reconstruction.json remove_paths = [] for sp in submodel_paths: sp_octx = OSFMContext(sp) aligned_recon = sp_octx.path('reconstruction.aligned.json') unaligned_recon = sp_octx.path('reconstruction.unaligned.json') main_recon = sp_octx.path('reconstruction.json') if io.file_exists(main_recon) and io.file_exists(unaligned_recon) and not self.rerun(): log.ODM_INFO("Submodel %s has already been aligned." % sp_octx.name()) continue if not io.file_exists(aligned_recon): log.ODM_WARNING("Submodel %s does not have an aligned reconstruction (%s). " "This could mean that the submodel could not be reconstructed " " (are there enough features to reconstruct it?). Skipping." % ( sp_octx.name(), aligned_recon)) remove_paths.append(sp) continue if io.file_exists(main_recon): shutil.move(main_recon, unaligned_recon) shutil.move(aligned_recon, main_recon) log.ODM_INFO("%s is now %s" % (aligned_recon, main_recon)) # Remove invalid submodels submodel_paths = [p for p in submodel_paths if not p in remove_paths] # Run ODM toolchain for each submodel if local_workflow: for sp in submodel_paths: sp_octx = OSFMContext(sp) log.ODM_INFO("========================") log.ODM_INFO("Processing %s" % sp_octx.name()) log.ODM_INFO("========================") argv = get_submodel_argv(args, tree.submodels_path, sp_octx.name()) # Re-run the ODM toolchain on the submodel system.run(" ".join(map(quote, map(str, argv))), env_vars=os.environ.copy()) else: lre.set_projects([os.path.abspath(os.path.join(p, "..")) for p in submodel_paths]) lre.run_toolchain() # Restore max_concurrency value args.max_concurrency = orig_max_concurrency octx.touch(split_done_file) else: log.ODM_WARNING('Found a split done file in: %s' % split_done_file) else: log.ODM_INFO("Normal dataset, will process all at once.") self.progress = 0.0
def process(self, args, outputs): tree = outputs['tree'] reconstruction = outputs['reconstruction'] photos = reconstruction.photos if not photos: raise system.ExitException( 'Not enough photos in photos array to start OpenSfM') octx = OSFMContext(tree.opensfm) octx.setup(args, tree.dataset_raw, reconstruction=reconstruction, rerun=self.rerun()) octx.photos_to_metadata(photos, self.rerun()) 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) def cleanup_disk_space(): if args.optimize_disk_space: for folder in ["features", "matches", "reports"]: folder_path = octx.path(folder) if os.path.exists(folder_path): if os.path.islink(folder_path): os.unlink(folder_path) else: shutil.rmtree(folder_path) # 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 cleanup_disk_space() return # 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) # We now switch to a geographic CRS if reconstruction.is_georeferenced() and (not io.file_exists( tree.opensfm_topocentric_reconstruction) or self.rerun()): octx.run( 'export_geocoords --reconstruction --proj "%s" --offset-x %s --offset-y %s' % (reconstruction.georef.proj4(), reconstruction.georef.utm_east_offset, reconstruction.georef.utm_north_offset)) shutil.move(tree.opensfm_reconstruction, tree.opensfm_topocentric_reconstruction) 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) updated_config_flag_file = octx.path('updated_config.txt') # 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()), get_depthmap_resolution(args, photos)) 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) # Undistorted images will be used for texturing / MVS alignment_info = None primary_band_name = None largest_photo = None undistort_pipeline = [] def undistort_callback(shot_id, image): for func in undistort_pipeline: image = func(shot_id, image) return image 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 def radiometric_calibrate(shot_id, image): photo = reconstruction.get_photo(shot_id) if photo.is_thermal(): return thermal.dn_to_temperature(photo, image, tree.dataset_raw) else: return multispectral.dn_to_reflectance( photo, image, use_sun_sensor=args.radiometric_calibration == "camera+sun") def align_to_primary_band(shot_id, image): photo = reconstruction.get_photo(shot_id) # No need to align if requested by user if args.skip_band_alignment: return image # 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 if reconstruction.multi_camera: largest_photo = find_largest_photo(photos) undistort_pipeline.append(resize_thermal_images) if args.radiometric_calibration != "none": undistort_pipeline.append(radiometric_calibrate) image_list_override = None if reconstruction.multi_camera: # 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') s2p, p2s = None, None 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) 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 = {} log.ODM_INFO("Adding shots to reconstruction") octx.backup_reconstruction() octx.add_shots_to_reconstruction(p2s) octx.touch(added_shots_file) undistort_pipeline.append(align_to_primary_band) octx.convert_and_undistort(self.rerun(), undistort_callback, image_list_override) self.update_progress(95) if reconstruction.multi_camera: octx.restore_reconstruction_backup() # Undistort primary band and write undistorted # reconstruction.json, tracks.csv octx.convert_and_undistort(self.rerun(), undistort_callback, runId='primary') 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) 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()) 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) for fname in p2s: # Primary band maps to itself if band['name'] == primary_band_name: img_map[add_image_format_extension( fname, 'tif')] = add_image_format_extension( fname, 'tif') else: band_filename = next( (p.filename for p in p2s[fname] if p.band_name == band['name']), None) if band_filename is not None: img_map[add_image_format_extension( fname, 'tif')] = add_image_format_extension( band_filename, 'tif') else: log.ODM_WARNING( "Cannot find %s band equivalent for %s" % (band, fname)) nvm.replace_nvm_images(tree.opensfm_reconstruction_nvm, img_map, nvm_file) else: log.ODM_WARNING("Found existing NVM file %s" % nvm_file) # Skip dense reconstruction if necessary and export # sparse reconstruction instead if args.fast_orthophoto: output_file = octx.path('reconstruction.ply') if not io.file_exists(output_file) or self.rerun(): octx.run('export_ply --no-cameras --point-num-views') else: log.ODM_WARNING("Found a valid PLY reconstruction in %s" % output_file) cleanup_disk_space() if args.optimize_disk_space: os.remove(octx.path("tracks.csv")) if io.file_exists(octx.recon_backup_file()): os.remove(octx.recon_backup_file()) if io.dir_exists(octx.path("undistorted", "depthmaps")): files = glob.glob( octx.path("undistorted", "depthmaps", "*.npz")) for f in files: os.remove(f) # Keep these if using OpenMVS if args.fast_orthophoto: files = [ octx.path("undistorted", "tracks.csv"), octx.path("undistorted", "reconstruction.json") ] for f in files: if os.path.exists(f): os.remove(f)
def process(self, args, outputs): tree = outputs['tree'] reconstruction = outputs['reconstruction'] if not os.path.exists(tree.odm_report): system.mkdir_p(tree.odm_report) log.ODM_INFO("Exporting shots.geojson") shots_geojson = os.path.join(tree.odm_report, "shots.geojson") if not io.file_exists(shots_geojson) or self.rerun(): # Extract geographical camera shots if reconstruction.is_georeferenced(): shots = get_geojson_shots_from_opensfm( tree.opensfm_reconstruction, utm_srs=reconstruction.get_proj_srs(), utm_offset=reconstruction.georef.utm_offset()) else: # Pseudo geo shots = get_geojson_shots_from_opensfm( tree.opensfm_reconstruction, pseudo_geotiff=tree.odm_orthophoto_tif) if shots: with open(shots_geojson, "w") as fout: fout.write(json.dumps(shots)) log.ODM_INFO("Wrote %s" % shots_geojson) else: log.ODM_WARNING("Cannot extract shots") else: log.ODM_WARNING('Found a valid shots file in: %s' % shots_geojson) if args.skip_report: # Stop right here log.ODM_WARNING("Skipping report generation as requested") return # Augment OpenSfM stats file with our own stats odm_stats_json = os.path.join(tree.odm_report, "stats.json") octx = OSFMContext(tree.opensfm) osfm_stats_json = octx.path("stats", "stats.json") odm_stats = None point_cloud_file = None views_dimension = None if not os.path.exists(odm_stats_json) or self.rerun(): if os.path.exists(osfm_stats_json): with open(osfm_stats_json, 'r') as f: odm_stats = json.loads(f.read()) # Add point cloud stats if os.path.exists(tree.odm_georeferencing_model_laz): point_cloud_file = tree.odm_georeferencing_model_laz views_dimension = "UserData" # pc_info_file should have been generated by cropper pc_info_file = os.path.join( tree.odm_georeferencing, "odm_georeferenced_model.info.json") odm_stats[ 'point_cloud_statistics'] = generate_point_cloud_stats( tree.odm_georeferencing_model_laz, pc_info_file, self.rerun()) else: ply_pc = os.path.join(tree.odm_filterpoints, "point_cloud.ply") if os.path.exists(ply_pc): point_cloud_file = ply_pc views_dimension = "views" pc_info_file = os.path.join(tree.odm_filterpoints, "point_cloud.info.json") odm_stats[ 'point_cloud_statistics'] = generate_point_cloud_stats( ply_pc, pc_info_file, self.rerun()) else: log.ODM_WARNING("No point cloud found") odm_stats['point_cloud_statistics'][ 'dense'] = not args.fast_orthophoto # Add runtime stats total_time = (system.now_raw() - outputs['start_time']).total_seconds() odm_stats['odm_processing_statistics'] = { 'total_time': total_time, 'total_time_human': hms(total_time), 'average_gsd': gsd.opensfm_reconstruction_average_gsd( octx.recon_file(), use_all_shots=reconstruction.has_gcp()), } with open(odm_stats_json, 'w') as f: f.write(json.dumps(odm_stats)) else: log.ODM_WARNING( "Cannot generate report, OpenSfM stats are missing") else: log.ODM_WARNING("Reading existing stats %s" % odm_stats_json) with open(odm_stats_json, 'r') as f: odm_stats = json.loads(f.read()) # Generate overlap diagram if odm_stats.get('point_cloud_statistics' ) and point_cloud_file and views_dimension: bounds = odm_stats['point_cloud_statistics'].get('stats', {}).get( 'bbox', {}).get('native', {}).get('bbox') if bounds: image_target_size = 1400 # pixels osfm_stats_dir = os.path.join(tree.opensfm, "stats") diagram_tiff = os.path.join(osfm_stats_dir, "overlap.tif") diagram_png = os.path.join(osfm_stats_dir, "overlap.png") width = bounds.get('maxx') - bounds.get('minx') height = bounds.get('maxy') - bounds.get('miny') max_dim = max(width, height) resolution = float(max_dim) / float(image_target_size) radius = resolution * math.sqrt(2) # Larger radius for sparse point cloud diagram if not odm_stats['point_cloud_statistics']['dense']: radius *= 10 system.run("pdal translate -i \"{}\" " "-o \"{}\" " "--writer gdal " "--writers.gdal.resolution={} " "--writers.gdal.data_type=uint8_t " "--writers.gdal.dimension={} " "--writers.gdal.output_type=max " "--writers.gdal.radius={} ".format( point_cloud_file, diagram_tiff, resolution, views_dimension, radius)) report_assets = os.path.abspath( os.path.join(os.path.dirname(__file__), "../opendm/report")) overlap_color_map = os.path.join(report_assets, "overlap_color_map.txt") bounds_file_path = os.path.join( tree.odm_georeferencing, 'odm_georeferenced_model.bounds.gpkg') if (args.crop > 0 or args.boundary) and os.path.isfile(bounds_file_path): Cropper.crop(bounds_file_path, diagram_tiff, get_orthophoto_vars(args), keep_original=False) system.run( "gdaldem color-relief \"{}\" \"{}\" \"{}\" -of PNG -alpha". format(diagram_tiff, overlap_color_map, diagram_png)) # Copy assets for asset in [ "overlap_diagram_legend.png", "dsm_gradient.png" ]: shutil.copy(os.path.join(report_assets, asset), os.path.join(osfm_stats_dir, asset)) # Generate previews of ortho/dsm if os.path.isfile(tree.odm_orthophoto_tif): osfm_ortho = os.path.join(osfm_stats_dir, "ortho.png") generate_png(tree.odm_orthophoto_tif, osfm_ortho, image_target_size) dems = [] if args.dsm: dems.append("dsm") if args.dtm: dems.append("dtm") for dem in dems: dem_file = tree.path("odm_dem", "%s.tif" % dem) if os.path.isfile(dem_file): # Resize first (faster) resized_dem_file = io.related_file_path( dem_file, postfix=".preview") system.run( "gdal_translate -outsize {} 0 \"{}\" \"{}\" --config GDAL_CACHEMAX {}%" .format(image_target_size, dem_file, resized_dem_file, get_max_memory())) log.ODM_INFO("Computing raster stats for %s" % resized_dem_file) dem_stats = get_raster_stats(resized_dem_file) if len(dem_stats) > 0: odm_stats[dem + '_statistics'] = dem_stats[0] osfm_dem = os.path.join(osfm_stats_dir, "%s.png" % dem) colored_dem, hillshade_dem, colored_hillshade_dem = generate_colored_hillshade( resized_dem_file) system.run( "gdal_translate -outsize {} 0 -of png \"{}\" \"{}\" --config GDAL_CACHEMAX {}%" .format(image_target_size, colored_hillshade_dem, osfm_dem, get_max_memory())) for f in [ resized_dem_file, colored_dem, hillshade_dem, colored_hillshade_dem ]: if os.path.isfile(f): os.remove(f) else: log.ODM_WARNING( "Cannot generate overlap diagram, cannot compute point cloud bounds" ) else: log.ODM_WARNING( "Cannot generate overlap diagram, point cloud stats missing") octx.export_report(os.path.join(tree.odm_report, "report.pdf"), odm_stats, self.rerun())
def process(self, args, outputs): tree = outputs['tree'] reconstruction = outputs['reconstruction'] # Export GCP information if available gcp_export_file = tree.path("odm_georeferencing", "ground_control_points.gpkg") gcp_gml_export_file = tree.path("odm_georeferencing", "ground_control_points.gml") gcp_geojson_export_file = tree.path("odm_georeferencing", "ground_control_points.geojson") if reconstruction.has_gcp() and (not io.file_exists(gcp_export_file) or self.rerun()): octx = OSFMContext(tree.opensfm) gcps = octx.ground_control_points(reconstruction.georef.proj4()) if len(gcps): gcp_schema = { 'geometry': 'Point', 'properties': OrderedDict([ ('id', 'str'), ('observations_count', 'int'), ('observations_list', 'str'), ('error_x', 'float'), ('error_y', 'float'), ('error_z', 'float'), ]) } # Write GeoPackage with fiona.open(gcp_export_file, 'w', driver="GPKG", crs=fiona.crs.from_string(reconstruction.georef.proj4()), schema=gcp_schema) as f: for gcp in gcps: f.write({ 'geometry': { 'type': 'Point', 'coordinates': gcp['coordinates'], }, 'properties': OrderedDict([ ('id', gcp['id']), ('observations_count', len(gcp['observations'])), ('observations_list', ",".join([obs['shot_id'] for obs in gcp['observations']])), ('error_x', gcp['error'][0]), ('error_y', gcp['error'][1]), ('error_z', gcp['error'][2]), ]) }) # Write GML try: system.run('ogr2ogr -of GML "{}" "{}"'.format(gcp_gml_export_file, gcp_export_file)) except Exception as e: log.ODM_WARNING("Cannot generate ground control points GML file: %s" % str(e)) # Write GeoJSON geojson = { 'type': 'FeatureCollection', 'features': [] } from_srs = CRS.from_proj4(reconstruction.georef.proj4()) to_srs = CRS.from_epsg(4326) transformer = location.transformer(from_srs, to_srs) for gcp in gcps: properties = gcp.copy() del properties['coordinates'] geojson['features'].append({ 'type': 'Feature', 'geometry': { 'type': 'Point', 'coordinates': transformer.TransformPoint(*gcp['coordinates']), }, 'properties': properties }) with open(gcp_geojson_export_file, 'w') as f: f.write(json.dumps(geojson, indent=4)) else: log.ODM_WARNING("GCPs could not be loaded for writing to %s" % gcp_export_file) if not io.file_exists(tree.odm_georeferencing_model_laz) or self.rerun(): cmd = ('pdal translate -i "%s" -o \"%s\"' % (tree.filtered_point_cloud, tree.odm_georeferencing_model_laz)) stages = ["ferry"] params = [ '--filters.ferry.dimensions="views => UserData"', '--writers.las.compression="lazip"', ] if reconstruction.is_georeferenced(): log.ODM_INFO("Georeferencing point cloud") stages.append("transformation") params += [ '--filters.transformation.matrix="1 0 0 %s 0 1 0 %s 0 0 1 0 0 0 0 1"' % reconstruction.georef.utm_offset(), '--writers.las.offset_x=%s' % reconstruction.georef.utm_east_offset, '--writers.las.offset_y=%s' % reconstruction.georef.utm_north_offset, '--writers.las.scale_x=0.001', '--writers.las.scale_y=0.001', '--writers.las.scale_z=0.001', '--writers.las.offset_z=0', '--writers.las.a_srs="%s"' % reconstruction.georef.proj4() ] if reconstruction.has_gcp() and io.file_exists(gcp_gml_export_file): log.ODM_INFO("Embedding GCP info in point cloud") params += [ '--writers.las.vlrs="{\\\"filename\\\": \\\"%s\\\", \\\"user_id\\\": \\\"ODM_GCP\\\", \\\"description\\\": \\\"Ground Control Points (GML)\\\"}"' % gcp_gml_export_file.replace(os.sep, "/") ] system.run(cmd + ' ' + ' '.join(stages) + ' ' + ' '.join(params)) self.update_progress(50) if args.crop > 0: log.ODM_INFO("Calculating cropping area and generating bounds shapefile from point cloud") cropper = Cropper(tree.odm_georeferencing, 'odm_georeferenced_model') if args.fast_orthophoto: decimation_step = 4 else: decimation_step = 40 # More aggressive decimation for large datasets if not args.fast_orthophoto: decimation_step *= int(len(reconstruction.photos) / 1000) + 1 decimation_step = min(decimation_step, 95) try: cropper.create_bounds_gpkg(tree.odm_georeferencing_model_laz, args.crop, decimation_step=decimation_step) except: log.ODM_WARNING("Cannot calculate crop bounds! We will skip cropping") args.crop = 0 if 'boundary' in outputs and args.crop == 0: log.ODM_INFO("Using boundary JSON as cropping area") bounds_base, _ = os.path.splitext(tree.odm_georeferencing_model_laz) bounds_json = bounds_base + ".bounds.geojson" bounds_gpkg = bounds_base + ".bounds.gpkg" export_to_bounds_files(outputs['boundary'], reconstruction.get_proj_srs(), bounds_json, bounds_gpkg) else: log.ODM_INFO("Converting point cloud (non-georeferenced)") system.run(cmd + ' ' + ' '.join(stages) + ' ' + ' '.join(params)) point_cloud.post_point_cloud_steps(args, tree, self.rerun()) else: log.ODM_WARNING('Found a valid georeferenced model in: %s' % tree.odm_georeferencing_model_laz) if args.optimize_disk_space and io.file_exists(tree.odm_georeferencing_model_laz) and io.file_exists(tree.filtered_point_cloud): os.remove(tree.filtered_point_cloud)
def process(self, args, outputs): # get inputs tree = outputs['tree'] reconstruction = outputs['reconstruction'] photos = reconstruction.photos if not photos: log.ODM_ERROR('Not enough photos in photos array to start OpenMVS') exit(1) # check if reconstruction was done before if not io.file_exists(tree.openmvs_model) or self.rerun(): if io.dir_exists(tree.openmvs): shutil.rmtree(tree.openmvs) # export reconstruction from opensfm octx = OSFMContext(tree.opensfm) cmd = 'export_openmvs' if reconstruction.multi_camera: # Export only the primary band primary = reconstruction.multi_camera[0] image_list = os.path.join(tree.opensfm, "image_list_%s.txt" % primary['name'].lower()) cmd += ' --image_list "%s"' % image_list octx.run(cmd) self.update_progress(10) depthmaps_dir = os.path.join(tree.openmvs, "depthmaps") if not io.dir_exists(depthmaps_dir): os.mkdir(depthmaps_dir) depthmap_resolution = get_depthmap_resolution(args, photos) if outputs["undist_image_max_size"] <= depthmap_resolution: resolution_level = 0 else: resolution_level = math.floor(math.log(outputs['undist_image_max_size'] / float(depthmap_resolution)) / math.log(2)) config = [ " --resolution-level %s" % int(resolution_level), "--min-resolution %s" % depthmap_resolution, "--max-resolution %s" % int(outputs['undist_image_max_size']), "--max-threads %s" % args.max_concurrency, '-w "%s"' % depthmaps_dir, "-v 0", ] log.ODM_INFO("Running dense reconstruction. This might take a while.") system.run('%s "%s" %s' % (context.omvs_densify_path, os.path.join(tree.openmvs, 'scene.mvs'), ' '.join(config))) self.update_progress(85) # Filter points scene_dense = os.path.join(tree.openmvs, 'scene_dense.mvs') if os.path.exists(scene_dense): config = [ "--filter-point-cloud -1", '-i "%s"' % scene_dense, "-v 0" ] system.run('%s %s' % (context.omvs_densify_path, ' '.join(config))) else: log.ODM_WARNING("Cannot find scene_dense.mvs, dense reconstruction probably failed. Exiting...") exit(1) self.update_progress(95) if args.optimize_disk_space: files = [scene_dense, os.path.join(tree.openmvs, 'scene_dense.ply'), os.path.join(tree.openmvs, 'scene_dense_dense_filtered.mvs'), octx.path("undistorted", "tracks.csv"), octx.path("undistorted", "reconstruction.json") ] for f in files: if os.path.exists(f): os.remove(f) shutil.rmtree(depthmaps_dir) else: log.ODM_WARNING('Found a valid OpenMVS reconstruction file in: %s' % tree.openmvs_model)
def process(self, args, outputs): tree = outputs['tree'] reconstruction = outputs['reconstruction'] photos = reconstruction.photos outputs['large'] = len(photos) > args.split if outputs['large']: # If we have a cluster address, we'll use a distributed workflow local_workflow = not bool(args.sm_cluster) octx = OSFMContext(tree.opensfm) split_done_file = octx.path("split_done.txt") if not io.file_exists(split_done_file) or self.rerun(): orig_max_concurrency = args.max_concurrency if not local_workflow: args.max_concurrency = max(1, args.max_concurrency - 1) log.ODM_INFO( "Setting max-concurrency to %s to better handle remote splits" % args.max_concurrency) log.ODM_INFO( "Large dataset detected (%s photos) and split set at %s. Preparing split merge." % (len(photos), args.split)) config = [ "submodels_relpath: ../submodels/opensfm", "submodel_relpath_template: ../submodels/submodel_%04d/opensfm", "submodel_images_relpath_template: ../submodels/submodel_%04d/images", "submodel_size: %s" % args.split, "submodel_overlap: %s" % args.split_overlap, ] octx.setup(args, tree.dataset_raw, photos, reconstruction=reconstruction, append_config=config, rerun=self.rerun()) octx.extract_metadata(self.rerun()) self.update_progress(5) if local_workflow: octx.feature_matching(self.rerun()) self.update_progress(20) # Create submodels if not io.dir_exists(tree.submodels_path) or self.rerun(): if io.dir_exists(tree.submodels_path): log.ODM_WARNING( "Removing existing submodels directory: %s" % tree.submodels_path) shutil.rmtree(tree.submodels_path) octx.run("create_submodels") else: log.ODM_WARNING( "Submodels directory already exist at: %s" % tree.submodels_path) # Find paths of all submodels mds = metadataset.MetaDataSet(tree.opensfm) submodel_paths = [ os.path.abspath(p) for p in mds.get_submodel_paths() ] for sp in submodel_paths: sp_octx = OSFMContext(sp) # Copy filtered GCP file if needed # One in OpenSfM's directory, one in the submodel project directory if reconstruction.gcp and reconstruction.gcp.exists(): submodel_gcp_file = os.path.abspath( sp_octx.path("..", "gcp_list.txt")) submodel_images_dir = os.path.abspath( sp_octx.path("..", "images")) if reconstruction.gcp.make_filtered_copy( submodel_gcp_file, submodel_images_dir): log.ODM_INFO("Copied filtered GCP file to %s" % submodel_gcp_file) io.copy( submodel_gcp_file, os.path.abspath(sp_octx.path("gcp_list.txt"))) else: log.ODM_INFO( "No GCP will be copied for %s, not enough images in the submodel are referenced by the GCP" % sp_octx.name()) # Reconstruct each submodel log.ODM_INFO( "Dataset has been split into %s submodels. Reconstructing each submodel..." % len(submodel_paths)) self.update_progress(25) if local_workflow: for sp in submodel_paths: log.ODM_INFO("Reconstructing %s" % sp) OSFMContext(sp).reconstruct(self.rerun()) else: lre = LocalRemoteExecutor(args.sm_cluster, self.rerun()) lre.set_projects([ os.path.abspath(os.path.join(p, "..")) for p in submodel_paths ]) lre.run_reconstruction() self.update_progress(50) # Align octx.align_reconstructions(self.rerun()) self.update_progress(55) # Aligned reconstruction is in reconstruction.aligned.json # We need to rename it to reconstruction.json remove_paths = [] for sp in submodel_paths: sp_octx = OSFMContext(sp) aligned_recon = sp_octx.path('reconstruction.aligned.json') unaligned_recon = sp_octx.path( 'reconstruction.unaligned.json') main_recon = sp_octx.path('reconstruction.json') if io.file_exists(main_recon) and io.file_exists( unaligned_recon) and not self.rerun(): log.ODM_INFO("Submodel %s has already been aligned." % sp_octx.name()) continue if not io.file_exists(aligned_recon): log.ODM_WARNING( "Submodel %s does not have an aligned reconstruction (%s). " "This could mean that the submodel could not be reconstructed " " (are there enough features to reconstruct it?). Skipping." % (sp_octx.name(), aligned_recon)) remove_paths.append(sp) continue if io.file_exists(main_recon): shutil.move(main_recon, unaligned_recon) shutil.move(aligned_recon, main_recon) log.ODM_INFO("%s is now %s" % (aligned_recon, main_recon)) # Remove invalid submodels submodel_paths = [ p for p in submodel_paths if not p in remove_paths ] # Run ODM toolchain for each submodel if local_workflow: for sp in submodel_paths: sp_octx = OSFMContext(sp) log.ODM_INFO("========================") log.ODM_INFO("Processing %s" % sp_octx.name()) log.ODM_INFO("========================") argv = get_submodel_argv(args, tree.submodels_path, sp_octx.name()) # Re-run the ODM toolchain on the submodel system.run(" ".join(map(quote, argv)), env_vars=os.environ.copy()) else: lre.set_projects([ os.path.abspath(os.path.join(p, "..")) for p in submodel_paths ]) lre.run_toolchain() # Restore max_concurrency value args.max_concurrency = orig_max_concurrency octx.touch(split_done_file) else: log.ODM_WARNING('Found a split done file in: %s' % split_done_file) else: log.ODM_INFO("Normal dataset, will process all at once.") self.progress = 0.0
def process(self, args, outputs): tree = outputs['tree'] reconstruction = outputs['reconstruction'] photos = reconstruction.photos if not photos: log.ODM_ERROR('Not enough photos in photos array to start OpenSfM') exit(1) octx = OSFMContext(tree.opensfm) octx.setup(args, tree.dataset_raw, photos, reconstruction=reconstruction, rerun=self.rerun()) octx.extract_metadata(self.rerun()) 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) if args.optimize_disk_space: for folder in ["features", "matches", "exif", "reports"]: folder_path = octx.path(folder) if os.path.islink(folder_path): os.unlink(folder_path) else: shutil.rmtree(folder_path) # 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 return if args.fast_orthophoto: output_file = octx.path('reconstruction.ply') elif args.use_opensfm_dense: output_file = tree.opensfm_model else: output_file = tree.opensfm_reconstruction updated_config_flag_file = octx.path('updated_config.txt') # 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()), args.depthmap_resolution) 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) # These will be used for texturing / MVS if args.radiometric_calibration == "none": octx.convert_and_undistort(self.rerun()) else: def radiometric_calibrate(shot_id, image): photo = reconstruction.get_photo(shot_id) return multispectral.dn_to_reflectance( photo, image, use_sun_sensor=args.radiometric_calibration == "camera+sun") octx.convert_and_undistort(self.rerun(), radiometric_calibrate) self.update_progress(80) if reconstruction.multi_camera: # Dump band image lists log.ODM_INFO("Multiple bands found") for band in reconstruction.multi_camera: log.ODM_INFO("Exporting %s band" % band['name']) image_list_file = octx.path("image_list_%s.txt" % band['name'].lower()) if not io.file_exists(image_list_file) or self.rerun(): with open(image_list_file, "w") as f: f.write("\n".join([p.filename for p in band['photos']])) log.ODM_INFO("Wrote %s" % image_list_file) else: log.ODM_WARNING( "Found a valid image list in %s for %s band" % (image_list_file, band['name'])) nvm_file = octx.path( "undistorted", "reconstruction_%s.nvm" % band['name'].lower()) if not io.file_exists(nvm_file) or self.rerun(): octx.run('export_visualsfm --points --image_list "%s"' % image_list_file) os.rename(tree.opensfm_reconstruction_nvm, nvm_file) else: log.ODM_WARNING( "Found a valid NVM file in %s for %s band" % (nvm_file, band['name'])) 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) self.update_progress(85) # Skip dense reconstruction if necessary and export # sparse reconstruction instead if args.fast_orthophoto: if not io.file_exists(output_file) or self.rerun(): octx.run('export_ply --no-cameras') else: log.ODM_WARNING("Found a valid PLY reconstruction in %s" % output_file) elif args.use_opensfm_dense: if not io.file_exists(output_file) or self.rerun(): octx.run('compute_depthmaps') else: log.ODM_WARNING("Found a valid dense reconstruction in %s" % output_file) self.update_progress(90) if reconstruction.is_georeferenced() and (not io.file_exists( tree.opensfm_transformation) or self.rerun()): octx.run('export_geocoords --transformation --proj \'%s\'' % reconstruction.georef.proj4()) else: log.ODM_WARNING("Will skip exporting %s" % tree.opensfm_transformation) if args.optimize_disk_space: os.remove(octx.path("tracks.csv")) os.remove(octx.path("undistorted", "tracks.csv")) os.remove(octx.path("undistorted", "reconstruction.json")) if io.dir_exists(octx.path("undistorted", "depthmaps")): files = glob.glob( octx.path("undistorted", "depthmaps", "*.npz")) for f in files: os.remove(f)
def process(self, args, outputs): # get inputs tree = outputs['tree'] reconstruction = outputs['reconstruction'] photos = reconstruction.photos if not photos: log.ODM_ERROR('Not enough photos in photos array to start MVE') exit(1) # check if reconstruction was done before if not io.file_exists(tree.mve_model) or self.rerun(): # cleanup if a rerun if io.dir_exists(tree.mve_path) and self.rerun(): shutil.rmtree(tree.mve_path) # make bundle directory if not io.file_exists(tree.mve_bundle): system.mkdir_p(tree.mve_path) system.mkdir_p(io.join_paths(tree.mve_path, 'bundle')) octx = OSFMContext(tree.opensfm) octx.save_absolute_image_list_to(tree.mve_image_list) io.copy(tree.opensfm_bundle, tree.mve_bundle) # mve makescene wants the output directory # to not exists before executing it (otherwise it # will prompt the user for confirmation) if io.dir_exists(tree.mve): shutil.rmtree(tree.mve) # run mve makescene if not io.dir_exists(tree.mve_views): system.run('%s %s %s' % (context.makescene_path, tree.mve_path, tree.mve), env_vars={'OMP_NUM_THREADS': args.max_concurrency}) self.update_progress(10) # Compute mve output scale based on depthmap_resolution max_width = 0 max_height = 0 for photo in photos: max_width = max(photo.width, max_width) max_height = max(photo.height, max_height) max_pixels = args.depthmap_resolution * args.depthmap_resolution if max_width * max_height <= max_pixels: mve_output_scale = 0 else: ratio = float(max_width * max_height) / float(max_pixels) mve_output_scale = int( math.ceil(math.log(ratio) / math.log(4.0))) dmrecon_config = [ "-s%s" % mve_output_scale, "--progress=silent", "--local-neighbors=2", ] # Run MVE's dmrecon log.ODM_INFO( ' ' ) log.ODM_INFO( ' ,*/** ' ) log.ODM_INFO( ' ,*@%*/@%* ' ) log.ODM_INFO( ' ,/@%******@&*. ' ) log.ODM_INFO( ' ,*@&*********/@&* ' ) log.ODM_INFO( ' ,*@&**************@&* ' ) log.ODM_INFO( ' ,/@&******************@&*. ' ) log.ODM_INFO( ' ,*@&*********************/@&* ' ) log.ODM_INFO( ' ,*@&**************************@&*. ' ) log.ODM_INFO( ' ,/@&******************************&&*, ' ) log.ODM_INFO( ' ,*&&**********************************@&*. ' ) log.ODM_INFO( ' ,*@&**************************************@&*. ' ) log.ODM_INFO( ' ,*@&***************#@@@@@@@@@%****************&&*, ' ) log.ODM_INFO( ' .*&&***************&@@@@@@@@@@@@@@****************@@*. ' ) log.ODM_INFO( ' .*@&***************&@@@@@@@@@@@@@@@@@%****(@@%********@@*. ' ) log.ODM_INFO( ' .*@@***************%@@@@@@@@@@@@@@@@@@@@@#****&@@@@%******&@*, ' ) log.ODM_INFO( ' .*&@****************@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@/*****@@*. ' ) log.ODM_INFO( ' .*@@****************@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*************@@*. ' ) log.ODM_INFO( ' .*@@****/***********@@@@@&**(@@@@@@@@@@@@@@@@@@@@@@@#*****************%@*, ' ) log.ODM_INFO( ' */@*******@*******#@@@@%*******/@@@@@@@@@@@@@@@@@@@@********************/@(, ' ) log.ODM_INFO( ' ,*@(********&@@@@@@#**************/@@@@@@@#**(@@&/**********************@&* ' ) log.ODM_INFO( ' *#@/*******************************@@@@@***&@&**********************&@*, ' ) log.ODM_INFO( ' *#@#******************************&@@@***@#*********************&@*, ' ) log.ODM_INFO( ' */@#*****************************@@@************************@@*. ' ) log.ODM_INFO( ' *#@/***************************/@@/*********************%@*, ' ) log.ODM_INFO( ' *#@#**************************#@@%******************%@*, ' ) log.ODM_INFO( ' */@#*************************(@@@@@@@&%/********&@*. ' ) log.ODM_INFO( ' *(@(*********************************/%@@%**%@*, ' ) log.ODM_INFO( ' *(@%************************************%@** ' ) log.ODM_INFO( ' **@%********************************&@*, ' ) log.ODM_INFO( ' *(@(****************************%@/* ' ) log.ODM_INFO( ' ,(@%************************#@/* ' ) log.ODM_INFO( ' ,*@%********************&@/, ' ) log.ODM_INFO( ' */@#****************#@/* ' ) log.ODM_INFO( ' ,/@&************#@/* ' ) log.ODM_INFO( ' ,*@&********%@/, ' ) log.ODM_INFO( ' */@#****(@/* ' ) log.ODM_INFO( ' ,/@@@@(* ' ) log.ODM_INFO( ' .**, ' ) log.ODM_INFO('') log.ODM_INFO( "Running dense reconstruction. This might take a while. Please be patient, the process is not dead or hung." ) log.ODM_INFO(" Process is running") # TODO: find out why MVE is crashing at random # MVE *seems* to have a race condition, triggered randomly, regardless of dataset # https://gist.github.com/pierotofy/6c9ce93194ba510b61e42e3698cfbb89 # Temporary workaround is to retry the reconstruction until we get it right # (up to a certain number of retries). retry_count = 1 while retry_count < 10: try: system.run( '%s %s %s' % (context.dmrecon_path, ' '.join(dmrecon_config), tree.mve), env_vars={'OMP_NUM_THREADS': args.max_concurrency}) break except Exception as e: if str(e) == "Child returned 134" or str( e) == "Child returned 1": retry_count += 1 log.ODM_WARNING( "Caught error code, retrying attempt #%s" % retry_count) else: raise e self.update_progress(90) scene2pset_config = ["-F%s" % mve_output_scale] # run scene2pset system.run('%s %s "%s" "%s"' % (context.scene2pset_path, ' '.join(scene2pset_config), tree.mve, tree.mve_model), env_vars={'OMP_NUM_THREADS': args.max_concurrency}) else: log.ODM_WARNING('Found a valid MVE reconstruction file in: %s' % tree.mve_model)
log.ODM_INFO('Found %s usable images' % len(photos)) from opendm import system system.mkdir_p(os.path.join(submodel_path, 'opensfm')) # Create reconstruction object reconstruction = types.ODM_Reconstruction(photos) opensfm_interface.invent_reference_lla(images_filepath, photo_list,os.path.join(submodel_path, 'opensfm')) system.mkdir_p(os.path.join(submodel_path,'odm_georeferencing')) odm_georeferencing = io.join_paths(submodel_path, 'odm_georeferencing') odm_georeferencing_coords = io.join_paths(odm_georeferencing, 'coords.txt') reconstruction.georeference_with_gps(photos, odm_georeferencing_coords, True) odm_geo_proj = io.join_paths(odm_georeferencing, 'proj.txt') reconstruction.save_proj_srs(odm_geo_proj) from opendm.osfm import OSFMContext octx = OSFMContext(os.path.join(submodel_path, 'opensfm')) print('----------Export geocroods--------') octx.run('export_geocoords --transformation --proj \'%s\'' % reconstruction.georef.proj4()) print('----------Export Geocoords Ppppp--------') lib.odm_mesh_function(opensfm_config,submodel_path, max_concurrency, reconstruction) #lib.odm_mesh_function(submodel_path, max_concurrency) end = timer()
def process(self, args, outputs): tree = outputs['tree'] reconstruction = outputs['reconstruction'] photos = reconstruction.photos if not photos: log.ODM_ERROR('Not enough photos in photos array to start OpenSfM') exit(1) octx = OSFMContext(tree.opensfm) octx.setup(args, tree.dataset_raw, photos, gcp_path=tree.odm_georeferencing_gcp, rerun=self.rerun()) octx.extract_metadata(self.rerun()) self.update_progress(20) octx.feature_matching(self.rerun()) self.update_progress(30) octx.reconstruct(self.rerun()) self.update_progress(70) # 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 return if args.fast_orthophoto: output_file = octx.path('reconstruction.ply') elif args.use_opensfm_dense: output_file = tree.opensfm_model else: output_file = tree.opensfm_reconstruction updated_config_flag_file = octx.path('updated_config.txt') if not io.file_exists(updated_config_flag_file) or self.rerun(): octx.update_config({ 'undistorted_image_max_size': gsd.image_max_size(photos, args.orthophoto_resolution, tree.opensfm_reconstruction, ignore_gsd=args.ignore_gsd) }) octx.touch(updated_config_flag_file) # These will be used for texturing undistorted_images_path = octx.path("undistorted") if not io.dir_exists(undistorted_images_path) or self.rerun(): octx.run('undistort') else: log.ODM_WARNING("Found an undistorted directory in %s" % undistorted_images_path) self.update_progress(80) if not io.file_exists(tree.opensfm_reconstruction_nvm) or self.rerun(): octx.run('export_visualsfm --undistorted') else: log.ODM_WARNING( 'Found a valid OpenSfM NVM reconstruction file in: %s' % tree.opensfm_reconstruction_nvm) self.update_progress(85) # Skip dense reconstruction if necessary and export # sparse reconstruction instead if args.fast_orthophoto: if not io.file_exists(output_file) or self.rerun(): octx.run('export_ply --no-cameras') else: log.ODM_WARNING("Found a valid PLY reconstruction in %s" % output_file) elif args.use_opensfm_dense: if not io.file_exists(output_file) or self.rerun(): octx.run('compute_depthmaps') else: log.ODM_WARNING("Found a valid dense reconstruction in %s" % output_file) # check if reconstruction was exported to bundler before octx.export_bundler(tree.opensfm_bundle_list, self.rerun()) self.update_progress(90) if reconstruction.georef and (not io.file_exists( tree.opensfm_transformation) or self.rerun()): octx.run('export_geocoords --transformation --proj \'%s\'' % reconstruction.georef.projection.srs) else: log.ODM_WARNING("Will skip exporting %s" % tree.opensfm_transformation)
def process(self, args, outputs): # get inputs tree = outputs['tree'] reconstruction = outputs['reconstruction'] photos = reconstruction.photos if not photos: log.ODM_ERROR('Not enough photos in photos array to start OpenMVS') exit(1) # check if reconstruction was done before if not io.file_exists(tree.openmvs_model) or self.rerun(): if self.rerun(): if io.dir_exists(tree.openmvs): shutil.rmtree(tree.openmvs) # export reconstruction from opensfm openmvs_scene_file = os.path.join(tree.openmvs, "scene.mvs") if not io.file_exists(openmvs_scene_file) or self.rerun(): octx = OSFMContext(tree.opensfm) cmd = 'export_openmvs' octx.run(cmd) else: log.ODM_WARNING("Found existing %s" % openmvs_scene_file) self.update_progress(10) depthmaps_dir = os.path.join(tree.openmvs, "depthmaps") if io.dir_exists(depthmaps_dir) and self.rerun(): shutil.rmtree(depthmaps_dir) if not io.dir_exists(depthmaps_dir): os.mkdir(depthmaps_dir) depthmap_resolution = get_depthmap_resolution(args, photos) if outputs["undist_image_max_size"] <= depthmap_resolution: resolution_level = 0 else: resolution_level = int( round( math.log(outputs['undist_image_max_size'] / float(depthmap_resolution)) / math.log(2))) log.ODM_INFO( "Running dense reconstruction. This might take a while.") log.ODM_INFO("Estimating depthmaps") densify_ini_file = os.path.join(tree.openmvs, 'config.ini') with open(densify_ini_file, 'w+') as f: f.write("Optimize = 0\n") # Disable depth-maps re-filtering config = [ " --resolution-level %s" % int(resolution_level), "--min-resolution %s" % depthmap_resolution, "--max-resolution %s" % int(outputs['undist_image_max_size']), "--max-threads %s" % args.max_concurrency, "--number-views-fuse 2", '-w "%s"' % depthmaps_dir, "-v 0" ] if args.pc_tile: config.append("--fusion-mode 1") system.run('%s "%s" %s' % (context.omvs_densify_path, openmvs_scene_file, ' '.join(config))) self.update_progress(85) files_to_remove = [] scene_dense = os.path.join(tree.openmvs, 'scene_dense.mvs') if args.pc_tile: log.ODM_INFO("Computing sub-scenes") config = [ "--sub-scene-area 660000", "--max-threads %s" % args.max_concurrency, '-w "%s"' % depthmaps_dir, "-v 0", ] system.run('%s "%s" %s' % (context.omvs_densify_path, openmvs_scene_file, ' '.join(config))) scene_files = glob.glob( os.path.join(tree.openmvs, "scene_[0-9][0-9][0-9][0-9].mvs")) if len(scene_files) == 0: log.ODM_ERROR( "No OpenMVS scenes found. This could be a bug, or the reconstruction could not be processed." ) exit(1) log.ODM_INFO("Fusing depthmaps for %s scenes" % len(scene_files)) scene_ply_files = [] for sf in scene_files: p, _ = os.path.splitext(sf) scene_ply = p + "_dense_dense_filtered.ply" scene_dense_mvs = p + "_dense.mvs" files_to_remove += [scene_ply, sf, scene_dense_mvs] scene_ply_files.append(scene_ply) if not io.file_exists(scene_ply) or self.rerun(): # Fuse config = [ '--resolution-level %s' % int(resolution_level), '--min-resolution %s' % depthmap_resolution, '--max-resolution %s' % int(outputs['undist_image_max_size']), '--dense-config-file "%s"' % densify_ini_file, '--number-views-fuse 2', '--max-threads %s' % args.max_concurrency, '-w "%s"' % depthmaps_dir, '-v 0', ] try: system.run('%s "%s" %s' % (context.omvs_densify_path, sf, ' '.join(config))) # Filter system.run( '%s "%s" --filter-point-cloud -1 -v 0' % (context.omvs_densify_path, scene_dense_mvs)) except: log.ODM_WARNING( "Sub-scene %s could not be reconstructed, skipping..." % sf) if not io.file_exists(scene_ply): scene_ply_files.pop() log.ODM_WARNING( "Could not compute PLY for subscene %s" % sf) else: log.ODM_WARNING("Found existing dense scene file %s" % scene_ply) # Merge log.ODM_INFO("Merging %s scene files" % len(scene_ply_files)) if len(scene_ply_files) == 0: log.ODM_ERROR( "Could not compute dense point cloud (no PLY files available)." ) if len(scene_ply_files) == 1: # Simply rename os.rename(scene_ply_files[0], tree.openmvs_model) log.ODM_INFO("%s --> %s" % (scene_ply_files[0], tree.openmvs_model)) else: # Merge fast_merge_ply(scene_ply_files, tree.openmvs_model) else: # Filter all at once if os.path.exists(scene_dense): config = [ "--filter-point-cloud -1", '-i "%s"' % scene_dense, "-v 0" ] system.run('%s %s' % (context.omvs_densify_path, ' '.join(config))) else: log.ODM_WARNING( "Cannot find scene_dense.mvs, dense reconstruction probably failed. Exiting..." ) exit(1) # TODO: add support for image masks self.update_progress(95) if args.optimize_disk_space: files = [ scene_dense, os.path.join(tree.openmvs, 'scene_dense.ply'), os.path.join(tree.openmvs, 'scene_dense_dense_filtered.mvs'), octx.path("undistorted", "tracks.csv"), octx.path("undistorted", "reconstruction.json") ] + files_to_remove for f in files: if os.path.exists(f): os.remove(f) shutil.rmtree(depthmaps_dir) else: log.ODM_WARNING( 'Found a valid OpenMVS reconstruction file in: %s' % tree.openmvs_model)
def process(self, args, outputs): # get inputs tree = outputs['tree'] reconstruction = outputs['reconstruction'] photos = reconstruction.photos octx = OSFMContext(tree.opensfm) if not photos: raise system.ExitException('Not enough photos in photos array to start OpenMVS') # check if reconstruction was done before if not io.file_exists(tree.openmvs_model) or self.rerun(): if self.rerun(): if io.dir_exists(tree.openmvs): shutil.rmtree(tree.openmvs) # export reconstruction from opensfm openmvs_scene_file = os.path.join(tree.openmvs, "scene.mvs") if not io.file_exists(openmvs_scene_file) or self.rerun(): cmd = 'export_openmvs' octx.run(cmd) else: log.ODM_WARNING("Found existing %s" % openmvs_scene_file) self.update_progress(10) depthmaps_dir = os.path.join(tree.openmvs, "depthmaps") if io.dir_exists(depthmaps_dir) and self.rerun(): shutil.rmtree(depthmaps_dir) if not io.dir_exists(depthmaps_dir): os.mkdir(depthmaps_dir) depthmap_resolution = get_depthmap_resolution(args, photos) log.ODM_INFO("Depthmap resolution set to: %spx" % depthmap_resolution) if outputs["undist_image_max_size"] <= depthmap_resolution: resolution_level = 0 else: resolution_level = int(round(math.log(outputs['undist_image_max_size'] / float(depthmap_resolution)) / math.log(2))) log.ODM_INFO("Running dense reconstruction. This might take a while.") log.ODM_INFO("Estimating depthmaps") number_views_fuse = 2 densify_ini_file = os.path.join(tree.openmvs, 'Densify.ini') subres_levels = 2 # The number of lower resolutions to process before estimating output resolution depthmap. config = [ " --resolution-level %s" % int(resolution_level), '--dense-config-file "%s"' % densify_ini_file, "--min-resolution %s" % depthmap_resolution, "--max-resolution %s" % int(outputs['undist_image_max_size']), "--max-threads %s" % args.max_concurrency, "--number-views-fuse %s" % number_views_fuse, "--sub-resolution-levels %s" % subres_levels, '-w "%s"' % depthmaps_dir, "-v 0" ] gpu_config = [] if not has_gpu(args): gpu_config.append("--cuda-device -2") if args.pc_tile: config.append("--fusion-mode 1") extra_config = [] if not args.pc_geometric: extra_config.append("--geometric-iters 0") masks_dir = os.path.join(tree.opensfm, "undistorted", "masks") masks = os.path.exists(masks_dir) and len(os.listdir(masks_dir)) > 0 if masks: extra_config.append("--ignore-mask-label 0") sharp = args.pc_geometric with open(densify_ini_file, 'w+') as f: f.write("Optimize = %s\n" % (7 if sharp else 3)) def run_densify(): system.run('"%s" "%s" %s' % (context.omvs_densify_path, openmvs_scene_file, ' '.join(config + gpu_config + extra_config))) try: run_densify() except system.SubprocessException as e: # If the GPU was enabled and the program failed, # try to run it again without GPU if e.errorCode == 1 and len(gpu_config) == 0: log.ODM_WARNING("OpenMVS failed with GPU, is your graphics card driver up to date? Falling back to CPU.") gpu_config.append("--cuda-device -2") run_densify() else: raise e self.update_progress(85) files_to_remove = [] scene_dense = os.path.join(tree.openmvs, 'scene_dense.mvs') if args.pc_tile: log.ODM_INFO("Computing sub-scenes") subscene_densify_ini_file = os.path.join(tree.openmvs, 'subscene-config.ini') with open(subscene_densify_ini_file, 'w+') as f: f.write("Optimize = 0\n") config = [ "--sub-scene-area 660000", "--max-threads %s" % args.max_concurrency, '-w "%s"' % depthmaps_dir, "-v 0", ] system.run('"%s" "%s" %s' % (context.omvs_densify_path, openmvs_scene_file, ' '.join(config + gpu_config))) scene_files = glob.glob(os.path.join(tree.openmvs, "scene_[0-9][0-9][0-9][0-9].mvs")) if len(scene_files) == 0: raise system.ExitException("No OpenMVS scenes found. This could be a bug, or the reconstruction could not be processed.") log.ODM_INFO("Fusing depthmaps for %s scenes" % len(scene_files)) scene_ply_files = [] for sf in scene_files: p, _ = os.path.splitext(sf) scene_ply_unfiltered = p + "_dense.ply" scene_ply = p + "_dense_dense_filtered.ply" scene_dense_mvs = p + "_dense.mvs" files_to_remove += [scene_ply, sf, scene_dense_mvs, scene_ply_unfiltered] scene_ply_files.append(scene_ply) if not io.file_exists(scene_ply) or self.rerun(): # Fuse config = [ '--resolution-level %s' % int(resolution_level), '--min-resolution %s' % depthmap_resolution, '--max-resolution %s' % int(outputs['undist_image_max_size']), '--dense-config-file "%s"' % subscene_densify_ini_file, '--number-views-fuse %s' % number_views_fuse, '--max-threads %s' % args.max_concurrency, '-w "%s"' % depthmaps_dir, '-v 0', ] try: system.run('"%s" "%s" %s' % (context.omvs_densify_path, sf, ' '.join(config + gpu_config + extra_config))) # Filter if args.pc_filter > 0: system.run('"%s" "%s" --filter-point-cloud -1 -v 0 %s' % (context.omvs_densify_path, scene_dense_mvs, ' '.join(gpu_config))) else: # Just rename log.ODM_INFO("Skipped filtering, %s --> %s" % (scene_ply_unfiltered, scene_ply)) os.rename(scene_ply_unfiltered, scene_ply) except: log.ODM_WARNING("Sub-scene %s could not be reconstructed, skipping..." % sf) if not io.file_exists(scene_ply): scene_ply_files.pop() log.ODM_WARNING("Could not compute PLY for subscene %s" % sf) else: log.ODM_WARNING("Found existing dense scene file %s" % scene_ply) # Merge log.ODM_INFO("Merging %s scene files" % len(scene_ply_files)) if len(scene_ply_files) == 0: log.ODM_ERROR("Could not compute dense point cloud (no PLY files available).") if len(scene_ply_files) == 1: # Simply rename os.replace(scene_ply_files[0], tree.openmvs_model) log.ODM_INFO("%s --> %s"% (scene_ply_files[0], tree.openmvs_model)) else: # Merge fast_merge_ply(scene_ply_files, tree.openmvs_model) else: # Filter all at once if args.pc_filter > 0: if os.path.exists(scene_dense): config = [ "--filter-point-cloud -1", '-i "%s"' % scene_dense, "-v 0" ] system.run('"%s" %s' % (context.omvs_densify_path, ' '.join(config + gpu_config + extra_config))) else: raise system.ExitException("Cannot find scene_dense.mvs, dense reconstruction probably failed. Exiting...") else: # Just rename scene_dense_ply = os.path.join(tree.openmvs, 'scene_dense.ply') log.ODM_INFO("Skipped filtering, %s --> %s" % (scene_dense_ply, tree.openmvs_model)) os.rename(scene_dense_ply, tree.openmvs_model) self.update_progress(95) if args.optimize_disk_space: files = [scene_dense, os.path.join(tree.openmvs, 'scene_dense.ply'), os.path.join(tree.openmvs, 'scene_dense_dense_filtered.mvs'), octx.path("undistorted", "tracks.csv"), octx.path("undistorted", "reconstruction.json") ] + files_to_remove for f in files: if os.path.exists(f): os.remove(f) shutil.rmtree(depthmaps_dir) else: log.ODM_WARNING('Found a valid OpenMVS reconstruction file in: %s' % tree.openmvs_model)
def process(self, args, outputs): tree = outputs['tree'] reconstruction = outputs['reconstruction'] photos = reconstruction.photos if not photos: log.ODM_ERROR('Not enough photos in photos array to start OpenSfM') exit(1) octx = OSFMContext(tree.opensfm) octx.setup(args, tree.dataset_raw, photos, gcp_path=tree.odm_georeferencing_gcp, rerun=self.rerun()) octx.extract_metadata(self.rerun()) self.update_progress(20) octx.feature_matching(self.rerun()) self.update_progress(30) octx.reconstruct(self.rerun()) self.update_progress(70) # 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 return if args.fast_orthophoto: output_file = octx.path('reconstruction.ply') elif args.use_opensfm_dense: output_file = tree.opensfm_model else: output_file = tree.opensfm_reconstruction # Always export VisualSFM's reconstruction and undistort images # as we'll use these for texturing (after GSD estimation and resizing) if not args.ignore_gsd: image_scale = gsd.image_scale_factor(args.orthophoto_resolution, tree.opensfm_reconstruction) else: image_scale = 1.0 if not io.file_exists(tree.opensfm_reconstruction_nvm) or self.rerun(): octx.run( 'export_visualsfm --image_extension png --scale_focal %s' % image_scale) else: log.ODM_WARNING( 'Found a valid OpenSfM NVM reconstruction file in: %s' % tree.opensfm_reconstruction_nvm) # These will be used for texturing undistorted_images_path = octx.path("undistorted") if not io.dir_exists(undistorted_images_path) or self.rerun(): octx.run('undistort --image_format png --image_scale %s' % image_scale) else: log.ODM_WARNING("Found an undistorted directory in %s" % undistorted_images_path) self.update_progress(80) # Skip dense reconstruction if necessary and export # sparse reconstruction instead if args.fast_orthophoto: if not io.file_exists(output_file) or self.rerun(): octx.run('export_ply --no-cameras') else: log.ODM_WARNING("Found a valid PLY reconstruction in %s" % output_file) elif args.use_opensfm_dense: # Undistort images at full scale in JPG # (TODO: we could compare the size of the PNGs if they are < than depthmap_resolution # and use those instead of re-exporting full resolution JPGs) if not io.file_exists(output_file) or self.rerun(): octx.run('undistort') octx.run('compute_depthmaps') else: log.ODM_WARNING("Found a valid dense reconstruction in %s" % output_file) # check if reconstruction was exported to bundler before octx.export_bundler(tree.opensfm_bundle_list, self.rerun()) self.update_progress(90) if reconstruction.georef and (not io.file_exists( tree.opensfm_transformation) or self.rerun()): octx.run('export_geocoords --transformation --proj \'%s\'' % reconstruction.georef.projection.srs) else: log.ODM_WARNING("Will skip exporting %s" % tree.opensfm_transformation)