So, there’s been quite a bit of discussion around filmic. Some find it hard to use, some want to change the curve around, and I … little bit of both. I think it’s a very useful tool, as evidenced by quite a few examples around these forums. But I also find it less intuitive. Which is of course always a subjective quality, but having a background in engineering maths and software for end users implementing mathematical models, I’ve also learned a bit about “user-proofing” inputs, and what a difference you can make by deciding to parameterize the model in different ways, and how those parameters are presented and/or limited. So I tried to apply some of those things to filmic, and below is where I got.
First, I need to thank @anon41087856, who was patient enough to explain a few of the concepts behind filmic in his blog, on this forum. I’ve got a decent enough idea about colour spaces but that’s mostly from a safe distance, so It’s been very helpful that he put some of his notes and testing code up online, and answered a few questions, too. Also credit to him for even starting the whole filmic business in Darktable, of course. It’s what even got me to fire up DT after a long time.
A quick mock-up of the proposed curve
Right, so here’s an interactive graph of what I came up with. You can drag some of the bits around the graph, zoom in and out, or manipulate the inputs on the left-hand side, to see how the curve behaves.
Blue is the new suggestion, black is (my interpretation of) the current 4th order polynomials in use by current Darktable (v3.4). The x axis is the logarithm of the (scene-referred, linear) input with theoretically-unlimited dynamic range, and the y axis (labelled O for Output) is the mapped output for display on a screen or for printing (which has to fit in between 0 and 1):
The controls in more detail:
- x_0, x_1 define the upper and lower bound. This determines where the two ends of the curve land on the graph. You can just drag the endpoints left or right, too. As long as c=0, this is equivalent to the current shadow and highlight range sliders.
- c determines the center of the range, in terms of input values. This defaults to 0, as it does in the current filmic version.
- O_c determines to which output level the center value is mapped. Default is 0.5, as per current filmic and conventional photographer wisdom (that is: in the graph. This is actually your 18% reference gray, but plotted on a linear scale, referring to optical density, as in the example graph, it’s 50%). You really don’t have to move it, but I’ll explain later why it might still be useful sometimes.
- l_{low} and l_{hi} determine the extents of the linear segment. This is how far above and below c, the curve stays linear. This is an alternative way to specify the “latitude” and “shadow/highlight balance” settings
- g is the steepness (“gradient”) of the linear segment and is equivalent to the “contrast” setting in filmic.
- b_{hyplow} and b_{hyphi} are two additional tuning parameters to influence how “flat” or “crunchy” the roll-off curves above and below the linear segment are. Depending on the other settings, you may have more or less range to play with there.
What’s with the roll-off curves?
One frustration I sometimes have with filmic in its current state is that the mapping curve can overshoot and produce negative values, like so:
If that happens and you’re not actually looking at the curve, you might think you’re increasing the shadow range but actually everything that’s orange in that graph produces negative numbers which are clipped to zero, and so the bottom 5 stops or so are mapped to pitch black. So you would actually include more shadows if you reduced the shadow range at this point. Of course, the overshoot can be dealt with by decreasing contrast in the look tab, or reducing latitude but that might not be the look you were going for, and it’s one more thing a user needs to notice and take into account while tweaking filmic.
However, this can be dealt with by using a curve that does not overshoot, ever.
This would also be closer to actual film because I don’t think there’s any film response curve which gets lighter first, then darker again. So if we can find something that’s mathematically forced to stay monotonic, the user won’t have to pay attention to this issue and solve it manually.
I have done modelling of enzyme reactions and photogrammetry of fluorescing proteins in the past, and one important curve in that context is the Michaelis-Menten curve. As the input increases, output increases, then asymptotically approaches saturation. That’s very similar to what the upper end of measured curves from actual film look like (and the chemistry is similar, in the very broad terms that shooting a photon at an almost-overexposed film has less of an effect than at a less-exposed one).
In slightly rearranged form, the Michaelis-Menten curve has this equation:
f(x) = a \frac{x}{x+c}
However, this simple hyperbola doesn’t quite give me enough degrees of freedom to make it fit both the tangent and the end point of the exposure range. Another search finds that the original filmic presentation cites some of the curves being used as slightly expanded hyperbola equations (slides 55 and 56). Slightly rearranged again:
f(x) = \frac{ax^2 + bx}{ax^2 + dx +c}
However, the curve above is meant to give a complete mapping from top to bottom, with no linear segment (because it models the whole sigmoid). Having four parameters also makes it a little harder to handle if you want to derive something parametric for users to play with, as the numbers are not very intuitive if you’re not used to them.
So, the version that I settled on is this:
f(x) = a\frac{bx^2 + x}{bx^2 + x + c}
The fraction converges to 1 as x approaches infinity, and so a tells us the asymptotic value. c is the parameter which determines the steepness of the tangent at x=0 (because bx^2 is zero, and has a derivative of zero if x=0), and b is a parameter we can use to tune the shape of the curve a little.
I worked out how which values these parameters need to get in order to conform to the following constraints:
- curve starts at the end of the linear segment, with the same steepness as the linear piece
- the curve must go through the user-determined end point of the exposure range. So the upper roll-off curve reaches 1 at x_1
- That’s it, really. This would be enough.
- However, to find the useful range for b, I’ve also worked out at which value of b the curvature (i.e. second derivative) of the curve at the hand-over point to the linear segment becomes zero. The useful range is then between 0 and whatever that value is. With b=0, you get a plain Michaelis-Menten curve, and with b=b_{max}, you get a rounder version of the curve, for “crunchier” shadows/highlights. If you went beyond b=b_{max}, the curve would be curving the wrong way first, which I don’t think makes too much sense.
The constraints on inputs
This was originally the main reason I started playing with these curves, before coming up with the hyperbola. I think by exposing the right kind of parameters and constraining them in the right way, the whole tool can become both more fool-proof, intuitive and require less back-and-forth to tune the curves to get the desired result (or to realize that your photo needs local adaptations to get the desired result…):
- the contrast parameter is constrained so that the linear segment can not be flatter than a straight line between the end points. Currently, the lowest contrast setting is 1.0 which is close to a straight line when using the minimum dynamic range (from -1EV to +1EV). If that happens, the new hyperbolae turn into straight lines, too. The upper bound is set so that the end points of the linear range don’t map to less than 0.1 or more than 0.9. That is to make sure that the roll-off curves still have something to work with, and to avoid negative inputs to the equations, because that would break them (in addition to falling outside the meaningful range)
- The linear ranges, l_{low} and l_{hi}, can be moved all the way to the ends of the exposure range – but the contrast parameter will be adapted using the rule above to make sure the line does not extend to negative numbers. So if you have a steep contrast and extend the linear range too far, it will automatically be come flatter.
- The tuning parameters b_{hyplow} and b_{hyphi} are constrained to stay between 0 and whatever value gives the curve zero curvature at the hand-over point. You can try removing that bound in Desmos and moving beyond it to see how the curve starts over-shooting. Negative numbers also have funny consequences, because they can permit the denominator in the equation to reach zero in the range of interest, which gets you a vertical asymptote.
There are some instances where the constraints fail, which may have to do with how Desmos implements/checks them. I think that could be done more robustly. Or maybe I overlooked some conditions that should have been checked? Please feel free to play with the graph and tell me what you think I missed.
Exposed handles, scaling
This was the second reason I started with this: I though it must be possible to reduce the number of sliders (or other DT modules) which a user needs to adjust and iterate between to get what they want. Humans are amazingly adaptive with such things (otherwise we could not ride bicycles), but if we can find a system that requires less adaptation, that should work quicker and for more people.
exposure/middle grey
Here’s an example. If I set up a curve in filmic to include the complete brightness range of a photo, but then decide that the picture needs overall brightening, I can go to the exposure module and correct exposure. This changes the input seen by filmic, and means that I then also need to adjust the shadow and highlight ranges, and possibly some other settings, too. The other way of brightening the picture is changing the middle grey luminance input. However, here are before/after screenshots of adjusting nothing but middle grey (rather large adjustment to make the effect more visible):
Note how both the shadow and highlight range were extended at the same time. I’m not sure if this is intended (and if yes, what the reason is), or maybe a bug (of which I know a few were discovered and fixed since v 3.4 came out), but the shadow end is now way below the point where it started, and the highlights in the picture have actually become darker. All ends of the curve are moving at the same time, and I need to re-adjust them again.
The c parameter in the proposed setup works a little differently: It does not touch the upper or lower end of the range and simply puts the pivot at a different input level. This means if you move it by 1EV down, 1 more EV is added to the highlight range, and 1 EV is removed from the shadow range, so whatever the darkest and brightest inputs which the users wanted to map to output black and white, they stay where they were:
I feel that this is a more intuuitive way of changing exposure, and (unless I misunderstand what the exposure module does), I think it is mathematically equivalent to changing exposure and then adapting the relative white and black exposure sliders in filmic to match the shifted input levels. Except it can be done using a single number.
Now, it might be that there are other modules applied between exposure compensation and filmic which benefit from having exposure corrected right at the start – I’m writing this to get some feedback, so please let me know! – but at least for a quick tweak, this should deliver exactly what most users would hope to achive with it. Even in more involved cases, if all the in-between modules are already set up and a user wants to modify overall exposure, changing it this way could avoid having to re-adjust all the other
latitude
I think the “latitude” and “shadows/highlight balance” parameters generally work well enough. However, if I find that there is a case to use parameters which affect only either shadows or highlights. So e.g. if my shadows are a little too crushed and I want to make more space for the shadow roll-off curve, I can change just one end, directly see the effect and know that I don’t need to check if it also affected highlights. Mathematically, this the results are completely equivalent to the current state – this is just a different way to input two numbers which define the linear segment.
That said: I’m actually starting to think it might be even better to let users input the proportion of the shadow range covered by the linear segment. So the user would specify either \frac{l_{low}}{c-x_0}, or \frac{g ~l_{low}}{O_c}. This means adjusting the black exposure would change the length of the linear segment, but the relative proportion (how much is linear vs. how much is covered by the roll-off curve) would stay constant. Opinions?
The mathematics of it
I’ll write up the full maths in another post here (because this is getting long), but all equations are in the Desmos widget above (equations can be copied and pasted as LaTeX from Desmos), but here’s a quick explainer:
This is the core function itself:
f(x) = a\frac{bx^2 + x}{bx^2 + x + c}
And this is its derivative (i.e. the steepness of the curve)
f'(x) = ac\frac{2bx + 1}{ \left( bx^2 + x + c \right)^2}
We can directly see that f(x=0)=0 – that’s fine. It’s actually very convenient, because we can move it to whatever x it needs to be later.
What we need to impose is the tangent at the origin and the end point of the exposure range:
f'(x=0)=g
f(x=x_1)=y_1
Note I’m using y_1 as stand-in for whatever the difference in outputs between the end of the linear segment and the top or bottom of the output range is.
Putting those conditions into the equations above and solving for a and c gives us:
a=c g
c=\frac{y_1}{g}\frac{bx_1^{2}+x}{bx_1^{2}+x_1-\frac{y_1}{g}}
…done! That’s the shape of our curve.
So now we only need to move it to wherever we need it. Say we need the upper roll-off, that means we need the curve to start at x=x_{hi} and O=O_{hi}, and end at x=x_1 and O=1. So, simply substitute (x-x_{hi} for x in the equations above, compute y_1= 1- O_{hi}, when computing the coefficiencts a and c, and add $O_{hi} to the equation as you’re evaluating it:
c_{hyphi}=\frac{1-O_{hi}}{g}\frac{b_{hyphi}\left(x_{1}-x_{hi}\right)^{2}+\left(x_{1}-x_{hi}\right)}{b_{hyphi}\left(x_{1}-x_{hi}\right)^{2}+\left(x_{1}-x_{hi}\right)-\frac{1-O_{hi}}{g}}
a_{hyphi}=c_{hyphi}\ g
O_{hyphi}\left(x\right)=O_{hi}+a_{hyphi}\frac{b_{hyphi}\left(x-x_{hi}\right)^{2}+\left(x-x_{hi}\right)}{b_{hyphi}\left(x-x_{hi}\right)^{2}+\left(x-x_{hi}\right)+c_{hyphi}}\left\{x_{hi}\le x\le x_{1}\right\}
…and that’s the upper roll-off curve.
The lower one uses x_{low} - x and y_1=O_{low} instead, and subtracts the resulting function from O_{low}.
Conclusion (for now)
I’m hoping to get some feedback here. I’ve not programmed in C++ so far (I’m an engineer by trade, so it’s been Matlab, FORTRAN, Python most of the time), and I’ve not compiled darktable before, or looked at its source code. So I’m kind of hoping for a few things:
- Someone finds this interesting enough to implement it in DT, to actually demonstrate what it can be like IRL. I’d be more than very happy to support this, of course. Example implementation in Python would be no problem, and the derivation of the maths is coming up.
- Someone is nice enough to show me the way around the Darktable source and how to set up for compiling it. I’m very comfortable on Linux, and computer stuff doesn’t scare me – but I’m new to C++ and DT, so I wouldn’t know about code conventions used there, and would like to offend other maintainers by brazenly ignoring them.
- In either case, unless everybody thinks that my proposal is completely worthless (how dare you!), there’d be some time to invest. So before I (possibly in tandem with someone else) invest that time, I think it’s smart to solicit views and friendly advice. Would be a shame to do it only to have it rejected, realize it could have worked way better if approached a bit differently, or that there’s some relevant information which I’m completely unaware of.
So: Please let me know what you think, what you like, what you don’t, what you think I may have overlooked …