Skip to content

Commit 7238a59

Browse files
authored
Merge pull request #23 from ramonaoptics/fix-3d-single-channel-monochrome
Fix writing 3D arrays with single channel dimension as monochrome
2 parents ee75de3 + 1e9dbe3 commit 7238a59

File tree

3 files changed

+41
-7
lines changed

3 files changed

+41
-7
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.5.1] - 2025-12-29
9+
10+
- Fix writing 3D arrays with a single channel dimension (shape `(H, W, 1)`) as monochrome
11+
images. The last dimension is now automatically collapsed when `num_components == 1`,
12+
restoring compatibility with version 0.4.6 behavior.
13+
814
## [0.5.0] - 2025-12-29
915

1016
- Optimize image reading and writing for multi-threaded workloads by releasing the

ojph/ojph_bindings.cpp

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,17 @@ PYBIND11_MODULE(ojph_bindings, m) {
130130
size_t component_stride;
131131

132132
if (num_components == 1) {
133-
if (buf.ndim != 2) {
134-
throw py::value_error("Image must be 2-dimensional for single component");
133+
if (buf.ndim == 2) {
134+
height = buf.shape[0];
135+
width = buf.shape[1];
136+
component_stride = 0;
137+
} else if (buf.ndim == 3 && buf.shape[2] == 1) {
138+
height = buf.shape[0];
139+
width = buf.shape[1];
140+
component_stride = 0;
141+
} else {
142+
throw py::value_error("Image must be 2-dimensional or 3-dimensional with last dimension of 1 for single component");
135143
}
136-
height = buf.shape[0];
137-
width = buf.shape[1];
138-
component_stride = 0;
139144
} else {
140145
if (buf.ndim != 3) {
141146
throw py::value_error("Image must be 3-dimensional for multiple components");
@@ -151,8 +156,17 @@ PYBIND11_MODULE(ojph_bindings, m) {
151156
}
152157
}
153158

154-
size_t row_stride = (num_components == 1 || channel_order == "CHW") ? buf.strides[buf.ndim - 2] : buf.strides[0];
155-
size_t col_stride = (num_components == 1 || channel_order == "CHW") ? buf.strides[buf.ndim - 1] : buf.strides[1];
159+
size_t row_stride, col_stride;
160+
if (num_components == 1) {
161+
row_stride = buf.strides[0];
162+
col_stride = buf.strides[1];
163+
} else if (channel_order == "CHW") {
164+
row_stride = buf.strides[1];
165+
col_stride = buf.strides[2];
166+
} else {
167+
row_stride = buf.strides[0];
168+
col_stride = buf.strides[1];
169+
}
156170

157171
{
158172
py::gil_scoped_release release;

tests/test_memory_compression.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,20 @@ def test_imwrite_to_memory_consistency():
115115
)
116116

117117

118+
def test_imwrite_3d_single_channel_as_monochrome(tmp_path):
119+
test_image = np.random.randint(0, 256, (100, 150, 1), dtype=np.uint8)
120+
121+
compressed_data = imwrite_to_memory(test_image)
122+
123+
filename = tmp_path / 'test.j2c'
124+
with open(filename, 'wb') as f:
125+
f.write(compressed_data.tobytes())
126+
127+
loaded_image = imread(filename)
128+
expected_image = test_image.squeeze()
129+
np.testing.assert_array_equal(loaded_image, expected_image)
130+
131+
118132
def test_lossless_vs_lossy_memory_error_increase(tmp_path):
119133
rng = np.random.default_rng(321)
120134
test_image = rng.integers(0, 256, (256, 256), dtype=np.uint8)

0 commit comments

Comments
 (0)