[FFmpeg-devel] avfilter: add median filter

Submitted by Paul B Mahol on Oct. 26, 2019, 7:44 p.m.

Details

Message ID 20191026194402.10298-1-onemda@gmail.com
State New
Headers show

Commit Message

Paul B Mahol Oct. 26, 2019, 7:44 p.m.
Signed-off-by: Paul B Mahol <onemda@gmail.com>
---
 doc/filters.texi              |  15 ++
 libavfilter/Makefile          |   1 +
 libavfilter/allfilters.c      |   1 +
 libavfilter/median.h          |  50 +++++++
 libavfilter/median_template.c | 162 +++++++++++++++++++++
 libavfilter/vf_median.c       | 267 ++++++++++++++++++++++++++++++++++
 6 files changed, 496 insertions(+)
 create mode 100644 libavfilter/median.h
 create mode 100644 libavfilter/median_template.c
 create mode 100644 libavfilter/vf_median.c

Comments

Moritz Barsnick Oct. 28, 2019, 1:05 p.m.
On Sat, Oct 26, 2019 at 21:44:02 +0200, Paul B Mahol wrote:
> +Pick median pixel from certain rectangle defined by radius.

Is radius the correct term within a rectangle? (Just wondering, I do
understand the intent.)

> +        memset(coarse, 0, sizeof(coarse));
> +        memset(fine, 0, sizeof(fine));
> +        memset(luc, 0, sizeof(luc));

Shouldn't this be
    memset(arrayptr, 0, sizeof(*arrayptr));
?

> +    MedianContext *s = ctx->priv;
> +
> +    for (int i = 0; i < s->nb_threads && s->coarse && s->fine; i++) {
> +        av_freep(&s->coarse[i]);
> +        av_freep(&s->fine[i]);
> +    }

s->coarse and s->fine are constant in this scope. Instead of checking
them on each iteration, you could just exit early of they are (or
either is) NULL. Not that performance matters here at all.

I don't understand the rest functionally, looks fine to me.

Cheers,
Moritz
Paul B Mahol Oct. 28, 2019, 1:28 p.m.
On 10/28/19, Moritz Barsnick <barsnick@gmx.net> wrote:
> On Sat, Oct 26, 2019 at 21:44:02 +0200, Paul B Mahol wrote:
>> +Pick median pixel from certain rectangle defined by radius.
>
> Is radius the correct term within a rectangle? (Just wondering, I do
> understand the intent.)
>
>> +        memset(coarse, 0, sizeof(coarse));
>> +        memset(fine, 0, sizeof(fine));
>> +        memset(luc, 0, sizeof(luc));
>
> Shouldn't this be
>     memset(arrayptr, 0, sizeof(*arrayptr));

They are on stack and and locally changed to use memset but
initialization instead.

> ?
>
>> +    MedianContext *s = ctx->priv;
>> +
>> +    for (int i = 0; i < s->nb_threads && s->coarse && s->fine; i++) {
>> +        av_freep(&s->coarse[i]);
>> +        av_freep(&s->fine[i]);
>> +    }
>
> s->coarse and s->fine are constant in this scope. Instead of checking
> them on each iteration, you could just exit early of they are (or
> either is) NULL. Not that performance matters here at all.
>
> I don't understand the rest functionally, looks fine to me.
>
> Cheers,
> Moritz
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
Paul B Mahol Oct. 28, 2019, 1:28 p.m.
On 10/28/19, Moritz Barsnick <barsnick@gmx.net> wrote:
> On Sat, Oct 26, 2019 at 21:44:02 +0200, Paul B Mahol wrote:
>> +Pick median pixel from certain rectangle defined by radius.
>
> Is radius the correct term within a rectangle? (Just wondering, I do
> understand the intent.)

Correct term.

