diff mbox series

[FFmpeg-devel] avfilter: add morpho filter

Message ID 20210920173359.4111-1-onemda@gmail.com
State New
Headers show
Series [FFmpeg-devel] avfilter: add morpho filter
Related show

Checks

Context Check Description
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished
andriy/makex86 warning New warnings during build

Commit Message

Paul B Mahol Sept. 20, 2021, 5:33 p.m. UTC
Signed-off-by: Paul B Mahol <onemda@gmail.com>
---
 doc/filters.texi         |  32 ++
 libavfilter/Makefile     |   1 +
 libavfilter/allfilters.c |   1 +
 libavfilter/vf_morpho.c  | 865 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 899 insertions(+)
 create mode 100644 libavfilter/vf_morpho.c

Comments

Andreas Rheinhardt Sept. 20, 2021, 5:42 p.m. UTC | #1
Paul B Mahol:
> Signed-off-by: Paul B Mahol <onemda@gmail.com>
> ---
> +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_YUVA422P12, AV_PIX_FMT_YUVA444P12,
> +        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_from_list(ctx, pix_fmts);
> +}
> +

[...]

> +
> +static int config_input_structure(AVFilterLink *inlink)
> +{
> +    AVFilterContext *ctx = inlink->dst;
> +    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
> +    MorphoContext *s = inlink->dst->priv;
> +
> +    if (ctx->inputs[0]->format != ctx->inputs[1]->format) {
> +        av_log(ctx, AV_LOG_ERROR, "Inputs must be of same pixel format.\n");
> +        return AVERROR(EINVAL);
> +    }

Isn't this redundant? After all, query_formats uses
ff_set_common_formats_from_list() which ensures that the formats of the
inputs and the output isn't set independently; instead it is always set
to the same format.

> +
> +    s->splanewidth[1] = s->splanewidth[2] = AV_CEIL_RSHIFT(inlink->w, desc->log2_chroma_w);
> +    s->splanewidth[0] = s->splanewidth[3] = inlink->w;
> +    s->splaneheight[1] = s->splaneheight[2] = AV_CEIL_RSHIFT(inlink->h, desc->log2_chroma_h);
> +    s->splaneheight[0] = s->splaneheight[3] = inlink->h;
> +
> +    return 0;
> +}
> +
Lynne Sept. 20, 2021, 8 p.m. UTC | #2
Sep 20, 2021, 19:33 by onemda@gmail.com:

