<!-- DO NOT EDIT. -->
<!-- THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. -->
<!-- TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: -->
<!-- "auto_examples/metrics/demo_custom_niqe.py" -->
<!-- LINE NUMBERS ARE GIVEN BELOW. -->

#### NOTE
New to DeepInverse? Get started with the basics with the
[5 minute quickstart tutorial](https://deepinv.org/auto_examples/basics/demo_quickstart.html.md#sphx-glr-auto-examples-basics-demo-quickstart-py)..

<a id="sphx-glr-auto-examples-metrics-demo-custom-niqe-py"></a>

# Fitting NIQE on a custom dataset

This example shows how to fit [`deepinv.loss.metric.NIQE`](https://deepinv.org/api/stubs/deepinv.loss.metric.NIQE.html.md#deepinv.loss.metric.NIQE) on a new dataset, and use it
to evaluate denoiser performance.

NIQE is a no-reference image quality metric that compares local image statistics
against those of pristine (distortion-free) images. Fitting NIQE on a domain-specific
dataset can better capture the expected image characteristics.

In this example, we fit NIQE on DIV2K. DIV2K is also natural imaging data, but the image
quality and sharpness is higher than the dataset NIQE was originally fitted on, so the
resulting weights characterise a sharper, higher-quality prior while remaining valid NIQE
statistics.

To apply this procedure to your own data, any dataset returning RGB or single-channel
tensors will work. The `denominator` constructor argument divides input pixels before
computing statistics; it serves two purposes: keeping pixel magnitudes from dominating
the local-statistics computation, and matching the input scale to the scale the weights
were fitted on. Two consequences:

- When using the bundled original NIQE weights (which were fitted on [0, 255] data with
  `denominator=1`), inputs must reach NIQE on a comparable [0, 255] scale. So for
  [0, 1] data, pass `denominator=1/255` (`x / (1/255) = 255 * x`), or scale to
  [0, 255] before calling and leave `denominator=1`.
- When fitting your own weights, the only requirement is that the *same* `denominator`
  is used at fit and evaluation time. The absolute scale is up to you.

In this example we want to compare the original and DIV2K-fitted weights on the same
inputs, so we keep both on the [0, 255] scale: the fitting transform multiplies by 255
(default `denominator=1` at fit time), and at evaluation we scale the denoised [0, 1]
outputs to [0, 255] before passing them to either NIQE instance.

We perform 5-fold cross-validation on the DIV2K validation set (80 fit / 20 test per fold)
and compare original NIQE weights against DIV2K-fitted weights at noise level σ=0.05.
A key finding is that the DIV2K-fitted NIQE assigns systematically higher (worse) scores
to over-smoothed outputs (e.g. large median filters), reflecting that it is more sensitive
to the loss of fine texture detail captured in the DIV2K prior.

<!-- GENERATED FROM PYTHON SOURCE LINES 43-45 -->

## Setup

<!-- GENERATED FROM PYTHON SOURCE LINES 45-58 -->
```Python
import deepinv as dinv
from deepinv.utils import plot
import torch
import numpy as np
import matplotlib.pyplot as plt
from torch.utils.data import Subset
from torchvision.transforms import Compose, ToTensor, CenterCrop, Lambda
from natsort import natsorted

device = dinv.utils.get_device()
```

```none
Selected GPU 0 with 15235.25 MiB free memory
```

<!-- GENERATED FROM PYTHON SOURCE LINES 60-65 -->

## Define transforms and load DIV2K

We create two instances of DIV2K with different transforms:
one that scales pixel values to [0, 255] for fitting NIQE weights,
and one that keeps values in [0, 1] for denoising.

<!-- GENERATED FROM PYTHON SOURCE LINES 65-97 -->
```Python
crop_size = 1024

fit_transform = Compose(
    [
        ToTensor(),
        CenterCrop(crop_size),
        Lambda(lambda x: x * 255),
    ]
)

test_transform = Compose(
    [
        ToTensor(),
        CenterCrop(crop_size),
    ]
)

div2k_fit = dinv.datasets.DIV2K(
    root=dinv.utils.get_data_home(), mode="val", download=True, transform=fit_transform
)
div2k_fit.x_paths = natsorted(div2k_fit.x_paths)
div2k_test = dinv.datasets.DIV2K(
    root=dinv.utils.get_data_home(),
    mode="val",
    download=False,
    transform=test_transform,
)
div2k_test.x_paths = natsorted(div2k_test.x_paths)
n_images = len(div2k_fit)
all_indices = list(range(n_images))
```

```none
/local/jtachell/deepinv/deepinv/examples/metrics/demo_custom_niqe.py:84: DeprecationWarning: Function 'get_data_home' is deprecated and will be removed in a future version.
  root=dinv.utils.get_data_home(), mode="val", download=True, transform=fit_transform

  0%|          | 0/448993893 [00:00<?, ?it/s]
  1%|▏         | 6.38M/428M [00:00<00:06, 66.8MB/s]
  4%|▍         | 17.1M/428M [00:00<00:04, 93.2MB/s]
  6%|▋         | 27.7M/428M [00:00<00:04, 101MB/s]
  9%|▉         | 38.2M/428M [00:00<00:03, 105MB/s]
 11%|█▏        | 48.7M/428M [00:00<00:03, 106MB/s]
 14%|█▍        | 59.2M/428M [00:00<00:03, 108MB/s]
 16%|█▋        | 69.9M/428M [00:00<00:03, 109MB/s]
 19%|█▉        | 80.8M/428M [00:00<00:03, 110MB/s]
 21%|██▏       | 91.5M/428M [00:00<00:03, 111MB/s]
 24%|██▍       | 102M/428M [00:01<00:03, 111MB/s]
 26%|██▋       | 113M/428M [00:01<00:02, 112MB/s]
 29%|██▉       | 124M/428M [00:01<00:02, 112MB/s]
 31%|███▏      | 135M/428M [00:01<00:02, 112MB/s]
 34%|███▍      | 146M/428M [00:01<00:02, 113MB/s]
 37%|███▋      | 156M/428M [00:01<00:02, 113MB/s]
 39%|███▉      | 167M/428M [00:01<00:02, 113MB/s]
 42%|████▏     | 178M/428M [00:01<00:02, 113MB/s]
 44%|████▍     | 189M/428M [00:01<00:02, 114MB/s]
 47%|████▋     | 200M/428M [00:01<00:02, 114MB/s]
 49%|████▉     | 211M/428M [00:02<00:02, 114MB/s]
 52%|█████▏    | 222M/428M [00:02<00:01, 114MB/s]
 54%|█████▍    | 233M/428M [00:02<00:01, 113MB/s]
 57%|█████▋    | 243M/428M [00:02<00:01, 113MB/s]
 59%|█████▉    | 254M/428M [00:02<00:01, 113MB/s]
 62%|██████▏   | 265M/428M [00:02<00:01, 113MB/s]
 64%|██████▍   | 276M/428M [00:02<00:01, 113MB/s]
 67%|██████▋   | 287M/428M [00:02<00:01, 113MB/s]
 70%|██████▉   | 298M/428M [00:02<00:01, 113MB/s]
 72%|███████▏  | 309M/428M [00:02<00:01, 114MB/s]
 75%|███████▍  | 319M/428M [00:03<00:01, 114MB/s]
 77%|███████▋  | 330M/428M [00:03<00:00, 114MB/s]
 80%|███████▉  | 341M/428M [00:03<00:00, 113MB/s]
 82%|████████▏ | 352M/428M [00:03<00:00, 113MB/s]
 85%|████████▍ | 363M/428M [00:03<00:00, 113MB/s]
 87%|████████▋ | 374M/428M [00:03<00:00, 113MB/s]
 90%|████████▉ | 385M/428M [00:03<00:00, 113MB/s]
 92%|█████████▏| 396M/428M [00:03<00:00, 113MB/s]
 95%|█████████▍| 406M/428M [00:03<00:00, 114MB/s]
 97%|█████████▋| 417M/428M [00:03<00:00, 114MB/s]
100%|█████████▉| 428M/428M [00:04<00:00, 114MB/s]
100%|██████████| 428M/428M [00:04<00:00, 112MB/s]

Extracting:   0%|          | 0/101 [00:00<?, ?it/s]
Extracting:   9%|▉         | 9/101 [00:00<00:01, 89.25it/s]
Extracting:  18%|█▊        | 18/101 [00:00<00:00, 83.18it/s]
Extracting:  27%|██▋       | 27/101 [00:00<00:00, 77.16it/s]
Extracting:  35%|███▍      | 35/101 [00:00<00:01, 63.66it/s]
Extracting:  43%|████▎     | 43/101 [00:00<00:00, 67.73it/s]
Extracting:  50%|█████     | 51/101 [00:00<00:00, 67.75it/s]
Extracting:  58%|█████▊    | 59/101 [00:00<00:00, 70.41it/s]
Extracting:  66%|██████▋   | 67/101 [00:00<00:00, 66.96it/s]
Extracting:  75%|███████▌  | 76/101 [00:01<00:00, 71.78it/s]
Extracting:  83%|████████▎ | 84/101 [00:01<00:00, 71.03it/s]
Extracting:  91%|█████████ | 92/101 [00:01<00:00, 67.33it/s]
Extracting:  98%|█████████▊| 99/101 [00:01<00:00, 63.64it/s]
Extracting: 100%|██████████| 101/101 [00:01<00:00, 68.82it/s]
Dataset has been successfully downloaded.
/local/jtachell/deepinv/deepinv/deepinv/datasets/base.py:495: DeprecationWarning: The tuple format for dataset outputs is deprecated and will be removed in a future version.It is recommended to set `use_dict_output=True` for better readability and flexibility in returned outputs.The default is currently `False` for backward compatibility, but will be switched to `True` in a future version.
  super().__init__(use_dict_output=use_dict_output)
/local/jtachell/deepinv/deepinv/examples/metrics/demo_custom_niqe.py:88: DeprecationWarning: Function 'get_data_home' is deprecated and will be removed in a future version.
  root=dinv.utils.get_data_home(),
```

<!-- GENERATED FROM PYTHON SOURCE LINES 98-103 -->

## Define denoisers

We compare DRUNet against MedianFilter at two kernel sizes. At inference time we wrap
each call in `torch.autocast(..., dtype=torch.float16)` so DRUNet fits in GPU memory
at the 1024×1024 crop size used here.

<!-- GENERATED FROM PYTHON SOURCE LINES 103-110 -->
```Python
denoisers = {
    "DRUNet": dinv.models.DRUNet(pretrained="download", device=device),
    "Median (k=6)": dinv.models.MedianFilter(kernel_size=6),
    "Median (k=9)": dinv.models.MedianFilter(kernel_size=9),
}
```

<!-- GENERATED FROM PYTHON SOURCE LINES 111-116 -->

## Load original NIQE weights

Constructing [`deepinv.loss.metric.NIQE`](https://deepinv.org/api/stubs/deepinv.loss.metric.NIQE.html.md#deepinv.loss.metric.NIQE) without an explicit `weights_path`
loads the original published NIQE weights bundled with the package. We use this
instance as the baseline for comparison against our custom-fitted weights.

<!-- GENERATED FROM PYTHON SOURCE LINES 116-119 -->
```Python
niqe_original = dinv.loss.metric.NIQE(device="cpu")
```

<!-- GENERATED FROM PYTHON SOURCE LINES 120-132 -->

## Fit NIQE and save/load weights

To fit NIQE on a custom dataset, we construct a NIQE instance with `weights_path=None`
(which skips loading the bundled weights, as they would be overwritten anyway) and call
[`deepinv.loss.metric.NIQE.create_weights()`](https://deepinv.org/api/stubs/deepinv.loss.metric.NIQE.html.md#deepinv.loss.metric.NIQE.create_weights) with a dataset of pristine images.
This populates the instance’s statistics in-place. The same object can then be called
directly to score new images.

To persist the fitted weights for reuse, pass `save_path="my_weights.pt"` to
`create_weights`. In a later session, load them back via
`NIQE(weights_path="my_weights.pt")`. Here we do not save: weights are computed
on each fold of the cross-validation below.

<!-- GENERATED FROM PYTHON SOURCE LINES 132-141 -->
```Python
def fit_niqe(fit_subset: Subset) -> dinv.loss.metric.NIQE:
    print(f"  Fitting NIQE on {len(fit_subset)} images...")
    niqe = dinv.loss.metric.NIQE(weights_path=None, device="cpu")
    niqe.create_weights(fit_subset)
    return niqe
```

<!-- GENERATED FROM PYTHON SOURCE LINES 142-147 -->

## Run 5-fold cross-validation

We split the 100 DIV2K validation images into 5 folds of 20 images each.
For each fold, we fit NIQE on the remaining 80 images and evaluate on the
held-out 20.

<!-- GENERATED FROM PYTHON SOURCE LINES 147-185 -->
```Python
sigma = 0.05
fold_size = n_images // 5
results = {
    denoiser_name: {"original_niqe": [], "div2k_niqe": []}
    for denoiser_name in denoisers.keys()
}
torch.manual_seed(16 * 16)
for fold in range(5):
    print(f"Fold {fold + 1} / 5")

    test_indices = all_indices[fold * fold_size : (fold + 1) * fold_size]
    fit_indices = (
        all_indices[: fold * fold_size] + all_indices[(fold + 1) * fold_size :]
    )

    fit_subset = Subset(div2k_fit, fit_indices)
    test_subset = Subset(div2k_test, test_indices)

    niqe_fitted = fit_niqe(fit_subset)

    for i, img in enumerate(test_subset):
        img = img.unsqueeze(0)
        noisy = img + sigma * torch.randn_like(img)

        images = {}
        with (
            torch.no_grad(),
            torch.autocast(device_type=device.type, dtype=torch.float16),
        ):
            for name, denoiser in denoisers.items():
                images[name] = denoiser(noisy.to(device), sigma).cpu()

        for name, im in images.items():
            im_255 = im.to(torch.float32) * 255
            results[name]["original_niqe"].append(float(niqe_original(im_255)))
            results[name]["div2k_niqe"].append(float(niqe_fitted(im_255)))
```

```none
Fold 1 / 5
  Fitting NIQE on 80 images...
Fold 2 / 5
  Fitting NIQE on 80 images...
Fold 3 / 5
  Fitting NIQE on 80 images...
Fold 4 / 5
  Fitting NIQE on 80 images...
Fold 5 / 5
  Fitting NIQE on 80 images...
```

<!-- GENERATED FROM PYTHON SOURCE LINES 186-198 -->

## Scatter plot: original vs DIV2K-fitted NIQE

Each point represents one test image, coloured by method. The x-axis shows the
score under original weights and the y-axis shows the score under DIV2K-fitted weights.
Points above the identity line are penalised *more* by the DIV2K prior.

The median filters’ NIQE score have a systematic upward shift: the DIV2K prior,
fitted on higher-quality natural images, is more sensitive to over-smoothing and
penalises the blurring introduced by large median filters more strongly than the
original weights fitted on lower-quality natural images.

DRUNet introduces less smoothing and has a less systematic shift.

<!-- GENERATED FROM PYTHON SOURCE LINES 198-232 -->
```Python
fig, ax = plt.subplots(figsize=(9, 6))

all_orig, all_div2k = [], []
print(
    "Average relative change by utilizing DIV2K fitted NIQE instead of original NIQE:"
)
for name in denoisers.keys():
    x = np.array(results[name]["original_niqe"])
    y = np.array(results[name]["div2k_niqe"])
    avg_relative_shift = np.mean((y - x) / x)
    print(f"{name}: {float(avg_relative_shift) * 100:.3f} %")
    mask = np.isfinite(x) & np.isfinite(y)
    x, y = x[mask], y[mask]
    all_orig.append(x)
    all_div2k.append(y)
    # Matplotlib doesn't automatically cycle through markers with `scatter``, but does with `plot`
    ax.plot(x, y, linestyle="none", markersize=5, label=name, alpha=0.8)

all_orig = np.concatenate(all_orig)
all_div2k = np.concatenate(all_div2k)
lim_min = min(all_orig.min(), all_div2k.min())
lim_max = max(all_orig.max(), all_div2k.max())
ax.plot([lim_min, lim_max], [lim_min, lim_max], "k--", linewidth=1, label="identity")

ax.set_xlabel("NIQE with original weights")
ax.set_ylabel("NIQE with DIV2K-fitted weights")
ax.set_title(
    f"Per-image NIQE scores ($\\sigma$ = {sigma})\nPoints above the line are penalised more by the DIV2K prior"
)
ax.legend()
plt.tight_layout()
plt.show()
```

```none
Average relative change by utilizing DIV2K fitted NIQE instead of original NIQE:
DRUNet: -0.278 %
Median (k=6): 10.488 %
Median (k=9): 14.334 %
```

<!-- GENERATED FROM PYTHON SOURCE LINES 233-236 -->

## Visual comparison between different denoisers

Finally, we visually confirm the blurring introduced by the median filters, which is absent in the ground-truth and DRUNet outputs.

<!-- GENERATED FROM PYTHON SOURCE LINES 236-252 -->
```Python
methods_all = ["gt", "noisy"] + list(denoisers.keys())
c = crop_size // 2
sample_img = div2k_test[5][:, c - 128 : c + 128, c - 128 : c + 128].unsqueeze(0)
sample_noisy = sample_img + sigma * torch.randn_like(sample_img)
images = {"gt": sample_img, "noisy": sample_noisy}
with torch.no_grad(), torch.autocast(device_type=device.type, dtype=torch.float16):
    for name, denoiser in denoisers.items():
        images[name] = denoiser(sample_noisy.to(device), 0.05).cpu()
plot(
    [images[m] for m in methods_all],
    titles=methods_all,
    vmin=0,
    vmax=1,
    rescale_mode="clip",
)
```

**Total running time of the script:** (1 minutes 4.459 seconds)

🏷 Tags: Denoising

<a id="sphx-glr-download-auto-examples-metrics-demo-custom-niqe-py"></a>

[`Download Jupyter notebook: demo_custom_niqe.ipynb`](https://deepinv.org/auto_examples/metrics/_downloads/3ba136e423d0890a98d52cc90a3e1723/demo_custom_niqe.ipynb)

[`Download Python source code: demo_custom_niqe.py`](https://deepinv.org/auto_examples/metrics/_downloads/be6513253fff53e5df8582e711c20b10/demo_custom_niqe.py)

[`Download zipped: demo_custom_niqe.zip`](https://deepinv.org/auto_examples/metrics/_downloads/4790ca9be3781471d4c2685b014e9ec7/demo_custom_niqe.zip)

[Gallery generated by Sphinx-Gallery](https://sphinx-gallery.github.io)
