NIQE Extractor¶
NIQE (Natural Image Quality Evaluator) is a no-reference, opinion-unaware blind image-quality metric (Mittal, Soundararajan & Bovik, IEEE Signal Processing Letters 20(3):209-212, 2013). It scores a single picture by measuring how far its natural-scene statistics (NSS) deviate from the statistics of a pristine image population — no reference frame and no subjective opinion scores are required. The fork ships it as a normal libvmaf feature extractor named niqe.
Because it is a no-reference metric, NIQE scores only the distorted picture; the reference frame is ignored, mirroring the existing CAMBI no-reference posture. Higher score means lower perceptual quality (further from the natural-scene model).
Output¶
| Field | Value |
|---|---|
| Feature name | niqe |
| Output metric | niqe |
| Direction | Lower is better (0 = closest to the pristine model) |
| Range | [0, ∞); typical natural content scores roughly 2–30 |
| Reference frame | Ignored (no-reference metric) |
| Pristine model | model/other_models/niqe_v0.1.pkl (embedded at build time) |
| Snapshot | testdata/scores_cpu_niqe.json (fork-added, not Netflix golden) |
Usage¶
vmaf \
--reference dist.yuv \
--distorted dist.yuv \
--width 1920 --height 1080 --pixel_format 420 --bitdepth 8 \
--feature niqe \
--output score.json
NIQE ignores the reference, so you may pass the same file for --reference and --distorted (or any reference of matching dimensions). The per-frame JSON metric key is niqe; pooled values appear under the same key in pooled_metrics.
Algorithm¶
Per frame, on the distorted luma plane only:
- MSCN (scale 1) — compute the mean-subtracted contrast-normalized coefficients via a separable 7-tap Gaussian (sigma = 7/6,
lw = 3) with anearest(clamp-to-edge) boundary and an additiveC = 1stabiliser:mscn = (luma - mu) / (sigma + 1). The convolutions run in float64; the resulting map is rounded to float32 (harness parity). - MSCN (scale 2) — bicubic-downscale the integer luma by 2 (PIL-compatible Catmull-Rom,
a = -0.5, output rounded to float32) and repeat the MSCN transform. - Patch features — for each non-overlapping 96×96 patch (48×48 on the half-resolution map), fit an asymmetric generalized Gaussian (AGGD) to the MSCN patch and to its four paired products (V, H, D1, D2), producing an 18-feature vector per scale, 36 features per patch.
- Pooling — average the per-patch vectors to a sample mean and compute their unbiased (ddof = 1) covariance.
- Distance — the score is the Mahalanobis distance
sqrt(Xᵀ · pinv((cov_pris + cov_sample)/2) · X)whereX = mu_sample - mu_prisandpinvis the symmetric pseudo-inverse (scipy default cutoffrtol = 36 · ε).
Fork-specific divergences (load-bearing)¶
The fork's pristine model niqe_v0.1.pkl was trained against the fork's Python harness, which differs from upstream NIQE (LIVE MATLAB, scikit-video) in two ways the C port must replicate exactly:
- AGGD mean parameter
Ncarries a trailing*aggdratiofactor that upstream omits. Without it,Nis ~0.245 instead of ~0.428 on the reference oracle. - float32 round-trip: the MSCN maps and the PIL bicubic half-resolution output are quantized to float32 before patch features are extracted. Skipping this shifts each feature by ~1e-7, which — amplified by the ill-conditioned averaged covariance — moves the final score by up to ~1e-4.
Consequence: the fork's NIQE scores match the fork Python harness but do not match reference LIVE / scikit-video NIQE. See ADR-1112.
Inputs and backends¶
| Property | Support |
|---|---|
| Backend | CPU (scalar) only in this release |
| Pixel formats | Any planar YUV (luma plane only; chroma ignored) |
| Bit depth | 8-bit fully supported; 10/12/16-bit feed raw luma (see Limitations) |
| Minimum frame size | ≥ 96 px in each dimension and ≥ 2 total 96×96 patches |
Limitations¶
- No-reference, SDR-trained: NIQE has no transfer-function awareness and scores raw luma. For PQ/HLG HDR content the NSS assumptions break down; the metric still produces a number but it is not calibrated for HDR.
- High bit depth: the pristine model was trained on 8-bit luma. For
8-bpc input the extractor feeds raw high-bit-depth values; because the MSCN
C = 1denominator is not scale-invariant, scores are not guaranteed to match the 8-bit-trained model. Convert to 8-bit (e.g. via the CLI's bit-depth handling) for calibrated results until an explicit scaling policy lands. - Upstream parity: scores match the fork Python harness, not reference LIVE/skvideo NIQE (see fork-specific divergences above).
- GPU twins: no CUDA/SYCL/HIP implementation yet; a future twin will keep the covariance + pinv in fp64 and is not expected to be bit-exact to CPU (consistent with the fork's GPU-parity posture, places = 4).
Regenerating the model header and score snapshot¶
core/src/feature/niqe_model.h is generated from the pristine .pkl. To regenerate it (and the score snapshot) after an intentional model change:
import pickle, numpy as np
with open("model/other_models/niqe_v0.1.pkl", "rb") as f:
d = pickle.load(f)
m = d["model_dict"]["model"]
mu, cov = np.asarray(m["mu"], float), np.asarray(m["cov"], float)
# Reorder the alphabetically-stored feature vector into the per-block
# interleaved order the C extractor emits:
atom = ["alpha_m1","blbr1","alpha11","N11","lsq11","rsq11","alpha12","N12",
"lsq12","rsq12","alpha13","N13","lsq13","rsq13","alpha14","N14",
"lsq14","rsq14","alpha_m2","blbr2","alpha21","N21","lsq21","rsq21",
"alpha22","N22","lsq22","rsq22","alpha23","N23","lsq23","rsq23",
"alpha24","N24","lsq24","rsq24"]
stored = [s.replace("NIQE_noref_feature_","").replace("_scores","")
for s in d["model_dict"]["feature_names"]]
perm = [stored.index(a) for a in atom]
mu_i, cov_i = mu[perm], cov[np.ix_(perm, perm)] # emit as %.17g into niqe_model.h
Build-time checksums asserted against the .pkl: mu.sum() = 21.0218816411, trace(cov) = 2.6622550083, mu sha256[:16] = f244e4a7538d6837, cov sha256[:16] = 75c0095bba8abc89.
Correctness test¶
core/test/test_niqe.c (run via meson test -C core/build-cpu test_niqe) asserts the gauss window, AGGD oracles, the bicubic resampler, the symmetric pseudo-inverse, and an end-to-end NIQE score against the fork Python-harness reference on frame 0 of testdata/ref_576x324_48f.yuv (testdata/scores_cpu_niqe.json) at places = 4.
See also¶
- ADR-1112: NIQE no-reference CPU feature extractor
- CAMBI — the other fork no-reference metric
- Features overview