diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index e9ab67d54f..81bff5213b 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -305,15 +305,31 @@ def getExportValue(self): return self._value def getEvalValue(self): + ''' + Return the value. If it is a string, expressions will be evaluated. + ''' if isinstance(self.value, str): return Template(self.value).safe_substitute(os.environ) return self.value - def getValueStr(self): + def getValueStr(self, withQuotes=True): + ''' + Return the value formatted as a string with quotes to deal with spaces. + If it is a string, expressions will be evaluated. + If it is an empty string, it will returns 2 quotes. + If it is an empty list, it will returns a really empty string. + If it is a list with one empty string element, it will returns 2 quotes. + ''' + # ChoiceParam with multiple values should be combined if isinstance(self.attributeDesc, desc.ChoiceParam) and not self.attributeDesc.exclusive: + # ensure value is a list as expected assert(isinstance(self.value, Sequence) and not isinstance(self.value, str)) - return self.attributeDesc.joinChar.join(self.getEvalValue()) - if isinstance(self.attributeDesc, (desc.StringParam, desc.File)): + v = self.attributeDesc.joinChar.join(self.getEvalValue()) + if withQuotes and v: + return '"{}"'.format(v) + return v + # String, File, single value Choice are based on strings and should includes quotes to deal with spaces + if withQuotes and isinstance(self.attributeDesc, (desc.StringParam, desc.File, desc.ChoiceParam)): return '"{}"'.format(self.getEvalValue()) return str(self.getEvalValue()) @@ -497,10 +513,15 @@ def getPrimitiveValue(self, exportDefault=True): else: return [attr.getPrimitiveValue(exportDefault=exportDefault) for attr in self._value if not attr.isDefault] - def getValueStr(self): - if isinstance(self.value, ListModel): - return self.attributeDesc.joinChar.join([v.getValueStr() for v in self.value]) - return super(ListAttribute, self).getValueStr() + def getValueStr(self, withQuotes=True): + assert(isinstance(self.value, ListModel)) + if self.attributeDesc.joinChar == ' ': + return self.attributeDesc.joinChar.join([v.getValueStr(withQuotes=withQuotes) for v in self.value]) + else: + v = self.attributeDesc.joinChar.join([v.getValueStr(withQuotes=False) for v in self.value]) + if withQuotes and v: + return '"{}"'.format(v) + return v def updateInternals(self): super(ListAttribute, self).updateInternals() @@ -616,7 +637,7 @@ def getPrimitiveValue(self, exportDefault=True): else: return {name: attr.getPrimitiveValue(exportDefault=exportDefault) for name, attr in self._value.items() if not attr.isDefault} - def getValueStr(self): + def getValueStr(self, withQuotes=True): # add brackets if requested strBegin = '' strEnd = '' @@ -626,10 +647,17 @@ def getValueStr(self): strEnd = self.attributeDesc.brackets[1] else: raise AttributeError("Incorrect brackets on GroupAttribute: {}".format(self.attributeDesc.brackets)) - + + # particular case when using space separator + spaceSep = self.attributeDesc.joinChar == ' ' + # sort values based on child attributes group description order - sortedSubValues = [self._value.get(attr.name).getValueStr() for attr in self.attributeDesc.groupDesc] - return strBegin + self.attributeDesc.joinChar.join(sortedSubValues) + strEnd + sortedSubValues = [self._value.get(attr.name).getValueStr(withQuotes=spaceSep) for attr in self.attributeDesc.groupDesc] + s = self.attributeDesc.joinChar.join(sortedSubValues) + + if withQuotes and not spaceSep: + return '"{}{}{}"'.format(strBegin, s, strEnd) + return '{}{}{}'.format(strBegin, s, strEnd) def updateInternals(self): super(GroupAttribute, self).updateInternals() diff --git a/meshroom/core/node.py b/meshroom/core/node.py index da68cf57c2..608d810f35 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -709,10 +709,15 @@ def _buildAttributeCmdVars(cmdVars, name, attr): group = attr.attributeDesc.group(attr.node) if isinstance(attr.attributeDesc.group, types.FunctionType) else attr.attributeDesc.group if group is not None: # if there is a valid command line "group" - v = attr.getValueStr() + v = attr.getValueStr(withQuotes=True) cmdVars[name] = '--{name} {value}'.format(name=name, value=v) - cmdVars[name + 'Value'] = str(v) + # xxValue is exposed without quotes to allow to compose expressions + cmdVars[name + 'Value'] = attr.getValueStr(withQuotes=False) + # List elements may give a fully empty string and will not be sent to the command line. + # String attributes will return only quotes if it is empty and thus will be send to the command line. + # But a List of string containing 1 element, + # and this element is an empty string will also return quotes and will be send to the command line. if v: cmdVars[group] = cmdVars.get(group, '') + ' ' + cmdVars[name] elif isinstance(attr, GroupAttribute): @@ -759,10 +764,11 @@ def _buildAttributeCmdVars(cmdVars, name, attr): except ValueError as e: logging.warning('Invalid expression value on "{nodeName}.{attrName}" with value "{defaultValue}".\nError: {err}'.format(nodeName=self.name, attrName=attr.name, defaultValue=defaultValue, err=str(e))) - v = attr.getValueStr() + v = attr.getValueStr(withQuotes=True) self._cmdVars[name] = '--{name} {value}'.format(name=name, value=v) - self._cmdVars[name + 'Value'] = str(v) + # xxValue is exposed without quotes to allow to compose expressions + self._cmdVars[name + 'Value'] = attr.getValueStr(withQuotes=False) if v: self._cmdVars[attr.attributeDesc.group] = self._cmdVars.get(attr.attributeDesc.group, '') + \ diff --git a/meshroom/core/utils.py b/meshroom/core/utils.py new file mode 100644 index 0000000000..edd1c25789 --- /dev/null +++ b/meshroom/core/utils.py @@ -0,0 +1,5 @@ +COLORSPACES = ["AUTO", "sRGB", "rec709", "Linear", "ACES2065-1", "ACEScg", "Linear ARRI Wide Gamut 3", + "ARRI LogC3 (EI800)", "Linear ARRI Wide Gamut 4", "ARRI LogC4", "Linear BMD WideGamut Gen5", + "BMDFilm WideGamut Gen5", "CanonLog2 CinemaGamut D55", "CanonLog3 CinemaGamut D55", + "Linear CinemaGamut D55", "Linear V-Gamut", "V-Log V-Gamut", "Linear REDWideGamutRGB", + "Log3G10 REDWideGamutRGB", "Linear Venice S-Gamut3.Cine", "S-Log3 Venice S-Gamut3.Cine", "no_conversion"] diff --git a/meshroom/nodes/aliceVision/FeatureExtraction.py b/meshroom/nodes/aliceVision/FeatureExtraction.py index 16f87081f6..2dc1326031 100644 --- a/meshroom/nodes/aliceVision/FeatureExtraction.py +++ b/meshroom/nodes/aliceVision/FeatureExtraction.py @@ -1,6 +1,7 @@ __version__ = "1.3" from meshroom.core import desc +from meshroom.core.utils import COLORSPACES class FeatureExtraction(desc.AVCommandLineNode): @@ -140,8 +141,8 @@ class FeatureExtraction(desc.AVCommandLineNode): name="workingColorSpace", label="Working Color Space", description="Allows you to choose the color space in which the data are processed.", + values=COLORSPACES, value="sRGB", - values=["sRGB", "Linear", "ACES2065-1", "ACEScg", "no_conversion"], exclusive=True, uid=[0], ), diff --git a/meshroom/nodes/aliceVision/ImageProcessing.py b/meshroom/nodes/aliceVision/ImageProcessing.py index 9d795176c5..8c22e39fa6 100644 --- a/meshroom/nodes/aliceVision/ImageProcessing.py +++ b/meshroom/nodes/aliceVision/ImageProcessing.py @@ -1,6 +1,7 @@ __version__ = "3.3" from meshroom.core import desc +from meshroom.core.utils import COLORSPACES import os.path @@ -497,8 +498,8 @@ class ImageProcessing(desc.AVCommandLineNode): name="inputColorSpace", label="Input Color Space", description="Allows you to force the color space of the input image.", + values=COLORSPACES, value="AUTO", - values=["AUTO", "sRGB", "rec709", "Linear", "ACES2065-1", "ACEScg", "no_conversion"], exclusive=True, uid=[0], ), @@ -506,8 +507,8 @@ class ImageProcessing(desc.AVCommandLineNode): name="outputColorSpace", label="Output Color Space", description="Allows you to choose the color space of the output image.", + values=COLORSPACES, value="AUTO", - values=["AUTO", "sRGB", "rec709", "Linear", "ACES2065-1", "ACEScg", "no_conversion"], exclusive=True, uid=[0], ), @@ -515,8 +516,8 @@ class ImageProcessing(desc.AVCommandLineNode): name="workingColorSpace", label="Working Color Space", description="Allows you to choose the color space in which the data are processed.", + values=COLORSPACES, value="Linear", - values=["sRGB", "rec709", "Linear", "ACES2065-1", "ACEScg", "no_conversion"], exclusive=True, uid=[0], enabled=lambda node: not node.applyDcpMetadata.value, diff --git a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py index 2c070a2793..a467b16737 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrCalibration.py +++ b/meshroom/nodes/aliceVision/LdrToHdrCalibration.py @@ -6,6 +6,7 @@ from collections import Counter from meshroom.core import desc +from meshroom.core.utils import COLORSPACES def findMetadata(d, keys, defaultValue): v = None @@ -126,8 +127,8 @@ class LdrToHdrCalibration(desc.AVCommandLineNode): 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'.", - value="auto", - values=["auto", "sRGB", "Linear", "ACES2065-1", "ACEScg"], + values=COLORSPACES, + value="AUTO", exclusive=True, uid=[], group="user", # not used directly on the command line diff --git a/meshroom/nodes/aliceVision/LdrToHdrMerge.py b/meshroom/nodes/aliceVision/LdrToHdrMerge.py index 3e5ca2c19f..564484606d 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrMerge.py +++ b/meshroom/nodes/aliceVision/LdrToHdrMerge.py @@ -6,6 +6,7 @@ from collections import Counter from meshroom.core import desc +from meshroom.core.utils import COLORSPACES def findMetadata(d, keys, defaultValue): v = None @@ -169,8 +170,8 @@ class LdrToHdrMerge(desc.AVCommandLineNode): 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'.", - value="auto", - values=["auto", "sRGB", "Linear", "ACES2065-1", "ACEScg", "no_conversion"], + values=COLORSPACES, + value="AUTO", exclusive=True, uid=[0], enabled=lambda node: node.byPass.enabled and not node.byPass.value, diff --git a/meshroom/nodes/aliceVision/LdrToHdrSampling.py b/meshroom/nodes/aliceVision/LdrToHdrSampling.py index 2ce8fa1567..9ab5c1f968 100644 --- a/meshroom/nodes/aliceVision/LdrToHdrSampling.py +++ b/meshroom/nodes/aliceVision/LdrToHdrSampling.py @@ -6,6 +6,7 @@ from collections import Counter from meshroom.core import desc +from meshroom.core.utils import COLORSPACES def findMetadata(d, keys, defaultValue): @@ -126,8 +127,8 @@ class LdrToHdrSampling(desc.AVCommandLineNode): 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'.", - value="auto", - values=["auto", "sRGB", "Linear", "ACES2065-1", "ACEScg", "no_conversion"], + values=COLORSPACES, + value="AUTO", exclusive=True, uid=[0], enabled=lambda node: node.byPass.enabled and not node.byPass.value, diff --git a/meshroom/nodes/aliceVision/PanoramaPostProcessing.py b/meshroom/nodes/aliceVision/PanoramaPostProcessing.py index 909eb96bd1..81a975aa78 100644 --- a/meshroom/nodes/aliceVision/PanoramaPostProcessing.py +++ b/meshroom/nodes/aliceVision/PanoramaPostProcessing.py @@ -4,6 +4,7 @@ import os from meshroom.core import desc +from meshroom.core.utils import COLORSPACES class PanoramaPostProcessing(desc.CommandLineNode): @@ -58,8 +59,8 @@ class PanoramaPostProcessing(desc.CommandLineNode): name="outputColorSpace", label="Output Color Space", description="The color space of the output image.", + values=COLORSPACES, value="Linear", - values=["sRGB", "rec709", "Linear", "ACES2065-1", "ACEScg"], exclusive=True, uid=[0], ), diff --git a/meshroom/nodes/aliceVision/PanoramaWarping.py b/meshroom/nodes/aliceVision/PanoramaWarping.py index cc29771d9b..be70ce9f0c 100644 --- a/meshroom/nodes/aliceVision/PanoramaWarping.py +++ b/meshroom/nodes/aliceVision/PanoramaWarping.py @@ -4,6 +4,7 @@ import os from meshroom.core import desc +from meshroom.core.utils import COLORSPACES class PanoramaWarping(desc.AVCommandLineNode): @@ -69,8 +70,8 @@ class PanoramaWarping(desc.AVCommandLineNode): name="workingColorSpace", label="Working Color Space", description="Colorspace in which the panorama warping will be performed.", + values=COLORSPACES, value="Linear", - values=["Linear", "ACES2065-1", "ACEScg", "no_conversion"], exclusive=True, uid=[0], ), diff --git a/meshroom/nodes/aliceVision/Texturing.py b/meshroom/nodes/aliceVision/Texturing.py index df5aa1234b..ed0b491b6f 100644 --- a/meshroom/nodes/aliceVision/Texturing.py +++ b/meshroom/nodes/aliceVision/Texturing.py @@ -1,6 +1,7 @@ __version__ = "6.0" from meshroom.core import desc, Version +from meshroom.core.utils import COLORSPACES import logging @@ -259,8 +260,8 @@ class Texturing(desc.AVCommandLineNode): name="workingColorSpace", label="Working Color Space", description="Color space for the texturing internal computation (does not impact the output file color space).", + values=COLORSPACES, value="sRGB", - values=("sRGB", "Linear", "ACES2065-1", "ACEScg"), exclusive=True, uid=[0], advanced=True, @@ -269,8 +270,8 @@ class Texturing(desc.AVCommandLineNode): name="outputColorSpace", label="Output Color Space", description="Color space for the output texture files.", + values=COLORSPACES, value="AUTO", - values=("sRGB", "rec709", "Linear", "ACES2065-1", "ACEScg", "AUTO"), exclusive=True, uid=[0], ), diff --git a/tests/test_nodeCommandLineFormatting.py b/tests/test_nodeCommandLineFormatting.py new file mode 100644 index 0000000000..8cf8c9076d --- /dev/null +++ b/tests/test_nodeCommandLineFormatting.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# coding:utf-8 +import os +import tempfile + +import meshroom.multiview +from meshroom.core.graph import Graph +from meshroom.core.node import Node + + +def test_formatting_listOfFiles(): + inputImages = ['/non/existing/fileA', '/non/existing/with space/fileB'] + + graph = Graph('') + n1 = graph.addNewNode('CameraInit') + n1.viewpoints.extend([{'path': image} for image in inputImages]) + # viewId, poseId, path, intrinsicId, rigId, subPoseId, metadata + assert n1.viewpoints.getValueStr() == '-1 -1 "/non/existing/fileA" -1 -1 -1 "" -1 -1 "/non/existing/with space/fileB" -1 -1 -1 ""' + + assert n1.allowedCameraModels.getValueStr() == '"pinhole,radial1,radial3,brown,fisheye4,fisheye1,3deanamorphic4,3deradial4,3declassicld"' + + graph = Graph('') + n1 = graph.addNewNode('ImageMatching') + assert n1.featuresFolders.getValueStr() == '' + + n1.featuresFolders.extend("single value with space") + assert n1.featuresFolders.getValueStr() == '"single value with space"' + + n1.featuresFolders.resetValue() + assert n1.featuresFolders.getValueStr() == '' + + n1.featuresFolders.extend(inputImages) + assert n1.featuresFolders.getValueStr() == '"/non/existing/fileA" "/non/existing/with space/fileB"' + + n1._buildCmdVars() # prepare vars for command line creation + # and check some values + name = 'featuresFolders' + assert n1._cmdVars[name + 'Value'] == '/non/existing/fileA /non/existing/with space/fileB' + + +def test_formatting_strings(): + graph = Graph('') + n1 = graph.addNewNode('ImageMatching') + name = 'weights' + assert n1.weights.getValueStr() == '""' # Empty string should generate empty quotes + assert n1._cmdVars[name + 'Value'] == '' + name = 'method' + assert n1.method.getValueStr() == '"SequentialAndVocabularyTree"' + assert n1._cmdVars[name + 'Value'] == 'SequentialAndVocabularyTree' + + n2 = graph.addNewNode('ImageMatching') + n2._buildCmdVars() # prepare vars for command line creation + name = 'featuresFolders' + assert n2._cmdVars[name + 'Value'] == '', 'Empty list should become fully empty' + n2.featuresFolders.extend('') + n2._buildCmdVars() # prepare vars for command line creation + assert n2.featuresFolders.getValueStr() == '""', 'A list with one empty string should generate empty quotes' + assert n2._cmdVars[name + 'Value'] == '', 'The Value is always only the value, so empty here' + n2.featuresFolders.extend('') + n2._buildCmdVars() # prepare vars for command line creation + assert n2.featuresFolders.getValueStr() == '"" ""', 'A list with 2 empty strings should generate quotes' + assert n2._cmdVars[name + 'Value'] == ' ', 'The Value is always only the value, so 2 empty with the space separator in the middle' + + +def test_formatting_groups(): + graph = Graph('') + n3 = graph.addNewNode('ImageProcessing') + n3._buildCmdVars() # prepare vars for command line creation + name = 'sharpenFilter' + assert n3.sharpenFilter.getValueStr() == '"False:3:1.0:0.0"' + assert n3._cmdVars[name + 'Value'] == 'False:3:1.0:0.0', 'The Value is always only the value, so no quotes' + name = 'fillHoles' + assert n3._cmdVars[name + 'Value'] == 'False', 'Booleans' + name = 'noiseFilter' + assert n3.noiseFilter.getValueStr() == '"False:uniform:0.0:1.0:True"' + assert n3._cmdVars[name + 'Value'] == 'False:uniform:0.0:1.0:True' +