colour
colour copied to clipboard
Implement support for alternative set of parametric transfer functions using the new CLF definitions.
Hi,
We discussed a bit about using the new CLF definitions to build some alternative variants for various camera log transfer functions. The main interest of such functionality is to be able to generate CLF LUTs by appending directly the transfer function of a colourspace with its metadata attached (or the necessary metadata) which in turn would inform one of the colour.models.logarithmic_function_camera
definition for example.
As there are a few ways to go about that, thus I'm creating a new issue to discuss this.
This relates to #636.
I have five somewhat overlapping desires:
-
Easily represent / convert
RGBColourspace
s to CLF transforms The CLFLog
andExponent
ProcessNodes (and in turncolour
's newly-addedexponent_function...
andlogarithmic_function...
methods) are capable of representing most, but not all, of the various decoding and encoding CCTFs supported bycolour
. For those that cannot be represented, it would be nice to have a mechanism for sampling (and possibly caching) a transform to acolour.LUT1D
or acolour.LUT3x1D
(which, in turn, can be serialized to CLF). Ideally, we'd have a standard idiom for converting RGB_Colourspaces to other representations that "does the right thing" without much user intervention. -
Serve as a reference for pre-computed
Log
CLF /LogCameraTransform
OCIO parameters for known CCTFs (and relevant permutations). @nick-shaw put it perfectly in slack: "Colour is an amazingly useful reference for colour science formulae and other information gathered in one place before you even start using the code!" I couldn't agree more. Among other things, it's definitely an extremely useful one-stop shop for device primaries, whitepoint, and vonkries methods data; and a map of precomputed coefficients for parametric representations of different device CCTFs seems like a natural next step. For the representable CCTFs, I propose we provide computed coefficients for the general formx < cut ? e * x + f : a + log(c * x + d) / log(base) + b
. Compared to the CLF and OCIO parameterization, we'd provide as a convenience an additional value forf
(the offset of the linear component). (A worthwhile deliverable, perhaps forcolour-nuke
, is a Nuke expression node with presets for different cameras). -
Optionally instantiate
RGBColourspace
s withcolour.models.logarithmic_function_camera
orcolour.models.exponent_function_monitor_curve
kwargs (i.e., in place of specifyingcctf_encoding
andcctf_decoding
). Being able to structure / unstructure certainRGBColourspace
instances to and from JSON would be sweet. There's a lot of value in being able to unambiguously represent an entire "color space" in a single line of text. -
Improve and expand
colour
's general LUT / transform toolset and user-experience Kind of a nebulous, larger end goal.colour
'sAbstractLUTOperator
framework makes it extremely easy to add almost arbitrary support for all sorts of custom operations (which may or may not align perfectly with CLF 3.0). Ostensibly,colour
could support its own flavor of CLF extensions without much difficulty. This is a larger conversation, and my fingers are getting tired. -
Improve
colour
's support for CLF / CTF / OCIO / ACES / etc. Another less-specific goal. To what degreecolour
could / should provide "support" for OCIO, ACES, etc. is definitely outside the scope of this issue, but it's worth considering how users will want to use parametric transfer function data outsidecolour
itself, and to what degreecolour
can serve as a conduit between representations and uses.
Hello,
1, 2, 3
So at the moment, we have that kind of functionality in the colour.RGB_Colourspace
class:
colour.RGB_Colourspace.matrix_RGB_to_XYZ
if not self._use_derived_matrix_RGB_to_XYZ:
return self._matrix_RGB_to_XYZ
else:
return self._derived_matrix_RGB_to_XYZ
colour.RGB_Colourspace.matrix_XYZ_to_RGB
if not self._use_derived_matrix_XYZ_to_RGB:
return self._matrix_XYZ_to_RGB
else:
return self._derived_matrix_XYZ_to_RGB
And the behaviour can be changed with the following methods:
-
colour.RGB_Colourspace.use_derived_matrix_RGB_to_XYZ
-
colour.RGB_Colourspace.use_derived_matrix_XYZ_to_RGB
-
colour.RGB_Colourspace.use_derived_transformation_matrices
Note: I think we should reverse the conditions, starting with if not
is not great!
Maybe the colour.RGB_Colourspace.cctf_encoding
and colour.RGB_Colourspace.cctf_decoding
properties could follow the same logic, e.g.
colour.RGB_Colourspace.cctf_encoding
if self._use_parametric_cctf_encoding:
if self._parametric_cctf_encoding is None:
raise RuntimeError('Parametric decoding CCTF for "{0}" colourspace is not defined!'.format(self._name))
return self._parametric_cctf_encoding
else:
return self._cctf_encoding
Then, in the __init__
method, we could maybe have something along those lines:
if parametric_cctf_encoding_kwargs is not None:
self._parametric_cctf_encoding_kwargs = parametric_cctf_encoding_kwargs
parametric_cctf_encoding_callable = parametric_cctf_encoding_kwargs.pop('callable')
self._parametric_cctf_encoding = partial(parametric_cctf_encoding_callable, **parametric_cctf_encoding_kwargs)
with parametric_cctf_encoding_kwargs={'callable': exponent_function_monitor_curve, 'exponent': 2.4, 'offset': 0.055, 'style': 'monCurveFwd'}
And three new methods:
-
colour.RGB_Colourspace.use_parametric_cctf_encoding
-
colour.RGB_Colourspace.use_parametric_cctf_decoding
-
colour.RGB_Colourspace.use_parametric_cctfs
As far as the conversion to 1D/3x1D and JSON serialisation goes, sure!
Compared to the CLF and OCIO parameterization, we'd provide as a convenience an additional value for f(the offset of the linear component)
That would break compatibility with CLF / OCIO though no?
4
Yes, a lot of possibilities there!
5
Once the data is in, it should be not so bad to make it available elegantly, this, however, raises the point about what is the canonical source for the parameterics values?
Hello,
1, 2, 3
So at the moment, we have that kind of functionality in the
colour.RGB_Colourspace
class: ... Maybe thecolour.RGB_Colourspace.cctf_encoding
andcolour.RGB_Colourspace.cctf_decoding
properties could follow the same logic
I was thinking the exact same thing.
Then, in the
__init__
method, we could maybe have something along those lines:if parametric_cctf_encoding_kwargs is not None: self._parametric_cctf_encoding_kwargs = parametric_cctf_encoding_kwargs parametric_cctf_encoding_callable = parametric_cctf_encoding_kwargs.pop('callable') self._parametric_cctf_encoding = partial(parametric_cctf_encoding_callable, **parametric_cctf_encoding_kwargs)
with
parametric_cctf_encoding_kwargs={'callable': exponent_function_monitor_curve, 'exponent': 2.4, 'offset': 0.055, 'style': 'monCurveFwd'}
What do you think about inferring the callable from the style? Providing the callable seems redundant, no? Otherwise, we may as well use a partial directly. (I also have other thoughts on styles in general -- basically, I'd prefer not to expose CLF styles at all, and rely on idioms like "encoding" / "decoding", or "forward / "reverse". For exponents, maybe we can provide a "negative_style"
My Log
class implements an external "log_function_factory" method that validates parameters and poops out partials based on the style (which is determined by an external method, detailed later) -- I think a similar approach might work pretty well for exponent functions -- i.e., if value for offset
is provided, we must need an exponent_function_monitor_curve
. And we can have a negative_style
parameter whose value can be either "mirror" or "passthru" (or None, the default).
def logarithmic_function_factory(lin_side_slope=None,
lin_side_offset=None,
log_side_slope=None,
log_side_offset=None,
lin_side_break=None,
linear_slope=None,
base=10,
decoding=True):
"""Returns an initilized *logarithmic_function_basic*,
*logarithmic_function_quasilog*, or *logarithmic_function_camera*
function for the given parameters and style.
"""
function_parameters = validate_log_parameters(
lin_side_slope=lin_side_slope,
lin_side_offset=lin_side_offset,
log_side_slope=log_side_slope,
log_side_offset=log_side_offset,
lin_side_break=lin_side_break,
linear_slope=linear_slope,
base=base,
decoding=decoding)
style = function_parameters['style']
# camera style
if style.startswith('camera'):
function = logarithmic_function_camera
# quasilog style
elif "to" in style.lower():
function = logarithmic_function_quasilog
# basic style
else:
function = logarithmic_function_basic
return partial(function, **filter_kwargs(function, **function_parameters))
Related, in my notebook, I've been putting together a Log
node factory, modeled after colour.models.rgb.transfer_functions.log_decoding
. In retrospect, I think maybe it might be more useful if it directly generated parameter dictionaries that could be used for initializing Log
instances, instead of the instances themselves. However, I'm also adding comment metadata and changing the names of certain CCTFs to reflect non-default variants -- for example:
>>> logarithmic_clf_factory('ALEXA Log C', EI=400, firmware='SUP 2.X')
Log(base=10, logSideSlope=0.2565982404692082, logSideOffset=0.39100684261974583, linSideSlope=5.061087192662738, linSideOffset=0.08900430532070719, linearSlope=5.656074507309531, linSideBreak=0.0, style="cameraLogToLin", name="ALEXA Log C v2 EI0400")
>>> print(logarithmic_clf_factory('ALEXA Log C', EI=400, firmware='SUP 2.X'))
Log - ALEXA Log C v2 EI400 - Log to Linear
------------------------------------------
style : cameraLogToLin
base : 10.0
logSideSlope : 0.256598
logSideOffset : 0.391007
linSideSlope : 5.061087
linSideOffset : 0.089004
linearSlope : 5.65619
linSideBreak : 0.0
CCTF: ALEXA Log C
Exposure Index: 400
Firmware: SUP 2.x
Method: Linear Scene Exposure Factor
Compared to the CLF and OCIO parameterization, we'd provide as a convenience an additional value for f(the offset of the linear component)
That would break compatibility with CLF / OCIO though no?
It does. I'm still on the fence about how much convenience is too much convenience. I figured we could use colour.utilities.filter_kwargs
to drop extra keys (as I was also thinking about adding log_side_break). Maybe it would make more sense to have a separate method just for computing linear_offset, log_side_break, and possibly linear_slope, or make computation an option...
I've been trying to refactor some of the complexity in my Log node, and I keep ending up writing the same jumbo function for validating and sanitizing log params:
def conform_nonuniform_values(max_channels=3, **kwargs):
if any([as_float_array(v).size > 1 for v in kwargs.values()]):
kwargs = {k: v * np.ones(max_channels) for k, v in kwargs.items()}
return kwargs
def validate_log_parameters(base=10,
lin_side_slope=None,
lin_side_offset=None,
log_side_slope=None,
log_side_offset=None,
lin_side_break=None,
linear_slope=None,
decoding=False):
base = int(base) if not float(base) == int(base) else float(base)
# If any of the following are undefined, use 'basic' style
kwargs = dict(log_side_slope=log_side_slope,
log_side_offset=log_side_offset,
lin_side_slope=lin_side_slope,
lin_side_offset=lin_side_offset)
if any([x is None for x in kwargs.values()]):
base = 'B' if not base in [2, 10] else base
style = f'antiLog{base}' if decoding else f'log{base}'
return {'base': base, 'style': style}
# If `lin_side_break` is definied, use 'camera' style
if lin_side_break is not None:
style = 'cameraLogToLin' if decoding else 'cameraLinToLog'
kwargs['lin_side_break'] = lin_side_break
if linear_slope is not None:
kwargs['linear_slope'] = linear_slope
# make sure all parameter values are the same shape
kwargs = conform_nonuniform_values(**kwargs)
# compute "linear_offset", "lin_side_break", and "linear_slope"
a, b, c, d, cut = [
kwargs[v] for v in [
'lin_side_slope', 'lin_side_offset',
'log_side_slope', 'log_side_offset',
'lin_side_break']]
if linear_slope is None:
kwargs['linear_slope'] = (
c * (a / ((a * cut + b) * np.log(base)))
)
kwargs['log_side_break'] = (
c * (np.log(a * cut + b) / np.log(base)) + d
)
kwargs['linear_offset'] = (
kwargs['log_side_break'] - kwargs['linear_slope'] * cut
)
# Otherwise, use 'quasilog' style
else:
style = 'logToLin' if decoding else 'linToLog'
# make sure all parameter values are the same shape
kwargs = conform_nonuniform_values(**kwargs)
kwargs.update({'base': base, 'style': style})
She's a bad mama jama for sure, and arguably does too much:
- Determines the value for
style
, based on which arguments are provided, and whether the style is decoding - If
lin_side_break
is provided, also computeslinear_slope
(if not provided),lin_side_break
, andlinear_offset
(taken from @njwardhan's implementation incolour.models.rgb.transfer_functions.log.logarithmic_function_camera
). - Makes sure all parameters except
base
andstyle
are either all single floats, or all RGB float tuples -- i.e., to make sure they're all the same size, and help avoid casting LUT1D tables to LUT3x1D-sized tables if the function is applied uniformly for all channels. In other words,Log.apply(...)
behaves as uniformly-applied transform unless a user articulates distinct R,G,B parameters for any argument (e.g., for PLog gamma values), in which case LUT1D table inputs are automatically resized to accommodate the shape of the parameter values)
(I also snipped out an option for converting keys from lowerCamelCase to lower_snake_case)
4
Yes, a lot of possibilities there!
See above, re: style stuff. One thing I'm eager to discuss is implementing inverse transforms, and discussing how to conceptualize and reconcile relative "transform direction" ("forwards" versus "reverse") and relative "node orientation" ("input-facing", "output-facing") styles with absolute encoding / decoding functions.
5
Once the data is in, it should be not so bad to make it available elegantly, this, however, raises the point about what is the canonical source for the parameterics values?
Maybe we could output a dictionary at build time, and if available, colour could load the precomputed values from json. It seems kind of convoluted, but I think it would be a useful artifact that gets you, in theory, the exact same parameter data as used in colour. We could then do all sorts of crazy things at that point -- maybe even procedurally generating frozen Log instances at import time or something.
1, 2, 3
Providing the callable seems redundant, no? Otherwise, we may as well use a partial directly.
Yes, good point, I'm wondering if we should not do that, it would make the implementation cleaner with less magic. If magic is required later we could still introspect the parameters from the partial:
>>> import inspect
>>> from functools import partial
>>> def foo(bar): pass
...
>>> f = partial(foo, bar=2)
>>> inspect.signature(f)
<Signature (*, bar=2)>
Should not logarithmic_function_factory
take only **kwargs
as arguments? There is a bit of branching in the validate_log_parameters
mama jama but it is not too bad. Should be careful that the parameters have similar ordering and defaults in any case.
4
Sorry, I did not understand this one properly but then it is late and I missed my post-lunch coffee :)
5
Maybe we could output a dictionary at build time
OCIO build time you mean for example? It would certainly be great to be able to obtain the parameterisation easily from wherever it is defined without having to manually extract it.
1, 2, 3
Providing the callable seems redundant, no? Otherwise, we may as well use a partial directly.
Yes, good point, I'm wondering if we should not do that, it would make the implementation cleaner with less magic. If magic is required later we could still introspect the parameters from the partial.
You're right, that makes much more sense. Seems much easier to map from RGBColorSpace encoding and decoding transforms to other representations this way, too. Right on.
Should not
logarithmic_function_factory
take only**kwargs
as arguments?
Now that you mention it... :) Looks like that call to validate_log_parameters
could use a good filter-kwargsing too. kwargs-filtering. filters kwarg.
There is a bit of branching in the
validate_log_parameters
mama jama but it is not too bad. Should be careful that the parameters have similar ordering and defaults in any case.
Word. I don't actually know what branching means, but I'll look it up and unbranchify. Intuitively, I think I have an idea of what you mean. It definitely feels awkward to me... I think you're on to something with this branching business. Good call re: parameter ordering and defaults -- I'll make sure I'm consistent with logarithmic_function_camera
4
Sorry, I did not understand this one properly but then it is late and I missed my post-lunch coffee :)
Oh god I don't know what I put this here. This is a conversation for an entirely separate thread.
In as few words as possible, I'm talking about the relationship between style "forwards" and "reverse" variants (i.e., monCurveFwd) vs cctf encoding and decoding variants vs a node's "orientation" in a DAG (output-facing vs input-facing).
As I said... a whooooole different thing.
5
Maybe we could output a dictionary at build time OCIO build time you mean for example? It would certainly be great to be able to obtain the parameterisation easily from wherever it is defined without having to manually extract it.
I didn't mean OCIO, but that's an interesting idea. Reminds me of a bug report I need to file.
Kind of a wacky idea. By "build", I think I meant generated as an artifact in a (color
) release hook, or during testing. If we were to solve approximate camera log coefficients for cctf samples, this could be a place to do it.
Maybe we could generate an XML catalog of CLF transforms for different parameterizations? That could be very powerful and handy. A job for colour_datasets
, maybe?