PR #25748 opened by Corneliu68
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25748
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25748.patch

This adds `hbnlmeans`, a wrapper for libhbnlmeans
(https://github.com/WebcasterLTD/hbnlmeans, release v1.0.0), which packages 
HandBrake's NLMeans
denoiser as a standalone library, the way zscale uses zimg and vidstab uses 
vid.stab.

Why a second NLMeans filter: the existing `nlmeans` is spatial only. HandBrake's
version also searches the frames after the current one (1 to 32), weights each
pixel's own patch separately (origin-tune), and has prefilters, presets and 
tunes
that a lot of encodes are made with. With this filter, a HandBrake encode's 
denoise
settings can be reproduced in FFmpeg exactly, from the same option string:

    ffmpeg -i in.mkv -vf hbnlmeans=preset=medium:tune=film out.mkv
    ffmpeg -i in.mkv -vf hbnlmeans=y-strength=6:y-origin-tune=0.8:cb-strength=8 
out.mkv

The output is byte for byte identical to HandBrake's own code for every preset 
and
tune, at 8 and 10 bit and at odd frame sizes. The library's
`test/compare-handbrake.run` builds HandBrake's unmodified nlmeans sources and
compares them with `ffmpeg -vf hbnlmeans`.

Licensing: HandBrake's code is GPL version 2 only, so libhbnlmeans is too. It is
added to EXTERNAL_LIBRARY_GPL_LIST, and configure refuses it with
`--enable-version3`. The wrapper itself is LGPL 2.1 or later, like the rest of
libavfilter.

Tested:
- `make fate` passes (3168 tests, no samples), including the two new tests
  `fate-filter-hbnlmeans` (8-bit 4:2:0, preset and tune, 97x65) and
  `fate-filter-hbnlmeans-prefilter-10bit` (10-bit 4:2:2, prefilter, 3 frames, 
97x65).
  Like the zscale tests, they run only when FFmpeg is configured with the 
library.
- The same checksums at 1, 3 and 16 filter threads.
- Builds standalone with `--disable-everything --enable-filter=hbnlmeans`
  (plus testsrc2, format, crop for the test).
- AddressSanitizer: no leaks or invalid accesses, including a 32-frame search 
on a
  10-frame input (flush with fewer frames than the search depth).
- `configure --enable-version3 --enable-libhbnlmeans` and
  `configure --enable-libhbnlmeans` without `--enable-gpl` both stop with a 
clear message.

Corneliu Ticu



>From 9fd2abff616fb6916ed31731de9d03d6f7477976 Mon Sep 17 00:00:00 2001
From: Corneliu Ticu <[email protected]>
Date: Wed, 4 Nov 2026 11:15:41 +0200
Subject: [PATCH] avfilter: add hbnlmeans filter

Add a wrapper for libhbnlmeans, which packages HandBrake's NLMeans
denoiser (libhb/nlmeans.c, by Dirk Farin and the HandBrake Team) as a
standalone library: https://github.com/WebcasterLTD/hbnlmeans

It differs from the existing nlmeans filter in three ways:
- it also searches the frames that follow the current one (temporal NLM,
  1 to 32 frames), not only the current frame;
- each patch's own weight is tunable (origin-tune), which keeps detail on
  noisy sources and flat areas in animation;
- it has HandBrake's prefilters (mean, median, CSM, reduce, edge boost)
  and HandBrake's presets and tunes.

The output is identical, byte for byte, to HandBrake's own NLMeans code
for every preset and tune, at 8 and 10 bit and at odd frame sizes. The
library's test/compare-handbrake.run builds HandBrake's unmodified
nlmeans sources and checks this. So a HandBrake encode's denoise settings
can be reproduced in FFmpeg exactly, using the same option string:

    ffmpeg -i in.mkv -vf hbnlmeans=preset=medium:tune=film out.mkv
    ffmpeg -i in.mkv -vf hbnlmeans=y-strength=6:y-origin-tune=0.8:cb-strength=8 
out.mkv

The filter holds back frame-count - 1 frames and flushes them at EOF,
uses slice threading, and supports planar YUV from 8 to 16 bit.

libhbnlmeans is GPL version 2 only, like HandBrake. It is added to the
GPL external libraries, and configure refuses it together with
--enable-version3.

Signed-off-by: Corneliu Ticu <[email protected]>
---
 Changelog                                     |   1 +
 configure                                     |   7 +
 doc/filters.texi                              | 135 +++++++++
 libavfilter/Makefile                          |   1 +
 libavfilter/allfilters.c                      |   1 +
 libavfilter/version.h                         |   2 +-
 libavfilter/vf_hbnlmeans.c                    | 286 ++++++++++++++++++
 tests/fate/filter-video.mak                   |   4 +
 tests/ref/fate/filter-hbnlmeans               |  15 +
 .../ref/fate/filter-hbnlmeans-prefilter-10bit |  15 +
 10 files changed, 466 insertions(+), 1 deletion(-)
 create mode 100644 libavfilter/vf_hbnlmeans.c
 create mode 100644 tests/ref/fate/filter-hbnlmeans
 create mode 100644 tests/ref/fate/filter-hbnlmeans-prefilter-10bit

diff --git a/Changelog b/Changelog
index 4c5e631d0ad..b89a3fbcc03 100644
--- a/Changelog
+++ b/Changelog
@@ -5,6 +5,7 @@ version <next>:
 - DTS:X height channels in the DCA decoder
 - LCEVC raw demuxer and muxer
 - AVS+ (AVS1-P16, Broadcasting profile) support in the Chinese AVS decoder
+- hbnlmeans filter (HandBrake NLMeans denoiser via libhbnlmeans)
 
 version 9.1:
 - ASTC image encoding and decoding via libastcenc, with .astc and KTX 1.0 
muxers and demuxers
diff --git a/configure b/configure
index 2bbc7812eb3..0ddc80f2afe 100755
--- a/configure
+++ b/configure
@@ -239,6 +239,7 @@ External library support:
   --enable-libfreetype     enable libfreetype, needed for drawtext filter [no]
   --enable-libfribidi      enable libfribidi, improves drawtext filter [no]
   --enable-libharfbuzz     enable libharfbuzz, needed for drawtext filter [no]
+  --enable-libhbnlmeans    enable HandBrake NLMeans denoiser via libhbnlmeans 
[no]
   --enable-libgme          enable Game Music Emu via libgme [no]
   --enable-libgsm          enable GSM de/encoding via libgsm [no]
   --enable-libiec61883     enable iec61883 via libiec61883 [no]
@@ -2009,6 +2010,7 @@ EXTERNAL_LIBRARY_GPL_LIST="
     libdavs2
     libdvdnav
     libdvdread
+    libhbnlmeans
     librubberband
     libvidstab
     libx264
@@ -4180,6 +4182,7 @@ fruc_vulkan_filter_deps="vulkan vulkan_1_4 spirv_compiler"
 fspp_filter_deps="gpl"
 fsync_filter_deps="avformat"
 gblur_vulkan_filter_deps="vulkan spirv_compiler"
+hbnlmeans_filter_deps="libhbnlmeans"
 hflip_vulkan_filter_deps="vulkan spirv_compiler"
 histeq_filter_deps="gpl"
 hqdn3d_filter_deps="gpl"
@@ -4823,6 +4826,9 @@ map "die_license_disabled version3" 
$EXTERNAL_LIBRARY_VERSION3_LIST $EXTERNAL_LI
 enabled gpl && map "die_license_disabled_gpl nonfree" 
$EXTERNAL_LIBRARY_NONFREE_LIST
 map "die_license_disabled nonfree" $HWACCEL_LIBRARY_NONFREE_LIST
 
+# GPL version 2 only, cannot be combined with (L)GPL version 3 code
+enabled version3 && enabled libhbnlmeans && die "libhbnlmeans is GPL version 2 
only and incompatible with --enable-version3."
+
 enabled version3 && { enabled gpl && enable gplv3 || enable lgplv3; }
 enabled gpl && enable lgpl_gpl # Files that are marked as LGPL but some 
developers prefer only building them with --enable-gpl
 
@@ -7261,6 +7267,7 @@ enabled libfontconfig     && require_pkg_config 
libfontconfig fontconfig "fontco
 enabled libfreetype       && require_pkg_config libfreetype freetype2 
"ft2build.h FT_FREETYPE_H" FT_Init_FreeType
 enabled libfribidi        && require_pkg_config libfribidi fribidi fribidi.h 
fribidi_version_info
 enabled libharfbuzz       && require_pkg_config libharfbuzz harfbuzz hb.h 
hb_buffer_create
+enabled libhbnlmeans      && require_pkg_config libhbnlmeans "hbnlmeans >= 
1.0.0" hbnlmeans.h hbnlm_create
 enabled libgme            && { check_pkg_config libgme libgme gme/gme.h 
gme_new_emu ||
                                require libgme gme/gme.h gme_new_emu -lgme 
-lstdc++; }
 enabled libgsm            && { for gsm_hdr in "gsm.h" "gsm/gsm.h"; do
diff --git a/doc/filters.texi b/doc/filters.texi
index b57a5e8a008..2bf64f49b10 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -15882,6 +15882,141 @@ Then, the effect of this Hald CLUT can be visualized 
with:
 ffplay input.mkv -vf "movie=clut.png, [in] haldclut"
 @end example
 
+@section hbnlmeans
+
+Denoise frames using HandBrake's Non-Local Means algorithm, through the
+libhbnlmeans library.
+
+Unlike @ref{nlmeans}, this filter also searches for similar patches in the
+frames that follow the current one, and it offers HandBrake's presets, tunes
+and prefilters. Its output is identical to HandBrake's own NLMeans denoiser
+with the same settings.
+
+Each frame is compared with the frames after it, so the filter holds back
+@option{frame-count} - 1 frames before it outputs the first one.
+
+To enable compilation of this filter you need to configure FFmpeg with
+@code{--enable-gpl --enable-libhbnlmeans}. libhbnlmeans is licensed under the
+GPL version 2 only, so it cannot be combined with @code{--enable-version3}.
+
+The option names are the ones HandBrake uses, so a HandBrake custom denoise
+string can be passed as is. Values set explicitly override the preset.
+Values left unset cascade: Cr takes the value of Cb, Cb takes the value of Y,
+and Y falls back to the defaults given below.
+
+The filter accepts the following options:
+
+@table @option
+@item preset
+Set the HandBrake preset. Default is @code{custom}.
+@table @samp
+@item custom
+No preset, only the values set explicitly.
+@item ultralight
+@item light
+@item medium
+@item strong
+@end table
+
+@item tune
+Set the HandBrake tune. Only used together with a @option{preset}.
+Default is @code{none}.
+@table @samp
+@item none
+@item film
+Most content, live action.
+@item grain
+Like @code{film}, but preserves luma grain.
+@item highmotion
+Like @code{film}, but avoids color smearing.
+@item animation
+Cel animation such as cartoons and anime.
+@item tape
+Analog tape sources such as VHS.
+@item sprite
+1-, 4-, 8- and 16-bit two-dimensional games.
+@end table
+
+@item y-strength
+@item cb-strength
+@item cr-strength
+Set the averaging weight decay. Larger values smooth more. Range is
+[0, 63], default for Y is 6.
+
+@item y-origin-tune
+@item cb-origin-tune
+@item cr-origin-tune
+Set the weight of the pixel's own patch. Lower values suit noisier sources
+and animation. Range is [0.01, 1], default for Y is 1.
+
+@item y-patch-size
+@item cb-patch-size
+@item cr-patch-size
+Set the patch width in pixels. Must be odd, range is [1, 15], default for Y
+is 7.
+
+@item y-range
+@item cb-range
+@item cr-range
+Set the width of the spatial search window in pixels. Must be odd. Larger
+values give better results and are much slower. Default for Y is 3.
+
+@item y-frame-count
+@item cb-frame-count
+@item cr-frame-count
+Set how many frames are searched, the current one included. Range is
+[1, 32], default for Y is 2. Too many frames smear motion.
+
+@item y-prefilter
+@item cb-prefilter
+@item cr-prefilter
+Set the prefilter applied before the patches are compared, as the sum of
+the following values. Default for Y is 0, no prefilter.
+@table @samp
+@item 1
+mean 3x3
+@item 2
+mean 5x5
+@item 4
+median 3x3
+@item 8
+median 5x5
+@item 16
+CSM 3x3
+@item 32
+CSM 5x5
+@item 256
+reduce the prefilter by 25%
+@item 512
+reduce the prefilter by 50%
+@item 1024
+edge boost
+@item 2048
+passthru: output the prefiltered image, without NLMeans
+@end table
+@end table
+
+@subsection Examples
+@itemize
+@item
+Use HandBrake's medium preset tuned for film:
+@example
+hbnlmeans=preset=medium:tune=film
+@end example
+
+@item
+Use a HandBrake custom string:
+@example
+hbnlmeans=y-strength=6:y-origin-tune=0.8:cb-strength=8
+@end example
+
+@item
+Denoise luma with a 3x3 mean prefilter reduced by 25%, with edge boost:
+@example
+hbnlmeans=y-strength=8:y-prefilter=1281
+@end example
+@end itemize
+
 @section hflip
 
 Flip the input video horizontally.
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index 6d052562e8f..0392efbca77 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -344,6 +344,7 @@ OBJS-$(CONFIG_GRAYWORLD_FILTER)              += 
vf_grayworld.o
 OBJS-$(CONFIG_GREYEDGE_FILTER)               += vf_colorconstancy.o
 OBJS-$(CONFIG_GUIDED_FILTER)                 += vf_guided.o framesync.o
 OBJS-$(CONFIG_HALDCLUT_FILTER)               += vf_lut3d.o framesync.o
+OBJS-$(CONFIG_HBNLMEANS_FILTER)              += vf_hbnlmeans.o
 OBJS-$(CONFIG_HFLIP_FILTER)                  += vf_hflip.o
 OBJS-$(CONFIG_HFLIP_VULKAN_FILTER)           += vf_flip_vulkan.o vulkan.o
 OBJS-$(CONFIG_HISTEQ_FILTER)                 += vf_histeq.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 9f07bf83dce..e0cbc739742 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -317,6 +317,7 @@ extern const FFFilter ff_vf_grayworld;
 extern const FFFilter ff_vf_greyedge;
 extern const FFFilter ff_vf_guided;
 extern const FFFilter ff_vf_haldclut;
+extern const FFFilter ff_vf_hbnlmeans;
 extern const FFFilter ff_vf_hflip;
 extern const FFFilter ff_vf_hflip_vulkan;
 extern const FFFilter ff_vf_histeq;
diff --git a/libavfilter/version.h b/libavfilter/version.h
index 64cd692ab67..537df129cd3 100644
--- a/libavfilter/version.h
+++ b/libavfilter/version.h
@@ -31,7 +31,7 @@
 
 #include "version_major.h"
 
-#define LIBAVFILTER_VERSION_MINOR  12
+#define LIBAVFILTER_VERSION_MINOR  13
 #define LIBAVFILTER_VERSION_MICRO 100
 
 
diff --git a/libavfilter/vf_hbnlmeans.c b/libavfilter/vf_hbnlmeans.c
new file mode 100644
index 00000000000..2e01ae21bfc
--- /dev/null
+++ b/libavfilter/vf_hbnlmeans.c
@@ -0,0 +1,286 @@
+/*
+ * Copyright (c) 2026 Corneliu Ticu
+ *
+ * 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
+ */
+
+/**
+ * @file
+ * HandBrake's NLMeans denoiser, through libhbnlmeans.
+ *
+ * Options use HandBrake's own names, so a HandBrake custom string works as is:
+ *     hbnlmeans=y-strength=6:y-origin-tune=0.8:cb-strength=8
+ * or a HandBrake preset and tune:
+ *     hbnlmeans=preset=medium:tune=film
+ * Explicit values override the preset.
+ */
+
+#include <string.h>
+
+#include <hbnlmeans.h>
+
+#include "libavutil/mem.h"
+#include "libavutil/opt.h"
+#include "libavutil/pixdesc.h"
+#include "avfilter.h"
+#include "filters.h"
+#include "video.h"
+
+static const char *const preset_names[] = { "custom", "ultralight", "light", 
"medium", "strong" };
+static const char *const tune_names[]    = { "none", "film", "grain", 
"highmotion", "animation", "tape", "sprite" };
+
+#define MAX_QUEUE 32
+
+typedef struct HBNLMeansFilterContext {
+    const AVClass *class;
+
+    int    preset;
+    int    tune;
+    double strength[3];
+    double origin_tune[3];
+    int    patch_size[3];
+    int    range[3];
+    int    frame_count[3];
+    int    prefilter[3];
+
+    HBNLMeans *nlm;
+    AVFrame   *props[MAX_QUEUE]; // queued frames' properties, no data
+    int        nb_queued;
+    int        eof;
+    int64_t    eof_pts;
+} HBNLMeansFilterContext;
+
+#define OFFSET(x) offsetof(HBNLMeansFilterContext, x)
+#define FLAGS AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM
+
+#define CHANNEL_OPTS(prefix, i)                                                
                                           \
+    { prefix "-strength",    "averaging weight decay, larger is smoother", 
OFFSET(strength[i]),    AV_OPT_TYPE_DOUBLE, {.dbl=-1}, -1, 63,   FLAGS }, \
+    { prefix "-origin-tune", "weight of the origin patch, 0.01..1",         
OFFSET(origin_tune[i]), AV_OPT_TYPE_DOUBLE, {.dbl=-1}, -1, 1,    FLAGS }, \
+    { prefix "-patch-size",  "patch width in pixels (odd)",                 
OFFSET(patch_size[i]),  AV_OPT_TYPE_INT,    {.i64=-1}, -1, 15,   FLAGS }, \
+    { prefix "-range",       "spatial search window width (odd)",           
OFFSET(range[i]),       AV_OPT_TYPE_INT,    {.i64=-1}, -1, 99,   FLAGS }, \
+    { prefix "-frame-count", "temporal search depth in frames",             
OFFSET(frame_count[i]), AV_OPT_TYPE_INT,    {.i64=-1}, -1, 32,   FLAGS }, \
+    { prefix "-prefilter",   "prefilter mode (sum of mode values)",         
OFFSET(prefilter[i]),   AV_OPT_TYPE_INT,    {.i64=-1}, -1, 4095, FLAGS }
+
+static const AVOption hbnlmeans_options[] = {
+    { "preset", "HandBrake preset", OFFSET(preset), AV_OPT_TYPE_INT, {.i64=0}, 
0, 4, FLAGS, .unit = "preset" },
+        { "custom",     "no preset, only the explicit values", 0, 
AV_OPT_TYPE_CONST, {.i64=0}, 0, 0, FLAGS, .unit = "preset" },
+        { "ultralight", NULL, 0, AV_OPT_TYPE_CONST, {.i64=1}, 0, 0, FLAGS, 
.unit = "preset" },
+        { "light",      NULL, 0, AV_OPT_TYPE_CONST, {.i64=2}, 0, 0, FLAGS, 
.unit = "preset" },
+        { "medium",     NULL, 0, AV_OPT_TYPE_CONST, {.i64=3}, 0, 0, FLAGS, 
.unit = "preset" },
+        { "strong",     NULL, 0, AV_OPT_TYPE_CONST, {.i64=4}, 0, 0, FLAGS, 
.unit = "preset" },
+    { "tune", "HandBrake tune (with a preset)", OFFSET(tune), AV_OPT_TYPE_INT, 
{.i64=0}, 0, 6, FLAGS, .unit = "tune" },
+        { "none",       NULL,                                    0, 
AV_OPT_TYPE_CONST, {.i64=0}, 0, 0, FLAGS, .unit = "tune" },
+        { "film",       "most content, live action",             0, 
AV_OPT_TYPE_CONST, {.i64=1}, 0, 0, FLAGS, .unit = "tune" },
+        { "grain",      "like film but preserves luma grain",    0, 
AV_OPT_TYPE_CONST, {.i64=2}, 0, 0, FLAGS, .unit = "tune" },
+        { "highmotion", "like film but avoids color smearing",   0, 
AV_OPT_TYPE_CONST, {.i64=3}, 0, 0, FLAGS, .unit = "tune" },
+        { "animation",  "cel animation such as cartoons, anime", 0, 
AV_OPT_TYPE_CONST, {.i64=4}, 0, 0, FLAGS, .unit = "tune" },
+        { "tape",       "analog tape sources such as VHS",       0, 
AV_OPT_TYPE_CONST, {.i64=5}, 0, 0, FLAGS, .unit = "tune" },
+        { "sprite",     "1-/4-/8-/16-bit 2-dimensional games",   0, 
AV_OPT_TYPE_CONST, {.i64=6}, 0, 0, FLAGS, .unit = "tune" },
+    CHANNEL_OPTS("y",  0),
+    CHANNEL_OPTS("cb", 1),
+    CHANNEL_OPTS("cr", 2),
+    { NULL }
+};
+
+AVFILTER_DEFINE_CLASS(hbnlmeans);
+
+static const enum AVPixelFormat pix_fmts[] = {
+    AV_PIX_FMT_YUV410P, AV_PIX_FMT_YUV411P, AV_PIX_FMT_YUV420P,
+    AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV440P, AV_PIX_FMT_YUV444P,
+    AV_PIX_FMT_YUV420P9,  AV_PIX_FMT_YUV422P9,  AV_PIX_FMT_YUV444P9,
+    AV_PIX_FMT_YUV420P10, AV_PIX_FMT_YUV422P10, AV_PIX_FMT_YUV440P10, 
AV_PIX_FMT_YUV444P10,
+    AV_PIX_FMT_YUV420P12, AV_PIX_FMT_YUV422P12, AV_PIX_FMT_YUV440P12, 
AV_PIX_FMT_YUV444P12,
+    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_NONE
+};
+
+static int config_input(AVFilterLink *inlink)
+{
+    AVFilterContext *ctx = inlink->dst;
+    HBNLMeansFilterContext *s = ctx->priv;
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
+    HBNLMParams p, used;
+
+    for (int c = 0; c < 3; c++) {
+        p.strength[c]    = s->strength[c];
+        p.origin_tune[c] = s->origin_tune[c];
+        p.patch_size[c]  = s->patch_size[c];
+        p.range[c]       = s->range[c];
+        p.frame_count[c] = s->frame_count[c];
+        p.prefilter[c]   = s->prefilter[c];
+    }
+    if (s->preset)
+        hbnlm_params_preset(&p, preset_names[s->preset], tune_names[s->tune]);
+    else if (s->tune)
+        av_log(ctx, AV_LOG_WARNING, "tune only applies with a preset; ignoring 
it.\n");
+
+    for (int i = 0; i < s->nb_queued; i++)
+        av_frame_free(&s->props[i]);
+    s->nb_queued = 0;
+    hbnlm_free(&s->nlm);
+    s->nlm = hbnlm_create(&p, inlink->w, inlink->h, desc->log2_chroma_w,
+                          desc->log2_chroma_h, desc->comp[0].depth);
+    if (!s->nlm)
+        return AVERROR(ENOMEM);
+
+    hbnlm_get_params(s->nlm, &used);
+    for (int c = 0; c < 3; c++)
+        av_log(ctx, AV_LOG_VERBOSE, "%s: strength %g origin-tune %g patch %d 
range %d frames %d prefilter %d\n",
+               c == 0 ? "y" : c == 1 ? "cb" : "cr", used.strength[c], 
used.origin_tune[c],
+               used.patch_size[c], used.range[c], used.frame_count[c], 
used.prefilter[c]);
+    return 0;
+}
+
+static int filter_slice(AVFilterContext *ctx, void *arg, int jobnr, int 
nb_jobs)
+{
+    HBNLMeansFilterContext *s = ctx->priv;
+    AVFrame *out = arg;
+    uint8_t *dst[3] = { out->data[0], out->data[1], out->data[2] };
+    const ptrdiff_t stride[3] = { out->linesize[0], out->linesize[1], 
out->linesize[2] };
+    int ret = hbnlm_filter_slice(s->nlm, dst, stride, jobnr, nb_jobs);
+    return ret < 0 ? AVERROR(-ret) : 0;
+}
+
+// Denoise the head of the queue, send it, and drop it
+static int output_frame(AVFilterContext *ctx)
+{
+    HBNLMeansFilterContext *s = ctx->priv;
+    AVFilterLink *outlink = ctx->outputs[0];
+    AVFrame *out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
+    int ret;
+
+    if (!out)
+        return AVERROR(ENOMEM);
+    ret = av_frame_copy_props(out, s->props[0]);
+    if (ret >= 0)
+        ret = ff_filter_execute(ctx, filter_slice, out, NULL,
+                                FFMIN(AV_CEIL_RSHIFT(outlink->h, 2), 
ff_filter_get_nb_threads(ctx)));
+    if (ret < 0) {
+        av_frame_free(&out);
+        return ret;
+    }
+
+    hbnlm_pop(s->nlm);
+    av_frame_free(&s->props[0]);
+    memmove(&s->props[0], &s->props[1], (s->nb_queued - 1) * 
sizeof(*s->props));
+    s->props[--s->nb_queued] = NULL;
+
+    return ff_filter_frame(outlink, out);
+}
+
+static int add_frame(AVFilterContext *ctx, AVFrame *in)
+{
+    HBNLMeansFilterContext *s = ctx->priv;
+    const uint8_t *src[3] = { in->data[0], in->data[1], in->data[2] };
+    const ptrdiff_t stride[3] = { in->linesize[0], in->linesize[1], 
in->linesize[2] };
+    AVFrame *props = av_frame_alloc();
+    int ret;
+
+    if (!props) {
+        av_frame_free(&in);
+        return AVERROR(ENOMEM);
+    }
+    ret = av_frame_copy_props(props, in);
+    if (ret >= 0) {
+        ret = hbnlm_push(s->nlm, src, stride);
+        ret = ret < 0 ? AVERROR(-ret) : 0;
+    }
+    av_frame_free(&in);
+    if (ret < 0) {
+        av_frame_free(&props);
+        return ret;
+    }
+    s->props[s->nb_queued++] = props;
+    return 0;
+}
+
+static int activate(AVFilterContext *ctx)
+{
+    HBNLMeansFilterContext *s = ctx->priv;
+    AVFilterLink *inlink  = ctx->inputs[0];
+    AVFilterLink *outlink = ctx->outputs[0];
+    AVFrame *in;
+    int64_t pts;
+    int status, ret;
+
+    FF_FILTER_FORWARD_STATUS_BACK(outlink, inlink);
+
+    if (!s->eof) {
+        ret = ff_inlink_consume_frame(inlink, &in);
+        if (ret < 0)
+            return ret;
+        if (ret > 0) {
+            ret = add_frame(ctx, in);
+            if (ret < 0)
+                return ret;
+            // Each frame is compared with the frames after it, so wait for 
them
+            if (s->nb_queued >= hbnlm_frames_needed(s->nlm))
+                ret = output_frame(ctx);
+            ff_filter_set_ready(ctx, 100);
+            return ret;
+        }
+        if (ff_inlink_acknowledge_status(inlink, &status, &pts)) {
+            s->eof = 1;
+            s->eof_pts = pts;
+        }
+    }
+
+    if (s->eof) {
+        // Flush: the last frames use only the frames that remain
+        if (s->nb_queued) {
+            ret = output_frame(ctx);
+            ff_filter_set_ready(ctx, 100);
+            return ret;
+        }
+        ff_outlink_set_status(outlink, AVERROR_EOF, s->eof_pts);
+        return 0;
+    }
+
+    FF_FILTER_FORWARD_WANTED(outlink, inlink);
+    return FFERROR_NOT_READY;
+}
+
+static av_cold void uninit(AVFilterContext *ctx)
+{
+    HBNLMeansFilterContext *s = ctx->priv;
+    for (int i = 0; i < s->nb_queued; i++)
+        av_frame_free(&s->props[i]);
+    s->nb_queued = 0;
+    hbnlm_free(&s->nlm);
+}
+
+static const AVFilterPad hbnlmeans_inputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = config_input,
+    },
+};
+
+const FFFilter ff_vf_hbnlmeans = {
+    .p.name        = "hbnlmeans",
+    .p.description = NULL_IF_CONFIG_SMALL("HandBrake's NLMeans denoiser 
(libhbnlmeans)."),
+    .p.priv_class  = &hbnlmeans_class,
+    .p.flags       = AVFILTER_FLAG_SLICE_THREADS,
+    .priv_size     = sizeof(HBNLMeansFilterContext),
+    .activate      = activate,
+    .uninit        = uninit,
+    FILTER_INPUTS(hbnlmeans_inputs),
+    FILTER_OUTPUTS(ff_video_default_filterpad),
+    FILTER_PIXFMTS_ARRAY(pix_fmts),
+};
diff --git a/tests/fate/filter-video.mak b/tests/fate/filter-video.mak
index 95254ff3fcf..efe058d7d03 100644
--- a/tests/fate/filter-video.mak
+++ b/tests/fate/filter-video.mak
@@ -250,6 +250,10 @@ fate-filter-negate: CMD = framecrc -c:v pgmyuv -i $(SRC) 
-vf perms=random,negate
 FATE_FILTER_VSYNTH_PGMYUV-$(CONFIG_HISTOGRAM_FILTER) += 
fate-filter-histogram-levels
 fate-filter-histogram-levels: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf 
histogram -flags +bitexact -sws_flags +accurate_rnd+bitexact
 
+FATE_FILTER-$(call FILTERFRAMECRC, TESTSRC2 FORMAT CROP HBNLMEANS) += 
fate-filter-hbnlmeans fate-filter-hbnlmeans-prefilter-10bit
+fate-filter-hbnlmeans: CMD = framecrc -lavfi 
testsrc2=s=98x66:r=5:d=2,format=yuv420p,crop=97:65:0:0:exact=1,hbnlmeans=preset=medium:tune=film
 -flags +bitexact
+fate-filter-hbnlmeans-prefilter-10bit: CMD = framecrc -lavfi 
testsrc2=s=98x66:r=5:d=2,format=yuv422p10,crop=97:65:0:0:exact=1,hbnlmeans=y-strength=8:y-prefilter=1281:y-frame-count=3:cb-strength=4:cb-range=5
 -flags +bitexact
+
 FATE_FILTER-$(call FILTERFRAMECRC, TESTSRC2 FORMAT THISTOGRAM) += 
fate-filter-thistogram-scroll
 fate-filter-thistogram-scroll: CMD = framecrc -lavfi 
testsrc2=s=65x49:r=1:d=1,format=yuv420p,thistogram=display_mode=parade:components=7:slide=scroll
 -flags +bitexact
 
diff --git a/tests/ref/fate/filter-hbnlmeans b/tests/ref/fate/filter-hbnlmeans
new file mode 100644
index 00000000000..013267a7a33
--- /dev/null
+++ b/tests/ref/fate/filter-hbnlmeans
@@ -0,0 +1,15 @@
+#tb 0: 1/5
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 97x65
+#sar 0: 1/1
+0,          0,          0,        1,     9539, 0xf9f39cb7
+0,          1,          1,        1,     9539, 0xa81ea0cf
+0,          2,          2,        1,     9539, 0x93a19389
+0,          3,          3,        1,     9539, 0x77198ade
+0,          4,          4,        1,     9539, 0xc22a9563
+0,          5,          5,        1,     9539, 0xd01f8527
+0,          6,          6,        1,     9539, 0xa1e496c0
+0,          7,          7,        1,     9539, 0x1732ae3c
+0,          8,          8,        1,     9539, 0x011bafbc
+0,          9,          9,        1,     9539, 0x1047ae82
diff --git a/tests/ref/fate/filter-hbnlmeans-prefilter-10bit 
b/tests/ref/fate/filter-hbnlmeans-prefilter-10bit
new file mode 100644
index 00000000000..4ed2aa76d74
--- /dev/null
+++ b/tests/ref/fate/filter-hbnlmeans-prefilter-10bit
@@ -0,0 +1,15 @@
+#tb 0: 1/5
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 97x65
+#sar 0: 1/1
+0,          0,          0,        1,    25350, 0xb4d281e6
+0,          1,          1,        1,    25350, 0x5ba9957f
+0,          2,          2,        1,    25350, 0xedae7c39
+0,          3,          3,        1,    25350, 0x19ee3108
+0,          4,          4,        1,    25350, 0xa3c98cce
+0,          5,          5,        1,    25350, 0x615c58b6
+0,          6,          6,        1,    25350, 0xc8c37aa1
+0,          7,          7,        1,    25350, 0x510d5e47
+0,          8,          8,        1,    25350, 0x5cc388b7
+0,          9,          9,        1,    25350, 0x2c1984bc
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to