aboutsummaryrefslogtreecommitdiffhomepage
path: root/third_party/skcms/src/TransferFunction.c
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/skcms/src/TransferFunction.c')
-rw-r--r--third_party/skcms/src/TransferFunction.c432
1 files changed, 0 insertions, 432 deletions
diff --git a/third_party/skcms/src/TransferFunction.c b/third_party/skcms/src/TransferFunction.c
deleted file mode 100644
index 7bab9d9286..0000000000
--- a/third_party/skcms/src/TransferFunction.c
+++ /dev/null
@@ -1,432 +0,0 @@
-/*
- * Copyright 2018 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "../skcms.h"
-#include "../skcms_internal.h"
-#include <assert.h>
-#include <limits.h>
-#include <string.h>
-
-float skcms_TransferFunction_eval(const skcms_TransferFunction* tf, float x) {
- float sign = x < 0 ? -1.0f : 1.0f;
- x *= sign;
-
- return sign * (x < tf->d ? tf->c * x + tf->f
- : powf_(tf->a * x + tf->b, tf->g) + tf->e);
-}
-
-bool skcms_TransferFunction_isValid(const skcms_TransferFunction* tf) {
- // Reject obviously malformed inputs
- if (!isfinitef_(tf->a + tf->b + tf->c + tf->d + tf->e + tf->f + tf->g)) {
- return false;
- }
-
- // All of these parameters should be non-negative
- if (tf->a < 0 || tf->c < 0 || tf->d < 0 || tf->g < 0) {
- return false;
- }
-
- return true;
-}
-
-// TODO: Adjust logic here? This still assumes that purely linear inputs will have D > 1, which
-// we never generate. It also emits inverted linear using the same formulation. Standardize on
-// G == 1 here, too?
-bool skcms_TransferFunction_invert(const skcms_TransferFunction* src, skcms_TransferFunction* dst) {
- // Original equation is: y = (ax + b)^g + e for x >= d
- // y = cx + f otherwise
- //
- // so 1st inverse is: (y - e)^(1/g) = ax + b
- // x = ((y - e)^(1/g) - b) / a
- //
- // which can be re-written as: x = (1/a)(y - e)^(1/g) - b/a
- // x = ((1/a)^g)^(1/g) * (y - e)^(1/g) - b/a
- // x = ([(1/a)^g]y + [-((1/a)^g)e]) ^ [1/g] + [-b/a]
- //
- // and 2nd inverse is: x = (y - f) / c
- // which can be re-written as: x = [1/c]y + [-f/c]
- //
- // and now both can be expressed in terms of the same parametric form as the
- // original - parameters are enclosed in square brackets.
- skcms_TransferFunction tf_inv = { 0, 0, 0, 0, 0, 0, 0 };
-
- // This rejects obviously malformed inputs, as well as decreasing functions
- if (!skcms_TransferFunction_isValid(src)) {
- return false;
- }
-
- // There are additional constraints to be invertible
- bool has_nonlinear = (src->d <= 1);
- bool has_linear = (src->d > 0);
-
- // Is the linear section not invertible?
- if (has_linear && src->c == 0) {
- return false;
- }
-
- // Is the nonlinear section not invertible?
- if (has_nonlinear && (src->a == 0 || src->g == 0)) {
- return false;
- }
-
- // If both segments are present, they need to line up
- if (has_linear && has_nonlinear) {
- float l_at_d = src->c * src->d + src->f;
- float n_at_d = powf_(src->a * src->d + src->b, src->g) + src->e;
- if (fabsf_(l_at_d - n_at_d) > (1 / 512.0f)) {
- return false;
- }
- }
-
- // Invert linear segment
- if (has_linear) {
- tf_inv.c = 1.0f / src->c;
- tf_inv.f = -src->f / src->c;
- }
-
- // Invert nonlinear segment
- if (has_nonlinear) {
- tf_inv.g = 1.0f / src->g;
- tf_inv.a = powf_(1.0f / src->a, src->g);
- tf_inv.b = -tf_inv.a * src->e;
- tf_inv.e = -src->b / src->a;
- }
-
- if (!has_linear) {
- tf_inv.d = 0;
- } else if (!has_nonlinear) {
- // Any value larger than 1 works
- tf_inv.d = 2.0f;
- } else {
- tf_inv.d = src->c * src->d + src->f;
- }
-
- *dst = tf_inv;
- return true;
-}
-
-// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
-
-// From here below we're approximating an skcms_Curve with an skcms_TransferFunction{g,a,b,c,d,e,f}:
-//
-// tf(x) = cx + f x < d
-// tf(x) = (ax + b)^g + e x ≥ d
-//
-// When fitting, we add the additional constraint that both pieces meet at d:
-//
-// cd + f = (ad + b)^g + e
-//
-// Solving for e and folding it through gives an alternate formulation of the non-linear piece:
-//
-// tf(x) = cx + f x < d
-// tf(x) = (ax + b)^g - (ad + b)^g + cd + f x ≥ d
-//
-// Our overall strategy is then:
-// For a couple tolerances,
-// - skcms_fit_linear(): fit c,d,f iteratively to as many points as our tolerance allows
-// - invert c,d,f
-// - fit_nonlinear(): fit g,a,b using Gauss-Newton given those inverted c,d,f
-// (and by constraint, inverted e) to the inverse of the table.
-// Return the parameters with least maximum error.
-//
-// To run Gauss-Newton to find g,a,b, we'll also need the gradient of the residuals
-// of round-trip f_inv(x), the inverse of the non-linear piece of f(x).
-//
-// let y = Table(x)
-// r(x) = x - f_inv(y)
-//
-// ∂r/∂g = ln(ay + b)*(ay + b)^g
-// - ln(ad + b)*(ad + b)^g
-// ∂r/∂a = yg(ay + b)^(g-1)
-// - dg(ad + b)^(g-1)
-// ∂r/∂b = g(ay + b)^(g-1)
-// - g(ad + b)^(g-1)
-
-// Return the residual of roundtripping skcms_Curve(x) through f_inv(y) with parameters P,
-// and fill out the gradient of the residual into dfdP.
-static float rg_nonlinear(float x,
- const skcms_Curve* curve,
- const skcms_TransferFunction* tf,
- const float P[3],
- float dfdP[3]) {
- const float y = skcms_eval_curve(curve, x);
-
- const float g = P[0], a = P[1], b = P[2],
- c = tf->c, d = tf->d, f = tf->f;
-
- const float Y = fmaxf_(a*y + b, 0.0f),
- D = a*d + b;
- assert (D >= 0);
-
- // The gradient.
- dfdP[0] = 0.69314718f*log2f_(Y)*powf_(Y, g)
- - 0.69314718f*log2f_(D)*powf_(D, g);
- dfdP[1] = y*g*powf_(Y, g-1)
- - d*g*powf_(D, g-1);
- dfdP[2] = g*powf_(Y, g-1)
- - g*powf_(D, g-1);
-
- // The residual.
- const float f_inv = powf_(Y, g)
- - powf_(D, g)
- + c*d + f;
- return x - f_inv;
-}
-
-int skcms_fit_linear(const skcms_Curve* curve, int N, float tol, float* c, float* d, float* f) {
- assert(N > 1);
- // We iteratively fit the first points to the TF's linear piece.
- // We want the cx + f line to pass through the first and last points we fit exactly.
- //
- // As we walk along the points we find the minimum and maximum slope of the line before the
- // error would exceed our tolerance. We stop when the range [slope_min, slope_max] becomes
- // emtpy, when we definitely can't add any more points.
- //
- // Some points' error intervals may intersect the running interval but not lie fully
- // within it. So we keep track of the last point we saw that is a valid end point candidate,
- // and once the search is done, back up to build the line through *that* point.
- const float dx = 1.0f / (N - 1);
-
- int lin_points = 1;
- *f = skcms_eval_curve(curve, 0);
-
- float slope_min = -INFINITY_;
- float slope_max = +INFINITY_;
- for (int i = 1; i < N; ++i) {
- float x = i * dx;
- float y = skcms_eval_curve(curve, x);
-
- float slope_max_i = (y + tol - *f) / x,
- slope_min_i = (y - tol - *f) / x;
- if (slope_max_i < slope_min || slope_max < slope_min_i) {
- // Slope intervals would no longer overlap.
- break;
- }
- slope_max = fminf_(slope_max, slope_max_i);
- slope_min = fmaxf_(slope_min, slope_min_i);
-
- float cur_slope = (y - *f) / x;
- if (slope_min <= cur_slope && cur_slope <= slope_max) {
- lin_points = i + 1;
- *c = cur_slope;
- }
- }
-
- // Set D to the last point that met our tolerance.
- *d = (lin_points - 1) * dx;
- return lin_points;
-}
-
-static bool gauss_newton_step(const skcms_Curve* curve,
- const skcms_TransferFunction* tf,
- float P[3],
- float x0, float dx, int N) {
- // We'll sample x from the range [x0,x1] (both inclusive) N times with even spacing.
- //
- // We want to do P' = P + (Jf^T Jf)^-1 Jf^T r(P),
- // where r(P) is the residual vector
- // and Jf is the Jacobian matrix of f(), ∂r/∂P.
- //
- // Let's review the shape of each of these expressions:
- // r(P) is [N x 1], a column vector with one entry per value of x tested
- // Jf is [N x 3], a matrix with an entry for each (x,P) pair
- // Jf^T is [3 x N], the transpose of Jf
- //
- // Jf^T Jf is [3 x N] * [N x 3] == [3 x 3], a 3x3 matrix,
- // and so is its inverse (Jf^T Jf)^-1
- // Jf^T r(P) is [3 x N] * [N x 1] == [3 x 1], a column vector with the same shape as P
- //
- // Our implementation strategy to get to the final ∆P is
- // 1) evaluate Jf^T Jf, call that lhs
- // 2) evaluate Jf^T r(P), call that rhs
- // 3) invert lhs
- // 4) multiply inverse lhs by rhs
- //
- // This is a friendly implementation strategy because we don't have to have any
- // buffers that scale with N, and equally nice don't have to perform any matrix
- // operations that are variable size.
- //
- // Other implementation strategies could trade this off, e.g. evaluating the
- // pseudoinverse of Jf ( (Jf^T Jf)^-1 Jf^T ) directly, then multiplying that by
- // the residuals. That would probably require implementing singular value
- // decomposition, and would create a [3 x N] matrix to be multiplied by the
- // [N x 1] residual vector, but on the upside I think that'd eliminate the
- // possibility of this gauss_newton_step() function ever failing.
-
- // 0) start off with lhs and rhs safely zeroed.
- skcms_Matrix3x3 lhs = {{ {0,0,0}, {0,0,0}, {0,0,0} }};
- skcms_Vector3 rhs = { {0,0,0} };
-
- // 1,2) evaluate lhs and evaluate rhs
- // We want to evaluate Jf only once, but both lhs and rhs involve Jf^T,
- // so we'll have to update lhs and rhs at the same time.
- for (int i = 0; i < N; i++) {
- float x = x0 + i*dx;
-
- float dfdP[3] = {0,0,0};
- float resid = rg_nonlinear(x,curve,tf,P, dfdP);
-
- for (int r = 0; r < 3; r++) {
- for (int c = 0; c < 3; c++) {
- lhs.vals[r][c] += dfdP[r] * dfdP[c];
- }
- rhs.vals[r] += dfdP[r] * resid;
- }
- }
-
- // If any of the 3 P parameters are unused, this matrix will be singular.
- // Detect those cases and fix them up to indentity instead, so we can invert.
- for (int k = 0; k < 3; k++) {
- if (lhs.vals[0][k]==0 && lhs.vals[1][k]==0 && lhs.vals[2][k]==0 &&
- lhs.vals[k][0]==0 && lhs.vals[k][1]==0 && lhs.vals[k][2]==0) {
- lhs.vals[k][k] = 1;
- }
- }
-
- // 3) invert lhs
- skcms_Matrix3x3 lhs_inv;
- if (!skcms_Matrix3x3_invert(&lhs, &lhs_inv)) {
- return false;
- }
-
- // 4) multiply inverse lhs by rhs
- skcms_Vector3 dP = skcms_MV_mul(&lhs_inv, &rhs);
- P[0] += dP.vals[0];
- P[1] += dP.vals[1];
- P[2] += dP.vals[2];
- return isfinitef_(P[0]) && isfinitef_(P[1]) && isfinitef_(P[2]);
-}
-
-
-// Fit the points in [L,N) to the non-linear piece of tf, or return false if we can't.
-static bool fit_nonlinear(const skcms_Curve* curve, int L, int N, skcms_TransferFunction* tf) {
- float P[3] = { tf->g, tf->a, tf->b };
-
- // No matter where we start, dx should always represent N even steps from 0 to 1.
- const float dx = 1.0f / (N-1);
-
- for (int j = 0; j < 3/*TODO: tune*/; j++) {
- // These extra constraints a >= 0 and ad+b >= 0 are not modeled in the optimization.
- // We don't really know how to fix up a if it goes negative.
- if (P[1] < 0) {
- return false;
- }
- // If ad+b goes negative, we feel just barely not uneasy enough to tweak b so ad+b is zero.
- if (P[1] * tf->d + P[2] < 0) {
- P[2] = -P[1] * tf->d;
- }
- assert (P[1] >= 0 &&
- P[1] * tf->d + P[2] >= 0);
-
- if (!gauss_newton_step(curve, tf,
- P,
- L*dx, dx, N-L)) {
- return false;
- }
- }
-
- // We need to apply our fixups one last time
- if (P[1] < 0) {
- return false;
- }
- if (P[1] * tf->d + P[2] < 0) {
- P[2] = -P[1] * tf->d;
- }
-
- tf->g = P[0];
- tf->a = P[1];
- tf->b = P[2];
- tf->e = tf->c*tf->d + tf->f
- - powf_(tf->a*tf->d + tf->b, tf->g);
- return true;
-}
-
-bool skcms_ApproximateCurve(const skcms_Curve* curve,
- skcms_TransferFunction* approx,
- float* max_error) {
- if (!curve || !approx || !max_error) {
- return false;
- }
-
- if (curve->table_entries == 0) {
- // No point approximating an skcms_TransferFunction with an skcms_TransferFunction!
- return false;
- }
-
- if (curve->table_entries == 1 || curve->table_entries > (uint32_t)INT_MAX) {
- // We need at least two points, and must put some reasonable cap on the maximum number.
- return false;
- }
-
- int N = (int)curve->table_entries;
- const float dx = 1.0f / (N - 1);
-
- *max_error = INFINITY_;
- const float kTolerances[] = { 1.5f / 65535.0f, 1.0f / 512.0f };
- for (int t = 0; t < ARRAY_COUNT(kTolerances); t++) {
- skcms_TransferFunction tf,
- tf_inv;
- int L = skcms_fit_linear(curve, N, kTolerances[t], &tf.c, &tf.d, &tf.f);
-
- if (L == N) {
- // If the entire data set was linear, move the coefficients to the nonlinear portion
- // with G == 1. This lets use a canonical representation with d == 0.
- tf.g = 1;
- tf.a = tf.c;
- tf.b = tf.f;
- tf.c = tf.d = tf.e = tf.f = 0;
- } else if (L == N - 1) {
- // Degenerate case with only two points in the nonlinear segment. Solve directly.
- tf.g = 1;
- tf.a = (skcms_eval_curve(curve, (N-1)*dx) -
- skcms_eval_curve(curve, (N-2)*dx))
- / dx;
- tf.b = skcms_eval_curve(curve, (N-2)*dx)
- - tf.a * (N-2)*dx;
- tf.e = 0;
- } else {
- // Start by guessing a gamma-only curve through the midpoint.
- int mid = (L + N) / 2;
- float mid_x = mid / (N - 1.0f);
- float mid_y = skcms_eval_curve(curve, mid_x);
- tf.g = log2f_(mid_y) / log2f_(mid_x);;
- tf.a = 1;
- tf.b = 0;
- tf.e = tf.c*tf.d + tf.f
- - powf_(tf.a*tf.d + tf.b, tf.g);
-
-
- if (!skcms_TransferFunction_invert(&tf, &tf_inv) ||
- !fit_nonlinear(curve, L,N, &tf_inv)) {
- continue;
- }
-
- // We fit tf_inv, so calculate tf to keep in sync.
- if (!skcms_TransferFunction_invert(&tf_inv, &tf)) {
- continue;
- }
- }
-
- // We find our error by roundtripping the table through tf_inv.
- //
- // (The most likely use case for this approximation is to be inverted and
- // used as the transfer function for a destination color space.)
- //
- // We've kept tf and tf_inv in sync above, but we can't guarantee that tf is
- // invertible, so re-verify that here (and use the new inverse for testing).
- if (!skcms_TransferFunction_invert(&tf, &tf_inv)) {
- continue;
- }
-
- float err = skcms_MaxRoundtripError(curve, &tf_inv);
- if (*max_error > err) {
- *max_error = err;
- *approx = tf;
- }
- }
- return isfinitef_(*max_error);
-}