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 7 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
26 changes: 15 additions & 11 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,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.')
Expand All @@ -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.')

Expand All @@ -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 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 @@ -126,19 +130,19 @@ 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, setupFileRef=True):
Copy link
Member

Choose a reason for hiding this comment

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

what about overrideProjectFile?

"""
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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
setupFileRef: Setup reference to the project file, like setup cacheDir, keep filepath for save, etc.
overrideProjectFile: Override the input project file (any changes will be overwritten on the file) and use its cache directories. If false, it only loads the graph of the project file as a template.

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)
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 setupFileRef:
# 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, setupFileRef=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 setupFileRef:
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
23 changes: 16 additions & 7 deletions meshroom/multiview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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=''):
Expand All @@ -47,11 +55,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
58 changes: 53 additions & 5 deletions meshroom/ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ 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('-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', '--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,
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 +121,44 @@ 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.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.loadUrl(QUrl.fromLocalFile(args.project))
r.load(args.project)

if args.input:
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't input and project be exclusive?

At least if project and input both contain a .mg. In this case, I think it would be better to throw an error to let the user know that there is something wrong. (unless it is able to load both projects?)

And if we give project and input with folders, does it add the images to the current project? Something in the same spirit of meshroom_photogrammetry https://github.com/alicevision/meshroom/pull/632/files#diff-4f70151d95f3b3a1c7ea377cbaef3f10R114

# initialize photogrammetry pipeline
if args.pipeline:
    # custom pipeline
    graph = meshroom.core.graph.loadGraph(args.pipeline)
    cameraInit = getOnlyNodeOfType(graph, 'CameraInit')
    # reset graph inputs
    cameraInit.viewpoints.resetValue()
    cameraInit.intrinsics.resetValue()
    # add views and intrinsics (if any) read from args.input
    cameraInit.viewpoints.extend(views)
    cameraInit.intrinsics.extend(intrinsics)

    if not graph.canComputeLeaves:
        raise RuntimeError("Graph cannot be computed. Check for compatibility issues.")

    if args.output:
        publish = getOnlyNodeOfType(graph, 'Publish')
        publish.output.value = args.output
else:
    # default pipeline
    graph = multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output)
    cameraInit = getOnlyNodeOfType(graph, 'CameraInit')

if images:
    views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, images)
    cameraInit.viewpoints.value = views
    cameraInit.intrinsics.value = intrinsics

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, there is no error for now but that's the usage intention.
So you're right, we can enforce the fact that it should be exclusive.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is changed and I have also added another argument "--import".

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)

if args.importRecursive:
r.importImagesFromFolder(args.importRecursive, recursive=True)

self.engine.load(os.path.normpath(url))

Expand Down
9 changes: 7 additions & 2 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, setupFileRef=False)

def load(self, filepath, setupFileRef=True):
g = Graph('')
g.load(filepath)
g.load(filepath, setupFileRef)
if not os.path.exists(g.cacheDir):
os.mkdir(g.cacheDir)
self.setGraph(g)
Expand Down
37 changes: 32 additions & 5 deletions meshroom/ui/reconstruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, setupFileRef=False)
else:
# default photogrammetry pipeline
self.setGraph(multiview.photogrammetry())

def load(self, filepath):
def load(self, filepath, setupFileRef=True):
try:
super(Reconstruction, self).load(filepath)
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(
Expand Down Expand Up @@ -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):
Expand All @@ -359,7 +364,29 @@ def getImageFilesFromDrop(drop):
images.append(localFile)
return images

def importImages(self, images, cameraInit):
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 = []
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. """
# Start the process of updating views and intrinsics
self.runAsync(self.buildIntrinsics, args=(cameraInit, images,))
Expand Down