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 kesh, 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 100644
    a b By default, a component curve is defined by the two points @var{(0;0)} and  
    1038910389"adjusted" to its own value, which means no change to the image.
    1039010390
    1039110391The 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.
     10392curve will be define to pass smoothly through all these new coordinates. The
     10393new 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
     10395formed by using a natural or monotonic cubic spline interpolation, depending
     10396on the @var{interp} option (default: @code{natural}). The @code{natural}
     10397spline produces a smoother curve in general while the monotonic (@code{pchip})
     10398spline guarantees the transitions between the specified points to be
     10399monotonic. If the computed curves happened to go outside the vector spaces,
     10400the values will be clipped accordingly.
    1039710401
    1039810402The filter accepts the following options:
    1039910403
    options. In this case, the unset component(s) will fallback on this  
    1043710441Specify a Photoshop curves file (@code{.acv}) to import the settings from.
    1043810442@item plot
    1043910443Save Gnuplot script of the curves in specified file.
     10444@item interp
     10445Specify the kind of interpolation. Available algorithms are:
     10446@table @samp
     10447@item natural
     10448Natural cubic spline using a piece-wise cubic polynomial that is twice continuously differentiable.
     10449@item pchip
     10450Monotonic cubic spline using a piecewise cubic Hermite interpolating polynomial (PCHIP).
     10451@end table
     10452
    1044010453@end table
    1044110454
    1044210455To 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 {  
    5858    NB_PRESETS,
    5959};
    6060
     61enum interp {
     62    INTERP_NATURAL,
     63    INTERP_PCHIP,
     64    NB_INTERPS,
     65};
     66
    6167typedef struct CurvesContext {
    6268    const AVClass *class;
    6369    int preset;
    typedef struct CurvesContext {  
    7379    int is_16bit;
    7480    int depth;
    7581    int parsed_psfile;
     82    int interp;
    7683
    7784    int (*filter_slice)(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs);
    7885} CurvesContext;
    static const AVOption curves_options[] = {  
    107114    { "all",   "set points coordinates for all components", OFFSET(comp_points_str_all), AV_OPT_TYPE_STRING, {.str=NULL}, .flags = FLAGS },
    108115    { "psfile", "set Photoshop curves file name", OFFSET(psfile), AV_OPT_TYPE_STRING, {.str=NULL}, .flags = FLAGS },
    109116    { "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" },
    110120    { NULL }
    111121};
    112122
    end:  
    336346    av_free(h);
    337347    av_free(r);
    338348    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*/
     368static 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;
    339382}
    340383
    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
     397static 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
     427end:
     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*/
     441static 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*/
     462static 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
     568end:
     569    av_free(xi);
     570    return ret;
    346571}
    347572
    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)
    354573
    355574static int parse_psfile(AVFilterContext *ctx, const char *fname)
    356575{
    static int config_input(AVFilterLink *inlink)  
    651870        ret = parse_points_str(ctx, comp_points + i, curves->comp_points_str[i], curves->lut_size);
    652871        if (ret < 0)
    653872            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);
    662877        if (ret < 0)
    663878            return ret;
    664879    }
    static int process_command(AVFilterContext *ctx, const char *cmd, const char *ar  
    735950
    736951    if (!strcmp(cmd, "plot")) {
    737952        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")) {
    739954        if (!strcmp(cmd, "psfile"))
    740955            curves->parsed_psfile = 0;
    741956        av_freep(&curves->comp_points_str_all);