>
>> +        memset(coarse, 0, sizeof(coarse));
>> +        memset(fine, 0, sizeof(fine));
>> +        memset(luc, 0, sizeof(luc));
>
> Shouldn't this be
>     memset(arrayptr, 0, sizeof(*arrayptr));
> ?
>
>> +    MedianContext *s = ctx->priv;
>> +
>> +    for (int i = 0; i < s->nb_threads && s->coarse && s->fine; i++) {
>> +        av_freep(&s->coarse[i]);
>> +        av_freep(&s->fine[i]);
>> +    }
>
> s->coarse and s->fine are constant in this scope. Instead of checking
> them on each iteration, you could just exit early of they are (or
> either is) NULL. Not that performance matters here at all.
>
> I don't understand the rest functionally, looks fine to me.
>
> Cheers,
> Moritz
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
Paul B Mahol Oct. 28, 2019, 6:30 p.m.
Will apply slightly improved version soon.

Patch hide | download patch | download mbox

diff --git a/doc/filters.texi b/doc/filters.texi
index 5b8062d806..c4477e6677 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -12805,6 +12805,21 @@  Higher values should result in a smoother motion vector field but less
 optimal individual vectors. Default value is 1.
 @end table
 
+@section median
+
+Pick median pixel from certain rectangle defined by radius.
+
+This filter accepts the following options:
+
+@table @option
+@item radius
+Set horizontal and vertical radius size. Default value is @code{1}.
+Allowed range is integer from 1 to 127.
+
+@item planes
+Set which planes to process. Default is @code{15}, which is all available planes.
+@end table
+
 @section mergeplanes
 
 Merge color channel components from several video streams.
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index bae566d433..2080eed559 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -297,6 +297,7 @@  OBJS-$(CONFIG_MASKEDMERGE_FILTER)            += vf_maskedmerge.o framesync.o
 OBJS-$(CONFIG_MASKEDMIN_FILTER)              += vf_maskedminmax.o framesync.o
 OBJS-$(CONFIG_MASKFUN_FILTER)                += vf_maskfun.o
 OBJS-$(CONFIG_MCDEINT_FILTER)                += vf_mcdeint.o
+OBJS-$(CONFIG_MEDIAN_FILTER)                 += vf_median.o
 OBJS-$(CONFIG_MERGEPLANES_FILTER)            += vf_mergeplanes.o framesync.o
 OBJS-$(CONFIG_MESTIMATE_FILTER)              += vf_mestimate.o motion_estimation.o
 OBJS-$(CONFIG_METADATA_FILTER)               += f_metadata.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index d136734338..cb609067b6 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -282,6 +282,7 @@  extern AVFilter ff_vf_maskedmerge;
 extern AVFilter ff_vf_maskedmin;
 extern AVFilter ff_vf_maskfun;
 extern AVFilter ff_vf_mcdeint;
+extern AVFilter ff_vf_median;
 extern AVFilter ff_vf_mergeplanes;
 extern AVFilter ff_vf_mestimate;
 extern AVFilter ff_vf_metadata;
