__version__ = "4.0" import json import math import os from collections import Counter from pyalicevision import sfmData as avsfmdata from pyalicevision import hdr as avhdr from meshroom.core import desc from meshroom.core.utils import COLORSPACES, VERBOSE_LEVEL def findMetadata(d, keys, defaultValue): v = None for key in keys: v = d.get(key, None) k = key.lower() if v is not None: return v for dk, dv in d.items(): 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 # s is the total number of inputs and may include outliers, that will not be used # during computations and should thus be excluded from the size computation return (s - node.outliersNb) / divParam.value class LdrToHdrSampling(desc.AVCommandLineNode): commandLine = 'aliceVision_LdrToHdrSampling {allParams}' size = DividedInputNodeSize('input', 'nbBrackets') parallelization = desc.Parallelization(blockSize=2) commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}' category = 'Panorama HDR' documentation = ''' Sample pixels from Low range images for HDR creation. ''' outliersNb = 0 # Number of detected outliers among the input images inputs = [ desc.File( name="input", label="SfMData", description="Input SfMData file.", value="", ), desc.IntParam( name="userNbBrackets", label="Number Of Brackets", description="Number of exposure brackets per HDR image (0 for automatic detection).", value=0, range=(0, 15, 1), invalidate=False, group="user", # not used directly on the command line errorMessage="The set number of brackets is not a multiple of the number of input images.\n" "Errors will occur during the computation.", exposed=True, ), desc.IntParam( name="nbBrackets", label="Automatic Nb Brackets", description="Number of exposure brackets used per HDR image.\n" "It is detected automatically from input Viewpoints metadata if 'userNbBrackets'\n" "is 0, else it is equal to 'userNbBrackets'.", value=0, range=(0, 15, 1), group="bracketsParams", ), desc.BoolParam( name="byPass", label="Bypass", description="Bypass HDR creation and use the medium bracket as the source for the next steps.", value=False, enabled=lambda node: node.nbBrackets.value != 1, exposed=True, ), desc.ChoiceParam( name="calibrationMethod", label="Calibration Method", description="Method used for camera calibration:\n" " - Auto: If RAW images are detected, the 'Linear' calibration method will be used. Otherwise, the 'Debevec' calibration method will be used.\n" " - Linear: Disables the calibration and assumes a linear Camera Response Function. If images are encoded in a known colorspace (like sRGB for JPEG), they will be automatically converted to linear.\n" " - Debevec: Standard method for HDR calibration.\n" " - Grossberg: Based on a learned database of cameras, allows to reduce the Camera Response Function to a few parameters while keeping all the precision.\n" " - Laguerre: Simple but robust method estimating the minimal number of parameters.", values=["auto", "linear", "debevec", "grossberg", "laguerre"], value="auto", enabled=lambda node: node.byPass.enabled and not node.byPass.value, exposed=True, ), desc.IntParam( name="channelQuantizationPower", label="Channel Quantization Power", description="Quantization level like 8 bits or 10 bits.", value=10, range=(8, 14, 1), advanced=True, enabled=lambda node: node.byPass.enabled and not node.byPass.value, exposed=True, ), desc.ChoiceParam( name="workingColorSpace", label="Working Color Space", description="Color space in which the data are processed.\n" "If 'auto' is selected, the working color space will be 'Linear' if RAW images are detected; otherwise, it will be set to 'sRGB'.", values=COLORSPACES, value="AUTO", enabled=lambda node: node.byPass.enabled and not node.byPass.value, exposed=True, ), desc.IntParam( name="blockSize", label="Block Size", description="Size of the image tile to extract a sample.", value=256, range=(8, 1024, 1), advanced=True, enabled=lambda node: node.byPass.enabled and not node.byPass.value, ), desc.IntParam( name="radius", label="Patch Radius", description="Radius of the patch used to analyze the sample statistics.", value=5, range=(0, 10, 1), advanced=True, enabled=lambda node: node.byPass.enabled and not node.byPass.value, ), desc.IntParam( name="maxCountSample", label="Max Number Of Samples", description="Maximum number of samples per image group.", value=200, range=(10, 1000, 10), advanced=True, enabled=lambda node: node.byPass.enabled and not node.byPass.value, ), desc.BoolParam( name="debug", label="Export Debug Files", description="Export debug files to analyze the sampling strategy.", value=False, invalidate=False, enabled=lambda node: node.byPass.enabled and not node.byPass.value, ), desc.ChoiceParam( name="verboseLevel", label="Verbose Level", description="Verbosity level (fatal, error, warning, info, debug, trace).", values=VERBOSE_LEVEL, value="info", ), ] outputs = [ desc.File( name="output", label="Folder", description="Output path for the samples.", value=desc.Node.internalFolder, ), ] def processChunk(self, chunk): if chunk.node.nbBrackets.value == 1: return # Trick to avoid sending --nbBrackets to the command line when the bracket detection is automatic. # Otherwise, the AliceVision executable has no way of determining whether the bracket detection was automatic # or if it was hard-set by the user. self.commandLine = "aliceVision_LdrToHdrSampling {allParams}" if chunk.node.userNbBrackets.value == chunk.node.nbBrackets.value: self.commandLine += "{bracketsParams}" super(LdrToHdrSampling, self).processChunk(chunk) @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 node.outliersNb = 0 # Reset the number of detected outliers node.userNbBrackets.validValue = True # Reset the status of "userNbBrackets" cameraInitOutput = node.input.getLinkParam(recursive=True) if not cameraInitOutput: node.nbBrackets.value = 0 return if node.userNbBrackets.value != 0: # The number of brackets has been manually forced: check whether it is valid or not if cameraInitOutput and cameraInitOutput.node and cameraInitOutput.node.hasAttribute("viewpoints"): viewpoints = cameraInitOutput.node.viewpoints.value # The number of brackets should be a multiple of the number of input images if (len(viewpoints) % node.userNbBrackets.value != 0): node.userNbBrackets.validValue = False else: node.userNbBrackets.validValue = True node.nbBrackets.value = node.userNbBrackets.value return if not cameraInitOutput.node.hasAttribute("viewpoints"): if cameraInitOutput.node.hasAttribute("input"): cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True) if cameraInitOutput and cameraInitOutput.node and cameraInitOutput.node.hasAttribute("viewpoints"): viewpoints = cameraInitOutput.node.viewpoints.value else: # No connected CameraInit node.nbBrackets.value = 0 return inputs = avhdr.vectorli() for viewpoint in viewpoints: jsonMetadata = viewpoint.metadata.value if not jsonMetadata: # no metadata, we cannot find the number of brackets node.nbBrackets.value = 0 return d = json.loads(jsonMetadata) # Find Fnumber fnumber = findMetadata(d, ["FNumber"], "") if fnumber == "": aperture = findMetadata(d, ["Exif:ApertureValue", "ApertureValue", "Aperture"], "") if aperture == "": fnumber = -1.0 else: fnumber = pow(2.0, aperture / 2.0) # Get shutter speed and ISO shutterSpeed = findMetadata(d, ["ExposureTime", "Exif:ShutterSpeedValue", "ShutterSpeedValue", "ShutterSpeed"], -1.0) iso = findMetadata(d, ["Exif:PhotographicSensitivity", "PhotographicSensitivity", "Photographic Sensitivity", "ISO"], -1.0) 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 exposure = LdrToHdrSampling.getExposure((float(fnumber), float(shutterSpeed), float(iso))) obj = avhdr.LuminanceInfo(viewpoint.viewId.value,viewpoint.path.value, exposure) inputs.append(obj) obj = avhdr.estimateGroups(inputs) if len(obj) == 0: node.nbBrackets.value = 0 return bracketSize = len(obj[0]) bracketCount = len(obj) node.nbBrackets.value = bracketSize node.outliersNb = len(inputs) - (bracketSize * bracketCount) @staticmethod def getExposure(exp, refIso = 100.0, refFnumber = 1.0): fnumber, shutterSpeed, iso = exp obj = avsfmdata.ExposureSetting(shutterSpeed, fnumber, iso) return obj.getExposure()