Coverage Report

Created: 2026-10-28 06:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/work/vvenc/source/Lib/EncoderLib/SEIFilmGrainAnalyzer.h
Line
Count
Source
1
/* -----------------------------------------------------------------------------
2
The copyright in this software is being made available under the Clear BSD
3
License, included below. No patent rights, trademark rights and/or
4
other Intellectual Property Rights other than the copyrights concerning
5
the Software are granted under this license.
6
7
The Clear BSD License
8
9
Copyright (c) 2019-2026, Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. & The VVenC Authors.
10
All rights reserved.
11
12
Redistribution and use in source and binary forms, with or without modification,
13
are permitted (subject to the limitations in the disclaimer below) provided that
14
the following conditions are met:
15
16
     * Redistributions of source code must retain the above copyright notice,
17
     this list of conditions and the following disclaimer.
18
19
     * Redistributions in binary form must reproduce the above copyright
20
     notice, this list of conditions and the following disclaimer in the
21
     documentation and/or other materials provided with the distribution.
22
23
     * Neither the name of the copyright holder nor the names of its
24
     contributors may be used to endorse or promote products derived from this
25
     software without specific prior written permission.
26
27
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
28
THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
29
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
31
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
32
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
33
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
34
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
35
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
36
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
37
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
38
POSSIBILITY OF SUCH DAMAGE.
39
40
41
------------------------------------------------------------------------------------------- */
42
43
/* Film grain analysis (--fga 1, 2 and 3): a port of the film grain analysis of the VTM reference software (VTM master,
44
 * including JVET-AL0282 and JVET-AN0237 by InterDigital), with improvements (--fga 2) and the auto-regressive grain
45
 * model (--fga 3, which signals an AR filter fitted to the grain spectrum instead of cut-off frequencies).
46
 *
47
 * For every analysed picture, the grain is estimated as the difference between the original and a temporally filtered
48
 * (denoised) version of it, as in VTM (--fga 1). With the improvements (--fga 2) it is estimated from the temporal
49
 * residual instead, twice the picture minus its motion compensated neighbours (FgaTemporalResidual), which also holds
50
 * the part of the grain that the filter keeps.
51
 * Flat blocks are found with a multi-scale Canny edge detector and morphological dilation on the denoised picture. On
52
 * 16x16 flat blocks the analysis collects
53
 *  - the block mean and the grain strength (twice the standard deviation of the grain), from which a polynomial
54
 *    scaling function is fitted and quantized with Lloyd-Max into intensity intervals and scaling factors, and
55
 *  - the squared DCT of the grain, per block mean, from which horizontal and vertical cut-off frequencies are
56
 *    estimated per intensity interval.
57
 * Both are accumulated over the analysed pictures with exponentially decreasing weights, so that the estimate follows
58
 * the current scene.
59
 */
