Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(928)

Unified Diff: ui/gfx/skia_color_space_util.cc

Issue 2715453002: Add UMAs for numerical approximation of table-based transfer functions. (Closed)
Patch Set: Review feedback Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: ui/gfx/skia_color_space_util.cc
diff --git a/ui/gfx/skia_color_space_util.cc b/ui/gfx/skia_color_space_util.cc
index 2dda075fe65c1b129c6488b6019aea778c29132d..a4cfa3b1a5f2c80e46ea204b90edd680623ef4a8 100644
--- a/ui/gfx/skia_color_space_util.cc
+++ b/ui/gfx/skia_color_space_util.cc
@@ -7,11 +7,341 @@
#include <algorithm>
#include <cmath>
+#include "base/logging.h"
+
namespace gfx {
namespace {
+
+// Evaluate the gradient of the linear component of fn
+void SkTransferFnEvalGradientLinear(const SkColorSpaceTransferFn& fn,
+ float x,
+ float* d_fn_d_fC_at_x,
+ float* d_fn_d_fF_at_x) {
+ *d_fn_d_fC_at_x = x;
+ *d_fn_d_fF_at_x = 1.f;
+}
+
+// Solve for the parameters fC and fF, given the parameter fD.
+void SkTransferFnSolveLinear(SkColorSpaceTransferFn* fn,
+ const float* x,
+ const float* t,
+ size_t n) {
+ // If this has no linear segment, don't try to solve for one.
+ if (fn->fD <= 0) {
+ fn->fC = 1;
+ fn->fF = 0;
+ return;
+ }
+
+ // Let ne_lhs be the left hand side of the normal equations, and let ne_rhs
+ // be the right hand side. This is a 4x4 matrix, but we will only update the
+ // upper-left 2x2 sub-matrix.
+ SkMatrix44 ne_lhs;
+ SkVector4 ne_rhs;
+ for (int row = 0; row < 2; ++row) {
+ for (int col = 0; col < 2; ++col) {
+ ne_lhs.set(row, col, 0);
+ }
+ }
+ for (int row = 0; row < 4; ++row)
+ ne_rhs.fData[row] = 0;
+
+ // Add the contributions from each sample to the normal equations.
+ float x_linear_max = 0;
+ for (size_t i = 0; i < n; ++i) {
+ // Ignore points in the nonlinear segment.
+ if (x[i] >= fn->fD)
+ continue;
+ x_linear_max = std::max(x_linear_max, x[i]);
+
+ // Let J be the gradient of fn with respect to parameters C and F, evaluated
+ // at this point.
+ float J[2];
+ SkTransferFnEvalGradientLinear(*fn, x[i], &J[0], &J[1]);
+
+ // Let r be the residual at this point.
+ float r = t[i];
+
+ // Update the normal equations left hand side with the outer product of J
+ // with itself.
+ for (int row = 0; row < 2; ++row) {
+ for (int col = 0; col < 2; ++col) {
+ ne_lhs.set(row, col, ne_lhs.get(row, col) + J[row] * J[col]);
+ }
+ // Update the normal equations right hand side the product of J with the
+ // residual
+ ne_rhs.fData[row] += J[row] * r;
+ }
+ }
+
+ // If we only have a single x point at 0, that isn't enough to construct a
+ // linear segment, so add an additional point connecting to the nonlinear
+ // segment.
+ if (x_linear_max == 0) {
+ float J[2];
+ SkTransferFnEvalGradientLinear(*fn, fn->fD, &J[0], &J[1]);
+ float r = SkTransferFnEval(*fn, fn->fD);
+ for (int row = 0; row < 2; ++row) {
+ for (int col = 0; col < 2; ++col) {
+ ne_lhs.set(row, col, ne_lhs.get(row, col) + J[row] * J[col]);
+ }
+ ne_rhs.fData[row] += J[row] * r;
+ }
+ }
+
+ // Solve the normal equations.
+ SkMatrix44 ne_lhs_inv;
+ bool invert_result = ne_lhs.invert(&ne_lhs_inv);
+ DCHECK(invert_result);
+ SkVector4 solution = ne_lhs_inv * ne_rhs;
+
+ // Update the transfer function.
+ fn->fC = solution.fData[0];
+ fn->fF = solution.fData[1];
+}
+
+// Evaluate the gradient of the nonlinear component of fn
+void SkTransferFnEvalGradientNonlinear(const SkColorSpaceTransferFn& fn,
+ float x,
+ float* d_fn_d_fA_at_x,
+ float* d_fn_d_fB_at_x,
+ float* d_fn_d_fE_at_x,
+ float* d_fn_d_fG_at_x) {
+ float base = fn.fA * x + fn.fB;
+ if (base > 0.f) {
+ *d_fn_d_fA_at_x = fn.fG * x * std::pow(base, fn.fG - 1.f);
+ *d_fn_d_fB_at_x = fn.fG * std::pow(base, fn.fG - 1.f);
+ *d_fn_d_fE_at_x = 1.f;
+ *d_fn_d_fG_at_x = std::pow(base, fn.fG) * std::log(base);
+ } else {
+ *d_fn_d_fA_at_x = 0.f;
+ *d_fn_d_fB_at_x = 0.f;
+ *d_fn_d_fE_at_x = 0.f;
+ *d_fn_d_fG_at_x = 0.f;
+ }
}
+// Take one Gauss-Newton step updating fA, fB, fE, and fG, given fD.
+bool SkTransferFnGaussNewtonStepNonlinear(SkColorSpaceTransferFn* fn,
+ float* error_Linfty_before,
+ const float* x,
+ const float* t,
+ size_t n) {
+ float kEpsilon = 1.f / 1024.f;
+ // Let ne_lhs be the left hand side of the normal equations, and let ne_rhs
+ // be the right hand side.
+ SkMatrix44 ne_lhs;
+ SkVector4 ne_rhs;
+ for (int row = 0; row < 4; ++row) {
+ for (int col = 0; col < 4; ++col) {
+ ne_lhs.set(row, col, 0);
+ }
+ ne_rhs.fData[row] = 0;
+ }
+
+ // Add the contributions from each sample to the normal equations.
+ *error_Linfty_before = 0;
+ for (size_t i = 0; i < n; ++i) {
+ // Ignore points in the linear segment.
+ if (x[i] < fn->fD)
+ continue;
+ // Let J be the gradient of fn with respect to parameters A, B, E, and G,
+ // evaulated at this point.
+ float J[4];
+ SkTransferFnEvalGradientNonlinear(*fn, x[i], &J[0], &J[1], &J[2], &J[3]);
+ // Let r be the residual at this point;
+ float r = t[i] - SkTransferFnEval(*fn, x[i]);
+ *error_Linfty_before += std::abs(r);
+ // Update the normal equations left hand side with the outer product of J
+ // with itself.
+ for (int row = 0; row < 4; ++row) {
+ for (int col = 0; col < 4; ++col) {
+ ne_lhs.set(row, col, ne_lhs.get(row, col) + J[row] * J[col]);
+ }
+ // Update the normal equations right hand side the product of J with the
+ // residual
+ ne_rhs.fData[row] += J[row] * r;
+ }
+ }
+
+ // Note that if fG = 1, then the normal equations will be singular (because,
+ // when fG = 1, because fA and fE are equivalent parameters). To avoid
+ // problems, fix fE (row/column 3) in these circumstances.
+ if (std::abs(fn->fG - 1) < kEpsilon) {
+ for (int row = 0; row < 4; ++row) {
+ float value = (row == 2) ? 1.f : 0.f;
+ ne_lhs.set(row, 2, value);
+ ne_lhs.set(2, row, value);
+ }
+ ne_rhs.fData[2] = 0.f;
+ }
+
+ // Solve the normal equations.
+ SkMatrix44 ne_lhs_inv;
+ if (!ne_lhs.invert(&ne_lhs_inv))
+ return false;
+ SkVector4 step = ne_lhs_inv * ne_rhs;
+
+ // Update the transfer function.
+ fn->fA += step.fData[0];
+ fn->fB += step.fData[1];
+ fn->fE += step.fData[2];
+ fn->fG += step.fData[3];
+ return true;
+}
+
+// Solve for fA, fB, fE, and fG, given fD. The initial value of |fn| is the
+// point from which iteration starts.
+bool SkTransferFnSolveNonlinear(SkColorSpaceTransferFn* fn,
+ const float* x,
+ const float* t,
+ size_t n) {
+ // Take a maximum of 16 Gauss-Newton steps.
+ const size_t kNumSteps = 16;
+
+ // The L-infinity error before each step.
+ float step_error[kNumSteps] = {0};
+
+ for (size_t step = 0; step < kNumSteps; ++step) {
+ // If the normal equations are singular, we can't continue.
+ if (!SkTransferFnGaussNewtonStepNonlinear(fn, &step_error[step], x, t, n))
+ return false;
+
+ // If the error is inf or nan, we are clearly not converging.
+ if (std::isnan(step_error[step]) || std::isinf(step_error[step]))
+ return false;
+
+ // If our error is non-negligable and increasing then we are not in the
+ // region of convergence.
+ const float kNonNegligbleErrorEpsilon = 1.f / 256.f;
+ const float kGrowthFactor = 1.25f;
+ if (step > 2 && step_error[step] > kNonNegligbleErrorEpsilon) {
+ if (step_error[step - 1] * kGrowthFactor < step_error[step] &&
+ step_error[step - 2] * kGrowthFactor < step_error[step - 1]) {
+ return false;
+ }
+ }
+ }
+
+ // We've converged to a reasonable solution. If some of the parameters are
+ // extremely close to 0 or 1, set them to 0 or 1.
+ const float kRoundEpsilon = 1.f / 1024.f;
+ if (std::abs(fn->fA - 1.f) < kRoundEpsilon)
+ fn->fA = 1.f;
+ if (std::abs(fn->fB) < kRoundEpsilon)
+ fn->fB = 0;
+ if (std::abs(fn->fE) < kRoundEpsilon)
+ fn->fE = 0;
+ if (std::abs(fn->fG - 1.f) < kRoundEpsilon)
+ fn->fG = 1.f;
+ return true;
+}
+
+void SkApproximateTransferFnInternal(const float* x,
+ const float* t,
+ size_t n,
+ SkColorSpaceTransferFn* fn,
+ float* error,
+ bool* nonlinear_fit_converged) {
+ // First, guess at a value of fD. Assume that the nonlinear segment applies
+ // to all x >= 0.1. This is generally a safe assumption (fD is usually less
+ // than 0.1).
+ fn->fD = 0.1f;
+
+ // Do a nonlinear regression on the nonlinear segment. Use a number of guesses
+ // for the initial value of fG, because not all values will converge.
+ *nonlinear_fit_converged = false;
+ {
+ const size_t kNumInitialGammas = 4;
+ float initial_gammas[kNumInitialGammas] = {1.f, 2.f, 3.f, 0.5f};
+ for (size_t i = 0; i < kNumInitialGammas; ++i) {
+ fn->fG = initial_gammas[i];
+ fn->fA = 1;
+ fn->fB = 0;
+ fn->fC = 1;
+ fn->fE = 0;
+ fn->fF = 0;
+ if (SkTransferFnSolveNonlinear(fn, x, t, n)) {
+ *nonlinear_fit_converged = true;
+ break;
+ }
+ }
+ }
+
+ // Now walk back fD from our initial guess to the point where our nonlinear
+ // fit no longer fits (or all the way to 0 if it fits).
+ if (*nonlinear_fit_converged) {
+ // Find the L-infinity error of this nonlinear fit (using our old fD value).
+ float max_error_in_nonlinear_fit = 0;
+ for (size_t i = 0; i < n; ++i) {
+ if (x[i] < fn->fD)
+ continue;
+ float error_at_xi = std::abs(t[i] - SkTransferFnEval(*fn, x[i]));
+ max_error_in_nonlinear_fit =
+ std::max(max_error_in_nonlinear_fit, error_at_xi);
+ }
+
+ // Now find the maximum x value where this nonlinear fit is no longer
+ // accurate or no longer defined.
+ fn->fD = 0.f;
+ float max_x_where_nonlinear_does_not_fit = -1.f;
+ for (size_t i = 0; i < n; ++i) {
+ bool nonlinear_fits_xi = true;
+ if (fn->fA * x[i] + fn->fB < 0) {
+ // The nonlinear segment is undefined when fA * x + fB is less than 0.
+ nonlinear_fits_xi = false;
+ } else {
+ // Define "no longer accurate" as "has more than 10% more error than
+ // the maximum error in the fit segment".
+ float error_at_xi = std::abs(t[i] - SkTransferFnEval(*fn, x[i]));
+ if (error_at_xi > 1.1f * max_error_in_nonlinear_fit)
+ nonlinear_fits_xi = false;
+ }
+ if (!nonlinear_fits_xi) {
+ max_x_where_nonlinear_does_not_fit =
+ std::max(max_x_where_nonlinear_does_not_fit, x[i]);
+ }
+ }
+
+ // Now let fD be the highest sample of x that is above the threshold where
+ // the nonlinear segment does not fit.
+ fn->fD = 1.f;
+ for (size_t i = 0; i < n; ++i) {
+ if (x[i] > max_x_where_nonlinear_does_not_fit)
+ fn->fD = std::min(fn->fD, x[i]);
+ }
+ } else {
+ // If the nonlinear fit didn't converge, then try a linear fit on the full
+ // range. Note that fD must be strictly greater than 1 because the nonlinear
+ // segment starts at fD.
+ const float kEpsilon = 1.f / 256.f;
+ fn->fD = 1.f + kEpsilon;
+ }
+
+ // Compute the linear segment, now that we have our definitive fD.
+ SkTransferFnSolveLinear(fn, x, t, n);
+
+ // If the nonlinear fit didn't converge, re-express the linear fit in
+ // canonical form as a nonlinear fit with fG as 1.
+ if (!*nonlinear_fit_converged) {
+ fn->fA = fn->fC;
+ fn->fB = fn->fF;
+ fn->fC = 1;
+ fn->fD = 0;
+ fn->fE = 0;
+ fn->fF = 0;
+ fn->fG = 1;
+ }
+
+ // Return the L-infinity error of the total approximation.
+ *error = 0.f;
+ for (size_t i = 0; i < n; ++i)
+ *error = std::max(*error, std::abs(t[i] - SkTransferFnEval(*fn, x[i])));
+}
+
+} // namespace
+
float SkTransferFnEval(const SkColorSpaceTransferFn& fn, float x) {
if (x < 0.f)
return 0.f;
@@ -61,6 +391,15 @@ bool SkTransferFnIsApproximatelyIdentity(const SkColorSpaceTransferFn& a) {
return true;
}
+void SkApproximateTransferFn(const float* x,
+ const float* t,
+ size_t n,
+ SkColorSpaceTransferFn* fn,
+ float* error,
+ bool* nonlinear_fit_converged) {
+ SkApproximateTransferFnInternal(x, t, n, fn, error, nonlinear_fit_converged);
+}
+
bool SkMatrixIsApproximatelyIdentity(const SkMatrix44& m) {
const float kEpsilon = 1.f / 256.f;
for (int i = 0; i < 4; ++i) {

Powered by Google App Engine
This is Rietveld 408576698