Meshroom/meshroom/nodes/aliceVision/LDRToHDR.py
2020-07-02 11:57:51 +02:00

291 lines
12 KiB
Python

__version__ = "2.0"
import json
import os
from meshroom.core import desc
def findMetadata(d, keys, defaultValue):
v = None
for key in keys:
v = d.get(key, None)
k = key.lower()
if v != None:
return v
for dk, dv in d.iteritems():
dkm = dk.lower().replace(" ", "")
if dkm == key.lower():
return dv
dkm = dkm.split(":")[-1]
dkm = dkm.split("/")[-1]
if dkm == k:
return dv
return defaultValue
class DividedInputNodeSize(desc.DynamicNodeSize):
"""
The LDR2HDR will reduce the amount of views in the SfMData.
This class converts the number of LDR input views into the number of HDR output views.
"""
def __init__(self, param, divParam):
super(DividedInputNodeSize, self).__init__(param)
self._divParam = divParam
def computeSize(self, node):
s = super(DividedInputNodeSize, self).computeSize(node)
divParam = node.attribute(self._divParam)
if divParam.value == 0:
return s
return s / divParam.value
class LDRToHDR(desc.CommandLineNode):
commandLine = 'aliceVision_convertLDRToHDR {allParams}'
size = DividedInputNodeSize('input', 'nbBrackets')
cpu = desc.Level.INTENSIVE
ram = desc.Level.NORMAL
documentation='''
This node fuse LDR (Low Dynamic Range) images with multi-bracketing into HDR (High Dynamic Range) images.
It is done in 2 steps:
1/ Estimation of the Camera Response Function (CRF)
2/ HDR fusion relying on the CRF
'''
inputs = [
desc.File(
name='input',
label='Input',
description="SfM Data File",
value='',
uid=[0],
),
desc.IntParam(
name='userNbBrackets',
label='Number of Brackets',
description='Manually set the number of exposure brackets per HDR image. Set 0 for automatic detection.',
value=0,
range=(0, 15, 1),
uid=[],
group='user', # not used directly on the command line
),
desc.IntParam(
name='nbBrackets',
label='Automatic Nb Brackets',
description='Number of exposure brackets used per HDR image. It is detected automatically from input Viewpoints metadata if "userNbBrackets" is 0, else it is equal to "userNbBrackets".',
value=0,
range=(0, 10, 1),
uid=[0],
),
desc.FloatParam(
name='highlightCorrectionFactor',
label='Highlights Correction',
description='Pixels saturated in all input images have a partial information about their real luminance.\n'
'We only know that the value should be >= to the standard hdr fusion.\n'
'This parameter allows to perform a post-processing step to put saturated pixels to a constant '
'value defined by the `highlightsMaxLuminance` parameter.\n'
'This parameter is float to enable to weight this correction.',
value=1.0,
range=(0.0, 1.0, 0.01),
uid=[0],
),
desc.FloatParam(
name='highlightTargetLux',
label='Highlight Target Luminance (Lux)',
description='This is an arbitrary target value (in Lux) used to replace the unknown luminance value of the saturated pixels.\n'
'\n'
'Some Outdoor Reference Light Levels:\n'
' * 120,000 lux : Brightest sunlight\n'
' * 110,000 lux : Bright sunlight\n'
' * 20,000 lux : Shade illuminated by entire clear blue sky, midday\n'
' * 1,000 lux : Typical overcast day, midday\n'
' * 400 lux : Sunrise or sunset on a clear day\n'
' * 40 lux : Fully overcast, sunset/sunrise\n'
'\n'
'Some Indoor Reference Light Levels:\n'
' * 20000 lux : Max Usually Used Indoor\n'
' * 750 lux : Supermarkets\n'
' * 500 lux : Office Work\n'
' * 150 lux : Home\n',
value=120000.0,
range=(1000.0, 150000.0, 1.0),
uid=[0],
),
desc.BoolParam(
name='fisheyeLens',
label='Fisheye Lens',
description="Enable if a fisheye lens has been used.\n "
"This will improve the estimation of the Camera's Response Function by considering only the pixels in the center of the image\n"
"and thus ignore undefined/noisy pixels outside the circle defined by the fisheye lens.",
value=False,
uid=[0],
),
desc.BoolParam(
name='calibrationRefineExposures',
label='Refine Exposures',
description="Refine exposures provided by metadata (shutter speed, f-number, iso). Only available for 'laguerre' calibration method.",
value=False,
uid=[0],
),
desc.BoolParam(
name='byPass',
label='bypass convert',
description="Bypass HDR creation and use the medium bracket as the source for the next steps",
value=False,
uid=[0],
advanced=True,
),
desc.ChoiceParam(
name='calibrationMethod',
label='Calibration Method',
description="Method used for camera calibration \n"
" * Linear: Disable the calibration and assumes a linear Camera Response Function. If images are encoded in a known colorspace (like sRGB for JPEG), the images will be automatically converted to linear. \n"
" * Debevec: This is the standard method for HDR calibration. \n"
" * Grossberg: Based on learned database of cameras, it allows to reduce the CRF to few parameters while keeping all the precision. \n"
" * Laguerre: Simple but robust method estimating the minimal number of parameters. \n"
" * Robertson: First method for HDR calibration in the literature. \n",
values=['linear', 'debevec', 'grossberg', 'laguerre', 'robertson'],
value='debevec',
exclusive=True,
uid=[0],
),
desc.ChoiceParam(
name='calibrationWeight',
label='Calibration Weight',
description="Weight function used to calibrate camera response \n"
" * default (automatically selected according to the calibrationMethod) \n"
" * gaussian \n"
" * triangle \n"
" * plateau",
value='default',
values=['default', 'gaussian', 'triangle', 'plateau'],
exclusive=True,
uid=[0],
),
desc.ChoiceParam(
name='fusionWeight',
label='Fusion Weight',
description="Weight function used to fuse all LDR images together:\n"
" * gaussian \n"
" * triangle \n"
" * plateau",
value='gaussian',
values=['gaussian', 'triangle', 'plateau'],
exclusive=True,
uid=[0],
),
desc.IntParam(
name='calibrationNbPoints',
label='Calibration Nb Points',
description='Internal number of points used for calibration.',
value=0,
range=(0, 10000000, 1000),
uid=[0],
advanced=True,
),
desc.IntParam(
name='calibrationDownscale',
label='Calibration Downscale',
description='Scaling factor applied to images before calibration of the response function to reduce the impact of misalignment.',
value=4,
range=(1, 16, 1),
uid=[0],
advanced=True,
),
desc.IntParam(
name='channelQuantizationPower',
label='Channel Quantization Power',
description='Quantization level like 8 bits or 10 bits.',
value=10,
range=(8, 14, 1),
uid=[0],
advanced=True,
),
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='outSfMDataFilename',
label='Output SfMData File',
description='Path to the output sfmdata file',
value=desc.Node.internalFolder + 'sfmData.sfm',
uid=[],
)
]
@classmethod
def update(cls, node):
if not isinstance(node.nodeDesc, cls):
raise ValueError("Node {} is not an instance of type {}".format(node, cls))
# TODO: use Node version for this test
if 'userNbBrackets' not in node.getAttributes().keys():
# Old version of the node
return
if node.userNbBrackets.value != 0:
node.nbBrackets.value = node.userNbBrackets.value
return
# logging.info("[LDRToHDR] Update start: version:" + str(node.packageVersion))
cameraInitOutput = node.input.getLinkParam()
if not cameraInitOutput:
node.nbBrackets.value = 0
return
viewpoints = cameraInitOutput.node.viewpoints.value
# logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints)))
inputs = []
for viewpoint in viewpoints:
jsonMetadata = viewpoint.metadata.value
if not jsonMetadata:
# no metadata, we cannot found the number of brackets
node.nbBrackets.value = 0
return
d = json.loads(jsonMetadata)
fnumber = findMetadata(d, ["FNumber", "Exif:ApertureValue", "ApertureValue", "Aperture"], "")
shutterSpeed = findMetadata(d, ["Exif:ShutterSpeedValue", "ShutterSpeedValue", "ShutterSpeed"], "")
iso = findMetadata(d, ["Exif:ISOSpeedRatings", "ISOSpeedRatings", "ISO"], "")
if not fnumber and not shutterSpeed:
# If one image without shutter or fnumber, we cannot found the number of brackets.
# We assume that there is no multi-bracketing, so nothing to do.
node.nbBrackets.value = 1
return
inputs.append((viewpoint.path.value, (fnumber, shutterSpeed, iso)))
inputs.sort()
exposureGroups = []
exposures = []
for path, exp in inputs:
if exposures and exp != exposures[-1] and exp == exposures[0]:
exposureGroups.append(exposures)
exposures = [exp]
else:
exposures.append(exp)
exposureGroups.append(exposures)
exposures = None
bracketSizes = set()
if len(exposureGroups) == 1:
node.nbBrackets.value = 1
else:
for expGroup in exposureGroups:
bracketSizes.add(len(expGroup))
if len(bracketSizes) == 1:
node.nbBrackets.value = bracketSizes.pop()
# logging.info("[LDRToHDR] nb bracket size:" + str(node.nbBrackets.value))
else:
node.nbBrackets.value = 0
# logging.info("[LDRToHDR] Update end")