Apply Photometric Perturbations#

This notebook demonstrates how to apply photometric perturbations, which are modifications to pixel intensity, color, and brightness properties, using NRTK.

We use a sample image from the VisDrone dataset to demonstrate noise, blur, and enhancement perturbations.

To run this notebook in Colab, use the link below:

Open In Colab

Set Up the Environment#

Note

We are suppressing warnings within this notebook to reduce visual clutter for demonstration purposes. If any issues arise while executing this notebook, we recommend that this cell is not executed so that any related warnings are shown.

import warnings

warnings.filterwarnings("ignore")

Important

This notebook requires NRTK with the following extra(s):

  • headless or graphics: OpenCV headless or graphics for blur perturbations

  • Pillow: PIL for enhancement perturbations (brightness, color, contrast, sharpness)

  • skimage: scikit-image for noise perturbations

The next cell will install these extra(s). See Installation for alternatives.

%pip install -qU pip
print("Installing nrtk with required extras...")
%pip install -q "nrtk[headless,pillow,skimage]"
print("Installing notebook-specific packages...")
%pip install -q matplotlib
print("Done!")

from nrtk.utils._extras import print_extras_status  # noqa: E402 - intentionally after %pip install

print_extras_status()
Note: you may need to restart the kernel to use updated packages.
Installing nrtk with required extras...
Note: you may need to restart the kernel to use updated packages.
Installing notebook-specific packages...
Note: you may need to restart the kernel to use updated packages.
Done!
Detected status of NRTK extras and their dependencies:
[albumentations]
  - nrtk-albumentations       ✗ missing

[diffusion]
  - torch                     ✗ missing
  - diffusers                 ✗ missing
  - accelerate                ✗ missing
  - Pillow                    ✓ 12.1.1
  - transformers              ✗ missing
  - protobuf                  ✗ missing

[graphics]
  - opencv-python             ✗ missing

[headless]
  - opencv-python-headless    ✓ 4.13.0.92

[maite]
  - maite                     ✗ missing

[pillow]
  - Pillow                    ✓ 12.1.1

[pybsm]
  - pybsm                     ✗ missing

[skimage]
  - scikit-image              ✗ missing

[tools]
  - kwcoco                    ✗ missing
  - Pillow                    ✓ 12.1.1
  - click                     ✓ 8.3.1
  - fastapi                   ✗ missing
  - uvicorn                   ✗ missing
  - pydantic                  ✗ missing
  - pydantic-settings         ✗ missing
  - python-json-logger        ✗ missing

[waterdroplet]
  - scipy                     ✓ 1.17.1
  - numba                     ✗ missing


For details about installing NRTK extras, please visit:
    https://nrtk.readthedocs.io/en/stable/

Note

Colab users: After setting up the environment, you may need to “Restart Runtime” in order to resolve package version conflicts (see the README for more info).

%matplotlib inline
%config InlineBackend.figure_format = "jpeg"  # Use JPEG format for inline visualizations
import os
import urllib.request
from typing import TYPE_CHECKING, Any

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.axes import Axes
from PIL import Image

from nrtk.impls.perturb_image.photometric.blur import AverageBlurPerturber, GaussianBlurPerturber, MedianBlurPerturber
from nrtk.impls.perturb_image.photometric.enhance import (
    BrightnessPerturber,
    ColorPerturber,
    ContrastPerturber,
    SharpnessPerturber,
)
from nrtk.impls.perturb_image.photometric.noise import (
    GaussianNoisePerturber,
    PepperNoisePerturber,
    SaltAndPepperNoisePerturber,
    SaltNoisePerturber,
    SpeckleNoisePerturber,
)

Select Initial Image#

We’ll carry out perturbations on a single image from VisDrone.

data_dir = "./data"
os.makedirs(data_dir, exist_ok=True)

url = "https://data.kitware.com/api/v1/item/623880f14acac99f429fe3ca/download"

img_path = os.path.join(data_dir, "visdrone_img.jpg")
if not os.path.isfile(img_path):
    _ = urllib.request.urlretrieve(url, img_path)  # noqa: S310

img = np.asarray(Image.open(img_path))

plt.figure(figsize=(8, 8))
plt.axis("off")
_ = plt.imshow(img)
../_images/9bd62d4b1c4fcfe18c160e862d6921939ed9816faef2af158112d33f2a37bb94.jpg

We’ll also define a couple of helper functions for displaying our perturbations.

