From a4d7554c0b53ae54b8edf6a3926d4d6150825069 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 12 Sep 2019 21:06:41 +0200 Subject: [PATCH 01/12] [bin] improve meshroom_photogrammetry command line: -i, -o, change --save, add --compute - add short names for input/output - change --save option which allow to save the graph used but now do not replace the computation - new option to disable the computation (makes sense only if you want to only save the created project) --- bin/meshroom_photogrammetry | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index dae5183b07..c03e2018e6 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -2,6 +2,7 @@ import argparse import os import sys +import distutils.util import meshroom meshroom.setupEnvironment() @@ -10,7 +11,7 @@ import meshroom.core.graph from meshroom import multiview parser = argparse.ArgumentParser(description='Launch the full photogrammetry pipeline.') -parser.add_argument('--input', metavar='FOLDER_OR_SFM', type=str, +parser.add_argument('-i', '--input', metavar='FOLDER_OR_SFM', type=str, default='', help='Input folder containing images or file (.sfm or .json) ' 'with images paths and optionally predefined camera intrinsics.') @@ -27,7 +28,7 @@ parser.add_argument('--pipeline', metavar='MESHROOM_FILE', type=str, required=Fa parser.add_argument('--overrides', metavar='SETTINGS', type=str, default=None, help='A JSON file containing the graph parameters override.') -parser.add_argument('--output', metavar='FOLDER', type=str, required=False, +parser.add_argument('-o', '--output', metavar='FOLDER', type=str, required=False, help='Output folder where results should be copied to. ' 'If not set, results will have to be retrieved directly from the cache folder.') @@ -37,7 +38,10 @@ parser.add_argument('--cache', metavar='FOLDER', type=str, 'If not set, the default cache folder will be used: ' + meshroom.core.defaultCacheFolder) parser.add_argument('--save', metavar='FILE', type=str, required=False, - help='Save the configured Meshroom project to a file (instead of running it).') + help='Save the configured Meshroom project to a file.') + +parser.add_argument('--compute', metavar='', type=lambda x: bool(distutils.util.strtobool(x)), default=True, required=False, + help='You can set it to to disable the computation.') parser.add_argument('--scale', type=int, default=-1, choices=[-1, 1, 2, 4, 8, 16], @@ -129,16 +133,16 @@ if args.scale > 0: if args.save: graph.save(args.save) print('File successfully saved:', args.save) - sys.exit(0) # setup cache directory graph.cacheDir = args.cache if args.cache else meshroom.core.defaultCacheFolder if not args.output: - print('No output set, results will be available in {}'.format(graph.cacheDir)) + print('No output set, results will be available in the cache folder: "{}"'.format(graph.cacheDir)) # find end nodes (None will compute all graph) toNodes = graph.findNodes(args.toNode) if args.toNode else None -# start computation -meshroom.core.graph.executeGraph(graph, toNodes=toNodes, forceCompute=args.forceCompute, forceStatus=args.forceStatus) +if args.compute: + # start computation + meshroom.core.graph.executeGraph(graph, toNodes=toNodes, forceCompute=args.forceCompute, forceStatus=args.forceStatus) From 231125efdf249868af68515a3db5cff8e9a0f38b Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 12 Sep 2019 21:14:52 +0200 Subject: [PATCH 02/12] [ui] improve meshroom command line - "meshroom -h / --help": now provides command line documentation (so argparse is done before GUI creation) - new positional argument to set project file or input folder - new "--pipeline" option to set a default pipeline, also support an environment variable --- meshroom/core/graph.py | 15 ++++++++++++--- meshroom/ui/app.py | 34 +++++++++++++++++++++++++++++----- meshroom/ui/graph.py | 9 +++++++-- meshroom/ui/reconstruction.py | 23 ++++++++++++++++++----- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 0427b33fd5..2b7fe3e471 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -234,7 +234,15 @@ def fileFeatures(self): return Graph.IO.getFeaturesForVersion(self.header.get(Graph.IO.Keys.FileVersion, "0.0")) @Slot(str) - def load(self, filepath): + def load(self, filepath, fileLink=True): + """ + Load a meshroom graph ".mg" file. + + Args: + filepath: project filepath to load + fileLink: Setup link to the project file, like setup cacheDir, keep filepath for save, etc. + This option allows to disable it, to only load the project file as a template + """ self.clear() with open(filepath) as jsonFile: fileData = json.load(jsonFile) @@ -265,8 +273,9 @@ def load(self, filepath): # Add node to the graph with raw attributes values self._addNode(n, nodeName) - # Update filepath related members - self._setFilepath(filepath) + if fileLink: + # Update filepath related members + self._setFilepath(filepath) # Create graph edges by resolving attributes expressions self._applyExpr() diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 6ad13d3171..266045442b 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -56,6 +56,18 @@ class MeshroomApp(QApplication): """ Meshroom UI Application. """ def __init__(self, args): QtArgs = [args[0], '-style', 'fusion'] + args[1:] # force Fusion style by default + + parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.', add_help=True) + + parser.add_argument('input', metavar='INPUT', type=str, nargs='?', + help='Meshroom project file (e.g. myProject.mg) or folder with images to reconstruct.') + parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False, + help='Meshroom project file (e.g. myProject.mg).') + parser.add_argument('--pipeline', metavar='MESHROOM_FILE', type=str, required=False, + help='Override the default Meshroom pipeline with this external graph.') + + args = parser.parse_args(args[1:]) + super(MeshroomApp, self).__init__(QtArgs) self.setOrganizationName('AliceVision') @@ -105,12 +117,24 @@ def __init__(self, args): # request any potential computation to stop on exit self.aboutToQuit.connect(r.stopChildThreads) - parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.') - parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False, - help='Meshroom project file (e.g. myProject.mg).') - args = parser.parse_args(args[1:]) + if args.pipeline: + # the pipeline from the command line has the priority + r.setDefaultPipeline(args.pipeline) + else: + # consider the environment variable + defaultPipeline = os.environ.get("MESHROOM_DEFAULT_PIPELINE", "") + if defaultPipeline: + r.setDefaultPipeline(args.pipeline) + if args.project: - r.loadUrl(QUrl.fromLocalFile(args.project)) + r.load(args.project) + + if args.input: + if os.path.isfile(args.input): + # we assume that it is an ".mg" file. + r.load(args.input) + elif os.path.isdir(args.input): + r.importImagesFromFolder(args.input) self.engine.load(os.path.normpath(url)) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 952713fe6c..3042e115f7 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -254,6 +254,7 @@ def __init__(self, filepath='', parent=None): self._layout = GraphLayout(self) self._selectedNode = None self._hoveredNode = None + self._defaultPipelineFilepath = None if filepath: self.load(filepath) @@ -310,9 +311,13 @@ def stopChildThreads(self): self.stopExecution() self._chunksMonitor.stop() - def load(self, filepath): + def setDefaultPipeline(self, pipelineFilepath): + self._defaultPipelineFilepath = pipelineFilepath + self._graph.load(pipelineFilepath, fileLink=False) + + def load(self, filepath, fileLink=True): g = Graph('') - g.load(filepath) + g.load(filepath, fileLink) if not os.path.exists(g.cacheDir): os.mkdir(g.cacheDir) self.setGraph(g) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 9ef09dadf4..dc2b826019 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -195,11 +195,16 @@ def __init__(self, graphFilepath='', parent=None): @Slot() def new(self): """ Create a new photogrammetry pipeline. """ - self.setGraph(multiview.photogrammetry()) + if self._defaultPipelineFilepath: + # use the user-provided default photogrammetry project file + self.load(self._defaultPipelineFilepath, fileLink=False) + else: + # default photogrammetry pipeline + self.setGraph(multiview.photogrammetry()) - def load(self, filepath): + def load(self, filepath, fileLink=True): try: - super(Reconstruction, self).load(filepath) + super(Reconstruction, self).load(filepath, fileLink) # warn about pre-release projects being automatically upgraded if Version(self._graph.fileReleaseVersion).major == "0": self.warning.emit(Message( @@ -344,7 +349,7 @@ def handleFilesDrop(self, drop, cameraInit): Fetching urls from dropEvent is generally expensive in QML/JS (bug ?). This method allows to reduce process time by doing it on Python side. """ - self.importImages(self.getImageFilesFromDrop(drop), cameraInit) + self.importImagesAsync(self.getImageFilesFromDrop(drop), cameraInit) @staticmethod def getImageFilesFromDrop(drop): @@ -359,7 +364,15 @@ def getImageFilesFromDrop(drop): images.append(localFile) return images - def importImages(self, images, cameraInit): + def importImagesFromFolder(self, path): + images = [] + if os.path.isdir(path): # get folder content + images.extend(multiview.findImageFiles(path)) + elif multiview.isImageFile(path): + images.append(path) + self.buildIntrinsics(self.cameraInit, images) + + def importImagesAsync(self, images, cameraInit): """ Add the given list of images to the Reconstruction. """ # Start the process of updating views and intrinsics self.runAsync(self.buildIntrinsics, args=(cameraInit, images,)) From ad9a2153c564017180db969bdee9995d26a4187a Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 12 Sep 2019 21:18:11 +0200 Subject: [PATCH 03/12] [core] use logging for plugins loaded message --- meshroom/core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/core/__init__.py b/meshroom/core/__init__.py index 20b5f774c8..1951198623 100644 --- a/meshroom/core/__init__.py +++ b/meshroom/core/__init__.py @@ -240,7 +240,7 @@ def loadAllNodes(folder): nodeTypes = loadNodes(folder, package) for nodeType in nodeTypes: registerNodeType(nodeType) - print('Plugins loaded: ', ', '.join([nodeType.__name__ for nodeType in nodeTypes])) + logging.debug('Plugins loaded: ', ', '.join([nodeType.__name__ for nodeType in nodeTypes])) def registerSubmitter(s): From ee2fc63bd5e0658f98b7300d69c5b3340d6d86af Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 12 Sep 2019 21:20:00 +0200 Subject: [PATCH 04/12] [multiview] use a single GraphModification when creating the default photogrammetry pipeline --- meshroom/multiview.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index 1474b222d2..fcc2c13083 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -47,11 +47,12 @@ def photogrammetry(inputImages=list(), inputViewpoints=list(), inputIntrinsics=l cameraInit.viewpoints.extend(inputViewpoints) cameraInit.intrinsics.extend(inputIntrinsics) - if output: - texturing = mvsNodes[-1] - graph.addNewNode('Publish', output=output, inputFiles=[texturing.outputMesh, - texturing.outputMaterial, - texturing.outputTextures]) + if output: + texturing = mvsNodes[-1] + graph.addNewNode('Publish', output=output, inputFiles=[texturing.outputMesh, + texturing.outputMaterial, + texturing.outputTextures]) + return graph From 979ee4ba940a2767844352d2abf5eec3a24d39e4 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 12 Sep 2019 21:36:36 +0200 Subject: [PATCH 05/12] [bin] meshroom_photogrammetry: setup cache folder according to --save path except if --cache is set explicitly --- bin/meshroom_photogrammetry | 10 +++++----- meshroom/core/graph.py | 14 +++++++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index c03e2018e6..0fdb7974d3 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -38,7 +38,7 @@ parser.add_argument('--cache', metavar='FOLDER', type=str, 'If not set, the default cache folder will be used: ' + meshroom.core.defaultCacheFolder) parser.add_argument('--save', metavar='FILE', type=str, required=False, - help='Save the configured Meshroom project to a file.') + help='Save the configured Meshroom graph to a project file. It will setup the cache folder accordingly if not explicitly changed by --cache.') parser.add_argument('--compute', metavar='', type=lambda x: bool(distutils.util.strtobool(x)), default=True, required=False, help='You can set it to to disable the computation.') @@ -130,13 +130,13 @@ if args.scale > 0: for node in graph.nodesByType('DepthMap'): node.downscale.value = args.scale -if args.save: - graph.save(args.save) - print('File successfully saved:', args.save) - # setup cache directory graph.cacheDir = args.cache if args.cache else meshroom.core.defaultCacheFolder +if args.save: + graph.save(args.save, fileLink=not bool(args.cache)) + print('File successfully saved: "{}"'.format(args.save)) + if not args.output: print('No output set, results will be available in the cache folder: "{}"'.format(graph.cacheDir)) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 2b7fe3e471..6ac38e4c61 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -905,7 +905,7 @@ def toDict(self): def asString(self): return str(self.toDict()) - def save(self, filepath=None): + def save(self, filepath=None, fileLink=True): path = filepath or self._filepath if not path: raise ValueError("filepath must be specified for unsaved files.") @@ -929,7 +929,7 @@ def save(self, filepath=None): with open(path, 'w') as jsonFile: json.dump(data, jsonFile, indent=4) - if path != self._filepath: + if path != self._filepath and fileLink: self._setFilepath(path) def _setFilepath(self, filepath): @@ -939,7 +939,9 @@ def _setFilepath(self, filepath): Args: filepath: the graph file path """ - assert os.path.isfile(filepath) + if not os.path.isfile(filepath): + self._unsetFilepath() + return if self._filepath == filepath: return @@ -951,6 +953,12 @@ def _setFilepath(self, filepath): self.cacheDir = os.path.join(os.path.abspath(os.path.dirname(filepath)), meshroom.core.cacheFolderName) self.filepathChanged.emit() + def _unsetFilepath(self): + self._filepath = "" + self.name = "" + self.cacheDir = meshroom.core.defaultCacheFolder + self.filepathChanged.emit() + def updateInternals(self, startNodes=None, force=False): nodes, edges = self.dfsOnFinish(startNodes=startNodes) for node in nodes: From fa7b4587ca32424741e47e3f6c14851f72065109 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sat, 21 Sep 2019 18:04:26 +0200 Subject: [PATCH 06/12] [ui] meshroom: improve import command line arguments - explicit error messages - allow to combine multiple inputs with new --import and --importRecursive options --- meshroom/multiview.py | 12 ++++++++++-- meshroom/ui/app.py | 30 +++++++++++++++++++++++++++--- meshroom/ui/reconstruction.py | 26 ++++++++++++++++++++------ 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/meshroom/multiview.py b/meshroom/multiview.py index fcc2c13083..d7323059fe 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -14,7 +14,7 @@ def isImageFile(filepath): return os.path.splitext(filepath)[1].lower() in imageExtensions -def findImageFiles(folder): +def findImageFiles(folder, recursive=False): """ Return all files that are images in 'folder' based on their extensions. @@ -24,7 +24,15 @@ def findImageFiles(folder): Returns: list: the list of image files. """ - return [os.path.join(folder, filename) for filename in os.listdir(folder) if isImageFile(filename)] + if recursive: + output = [] + for root, directories, files in os.walk(folder): + for filename in files: + if isImageFile(filename): + output.append(os.path.join(root, filename)) + return output + else: + return [os.path.join(folder, filename) for filename in os.listdir(folder) if isImageFile(filename)] def photogrammetry(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), output=''): diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 266045442b..817375ea0b 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -61,9 +61,13 @@ def __init__(self, args): parser.add_argument('input', metavar='INPUT', type=str, nargs='?', help='Meshroom project file (e.g. myProject.mg) or folder with images to reconstruct.') - parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False, + parser.add_argument('-i', '--import', metavar='IMAGES/FOLDERS', type=str, nargs='*', + help='Import images or folder with images to reconstruct.') + parser.add_argument('-I', '--importRecursive', metavar='FOLDERS', type=str, nargs='*', + help='Import images to reconstruct from specified folder and sub-folders.') + parser.add_argument('-p', '--project', metavar='MESHROOM_FILE', type=str, required=False, help='Meshroom project file (e.g. myProject.mg).') - parser.add_argument('--pipeline', metavar='MESHROOM_FILE', type=str, required=False, + parser.add_argument('-P', '--pipeline', metavar='MESHROOM_FILE', type=str, required=False, help='Override the default Meshroom pipeline with this external graph.') args = parser.parse_args(args[1:]) @@ -126,6 +130,19 @@ def __init__(self, args): if defaultPipeline: r.setDefaultPipeline(args.pipeline) + if args.input: + if not os.path.isfile(args.input) and not os.path.isdir(args.input): + raise RuntimeError( + "Meshroom Command Line Error: 'INPUT' argument should be a Meshroom project file (.mg) or a folder with input images.\n" + "Invalid value: '{}'".format(args.input)) + if args.project and not os.path.isfile(args.project): + raise RuntimeError( + "Meshroom Command Line Error: '--project' argument should be a Meshroom project file (.mg).\n" + "Invalid value: '{}'".format(args.project)) + + if args.project and args.input and not os.path.isdir(args.input): + raise RuntimeError("Meshroom Command Line Error: 'INPUT' and '--project' arguments cannot both load a Meshroom project file (.mg).") + if args.project: r.load(args.project) @@ -134,7 +151,14 @@ def __init__(self, args): # we assume that it is an ".mg" file. r.load(args.input) elif os.path.isdir(args.input): - r.importImagesFromFolder(args.input) + r.importImagesFromFolder(args.input, recursive=False) + + # import is a python keyword, so we have to access the attribute by a string + if getattr(args, "import", None): + r.importImagesFromFolder(getattr(args, "import"), recursive=False) + + if args.importRecursive: + r.importImagesFromFolder(args.importRecursive, recursive=True) self.engine.load(os.path.normpath(url)) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index dc2b826019..b885248080 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -364,13 +364,27 @@ def getImageFilesFromDrop(drop): images.append(localFile) return images - def importImagesFromFolder(self, path): + def importImagesFromFolder(self, path, recursive=False): + """ + + Args: + path: A path to a folder or file or a list of files/folders + recursive: List files in folders recursively. + + """ images = [] - if os.path.isdir(path): # get folder content - images.extend(multiview.findImageFiles(path)) - elif multiview.isImageFile(path): - images.append(path) - self.buildIntrinsics(self.cameraInit, images) + paths = [] + if isinstance(path, (list, tuple)): + paths = path + else: + paths.append(path) + for p in paths: + if os.path.isdir(p): # get folder content + images.extend(multiview.findImageFiles(p, recursive)) + elif multiview.isImageFile(p): + images.append(p) + if images: + self.buildIntrinsics(self.cameraInit, images) def importImagesAsync(self, images, cameraInit): """ Add the given list of images to the Reconstruction. """ From 06543f89a95e8f1220a636fc7af4088b5026fce5 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Sat, 21 Sep 2019 18:19:17 +0200 Subject: [PATCH 07/12] [core] use more explicit argument name: setupFileRef --- bin/meshroom_photogrammetry | 2 +- meshroom/core/graph.py | 12 ++++++------ meshroom/ui/graph.py | 6 +++--- meshroom/ui/reconstruction.py | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 0fdb7974d3..050e0e20e0 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -134,7 +134,7 @@ if args.scale > 0: graph.cacheDir = args.cache if args.cache else meshroom.core.defaultCacheFolder if args.save: - graph.save(args.save, fileLink=not bool(args.cache)) + graph.save(args.save, setupFileRef=not bool(args.cache)) print('File successfully saved: "{}"'.format(args.save)) if not args.output: diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 6ac38e4c61..4f854c7ae4 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -234,14 +234,14 @@ def fileFeatures(self): return Graph.IO.getFeaturesForVersion(self.header.get(Graph.IO.Keys.FileVersion, "0.0")) @Slot(str) - def load(self, filepath, fileLink=True): + def load(self, filepath, setupFileRef=True): """ Load a meshroom graph ".mg" file. Args: filepath: project filepath to load - fileLink: Setup link to the project file, like setup cacheDir, keep filepath for save, etc. - This option allows to disable it, to only load the project file as a template + setupFileRef: Setup reference to the project file, like setup cacheDir, keep filepath for save, etc. + This option allows to disable it, to only load the project file as a template. """ self.clear() with open(filepath) as jsonFile: @@ -273,7 +273,7 @@ def load(self, filepath, fileLink=True): # Add node to the graph with raw attributes values self._addNode(n, nodeName) - if fileLink: + if setupFileRef: # Update filepath related members self._setFilepath(filepath) @@ -905,7 +905,7 @@ def toDict(self): def asString(self): return str(self.toDict()) - def save(self, filepath=None, fileLink=True): + def save(self, filepath=None, setupFileRef=True): path = filepath or self._filepath if not path: raise ValueError("filepath must be specified for unsaved files.") @@ -929,7 +929,7 @@ def save(self, filepath=None, fileLink=True): with open(path, 'w') as jsonFile: json.dump(data, jsonFile, indent=4) - if path != self._filepath and fileLink: + if path != self._filepath and setupFileRef: self._setFilepath(path) def _setFilepath(self, filepath): diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 3042e115f7..d46a2469e2 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -313,11 +313,11 @@ def stopChildThreads(self): def setDefaultPipeline(self, pipelineFilepath): self._defaultPipelineFilepath = pipelineFilepath - self._graph.load(pipelineFilepath, fileLink=False) + self._graph.load(pipelineFilepath, setupFileRef=False) - def load(self, filepath, fileLink=True): + def load(self, filepath, setupFileRef=True): g = Graph('') - g.load(filepath, fileLink) + g.load(filepath, setupFileRef) if not os.path.exists(g.cacheDir): os.mkdir(g.cacheDir) self.setGraph(g) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index b885248080..ad43b473aa 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -197,14 +197,14 @@ def new(self): """ Create a new photogrammetry pipeline. """ if self._defaultPipelineFilepath: # use the user-provided default photogrammetry project file - self.load(self._defaultPipelineFilepath, fileLink=False) + self.load(self._defaultPipelineFilepath, setupFileRef=False) else: # default photogrammetry pipeline self.setGraph(multiview.photogrammetry()) - def load(self, filepath, fileLink=True): + def load(self, filepath, setupFileRef=True): try: - super(Reconstruction, self).load(filepath, fileLink) + super(Reconstruction, self).load(filepath, setupFileRef) # warn about pre-release projects being automatically upgraded if Version(self._graph.fileReleaseVersion).major == "0": self.warning.emit(Message( From a53940d29f7b1b18c364ae70f17618c6f6483c9e Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 25 Sep 2019 20:55:31 +0200 Subject: [PATCH 08/12] [core] graph: variable rename --- meshroom/core/graph.py | 12 ++++++------ meshroom/ui/graph.py | 8 ++++---- meshroom/ui/reconstruction.py | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 4f854c7ae4..1b176e8a59 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -234,14 +234,14 @@ def fileFeatures(self): return Graph.IO.getFeaturesForVersion(self.header.get(Graph.IO.Keys.FileVersion, "0.0")) @Slot(str) - def load(self, filepath, setupFileRef=True): + def load(self, filepath, setupProjectFile=True): """ Load a meshroom graph ".mg" file. Args: filepath: project filepath to load - setupFileRef: Setup reference to the project file, like setup cacheDir, keep filepath for save, etc. - This option allows to disable it, to only load the project file as a template. + setupProjectFile: Store the reference to the project file and setup the cache directory. + If false, it only loads the graph of the project file as a template. """ self.clear() with open(filepath) as jsonFile: @@ -273,7 +273,7 @@ def load(self, filepath, setupFileRef=True): # Add node to the graph with raw attributes values self._addNode(n, nodeName) - if setupFileRef: + if setupProjectFile: # Update filepath related members self._setFilepath(filepath) @@ -905,7 +905,7 @@ def toDict(self): def asString(self): return str(self.toDict()) - def save(self, filepath=None, setupFileRef=True): + def save(self, filepath=None, setupProjectFile=True): path = filepath or self._filepath if not path: raise ValueError("filepath must be specified for unsaved files.") @@ -929,7 +929,7 @@ def save(self, filepath=None, setupFileRef=True): with open(path, 'w') as jsonFile: json.dump(data, jsonFile, indent=4) - if path != self._filepath and setupFileRef: + if path != self._filepath and setupProjectFile: self._setFilepath(path) def _setFilepath(self, filepath): diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index d46a2469e2..d0e8bd3dc3 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -313,11 +313,11 @@ def stopChildThreads(self): def setDefaultPipeline(self, pipelineFilepath): self._defaultPipelineFilepath = pipelineFilepath - self._graph.load(pipelineFilepath, setupFileRef=False) + self._graph.load(pipelineFilepath, setupProjectFile=False) - def load(self, filepath, setupFileRef=True): + def load(self, filepath, setupProjectFile=True): g = Graph('') - g.load(filepath, setupFileRef) + g.load(filepath, setupProjectFile) if not os.path.exists(g.cacheDir): os.mkdir(g.cacheDir) self.setGraph(g) @@ -372,7 +372,7 @@ def stopExecution(self): def submit(self, node=None): """ Submit the graph to the default Submitter. If a node is specified, submit this node and its uncomputed predecessors. - Otherwise, submit the whole + Otherwise, submit the whole Notes: Default submitter is specified using the MESHROOM_DEFAULT_SUBMITTER environment variable. diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index ad43b473aa..2f37c381d3 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -197,14 +197,14 @@ def new(self): """ Create a new photogrammetry pipeline. """ if self._defaultPipelineFilepath: # use the user-provided default photogrammetry project file - self.load(self._defaultPipelineFilepath, setupFileRef=False) + self.load(self._defaultPipelineFilepath, setupProjectFile=False) else: # default photogrammetry pipeline self.setGraph(multiview.photogrammetry()) - def load(self, filepath, setupFileRef=True): + def load(self, filepath, setupProjectFile=True): try: - super(Reconstruction, self).load(filepath, setupFileRef) + super(Reconstruction, self).load(filepath, setupProjectFile) # warn about pre-release projects being automatically upgraded if Version(self._graph.fileReleaseVersion).major == "0": self.warning.emit(Message( From ce6dcbae4d1b7a101633d4070b06e848e0fbb30c Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 25 Sep 2019 21:12:46 +0200 Subject: [PATCH 09/12] [ui] simplify meshroom command line rules --- meshroom/ui/app.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 817375ea0b..e8e22fc90c 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -59,15 +59,13 @@ def __init__(self, args): parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.', add_help=True) - parser.add_argument('input', metavar='INPUT', type=str, nargs='?', + parser.add_argument('project', metavar='PROJECT', type=str, nargs='?', help='Meshroom project file (e.g. myProject.mg) or folder with images to reconstruct.') parser.add_argument('-i', '--import', metavar='IMAGES/FOLDERS', type=str, nargs='*', help='Import images or folder with images to reconstruct.') parser.add_argument('-I', '--importRecursive', metavar='FOLDERS', type=str, nargs='*', help='Import images to reconstruct from specified folder and sub-folders.') - parser.add_argument('-p', '--project', metavar='MESHROOM_FILE', type=str, required=False, - help='Meshroom project file (e.g. myProject.mg).') - parser.add_argument('-P', '--pipeline', metavar='MESHROOM_FILE', type=str, required=False, + parser.add_argument('-p', '--pipeline', metavar='MESHROOM_FILE', type=str, required=False, help='Override the default Meshroom pipeline with this external graph.') args = parser.parse_args(args[1:]) @@ -130,29 +128,14 @@ def __init__(self, args): if defaultPipeline: r.setDefaultPipeline(args.pipeline) - if args.input: - if not os.path.isfile(args.input) and not os.path.isdir(args.input): - raise RuntimeError( - "Meshroom Command Line Error: 'INPUT' argument should be a Meshroom project file (.mg) or a folder with input images.\n" - "Invalid value: '{}'".format(args.input)) if args.project and not os.path.isfile(args.project): raise RuntimeError( - "Meshroom Command Line Error: '--project' argument should be a Meshroom project file (.mg).\n" + "Meshroom Command Line Error: 'PROJECT' argument should be a Meshroom project file (.mg).\n" "Invalid value: '{}'".format(args.project)) - if args.project and args.input and not os.path.isdir(args.input): - raise RuntimeError("Meshroom Command Line Error: 'INPUT' and '--project' arguments cannot both load a Meshroom project file (.mg).") - if args.project: r.load(args.project) - if args.input: - if os.path.isfile(args.input): - # we assume that it is an ".mg" file. - r.load(args.input) - elif os.path.isdir(args.input): - r.importImagesFromFolder(args.input, recursive=False) - # import is a python keyword, so we have to access the attribute by a string if getattr(args, "import", None): r.importImagesFromFolder(getattr(args, "import"), recursive=False) From 49c57892f1c20b4a0c21d92c6f33b2ab6a4e62b4 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Wed, 25 Sep 2019 22:11:43 +0200 Subject: [PATCH 10/12] [bin] meshroom_photogrammetry: add command line paramOverrides --- bin/meshroom_photogrammetry | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 050e0e20e0..40e0b7511f 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -28,6 +28,9 @@ parser.add_argument('--pipeline', metavar='MESHROOM_FILE', type=str, required=Fa parser.add_argument('--overrides', metavar='SETTINGS', type=str, default=None, help='A JSON file containing the graph parameters override.') +parser.add_argument('--paramOverrides', metavar='NODETYPE:param=value NODEINSTANCE.param=value', type=str, default=None, nargs='*', + help='Override specific parameters directly from the command line (by node type or by node names).') + parser.add_argument('-o', '--output', metavar='FOLDER', type=str, required=False, help='Output folder where results should be copied to. ' 'If not set, results will have to be retrieved directly from the cache folder.') @@ -125,6 +128,29 @@ if args.overrides: for attrName, value in overrides.items(): graph.findNode(nodeName).attribute(attrName).value = value +if args.paramOverrides: + print("\n") + import re + reExtract = re.compile('(\w+)([:.])(\w+)=(.*)') + for p in args.paramOverrides: + result = reExtract.match(p) + if not result: + raise ValueError('Invalid param override: ' + str(p)) + node, t, param, value = result.groups() + if t == ':': + nodesByType = graph.nodesByType(node) + if not nodesByType: + raise ValueError('No node with the type "{}" in the scene.'.format(node)) + for n in nodesByType: + print('Overrides {node}.{param}={value}'.format(node=node, param=param, value=value)) + n.attribute(param).value = value + elif t == '.': + print('Overrides {node}.{param}={value}'.format(node=node, param=param, value=value)) + graph.findNode(node).attribute(param).value = value + else: + raise ValueError('Invalid param override: ' + str(p)) + print("\n") + # setup DepthMap downscaling if args.scale > 0: for node in graph.nodesByType('DepthMap'): From 9a473ad7c4e482097d4adcf31534b3db606e26ac Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Sep 2019 11:35:08 +0200 Subject: [PATCH 11/12] [bin] meshroom_photogrammetry: add --inputRecursive --- bin/meshroom_photogrammetry | 30 +++++++++++++++--------------- meshroom/multiview.py | 31 +++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 40e0b7511f..465b7892e1 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -11,15 +11,15 @@ import meshroom.core.graph from meshroom import multiview parser = argparse.ArgumentParser(description='Launch the full photogrammetry pipeline.') -parser.add_argument('-i', '--input', metavar='FOLDER_OR_SFM', type=str, - default='', - help='Input folder containing images or file (.sfm or .json) ' +parser.add_argument('-i', '--input', metavar='SFM/FOLDERS/IMAGES', type=str, nargs='*', + default=[], + help='Input folder containing images or folders of images or file (.sfm or .json) ' 'with images paths and optionally predefined camera intrinsics.') -parser.add_argument('--inputImages', metavar='IMAGES', type=str, nargs='*', +parser.add_argument('-I', '--inputRecursive', metavar='FOLDERS/IMAGES', type=str, nargs='*', default=[], - help='Input images.') + help='Input folders containing all images recursively.') -parser.add_argument('--pipeline', metavar='MESHROOM_FILE', type=str, required=False, +parser.add_argument('-p', '--pipeline', metavar='MESHROOM_FILE', type=str, required=False, help='Meshroom file containing a pre-configured photogrammetry pipeline to run on input images. ' 'If not set, the default photogrammetry pipeline will be used. ' 'Requirements: the graph must contain one CameraInit node, ' @@ -72,24 +72,24 @@ def getOnlyNodeOfType(g, nodeType): return nodes[0] -if not args.input and not args.inputImages: - print('Nothing to compute. You need to set --input or --inputImages.') +if not args.input and not args.inputRecursive: + print('Nothing to compute. You need to set --input or --inputRecursive.') sys.exit(1) views, intrinsics = [], [] # Build image files list from inputImages arguments -images = [f for f in args.inputImages if multiview.isImageFile(f)] +images = [] if args.input: - if os.path.isdir(args.input): - # args.input is a folder: extend images list with images in that folder - images += multiview.findImageFiles(args.input) - elif os.path.isfile(args.input) and os.path.splitext(args.input)[-1] in ('.json', '.sfm'): + if len(args.input) == 1 and os.path.isfile(args.input[0]) and os.path.splitext(args.input[0])[-1] in ('.json', '.sfm'): # args.input is a sfmData file: setup pre-calibrated views and intrinsics from meshroom.nodes.aliceVision.CameraInit import readSfMData - views, intrinsics = readSfMData(args.input) + views, intrinsics = readSfMData(args.input[0]) else: - raise RuntimeError(args.input + ': format not supported.') + images += multiview.findImageFiles(args.input, recursive=False) + +if args.inputRecursive: + images += multiview.findImageFiles(args.inputRecursive, recursive=True) # initialize photogrammetry pipeline if args.pipeline: diff --git a/meshroom/multiview.py b/meshroom/multiview.py index d7323059fe..52c105f1a6 100644 --- a/meshroom/multiview.py +++ b/meshroom/multiview.py @@ -19,20 +19,31 @@ def findImageFiles(folder, recursive=False): Return all files that are images in 'folder' based on their extensions. Args: - folder (str): the folder to look into + folder (str): folder to look into or list of folder/files Returns: - list: the list of image files. + list: the list of image files with a supported extension. """ - if recursive: - output = [] - for root, directories, files in os.walk(folder): - for filename in files: - if isImageFile(filename): - output.append(os.path.join(root, filename)) - return output + inputFolders = [] + if isinstance(folder, (list, tuple)): + inputFolders = folder else: - return [os.path.join(folder, filename) for filename in os.listdir(folder) if isImageFile(filename)] + inputFolders.append(folder) + + output = [] + for currentFolder in inputFolders: + if os.path.isfile(currentFolder): + if isImageFile(currentFolder): + output.append(currentFolder) + continue + if recursive: + for root, directories, files in os.walk(currentFolder): + for filename in files: + if isImageFile(filename): + output.append(os.path.join(root, filename)) + else: + output.extend([os.path.join(currentFolder, filename) for filename in os.listdir(currentFolder) if isImageFile(filename)]) + return output def photogrammetry(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), output=''): From ab7b940060c9d759e7bf0042eefa8ca229f36666 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 26 Sep 2019 12:17:53 +0200 Subject: [PATCH 12/12] [bin] meshroom_photogrammetry: add explicit check if no image found --- bin/meshroom_photogrammetry | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bin/meshroom_photogrammetry b/bin/meshroom_photogrammetry index 465b7892e1..af4c592b51 100755 --- a/bin/meshroom_photogrammetry +++ b/bin/meshroom_photogrammetry @@ -80,6 +80,8 @@ views, intrinsics = [], [] # Build image files list from inputImages arguments images = [] +hasSearchedForImages = False + if args.input: if len(args.input) == 1 and os.path.isfile(args.input[0]) and os.path.splitext(args.input[0])[-1] in ('.json', '.sfm'): # args.input is a sfmData file: setup pre-calibrated views and intrinsics @@ -87,9 +89,15 @@ if args.input: views, intrinsics = readSfMData(args.input[0]) else: images += multiview.findImageFiles(args.input, recursive=False) + hasSearchedForImages = True if args.inputRecursive: images += multiview.findImageFiles(args.inputRecursive, recursive=True) + hasSearchedForImages = True + +if hasSearchedForImages and not images: + print("No image found") + exit(-1) # initialize photogrammetry pipeline if args.pipeline: