Skip to content

Commit

Permalink
Merge pull request #639 from alicevision/dev_hdriStitching
Browse files Browse the repository at this point in the history
360 HDR creation
  • Loading branch information
fabiencastan authored Jan 14, 2020
2 parents ff96004 + 25127bb commit 44901f0
Show file tree
Hide file tree
Showing 15 changed files with 1,133 additions and 192 deletions.
146 changes: 82 additions & 64 deletions bin/meshroom_photogrammetry
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ meshroom.setupEnvironment()
import meshroom.core.graph
from meshroom import multiview

parser = argparse.ArgumentParser(description='Launch the full photogrammetry pipeline.')
parser = argparse.ArgumentParser(description='Launch the full photogrammetry or HDRI pipeline.')
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) '
Expand All @@ -19,9 +19,8 @@ parser.add_argument('-I', '--inputRecursive', metavar='FOLDERS/IMAGES', type=str
default=[],
help='Input folders containing all images recursively.')

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. '
parser.add_argument('-p', '--pipeline', metavar='photogrammetry/hdri/MG_FILE', type=str, default='photogrammetry',
help='"photogrammetry" pipeline, "hdri" pipeline 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.')

Expand Down Expand Up @@ -60,6 +59,13 @@ parser.add_argument('--forceStatus', help='Force computation if status is RUNNIN
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('--submitter',
type=str,
default='SimpleFarm',
help='Execute job with a specific submitter.')

args = parser.parse_args()


Expand All @@ -78,7 +84,7 @@ if not args.input and not args.inputRecursive:

views, intrinsics = [], []
# Build image files list from inputImages arguments
images = []
filesByType = multiview.FilesByType()

hasSearchedForImages = False

Expand All @@ -88,21 +94,32 @@ if args.input:
from meshroom.nodes.aliceVision.CameraInit import readSfMData
views, intrinsics = readSfMData(args.input[0])
else:
images += multiview.findImageFiles(args.input, recursive=False)
filesByType.extend(multiview.findFilesByTypeInFolder(args.input, recursive=False))
hasSearchedForImages = True

if args.inputRecursive:
images += multiview.findImageFiles(args.inputRecursive, recursive=True)
filesByType.extend(multiview.findFilesByTypeInFolder(args.inputRecursive, recursive=True))
hasSearchedForImages = True

if hasSearchedForImages and not images:
if hasSearchedForImages and not filesByType.images:
print("No image found")
exit(-1)

# initialize photogrammetry pipeline
if args.pipeline:
# custom pipeline
graph = meshroom.core.graph.loadGraph(args.pipeline)
graph = multiview.Graph(name=args.pipeline)

with multiview.GraphModification(graph):
# initialize photogrammetry pipeline
if args.pipeline.lower() == "photogrammetry":
# default photogrammetry pipeline
multiview.photogrammetry(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output, graph=graph)
elif args.pipeline.lower() == "hdri":
# default hdri pipeline
graph = multiview.hdri(inputViewpoints=views, inputIntrinsics=intrinsics, output=args.output, graph=graph)
else:
# custom pipeline
graph.load(args.pipeline)
# graph.update()

cameraInit = getOnlyNodeOfType(graph, 'CameraInit')
# reset graph inputs
cameraInit.viewpoints.resetValue()
Expand All @@ -117,66 +134,67 @@ if args.pipeline:
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

if args.overrides:
import io
import json
with io.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")
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:
if filesByType.images:
views, intrinsics = cameraInit.nodeDesc.buildIntrinsics(cameraInit, filesByType.images)
cameraInit.viewpoints.value = views
cameraInit.intrinsics.value = intrinsics

if args.overrides:
import io
import json
with io.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")
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))
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'):
node.downscale.value = args.scale

# 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))
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'):
node.downscale.value = args.scale

# 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.compute:
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=toNodes)
elif args.compute:
# start computation
meshroom.core.graph.executeGraph(graph, toNodes=toNodes, forceCompute=args.forceCompute, forceStatus=args.forceStatus)
147 changes: 133 additions & 14 deletions meshroom/multiview.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,53 @@
from meshroom.core.graph import Graph, GraphModification

# Supported image extensions
imageExtensions = ('.jpg', '.jpeg', '.tif', '.tiff', '.png', '.exr', '.rw2', '.cr2', '.nef', '.arw', '.dng')

imageExtensions = ('.jpg', '.jpeg', '.tif', '.tiff', '.png', '.exr', '.rw2', '.cr2', '.nef', '.arw')
videoExtensions = ('.avi', '.mov', '.qt',
'.mkv', '.webm',
'.mp4', '.mpg', '.mpeg', '.m2v', '.m4v',
'.wmv',
'.ogv', '.ogg',
'.mxf')
panoramaInfoExtensions = ('.xml')


def hasExtension(filepath, extensions):
""" Return whether filepath is one of the following extensions. """
return os.path.splitext(filepath)[1].lower() in extensions