diff --git a/libavfilter/median.h b/libavfilter/median.h
new file mode 100644
index 0000000000..5aef7e76f2
--- /dev/null
+++ b/libavfilter/median.h
@@ -0,0 +1,50 @@ 
+/*
+ * Copyright (c) 2019 Paul B Mahol
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ */
+
+#ifndef AVFILTER_MEDIAN_H
+#define AVFILTER_MEDIAN_H
+
+#include "avfilter.h"
+
+typedef struct MedianContext {
+    const AVClass *class;
+
+    int planes;
+    int radius;
+
+    int planewidth[4];
+    int planeheight[4];
+    int depth;
+    int nb_planes;
+    int nb_threads;
+
+    uint16_t **coarse, **fine;
+    int coarse_size, fine_size;
+    int bins;
+    int t;
+
+    void (*filter_plane)(AVFilterContext *ctx, const uint8_t *ssrc, int src_linesize,
+                         uint8_t *ddst, int dst_linesize, int width, int height,
+                         int slice_h_start, int slice_h_end, int jobnr);
+} MedianContext;
+
+#endif
diff --git a/libavfilter/median_template.c b/libavfilter/median_template.c
new file mode 100644
index 0000000000..466e05fd40
--- /dev/null
+++ b/libavfilter/median_template.c
@@ -0,0 +1,162 @@ 
+/*
+ * Copyright (c) 2019 Paul B Mahol
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ */
+
+#include "libavutil/avassert.h"
+#include "avfilter.h"
+#include "formats.h"
+#include "internal.h"
+#include "video.h"
+
+#undef pixel
+#if DEPTH == 8
+#define pixel uint8_t
+#else
+#define pixel uint16_t
+#endif
+
+#undef fn
+#undef fn2
+#undef fn3
+#define SHIFT   ((DEPTH + 1) / 2)
+#define BINS    (1 << SHIFT)
+#define MASK    (BINS - 1)
+#define fn3(a,b)   a##_##b
+#define fn2(a,b)   fn3(a,b)
+#define fn(a)      fn2(a, DEPTH)
+
+#define PICK_COARSE_BIN(x, y) (BINS * (x) + ((y) >> SHIFT))
+#define PICK_FINE_BIN(x, y, z) (BINS * ((x) * ((y) >> SHIFT) + (z)) + ((y) & MASK))
+
+static void fn(filter_plane)(AVFilterContext *ctx, const uint8_t *ssrc, int src_linesize,
+                             uint8_t *ddst, int dst_linesize, int width, int height,
+                             int slice_h_start, int slice_h_end, int jobnr)
+{
+    MedianContext *s = ctx->priv;
+    uint16_t *ccoarse = s->coarse[jobnr];
+    uint16_t *cfine = s->fine[jobnr];
+    uint16_t coarse[BINS];
+    uint16_t fine[BINS][BINS];
+    uint16_t luc[BINS];
+    const int radius = s->radius;
+    const int t = s->t;
+    const pixel *src = (const pixel *)ssrc;
+    pixel *dst = (pixel *)ddst;
+    const pixel *srcp;
+    const pixel *p, *q;
+
+    src_linesize /= sizeof(pixel);
+    dst_linesize /= sizeof(pixel);
+
+    memset(cfine, 0, s->fine_size * sizeof(*cfine));
+    memset(ccoarse, 0, s->coarse_size * sizeof(*ccoarse));
+
+    srcp = src + FFMAX(0, slice_h_start - radius) * src_linesize;;
+    if (jobnr == 0) {
+        for (int i = 0; i < width; i++) {
+            cfine[PICK_FINE_BIN(width, srcp[i], i)] += radius + 1;
+            ccoarse[PICK_COARSE_BIN(i, srcp[i])] += radius + 1;
+        }
+    }
+
+    srcp = src + FFMAX(0, slice_h_start - radius - (jobnr != 0)) * src_linesize;;
+    for (int i = 0; i < radius + (jobnr != 0) * (1 + radius); i++) {
+        for (int j = 0; j < width; j++) {
+            cfine[PICK_FINE_BIN(width, srcp[j], j)]++;
+            ccoarse[PICK_COARSE_BIN(j, srcp[j])]++;
+        }
+        srcp += src_linesize;
+    }
+
+    srcp = src;
+
+    for (int i = slice_h_start; i < slice_h_end; i++) {
+        p = srcp + src_linesize * FFMAX(0, i - radius - 1);
+        q = p + width;
+        for (int j = 0; p != q; j++, p++) {
+            cfine[PICK_FINE_BIN(width, *p, j)]--;
+            ccoarse[PICK_COARSE_BIN(j, *p)]--;
+        }
+
+        p = srcp + src_linesize * FFMIN(height - 1, i + radius);
+        q = p + width;
+        for (int j = 0; p != q; j++, p++) {
+            cfine[PICK_FINE_BIN(width, *p, j)]++;
+            ccoarse[PICK_COARSE_BIN(j, *p)]++;
+        }
+
+        memset(coarse, 0, sizeof(coarse));
+        memset(fine, 0, sizeof(fine));
+        memset(luc, 0, sizeof(luc));
+
+        hmuladd(coarse, &ccoarse[0], radius, BINS);
+        for (int j = 0; j < radius; j++)
+            hadd(coarse, &ccoarse[BINS * j], BINS);
+        for (int k = 0; k < BINS; k++)
+            hmuladd(&fine[k][0], &cfine[BINS * width * k], 2 * radius + 1, BINS);
+
+        for (int j = 0; j < width; j++) {
+            uint16_t sum = 0;
+            uint16_t *segment;
+            int k, b;
+
+            hadd(coarse, &ccoarse[BINS * FFMIN(j + radius, width - 1)], BINS);
+
+            for (k = 0; k < BINS; k++) {
+                sum += coarse[k];
+                if (sum > t) {
+                    sum -= coarse[k];
+                    break;
+                }
+            }
+            av_assert0(k < BINS);
+
+            if (luc[k] <= j - radius) {
+                memset(&fine[k], 0, BINS * sizeof(uint16_t));
+                for (luc[k] = j - radius; luc[k] < FFMIN(j + radius + 1, width); luc[k]++)
+                    hadd(fine[k], &cfine[BINS * (width * k + luc[k])], BINS);
+                if (luc[k] < j + radius + 1) {
+                    hmuladd(&fine[k][0], &cfine[BINS * (width * k + width - 1)], j + radius + 1 - width, BINS);
+                    luc[k] = j + radius + 1;
+                }
+            } else {
+                for (; luc[k] < j + radius + 1; luc[k]++) {
+                    hsub(fine[k], &cfine[BINS * (width * k + FFMAX(luc[k] - 2 * radius - 1, 0))], BINS);
+                    hadd(fine[k], &cfine[BINS * (width * k + FFMIN(luc[k], width - 1))], BINS);
+                }
+            }
+
+            hsub(coarse, &ccoarse[BINS * FFMAX(j - radius, 0)], BINS);
+
+            segment = fine[k];
+            for (b = 0; b < BINS; b++) {
+                sum += segment[b];
+                if (sum > t) {
+                    dst[j] = BINS * k + b;
+                    break;
+                }
+            }
+            av_assert0(b < BINS);
+        }
+
+        dst += dst_linesize;
+    }
+}
diff --git a/libavfilter/vf_median.c b/libavfilter/vf_median.c
new file mode 100644
index 0000000000..be3b8c6faf
--- /dev/null
+++ b/libavfilter/vf_median.c
@@ -0,0 +1,267 @@ 
+/*
+ * Copyright (c) 2019 Paul B Mahol
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ */
+
+#include "libavutil/avassert.h"
+#include "libavutil/imgutils.h"
+#include "libavutil/opt.h"
+#include "libavutil/pixdesc.h"
+#include "avfilter.h"
+#include "formats.h"
+#include "internal.h"
+#include "median.h"
+#include "video.h"
+
+static void hadd(uint16_t *dst, const uint16_t *src, int bins)
+{
+    for (int i = 0; i < bins; i++)
+        dst[i] += src[i];
+}
+
+static void hsub(uint16_t *dst, const uint16_t *src, int bins)
+{
+    for (int i = 0; i < bins; i++)
+        dst[i] -= src[i];
+}
+
+static void hmuladd(uint16_t *dst, const uint16_t *src, uint16_t f, int bins)
+{
+    for (int i = 0; i < bins; i++)
+        dst[i] += f * src[i];
+}
+
+#define DEPTH 8
+#include "median_template.c"
+
+#undef DEPTH
+#define DEPTH 9
+#include "median_template.c"
+
+#undef DEPTH
+#define DEPTH 10
+#include "median_template.c"
+
+#undef DEPTH
+#define DEPTH 12
+#include "median_template.c"
+
+#undef DEPTH
+#define DEPTH 14
+#include "median_template.c"
+
+#undef DEPTH
+#define DEPTH 16
+#include "median_template.c"
+
+#define OFFSET(x) offsetof(MedianContext, x)
+#define FLAGS AV_OPT_FLAG_VIDEO_PARAM|AV_OPT_FLAG_FILTERING_PARAM
+
+static const AVOption median_options[] = {
+    { "radius", "set median radius",    OFFSET(radius), AV_OPT_TYPE_INT,   {.i64=1},     1,  127, FLAGS },
+    { "planes", "set planes to filter", OFFSET(planes), AV_OPT_TYPE_INT,   {.i64=0xF},   0,  0xF, FLAGS },
+    { NULL }
+};
+
+AVFILTER_DEFINE_CLASS(median);
+
+static int query_formats(AVFilterContext *ctx)
+{
+    static const enum AVPixelFormat pix_fmts[] = {
+        AV_PIX_FMT_YUVA444P, AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV440P,
+        AV_PIX_FMT_YUVJ444P, AV_PIX_FMT_YUVJ440P,
+        AV_PIX_FMT_YUVA422P, AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUVA420P, AV_PIX_FMT_YUV420P,
+        AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUVJ420P,
+        AV_PIX_FMT_YUVJ411P, AV_PIX_FMT_YUV411P, AV_PIX_FMT_YUV410P,
+        AV_PIX_FMT_GBRP, AV_PIX_FMT_GBRAP, AV_PIX_FMT_GRAY8, AV_PIX_FMT_GRAY9,
+        AV_PIX_FMT_YUV420P9, AV_PIX_FMT_YUV422P9, AV_PIX_FMT_YUV444P9, AV_PIX_FMT_GBRP9,
+        AV_PIX_FMT_YUVA420P9, AV_PIX_FMT_YUVA422P9, AV_PIX_FMT_YUVA444P9,
+        AV_PIX_FMT_YUV420P10, AV_PIX_FMT_YUV422P10, AV_PIX_FMT_YUV444P10,
+        AV_PIX_FMT_YUV420P12, AV_PIX_FMT_YUV422P12, AV_PIX_FMT_YUV444P12, AV_PIX_FMT_YUV440P12,
+        AV_PIX_FMT_YUV420P14, AV_PIX_FMT_YUV422P14, AV_PIX_FMT_YUV444P14,
+        AV_PIX_FMT_YUV420P16, AV_PIX_FMT_YUV422P16, AV_PIX_FMT_YUV444P16,
+        AV_PIX_FMT_YUVA420P10, AV_PIX_FMT_YUVA422P10, AV_PIX_FMT_YUVA444P10,
+        AV_PIX_FMT_YUVA420P16, AV_PIX_FMT_YUVA422P16, AV_PIX_FMT_YUVA444P16,
+        AV_PIX_FMT_GBRP10, AV_PIX_FMT_GBRP12, AV_PIX_FMT_GBRP14, AV_PIX_FMT_GBRP16,
+        AV_PIX_FMT_GBRAP10, AV_PIX_FMT_GBRAP12, AV_PIX_FMT_GBRAP16,
+        AV_PIX_FMT_GRAY10, AV_PIX_FMT_GRAY12, AV_PIX_FMT_GRAY14, AV_PIX_FMT_GRAY16,
+        AV_PIX_FMT_NONE
+    };
+
+    return ff_set_common_formats(ctx, ff_make_format_list(pix_fmts));
+}
+
+static int config_input(AVFilterLink *inlink)
+{
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
+    MedianContext *s = inlink->dst->priv;
+
+    s->depth = desc->comp[0].depth;
+    s->planewidth[1] = s->planewidth[2] = AV_CEIL_RSHIFT(inlink->w, desc->log2_chroma_w);
+    s->planewidth[0] = s->planewidth[3] = inlink->w;
+    s->planeheight[1] = s->planeheight[2] = AV_CEIL_RSHIFT(inlink->h, desc->log2_chroma_h);
+    s->planeheight[0] = s->planeheight[3] = inlink->h;
+
+    s->nb_planes = av_pix_fmt_count_planes(inlink->format);
+    s->t = 2 * s->radius * s->radius + 2 * s->radius;
+
+    for (int i = 0; i < s->nb_planes; i++) {
+        if (!(s->planes & (1 << i)))
+            continue;
+
+        if (s->planewidth[i] < s->radius * 2 + 1) {
+            av_log(inlink->dst, AV_LOG_ERROR, "The %d plane width %d must be not less than %d\n", i, s->planewidth[i], s->radius * 2 + 1);
+            return AVERROR(EINVAL);
+        }
+
+        if (s->planeheight[i] < s->radius * 2 + 1) {
+            av_log(inlink->dst, AV_LOG_ERROR, "The %d plane height %d must be not less than %d\n", i, s->planeheight[i], s->radius * 2 + 1);
+            return AVERROR(EINVAL);
+        }
+    }
+
+    s->nb_threads = FFMAX(1, FFMIN(s->planeheight[1] / (s->radius + 1), ff_filter_get_nb_threads(inlink->dst)));
+    s->bins   = 1 << ((s->depth + 1) / 2);
+    s->fine_size = s->bins * s->bins * inlink->w;
+    s->coarse_size = s->bins * inlink->w;
+    s->coarse = av_calloc(s->nb_threads, sizeof(*s->coarse));
+    s->fine   = av_calloc(s->nb_threads, sizeof(*s->fine));
+    if (!s->coarse || !s->fine)
+        return AVERROR(ENOMEM);
+    for (int i = 0; i < s->nb_threads; i++) {
+        s->coarse[i] = av_malloc_array(s->coarse_size, sizeof(**s->coarse));
+        s->fine[i]   = av_malloc_array(s->fine_size, sizeof(**s->fine));
+        if (!s->coarse[i] || !s->fine[i])
+            return AVERROR(ENOMEM);
+    }
+
+    switch (s->depth) {
+    case  8: s->filter_plane = filter_plane_8;  break;
+    case  9: s->filter_plane = filter_plane_9;  break;
+    case 10: s->filter_plane = filter_plane_10; break;
+    case 12: s->filter_plane = filter_plane_12; break;
+    case 14: s->filter_plane = filter_plane_14; break;
+    case 16: s->filter_plane = filter_plane_16; break;
+    }
+
+    return 0;
+}
+
+typedef struct ThreadData {
+    AVFrame *in, *out;
+} ThreadData;
+
+static int filter_slice(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs)
+{
+    MedianContext *s = ctx->priv;
+    ThreadData *td = arg;
+    AVFrame *in = td->in;
+    AVFrame *out = td->out;
+
+    for (int plane = 0; plane < s->nb_planes; plane++) {
+        const int h = s->planeheight[plane];
+        const int w = s->planewidth[plane];
+        const int slice_h_start = (h * jobnr) / nb_jobs;
+        const int slice_h_end = (h * (jobnr+1)) / nb_jobs;
+
+        if (!(s->planes & (1 << plane))) {
+            av_image_copy_plane(out->data[plane] + slice_h_start * out->linesize[plane],
+                                out->linesize[plane],
+                                in->data[plane] + slice_h_start * in->linesize[plane],
+                                in->linesize[plane],
+                                w * ((s->depth + 7) / 8),
+                                slice_h_end - slice_h_start);
+            continue;
+        }
+
+        s->filter_plane(ctx, in->data[plane],
+                        in->linesize[plane],
+                        out->data[plane] + slice_h_start * out->linesize[plane],
+                        out->linesize[plane], w, h,
+                        slice_h_start, slice_h_end, jobnr);
+    }
+
+    return 0;
+}
+
+static int filter_frame(AVFilterLink *inlink, AVFrame *in)
+{
+    AVFilterContext *ctx = inlink->dst;
+    MedianContext *s = ctx->priv;
+    AVFilterLink *outlink = ctx->outputs[0];
+    ThreadData td;
+    AVFrame *out;
+
+    out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
+    if (!out) {
+        av_frame_free(&in);
+        return AVERROR(ENOMEM);
+    }
+    av_frame_copy_props(out, in);
+
+    td.in = in; td.out = out;
+    ctx->internal->execute(ctx, filter_slice, &td, NULL, s->nb_threads);
+
+    av_frame_free(&in);
+    return ff_filter_frame(outlink, out);
+}
+
+static av_cold void uninit(AVFilterContext *ctx)
+{
+    MedianContext *s = ctx->priv;
+
+    for (int i = 0; i < s->nb_threads && s->coarse && s->fine; i++) {
+        av_freep(&s->coarse[i]);
+        av_freep(&s->fine[i]);
+    }
+
+    av_freep(&s->coarse);
+    av_freep(&s->fine);
+}
+
+static const AVFilterPad median_inputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = config_input,
+        .filter_frame = filter_frame,
+    },
+    { NULL }
+};
+
+static const AVFilterPad median_outputs[] = {
+    {
+        .name = "default",
+        .type = AVMEDIA_TYPE_VIDEO,
+    },
+    { NULL }
+};
+
+AVFilter ff_vf_median = {
+    .name          = "median",
+    .description   = NULL_IF_CONFIG_SMALL("Apply Median filter."),
+    .priv_size     = sizeof(MedianContext),
+    .priv_class    = &median_class,
+    .uninit        = uninit,
+    .query_formats = query_formats,
+    .inputs        = median_inputs,
+    .outputs       = median_outputs,
+    .flags         = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC | AVFILTER_FLAG_SLICE_THREADS,
+};