def display_pert(img: np.ndarray, descriptor: str = "") -> None:
    """Display perturbation."""
    _, axs = plt.subplots(figsize=(8, 8))
    if TYPE_CHECKING:
        assert isinstance(axs, Axes)
    axs.set_title(descriptor)
    axs.imshow(img)
    axs.axis("off")
def config_to_str(config: dict[str, Any]) -> str:
    """Generate string to describe config."""
    out = ", ".join([f"{k}={v}" for k, v in config.items()])

    return f"({out})"

Noise Perturbers#

Noise perturbations add different forms of random variation to the image, and are implemented using scikit-image’s noise functions. For all of these perturbers, you can specify a seed for reproducible results.

Salt Noise Perturber#

The SaltNoisePerturber replaces random pixels with 1.

seed = 42
amount = 0.25

salt_noise_perturber = SaltNoisePerturber(
    seed=seed,
    amount=amount,
)
salt_noise_out, _ = salt_noise_perturber(image=img)
display_pert(salt_noise_out, f"Salt Noise Perturber {config_to_str(salt_noise_perturber.get_config())}")
../_images/03c373cb2c38093130134a9eefd22123b3cd8afec8f6841c5d983d640f9aca22.jpg

Since we specified a seed, we can confirm that our results are reproducible:

salt_noise_perturber_2 = SaltNoisePerturber(
    seed=seed,
    amount=amount,
)
salt_noise_out_2, _ = salt_noise_perturber_2(image=img)
print(np.array_equal(salt_noise_out, salt_noise_out_2))
assert np.array_equal(salt_noise_out, salt_noise_out_2)  # noqa: S101
True

Pepper Noise Perturber#

The PepperNoisePerturber replaces random pixels with 0 (for unsigned images) or -1 (for signed images).

seed = 42
amount = 0.25

pepper_noise_perturber = PepperNoisePerturber(
    seed=seed,
    amount=amount,
)
pepper_noise_out, _ = pepper_noise_perturber(image=img)
display_pert(pepper_noise_out, f"Pepper Noise Perturber {config_to_str(pepper_noise_perturber.get_config())}")
../_images/ed95ad2d2f52e23c4bb767397077c1f31f45b577ddc4014458a6ddef9452538e.jpg

Again, we can confirm that our results are reproducible by using the same seed.

pepper_noise_perturber_2 = PepperNoisePerturber(
    seed=seed,
    amount=amount,
)
pepper_noise_out_2, _ = pepper_noise_perturber_2(image=img)
print(np.array_equal(pepper_noise_out, pepper_noise_out_2))
assert np.array_equal(pepper_noise_out, pepper_noise_out_2)  # noqa: S101
True

Salt and Pepper Noise Perturber#

The SaltAndPepperNoisePerturber replaces random pixels with either salt or pepper noise. Just as before, reproducibility is possible with the seed parameter, but we won’t continue to demonstrate that.

seed = 42
amount = 0.25
salt_vs_pepper = 0.5

sp_noise_perturber = SaltAndPepperNoisePerturber(
    seed=seed,
    amount=amount,
    salt_vs_pepper=salt_vs_pepper,
)
sp_noise_out, _ = sp_noise_perturber(image=img)
display_pert(sp_noise_out, f"Salt and Pepper Noise Perturber {config_to_str(sp_noise_perturber.get_config())}")
../_images/0641e06677a4451b7ed91690a42f0006c1f24c605d46aeb86a5ada0c167d08b8.jpg

Gaussian Noise Perturber#

The GaussianNoisePerturber adds Gaussian-distributed noise to the image.

seed = 42
mean = 0
var = 0.05

gaussian_noise_perturber = GaussianNoisePerturber(
    seed=seed,
    mean=mean,
    var=var,
)
gaussian_noise_out, _ = gaussian_noise_perturber(image=img)
display_pert(gaussian_noise_out, f"Gaussian Noise Perturber {config_to_str(gaussian_noise_perturber.get_config())}")
../_images/0b74db75f5e104ed8371be48e981505138dbd9ca0431f2cb1d31b2dfd629894c.jpg

Speckle Noise Perturber#

The SpeckleNoisePerturber adds multiplicative, Gaussian-distributed noise to the image.

seed = 42
mean = 0
var = 0.05

