#!/usr/bin/env python import argparse import os import sys import distutils.util import json import re import meshroom meshroom.setupEnvironment() import meshroom.core.graph from meshroom import multiview from meshroom.core.desc import InitNode import logging parser = argparse.ArgumentParser(description='Launch the full photogrammetry or Panorama HDR pipeline.') parser.add_argument('-i', '--input', metavar='NODEINSTANCE:"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('-I', '--inputRecursive', metavar='NODEINSTANCE:"FOLDERS/IMAGES;..."', type=str, nargs='*', default=[], help='Input folders containing all images recursively.') parser.add_argument('-p', '--pipeline', metavar='FILE.mg/' + '/'.join(meshroom.core.pipelineTemplates), type=str, default='photogrammetry', help='Template pipeline among those listed or a Meshroom file containing a custom pipeline to run on input images. ' 'Requirements: the graph must contain one CameraInit node, ' 'and one Publish node if --output is set.') 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.') parser.add_argument('--cache', metavar='FOLDER', type=str, default=None, help='Custom cache folder to write computation results. ' '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 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.') parser.add_argument('--toNode', metavar='NODE', type=str, nargs='*', default=None, help='Process the node(s) with its dependencies.') parser.add_argument('--forceStatus', help='Force computation if status is RUNNING or SUBMITTED.', action='store_true') parser.add_argument('--forceCompute', help='Compute in all cases even if already computed.', action='store_true') parser.add_argument('--submit', help='Submit on renderfarm instead of local computation.', action='store_true') parser.add_argument("--submitLabel", type=str, default='{projectName} [Meshroom]', help="Label of a node in the submitter") parser.add_argument('--submitter', type=str, default='SimpleFarm', help='Execute job with a specific submitter.') parser.add_argument('-v', '--verbose', help="Verbosity level", default='', choices=['', 'fatal', 'error', 'warning', 'info', 'debug', 'trace'],) args = parser.parse_args() logStringToPython = { 'fatal': logging.FATAL, 'error': logging.ERROR, 'warning': logging.WARNING, 'info': logging.INFO, 'debug': logging.DEBUG, 'trace': logging.DEBUG, } if args.verbose: logging.getLogger().setLevel(logStringToPython[args.verbose]) if not args.input and not args.inputRecursive: print('Nothing to compute. You need to set --input or --inputRecursive.') sys.exit(1) graph = multiview.Graph(name=args.pipeline) with multiview.GraphModification(graph): # initialize template pipeline loweredPipelineTemplates = dict((k.lower(), v) for k, v in meshroom.core.pipelineTemplates.items()) if args.pipeline.lower() in loweredPipelineTemplates: graph.load(loweredPipelineTemplates[args.pipeline.lower()], setupProjectFile=False, publishOutputs=True if args.output else False) else: # custom pipeline graph.load(args.pipeline, setupProjectFile=False, publishOutputs=True if args.output else False) def parseInputs(inputs, uniqueInitNode): """Utility method for parsing the input and inputRecursive arguments.""" mapInputs = {} for inp in inputs: # Stop after the first occurrence to handle multiple input paths on Windows platforms inputGroup = inp.split(':', 1) nodeName = None if len(inputGroup) == 1 and uniqueInitNode: nodeName = uniqueInitNode.getName() elif len(inputGroup) == 2: # If inputGroup[0] is a valid node name, use it as such. if (inputGroup[0] in graph.toDict().keys()): nodeName = inputGroup[0] # Otherwise, the platform might be Windows and inputGroup[0] is part of the input path: # in that case, we should behave as if len(inputGroup) == 1, use the uniqueInitNode's name # and re-join the path together. elif uniqueInitNode: nodeName = uniqueInitNode.getName() inputGroup = [":".join(inputGroup)] else: print('Syntax error in input argument') # If a node name has been provided and the platform is Windows, the length of inputGroup might be # 3: first the node name, then the first path of the input path, then the rest of the input path. # The input path should also be re-joined here. elif len(inputGroup) == 3: nodeName = inputGroup[0] inputGroup[1] = ":".join(inputGroup[1:]) inputGroup.pop(-1) else: print('Syntax error in input argument') sys.exit(1) nodeInputs = inputGroup[-1].split(';') mapInputs[nodeName] = [os.path.abspath(path) for path in nodeInputs] return mapInputs # get init nodes initNodes = graph.findInitNodes() uniqueInitNode = initNodes[0] if (len(initNodes) == 1) else None # parse inputs for each init node mapInput = parseInputs(args.input, uniqueInitNode) # parse recursive inputs for each init node mapInputRecursive = parseInputs(args.inputRecursive, uniqueInitNode) # feed inputs and recursive inputs to init nodes for initNode in initNodes: nodeName = initNode.getName() inp = mapInput.get(nodeName) inpRec = mapInputRecursive.get(nodeName) if not inp and not inpRec: print(f'Nothing to compute for node {nodeName}.') sys.exit(1) initNode.nodeDesc.initialize(initNode, inp, inpRec) if not graph.canComputeLeaves: raise RuntimeError("Graph cannot be computed. Check for compatibility issues.") if args.verbose: graph.setVerbose(args.verbose) if args.output: # if there is more than 1 Publish node, they will all be set to the same output; # depending on what they are connected to, some input files might be overwritten in the output folder # (e.g. if two Publish nodes are connected to two Texturing nodes) publishNodes = graph.nodesOfType('Publish') if len(publishNodes) > 0: for node in publishNodes: node.output.value = os.path.abspath(args.output) else: raise RuntimeError("meshroom_batch requires a pipeline graph with at least one Publish node, none found.") if args.overrides: with open(args.overrides, 'r', encoding='utf-8', errors='ignore') as f: data = json.load(f) for nodeName, overrides in data.items(): for attrName, value in overrides.items(): graph.findNode(nodeName).attribute(attrName).value = value if args.paramOverrides: print("\n") reExtract = re.compile('(\w+)([:.])(\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 == ':': nodesOfType = graph.nodesOfType(node) if not nodesOfType: raise ValueError('No node with the type "{}" in the scene.'.format(node)) for n in nodesOfType: 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 cache directory graph.cacheDir = args.cache if args.cache else meshroom.core.defaultCacheFolder if args.save: graph.save(args.save, setupProjectFile=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)) # find end nodes (None will compute all graph) toNodes = graph.findNodes(args.toNode) if args.toNode else None if args.submit: if not args.save: raise ValueError('Need to save the project to file to submit on renderfarm.') # submit on renderfarm meshroom.core.graph.submit(args.save, args.submitter, toNode=args.toNode, submitLabel=args.submitLabel) elif args.compute: # 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)