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

Side by Side Diff: ui/gfx/skia_color_space_util.cc

Issue 2715453002: Add UMAs for numerical approximation of table-based transfer functions. (Closed)
Patch Set: Fix error computation 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 unified diff | Download patch
OLDNEW
1 // Copyright 2017 The Chromium Authors. All rights reserved. 1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "ui/gfx/skia_color_space_util.h" 5 #include "ui/gfx/skia_color_space_util.h"
6 6
7 #include <algorithm> 7 #include <algorithm>
8 #include <cmath> 8 #include <cmath>
9 #include <vector>
10
11 #include "base/logging.h"
9 12
10 namespace gfx { 13 namespace gfx {
11 14
12 namespace { 15 namespace {
13 } 16
17 // Evaluate the gradient of the linear component of fn
18 void SkTransferFnEvalGradientLinear(const SkColorSpaceTransferFn& fn,
19 float x,
20 float* d_fn_d_fC_at_x,
21 float* d_fn_d_fF_at_x) {
22 *d_fn_d_fC_at_x = x;
23 *d_fn_d_fF_at_x = 1.f;
24 }
25
26 // Solve for the parameters fC and fF, given the parameter fD.
27 bool SkTransferFnSolveLinear(SkColorSpaceTransferFn* fn,
msarett1 2017/02/22 19:53:18 I imagined this function being simpler. Ex: "draw
ccameron 2017/02/22 22:10:48 Yes, this is a bit scary at the moment -- it can h
28 const float* x,
29 const float* t,
30 size_t n) {
31 // If this has no linear segment, don't try to solve for one.
32 if (fn->fD <= 0) {
33 fn->fC = 1;
34 fn->fF = 0;
35 return true;
36 }
37
38 // Let ne_lhs be the left hand side of the normal equations, and let ne_rhs
39 // be the right hand side. This is a 4x4 matrix, but we will only update the
40 // upper-left 2x2 sub-matrix.
41 SkMatrix44 ne_lhs;
42 SkVector4 ne_rhs;
43 for (int row = 0; row < 2; ++row) {
44 for (int col = 0; col < 2; ++col) {
45 ne_lhs.set(row, col, 0);
46 }
47 }
48 for (int row = 0; row < 4; ++row)
49 ne_rhs.fData[row] = 0;
50
51 // Add the contributions from each sample to the normal equations.
52 float x_linear_max = 0;
53 for (size_t i = 0; i < n; ++i) {
54 // Ignore points in the nonlinear segment.
55 if (x[i] >= fn->fD)
56 continue;
57 x_linear_max = std::max(x_linear_max, x[i]);
58
59 // Let J be the gradient of fn with respect to parameters C and F, evaluated
60 // at this point.
61 float J[2];
62 SkTransferFnEvalGradientLinear(*fn, x[i], &J[0], &J[1]);
63
64 // Let r be the residual at this point.
65 float r = t[i];
66
67 // Update the normal equations left hand side with the outer product of J
68 // with itself.
69 for (int row = 0; row < 2; ++row) {
70 for (int col = 0; col < 2; ++col) {
71 ne_lhs.set(row, col, ne_lhs.get(row, col) + J[row] * J[col]);
72 }
73 // Update the normal equations right hand side the product of J with the
74 // residual
75 ne_rhs.fData[row] += J[row] * r;
76 }
77 }
78
79 // If we only have a single x point at 0, that isn't enough to construct a
80 // linear segment, so add an additional point connecting to the nonlinear
81 // segment.
82 if (x_linear_max == 0) {
83 float J[2];
84 SkTransferFnEvalGradientLinear(*fn, fn->fD, &J[0], &J[1]);
85 float r = SkTransferFnEval(*fn, fn->fD);
86 for (int row = 0; row < 2; ++row) {
87 for (int col = 0; col < 2; ++col) {
88 ne_lhs.set(row, col, ne_lhs.get(row, col) + J[row] * J[col]);
89 }
90 ne_rhs.fData[row] += J[row] * r;
91 }
92 }
93
94 // Solve the normal equations.
95 SkMatrix44 ne_lhs_inv;
96 if (!ne_lhs.invert(&ne_lhs_inv))
97 return false;
msarett1 2017/02/22 19:53:18 Logically, when can this fail? Sure, matrix inver
ccameron 2017/02/22 22:10:48 True, if we have at least 1 data point, then, up t
98 SkVector4 solution = ne_lhs_inv * ne_rhs;
99
100 // Update the transfer function.
101 fn->fC = solution.fData[0];
102 fn->fF = solution.fData[1];
103 return true;
104 }
105
106 // Evaluate the gradient of the nonlinear component of fn
107 void SkTransferFnEvalGradientNonlinear(const SkColorSpaceTransferFn& fn,
108 float x,
109 float* d_fn_d_fA_at_x,
110 float* d_fn_d_fB_at_x,
111 float* d_fn_d_fE_at_x,
112 float* d_fn_d_fG_at_x) {
113 float base = fn.fA * x + fn.fB;
114 if (base > 0.f) {
115 *d_fn_d_fA_at_x = fn.fG * x * std::pow(base, fn.fG - 1.f);
116 *d_fn_d_fB_at_x = fn.fG * std::pow(base, fn.fG - 1.f);
117 *d_fn_d_fE_at_x = 1.f;
118 *d_fn_d_fG_at_x = std::pow(base, fn.fG) * std::log(base);
119 } else {
120 *d_fn_d_fA_at_x = 0.f;
121 *d_fn_d_fB_at_x = 0.f;
122 *d_fn_d_fE_at_x = 0.f;
123 *d_fn_d_fG_at_x = 0.f;
124 }
125 }
126
127 // Take one Gauss-Newton step updating fA, fB, fE, and fG, given fD.
128 bool SkTransferFnGaussNewtonStepNonlinear(SkColorSpaceTransferFn* fn,
129 float* error_Linfty_before,
130 const float* x,
131 const float* t,
132 size_t n) {
133 float kEpsilon = 1.f / 1024.f;
134 // Let ne_lhs be the left hand side of the normal equations, and let ne_rhs
135 // be the right hand side.
136 SkMatrix44 ne_lhs;
137 SkVector4 ne_rhs;
138 for (int row = 0; row < 4; ++row) {
139 for (int col = 0; col < 4; ++col) {
140 ne_lhs.set(row, col, 0);
141 }
142 ne_rhs.fData[row] = 0;
143 }
144
145 // Add the contributions from each sample to the normal equations.
146 *error_Linfty_before = 0;
147 for (size_t i = 0; i < n; ++i) {
148 // Ignore points in the linear segment.
149 if (x[i] < fn->fD)
150 continue;
151 // Let J be the gradient of fn with respect to parameters A, B, E, and G,
152 // evaulated at this point.
153 float J[4];
154 SkTransferFnEvalGradientNonlinear(*fn, x[i], &J[0], &J[1], &J[2], &J[3]);
155 // Let r be the residual at this point;
156 float r = t[i] - SkTransferFnEval(*fn, x[i]);
157 *error_Linfty_before += std::abs(r);
158 // Update the normal equations left hand side with the outer product of J
159 // with itself.
160 for (int row = 0; row < 4; ++row) {
161 for (int col = 0; col < 4; ++col) {
162 ne_lhs.set(row, col, ne_lhs.get(row, col) + J[row] * J[col]);
163 }
164 // Update the normal equations right hand side the product of J with the
165 // residual
166 ne_rhs.fData[row] += J[row] * r;
167 }
168 }
169
ccameron 2017/02/22 10:21:30 If we find that we can assume that TrFn(1)=1, then
msarett1 2017/02/22 19:53:18 Acknowledged, really hope this is the case.
170 // Note that if fG = 1, then the normal equations will be singular (because,
171 // when fG = 1, because fA and fE are equivalent parameters). To avoid
172 // problems, fix fE (row/column 3) in these circumstances.
173 if (std::abs(fn->fG - 1) < kEpsilon) {
174 for (int row = 0; row < 4; ++row) {
175 float value = (row == 2) ? 1.f : 0.f;
176 ne_lhs.set(row, 2, value);
177 ne_lhs.set(2, row, value);
178 }
179 ne_rhs.fData[2] = 0.f;
180 }
181
182 // Solve the normal equations.
183 SkMatrix44 ne_lhs_inv;
184 if (!ne_lhs.invert(&ne_lhs_inv))
185 return false;
186 SkVector4 step = ne_lhs_inv * ne_rhs;
187
188 // Update the transfer function.
189 fn->fA += step.fData[0];
190 fn->fB += step.fData[1];
191 fn->fE += step.fData[2];
192 fn->fG += step.fData[3];
193 return true;
194 }
195
196 // Solve for fA, fB, fE, and fG, given fD. The initial value of |fn| is the
197 // point from which iteration starts.
198 bool SkTransferFnSolveNonlinear(SkColorSpaceTransferFn* fn,
199 const float* x,
200 const float* t,
201 size_t n) {
202 // Take a maximum of 16 Gauss-Newton steps.
203 const size_t kNumSteps = 16;
204
205 // The L-infinity error before each step.
206 float step_error[kNumSteps] = {0};
207
208 for (size_t step = 0; step < kNumSteps; ++step) {
209 // If the normal equations are singular, we can't continue.
210 if (!SkTransferFnGaussNewtonStepNonlinear(fn, &step_error[step], x, t, n))
211 return false;
212
213 // If the error is inf or nan, we are clearly not converging.
214 if (std::isnan(step_error[step]) || std::isinf(step_error[step]))
215 return false;
216
217 // If our error is already tiny, don't bother with more iterations.
218 const float kConvergedEarlyEpsilon = 1.f / 32768.f;
msarett1 2017/02/22 19:53:18 How important is it to have this case? Are we wor
ccameron 2017/02/22 22:10:48 In the final version I'll want to have some early-
219 if (step_error[step] < kConvergedEarlyEpsilon)
220 break;
221
222 // If our error is non-negligable and increasing then we are not in the
223 // region of convergence.
224 const float kNonNegligbleErrorEpsilon = 1.f / 256.f;
225 const float kGrowthFactor = 1.25f;
226 if (step > 2 && step_error[step] > kNonNegligbleErrorEpsilon) {
227 if (step_error[step - 1] * kGrowthFactor < step_error[step] &&
228 step_error[step - 2] * kGrowthFactor < step_error[step - 1]) {
229 return false;
230 }
231 }
232 }
233
234 // We've converged to a reasonable solution. If some of the parameters are
235 // extremely close to 0 or 1, set them to 0 or 1.
236 const float kRoundEpsilon = 1.f / 1024.f;
237 if (std::abs(fn->fA - 1.f) < kRoundEpsilon)
238 fn->fA = 1.f;
239 if (std::abs(fn->fB) < kRoundEpsilon)
240 fn->fB = 0;
241 if (std::abs(fn->fE) < kRoundEpsilon)
242 fn->fE = 0;
243 if (std::abs(fn->fG - 1.f) < kRoundEpsilon)
244 fn->fG = 1.f;
245 return true;
246 }
247
248 void SkApproximateTransferFnInternal(const float* x,
249 const float* t,
250 size_t n,
251 SkColorSpaceTransferFn* fn,
252 float* error,
253 bool* nonlinear_fit_converged,
254 bool* linear_fit_succeeded) {
255 // First, guess at a value of fD. Assume that the nonlinear segment applies
256 // to all x >= 0.1. This is generally a safe assumption (fD is usually less
257 // than 0.1).
258 fn->fD = 0.1f;
259
260 // Do a nonlinear regression on the nonlinear segment. Use a number of guesses
261 // for the initial value of fG, because not all values will converge.
262 *nonlinear_fit_converged = false;
263 {
264 const size_t kNumInitialGammas = 4;
265 float initial_gammas[kNumInitialGammas] = {1.f, 2.f, 3.f, 0.5f};
266 for (size_t i = 0; i < kNumInitialGammas; ++i) {
267 fn->fG = initial_gammas[i];
268 fn->fA = 1;
269 fn->fB = 0;
270 fn->fC = 1;
271 fn->fE = 0;
272 fn->fF = 0;
273 if (SkTransferFnSolveNonlinear(fn, x, t, n)) {
274 *nonlinear_fit_converged = true;
275 break;
276 }
277 }
278 }
279
280 // Now walk back fD from our initial guess to the point where our nonlinear
281 // fit no longer fits (or all the way to 0 if it fits).
282 if (*nonlinear_fit_converged) {
283 // Find the L-infinity error of this nonlinear fit (using our old fD value).
284 float max_error_in_nonlinear_fit = 0;
285 for (size_t i = 0; i < n; ++i) {
286 if (x[i] < fn->fD)
287 continue;
288 float error_at_xi = std::abs(t[i] - SkTransferFnEval(*fn, x[i]));
289 max_error_in_nonlinear_fit =
290 std::max(max_error_in_nonlinear_fit, error_at_xi);
291 }
292
293 // Now find the maximum x value where this nonlinear fit is no longer
294 // accurate or no longer defined.
295 fn->fD = 0.f;
296 float max_x_where_nonlinear_does_not_fit = -1.f;
297 for (size_t i = 0; i < n; ++i) {
298 bool nonlinear_fits_xi = true;
299 if (fn->fA * x[i] + fn->fB < 0) {
300 // The nonlinear segment is undefined when fA * x + fB is less than 0.
301 nonlinear_fits_xi = false;
302 } else {
303 // Define "no longer accurate" as "has more than 10% more error than
304 // the maximum error in the fit segment".
305 float error_at_xi = std::abs(t[i] - SkTransferFnEval(*fn, x[i]));
306 if (error_at_xi > 1.1f * max_error_in_nonlinear_fit)
307 nonlinear_fits_xi = false;
308 }
309 if (!nonlinear_fits_xi) {
310 max_x_where_nonlinear_does_not_fit =
311 std::max(max_x_where_nonlinear_does_not_fit, x[i]);
312 }
313 }
314
315 // Now let fD be the highest sample of x that is above the threshold where
316 // the nonlinear segment does not fit.
317 fn->fD = 1.f;
318 for (size_t i = 0; i < n; ++i) {
319 if (x[i] > max_x_where_nonlinear_does_not_fit)
320 fn->fD = std::min(fn->fD, x[i]);
321 }
322 } else {
323 // If the nonlinear fit didn't converge, then try a linear fit on the full
324 // range.
325 fn->fD = 1.1f;
msarett1 2017/02/22 19:53:18 I think you choose 1.1f here because the linear ra
ccameron 2017/02/22 22:10:48 Done.
326 }
327
328 // Compute the linear segment, now that we have our definitive fD.
329 *linear_fit_succeeded = SkTransferFnSolveLinear(fn, x, t, n);
330
331 // If the nonlinear fit didn't converge, re-express the linear fit in
332 // canonical form as a nonlinear fit with fG as 1.
333 if (*linear_fit_succeeded && !*nonlinear_fit_converged) {
msarett1 2017/02/22 19:53:18 I'd like to think this will always be: A = 1.0f B
ccameron 2017/02/22 22:10:48 Yeah, it'll be nice to not have to worry about the
334 fn->fA = fn->fC;
335 fn->fB = fn->fF;
336 fn->fC = 1;
337 fn->fD = 0;
338 fn->fE = 0;
339 fn->fF = 0;
340 fn->fG = 1;
341 }
342
343 // Return the L-infinity error of the total approximation.
344 *error = 0.f;
345 for (size_t i = 0; i < n; ++i)
346 *error = std::max(*error, std::abs(t[i] - SkTransferFnEval(*fn, x[i])));
347 }
348
349 } // namespace
14 350
15 float SkTransferFnEval(const SkColorSpaceTransferFn& fn, float x) { 351 float SkTransferFnEval(const SkColorSpaceTransferFn& fn, float x) {
16 if (x < 0.f) 352 if (x < 0.f)
17 return 0.f; 353 return 0.f;
18 if (x < fn.fD) 354 if (x < fn.fD)
19 return fn.fC * x + fn.fF; 355 return fn.fC * x + fn.fF;
20 return std::pow(fn.fA * x + fn.fB, fn.fG) + fn.fE; 356 return std::pow(fn.fA * x + fn.fB, fn.fG) + fn.fE;
21 } 357 }
22 358
23 SkColorSpaceTransferFn SkTransferFnInverse(const SkColorSpaceTransferFn& fn) { 359 SkColorSpaceTransferFn SkTransferFnInverse(const SkColorSpaceTransferFn& fn) {
(...skipping 30 matching lines...) Expand all
54 const float kStep = 1.f / 8.f; 390 const float kStep = 1.f / 8.f;
55 const float kEpsilon = 2.5f / 256.f; 391 const float kEpsilon = 2.5f / 256.f;
56 for (float x = 0; x <= 1.f; x += kStep) { 392 for (float x = 0; x <= 1.f; x += kStep) {
57 float a_of_x = SkTransferFnEval(a, x); 393 float a_of_x = SkTransferFnEval(a, x);
58 if (std::abs(a_of_x - x) > kEpsilon) 394 if (std::abs(a_of_x - x) > kEpsilon)
59 return false; 395 return false;
60 } 396 }
61 return true; 397 return true;
62 } 398 }
63 399
400 void SkApproximateTransferFn(const float* x,
401 const float* t,
402 size_t n,
403 SkColorSpaceTransferFn* fn,
404 float* error,
405 bool* nonlinear_fit_converged,
406 bool* linear_fit_succeeded) {
407 SkApproximateTransferFnInternal(x, t, n, fn, error, nonlinear_fit_converged,
408 linear_fit_succeeded);
409 }
410
64 bool SkMatrixIsApproximatelyIdentity(const SkMatrix44& m) { 411 bool SkMatrixIsApproximatelyIdentity(const SkMatrix44& m) {
65 const float kEpsilon = 1.f / 256.f; 412 const float kEpsilon = 1.f / 256.f;
66 for (int i = 0; i < 4; ++i) { 413 for (int i = 0; i < 4; ++i) {
67 for (int j = 0; j < 4; ++j) { 414 for (int j = 0; j < 4; ++j) {
68 float identity_value = i == j ? 1 : 0; 415 float identity_value = i == j ? 1 : 0;
69 float value = m.get(i, j); 416 float value = m.get(i, j);
70 if (std::abs(identity_value - value) > kEpsilon) 417 if (std::abs(identity_value - value) > kEpsilon)
71 return false; 418 return false;
72 } 419 }
73 } 420 }
74 return true; 421 return true;
75 } 422 }
76 423
77 } // namespace gfx 424 } // namespace gfx
OLDNEW
« ui/gfx/color_space_unittest.cc ('K') | « ui/gfx/skia_color_space_util.h ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698