speckle_noise_perturber = SpeckleNoisePerturber(
    seed=seed,
    mean=mean,
    var=var,
)
speckle_noise_out, _ = speckle_noise_perturber(image=img)
display_pert(speckle_noise_out, f"Speckle Noise Perturber {config_to_str(speckle_noise_perturber.get_config())}")
../_images/ad2cd197b89b3f28dc819725a41173c877819e1ddb3845fa8dc2106d6f1857f4.jpg

Blur Perturbers#

These blur perturbers apply various convolution-based smoothing operations and are implemented using OpenCV.

Average Blur Perturber#

The AverageBlurPerturber applies “average” blurring to the image stimulus. To achieve this, OpenCV convolves the image with a normalized box filter, so the average of the pixels under the kernel replaces the central element.

ksize = 7

avg_blur_perturber = AverageBlurPerturber(
    ksize=ksize,
)
avg_blur_out, _ = avg_blur_perturber(image=img)
display_pert(avg_blur_out, f"Average Blur Perturber {config_to_str(avg_blur_perturber.get_config())}")
../_images/3cdd1b07e3d24fb9d0295abef103147d0f7a46f961c33ba8fc851c789315fc7e.jpg

Gaussian Blur Perturber#

The GaussianBlurPerturber applies Gaussian blurring to the image stimulus by using a Gaussian kernel instead of a normalized box filter.

ksize = 7

gaussian_blur_perturber = GaussianBlurPerturber(
    ksize=ksize,
)
gaussian_blur_out, _ = gaussian_blur_perturber(image=img)
display_pert(gaussian_blur_out, f"Gaussian Blur Perturber {config_to_str(gaussian_blur_perturber.get_config())}")
../_images/5a207fc90018164b77e4097e85ce9617de767431c3d2028cfecd9bb64b32c139.jpg

Median Blur Perturber#

The MedianBlurPerturber applies median blurring to the image stimulus, where the central element is replaced with the median value of the pixels under the kernel.

ksize = 7

median_blur_perturber = MedianBlurPerturber(
    ksize=ksize,
)
median_blur_out, _ = median_blur_perturber(image=img)
display_pert(median_blur_out, f"Median Blur Perturber {config_to_str(median_blur_perturber.get_config())}")
../_images/fab7a9ceb82f1f20bd411cca2128384c7cb70f925c620de5065bd3df20cb4545.jpg

Enhancement Perturbers#

Enhancement perturbations adjust properties such as brightness, color balance, contrast, and sharpness, and are implemented using the Python Imaging Library (PIL). A factor of 1.0 returns a copy of the original image. Lower factor values mean less of the relevant property (e.g. darker for brightness), while higher values mean more (e.g. brighter for brightness).

Brightness Perturber#

The BrightnessPerturber adjusts the brightness of the image stimulus.

factor = 0.25

brightness_perturber = BrightnessPerturber(
    factor=factor,
)
brightness_out, _ = brightness_perturber(image=img)
display_pert(brightness_out, f"Brightness Perturber {config_to_str(brightness_perturber.get_config())}")
../_images/2c1e4cc2b62f7ae11982b220a3b70544b044570fd99ede6bfc44797a4bd023e5.jpg

Color Perturber#

The ColorPerturber adjusts the color balance of the image stimulus.

factor = 0.15

color_perturber = ColorPerturber(
    factor=factor,
)
color_out, _ = color_perturber(image=img)
display_pert(color_out, f"Color Perturber {config_to_str(color_perturber.get_config())}")
../_images/eca659c449e8bad24e5644d10c1a3375698c630f6914dc71b0b9a9ad7be962d5.jpg

Contrast Perturber#

The ContrastPerturber adjusts the contrast of the image stimulus.

factor = 3.5

contrast_perturber = ContrastPerturber(
    factor=factor,
)
contrast_out, _ = contrast_perturber(image=img)
display_pert(contrast_out, f"Contrast Perturber {config_to_str(contrast_perturber.get_config())}")
../_images/2bd03bc6823cb9f592884c397db03e6bca20c21de518dad08f59cc5600af014b.jpg

Sharpness Perturber#

The SharpnessPerturber adjusts the sharpness of the image stimulus. The sharpness factor is limited to [0.0, 2.0].

factor = 0.15

sharpness_perturber = SharpnessPerturber(
    factor=factor,
)
sharpness_out, _ = sharpness_perturber(image=img)
display_pert(sharpness_out, f"Sharpness Perturber {config_to_str(sharpness_perturber.get_config())}")
../_images/a816451c3bfb2031a0b184089fbe62c795c539f123205f2d39ae84e3b54b188c.jpg