> Signed-off-by: Paul B Mahol <onemda@gmail.com>
> ---
>  doc/filters.texi         |  32 ++
>  libavfilter/Makefile     |   1 +
>  libavfilter/allfilters.c |   1 +
>  libavfilter/vf_morpho.c  | 865 +++++++++++++++++++++++++++++++++++++++
>  4 files changed, 899 insertions(+)
>  create mode 100644 libavfilter/vf_morpho.c
>
> diff --git a/doc/filters.texi b/doc/filters.texi
> index 0c45fb710d..b46556c12c 100644
> --- a/doc/filters.texi
> +++ b/doc/filters.texi
> @@ -10225,6 +10225,7 @@ A number representing position of the first frame with respect to the telecine
>  pattern. This is to be used if the stream is cut. The default value is @code{0}.
>  @end table
>  
> +@anchor{dilation}
>  @section dilation
>  
>  Apply dilation effect to the video.
> @@ -11541,6 +11542,7 @@ value.
>  
>  @end table
>  
> +@anchor{erosion}
>  @section erosion
>  
>  Apply erosion effect to the video.
> @@ -15316,6 +15318,36 @@ Default value is 0.
>  
>  This filter supports the all above options as @ref{commands}.
>  
> +@section morpho
> +
> +This filter allows to apply main morphological grayscale transforms,
> +erode and dilate with arbitrary structures set in second input stream.
> +
> +Unlike naive implementation and much slower performance in @ref{erosion}
> +and @ref{dilation} filters, when speed is critical @code{morpho} filter
> +should be used instead.
> +
> +A description of accepted options follows,
> +
> +@table @option
> +@item mode
> +Set morphological transform to apply, can be @code{erode} or @code{dilate}.
> +Default is @code{erode}.
> +
> +@item planes
> +Set planes to filter, by default all planes except alpha are filtered.
> +
> +@item structure
> +Set which structure video frames will be processed from second input stream,
> +can be @var{first} or @var{all}. Default is @var{all}.
> +@end table
> +
> +The @code{morpho} filter also supports the @ref{framesync} options.
> +
> +@subsection Commands
> +
> +This filter supports same @ref{commands} as options.
> +
>  @section mpdecimate
>  
>  Drop frames that do not differ greatly from the previous frame in
> diff --git a/libavfilter/Makefile b/libavfilter/Makefile
> index 272f876c07..06e01da38e 100644
> --- a/libavfilter/Makefile
> +++ b/libavfilter/Makefile
> @@ -350,6 +350,7 @@ OBJS-$(CONFIG_MIDEQUALIZER_FILTER)           += vf_midequalizer.o framesync.o
>  OBJS-$(CONFIG_MINTERPOLATE_FILTER)           += vf_minterpolate.o motion_estimation.o
>  OBJS-$(CONFIG_MIX_FILTER)                    += vf_mix.o framesync.o
>  OBJS-$(CONFIG_MONOCHROME_FILTER)             += vf_monochrome.o
> +OBJS-$(CONFIG_MORPHO_FILTER)                 += vf_morpho.o
>  OBJS-$(CONFIG_MPDECIMATE_FILTER)             += vf_mpdecimate.o
>  OBJS-$(CONFIG_NEGATE_FILTER)                 += vf_lut.o
>  OBJS-$(CONFIG_NLMEANS_FILTER)                += vf_nlmeans.o
> diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
> index 1283d124b8..10dfe72131 100644
> --- a/libavfilter/allfilters.c
> +++ b/libavfilter/allfilters.c
> @@ -335,6 +335,7 @@ extern const AVFilter ff_vf_midequalizer;
>  extern const AVFilter ff_vf_minterpolate;
>  extern const AVFilter ff_vf_mix;
>  extern const AVFilter ff_vf_monochrome;
> +extern const AVFilter ff_vf_morpho;
>  extern const AVFilter ff_vf_mpdecimate;
>  extern const AVFilter ff_vf_msad;
>  extern const AVFilter ff_vf_negate;
> diff --git a/libavfilter/vf_morpho.c b/libavfilter/vf_morpho.c
> new file mode 100644
> index 0000000000..46c641c4b9
> --- /dev/null
> +++ b/libavfilter/vf_morpho.c
> @@ -0,0 +1,865 @@
> +/*
> + * Copyright (c) 2016 ReneBrals
> + * Copyright (c) 2021 Paul B Mahol
> + *
> + * This file is part of FFmpeg.
> + *
> + * Permission is hereby granted, free of charge, to any person obtaining a copy
> + * of this software and associated documentation files (the "Software"), to deal
> + * in the Software without restriction, including without limitation the rights
> + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> + * copies of the Software, and to permit persons to whom the Software is
> + * furnished to do so, subject to the following conditions:
> + *
> + * The above copyright notice and this permission notice shall be included in all
> + * copies or substantial portions of the Software.
> + *
> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> + * SOFTWARE.
> + */
> +
> +#include "avfilter.h"
> +#include "formats.h"
> +#include "internal.h"
> +#include "libavutil/intreadwrite.h"
> +#include "libavutil/imgutils.h"
> +#include "libavutil/opt.h"
> +#include "libavutil/pixdesc.h"
> +#include "video.h"
> +#include "framesync.h"
> +
> +typedef struct IPlane {
> +    uint8_t **img;
> +    size_t W;
> +    size_t H;
> +    size_t range;
> +    int type_size;
> +
> +    void (*max)(uint8_t *c, uint8_t *a, uint8_t *b, size_t x);
> +    void (*min)(uint8_t *c, uint8_t *a, uint8_t *b, size_t x);
> +    void (*max_in_place)(uint8_t *a, uint8_t *b, size_t x);
> +    void (*min_in_place)(uint8_t *a, uint8_t *b, size_t x);
> +} IPlane;
> +
> +typedef struct LUT {
> +    uint8_t ***arr;
> +    int minR;
> +    int maxR;
> +    size_t I;
> +    size_t X;
> +    int padX;
> +} LUT;
> +
> +typedef struct chord {
> +    int x;
> +    int y;
> +    int l;
> +    int i;
> +} chord;
> +
> +typedef struct chordSet {
> +    chord *C;
> +    size_t size;
> +    size_t cap;
> +
> +    int *R;
> +    size_t Lnum;
> +
> +    int minX;
> +    int maxX;
> +    int minY;
> +    int maxY;
> +} chordSet;
> +
> +typedef struct MorphoContext {
> +    const AVClass *class;
> +    FFFrameSync fs;
> +
> +    chordSet SE[4];
> +    IPlane SEimg[4];
> +    IPlane g[4], f[4];
> +    LUT Ty[4];
> +
> +    int mode;
> +    int planes;
> +    int structures;
> +
> +    int planewidth[4];
> +    int planeheight[4];
> +    int splanewidth[4];
> +    int splaneheight[4];
> +    int depth;
> +    int type_size;
> +    int nb_planes;
> +
> +    int got_structure[4];
> +} MorphoContext;
> +
> +#define OFFSET(x) offsetof(MorphoContext, x)
> +#define FLAGS AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_RUNTIME_PARAM
> +
> +static const AVOption morpho_options[] = {
> +    { "mode",  "set morphological transform",                 OFFSET(mode),       AV_OPT_TYPE_INT,   {.i64=0}, 0,  1, FLAGS, "mode" },
> +    { "erode",  NULL,                                         0,                  AV_OPT_TYPE_CONST, {.i64=0}, 0,  0, FLAGS, "mode" },
> +    { "dilate", NULL,                                         0,                  AV_OPT_TYPE_CONST, {.i64=1}, 0,  0, FLAGS, "mode" },
> +    { "planes",  "set planes to filter",                      OFFSET(planes),     AV_OPT_TYPE_INT,   {.i64=7}, 0, 15, FLAGS },
> +    { "structure", "when to process structures",              OFFSET(structures), AV_OPT_TYPE_INT,   {.i64=1}, 0,  1, FLAGS, "str" },
> +    {   "first", "process only first structure, ignore rest", 0,                  AV_OPT_TYPE_CONST, {.i64=0}, 0,  0, FLAGS, "str" },
> +    {   "all",   "process all structure",                     0,                  AV_OPT_TYPE_CONST, {.i64=1}, 0,  0, FLAGS, "str" },
> +    { NULL }
> +};
> +
> +FRAMESYNC_DEFINE_CLASS(morpho, MorphoContext, fs);
> +
> +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_YUVA422P12, AV_PIX_FMT_YUVA444P12,
> +        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_from_list(ctx, pix_fmts);
> +}
> +
> +static void Min(uint8_t *c, uint8_t *a, uint8_t *b, size_t x)
> +{
> +    for (size_t i = 0; i < x; i++)
> +        c[i] = FFMIN(b[i], a[i]);
> +}
> +
> +static void MinInPlace(uint8_t *a, uint8_t *b, size_t x)
> +{
> +    for (size_t i = 0; i < x; i++)
> +        a[i] = FFMIN(a[i], b[i]);
> +}
> +
> +static void Max(uint8_t *c, uint8_t *a, uint8_t *b, size_t x)
> +{
> +    for (size_t i = 0; i < x; i++)
> +        c[i] = FFMAX(a[i], b[i]);
> +}
> +
> +static void MaxInPlace(uint8_t *a, uint8_t *b, size_t x)
> +{
> +    for (size_t i = 0; i < x; i++)
> +        a[i] = FFMAX(a[i], b[i]);
> +}
> +
> +static void Min16(uint8_t *cc, uint8_t *aa, uint8_t *bb, size_t x)
> +{
> +    uint16_t *a = (uint16_t *)aa;
> +    uint16_t *b = (uint16_t *)bb;
> +    uint16_t *c = (uint16_t *)cc;
> +
> +    for (size_t i = 0; i < x; i++)
> +        c[i] = FFMIN(b[i], a[i]);
> +}
> +
> +static void MinInPlace16(uint8_t *aa, uint8_t *bb, size_t x)
> +{
> +    uint16_t *a = (uint16_t *)aa;
> +    uint16_t *b = (uint16_t *)bb;
> +
> +    for (size_t i = 0; i < x; i++)
> +        a[i] = FFMIN(a[i], b[i]);
> +}
> +
> +static void Max16(uint8_t *cc, uint8_t *aa, uint8_t *bb, size_t x)
> +{
> +    uint16_t *a = (uint16_t *)aa;
> +    uint16_t *b = (uint16_t *)bb;
> +    uint16_t *c = (uint16_t *)cc;
> +
> +    for (size_t i = 0; i < x; i++)
> +        c[i] = FFMAX(a[i], b[i]);
> +}
> +
> +static void MaxInPlace16(uint8_t *aa, uint8_t *bb, size_t x)
> +{
> +    uint16_t *a = (uint16_t *)aa;
> +    uint16_t *b = (uint16_t *)bb;
> +
> +    for (size_t i = 0; i < x; i++)
> +        a[i] = FFMAX(a[i], b[i]);
> +}
>

