Skip to content

Commit

Permalink
Merge pull request #2251 from alicevision/dev/addCameraColorSpaces
Browse files Browse the repository at this point in the history
Add camera color spaces
  • Loading branch information
fabiencastan authored Feb 2, 2024
2 parents 1141d44 + c6d0933 commit 18c393d
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 29 deletions.
50 changes: 39 additions & 11 deletions meshroom/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 = ''
Expand All @@ -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()
Expand Down
14 changes: 10 additions & 4 deletions meshroom/core/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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, '') + \
Expand Down
5 changes: 5 additions & 0 deletions meshroom/core/utils.py
Original file line number Diff line number Diff line change
@@ -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"]
3 changes: 2 additions & 1 deletion meshroom/nodes/aliceVision/FeatureExtraction.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__version__ = "1.3"

from meshroom.core import desc
from meshroom.core.utils import COLORSPACES


class FeatureExtraction(desc.AVCommandLineNode):
Expand Down Expand Up @@ -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],
),
Expand Down
7 changes: 4 additions & 3 deletions meshroom/nodes/aliceVision/ImageProcessing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__version__ = "3.3"

from meshroom.core import desc
from meshroom.core.utils import COLORSPACES

import os.path

Expand Down Expand Up @@ -497,26 +498,26 @@ 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],
),
desc.ChoiceParam(
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],
),
desc.ChoiceParam(
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,
Expand Down
5 changes: 3 additions & 2 deletions meshroom/nodes/aliceVision/LdrToHdrCalibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions meshroom/nodes/aliceVision/LdrToHdrMerge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions meshroom/nodes/aliceVision/LdrToHdrSampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from collections import Counter

from meshroom.core import desc
from meshroom.core.utils import COLORSPACES


def findMetadata(d, keys, defaultValue):
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion meshroom/nodes/aliceVision/PanoramaPostProcessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os

from meshroom.core import desc
from meshroom.core.utils import COLORSPACES


class PanoramaPostProcessing(desc.CommandLineNode):
Expand Down Expand Up @@ -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],
),
Expand Down
3 changes: 2 additions & 1 deletion meshroom/nodes/aliceVision/PanoramaWarping.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os

from meshroom.core import desc
from meshroom.core.utils import COLORSPACES


class PanoramaWarping(desc.AVCommandLineNode):
Expand Down Expand Up @@ -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],
),
Expand Down
5 changes: 3 additions & 2 deletions meshroom/nodes/aliceVision/Texturing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__version__ = "6.0"

from meshroom.core import desc, Version
from meshroom.core.utils import COLORSPACES
import logging


Expand Down Expand Up @@ -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,
Expand All @@ -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],
),
Expand Down
77 changes: 77 additions & 0 deletions tests/test_nodeCommandLineFormatting.py
Original file line number Diff line number Diff line change
@@ -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'

0 comments on commit 18c393d

Please sign in to comment.