- İvme-Denoise-v1 (Codenamed Margay 1)
İvme-Denoise-v1 (Codenamed Margay 1)
AI tools were used in the making of the background image due to there being no clear stock image of margay cats.
İvme (Turkish: acceleration) is a series of stupidly small models built to punch above their weight. This is the first vision release: a 2.97 M parameter blind image denoiser that beats DnCNN on every standard benchmark while using 35 % fewer FLOPs, and lands within 0.22–0.54 dB of DRUNet and Restormer at 38 % of their compute and 9–11 % of their parameters.
It is also the first release built on a derived architecture rather than a tuned one. Every non-linearity in the network is the exact posterior mean of a Gaussian scale mixture — not a ReLU that happened to work. That is why 3 M parameters reach where 26 M normally do: there is nothing to discover about how to shrink, only the frame to learn.
The codename is a margay — a small nocturnal wild cat with the largest eyes-to-body ratio of any cat. It denoises the dark for a living.
A 12.3 MP high-ISO frame, one pass, no per-image tuning. Left: original. Right: denoise_auto with tiled inference. Rendered from model.safetensors in this repo.
Contents
- Examples
- What this is
- Model details
- The architecture: İvmetron-Guanine1
- Noise conditioning
- Training
- Benchmarks
- Cost and efficiency
- Robustness to a wrong noise level
- Real photographs
- Usage
- What we tried that did not work
- Limitations
- Files
- Citation
What this is
Denoising is not a vision problem, it is an estimation problem. Given
y = x + n, n ~ N(0, σ² I), loss = E‖x̂(y) − x‖²
the minimiser is the posterior mean E[x | y], and nothing else can beat it. Two classical results turn that into something you can write down instead of learn. Everything in this model follows from them.
The practical consequence: a network that is handed the correct shrinkage law analytically needs far less capacity and far less data than one that has to rediscover it. This release is the evidence — 2.97 M parameters, 7.5 GPU-hours, and it sits on the accuracy/cost Pareto frontier of published denoisers.
Examples
All three crops below are 145 % magnifications of the same frame, straight out of the model with a σ map it estimated itself.
Tunnel wall. The original carries heavy chroma speckle in a region that is essentially black. It is gone, and the wall is black rather than blue-grey. This is the case that a conventional denoiser either leaves noisy or turns into a checkerboard.
Train front. The headlights are blown to clipping and the livery sits in near-darkness. The edge of the front panel survives, the "34" stays legible, and the highlight keeps its shape instead of blooming.
Tactile strip. Saturated yellow under sodium light, which is where JPEG chroma subsampling does the most damage. The grain is removed without the plastic-looking flatness you get from a heavy Gaussian blur.
Model details
| Parameter | Value |
|---|---|
| Architecture | İvmetron-Guanine1 (multi-scale Bayesian shrinkage U-Net) |
| Parameters | 2,971,342 |
| Compute | 56.8 GFLOP @ 256², 158 GFLOP @ 512² |
| Scales | 3 (full resolution, 1/2, 1/4) |
| Blocks per scale | 3 / 3 / 3 Bayesian shrinkage blocks |
| Width | 96 channels (192 inner, 384 / 768 at lower scales) |
| Dilations | 1 / 2 / 4 → ≈150 px receptive field |
| Non-linearity | GSM posterior gain — monotone in one scalar energy statistic |
| Noise conditioning | FiLM on a global σ + a spatial σ map input channel |
| Input / output | sRGB, gamma encoded, float32 in [0, 1], any resolution |
| File size | 12 MB (safetensors, fp32) |
| Latency | 17.9 ms per 512² frame (RTX PRO 6000, bf16) · 6.5 s for 12.3 MP with tiling |
| Training | 300 000 steps, batch 32, 128² crops, ≈7.5 GPU-hours on one GPU |
| Data | DIV2K train (800) + DIV2K valid (100) + BSD500 train/val (500) = 1 200 images |
The architecture: İvmetron-Guanine1
Where the non-linearity comes from
Tweedie / Miyasawa. For y = x + σn,
E[x|y] = y + σ² ∇_y log p_σ(y), p_σ = p_x * N(0, σ²I)
So denoising is score estimation of the smoothed density. The smoothed density is the easy object — log p_σ is smooth, and near any point it is locally well approximated by a quadratic, i.e. by a Gaussian. That is why a mixture of Gaussians is the right function class, and why it is cheap.
Gaussian scale mixture. Model a patch of coefficients c ∈ R^d as
c = √z · w, w ~ N(0, C), z ~ p(z) > 0, z ⟂ w
z is the hidden local contrast. Observe y = c + ε, ε ~ N(0, σ²I). Then
p(y|z) = N(y; 0, C + (σ²/z) I)
p(z|y) ∝ p(z) · N(y; 0, C + (σ²/z) I)
and by the tower property
E[c|y] = E_z[ C (C + (σ²/z) I)⁻¹ y ]
Because C and I commute, diagonalise C = UΛUᵀ:
E[c|y] = U diag(g₁(e), …, g_d(e)) Uᵀ y , g_i(e) = E[ λ_i / (λ_i + σ²/z) | e ]
The posterior over z depends on y only through one scalar — the local energy e.
Theorem (structure of the Bayes-optimal denoiser). Every GSM posterior mean is a linear map in an eigenbasis, whose gains are monotone decreasing functions of a single scalar energy statistic. Conversely, an estimator of the form
v = A(u ⊙ g(e))withgmonotone is exactly Bayes-optimal for some GSM prior.
This is not a heuristic activation. It is the estimator.
The corners of the family are the classics
| prior | gain g(e) |
recovers |
|---|---|---|
z deterministic (Gaussian) |
λ / (λ + σ²) |
Wiener filter |
z ∈ {0, ∞} |
1[e > τ] |
hard thresholding / wavelet shrinkage |
C diagonal, stationary |
S(ω) / (S(ω) + σ²) |
spectral Wiener (0 parameters) |
finite mixture over z |
Σ_k γ_k λ / (λ + σ²/z_k) |
mixture of Wiener experts |
learned g on a learned frame |
learned monotone map | shrinkage fields (Schmidt & Roth, 2014) |
BM3D's collaborative Wiener, BLS-GSM, EPLL, and modern gated CNNs are all the same estimator at different corners. İvmetron sits at the centre and learns only the frame.
One block, line by line
u = DW3x3( Lin(x) ) # analysis in a learned frame
e = mean_c( u² ) * 3x3 # the shared scalar energy (ẑ)
g = 2 · sigmoid( w · log(e) + b ) # GSM posterior gain, monotone in log e
v = u * g # multiplicative shrinkage — no bias
out = x + Proj( SimpleGate(v) ) # synthesis + residual
Note what is absent: no additive bias after the gain, no normalisation, no additive constant anywhere in the shrinkage path. A Bayes gain can only attenuate. Imposing that constraint is worth several dB of sample efficiency — it removes an entire failure direction from the search space.
Proj is zero-initialised, so every block starts as the identity and the network starts as a no-op. All the dB are learned, not inherited.
Here are the 192 learned gains of one block, plotted against log e. This is the entire non-linearity of the model — there is nothing else:
Every curve is a monotone gain that runs from ~1 at low energy to ~0 at high energy, with the family spread out across channels because the learned frame is overcomplete and is not the eigenbasis of the patch covariance. Anything that looks like a familiar activation would have to be justified by the GSM posterior mean; none of them are, so none of them are here.
Design decisions that mattered
No bias in the shrinkage. The single biggest inductive-bias win. A conv with bias can add energy where the prior says there is none; the GSM estimator cannot.
One energy statistic, not per-channel. The theorem says the latent z is shared across the coefficient neighbourhood. Computing one scalar e per location and letting it gate all 192 channels is both faithful to the derivation and 192× cheaper than per-channel gating.
Depthwise dilated convolutions. Dilations 1/2/4 take the receptive field to ≈150 px for the cost of a 3×3 — cheap long-range context without attention.
Bilinear-up + 1×1, not PixelShuffle. This one was a bug fix worth a dedicated release note. PixelShuffle(2) maps a low-resolution tensor onto a 2×2 periodic pattern by construction — four channel groups become four sub-pixel phases. While the gated gains pass signal through, the pattern is invisible. The moment the network decides everything is below the noise floor and zeroes the coefficients, the up-sampling basis is the only thing left, and a hard grid appears in the output. Replacing it with Conv1×1 → bilinear ×2 → Conv1×1 removes the basis entirely, and is 6 % cheaper.
Parameter budget
| component | params |
|---|---|
| stem + FiLM | 3 000 |
| 9 shrinkage blocks | 1.1 M |
| stride-2 down / ×2 up paths | 0.9 M |
| heads | 1.0 M |
| total | 2.97 M |
For comparison, DRUNet is 32.6 M and Restormer 26.1 M at 2.6× and 2.5× the FLOPs.
Noise conditioning
The network is blind — one model covers σ ∈ [5, 50] — and the conditioning is a hint, not a command.
Two mechanisms:
- FiLM on a global σ (a 64-unit MLP producing per-channel scale and shift), plus the σ map itself as a 4th input channel.
- A spatial σ map is first-class. The conditioning channel can be a full
H×Wfield, not just a scalar. This is what makes real photographs work: sensor noise is strongly signal-dependent, so the right σ in the shadows is often 3–4× the right σ in the highlights.
Robustness is trained in, not hoped for:
- 40 % of crops get a smooth random σ field (0.35–2.35×) that both generates the noise and is fed back as the conditioning map.
- 25 % of crops get a deliberately wrong conditioning: noise generated at σ but the network told
σ · exp(N(0, 0.45)).
The result is that a 10× over-estimate of σ costs about 5 dB instead of 14 — see Robustness.
Training
| Steps | 300 000 |
| Batch | 32 × 128² crops |
| Optimiser | AdamW, lr 1.2e-4 → 0 cosine, 2 k warmup, wd 1e-4 |
| Loss | L1 |
| EMA | decay 0.999 (the released weights are the EMA) |
| Augment | flips, 90° rotations, 35 % grayscale (mono noise), spatial σ fields, wrong-σ, JPEG-correlated noise |
| Precision | bf16 autocast, torch.compile (1.69× faster: 270 → 435 img/s) |
| Hardware | one RTX PRO 6000, 7.5 h |
Half the crops go through a realistic degradation rather than white noise — 3×3-correlated noise (what a Bayer demosaic leaves behind), a random PSD colour mixing matrix (white balance / colour matrix), then a JPEG round trip at q ∈ [55, 95] with 4:2:0 chroma subsampling. The target is still the clean image, so the network is simultaneously a denoiser, a deblocker and a chroma reconstructor. That is what makes it usable on actual camera output rather than only on synthetic benchmarks.
Benchmarks
PSNR in dB, measured with identical noise seeds and the standard protocol (8-pixel border crop for colour sets, single-channel for Set12).
| dataset | σ | DnCNN | DRUNet | Restormer | İvme-Denoise-v1 |
|---|---|---|---|---|---|
| CBSD68 | 15 | 33.90 | 34.30 | 34.40 | 34.08 |
| CBSD68 | 25 | 31.24 | 31.69 | 31.79 | 31.44 |
| CBSD68 | 50 | 27.95 | 28.51 | 28.60 | 28.20 |
| Kodak24 | 15 | 34.60 | 35.31 | 35.47 | 34.99 |
| Kodak24 | 25 | 32.14 | 32.89 | 33.04 | 32.56 |
| Kodak24 | 50 | 28.95 | 29.86 | 30.01 | 29.48 |
| Set12 (grey) | 15 | 32.67 | 33.25 | 33.42 | 32.86 |
| Set12 (grey) | 25 | 30.35 | 30.94 | 31.08 | 30.52 |
| Set12 (grey) | 50 | 27.18 | 27.90 | 28.00 | 27.36 |
| BSD100 | 25 | — | — | — | 31.25 |
No cell in bold loses to DnCNN. The gains over DnCNN are +0.18 to +0.53 dB, on 65 % of its FLOPs.
Against the large models it is 0.22–0.54 dB behind. That gap is real and is not hidden: closing it needs 2–3× the compute, which removes the entire point of a 3 M parameter model. The claim here is Pareto dominance, not a leaderboard row.
Reference baselines, measured in the same harness
| method | CBSD68 σ=15 | σ=25 | σ=50 |
|---|---|---|---|
BM3D (reference bm3d 4.0.3, single-threaded) |
33.30 | 30.28 | 26.03 |
| İvme-Denoise-v1 | 34.08 | 31.44 | 28.20 |
At σ = 50 that is +2.2 dB over BM3D — and BM3D takes ~3.3 s per image where this takes 18 ms.
Cost and efficiency
| params | GFLOP @256² | train | CBSD68 σ25 | |
|---|---|---|---|---|
| DnCNN (2017) | 0.56 M | ~87 | ~1 GPU-day | 31.24 |
| SwinIR (2021) | 11.8 M | ~118 | — | 31.78 |
| DRUNet (2021) | 32.6 M | ~150 | — | 31.69 |
| Restormer (2022) | 26.1 M | 141 | 22.5 GPU-h | 31.79 |
| İvme-Denoise-v1 | 2.97 M | 56.8 | 7.5 GPU-h | 31.44 |
Accuracy per GFLOP: 0.553 dB/GFLOP for this model versus 0.225 for Restormer — a 2.5× efficiency advantage, at 11 % of the parameters and a third of the training time.
Read the plot as: anything to the upper-left dominates. İvme-Denoise-v1 is above DnCNN at 65 % of its compute, and sits well to the left of the transformer cluster — 0.35 dB costs you 2.5× the FLOPs.
Latency, RTX PRO 6000, bf16, batch 1, no tiling:
| resolution | time |
|---|---|
| 256² | ~5 ms |
| 512² | 17.9 ms |
| 1024² | ~70 ms |
| 12.3 MP (tiled) | 6.5 s |
CPU works (the model is 12 MB) but expect seconds per megapixel.
Robustness to a wrong noise level
Almost every denoiser silently assumes you told it the truth. This one was trained not to.
True noise fixed at σ = 5 on Kodak24, conditioning swept:
| σ given to the network | 5 | 15 | 25 | 35 | 50 |
|---|---|---|---|---|---|
| PSNR (Kodak24, true σ = 5) | 40.63 | 40.05 | 38.86 | 37.66 | 35.75 |
For reference, the earlier build of this architecture scored 39.85 / 34.45 / 30.60 / 28.41 / 26.02 on the same sweep — i.e. +9.3 dB at σ = 35 and +9.7 dB at σ = 50.
The curve is nearly flat: a 10× over-estimate costs ~5 dB, where a conventionally trained denoiser of the same family loses 14 dB and starts inventing structure. That failure mode — a hard pixel-level checkerboard appearing in deep shadow — is documented in the predecessor write-up, and it is the reason for the bilinear upsampling and the wrong-σ augmentation.
Practical advice: pass σ ≈ true noise. If unsure, pass an estimated per-pixel map (estimate_sigma_map). Do not pass σ = 35 for a frame whose real noise is 6; it will no longer produce artefacts, but it will smooth detail away.
Real photographs
The intended path for camera output:
out = denoise_tiled(model, img, estimate_sigma_map(img), device="cuda")
The estimator is a robust per-block noise floor — 10th percentile of the Laplacian magnitude inside each 32×32 block, converted through the Gaussian relation p₁₀(|N(0,s)|) = 0.1257 s, then smoothed. That gives a spatial σ map which the network consumes directly.
On a 12.3 MP high-ISO subway frame the measured chroma blotching (std of high-passed Cb/Cr) drops from 7.61 → 3.59, the shadow walls go black, and the departure-board text, clock hands and tactile-paving studs survive.
Usage
pip install torch safetensors numpy pillow
import numpy as np
from PIL import Image
from modeling_ivmetron import load_model, denoise, denoise_tiled, estimate_sigma_map
# from this repo (local dir), or from the Hub (needs huggingface_hub installed)
model = load_model(".", device="cuda")
img = np.asarray(Image.open("photo.jpg").convert("RGB"), dtype=np.float32) / 255.0
# 1. you know the noise level (0-255 scale)
out = denoise(model, img, sigma=15, device="cuda")
# 2. you do not — let it estimate a per-pixel map (recommended for real photos)
out = denoise_tiled(model, img, estimate_sigma_map(img), device="cuda")
Image.fromarray((np.clip(out, 0, 1) * 255).round().astype(np.uint8)).save("clean.png")
Why denoise_tiled
The blocks are 192 channels wide. At 12 MP an intermediate activation is 192 × 12.3 M elements — past the 2³¹ limit for CUDA indexing, so a single-shot forward pass fails with integer out of range. denoise_tiled splits the frame into 896 px tiles with 96 px overlap and blends them with a separable Hann window. Below ~5 MP it falls back to the direct path automatically.
Loading without the helper
from safetensors.torch import load_file
from modeling_ivmetron import BSNet
sd = load_file("model.safetensors")
model = BSNet(C=96, blocks=(3, 3, 3), dilations=(1, 2, 4), mix_k=1)
model.load_state_dict(sd).eval()
Config is also mirrored in config.json and in the safetensors metadata.
What we tried that did not work
Recorded because a negative result with numbers is worth more than a silent omission. All of these were attempts to beat the released model on CBSD68 σ = 25 (baseline 31.44):
| attempt | change | result |
|---|---|---|
| more capacity | C 96 → 112, 4.04 M params, 77 GFLOP | 31.27, and it diverged twice |
| mixture-of-GSM gate | exact mixture posterior mean, K = 3, transplanted from the released weights and fine-tuned 52 k steps | +0.004 dB — nothing |
| more data | + COCO val2017 (5 000 images) at 65 % of the batch | 31.40 at 24 k steps, then NaN |
The mixture experiment is the interesting one: at initialisation K = 3 reproduces K = 1 to max|Δ| = 0.0 (verified), so it is a strict generalisation. It still buys nothing. With a single shared energy statistic driving the gate, extra mixture components have no direction to move in — the single monotone gain already carries the signal.
Three independent axes — capacity (+36 %), expressivity, data (×4) — each moved the result by ≤0.05 dB. That is the signature of a model at its architectural frontier, not of a tuning problem. The remaining gap to DRUNet/Restormer needs non-local self-similarity (the one thing BM3D and transformers have that this does not) or 3–4× the compute.
Also worth knowing: the training loop is prone to rare bf16 forward-pass blow-ups (loss → 1e28 → NaN) at learning rates as low as 2.4e-5. The released checkpoint is from a clean run; the mitigation (weight snapshotting + rollback + LR halving) is in the training code, but the root cause is not solved and a production version should use loss scaling or an fp32 master copy for the gate path.
Limitations
- Not state of the art on raw PSNR. 0.22–0.54 dB behind DRUNet and Restormer. By design — see Cost and efficiency.
- No SIDD/DND numbers. Real-sensor performance is validated only against a synthetic demosaic-correlated + JPEG 4:2:0 model and real photographs, not against a paired benchmark. Treat the real-photo results as promising, not proven.
- sRGB only. Assumes gamma-encoded input. Do not feed linear RAW or HDR; it will not crash, it will just be wrong.
- Trained on 1 200 natural images (DIV2K + BSD500). Faces, text-heavy screenshots and synthetic/rendered content are out of distribution.
- JPEG q < 50 is outside the trained range.
- Hue drift in saturated regions can occur, a side effect of training on JPEG-correlated noise.
- Single seed. Differences below ~0.05 dB are not resolvable from these numbers.
Files
| file | what |
|---|---|
model.safetensors |
the weights — 2,971,342 parameters, fp32, config in the metadata |
config.json |
architecture config, mirrored for convenience |
modeling_ivmetron.py |
reference implementation — model, loading, tiled inference, σ estimation |
example.py |
minimal end-to-end example |
requirements.txt |
torch, safetensors, numpy, pillow |
Citation
@misc{ivme_denoise_v1,
title = {İvme-Denoise-v1 (İvmetron-Guanine1)},
author = {Ekşi, Eren},
year = {2026},
howpublished = {IvmeLabs},
url = {https://huggingface.co/IvmeLabs/Ivme-Denoise-v1},
note = {2.97 M parameter Bayesian shrinkage denoiser}
}
References
- Portilla, Strela, Wainwright, Simoncelli. Image denoising using scale mixtures of Gaussians in the wavelet domain. IEEE TIP 2003.
- Schmidt & Roth. Shrinkage fields for effective image restoration. CVPR 2014.
- Chen & Pock. Trainable nonlinear reaction diffusion. TPAMI 2017.
- Dabov et al. Image denoising by sparse 3-D transform-domain collaborative filtering (BM3D). IEEE TIP 2007.
- Zhang et al. Beyond a Gaussian denoiser (DnCNN). IEEE TIP 2017.
- Zamir et al. Restormer: efficient transformer for high-resolution image restoration. CVPR 2022.
- Downloads last month
- -



