Ticket #9947: 0001-avfilter-vf_curves-add-PCHIP-interpolator-and-interp.patch
| File 0001-avfilter-vf_curves-add-PCHIP-interpolator-and-interp.patch, 14.8 KB (added by , 4 years ago) |
|---|
-
doc/filters.texi
From e0402d1272d8ef4964ee027cb0b510ebb62f7d6c Mon Sep 17 00:00:00 2001 From: "Takeshi (Kesh) Ikuma" <tikuma@hotmail.com> Date: Sat, 1 Oct 2022 21:12:59 -0500 Subject: [PATCH] avfilter/vf_curves: add PCHIP interpolator and interp option summary: This patch modifies the `curves` filter with new `interp` option to let user pick the existing natural cubic spline interpolation and the new PCHIP interapolation. reason: The natural cubic spline does not impose monotonicity between the keypoints. As such, the fitted curve may vary wildly against user's intension. The PCHIP interpolation is not as smooth as the natural spline but guarantees the monotonicity. Providing both options enhances users experience (e.g., reduces the number of keypoints to realize the desired curve). See the related bug report for the example of an ill-interpolated curve. alternate solution: Both Photoshop and GIMP appear to use monotonic interpolation in their curve tools, which were the models for this filter. As such, an alternate solution is to drop the natural spline and go without the `interp` option. related bug report: https://trac.ffmpeg.org/ticket/9947 (filed by myself) Signed-off-by: Takeshi (Kesh) Ikuma <tikuma@hotmail.com> modified: doc/filters.texi modified: libavfilter/vf_curves.c --- doc/filters.texi | 23 +++- libavfilter/vf_curves.c | 255 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 253 insertions(+), 25 deletions(-) diff --git a/doc/filters.texi b/doc/filters.texi index d0f718678c..08a79644e1 100644a b By default, a component curve is defined by the two points @var{(0;0)} and 10389 10389 "adjusted" to its own value, which means no change to the image. 10390 10390 10391 10391 The filter allows you to redefine these two points and add some more. A new 10392 curve (using a natural cubic spline interpolation) will be define to pass 10393 smoothly through all these new coordinates. The new defined points needs to be 10394 strictly increasing over the x-axis, and their @var{x} and @var{y} values must 10395 be in the @var{[0;1]} interval. If the computed curves happened to go outside 10396 the vector spaces, the values will be clipped accordingly. 10392 curve will be define to pass smoothly through all these new coordinates. The 10393 new defined points needs to be strictly increasing over the x-axis, and their 10394 @var{x} and @var{y} values must be in the @var{[0;1]} interval. The curve is 10395 formed by using a natural or monotonic cubic spline interpolation, depending 10396 on the @var{interp} option (default: @code{natural}). The @code{natural} 10397 spline produces a smoother curve in general while the monotonic (@code{pchip}) 10398 spline guarantees the transitions between the specified points to be 10399 monotonic. If the computed curves happened to go outside the vector spaces, 10400 the values will be clipped accordingly. 10397 10401 10398 10402 The filter accepts the following options: 10399 10403 … … options. In this case, the unset component(s) will fallback on this 10437 10441 Specify a Photoshop curves file (@code{.acv}) to import the settings from. 10438 10442 @item plot 10439 10443 Save Gnuplot script of the curves in specified file. 10444 @item interp 10445 Specify the kind of interpolation. Available algorithms are: 10446 @table @samp 10447 @item natural 10448 Natural cubic spline using a piece-wise cubic polynomial that is twice continuously differentiable. 10449 @item pchip 10450 Monotonic cubic spline using a piecewise cubic Hermite interpolating polynomial (PCHIP). 10451 @end table 10452 10440 10453 @end table 10441 10454 10442 10455 To avoid some filtergraph syntax conflicts, each key points list need to be -
libavfilter/vf_curves.c
diff --git a/libavfilter/vf_curves.c b/libavfilter/vf_curves.c index 498b06f6e5..d0efa380e1 100644
a b enum preset { 58 58 NB_PRESETS, 59 59 }; 60 60 61 enum interp { 62 INTERP_NATURAL, 63 INTERP_PCHIP, 64 NB_INTERPS, 65 }; 66 61 67 typedef struct CurvesContext { 62 68 const AVClass *class; 63 69 int preset; … … typedef struct CurvesContext { 73 79 int is_16bit; 74 80 int depth; 75 81 int parsed_psfile; 82 int interp; 76 83 77 84 int (*filter_slice)(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs); 78 85 } CurvesContext; … … static const AVOption curves_options[] = { 107 114 { "all", "set points coordinates for all components", OFFSET(comp_points_str_all), AV_OPT_TYPE_STRING, {.str=NULL}, .flags = FLAGS }, 108 115 { "psfile", "set Photoshop curves file name", OFFSET(psfile), AV_OPT_TYPE_STRING, {.str=NULL}, .flags = FLAGS }, 109 116 { "plot", "save Gnuplot script of the curves in specified file", OFFSET(plot_filename), AV_OPT_TYPE_STRING, {.str=NULL}, .flags = FLAGS }, 117 { "interp", "specify the kind of interpolation", OFFSET(interp), AV_OPT_TYPE_INT, {.i64=INTERP_NATURAL}, INTERP_NATURAL, NB_INTERPS-1, FLAGS, "interp_name" }, 118 { "natural", "natural cubic spline", 0, AV_OPT_TYPE_CONST, {.i64=INTERP_NATURAL}, 0, 0, FLAGS, "interp_name" }, 119 { "pchip", "monotonically cubic interpolation", 0, AV_OPT_TYPE_CONST, {.i64=INTERP_PCHIP}, 0, 0, FLAGS, "interp_name" }, 110 120 { NULL } 111 121 }; 112 122 … … end: 336 346 av_free(h); 337 347 av_free(r); 338 348 return ret; 349 350 } 351 352 #define SIGN(x) (x>0.0?1:x<0.0?-1:0) 353 354 /** 355 * Evalaute the derivative of an edge endpoint 356 * 357 * @param h0 input interval of the interval closest to the edge 358 * @param h1 input interval of the interval next to the closest 359 * @param m0 linear slope of the interval closest to the edge 360 * @param m1 linear slope of the intervalnext to the closest 361 * @return edge endpoint derivative 362 * 363 * Based on scipy.interpolate._edge_case() 364 * https://github.com/scipy/scipy/blob/2e5883ef7af4f5ed4a5b80a1759a45e43163bf3f/scipy/interpolate/_cubic.py#L239 365 * which is a python implementation of the special case endpoints, as suggested in 366 * Cleve Moler, Numerical Computing with MATLAB, Chap 3.6 (pchiptx.m) 367 */ 368 static double pchip_edge_case(double h0, double h1, double m0, double m1) 369 { 370 int mask, mask2; 371 double d; 372 373 d = ((2 * h0 + h1) * m0 - h0 * m1) / (h0 + h1); 374 375 mask = SIGN(d) != SIGN(m0); 376 mask2 = (SIGN(m0) != SIGN(m1)) && (fabs(d) > 3. * fabs(m0)); 377 378 if (mask) d = 0.0; 379 else if (mask2) d = 3.0 * m0; 380 381 return d; 339 382 } 340 383 341 #define DECLARE_INTERPOLATE_FUNC(nbits) \ 342 static int interpolate##nbits(void *log_ctx, uint16_t *y, \ 343 const struct keypoint *points) \ 344 { \ 345 return interpolate(log_ctx, y, points, nbits); \ 384 /** 385 * Evalaute the piecewise polynomial derivatives at endpoints 386 * 387 * @param n input interval of the interval closest to the edge 388 * @param hk input intervals 389 * @param mk linear slopes over intervals 390 * @param dk endpoint derivatives (output) 391 * @return 0 success 392 * 393 * Based on scipy.interpolate._find_derivatives() 394 * https://github.com/scipy/scipy/blob/2e5883ef7af4f5ed4a5b80a1759a45e43163bf3f/scipy/interpolate/_cubic.py#L254 395 */ 396 397 static int pchip_find_derivatives(const int n, const double *hk, const double *mk, double *dk) 398 { 399 int ret = 0; 400 const int m = n - 1; 401 int8_t *smk; 402 403 smk = av_malloc(n); 404 if (!smk) { 405 ret = AVERROR(ENOMEM); 406 goto end; 407 } 408 409 /* smk = sgn(mk) */ 410 for (int i = 0; i < n; ++i) smk[i] = SIGN(mk[i]); 411 412 /* check the strict monotonicity */ 413 for (int i = 0; i < m; ++i) { 414 int8_t condition = (smk[i + 1] != smk[i]) || (mk[i + 1] == 0) || (mk[i] == 0); 415 if (condition) { 416 dk[i + 1] = 0.0; 417 } else { 418 double w1 = 2 * hk[i + 1] + hk[i]; 419 double w2 = hk[i + 1] + 2 * hk[i]; 420 dk[i + 1] = (w1 + w2) / (w1 / mk[i] + w2 / mk[i + 1]); 421 } 422 } 423 424 dk[0] = pchip_edge_case(hk[0], hk[1], mk[0], mk[1]); 425 dk[n] = pchip_edge_case(hk[n - 1], hk[n - 2], mk[n - 1], mk[n - 2]); 426 427 end: 428 av_free(smk); 429 430 return ret; 431 } 432 433 /** 434 * Evalaute half of the cubic hermite interpolation expression, wrt one interval endpoint 435 * 436 * @param x normalized input value at the endpoint 437 * @param f output value at the endpoint 438 * @param d derivative at the endpoint: normalized to the interval, and properly sign adjusted 439 * @return half of the interpolated value 440 */ 441 static inline double interp_cubic_hermite_half(const double x, const double f, 442 const double d) 443 { 444 double x2 = x * x, x3 = x2 * x; 445 return f * (3.0 * x2 - 2.0 * x3) + d * (x3 - x2); 446 } 447 448 /** 449 * Prepare the lookup table by piecewise monotonic cubic interpolation (PCHIP) 450 * 451 * @param log_ctx for logging 452 * @param y output lookup table (output) 453 * @param points user-defined control points/endpoints 454 * @param nbits bitdepth 455 * @return 0 success 456 * 457 * References: 458 * [1] F. N. Fritsch and J. Butland, A method for constructing local monotone piecewise 459 * cubic interpolants, SIAM J. Sci. Comput., 5(2), 300-304 (1984). DOI:10.1137/0905021. 460 * [2] scipy.interpolate: https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.PchipInterpolator.html 461 */ 462 static inline int interpolate_pchip(void *log_ctx, uint16_t *y, 463 const struct keypoint *points, int nbits) 464 { 465 int i, ret = 0; 466 const struct keypoint *point = points; 467 const int lut_size = 1<<nbits; 468 const int n = get_nb_points(points); // number of endpoints 469 double *xi, *fi, *di, *hi, *mi; 470 const int scale = lut_size - 1; // white value 471 uint16_t x, x0; /* input index/value */ 472 473 /* no change for n = 0 or 1 */ 474 if (n == 0) { 475 /* no points, no change */ 476 for (i = 0; i < lut_size; ++i) y[i] = i; 477 return 0; 478 } 479 480 if (n == 1) { 481 /* 1 point - 1 color everywhere */ 482 const uint16_t yval = CLIP(point->y * scale); 483 for (i = 0; i < lut_size; ++i) y[i] = yval; 484 return 0; 485 } 486 487 xi = av_calloc(3*n + 2*(n-1), sizeof(double)); /* output values at inteval endpoints */ 488 489 if (!xi) { 490 ret = AVERROR(ENOMEM); 491 goto end; 492 } 493 494 fi = xi + n; /* output values at inteval endpoints */ 495 di = fi + n; /* output slope wrt normalized input at interval endpoints */ 496 hi = di + n; /* interval widths */ 497 mi = hi + n - 1; /* linear slope over intervals */ 498 499 /* scale endpoints and store them in a contiguous memory block */ 500 for (i = 0; i < n; ++i) { 501 xi[i] = point->x * scale; 502 fi[i] = point->y * scale; 503 point = point->next; 504 } 505 506 /* h(i) = x(i+1) - x(i); mi(i) = (f(i+1)-f(i))/h(i) */ 507 for (i = 0; i < n - 1; ++i) { 508 const double val = (xi[i+1]-xi[i]); 509 hi[i] = val; 510 mi[i] = (fi[i+1]-fi[i]) / val; 511 } 512 513 if (n == 2) { 514 /* edge case, use linear interpolation */ 515 const double m = mi[0], b = fi[0] - xi[0]*m; 516 for (i = 0; i < lut_size; ++i) y[i] = CLIP((i*m + b)); 517 goto end; 518 } 519 520 /* compute the derivatives at the endpoints*/ 521 ret = pchip_find_derivatives(n-1,hi,mi,di); 522 if (ret) goto end; 523 524 /* interpolate/extrapolate */ 525 x = 0; 526 if (xi[0] > 0) { 527 /* below first endpoint, use the first endpoint value*/ 528 const double xi0 = xi[0]; 529 const uint16_t yval = CLIP(fi[0]); 530 for (; x < xi0; ++x) y[x] = yval; 531 av_log(log_ctx, AV_LOG_DEBUG, "Interval -1: [0, %d] -> %d\n", x - 1, yval); 532 } 533 534 /* for each interval */ 535 for (i = 0, x0 = x; i < n-1; ++i, x0 = x) { 536 537 const double xi0 = xi[i]; /* start-of-interval input value */ 538 const double xi1 = xi[i + 1]; /* end-of-interval input value */ 539 const double h = hi[i]; /* interval width */ 540 const double f0 = fi[i]; /* start-of-interval output value */ 541 const double f1 = fi[i + 1]; /* end-of-interval output value */ 542 const double d0 = di[i]; /* start-of-interval derivative */ 543 const double d1 = di[i + 1]; /* end-of-interval derivative */ 544 545 /* fill the lut over the interval */ 546 for (; x < xi1; ++x) { /* safe not to check j < lut_size */ 547 const double xx = (x - xi0) / h; /* normalize input */ 548 const double yy = interp_cubic_hermite_half(1 - xx, f0, -h * d0) 549 + interp_cubic_hermite_half(xx, f1, h * d1); 550 y[x] = CLIP(yy); 551 } 552 553 if (x > x0) 554 av_log(log_ctx, AV_LOG_DEBUG, "Interval %d: [%d, %d] -> [%d, %d]\n", 555 i, x0, x-1, y[x0], y[x-1]); 556 else 557 av_log(log_ctx, AV_LOG_DEBUG, "Interval %d: empty\n", i); 558 } 559 560 if (x < lut_size) { 561 /* above the last endpoint, use the last endpoint value*/ 562 const uint16_t yval = CLIP(fi[n - 1]); 563 av_log(log_ctx, AV_LOG_DEBUG, "Interval %d: [%d, %d] -> %d\n", 564 n, x, lut_size - 1, yval); 565 for (; x < lut_size; ++x) y[x] = yval; 566 } 567 568 end: 569 av_free(xi); 570 return ret; 346 571 } 347 572 348 DECLARE_INTERPOLATE_FUNC(8)349 DECLARE_INTERPOLATE_FUNC(9)350 DECLARE_INTERPOLATE_FUNC(10)351 DECLARE_INTERPOLATE_FUNC(12)352 DECLARE_INTERPOLATE_FUNC(14)353 DECLARE_INTERPOLATE_FUNC(16)354 573 355 574 static int parse_psfile(AVFilterContext *ctx, const char *fname) 356 575 { … … static int config_input(AVFilterLink *inlink) 651 870 ret = parse_points_str(ctx, comp_points + i, curves->comp_points_str[i], curves->lut_size); 652 871 if (ret < 0) 653 872 return ret; 654 switch (curves->depth) { 655 case 8: ret = interpolate8 (ctx, curves->graph[i], comp_points[i]); break; 656 case 9: ret = interpolate9 (ctx, curves->graph[i], comp_points[i]); break; 657 case 10: ret = interpolate10(ctx, curves->graph[i], comp_points[i]); break; 658 case 12: ret = interpolate12(ctx, curves->graph[i], comp_points[i]); break; 659 case 14: ret = interpolate14(ctx, curves->graph[i], comp_points[i]); break; 660 case 16: ret = interpolate16(ctx, curves->graph[i], comp_points[i]); break; 661 } 873 if (curves->interp==INTERP_PCHIP) 874 ret = interpolate_pchip (ctx, curves->graph[i], comp_points[i], curves->depth); 875 else 876 ret = interpolate (ctx, curves->graph[i], comp_points[i], curves->depth); 662 877 if (ret < 0) 663 878 return ret; 664 879 } … … static int process_command(AVFilterContext *ctx, const char *cmd, const char *ar 735 950 736 951 if (!strcmp(cmd, "plot")) { 737 952 curves->saved_plot = 0; 738 } else if (!strcmp(cmd, "all") || !strcmp(cmd, "preset") || !strcmp(cmd, "psfile") ) {953 } else if (!strcmp(cmd, "all") || !strcmp(cmd, "preset") || !strcmp(cmd, "psfile") || !strcmp(cmd, "interp")) { 739 954 if (!strcmp(cmd, "psfile")) 740 955 curves->parsed_psfile = 0; 741 956 av_freep(&curves->comp_points_str_all);
