1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
|
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""Unit tests for image convolution/deconvolution features."""
from __future__ import annotations
from contextlib import contextmanager
import numpy as np
import pytest
import scipy.signal as sps
import sigima.proc.image
from sigima.config import options as sigima_options
from sigima.objects import create_image, create_image_from_param
from sigima.objects.image import Gauss2DParam, ImageObj, Zero2DParam
from sigima.tests import guiutils
from sigima.tests.helpers import check_array_result
from sigima.tools.image import deconvolve
@contextmanager
def disable_kernel_normalization():
"""Context manager to temporarily disable kernel normalization."""
# Save current values
original_auto_normalize = sigima_options.auto_normalize_kernel.get()
# Disable for test
sigima_options.auto_normalize_kernel.set(False)
try:
yield
finally:
# Restore original values
sigima_options.auto_normalize_kernel.set(original_auto_normalize)
def _generate_rectangle_image(title: str = "Rectangle", size: int = 32) -> ImageObj:
"""Generate a test square image with a rectangle in the center."""
data = np.zeros((size, size), dtype=np.float64)
data[size // 5 : 2 * size // 5, size // 7 : 5 * size // 7] = 1.0
img = create_image(title, data)
return img
def _generate_image(size: int = 32) -> ImageObj:
"""Generate a test square image.
Args:
size: The dimension of the square image to generate.
Returns:
An image object.
"""
# Gaussian image.
gauss_img = create_image_from_param(Gauss2DParam.create(height=size, width=size))
return gauss_img
def _generate_gaussian_kernel(size: int = 32, sigma: float = 1.0) -> ImageObj:
"""Generate a Gaussian kernel image.
Args:
size: The dimension of the square kernel to generate.
sigma: The standard deviation of the Gaussian.
Returns:
An image object.
"""
kernel = create_image_from_param(
Gauss2DParam.create(height=size, width=size, sigma=sigma)
)
return kernel
def _generate_identity_kernel(size: int = 7, ignore_odd: bool = False) -> ImageObj:
"""Generate an identity kernel image.
Args:
size: The dimension of the square kernel to generate (must be odd).
ignore_odd: If True, do not check for odd size.
Returns:
An image object.
"""
if not ignore_odd:
assert size % 2 == 1, "Identity kernel size must be odd."
kernel = create_image_from_param(Zero2DParam.create(height=size, width=size))
assert kernel.data is not None
kernel.data[size // 2, size // 2] = 1.0
return kernel
def __convolve_image(kernel: ImageObj, size: int = 32) -> tuple[ImageObj, ImageObj]:
"""Generate a test image and its convolution with a Gaussian kernel.
Returns:
A tuple (source image, kernel, convolved image).
"""
original = _generate_rectangle_image(size=size)
assert original.data is not None
convolved = sigima.proc.image.convolution(original, kernel)
assert convolved.data is not None
return original, convolved
@pytest.mark.validation
def test_image_convolution(size: int = 32) -> None:
"""Validation test for the image convolution processing.
Note: This test disables kernel normalization to compare against raw scipy results.
"""
with disable_kernel_normalization():
# Test with a Gaussian kernel.
kernel = _generate_gaussian_kernel(size=size, sigma=1.0)
original, convolved = __convolve_image(kernel, size=size)
exp = sps.convolve(original.data, kernel.data, mode="same", method="auto")
check_array_result("Convolution", convolved.data, exp)
guiutils.view_images_side_by_side_if_gui(
[original, kernel, convolved],
["Original", "Kernel", "Convolved"],
title="Image Convolution Test: Gaussian Kernel",
)
# Test with an identity kernel.
kernel = _generate_identity_kernel(size=7)
original, convolved = __convolve_image(kernel, size=size)
# check_array_result("Convolution identity", convolved.data, original.data)
guiutils.view_images_side_by_side_if_gui(
[original, kernel, convolved],
["Original", "Kernel", "Convolved"],
title="Image Convolution Test: Identity Kernel",
)
# Test with a null kernel.
kernel.data = np.array([])
with pytest.raises(ValueError, match="Convolution kernel cannot be null."):
sigima.proc.image.convolution(original, kernel)
@pytest.mark.validation
def test_image_deconvolution(size: int = 32) -> None:
"""Validation test for image deconvolution.
Note: This test disables kernel normalization to compare against expected results.
"""
with disable_kernel_normalization():
# Test with an identity kernel.
kernel = _generate_identity_kernel(size=31)
original, convolved = __convolve_image(kernel, size=size)
deconvolved = sigima.proc.image.deconvolution(convolved, kernel)
guiutils.view_images_side_by_side_if_gui(
[original, kernel, convolved, deconvolved],
["Original", "Kernel", "Convolved", "Deconvolved"],
title="Image Deconvolution Test: Identity Kernel",
)
check_array_result("Deconvolution identity", deconvolved.data, original.data)
# Test with a Gaussian kernel.
kernel = _generate_gaussian_kernel(size=31, sigma=1.0)
original, convolved = __convolve_image(kernel, size=64)
deconvolved = sigima.proc.image.deconvolution(convolved, kernel)
# check_array_result("Deconvolution Gaussian", deconvolved.data, original.data)
guiutils.view_images_side_by_side_if_gui(
[original, kernel, convolved, deconvolved],
["Original", "Kernel", "Convolved", "Deconvolved"],
title="Image Deconvolution Test: Gaussian Kernel",
)
# Test with a non-odd-sized kernel.
kernel = _generate_identity_kernel(size=8, ignore_odd=True)
# Check that a warning is raised for non-odd-sized kernel.
with pytest.warns(
UserWarning, match=r"Deconvolution kernel has even dimension\(s\).*"
):
deconvolved = sigima.proc.image.deconvolution(convolved, kernel)
def test_tools_image_deconvolve_null_kernel() -> None:
"""Test deconvolution with a null kernel."""
size = 32
src = _generate_image(size)
assert src.data is not None
kernel = create_image_from_param(Zero2DParam.create(height=size, width=size))
assert kernel.data is not None
with pytest.raises(ValueError, match="Deconvolution kernel cannot be null."):
deconvolve(src.data, kernel.data)
if __name__ == "__main__":
guiutils.enable_gui()
test_image_convolution()
test_image_deconvolution()
test_tools_image_deconvolve_null_kernel()
|