Remove the pointless wrapper functions and remove the CamelCase
naming convention.
diff mbox series

Patch

diff --git a/doc/filters.texi b/doc/filters.texi
index 0c45fb710d..b46556c12c 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -10225,6 +10225,7 @@  A number representing position of the first frame with respect to the telecine
 pattern. This is to be used if the stream is cut. The default value is @code{0}.
 @end table
 
+@anchor{dilation}
 @section dilation
 
 Apply dilation effect to the video.
@@ -11541,6 +11542,7 @@  value.
 
 @end table
 
+@anchor{erosion}
 @section erosion
 
 Apply erosion effect to the video.
@@ -15316,6 +15318,36 @@  Default value is 0.
 
 This filter supports the all above options as @ref{commands}.
 
+@section morpho
+
+This filter allows to apply main morphological grayscale transforms,
+erode and dilate with arbitrary structures set in second input stream.
+
+Unlike naive implementation and much slower performance in @ref{erosion}
+and @ref{dilation} filters, when speed is critical @code{morpho} filter
+should be used instead.
+
+A description of accepted options follows,
+
+@table @option
+@item mode
+Set morphological transform to apply, can be @code{erode} or @code{dilate}.
+Default is @code{erode}.
+
+@item planes
+Set planes to filter, by default all planes except alpha are filtered.
+
+@item structure
+Set which structure video frames will be processed from second input stream,
+can be @var{first} or @var{all}. Default is @var{all}.
+@end table
+
+The @code{morpho} filter also supports the @ref{framesync} options.
+
+@subsection Commands
+
+This filter supports same @ref{commands} as options.
+
 @section mpdecimate
 
 Drop frames that do not differ greatly from the previous frame in
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index 272f876c07..06e01da38e 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -350,6 +350,7 @@  OBJS-$(CONFIG_MIDEQUALIZER_FILTER)           += vf_midequalizer.o framesync.o
 OBJS-$(CONFIG_MINTERPOLATE_FILTER)           += vf_minterpolate.o motion_estimation.o
 OBJS-$(CONFIG_MIX_FILTER)                    += vf_mix.o framesync.o
 OBJS-$(CONFIG_MONOCHROME_FILTER)             += vf_monochrome.o
+OBJS-$(CONFIG_MORPHO_FILTER)                 += vf_morpho.o
 OBJS-$(CONFIG_MPDECIMATE_FILTER)             += vf_mpdecimate.o
 OBJS-$(CONFIG_NEGATE_FILTER)                 += vf_lut.o
 OBJS-$(CONFIG_NLMEANS_FILTER)                += vf_nlmeans.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 1283d124b8..10dfe72131 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -335,6 +335,7 @@  extern const AVFilter ff_vf_midequalizer;
 extern const AVFilter ff_vf_minterpolate;
 extern const AVFilter ff_vf_mix;
 extern const AVFilter ff_vf_monochrome;
+extern const AVFilter ff_vf_morpho;
 extern const AVFilter ff_vf_mpdecimate;
 extern const AVFilter ff_vf_msad;
 extern const AVFilter ff_vf_negate;
