Meshroom/meshroom/nodes/aliceVision/CameraInit.py
2020-06-30 20:41:13 +02:00

276 lines
12 KiB
Python

__version__ = "2.0"
import os
import json
import psutil
import shutil
import tempfile
from meshroom.core import desc
Viewpoint = [
desc.IntParam(name="viewId", label="Id", description="Image UID", value=-1, uid=[0], range=None),
desc.IntParam(name="poseId", label="Pose Id", description="Pose Id", value=-1, uid=[0], range=None),
desc.File(name="path", label="Image Path", description="Image Filepath", value="", uid=[0]),
desc.IntParam(name="intrinsicId", label="Intrinsic", description="Internal Camera Parameters", value=-1, uid=[0], range=None),
desc.IntParam(name="rigId", label="Rig", description="Rig Parameters", value=-1, uid=[0], range=None),
desc.IntParam(name="subPoseId", label="Rig Sub-Pose", description="Rig Sub-Pose Parameters", value=-1, uid=[0], range=None),
desc.StringParam(name="metadata", label="Image Metadata", description="", value="", uid=[], advanced=True),
]
Intrinsic = [
desc.IntParam(name="intrinsicId", label="Id", description="Intrinsic UID", value=-1, uid=[0], range=None),
desc.FloatParam(name="pxInitialFocalLength", label="Initial Focal Length", description="Initial Guess on the Focal Length", value=-1.0, uid=[0], range=None),
desc.FloatParam(name="pxFocalLength", label="Focal Length", description="Known/Calibrated Focal Length", value=-1.0, uid=[0], range=None),
desc.ChoiceParam(name="type", label="Camera Type", description="Camera Type", value="", values=['', 'pinhole', 'radial1', 'radial3', 'brown', 'fisheye4'], exclusive=True, uid=[0]),
desc.IntParam(name="width", label="Width", description="Image Width", value=0, uid=[], range=(0, 10000, 1)),
desc.IntParam(name="height", label="Height", description="Image Height", value=0, uid=[], range=(0, 10000, 1)),
desc.StringParam(name="serialNumber", label="Serial Number", description="Device Serial Number (camera and lens combined)", value="", uid=[]),
desc.GroupAttribute(name="principalPoint", label="Principal Point", description="", groupDesc=[
desc.FloatParam(name="x", label="x", description="", value=0, uid=[], range=(0, 10000, 1)),
desc.FloatParam(name="y", label="y", description="", value=0, uid=[], range=(0, 10000, 1)),
]),
desc.ChoiceParam(name="initializationMode", label="Initialization Mode",
description="Defines how this Intrinsic was initialized:\n"
" * calibrated: calibrated externally.\n"
" * estimated: estimated from metadata and/or sensor width. \n"
" * unknown: unknown camera parameters (can still have default value guess)\n"
" * none: not set",
values=("calibrated", "estimated", "unknown", "none"),
value="none",
exclusive=True,
uid=[],
advanced=True
),
desc.ListAttribute(
name="distortionParams",
elementDesc=desc.FloatParam(name="p", label="", description="", value=0.0, uid=[0], range=(-0.1, 0.1, 0.01)),
label="Distortion Params",
description="Distortion Parameters",
),
desc.BoolParam(name='locked', label='Locked',
description='If the camera has been calibrated, the internal camera parameters (intrinsics) can be locked. It should improve robustness and speedup the reconstruction.',
value=False, uid=[0]),
]
def readSfMData(sfmFile):
""" Read views and intrinsics from a .sfm file
Args:
sfmFile: the .sfm file containing views and intrinsics
Returns:
The views and intrinsics of the .sfm as two separate lists
"""
import io # use io.open for Python2/3 compatibility (allow to specify encoding + errors handling)
# skip decoding errors to avoid potential exceptions due to non utf-8 characters in images metadata
with io.open(sfmFile, 'r', encoding='utf-8', errors='ignore') as f:
data = json.load(f)
intrinsicsKeys = [i.name for i in Intrinsic]
intrinsics = [{k: v for k, v in item.items() if k in intrinsicsKeys} for item in data.get("intrinsics", [])]
for intrinsic in intrinsics:
pp = intrinsic['principalPoint']
intrinsic['principalPoint'] = {}
intrinsic['principalPoint']['x'] = pp[0]
intrinsic['principalPoint']['y'] = pp[1]
# convert empty string distortionParams (i.e: Pinhole model) to empty list
if intrinsic['distortionParams'] == '':
intrinsic['distortionParams'] = list()
viewsKeys = [v.name for v in Viewpoint]
views = [{k: v for k, v in item.items() if k in viewsKeys} for item in data.get("views", [])]
for view in views:
view['metadata'] = json.dumps(view['metadata']) # convert metadata to string
return views, intrinsics
class CameraInit(desc.CommandLineNode):
commandLine = 'aliceVision_cameraInit {allParams} --allowSingleView 1' # don't throw an error if there is only one image
size = desc.DynamicNodeSize('viewpoints')
inputs = [
desc.ListAttribute(
name="viewpoints",
elementDesc=desc.GroupAttribute(name="viewpoint", label="Viewpoint", description="", groupDesc=Viewpoint),
label="Viewpoints",
description="Input viewpoints",
group="",
),
desc.ListAttribute(
name="intrinsics",
elementDesc=desc.GroupAttribute(name="intrinsic", label="Intrinsic", description="", groupDesc=Intrinsic),
label="Intrinsics",
description="Camera Intrinsics",
group="",
),
desc.File(
name='sensorDatabase',
label='Sensor Database',
description='''Camera sensor width database path.''',
value=os.environ.get('ALICEVISION_SENSOR_DB', ''),
uid=[],
),
desc.FloatParam(
name='defaultFieldOfView',
label='Default Field Of View',
description='Empirical value for the field of view in degree.',
value=45.0,
range=(0, 180.0, 1),
uid=[0],
),
desc.ChoiceParam(
name='groupCameraFallback',
label='Group Camera Fallback',
description="If there is no serial number in image metadata, devices cannot be accurately identified.\n"
"Therefore, internal camera parameters cannot be shared among images reliably.\n"
"A fallback grouping strategy must be chosen:\n"
" * global: group images from comparable devices (same make/model/focal) globally.\n"
" * folder: group images from comparable devices only within the same folder.\n"
" * image: never group images from comparable devices",
values=['global', 'folder', 'image'],
value='folder',
exclusive=True,
uid=[0],
advanced=True
),
desc.ChoiceParam(
name='allowedCameraModels',
label='Allowed Camera Models',
description='the Camera Models that can be attributed.',
value=['pinhole', 'radial1', 'radial3', 'brown', 'fisheye4', 'fisheye1'],
values=['pinhole', 'radial1', 'radial3', 'brown', 'fisheye4', 'fisheye1'],
exclusive=False,
uid=[0],
joinChar=',',
advanced= True
),
desc.ChoiceParam(
name='viewIdMethod',
label='ViewId Method',
description="Allows to choose the way the viewID is generated:\n"
" * metadata : Generate viewId from image metadata.\n"
" * filename : Generate viewId from file names using regex.",
value='metadata',
values=['metadata', 'filename'],
exclusive=True,
uid=[0],
advanced= True
),
desc.StringParam(
name='viewIdRegex',
label='ViewId Regex',
description='Regex used to catch number used as viewId in filename.'
'You should capture specific parts of the filename with parenthesis to define matching elements. (only number will works)\n'
'Some examples of patterns:\n'
' - Match the longest number at the end of filename (default value): ".*?(\d+)"\n'
' - Match the first number found in filename : "(\d+).*"\n',
value='.*?(\d+)',
uid=[0],
),
desc.ChoiceParam(
name='verboseLevel',
label='Verbose Level',
description='''verbosity level (fatal, error, warning, info, debug, trace).''',
value='info',
values=['fatal', 'error', 'warning', 'info', 'debug', 'trace'],
exclusive=True,
uid=[],
),
]
outputs = [
desc.File(
name='output',
label='Output SfMData File',
description='''Output SfMData.''',
value=desc.Node.internalFolder + 'cameraInit.sfm',
uid=[],
),
]
def readSfMData(self, sfmFile):
return readSfMData(sfmFile)
def buildIntrinsics(self, node, additionalViews=()):
""" Build intrinsics from node current views and optional additional views
Args:
node: the CameraInit node instance to build intrinsics for
additionalViews: (optional) the new views (list of path to images) to add to the node's viewpoints
Returns:
The updated views and intrinsics as two separate lists
"""
assert isinstance(node.nodeDesc, CameraInit)
if node.graph:
# make a copy of the node outside the graph
# to change its cache folder without modifying the original node
node = node.graph.copyNode(node)[0]
tmpCache = tempfile.mkdtemp()
node.updateInternals(tmpCache)
try:
os.makedirs(os.path.join(tmpCache, node.internalFolder))
self.createViewpointsFile(node, additionalViews)
cmd = self.buildCommandLine(node.chunks[0])
# logging.debug(' - commandLine:', cmd)
proc = psutil.Popen(cmd, stdout=None, stderr=None, shell=True)
stdout, stderr = proc.communicate()
# proc.wait()
if proc.returncode != 0:
raise RuntimeError('CameraInit failed with error code {}.\nCommand was: "{}".\n'.format(
proc.returncode, cmd)
)
# Reload result of aliceVision_cameraInit
cameraInitSfM = node.output.value
return readSfMData(cameraInitSfM)
except Exception:
raise
finally:
shutil.rmtree(tmpCache)
def createViewpointsFile(self, node, additionalViews=()):
node.viewpointsFile = ""
if node.viewpoints or additionalViews:
newViews = []
for path in additionalViews: # format additional views to match json format
newViews.append({"path": path})
intrinsics = node.intrinsics.getPrimitiveValue(exportDefault=True)
for intrinsic in intrinsics:
intrinsic['principalPoint'] = [intrinsic['principalPoint']['x'], intrinsic['principalPoint']['y']]
views = node.viewpoints.getPrimitiveValue(exportDefault=False)
# convert the metadata string into a map
for view in views:
if 'metadata' in view:
view['metadata'] = json.loads(view['metadata'])
sfmData = {
"version": [1, 0, 0],
"views": views + newViews,
"intrinsics": intrinsics,
"featureFolder": "",
"matchingFolder": "",
}
node.viewpointsFile = (node.nodeDesc.internalFolder + '/viewpoints.sfm').format(**node._cmdVars)
with open(node.viewpointsFile, 'w') as f:
json.dump(sfmData, f, indent=4)
def buildCommandLine(self, chunk):
cmd = desc.CommandLineNode.buildCommandLine(self, chunk)
if chunk.node.viewpointsFile:
cmd += ' --input "{}"'.format(chunk.node.viewpointsFile)
return cmd
def processChunk(self, chunk):
self.createViewpointsFile(chunk.node)
desc.CommandLineNode.processChunk(self, chunk)