class FilesByType:
def __init__(self):
self.images = []
self.videos = []
self.panoramaInfo = []
self.other = []

def __bool__(self):
return self.images or self.videos or self.panoramaInfo

def extend(self, other):
self.images.extend(other.images)
self.videos.extend(other.videos)
self.panoramaInfo.extend(other.panoramaInfo)
self.other.extend(other.other)

def addFile(self, file):
if hasExtension(file, imageExtensions):
self.images.append(file)
elif hasExtension(file, videoExtensions):
self.videos.append(file)
elif hasExtension(file, panoramaInfoExtensions):
self.panoramaInfo.append(file)
else:
self.other.append(file)

def isImageFile(filepath):
""" Return whether filepath is a path to an image file supported by Meshroom. """
return os.path.splitext(filepath)[1].lower() in imageExtensions
def addFiles(self, files):
for file in files:
self.addFile(file)


def findImageFiles(folder, recursive=False):
def findFilesByTypeInFolder(folder, recursive=False):
"""
Return all files that are images in 'folder' based on their extensions.
Expand All @@ -30,23 +68,103 @@ def findImageFiles(folder, recursive=False):
else:
inputFolders.append(folder)

output = []
output = FilesByType()
for currentFolder in inputFolders:
if os.path.isfile(currentFolder):
if isImageFile(currentFolder):
output.append(currentFolder)
output.addFile(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))
output.addFile(os.path.join(root, filename))
else:
output.extend([os.path.join(currentFolder, filename) for filename in os.listdir(currentFolder) if isImageFile(filename)])
output.addFiles([os.path.join(currentFolder, filename) for filename in os.listdir(currentFolder)])
return output


def photogrammetry(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), output=''):
def hdri(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), output='', graph=None):
"""
Create a new Graph with a complete HDRI pipeline.
Args:
inputImages (list of str, optional): list of image file paths
inputViewpoints (list of Viewpoint, optional): list of Viewpoints
output (str, optional): the path to export reconstructed model to
Returns:
Graph: the created graph
"""
if not graph:
graph = Graph('HDRI')
with GraphModification(graph):
nodes = hdriPipeline(graph)
cameraInit = nodes[0]
cameraInit.viewpoints.extend([{'path': image} for image in inputImages])
cameraInit.viewpoints.extend(inputViewpoints)
cameraInit.intrinsics.extend(inputIntrinsics)

if output:
stitching = nodes[-1]
graph.addNewNode('Publish', output=output, inputFiles=[stitching.output])

return graph


def hdriPipeline(graph):
"""
Instantiate an HDRI pipeline inside 'graph'.
Args:
graph (Graph/UIGraph): the graph in which nodes should be instantiated
Returns:
list of Node: the created nodes
"""
cameraInit = graph.addNewNode('CameraInit')

ldr2hdr = graph.addNewNode('LDRToHDR',
input=cameraInit.output)

featureExtraction = graph.addNewNode('FeatureExtraction',
input=ldr2hdr.outSfMDataFilename)
featureExtraction.describerPreset.value = 'ultra'
imageMatching = graph.addNewNode('ImageMatching',
input=featureExtraction.input,
featuresFolders=[featureExtraction.output])
featureMatching = graph.addNewNode('FeatureMatching',
input=imageMatching.input,
featuresFolders=imageMatching.featuresFolders,
imagePairsList=imageMatching.output)

panoramaExternalInfo = graph.addNewNode('PanoramaExternalInfo',
input=ldr2hdr.outSfMDataFilename,
matchesFolders=[featureMatching.output] # Workaround for tractor submission with a fake dependency
)

panoramaEstimation = graph.addNewNode('PanoramaEstimation',
input=panoramaExternalInfo.outSfMDataFilename,
featuresFolders=featureMatching.featuresFolders,
matchesFolders=[featureMatching.output])

panoramaWarping = graph.addNewNode('PanoramaWarping',
input=panoramaEstimation.outSfMDataFilename)

panoramaCompositing = graph.addNewNode('PanoramaCompositing',
input=panoramaWarping.output)

return [
cameraInit,
featureExtraction,
imageMatching,
featureMatching,
panoramaExternalInfo,
panoramaEstimation,
panoramaWarping,
panoramaCompositing,
]



def photogrammetry(inputImages=list(), inputViewpoints=list(), inputIntrinsics=list(), output='', graph=None):
"""
Create a new Graph with a complete photogrammetry pipeline.
Expand All @@ -58,7 +176,8 @@ def photogrammetry(inputImages=list(), inputViewpoints=list(), inputIntrinsics=l
Returns:
Graph: the created graph
"""
graph = Graph('Photogrammetry')
if not graph:
graph = Graph('Photogrammetry')
with GraphModification(graph):
sfmNodes, mvsNodes = photogrammetryPipeline(graph)
cameraInit = sfmNodes[0]
Expand Down
Loading

0 comments on commit 44901f0

Please sign in to comment.