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

360 HDR creation #639

Merged
merged 46 commits into from
Jan 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
bd88d16
[nodes][aliceVision] New node for hdri panorama stitching
ToukL Aug 8, 2019
7fc3d50
[nodes] New GlobalSfM node
fabiencastan Sep 16, 2019
0cdb193
[nodes] New Panorama estimation node
fabiencastan Sep 16, 2019
fad4d53
[nodes] split Panorama into PanoramaEstimation and PanoramaStitching
fabiencastan Sep 18, 2019
36b591f
[nodes] PanoramaStitching: add a new debug param
fabiencastan Sep 18, 2019
e7022f0
[nodes] PanoramaEstimation: declare a param as "advanced"
fabiencastan Sep 18, 2019
2862458
[nodes] PanoramaStitching: add a new debug option
fabiencastan Sep 18, 2019
ade9360
[ui] WorkspaceView: open images in the 2D Viewer from the Graph/NodeE…
fabiencastan Sep 18, 2019
b87432f
[nodes] Panorama: new param "refine"
fabiencastan Sep 18, 2019
38d27c9
[nodes] new debug node: ExportMatches
fabiencastan Sep 18, 2019
ee6772b
adding a node for rotating head xml
servantftechnicolor Oct 1, 2019
77b867c
restart stitching
servantftechnicolor Oct 15, 2019
e97676f
Control panorama width
servantftechnicolor Oct 24, 2019
9d78c7b
Make a compatible LDRToHDR node
servantftechnicolor Oct 24, 2019
54a57a7
[nodes] LDR2HDR: resize dynamic node size based on groupSize param
fabiencastan Oct 25, 2019
ff9b0e6
Merge branch 'develop' of https://github.com/alicevision/meshroom int…
fabiencastan Nov 1, 2019
a85730a
Merge branch 'develop' of https://github.com/alicevision/meshroom int…
fabiencastan Nov 1, 2019
07f564d
[bin] meshroom: option to choose HDRI pipeline
fabiencastan Nov 3, 2019
f03b52e
[multiview] setup xml files to Panorama node, create KeyframeSelectio…
fabiencastan Nov 4, 2019
00e9067
Adding node for camera downscale
servantftechnicolor Nov 8, 2019
45efa8b
[nodes] LDRToHDR: expose more options
fabiencastan Nov 12, 2019
a4f1d84
Merge branch 'dev_hdriStitching' of https://github.com/alicevision/Me…
fabiencastan Nov 12, 2019
8b34d08
Splitting compositing and warping
servantftechnicolor Nov 13, 2019
6730dd3
add option for multiband
servantftechnicolor Nov 25, 2019
d72dd03
[hdri] update default hdri pipeline
fabiencastan Nov 26, 2019
7eff5fb
[multiview] update hdri pipeline with PanoramaWarping/Compositing
fabiencastan Dec 9, 2019
d21079e
[nodes] ldr2hdr: add laguerre, refine exposures and calibration downs…
fabiencastan Dec 9, 2019
eeff5c2
add option to bypass HDR
servantftechnicolor Dec 11, 2019
d29b48e
add rotation offsets
servantftechnicolor Dec 11, 2019
1e7c201
[hdri] update default values for hdri pipeline
fabiencastan Dec 11, 2019
d92e761
[nodes] LDRToHDR: add highlight params
fabiencastan Dec 12, 2019
a1c9802
[hdri] workaround for HDRI pipeline on tractor
fabiencastan Dec 13, 2019
9b1962f
[hdri] update default values for FeatureExtraction and LDRToHDR
fabiencastan Dec 13, 2019
180a256
[bin] meshroom_photogrammetry: add hdri pipeline option
fabiencastan Dec 13, 2019
b3653aa
[bin] meshroom_photogrammetry: add submit on renderfarm
fabiencastan Dec 13, 2019
ec94a21
[nodes] LDRToHDR: rename param to nbBrackets
fabiencastan Dec 13, 2019
7d70da2
[nodes] LDRToHDR: particular case for nbBrackets=0
fabiencastan Dec 13, 2019
af98984
Merge branch 'develop' of https://github.com/alicevision/meshroom int…
fabiencastan Dec 13, 2019
7e634ea
Add a selector for compositer type
servantftechnicolor Dec 16, 2019
18f2161
[ui] reconstruction: avoid problems on empty metadata
fabiencastan Dec 16, 2019
cbb176d
[nodes] LDRToHDR: add automatic detection of the number of brackets
fabiencastan Dec 16, 2019
e0f4fcb
[nodes] Panorama: minor fix to compositerType default value
fabiencastan Dec 16, 2019
9775924
[bin] photogrammetry: reduce the number of updates with "GraphModific…
fabiencastan Dec 16, 2019
69a1f15
[nodes] CameraInit: remove subprocess wait (communicate is enough)
fabiencastan Dec 16, 2019
be0fb57
[ui] set AA_EnableHighDpiScaling earlier to fix qt warnings
fabiencastan Dec 18, 2019
25127bb
[nodes] LDRToHDR: declare cpu/ram usages
fabiencastan Dec 18, 2019
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
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