diff --git a/libavfilter/vf_morpho.c b/libavfilter/vf_morpho.c
new file mode 100644
index 0000000000..46c641c4b9
--- /dev/null
+++ b/libavfilter/vf_morpho.c
@@ -0,0 +1,865 @@ 
+/*
+ * Copyright (c) 2016 ReneBrals
+ * Copyright (c) 2021 Paul B Mahol
+ *
+ * This file is part of FFmpeg.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "avfilter.h"
+#include "formats.h"
+#include "internal.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/imgutils.h"
+#include "libavutil/opt.h"
+#include "libavutil/pixdesc.h"
+#include "video.h"
+#include "framesync.h"
+
+typedef struct IPlane {
+    uint8_t **img;
+    size_t W;
+    size_t H;
+    size_t range;
+    int type_size;
+
+    void (*max)(uint8_t *c, uint8_t *a, uint8_t *b, size_t x);
+    void (*min)(uint8_t *c, uint8_t *a, uint8_t *b, size_t x);
+    void (*max_in_place)(uint8_t *a, uint8_t *b, size_t x);
+    void (*min_in_place)(uint8_t *a, uint8_t *b, size_t x);
+} IPlane;
+
+typedef struct LUT {
+    uint8_t ***arr;
+    int minR;
+    int maxR;
+    size_t I;
+    size_t X;
+    int padX;
+} LUT;
+
+typedef struct chord {
+    int x;
+    int y;
+    int l;
+    int i;
+} chord;
+
+typedef struct chordSet {
+    chord *C;
+    size_t size;
+    size_t cap;
+
+    int *R;
+    size_t Lnum;
+
+    int minX;
+    int maxX;
+    int minY;
+    int maxY;
+} chordSet;
+
+typedef struct MorphoContext {
+    const AVClass *class;
+    FFFrameSync fs;
+
+    chordSet SE[4];
+    IPlane SEimg[4];
+    IPlane g[4], f[4];
+    LUT Ty[4];
+
+    int mode;
+    int planes;
+    int structures;
+
+    int planewidth[4];
+    int planeheight[4];
+    int splanewidth[4];
+    int splaneheight[4];
+    int depth;
+    int type_size;
+    int nb_planes;
+
+    int got_structure[4];
+} MorphoContext;
+
+#define OFFSET(x) offsetof(MorphoContext, x)
+#define FLAGS AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_RUNTIME_PARAM
+
+static const AVOption morpho_options[] = {
+    { "mode",  "set morphological transform",                 OFFSET(mode),       AV_OPT_TYPE_INT,   {.i64=0}, 0,  1, FLAGS, "mode" },
+    { "erode",  NULL,                                         0,                  AV_OPT_TYPE_CONST, {.i64=0}, 0,  0, FLAGS, "mode" },
+    { "dilate", NULL,                                         0,                  AV_OPT_TYPE_CONST, {.i64=1}, 0,  0, FLAGS, "mode" },
+    { "planes",  "set planes to filter",                      OFFSET(planes),     AV_OPT_TYPE_INT,   {.i64=7}, 0, 15, FLAGS },
+    { "structure", "when to process structures",              OFFSET(structures), AV_OPT_TYPE_INT,   {.i64=1}, 0,  1, FLAGS, "str" },
+    {   "first", "process only first structure, ignore rest", 0,                  AV_OPT_TYPE_CONST, {.i64=0}, 0,  0, FLAGS, "str" },
+    {   "all",   "process all structure",                     0,                  AV_OPT_TYPE_CONST, {.i64=1}, 0,  0, FLAGS, "str" },
+    { NULL }
+};
+
+FRAMESYNC_DEFINE_CLASS(morpho, MorphoContext, fs);
+
+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_YUVA422P12, AV_PIX_FMT_YUVA444P12,
+        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_from_list(ctx, pix_fmts);
+}
+
+static void Min(uint8_t *c, uint8_t *a, uint8_t *b, size_t x)
+{
+    for (size_t i = 0; i < x; i++)
+        c[i] = FFMIN(b[i], a[i]);
+}
+
+static void MinInPlace(uint8_t *a, uint8_t *b, size_t x)
+{
+    for (size_t i = 0; i < x; i++)
+        a[i] = FFMIN(a[i], b[i]);
+}
+
+static void Max(uint8_t *c, uint8_t *a, uint8_t *b, size_t x)
+{
+    for (size_t i = 0; i < x; i++)
+        c[i] = FFMAX(a[i], b[i]);
+}
+
+static void MaxInPlace(uint8_t *a, uint8_t *b, size_t x)
+{
+    for (size_t i = 0; i < x; i++)
+        a[i] = FFMAX(a[i], b[i]);
+}
+
+static void Min16(uint8_t *cc, uint8_t *aa, uint8_t *bb, size_t x)
+{
+    uint16_t *a = (uint16_t *)aa;
+    uint16_t *b = (uint16_t *)bb;
+    uint16_t *c = (uint16_t *)cc;
+
+    for (size_t i = 0; i < x; i++)
+        c[i] = FFMIN(b[i], a[i]);
+}
+
+static void MinInPlace16(uint8_t *aa, uint8_t *bb, size_t x)
+{
+    uint16_t *a = (uint16_t *)aa;
+    uint16_t *b = (uint16_t *)bb;
+
+    for (size_t i = 0; i < x; i++)
+        a[i] = FFMIN(a[i], b[i]);
+}
+
+static void Max16(uint8_t *cc, uint8_t *aa, uint8_t *bb, size_t x)
+{
+    uint16_t *a = (uint16_t *)aa;
+    uint16_t *b = (uint16_t *)bb;
+    uint16_t *c = (uint16_t *)cc;
+
+    for (size_t i = 0; i < x; i++)
+        c[i] = FFMAX(a[i], b[i]);
+}
+
+static void MaxInPlace16(uint8_t *aa, uint8_t *bb, size_t x)
+{
+    uint16_t *a = (uint16_t *)aa;
+    uint16_t *b = (uint16_t *)bb;
+
+    for (size_t i = 0; i < x; i++)
+        a[i] = FFMAX(a[i], b[i]);
+}
+
+static int allocateLUT(LUT *Ty, chordSet SE, int type_size)
+{
+    size_t prePadX = 0;
+
+    /*
+     * The LUT is pre-padded on X. This is needed for dilation, where the
+     * padding might contain valid information.
+     */
+    if (SE.minX < 0)
+        prePadX = 0 - SE.minX;
+    Ty->padX = prePadX;
+
+    Ty->arr = av_calloc((Ty->maxR - Ty->minR + 1), sizeof(uint8_t **));
+    if (!Ty->arr)
+        return AVERROR(ENOMEM);
+    for (int r = 0; r < (Ty->maxR - Ty->minR + 1); r++) {
+        Ty->arr[r] = av_calloc(Ty->I, sizeof(uint8_t *));
+        if (!Ty->arr[r])
+            return AVERROR(ENOMEM);
+        for (int i = 0; i < Ty->I; i++) {
+            Ty->arr[r][i] = av_calloc(Ty->X + prePadX, type_size);
+            if (!Ty->arr[r][i])
+                return AVERROR(ENOMEM);
+            /* Shifting the X index such that negative indices correspond to
+             * the pre-padding.
+             */
+            Ty->arr[r][i] = &(Ty->arr[r][i][prePadX * type_size]);
+        }
+    }
+
+    Ty->arr = &(Ty->arr[0 - Ty->minR]);
+
+    return 0;
+}
+
+static void freeLUT(LUT *table, int type_size)
+{
+    uint8_t ***rp;
+
+    if (!table->arr)
+        return;
+
+    // The R index was shifted, create a pointer to the original array
+    rp = &(table->arr[table->minR]);
+
+    for (int r = table->minR; r <= table->maxR; r++) {
+        for (int i = 0; i < table->I; i++) {
+            // The X index was also shifted, for padding purposes.
+            av_free(table->arr[r][i] - table->padX * type_size);
+        }
+        av_freep(&table->arr[r]);
+    }
+    av_freep(&rp);
+}
+
+static void circularSwapPointers(LUT Ty)
+{
+    uint8_t **Ty0;
+    int r;
+
+    /*
+     * Swap the pointers to r-indices in a circle. This is useful because
+     * Ty(r,i,x) = Ty-1(r+1,i,x) for r < ymax.
+     */
+    if (Ty.maxR - Ty.minR > 0) {
+        Ty0 = Ty.arr[Ty.minR];
+
+        for (r = Ty.minR; r < Ty.maxR; r++) {
+            Ty.arr[r] = Ty.arr[r + 1];
+        }
+
+        r = Ty.maxR;
+        Ty.arr[r] = Ty0;
+    }
+}
+
+static void computeMinRow(IPlane f, LUT *Ty, chordSet SE, int r, size_t y)
+{
+    if (y + r > 0 && y + r < f.H) {
+        memcpy(Ty->arr[r][0], f.img[y + r], Ty->X * f.type_size);
+    } else {
+        memset(Ty->arr[r][0], UINT8_MAX, Ty->X * f.type_size);
+    }
+
+    for (int i = 1; i < SE.Lnum; i++) {
+        int d = SE.R[i] - SE.R[i - 1];
+        f.min(Ty->arr[r][i],
+            Ty->arr[r][i - 1],
+            Ty->arr[r][i - 1] + d * f.type_size,
+            FFMAX((int)Ty->X - (int)d, 0));
+    }
+}
+
+static void updateMinLUT(IPlane f, LUT *Ty, chordSet SE, size_t y, size_t tid, size_t num)
+{
+    for (size_t i = 0; i < num; i++)
+        circularSwapPointers(*Ty);
+
+    computeMinRow(f, Ty, SE, Ty->maxR - tid, y);
+}
+
+static int computeMinLUT(LUT *Ty, IPlane f, chordSet SE, size_t y, size_t num)
+{
+    if (Ty->I != SE.Lnum ||
+        Ty->X != f.W ||
+        Ty->minR != SE.minY ||
+        Ty->maxR != SE.maxY + num - 1) {
+        int ret;
+
+        freeLUT(Ty, f.type_size);
+
+        Ty->I = SE.Lnum;
+        Ty->X = f.W;
+        Ty->minR = SE.minY;
+        Ty->maxR = SE.maxY + num - 1;
+        ret = allocateLUT(Ty, SE, f.type_size);
+        if (ret < 0)
+            return ret;
+    }
+
+    for (int r = Ty->minR; r <= Ty->maxR; r++)
+        computeMinRow(f, Ty, SE, r, y);
+
+    return 0;
+}
+
+static void computeMaxRow(IPlane f, LUT *Ty, chordSet SE, int r, size_t y)
+{
+    if (y + r > 0 && y + r < f.H) {
+        memcpy(Ty->arr[r][0], f.img[y + r], Ty->X * f.type_size);
+    } else {
+        memset(Ty->arr[r][0], 0, Ty->X * f.type_size);
+    }
+
+    for (int i = 1; i < SE.Lnum; i++) {
+        int d = SE.R[i] - SE.R[i - 1];
+
+        f.max(Ty->arr[r][i] - Ty->padX * f.type_size,
+            Ty->arr[r][i - 1] - Ty->padX * f.type_size,
+            Ty->arr[r][i - 1] + (d - Ty->padX) * f.type_size,
+            Ty->X + Ty->padX - d);
+        memcpy(Ty->arr[r][i] + (Ty->X - d) * f.type_size,
+               Ty->arr[r][i - 1] + (Ty->X - d) * f.type_size,
+               d * f.type_size);
+    }
+}
+
+static void updateMaxLUT(IPlane f, LUT *Ty, chordSet SE, size_t y, size_t tid, size_t num)
+{
+    for (size_t i = 0; i < num; i++)
+        circularSwapPointers(*Ty);
+
+    computeMaxRow(f, Ty, SE, Ty->maxR - tid, y);
+}
+
+static int computeMaxLUT(LUT *Ty, IPlane f, chordSet SE, size_t y, size_t num)
+{
+    if (Ty->I != SE.Lnum ||
+        Ty->X != f.W ||
+        Ty->minR != SE.minY ||
+        Ty->maxR != SE.maxY + num - 1) {
+        int ret;
+
+        freeLUT(Ty, f.type_size);
+
+        Ty->I = SE.Lnum;
+        Ty->X = f.W;
+        Ty->minR = SE.minY;
+        Ty->maxR = SE.maxY + num - 1;
+        ret = allocateLUT(Ty, SE, f.type_size);
+        if (ret < 0)
+            return ret;
+    }
+
+    for (int r = Ty->minR; r <= Ty->maxR; r++)
+        computeMaxRow(f, Ty, SE, r, y);
+
+    return 0;
+}
+
+static void lineDilate(IPlane *g, LUT *Ty, chordSet SE, int y, size_t tid)
+{
+    memset(g->img[y], 0, g->W * g->type_size);
+
+    for (size_t c = 0; c < SE.size; c++) {
+        g->max_in_place(g->img[y],
+            Ty->arr[SE.C[c].y + tid][SE.C[c].i] + SE.C[c].x * g->type_size,
+            FFMIN(FFMAX((int)g->W - SE.C[c].x, 0), g->W));
+    }
+}
+
+static void lineErode(IPlane *g, LUT *Ty, chordSet SE, int y, size_t tid)
+{
+    memset(g->img[y], UINT8_MAX, g->W * g->type_size);
+
+    for (size_t c = 0; c < SE.size; c++) {
+        g->min_in_place(g->img[y],
+            Ty->arr[SE.C[c].y + tid][SE.C[c].i] + SE.C[c].x * g->type_size,
+            FFMIN(FFMAX((int)g->W - SE.C[c].x, 0), g->W));
+    }
+}
+
+static int dilate(IPlane *g, IPlane f, chordSet SE, LUT *Ty)
+{
+    int ret = computeMaxLUT(Ty, f, SE, 0, 1);
+    if (ret < 0)
+        return ret;
+
+    lineDilate(g, Ty, SE, 0, 0);
+    for (size_t y = 1; y < f.H; y++) {
+        updateMaxLUT(f, Ty, SE, y, 0, 1);
+        lineDilate(g, Ty, SE, y, 0);
+    }
+
+    return 0;
+}
+
+static int erode(IPlane *g, IPlane f, chordSet SE, LUT *Ty)
+{
+    int ret = computeMinLUT(Ty, f, SE, 0, 1);
+    if (ret < 0)
+        return ret;
+
+    lineErode(g, Ty, SE, 0, 0);
+    for (size_t y = 1; y < f.H; y++) {
+        updateMinLUT(f, Ty, SE, y, 0, 1);
+        lineErode(g, Ty, SE, y, 0);
+    }
+
+    return 0;
+}
+
+static void insertChordSet(chordSet *chords, chord c)
+{
+    // Checking if chord fits in dynamic array, resize if not.
+    if (chords->size == chords->cap) {
+        chords->C = av_realloc_f(chords->C, chords->cap * 2, sizeof(chord));
+        chords->cap *= 2;
+    }
+
+    // Add the chord to the dynamic array.
+    chords->C[chords->size].x = c.x;
+    chords->C[chords->size].y = c.y;
+    chords->C[chords->size++].l = c.l;
+
+    // Update minimum/maximum x/y offsets of the chord set.
+    if (c.x < chords->minX)
+        chords->minX = c.x;
+    if (c.x > chords->maxX)
+        chords->maxX = c.x;
+
+    if (c.y < chords->minY)
+        chords->minY = c.y;
+    if (c.y > chords->maxY)
+        chords->maxY = c.y;
+}
+
+static void freeChordSet(chordSet *SE)
+{
+    av_freep(&SE->C);
+    SE->size = 0;
+    SE->cap = 0;
+
+    av_freep(&SE->R);
+    SE->Lnum = 0;
+}
+
+static int init_chordset(chordSet *chords)
+{
+    chords->size = 0;
+    chords->C = av_calloc(1, sizeof(chord));
+    if (!chords->C)
+        return AVERROR(ENOMEM);
+
+    chords->cap = 1;
+    chords->minX = INT_MAX;
+    chords->maxX = INT_MIN;
+    chords->minY = INT_MAX;
+    chords->maxY = INT_MIN;
+
+    return 0;
+}
+
+static int compChordLength(const void *p, const void *q)
+{
+    chord a, b;
+    a = *((chord *)p);
+    b = *((chord *)q);
+
+    return (a.l > b.l) - (a.l < b.l);
+}
+
+static int compChordY(const void *p, const void *q)
+{
+    chord a, b;
+    a = *((chord *)p);
+    b = *((chord *)q);
+
+    return (a.y > b.y) - (a.y < b.y);
+}
+
+static int buildChordSet(IPlane SE, chordSet *chords)
+{
+    size_t i, chordLengthIndex, x, y;
+    int chordStart, val, ret;
+    chord c;
+
+    ret = init_chordset(chords);
+    if (ret < 0)
+        return ret;
+    /*
+     * In erosion/dilation, the center of the IPlane has S.E. offset (0,0).
+     * Otherwise, the resulting IPlane would be shifted to the top-left.
+     */
+    int centerX = (SE.W - 1) / 2;
+    int centerY = (SE.H - 1) / 2;
+
+    /*
+     * Computing the set of chords C.
+     */
+    for (y = 0; y < SE.H; y++) {
+        chordStart = -1;
+        for (x = 0; x < SE.W; x++) {
+            if (SE.type_size == 1) {
+                //A chord is a run of non-zero pixels.
+                if (SE.img[y][x] != 0 && chordStart == -1) {
+                    // Chord starts.
+                    chordStart = x;
+                } else if (SE.img[y][x] == 0 && chordStart != -1) {
+                    // Chord ends before end of line.
+                    c.x = chordStart - centerX;
+                    c.y = y - centerY;
+                    c.l = x - chordStart;
+                    insertChordSet(chords, c);
+                    chordStart = -1;
+                }
+            } else {
+                //A chord is a run of non-zero pixels.
+                if (AV_RN16(&SE.img[y][x * 2]) != 0 && chordStart == -1) {
+                    // Chord starts.
+                    chordStart = x;
+                } else if (AV_RN16(&SE.img[y][x + 2]) == 0 && chordStart != -1) {
+                    // Chord ends before end of line.
+                    c.x = chordStart - centerX;
+                    c.y = y - centerY;
+                    c.l = x - chordStart;
+                    insertChordSet(chords, c);
+                    chordStart = -1;
+                }
+            }
+        }
+        if (chordStart != -1) {
+            // Chord ends at end of line.
+            c.x = chordStart - centerX;
+            c.y = y - centerY;
+            c.l = x - chordStart;
+            insertChordSet(chords, c);
+        }
+    }
+
+    /*
+     * Computing the array of chord lengths R(i).
+     * This is needed because the lookup table will contain a row for each
+     * length index i.
+     */
+    qsort(chords->C, chords->size, sizeof(chord), compChordLength);
+    chords->R = av_calloc(1, sizeof(int));
+    chords->Lnum = 0;
+    val = 0;
+    size_t rCap = 1;
+
+    if (chords->size > 0) {
+        val = 1;
+        if (chords->Lnum >= rCap) {
+            chords->R = av_realloc_f(chords->R, rCap * 2, sizeof(int));
+            rCap *= 2;
+        }
+        chords->R[chords->Lnum++] = 1;
+        val = 1;
+    }
+
+    for (i = 0; i < chords->size; i++) {
+        if (val != chords->C[i].l) {
+            while (2 * val < chords->C[i].l && val != 0) {
+
+                if (chords->Lnum >= rCap) {
+                    chords->R = av_realloc_f(chords->R, rCap * 2, sizeof(int));
+                    rCap *= 2;
+                }
+                chords->R[chords->Lnum++] = 2 * val;
+                val *= 2;
+            }
+            val = chords->C[i].l;
+
+            if (chords->Lnum >= rCap) {
+                chords->R = av_realloc_f(chords->R, rCap * 2, sizeof(int));
+                rCap *= 2;
+            }
+            chords->R[chords->Lnum++] = val;
+        }
+    }
+
+    /*
+     * Setting the length indices of chords.
+     * These are needed so that the algorithm can, for each chord,
+     * access the lookup table at the correct length in constant time.
+     */
+    chordLengthIndex = 0;
+    for (i = 0; i < chords->size; i++) {
+        while (chords->R[chordLengthIndex] < chords->C[i].l) {
+            chordLengthIndex++;
+        }
+        chords->C[i].i = chordLengthIndex;
+    }
+
+    /*
+     * Chords are sorted on Y. This way, when a row of the lookup table or IPlane
+     * is cached, the next chord offset has a better chance of being on the
+     * same cache line.
+     */
+    qsort(chords->C, chords->size, sizeof(chord), compChordY);
+
+    return 0;
+}
+
+static void freeIPlane(IPlane *imp)
+{
+    av_freep(&imp->img);
+}
+
+static int read_iplane(IPlane *imp, const uint8_t *dst, int dst_linesize,
+                       int W, int H, int R, size_t type_size)
+{
+    if (!imp->img)
+        imp->img = av_calloc(H, sizeof(*imp->img));
+    if (!imp->img)
+        return AVERROR(ENOMEM);
+
+    imp->W = W;
+    imp->H = H;
+    imp->range = R;
+    imp->type_size = type_size;
+    imp->max = type_size == 1 ? Max : Max16;
+    imp->min = type_size == 1 ? Min : Min16;
+    imp->max_in_place = type_size == 1 ? MaxInPlace : MaxInPlace16;
+    imp->min_in_place = type_size == 1 ? MinInPlace : MinInPlace16;
+
+    for (int y = 0; y < H; y++)
+        imp->img[y] = (uint8_t *)(dst + y * dst_linesize);
+
+    return 0;
+}
+
+static int config_input(AVFilterLink *inlink)
+{
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
+    MorphoContext *s = inlink->dst->priv;
+
+    s->nb_planes = desc->nb_components;
+    s->depth = desc->comp[0].depth;
+    s->type_size = (s->depth + 7) / 8;
+    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;
+
+    switch (s->depth) {
+    case 8:
+        break;
+    }
+
+    return 0;
+}
+
+static int config_input_structure(AVFilterLink *inlink)
+{
+    AVFilterContext *ctx = inlink->dst;
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
+    MorphoContext *s = inlink->dst->priv;
+
+    if (ctx->inputs[0]->format != ctx->inputs[1]->format) {
+        av_log(ctx, AV_LOG_ERROR, "Inputs must be of same pixel format.\n");
+        return AVERROR(EINVAL);
+    }
+
+    s->splanewidth[1] = s->splanewidth[2] = AV_CEIL_RSHIFT(inlink->w, desc->log2_chroma_w);
+    s->splanewidth[0] = s->splanewidth[3] = inlink->w;
+    s->splaneheight[1] = s->splaneheight[2] = AV_CEIL_RSHIFT(inlink->h, desc->log2_chroma_h);
+    s->splaneheight[0] = s->splaneheight[3] = inlink->h;
+
+    return 0;
+}
+
+typedef struct ThreadData {
+    AVFrame *in, *out;
+} ThreadData;
+
+static int activate(AVFilterContext *ctx)
+{
+    MorphoContext *s = ctx->priv;
+    return ff_framesync_activate(&s->fs);
+}
+
+static int do_morpho(FFFrameSync *fs)
+{
+    AVFilterContext *ctx = fs->parent;
+    AVFilterLink *outlink = ctx->outputs[0];
+    MorphoContext *s = ctx->priv;
+    AVFrame *in = NULL, *structurepic = NULL;
+    AVFrame *out;
+    int ret;
+
+    ret = ff_framesync_dualinput_get(fs, &in, &structurepic);
+    if (ret < 0)
+        return ret;
+    if (!structurepic)
+        return ff_filter_frame(outlink, in);
+
+    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);
+
+    for (int p = 0; p < s->nb_planes; p++) {
+        const uint8_t *src = in->data[p];
+        const int src_linesize = in->linesize[p];
+        const uint8_t *ssrc = structurepic->data[p];
+        const int ssrc_linesize = structurepic->linesize[p];
+        uint8_t *ddst = out->data[p];
+        const int dst_linesize = out->linesize[p];
+        const int swidth = s->splanewidth[p];
+        const int sheight = s->splaneheight[p];
+        const int width = s->planewidth[p];
+        const int height = s->planeheight[p];
+        const int type_size = s->type_size;
+
+        if (!(s->planes & (1 << p))) {
+            av_image_copy_plane(out->data[p] + 0 * out->linesize[p],
+                out->linesize[p],
+                in->data[p] + 0 * in->linesize[p],
+                in->linesize[p],
+                width * ((s->depth + 7) / 8),
+                height);
+            continue;
+        }
+
+        if (!s->got_structure[p] || s->structures) {
+            ret = read_iplane(&s->SEimg[p], ssrc, ssrc_linesize, swidth, sheight, 1, type_size);
+            if (ret < 0)
+                goto fail;
+            ret = buildChordSet(s->SEimg[p], &s->SE[p]);
+            if (ret < 0)
+                goto fail;
+            s->got_structure[p] = 1;
+        }
+
+        ret = read_iplane(&s->f[p], src, src_linesize, width, height, 1, type_size);
+        if (ret < 0)
+            goto fail;
+
+        ret = read_iplane(&s->g[p], ddst, dst_linesize, s->f[p].W, s->f[p].H, s->f[p].range, type_size);
+        if (ret < 0)
+            goto fail;
+
+        if (s->mode)
+            ret = dilate(&s->g[p], s->f[p], s->SE[p], &s->Ty[p]);
+        else
+            ret = erode(&s->g[p], s->f[p], s->SE[p], &s->Ty[p]);
+        if (ret < 0)
+            goto fail;
+
+        if (s->structures)
+            freeChordSet(&s->SE[p]);
+    }
+
+    av_frame_free(&in);
+    out->pts = av_rescale_q(s->fs.pts, s->fs.time_base, outlink->time_base);
+    return ff_filter_frame(outlink, out);
+fail:
+    av_frame_free(&in);
+    return ret;
+}
+
+static int config_output(AVFilterLink *outlink)
+{
+    AVFilterContext *ctx = outlink->src;
+    MorphoContext *s = ctx->priv;
+    AVFilterLink *mainlink = ctx->inputs[0];
+    int ret;
+
+    s->fs.on_event = do_morpho;
+    ret = ff_framesync_init_dualinput(&s->fs, ctx);
+    if (ret < 0)
+        return ret;
+    outlink->w = mainlink->w;
+    outlink->h = mainlink->h;
+    outlink->time_base = mainlink->time_base;
+    outlink->sample_aspect_ratio = mainlink->sample_aspect_ratio;
+    outlink->frame_rate = mainlink->frame_rate;
+
+    if ((ret = ff_framesync_configure(&s->fs)) < 0)
+        return ret;
+    outlink->time_base = s->fs.time_base;
+
+    return 0;
+}
+static av_cold void uninit(AVFilterContext *ctx)
+{
+    MorphoContext *s = ctx->priv;
+
+    for (int p = 0; p < 4; p++) {
+        freeIPlane(&s->SEimg[p]);
+        freeIPlane(&s->g[p]);
+        freeIPlane(&s->f[p]);
+        freeChordSet(&s->SE[p]);
+        freeLUT(&s->Ty[p], s->type_size);
+    }
+
+    ff_framesync_uninit(&s->fs);
+}
+
+static const AVFilterPad morpho_inputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = config_input,
+    },
+    {
+        .name         = "structure",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = config_input_structure,
+    },
+};
+
+static const AVFilterPad morpho_outputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = config_output,
+    },
+};
+
+const AVFilter ff_vf_morpho = {
+    .name            = "morpho",
+    .description     = NULL_IF_CONFIG_SMALL("Apply Morphological filter."),
+    .preinit         = morpho_framesync_preinit,
+    .priv_size       = sizeof(MorphoContext),
+    .priv_class      = &morpho_class,
+    .activate        = activate,
+    .uninit          = uninit,
+    .query_formats   = query_formats,
+    FILTER_INPUTS(morpho_inputs),
+    FILTER_OUTPUTS(morpho_outputs),
+    .flags           = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC,
+    .process_command = ff_filter_process_command,
+};