File: fft2d_unit_test.py

package info (click to toggle)
python-sigima 1.1.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 25,608 kB
  • sloc: python: 35,251; makefile: 3
file content (190 lines) | stat: -rw-r--r-- 7,060 bytes parent folder | download
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
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Image FFT unit test.
"""

# pylint: disable=invalid-name  # Allows short reference names like x, y, ...
# pylint: disable=duplicate-code

import numpy as np
import pytest

import sigima.objects
import sigima.params
import sigima.proc.image
import sigima.tests.data
import sigima.tools.image
from sigima.tests import guiutils
from sigima.tests.env import execenv
from sigima.tests.helpers import check_array_result, check_scalar_result


@pytest.mark.gui
def test_image_fft_interactive():
    """2D FFT interactive test."""
    with guiutils.lazy_qt_app_context(force=True):
        from sigima import viz  # pylint: disable=import-outside-toplevel

        # Create a 2D ring image
        execenv.print("Generating 2D ring image...", end=" ")
        data = sigima.tests.data.create_ring_image().data
        execenv.print("OK")

        # FFT
        execenv.print("Computing FFT of image...", end=" ")
        f = sigima.tools.image.fft2d(data)
        data2 = sigima.tools.image.ifft2d(f)
        execenv.print("OK")
        execenv.print("Comparing original and FFT/iFFT images...", end=" ")
        check_array_result(
            "Image FFT/iFFT", np.array(data2.real, dtype=data.dtype), data, rtol=1e-3
        )
        execenv.print("OK")

        images = [data, f.real, f.imag, np.abs(f), data2.real, data2.imag]
        titles = ["Original", "Re(FFT)", "Im(FFT)", "Abs(FFT)", "Re(iFFT)", "Im(iFFT)"]
        viz.view_images_side_by_side(images, titles, rows=2, title="2D FFT/iFFT")


@pytest.mark.validation
def test_image_zero_padding() -> None:
    """2D FFT zero padding validation test."""
    ima1 = sigima.tests.data.create_checkerboard()
    rows, cols = 2, 2
    param = sigima.params.ZeroPadding2DParam.create(rows=rows, cols=cols)
    assert param.strategy == "custom", (
        f"Wrong default strategy: {param.strategy} (expected 'custom')"
    )

    # Validate the zero padding with bottom-right position
    param.position = "bottom-right"
    ima2 = sigima.proc.image.zero_padding(ima1, param)
    sh1, sh2 = ima1.data.shape, ima2.data.shape
    exp_sh2 = (sh1[0] + rows, sh1[1] + cols)
    execenv.print("Validating zero padding for bottom-right position...", end=" ")
    assert sh2 == exp_sh2, f"Wrong shape: {sh2} (expected {exp_sh2})"
    assert np.all(ima2.data[0 : sh1[0], 0 : sh1[1]] == ima1.data), (
        "Altered data in original image area"
    )
    assert np.all(ima2.data[sh1[0] : sh2[0], sh1[1] : sh2[1]] == 0), (
        "Altered data in padded area"
    )
    execenv.print("OK")

    # Validate the zero padding with center position
    param.position = "around"
    ima3 = sigima.proc.image.zero_padding(ima1, param)
    sh3 = ima3.data.shape
    exp_sh3 = (sh1[0] + rows, sh1[1] + cols)
    execenv.print("Validating zero padding for around position...", end=" ")
    assert sh3 == exp_sh3, f"Wrong shape: {sh3} (expected {exp_sh3})"
    assert np.all(
        ima3.data[rows // 2 : sh1[0] + rows // 2, cols // 2 : sh1[1] + cols // 2]
        == ima1.data
    ), "Altered data in original image area"
    assert np.all(ima3.data[0 : rows // 2, :] == 0), "Altered data in padded area (top)"
    assert np.all(ima3.data[sh1[0] + rows // 2 :, :] == 0), (
        "Altered data in padded area (bottom)"
    )
    assert np.all(ima3.data[:, 0 : cols // 2] == 0), (
        "Altered data in padded area (left)"
    )
    assert np.all(ima3.data[:, sh1[1] + cols // 2 :] == 0), (
        "Altered data in padded area (right)"
    )
    execenv.print("OK")

    # Validate zero padding with strategies other than custom size
    # Image size is (200, 300) and the next power of 2 is (256, 512)
    # The multiple of 64 is (256, 320)
    ima4 = sigima.objects.create_image("", np.zeros((200, 300)))
    for strategy, (exp_rows, exp_cols) in (
        ("next_pow2", (56, 212)),
        ("multiple_of_64", (56, 20)),
    ):
        param = sigima.params.ZeroPadding2DParam.create(strategy=strategy)
        param.update_from_obj(ima4)
        assert param.rows == exp_rows, (
            f"Wrong row number for '{param.strategy}' strategy: {param.rows}"
            f" (expected {exp_rows})"
        )
        assert param.cols == exp_cols, (
            f"Wrong column number for '{param.strategy}' strategy: {param.cols}"
            f" (expected {exp_cols})"
        )


@pytest.mark.validation
def test_image_fft() -> None:
    """2D FFT validation test."""
    ima1 = sigima.tests.data.create_checkerboard()
    fft = sigima.proc.image.fft(ima1)
    ifft = sigima.proc.image.ifft(fft)

    # Check that the inverse FFT reconstructs the original image
    check_array_result("Checkerboard image FFT/iFFT", ifft.data.real, ima1.data)

    # Parseval's Theorem Validation
    original_energy = np.sum(np.abs(ima1.data) ** 2)
    transformed_energy = np.sum(np.abs(fft.data) ** 2) / (ima1.data.size)
    check_scalar_result("Parseval's Theorem", transformed_energy, original_energy)


@pytest.mark.skip(reason="Already covered by the `test_image_fft` test.")
@pytest.mark.validation
def test_image_ifft() -> None:
    """2D iFFT validation test."""
    # This is just a way of marking the iFFT test as a validation test because it is
    # already covered by the FFT test above (there is no need to repeat the same test).
    # The tested function is :py:func:`sigima.proc.image.ifft`.


@pytest.mark.validation
def test_image_magnitude_spectrum() -> None:
    """2D magnitude spectrum validation test."""
    ima1 = sigima.tests.data.create_checkerboard()
    fft = sigima.proc.image.fft(ima1)
    param = sigima.params.SpectrumParam()
    for decibel in (True, False):
        param.decibel = decibel
        mag = sigima.proc.image.magnitude_spectrum(ima1, param)

    # Check that the magnitude spectrum is correct
    exp = np.abs(fft.data)
    check_array_result("Checkerboard image FFT magnitude spectrum", mag.data, exp)


@pytest.mark.validation
def test_image_phase_spectrum() -> None:
    """2D phase spectrum validation test."""
    ima1 = sigima.tests.data.create_checkerboard()
    fft = sigima.proc.image.fft(ima1)
    phase = sigima.proc.image.phase_spectrum(ima1)

    # Check that the phase spectrum is correct
    exp = np.rad2deg(np.angle(fft.data))
    check_array_result("Checkerboard image FFT phase spectrum", phase.data, exp)


@pytest.mark.validation
def test_image_psd() -> None:
    """2D Power Spectral Density validation test."""
    ima1 = sigima.tests.data.create_checkerboard()
    param = sigima.params.SpectrumParam()
    for decibel in (True, False):
        param.decibel = decibel
        psd = sigima.proc.image.psd(ima1, param)

    # Check that the PSD is correct
    exp = np.abs(sigima.proc.image.fft(ima1).data) ** 2
    check_array_result("Checkerboard image PSD", psd.data, exp)


if __name__ == "__main__":
    test_image_fft_interactive()
    test_image_zero_padding()
    test_image_fft()
    test_image_magnitude_spectrum()
    test_image_phase_spectrum()
    test_image_psd()