Skip to content

Commit 8bb6513

Browse files
committed
Zero padding (1D/2D): add validation tests
Fix #171
1 parent 4b065b1 commit 8bb6513

File tree

5 files changed

+122
-8
lines changed

5 files changed

+122
-8
lines changed

cdl/algorithms/image.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def zero_padding(
7878
image: np.ndarray,
7979
rows: int = 0,
8080
cols: int = 0,
81-
position: Literal["bottom-right", "centered"] = "bottom-right",
81+
position: Literal["bottom-right", "center"] = "bottom-right",
8282
) -> np.ndarray:
8383
"""
8484
Zero-pad a 2D image by adding rows and/or columns.
@@ -89,7 +89,7 @@ def zero_padding(
8989
cols: Number of columns to add in total (default: 0)
9090
position: Padding placement strategy:
9191
- "bottom-right": all padding is added to the bottom and right
92-
- "centered": padding is split equally on top/bottom and left/right
92+
- "center": padding is split equally on top/bottom and left/right
9393
9494
Returns:
9595
The padded 2D image as a NumPy array.
@@ -104,7 +104,7 @@ def zero_padding(
104104

105105
if position == "bottom-right":
106106
pad_width = ((0, rows), (0, cols))
107-
elif position == "centered":
107+
elif position == "center":
108108
pad_width = (
109109
(rows // 2, rows - rows // 2),
110110
(cols // 2, cols - cols // 2),

cdl/computation/image/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,7 +1243,7 @@ def __init__(self, *args, **kwargs) -> None:
12431243
def update_from_image(self, obj: ImageObj) -> None:
12441244
"""Update parameters from image"""
12451245
self.__obj = obj
1246-
# self.choice_callback(None, self.strategy)
1246+
self.choice_callback(None, self.strategy)
12471247

12481248
def choice_callback(self, item, value):
12491249
"""Callback to update padding values"""
@@ -1260,7 +1260,7 @@ def choice_callback(self, item, value):
12601260
strategies = ("next_pow2", "multiple_of_64", "custom")
12611261
_prop = gds.GetAttrProp("strategy")
12621262
strategy = gds.ChoiceItem(
1263-
_("Padding strategy"), zip(strategies, strategies), default=strategies[0]
1263+
_("Padding strategy"), zip(strategies, strategies), default=strategies[-1]
12641264
).set_prop("display", store=_prop, callback=choice_callback)
12651265

12661266
_func_prop = gds.FuncProp(_prop, lambda x: x == "custom")
@@ -1269,7 +1269,7 @@ def choice_callback(self, item, value):
12691269
"display", active=_func_prop
12701270
)
12711271

1272-
positions = ("bottom-right", "centered")
1272+
positions = ("bottom-right", "center")
12731273
position = gds.ChoiceItem(
12741274
_("Padding position"), zip(positions, positions), default=positions[0]
12751275
)

cdl/computation/signal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ def __init__(self, *args, **kwargs) -> None:
907907
def update_from_signal(self, obj: SignalObj) -> None:
908908
"""Update parameters from signal"""
909909
self.__obj = obj
910-
# self.choice_callback(None, self.strategy)
910+
self.choice_callback(None, self.strategy)
911911

912912
def choice_callback(self, item, value):
913913
"""Callback for choice item"""
@@ -922,7 +922,7 @@ def choice_callback(self, item, value):
922922
strategies = ("next_pow2", "double", "triple", "custom")
923923
_prop = gds.GetAttrProp("strategy")
924924
strategy = gds.ChoiceItem(
925-
_("Strategy"), zip(strategies, strategies), default=strategies[0]
925+
_("Strategy"), zip(strategies, strategies), default=strategies[-1]
926926
).set_prop("display", store=_prop, callback=choice_callback)
927927
_func_prop = gds.FuncProp(_prop, lambda x: x == "custom")
928928
n = gds.IntItem(

cdl/tests/features/images/fft2d_unit_test.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import cdl.algorithms.image as alg
1616
import cdl.computation.image as cpi
17+
import cdl.obj
18+
import cdl.param
1719
import cdl.tests.data as ctd
1820
from cdl.env import execenv
1921
from cdl.utils.tests import check_array_result, check_scalar_result
@@ -44,6 +46,74 @@ def test_image_fft_interactive():
4446
view_images_side_by_side(images, titles, rows=2, title="2D FFT/iFFT")
4547

4648

49+
@pytest.mark.validation
50+
def test_image_zero_padding() -> None:
51+
"""2D FFT zero padding validation test."""
52+
ima1 = ctd.create_checkerboard()
53+
rows, cols = 2, 2
54+
param = cdl.param.ZeroPadding2DParam.create(rows=rows, cols=cols)
55+
assert param.strategy == "custom", (
56+
f"Wrong default strategy: {param.strategy} (expected 'custom')"
57+
)
58+
59+
# Validate the zero padding with bottom-right position
60+
param.position = "bottom-right"
61+
ima2 = cpi.compute_zero_padding(ima1, param)
62+
sh1, sh2 = ima1.data.shape, ima2.data.shape
63+
exp_sh2 = (sh1[0] + rows, sh1[1] + cols)
64+
execenv.print("Validating zero padding for bottom-right position...", end=" ")
65+
assert sh2 == exp_sh2, f"Wrong shape: {sh2} (expected {exp_sh2})"
66+
assert np.all(ima2.data[0 : sh1[0], 0 : sh1[1]] == ima1.data), (
67+
"Altered data in original image area"
68+
)
69+
assert np.all(ima2.data[sh1[0] : sh2[0], sh1[1] : sh2[1]] == 0), (
70+
"Altered data in padded area"
71+
)
72+
execenv.print("OK")
73+
74+
# Validate the zero padding with center position
75+
param.position = "center"
76+
ima3 = cpi.compute_zero_padding(ima1, param)
77+
sh3 = ima3.data.shape
78+
exp_sh3 = (sh1[0] + rows, sh1[1] + cols)
79+
execenv.print("Validating zero padding for center position...", end=" ")
80+
assert sh3 == exp_sh3, f"Wrong shape: {sh3} (expected {exp_sh3})"
81+
assert np.all(
82+
ima3.data[rows // 2 : sh1[0] + rows // 2, cols // 2 : sh1[1] + cols // 2]
83+
== ima1.data
84+
), "Altered data in original image area"
85+
assert np.all(ima3.data[0 : rows // 2, :] == 0), "Altered data in padded area (top)"
86+
assert np.all(ima3.data[sh1[0] + rows // 2 :, :] == 0), (
87+
"Altered data in padded area (bottom)"
88+
)
89+
assert np.all(ima3.data[:, 0 : cols // 2] == 0), (
90+
"Altered data in padded area (left)"
91+
)
92+
assert np.all(ima3.data[:, sh1[1] + cols // 2 :] == 0), (
93+
"Altered data in padded area (right)"
94+
)
95+
execenv.print("OK")
96+
97+
# Validate zero padding with strategies other than custom size
98+
# Image size is (200, 300) and the next power of 2 is (256, 512)
99+
# The multiple of 64 is (256, 320)
100+
ima4 = cdl.obj.create_image("", np.zeros((200, 300)))
101+
for strategy, (exp_rows, exp_cols) in (
102+
("next_pow2", (56, 212)),
103+
("multiple_of_64", (56, 20)),
104+
):
105+
param = cdl.param.ZeroPadding2DParam.create(strategy=strategy)
106+
param.update_from_image(ima4)
107+
assert param.rows == exp_rows, (
108+
f"Wrong row number for '{param.strategy}' strategy: {param.rows}"
109+
f" (expected {exp_rows})"
110+
)
111+
assert param.cols == exp_cols, (
112+
f"Wrong column number for '{param.strategy}' strategy: {param.cols}"
113+
f" (expected {exp_cols})"
114+
)
115+
116+
47117
@pytest.mark.validation
48118
def test_image_fft() -> None:
49119
"""2D FFT validation test."""
@@ -105,6 +175,7 @@ def test_image_psd() -> None:
105175

106176
if __name__ == "__main__":
107177
test_image_fft_interactive()
178+
test_image_zero_padding()
108179
test_image_fft()
109180
test_image_magnitude_spectrum()
110181
test_image_phase_spectrum()

cdl/tests/features/signals/fft1d_unit_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,48 @@ def test_signal_fft_interactive() -> None:
4949
view_curves([(t, y), (t2, y2)])
5050

5151

52+
@pytest.mark.validation
53+
def test_signal_zero_padding() -> None:
54+
"""1D FFT zero padding validation test."""
55+
s1 = ctd.create_periodic_signal(cdl.obj.SignalTypes.COSINUS, freq=50.0, size=1000)
56+
57+
# Validate zero padding with custom length
58+
param = cdl.param.ZeroPadding1DParam.create(n=250)
59+
assert param.strategy == "custom", (
60+
f"Wrong default strategy: {param.strategy} (expected 'custom')"
61+
)
62+
s2 = cps.compute_zero_padding(s1, param)
63+
len1 = len(s1.y)
64+
exp_len2 = len1 + param.n
65+
execenv.print("Validating zero padding with custom length...", end=" ")
66+
assert len(s2.y) == exp_len2, f"Wrong length: {len(s2.y)} (expected {exp_len2})"
67+
assert np.all(s2.x[:len1] == s1.x[:len1]), "Altered X data in original signal area"
68+
assert np.all(s2.y[:len1] == s1.y[:len1]), "Altered Y data in original signal area"
69+
assert np.all(s2.y[len1:] == 0), "Non-zero data in zero-padded area"
70+
execenv.print("OK")
71+
step1 = s1.x[1] - s1.x[0]
72+
check_array_result(
73+
"Zero padding X data",
74+
s2.x[len1:],
75+
np.arange(
76+
s1.x[len1 - 1] + step1, s1.x[len1 - 1] + step1 * (param.n + 1), step1
77+
),
78+
)
79+
80+
# Validate zero padding with strategies other than custom length
81+
for strategy, expected_length in (
82+
("next_pow2", 24),
83+
("double", 1000),
84+
("triple", 2000),
85+
):
86+
param = cdl.param.ZeroPadding1DParam.create(strategy=strategy)
87+
param.update_from_signal(s1)
88+
assert param.n == expected_length, (
89+
f"Wrong length for '{param.strategy}' strategy: {param.n}"
90+
f" (expected {expected_length})"
91+
)
92+
93+
5294
@pytest.mark.validation
5395
def test_signal_fft() -> None:
5496
"""1D FFT validation test."""
@@ -158,6 +200,7 @@ def test_signal_psd() -> None:
158200

159201
if __name__ == "__main__":
160202
test_signal_fft_interactive()
203+
test_signal_zero_padding()
161204
test_signal_fft()
162205
test_signal_magnitude_spectrum()
163206
test_signal_phase_spectrum()

0 commit comments

Comments
 (0)