60
61
#pragma once
62
63
#include "CommonLib/CommonDef.h"
64
#include "CommonLib/Unit.h"
65
#include "CommonLib/SEI.h"
66
67
#include <utility>
68
#include <vector>
69
70
namespace vvenc {
71
72
// Row and block kernels of the analysis, with SIMD versions in CommonLib/x86/FGAX86.h and
73
// CommonLib/arm/neon/FGA_neon.cpp. A row is processed from x = 0 to width - 1; neighbours outside the picture repeat
74
// the border sample.
75
struct FgaRowKernels
76
{
77
  // Sobel gradients of the row "cur": magnitude, (|gx| + |gy|) / 2 limited to maxVal, and edge direction (0, 45, 90
78
  // or 135 degrees).
79
  void ( *sobel )   ( const Pel* above, const Pel* cur, const Pel* below, int width, Pel maxVal, Pel* mag, Pel* dir );
80
  // Non-maximum suppression: the magnitude where it is not smaller than both neighbours along the direction, else 0.
81
  void ( *nonMax )  ( const Pel* magAbove, const Pel* mag, const Pel* magBelow, const Pel* dir, int width, Pel* dst );
82
  // 3-tap maximum (dilate) or minimum (erode) along a row, and over three rows.
83
  void ( *morphRow )( const Pel* src, int width, bool dilate, Pel* dst );
84
  void ( *morphCol )( const Pel* above, const Pel* cur, const Pel* below, int width, bool dilate, Pel* dst );
85
  // Largest of n non-negative samples.
86
  Pel  ( *rowMax )  ( const Pel* src, int n );
87
  // Double threshold: strong above high, weak above low, else 0.
88
  void ( *classify )( Pel* row, int n, Pel low, Pel high, Pel weak, Pel strong );
89
  // Positions x of the samples equal to value, in increasing order; returns their number.
90
  int  ( *findEqual )( const Pel* row, int n, Pel value, int* positions );
91
92
  // Block kernels, on 16x16 blocks.
93
  bool ( *isZeroBlock ) ( const Pel* src, int stride );
94
  void ( *blockSums )   ( const Pel* src, int stride, int64_t& sum, int64_t& sumSq );
95
  // 16x16 DCT-II of VVC (rounding shift 8 after each stage, the second stage clipped to +-maxRange); the squared
96
  // coefficients are added to sum[ horizontal * 16 + vertical ].
97
  void ( *squaredDct16 )( const Pel* src, int stride, int maxRange, double* sum );
98
99
  // Temporal residual kernels (FgaTemporalResidual).
100
  // 4x downsampling of n samples: dst[ x ] = ( sum of the 4x4 samples from src[ 4 x ] on + 8 ) >> 4.
101
  void ( *downsample4 ) ( const Pel* src, int stride, int n, Pel* dst );
102
  // Sums of absolute differences of n 4x4 blocks next to each other: sad[ b ] of the blocks at x = 4 b.
103
  void ( *sad4x4Row )   ( const Pel* cur, int curStride, const Pel* ref, int refStride, int n, int* sad );
104
  // Temporal residual of n samples: 2 cur - prev - next, or cur - prev when next is null.
105
  void ( *residualRow ) ( const Pel* cur, const Pel* prev, const Pel* next, int n, Pel* dst );
106
};
107
108
// Scalar row kernels for x = xStart to xEnd - 1; the SIMD kernels use them at the picture borders.
109
void fgaSobelCore   ( const Pel* above, const Pel* cur, const Pel* below, int width, int xStart, int xEnd, Pel maxVal, Pel* mag, Pel* dir );
110
void fgaNonMaxCore  ( const Pel* magAbove, const Pel* mag, const Pel* magBelow, const Pel* dir, int width, int xStart, int xEnd, Pel* dst );
111
void fgaMorphRowCore( const Pel* src, int width, int xStart, int xEnd, bool dilate, Pel* dst );
112
void fgaMorphColCore( const Pel* above, const Pel* cur, const Pel* below, int xStart, int xEnd, bool dilate, Pel* dst );
113
Pel  fgaRowMaxCore  ( const Pel* src, int n );
114
int  fgaFindEqualCore( const Pel* row, int n, Pel value, int* positions );
115
void fgaClassifyCore( Pel* row, int n, Pel low, Pel high, Pel weak, Pel strong );
116
bool fgaIsZeroBlockCore ( const Pel* src, int stride );
117
void fgaBlockSumsCore   ( const Pel* src, int stride, int64_t& sum, int64_t& sumSq );
118
void fgaSquaredDct16Core( const Pel* src, int stride, int maxRange, double* sum );
119
void fgaDownsample4Core ( const Pel* src, int stride, int n, Pel* dst );
120
void fgaSad4x4RowCore   ( const Pel* cur, int curStride, const Pel* ref, int refStride, int n, int* sad );
121
void fgaResidualRowCore ( const Pel* cur, const Pel* prev, const Pel* next, int n, Pel* dst );
122
123
class FGAnalyzer
124
{
125
public:
126
  FGAnalyzer();
127
128
  // improved: the analysis with the improvements beyond VTM (--fga 2), else VTM's analysis with its bugs fixed
129
  // (--fga 1). arModel: the improved analysis with the auto-regressive grain model (--fga 3). See m_improved.
130
  void init( int width, int height, ChromaFormat chromaFormat, const int bitDepths[ MAX_NUM_CH ], const bool doAnalysis[ MAX_NUM_COMP ], bool improved, bool arModel = false );
131
132
  // Analyses one picture: orig is the original, denoised its temporally filtered version (same size). residual is the
133
  // temporal residual of FgaTemporalResidual, whose variance is residualWeight times the grain variance; without it
134
  // the grain is estimated as denoised minus orig. With sceneCut the statistics of the previous pictures are dropped
135
  // first, as they describe another scene.
136
  void estimateGrainParameters( const CPelUnitBuf& orig, const CPelUnitBuf& denoised, const CPelUnitBuf* residual = nullptr, int residualWeight = 0,
137
                                bool sceneCut = false );
138
139
  // Sets the row kernels: the scalar ones, then the SIMD ones the CPU supports.
140
  static void initKernels( FgaRowKernels& k );
141
142
  // The parameters to signal in the FGC SEI, with the grain strength set to strength percent of the analysed grain:
143
  // the log2 scale factor and the component models, with scaling factors calibrated against the synthesis.
144
  void getSignalledParameters( double strength, int& log2ScaleFactor, SeiFgc::CompModel compModel[ MAX_NUM_COMP ] ) const;
145
146
  // A plane of samples with its own storage; the analysis works on whole planes.
147
  struct Plane
148
  {
149
    int              width  = 0;
150
    int              height = 0;
151
    std::vector<Pel> samples;
152
153
    // Sets the size; the storage only grows, so planes reused for smaller sizes are not cleared again.
154
0
    void resize( int w, int h )                     { width = w; height = h; if( samples.size() < (size_t) w * h ) samples.resize( (size_t) w * h ); }
155
0
    Pel* row   ( int y )                            { return samples.data() + (size_t) y * width; }
156
0
    const Pel* row( int y ) const                   { return samples.data() + (size_t) y * width; }
157
0
    Pel  at    ( int x, int y ) const               { return samples[ (size_t) y * width + x ]; }
158
    // Sample at (x, y) with the coordinates clamped to the plane, as on a border-extended buffer.
159
    Pel  clampedAt( int x, int y ) const;
160
  };
161
162
private:
163
  static constexpr int    BLOCK_SIZE         = 16;    // analysis block size, for scaling function and cut-off frequencies
164
  // Weight of the data accumulated so far when a picture is added. VTM sums the data of all pictures without limit,
165
  // so a new scene hardly changes the estimate; halving the old data lets it follow the scene within a few analyses.
166
  static constexpr double ACCUMULATION_DECAY = 0.5;
167
168
  // Squared DCT coefficients of the grain, summed over the blocks with one block mean.
169
  typedef std::vector<double> SquaredDctSum;   // BLOCK_SIZE * BLOCK_SIZE entries, [horizontal][vertical]
170
171
  struct CutoffPair
172
  {
173
    std::pair<int, int> p;      // horizontal and vertical cut-off
174
    int                 w = 0;  // weight (interval width)
175
    std::vector<int>    idx;    // intensity intervals having this pair
176
  };
177
178
  int               m_width  = 0;
179
  int               m_height = 0;
180
  ChromaFormat      m_chromaFormat = CHROMA_420;
181
  int               m_bitDepths[ MAX_NUM_CH ] = { 10, 10 };
182
  bool              m_doAnalysis[ MAX_NUM_COMP ] = { false, false, false };
183
184
  // The analysed parameters: intensities at 8 bits, compModelValue[ 0 ] the grain strength (m_strengthScale times twice
185
  // the standard deviation of the grain on 16x16 blocks, at the component's bit depth), cut-off frequencies at the
186
  // component's resolution. getSignalledParameters() converts them for the synthesis.
187
  SeiFgc::CompModel m_compModel[ MAX_NUM_COMP ];
188
  int               m_cutoffPairs[ MAX_NUM_COMP ] = { 0, 0, 0 };
189
  int               m_failedFits [ MAX_NUM_COMP ] = { 0, 0, 0 };   // analyses in a row whose scaling function fit failed
190
191
  // With the improvements beyond VTM (--fga 2): the grain is measured from the temporal residual when MCTF provides
192
  // it, the strengths are kept in finer units, the scaling function fit works with few intensity intervals (chroma),
193
  // the scaling factors are calibrated against the synthesis, and the statistics follow grain that changes or
194
  // disappears. Without them (--fga 1) the analysis is VTM's, with its bugs fixed.
195
  bool              m_improved = true;
196
  int               m_strengthScale = 1;   // units of the strengths: twice the standard deviation / m_strengthScale
197
198
  // The auto-regressive grain model (--fga 3, FGC model_id 1): instead of cut-off frequencies, every component
199
  // signals the filter of vvdec's SEI AR mode, x = a1 * left + a1 * r * top + a3 * r * ( top-left + top-right )
200
  // + a5 * left-left + a5 * r^2 * top-top + noise, fitted to the measured grain spectrum. One filter per component,
201
  // the same for all intensity intervals, so that the synthesis makes one pattern per component.
202
  struct ArFilter
203
  {
204
    double a1 = 0.0, a3 = 0.0, a5 = 0.0, r = 1.0;
205
    double nonDcShare = 1.0;   // share of the grain variance outside the DC coefficient of 16x16 blocks
206
    bool   valid = false;
207
  };
208
  bool              m_arModel = false;
209
  ArFilter          m_arFilter[ MAX_NUM_COMP ];
210
211
  // Data accumulated over the analysed pictures, weighted (see ACCUMULATION_DECAY).
212
  std::vector<SquaredDctSum> m_squaredDctSums[ MAX_NUM_COMP ];   // per block mean
213
  std::vector<double>        m_numDctBlocks  [ MAX_NUM_COMP ];   // per block mean
214
  std::vector<double>        m_storedMeanIntensity   [ MAX_NUM_COMP ];
215
  std::vector<double>        m_storedStrengthIntensity[ MAX_NUM_COMP ];
216
  std::vector<double>        m_storedElementsPerInterval[ MAX_NUM_COMP ];
217
218
  // Planes of the picture being analysed.
219
  Plane             m_denoised[ MAX_NUM_COMP ];
220
  Plane             m_grain   [ MAX_NUM_COMP ];   // temporal residual, or denoised minus original
221
  int               m_grainWeight = 1;            // variance of m_grain relative to the grain variance
222
  Plane             m_mask    [ MAX_NUM_COMP ];   // non-zero where the picture is not flat
223
224
  // Scratch planes and lists of the mask search, kept to avoid allocating and clearing them for every picture.
225
  struct Scratch
226
  {
227
    Plane            magnitude, direction, morph, half, quarter, lowMask;
228
    std::vector<int> positions, weakX, weakY, columnStart;
229
    std::vector<Pel> wide, top, bottom, left, right;
230
  };
231
  Scratch           m_scratch;
232
233
  // flat area mask
234
  void resetStatistics       ( int c );
235
  void findMask              ( ComponentID compId );
236
  void maskForScale          ( const Plane& picture, Plane& mask, int bitDepth, int numDilations );
237
238
  // parameter estimation
239
  void estimateComponent     ( ComponentID compId );
240
  void estimateScalingFactors( std::vector<int>& dataX, std::vector<int>& dataY, int bitDepth, ComponentID compId );
241
  bool fitFunction           ( std::vector<int>& dataX, std::vector<int>& dataY, std::vector<double>& coeffs, std::vector<double>& scalingVec,
242
                               int order, int bitDepth, bool secondPass, ComponentID compId );
243
  void extendPoints          ( std::vector<int>& dataX, std::vector<int>& dataY, int bitDepth ) const;
244
  bool lloydMax              ( std::vector<double>& scalingVec, std::vector<int>& quantizedVec, int numQuantizedLevels, int bitDepth ) const;
245
  void setEstimatedParameters( std::vector<int>& quantizedVec, int bitDepth, ComponentID compId );
246
  void holdEdgeStrengths     ( std::vector<int>& quantizedVec, int bitDepth ) const;
247
  void mergeIntervals        ( std::vector<std::vector<int>>& parameters, int bitDepth ) const;
248
  void scaleDown             ( std::vector<std::vector<int>>& parameters, int bitDepth ) const;
249
250
  void getVtmParameters      ( double strength, int& log2ScaleFactor, SeiFgc::CompModel compModel[ MAX_NUM_COMP ] ) const;
251
  void getArParameters       ( double strength, int& log2ScaleFactor, SeiFgc::CompModel compModel[ MAX_NUM_COMP ] ) const;
252
  void fitArFilter           ( ComponentID compId );
253
254
  void estimateCutoffFreq    ( ComponentID compId, int bitDepth );
255
  int  cutoffFrequency       ( const std::vector<double>& mean ) const;
256
  void fitCutoffPair         ( const std::vector<double>& meanSq, bool chroma420, int& horizontal, int& vertical ) const;
257
  void replaceCutoff         ( ComponentID compId, int& replacementH, int& replacementV );
258
  void limitCutoff           ( ComponentID compId, int avgH, int avgV );
259
  void limitCutoffConsecutive( ComponentID compId );
260
  int  limitCutoffPairs      ( std::vector<CutoffPair>& pairs, int maxUnique ) const;
261
262
  FgaRowKernels     m_kernels;
263
264
#if ENABLE_SIMD_OPT_FGA && defined( TARGET_SIMD_X86 )
265
  static void initFGAnalyzerX86( FgaRowKernels& k );
266
  template <x86_simd::X86_VEXT vext>
267
  static void _initFGAnalyzerX86( FgaRowKernels& k );
268
#endif
269
#if ENABLE_SIMD_OPT_FGA && defined( TARGET_SIMD_ARM )
270
  static void initFGAnalyzerARM( FgaRowKernels& k );
271
  template <arm_simd::ARM_VEXT vext>
272
  static void _initFGAnalyzerARM( FgaRowKernels& k );
273
#endif
274
};
275
276
// Temporal residual of the film grain for the improved analysis (--fga 2). For grain that is independent from picture
277
// to picture, the residual e = 2 O - P - N of a picture O and its motion compensated neighbours P and N has six times
278
// the grain variance and the spectrum of the grain, whatever the denoiser would have kept of it. MCTF computes it for
279
// the analysed pictures, while their neighbours are available.
280
class FgaTemporalResidual
281
{
282
public:
283
  FgaTemporalResidual();
284
285
  // Computes the residual of cur from the previous and next picture (null when missing) into residual (created here).
286
  // The motion is searched per 16x16 block on 4x downsampled luma, with a bias towards zero motion, so that the vectors
287
  // do not follow the grain in flat areas. A neighbour that matches much worse than the other (a scene cut) is left
288
  // out. Returns the variance of the residual relative to the grain variance: 6 with both neighbours, 2 with one, and
289
  // 0, with no residual made, without neighbours or for pictures smaller than one block.
290
  int compute( const CPelUnitBuf& cur, const CPelUnitBuf* prev, const CPelUnitBuf* next, PelStorage& residual ) const;
291
292
private:
293
  // Block motion in samples of the 4x downsampled pictures, per 16x16 block of the full picture.
294
  struct Motion
295
  {
296
    std::vector<int8_t> dx, dy;
297
    double              meanSad = 0.0;   // mean absolute difference per downsampled sample, at the chosen vectors
298
  };
299
300
  void downsample( const CPelBuf& src, FGAnalyzer::Plane& dst ) const;
301
  void search    ( const FGAnalyzer::Plane& cur, const FGAnalyzer::Plane& ref, int numBlocksX, int numBlocksY, Motion& motion ) const;
302
303
  FgaRowKernels m_kernels;
304
};
305
306
} // namespace vvenc