Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve command line arguments #632

Merged
merged 12 commits into from
Sep 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 63 additions & 25 deletions bin/meshroom_photogrammetry
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import argparse
import os
import sys
import distutils.util

import meshroom
meshroom.setupEnvironment()
Expand All @@ -10,15 +11,15 @@ 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,
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, '
Expand All @@ -27,7 +28,10 @@ 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('--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.')

Expand All @@ -37,7 +41,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 graph to a project file. It will setup the cache folder accordingly if not explicitly changed by --cache.')

parser.add_argument('--compute', metavar='<yes/no>', type=lambda x: bool(distutils.util.strtobool(x)), default=True, required=False,
help='You can set it to <no/false/0> to disable the computation.')

parser.add_argument('--scale', type=int, default=-1,
choices=[-1, 1, 2, 4, 8, 16],
Expand Down Expand Up @@ -65,24 +72,32 @@ 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 = []

hasSearchedForImages = False

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)
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:
Expand Down Expand Up @@ -121,24 +136,47 @@ 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just asking, findNode(node) can fail in the same way as nodesByType(node)? Shall we manage the error?
Also, very minor thing, I'd put the print after the setting is done so it is not print in case of failure (hopefully findNode(node) or attribute(param) will print or raise some kind of error?)

In case, the same for the previous nodesByType(node)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findNode is looking for one specific node and fails with an explicit message if it doesn't find it.
nodesByType looks for all nodes of a specific type and can return an empty list without error (that's why there is a specific check to provide an error message).
I put the print before so it's more visible in the error message :s, the last NODE.PARAM is printed and then you have the error message saying that it doesn't find PARAM for instance.

else:
raise ValueError('Invalid param override: ' + str(p))
print("\n")

# setup DepthMap downscaling
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)
sys.exit(0)

# setup cache directory
graph.cacheDir = args.cache if args.cache else meshroom.core.defaultCacheFolder

if args.save:
graph.save(args.save, setupFileRef=not bool(args.cache))
print('File successfully saved: "{}"'.format(args.save))

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)
2 changes: 1 addition & 1 deletion meshroom/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
29 changes: 23 additions & 6 deletions meshroom/core/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, setupProjectFile=True):
"""
Load a meshroom graph ".mg" file.

Args:
filepath: project filepath to load
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:
fileData = json.load(jsonFile)
Expand Down Expand Up @@ -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 setupProjectFile:
# Update filepath related members
self._setFilepath(filepath)

# Create graph edges by resolving attributes expressions
self._applyExpr()
Expand Down Expand Up @@ -896,7 +905,7 @@ def toDict(self):
def asString(self):
return str(self.toDict())

def save(self, filepath=None):
def save(self, filepath=None, setupProjectFile=True):
path = filepath or self._filepath
if not path:
raise ValueError("filepath must be specified for unsaved files.")
Expand All @@ -920,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 setupProjectFile:
self._setFilepath(path)

def _setFilepath(self, filepath):
Expand All @@ -930,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
Expand All @@ -942,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:
Expand Down
38 changes: 29 additions & 9 deletions meshroom/multiview.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,36 @@ 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.

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.
"""
return [os.path.join(folder, filename) for filename in os.listdir(folder) if isImageFile(filename)]
inputFolders = []
if isinstance(folder, (list, tuple)):
inputFolders = folder
else:
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=''):
Expand All @@ -47,11 +66,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


Expand Down
41 changes: 36 additions & 5 deletions meshroom/ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ 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('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='*',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't now the arguments a little bit too redundant?
We can get rid of input at this point and just use project and the imports. It makes life easier, it's more readable and less confusing, and we don't need to check for conflicts. And too bad if we break compatibility, we already broke it with photogrammetry.

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', '--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')
Expand Down Expand Up @@ -105,12 +119,29 @@ 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 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:
r.loadUrl(QUrl.fromLocalFile(args.project))
r.load(args.project)

# 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))

Expand Down
11 changes: 8 additions & 3 deletions meshroom/ui/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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, setupProjectFile=False)

def load(self, filepath, setupProjectFile=True):
g = Graph('')
g.load(filepath)
g.load(filepath, setupProjectFile)
if not os.path.exists(g.cacheDir):
os.mkdir(g.cacheDir)
self.setGraph(g)
Expand Down Expand Up @@ -367,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.
Expand Down
Loading