#!/usr/bin/python3 # -*- encoding: utf-8 -*- # # Created by @FlachyJoe """ This script is for an easy use of OpenMVG, COLMAP, and OpenMVS usage: MvgMvs_Pipeline.py [-h] [--steps STEPS [STEPS ...]] [--preset PRESET] [--0 0 [0 ...]] [--1 1 [1 ...]] [--2 2 [2 ...]] [--3 3 [3 ...]] [--4 4 [4 ...]] [--5 5 [5 ...]] [--6 6 [6 ...]] [--7 7 [7 ...]] [--8 8 [8 ...]] [--9 9 [9 ...]] [--10 10 [10 ...]] [--11 11 [11 ...]] [--12 12 [12 ...]] [--13 13 [13 ...]] [--14 14 [14 ...]] [--15 15 [15 ...]] [--16 16 [16 ...]] [--17 17 [17 ...]] [--18 18 [18 ...]] [--19 19 [19 ...]] [--20 20 [20 ...]] [--21 21 [21 ...]] [--22 22 [22 ...]] input_dir output_dir Photogrammetry reconstruction with these steps: 0. Intrinsics analysis openMVG_main_SfMInit_ImageListing 1. Compute features openMVG_main_ComputeFeatures 2. Compute pairs openMVG_main_PairGenerator 3. Compute matches openMVG_main_ComputeMatches 4. Filter matches openMVG_main_GeometricFilter 5. Incremental reconstruction openMVG_main_SfM 6. Global reconstruction openMVG_main_SfM 7. Colorize Structure openMVG_main_ComputeSfM_DataColor 8. Structure from Known Poses openMVG_main_ComputeStructureFromKnownPoses 9. Colorized robust triangulation openMVG_main_ComputeSfM_DataColor 10. Control Points Registration ui_openMVG_control_points_registration 11. Export to openMVS openMVG_main_openMVG2openMVS 12. Feature Extractor colmap 13. Exhaustive Matcher colmap 14. Mapper colmap 15. Image Undistorter colmap 16. Export to openMVS InterfaceCOLMAP 17. Densify point-cloud DensifyPointCloud 18. Reconstruct the mesh ReconstructMesh 19. Refine the mesh RefineMesh 20. Texture the mesh TextureMesh 21. Estimate disparity-maps DensifyPointCloud 22. Fuse disparity-maps DensifyPointCloud positional arguments: input_dir the directory which contains the pictures set. output_dir the directory which will contain the resulting files. optional arguments: -h, --help show this help message and exit --steps STEPS [STEPS ...] steps to process --preset PRESET steps list preset in SEQUENTIAL = [0, 1, 2, 3, 4, 5, 11, 17, 18, 19, 20] GLOBAL = [0, 1, 2, 3, 4, 6, 11, 17, 18, 19, 20] MVG_SEQ = [0, 1, 2, 3, 4, 5, 7, 8, 9, 11] MVG_GLOBAL = [0, 1, 2, 3, 4, 6, 7, 8, 9, 11] COLMAP_MVS = [12, 13, 14, 15, 16, 17, 18, 19, 20] COLMAP = [12, 13, 14, 15, 16] MVS = [17, 18, 19, 20] MVS_SGM = [21, 22] default : SEQUENTIAL Passthrough: Option to be passed to command lines (remove - in front of option names) e.g. --1 p ULTRA to use the ULTRA preset in openMVG_main_ComputeFeatures For example, running the script [MvgMvsPipeline.py input_dir output_dir --steps 0 1 2 3 4 5 11 17 18 20 --1 p HIGH n 8 --3 n HNSWL2] [--steps 0 1 2 3 4 5 11 17 18 20] runs only the desired steps [--1 p HIGH n 8] where --1 refer to openMVG_main_ComputeFeatures, p refers to describerPreset option and set to HIGH, and n refers to numThreads and set to 8. The second step (Compute matches), [--3 n HNSWL2] where --3 refer to openMVG_main_ComputeMatches, n refers to nearest_matching_method option and set to HNSWL2 """ import os import subprocess import sys import argparse # Enable debug mode conditionally DEBUGDEBUG_MODE_ENABLED = False # Define delimiters based on the operating system PATH_DELIMITER = ';' if sys.platform.startswith('win') else ':' FOLDER_DELIMITER = '\\' if sys.platform.startswith('win') else '/' # Append the script's and current working directory's paths to the system PATH script_directory = os.path.dirname(os.path.abspath(__file__)) # add this script's directory to PATH os.environ['PATH'] += PATH_DELIMITER + script_directory # add current directory to PATH os.environ['PATH'] += PATH_DELIMITER + os.getcwd() def locate_executable(afile): """ return directory in which afile is, None if not found. Look in PATH Attempts to find the directory containing the executable specified by 'file_name'. Uses 'where' command on Windows and 'which' on other platforms. Returns the directory path if found, None otherwise. """ command = "where" if sys.platform.startswith('win') else "which" try: process_result = subprocess.run([command, afile], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True) return os.path.split(process_result.stdout.decode())[0] except subprocess.CalledProcessError: return None def find_file_in_path(afile): """ As whereis look only for executable on linux, this find look for all file type Searches for 'file_name' in all directories specified in the PATH environment variable. Returns the first directory containing the file, or None if not found. """ for directory in os.environ['PATH'].split(PATH_DELIMITER): if os.path.isfile(os.path.join(directory, afile)): return directory return None # Attempt to locate binaries for specific software tools openMVG_binary = locate_executable("openMVG_main_SfMInit_ImageListing") colmap_binary = locate_executable("colmap") openMVS_binary = locate_executable("ReconstructMesh") # Try to find openMVG camera sensor database camera_sensor_db_file = "sensor_width_camera_database.txt" camera_sensor_db_directory = find_file_in_path(camera_sensor_db_file) # Prompt user for directories if software binaries or database files weren't found if not openMVG_binary: openMVG_binary = input("Directory for openMVG binaries?\n") if not colmap_binary: colmap_binary = input("Directory for COLMAP binaries?\n") if not openMVS_binary: openMVS_binary = input("Directory for openMVS binaries?\n") if not camera_sensor_db_directory: camera_sensor_db_directory = input(f"Directory for the openMVG camera database ({camera_sensor_db_file})?\n") colmap_binary = os.path.join(colmap_binary, "colmap") # Adjust the binary name for COLMAP on Windows if sys.platform.startswith('win'): colmap_binary += ".bat" # Define presets for various software tools SOFTWARE_PRESETS = {'SEQUENTIAL': [0, 1, 2, 3, 4, 5, 11, 17, 18, 19, 20], 'GLOBAL': [0, 1, 2, 3, 4, 6, 11, 17, 18, 19, 20], 'MVG_SEQ': [0, 1, 2, 3, 4, 5, 7, 8, 9, 11], 'MVG_GLOBAL': [0, 1, 2, 3, 4, 6, 7, 8, 9, 11], 'COLMAP_MVS': [12, 13, 14, 15, 16, 17, 18, 19, 20], 'COLMAP': [12, 13, 14, 15, 16], 'MVS': [17, 18, 19, 20], 'MVS_SGM': [21, 22], 'TEXTURE': [20]} # Default preset selection PRESET_DEFAULT = 'COLMAP' # HELPERS for terminal colors BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) NO_EFFECT, BOLD, UNDERLINE, BLINK, INVERSE, HIDDEN = (0, 1, 4, 5, 7, 8) # from Python cookbook, #475186 def has_colors(stream): ''' Return stream colours capability Checks if the given stream supports colors. Returns True if it does, False otherwise. ''' if not hasattr(stream, "isatty"): return False if not stream.isatty(): return False # auto color only on TTYs try: import curses curses.setupterm() return curses.tigetnum("colors") > 2 except Exception: # guess false in case of error return False HAS_COLORS = has_colors(sys.stdout) def printout(text, colour=WHITE, background=BLACK, effect=NO_EFFECT): """ print() with colour """ if HAS_COLORS: seq = "\x1b[%d;%d;%dm" % (effect, 30+colour, 40+background) + text + "\x1b[0m" sys.stdout.write(seq+'\r\n') else: sys.stdout.write(text+'\r\n') # OBJECTS to store config and data in class ConfigContainer: """ Container for storing configuration variables. """ def __init__(self): pass class ProcessStep: """ Represents a step in the processing pipeline, storing necessary information to execute it. """ def __init__(self, description, command, options): self.info = description self.cmd = command self.opt = options class StepsStore: """ List of steps with facilities to configure them """ def __init__(self): self.steps_data = [ ["Intrinsics analysis", # 0 os.path.join(openMVG_binary, "openMVG_main_SfMInit_ImageListing"), ["-i", "%input_dir%", "-o", "%matches_dir%", "-d", "%camera_file_params%"]], ["Compute features", # 1 os.path.join(openMVG_binary, "openMVG_main_ComputeFeatures"), ["-i", "%matches_dir%"+FOLDER_DELIMITER+"sfm_data.json", "-o", "%matches_dir%", "-m", "SIFT"]], ["Compute pairs", # 2 os.path.join(openMVG_binary, "openMVG_main_PairGenerator"), ["-i", "%matches_dir%"+FOLDER_DELIMITER+"sfm_data.json", "-o", "%matches_dir%"+FOLDER_DELIMITER+"pairs.bin"]], ["Compute matches", # 3 os.path.join(openMVG_binary, "openMVG_main_ComputeMatches"), ["-i", "%matches_dir%"+FOLDER_DELIMITER+"sfm_data.json", "-p", "%matches_dir%"+FOLDER_DELIMITER+"pairs.bin", "-o", "%matches_dir%"+FOLDER_DELIMITER+"matches.putative.bin", "-n", "AUTO"]], ["Filter matches", # 4 os.path.join(openMVG_binary, "openMVG_main_GeometricFilter"), ["-i", "%matches_dir%"+FOLDER_DELIMITER+"sfm_data.json", "-m", "%matches_dir%"+FOLDER_DELIMITER+"matches.putative.bin", "-o", "%matches_dir%"+FOLDER_DELIMITER+"matches.f.bin"]], ["Incremental reconstruction", # 5 os.path.join(openMVG_binary, "openMVG_main_SfM"), ["-i", "%matches_dir%"+FOLDER_DELIMITER+"sfm_data.json", "-m", "%matches_dir%", "-o", "%reconstruction_dir%", "-s", "INCREMENTAL"]], ["Global reconstruction", # 6 os.path.join(openMVG_binary, "openMVG_main_SfM"), ["-i", "%matches_dir%"+FOLDER_DELIMITER+"sfm_data.json", "-m", "%matches_dir%", "-o", "%reconstruction_dir%", "-s", "GLOBAL", "-M", "%matches_dir%"+FOLDER_DELIMITER+"matches.e.bin"]], ["Colorize Structure", # 7 os.path.join(openMVG_binary, "openMVG_main_ComputeSfM_DataColor"), ["-i", "%reconstruction_dir%"+FOLDER_DELIMITER+"sfm_data.bin", "-o", "%reconstruction_dir%"+FOLDER_DELIMITER+"colorized.ply"]], ["Structure from Known Poses", # 8 os.path.join(openMVG_binary, "openMVG_main_ComputeStructureFromKnownPoses"), ["-i", "%reconstruction_dir%"+FOLDER_DELIMITER+"sfm_data.bin", "-m", "%matches_dir%", "-f", "%matches_dir%"+FOLDER_DELIMITER+"matches.f.bin", "-o", "%reconstruction_dir%"+FOLDER_DELIMITER+"robust.bin"]], ["Colorized robust triangulation", # 9 os.path.join(openMVG_binary, "openMVG_main_ComputeSfM_DataColor"), ["-i", "%reconstruction_dir%"+FOLDER_DELIMITER+"robust.bin", "-o", "%reconstruction_dir%"+FOLDER_DELIMITER+"robust_colorized.ply"]], ["Control Points Registration", # 10 os.path.join(openMVG_binary, "ui_openMVG_control_points_registration"), ["-i", "%reconstruction_dir%"+FOLDER_DELIMITER+"sfm_data.bin"]], ["Export to openMVS", # 11 os.path.join(openMVG_binary, "openMVG_main_openMVG2openMVS"), ["-i", "%reconstruction_dir%"+FOLDER_DELIMITER+"sfm_data.bin", "-o", "%mvs_dir%"+FOLDER_DELIMITER+"scene.mvs", "-d", "%mvs_dir%"+FOLDER_DELIMITER+"images"]], ["Feature Extractor", # 12 colmap_binary, ["feature_extractor", "--database_path", "%matches_dir%"+FOLDER_DELIMITER+"database.db", "--image_path", "%input_dir%"]], ["Exhaustive Matcher", # 13 colmap_binary, ["exhaustive_matcher", "--database_path", "%matches_dir%"+FOLDER_DELIMITER+"database.db"]], ["Mapper", # 14 colmap_binary, ["mapper", "--database_path", "%matches_dir%"+FOLDER_DELIMITER+"database.db", "--image_path", "%input_dir%", "--output_path", "%reconstruction_dir%"]], ["Image Undistorter", # 15 colmap_binary, ["image_undistorter", "--image_path", "%input_dir%", "--input_path", "%reconstruction_dir%"+FOLDER_DELIMITER+"0", "--output_path", "%reconstruction_dir%"+FOLDER_DELIMITER+"dense", "--output_type", "COLMAP"]], ["Export to openMVS", # 16 os.path.join(openMVS_binary, "InterfaceCOLMAP"), ["-i", "%reconstruction_dir%"+FOLDER_DELIMITER+"dense", "-o", "scene.mvs", "--image-folder", "%reconstruction_dir%"+FOLDER_DELIMITER+"dense"+FOLDER_DELIMITER+"images", "-w", "\"%mvs_dir%\""]], ["Densify point cloud", # 17 os.path.join(openMVS_binary, "DensifyPointCloud"), ["scene.mvs", "--dense-config-file", "Densify.ini", "--resolution-level", "1", "--number-views", "8", "-w", "\"%mvs_dir%\""]], ["Reconstruct the mesh", # 18 os.path.join(openMVS_binary, "ReconstructMesh"), ["scene_dense.mvs", "-p", "scene_dense.ply", "-w", "\"%mvs_dir%\""]], ["Refine the mesh", # 19 os.path.join(openMVS_binary, "RefineMesh"), ["scene_dense.mvs", "-m", "scene_dense_mesh.ply", "-o", "scene_dense_mesh_refine.mvs", "--scales", "1", "--gradient-step", "25.05", "-w", "\"%mvs_dir%\""]], ["Texture the mesh", # 20 os.path.join(openMVS_binary, "TextureMesh"), ["scene_dense.mvs", "-m", "scene_dense_mesh_refine.ply", "-o", "scene_dense_mesh_refine_texture.mvs", "--decimate", "0.5", "-w", "\"%mvs_dir%\""]], ["Estimate disparity-maps", # 21 os.path.join(openMVS_binary, "DensifyPointCloud"), ["scene.mvs", "--dense-config-file", "Densify.ini", "--fusion-mode", "-1", "-w", "\"%mvs_dir%\""]], ["Fuse disparity-maps", # 22 os.path.join(openMVS_binary, "DensifyPointCloud"), ["scene.mvs", "--dense-config-file", "Densify.ini", "--fusion-mode", "-2", "-w", "\"%mvs_dir%\""]] ] def __getitem__(self, indice): return ProcessStep(*self.steps_data[indice]) def length(self): return len(self.steps_data) def configure_steps(self, conf): """ replace each %var% per conf.var value in steps data """ for step in self.steps_data: updated_options = [] for option in step[2]: configured_option = option.replace("%input_dir%", conf.input_dir) configured_option = configured_option.replace("%output_dir%", conf.output_dir) configured_option = configured_option.replace("%matches_dir%", conf.matches_dir) configured_option = configured_option.replace("%reconstruction_dir%", conf.reconstruction_dir) configured_option = configured_option.replace("%mvs_dir%", conf.mvs_dir) configured_option = configured_option.replace("%camera_file_params%", conf.camera_file_params) updated_options.append(configured_option) step[2] = updated_options def replace_option(self, idx, str_exist, str_new): """ replace each existing str_exist with str_new per opt value in step idx data """ step = self.steps_data[idx] updated_options = [] for option in step[2]: configured_option = option.replace(str_exist, str_new) updated_options.append(configured_option) step[2] = updated_options CONF = ConfigContainer() STEPS = StepsStore() # ARGS PARSER = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter, description="Photogrammetry reconstruction with these steps: \r\n" + "\r\n".join(("\t%i. %s\t %s" % (t, STEPS[t].info, STEPS[t].cmd) for t in range(STEPS.length()))) ) PARSER.add_argument('input_dir', help="the directory which contains the pictures set.") PARSER.add_argument('output_dir', help="the directory which will contain the resulting files.") PARSER.add_argument('--steps', type=int, nargs="+", help="Specify steps to process by index.") PARSER.add_argument('--preset', help="steps list preset in \r\n" + " \r\n".join([k + " = " + str(SOFTWARE_PRESETS[k]) for k in SOFTWARE_PRESETS]) + " \r\ndefault : " + PRESET_DEFAULT) GROUP = PARSER.add_argument_group('Passthrough', description="Option to be passed to command lines (remove - in front of option names)\r\ne.g. --1 p ULTRA to use the ULTRA preset in openMVG_main_ComputeFeatures\r\nFor example, running the script as follows,\r\nMvgMvsPipeline.py input_dir output_dir --1 p HIGH n 8 --3 n ANNL2\r\nwhere --1 refer to openMVG_main_ComputeFeatures, p refers to\r\ndescriberPreset option which HIGH was chosen, and n refers to\r\nnumThreads which 8 was used. --3 refer to second step (openMVG_main_ComputeMatches),\r\nn refers to nearest_matching_method option which ANNL2 was chosen") for n in range(STEPS.length()): GROUP.add_argument('--'+str(n), nargs='+') PARSER.parse_args(namespace=CONF) # store args in the ConfContainer # FOLDERS # Method to ensure a directory exists; creates it if it does not def ensure_directory_exists(dirname): """Create the folder if not presents""" if not os.path.exists(dirname): os.mkdir(dirname) # Absolute path for input and output dirs CONF.input_dir = os.path.abspath(CONF.input_dir) CONF.output_dir = os.path.abspath(CONF.output_dir) if not os.path.exists(CONF.input_dir): sys.exit("%s: path not found" % CONF.input_dir) CONF.reconstruction_dir = os.path.join(CONF.output_dir, "sfm") CONF.matches_dir = os.path.join(CONF.reconstruction_dir, "matches") CONF.mvs_dir = os.path.join(CONF.output_dir, "mvs") CONF.camera_file_params = os.path.join(camera_sensor_db_directory, camera_sensor_db_file) ensure_directory_exists(CONF.output_dir) ensure_directory_exists(CONF.reconstruction_dir) ensure_directory_exists(CONF.matches_dir) ensure_directory_exists(CONF.mvs_dir) # Update directories in steps commandlines STEPS.configure_steps(CONF) # PRESET if CONF.steps and CONF.preset: sys.exit("Steps and preset arguments can't be set together.") elif CONF.preset: try: CONF.steps = SOFTWARE_PRESETS[CONF.preset] except KeyError: sys.exit("Unknown preset %s, choose %s" % (CONF.preset, ' or '.join([s for s in SOFTWARE_PRESETS]))) elif not CONF.steps: CONF.steps = SOFTWARE_PRESETS[PRESET_DEFAULT] # WALK print("# Using input dir: %s" % CONF.input_dir) print("# output dir: %s" % CONF.output_dir) print("# Steps: %s" % str(CONF.steps)) if 4 in CONF.steps: # GeometricFilter if 6 in CONF.steps: # GlobalReconstruction # Set the geometric_model of ComputeMatches to Essential STEPS.replace_option(4, FOLDER_DELIMITER+"matches.f.bin", FOLDER_DELIMITER+"matches.e.bin") STEPS[4].opt.extend(["-g", "e"]) if 20 in CONF.steps: # TextureMesh if 19 not in CONF.steps: # RefineMesh # RefineMesh step is not run, use ReconstructMesh output STEPS.replace_option(20, "scene_dense_mesh_refine.ply", "scene_dense_mesh.ply") STEPS.replace_option(20, "scene_dense_mesh_refine_texture.mvs", "scene_dense_mesh_texture.mvs") for cstep in CONF.steps: printout("#%i. %s" % (cstep, STEPS[cstep].info), effect=INVERSE) # Retrieve "passthrough" commandline options options = getattr(CONF, str(cstep)) if options: # add - sign to short options and -- to long ones for o in range(0, len(options), 2): if len(options[o]) > 1: options[o] = '-' + options[o] options[o] = '-' + options[o] else: options = [] # Remove STEPS[cstep].opt options now defined in opt for anOpt in STEPS[cstep].opt: if anOpt in options: idx = STEPS[cstep].opt.index(anOpt) if DEBUGDEBUG_MODE_ENABLED: print('#\tRemove ' + str(anOpt) + ' from defaults options at id ' + str(idx)) del STEPS[cstep].opt[idx:idx+2] # create a commandline for the current step cmdline = [STEPS[cstep].cmd] + STEPS[cstep].opt + options print('CMD: ' + ' '.join(cmdline)) if not DEBUGDEBUG_MODE_ENABLED: # Launch the current step try: pStep = subprocess.Popen(cmdline) pStep.wait() if pStep.returncode != 0: break except KeyboardInterrupt: sys.exit('\r\nProcess canceled by user, all files remains') else: print('\t'.join(cmdline)) printout("# Pipeline end #", effect=INVERSE)