Aaand I just found the Python prototype…
%matplotlib notebook
import numpy as np
import matplotlib
matplotlib.rcParams['pdf.fonttype']=42
matplotlib.rcParams['ps.fonttype']=42
import matplotlib.pyplot as plt
from ipywidgets import interact
plt.style.use('bmh')
# define the curve via these control vertices here:
x=np.array([0.3,0.45,0.80,1.00])
y=np.array([0.0,0.62,0.14,0.97])
m=np.array([0.0,0.0,0.0,0.0,0.0])
d=np.array([0.0,0.0,0.0,0.0,0.0])
def fma(a, b, c):
# fake FMA just for the logic of it
return a * b + c
def filmic_spline_draw(x, coeffs, toe, shoulder):
# coeffs = [a, b, c, d, e, f, g, h, i, j, k, l]
# s.t. :
# P0 = ax⁴ + bx³ + cx² + dx + e
# P1 = fx + g
# P2 = hx⁴ + ix³ + jx² + kx + l
# latitude = [toe ; shoulder] < dynamic range
# See https://bit.ly/2IXfLnQ
# build the value masking row vector
part0 = x < toe
part2 = x > shoulder
part1 = part0 == part2 # where part0 == part2 == False, toe < x < shoulder
mask = np.array([part0, part1, part2])
# make x a 2D row vector
x = x[np.newaxis, :]
# unpack coeffs
a = coeffs[0]
b = coeffs[1]
c = coeffs[2]
d = coeffs[3]
e = coeffs[4]
f = coeffs[5]
g = coeffs[6]
h = coeffs[7]
i = coeffs[8]
j = coeffs[9]
k = coeffs[10]
l = coeffs[11]
# repack coeffs as column vectors
M1 = np.array([[e, g, l]]).transpose() # const
M2 = np.array([[d, f, k]]).transpose() # factors of x
M3 = np.array([[c, 0, j]]).transpose() # factors of x²
M4 = np.array([[b, 0., i]]).transpose() # factors of x³
M5 = np.array([[a, 0., h]]).transpose() # factors of x⁴
# evaluate the 3 parts of the curve
y = fma(x, fma(x, (fma(x, fma(x, M5, M4), M3)), M2), M1)
# apply masks and sum
return (y * mask).sum(axis=0)
def filmic_desaturation_draw(x, coeffs, toe, shoulder):
# coeffs = [a, b, c, d, e, f, g, h, i, j, k, l]
# s.t. :
# P0 = ax⁴ + bx³ + cx² + dx + e
# P1 = fx + g
# P2 = hx⁴ + ix³ + jx² + kx + l
# latitude = [toe ; shoulder] < dynamic range
# See https://bit.ly/2IXfLnQ
# build the value masking row vector
part0 = x < toe
part2 = x > shoulder
part1 = part0 == part2 # where part0 == part2 == False, toe < x < shoulder
mask = np.array([part0, part1, part2])
# make x a 2D row vector
x = x[np.newaxis, :]
# unpack coeffs
a = coeffs[0]
b = coeffs[1]
c = coeffs[2]
d = coeffs[3]
e = coeffs[4]
f = coeffs[5]
g = coeffs[6]
h = coeffs[7]
i = coeffs[8]
j = coeffs[9]
k = coeffs[10]
l = coeffs[11]
# repack coeffs as column vectors
M3 = np.array([[c, 0, j]]).transpose() # factors of x²
M4 = np.array([[b, 0., i]]).transpose() # factors of x³
M5 = np.array([[a, 0., h]]).transpose() # factors of x⁴
# evaluate the 3 parts of the curve
y = 2 * fma(x, 3 * fma(x, 2 * M5, M4), M3)
# apply masks and sum
return np.abs((y * mask).sum(axis=0)) / 8.
def filmic_spline_solve(T_l, T_d, S_l, S_d):
# Contrast/slope of the latitude
C = (S_d - T_d) / (S_l - T_l)
# Get params of the linear part : P1 = fx + g
M1 = np.array([[1. , 0.],
[T_l , 1.]])
y1 = np.array([C, T_d])
P1 = np.linalg.solve(M1, y1)
f = P1[0]
g = P1[1]
# Get params of the cubic toe P0 = ax⁴ + bx³ + cx² + dx + e
M0 = np.array([[0. , 0. , 0. , 0. , 1.],
[0. , 0. , 0. , 1. , 0.],
[T_l**4 , T_l**3 , T_l**2 , 1. , 0.],
[4. * T_l**3 , 3. * T_l**2 , 2. * T_l , 1. , 0.],
[12. * T_l**2 , 6. * T_l , 2. , 0. , 0.]])
y0 = np.array([0., 0., T_d, f, 0.])
P0 = np.linalg.solve(M0, y0)
a = P0[0]
b = P0[1]
c = P0[2]
d = P0[3]
e = P0[4]
# Get params of the cubic shoulder P2 = hx⁴ + ix³ + jx² + kx + l
M2 = np.array([[1. , 1. , 1. , 1. ],
[S_l**3 , S_l**2 , S_l , 1.],
[3. * S_l**2 , 2. * S_l, 1. , 0.],
[6. * S_l , 2. , 0. , 0.]])
y2 = np.array([1., S_d, f, 0.])
P2 = np.linalg.solve(M2, y2)
h = 0.
i = P2[0]
j = P2[1]
k = P2[2]
l = P2[3]
# pack the params of the solution
return [a, b, c, d, e, f, g, h, i, j, k, l]
fig, ax = plt.subplots(figsize=(5,5))
ax.set_ylim([-2.,2.])
ax.set_xlim([-2.,2.])
pos = np.linspace(0., 1., 1000) # 4 Mpix to make benchmarks meaningful
ax.axvline(x=0.0,ymin=0.0,ymax=1.0)
ax.axvline(x=1.0,ymin=0.0,ymax=1.0)
px1 = ax.axvline(x=x[1],ymin=0.0,ymax=1.0)
px2 = ax.axvline(x=x[2],ymin=0.0,ymax=1.0)
ax.axhline(y=0.0,xmin=0.0,xmax=1.0)
ax.axhline(y=1.0,xmin=0.0,xmax=1.0)
plot1 = ax.plot(pos, np.zeros(pos.shape), color='C1')[0]
plot2 = ax.plot(pos, np.zeros(pos.shape), color='C3')[0]
plot3 = ax.plot(pos, np.zeros(pos.shape), color='C5')[0]
@interact(ix1=(0.0,1.0,0.01),ix2=(0.0,1.0,0.01),iy1=(0.0,1.0,0.01),iy2=(0.0,1.0,0.01))
def test_spline(iy1=0.55, iy2=0.92, ix1=0.67, ix2=0.92):
y[0] = 0.
y[1] = iy1
y[2] = iy2
y[3] = 1.
x[0] = 0.
x[1] = ix1
x[2] = ix2
x[3] = 1.
px1.set_xdata(x[1])
px2.set_xdata(x[2])
# plt.plot(pos, np.power(pos, 0)*c[0]+ pos*c[1] + pos*pos*c[2] + pos*pos*pos*c[3])
setup_spline()
plot1.set_ydata(np.array([hermite(t) for t in pos]))
params = filmic_spline_solve(ix1, iy1, ix2, iy2)
y_filmic = filmic_spline_draw(pos, params, ix1, ix2)
plot2.set_ydata(y_filmic)
y_desat = filmic_desaturation_draw(pos, params, ix1, ix2)
plot3.set_ydata(y_desat)
ax.legend((plot1, plot2, plot3), ["Hermite", "Filmic", "Desaturation"])
fig.canvas.draw()
# test_spline(0.0, 0.2, 0.8, 1.0)