Skip to content

Commit 9e3a70a

Browse files
authored
Band names for arrow exported images (#9099)
2 parents 608619c + 014f421 commit 9e3a70a

File tree

5 files changed

+724
-2
lines changed

5 files changed

+724
-2
lines changed

Tests/test_arro3.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from typing import Any, NamedTuple
5+
6+
import pytest
7+
8+
from PIL import Image
9+
10+
from .helper import (
11+
assert_deep_equal,
12+
assert_image_equal,
13+
hopper,
14+
is_big_endian,
15+
)
16+
17+
TYPE_CHECKING = False
18+
if TYPE_CHECKING:
19+
from arro3 import compute # type: ignore [import-not-found]
20+
from arro3.core import ( # type: ignore [import-not-found]
21+
Array,
22+
DataType,
23+
Field,
24+
fixed_size_list_array,
25+
)
26+
else:
27+
arro3 = pytest.importorskip("arro3", reason="Arro3 not installed")
28+
from arro3 import compute
29+
from arro3.core import Array, DataType, Field, fixed_size_list_array
30+
31+
TEST_IMAGE_SIZE = (10, 10)
32+
33+
34+
def _test_img_equals_pyarray(
35+
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
36+
) -> None:
37+
assert img.height * img.width * elts_per_pixel == len(arr)
38+
px = img.load()
39+
assert px is not None
40+
if elts_per_pixel > 1 and mask is None:
41+
# have to do element-wise comparison when we're comparing
42+
# flattened r,g,b,a to a pixel.
43+
mask = list(range(elts_per_pixel))
44+
for x in range(0, img.size[0], int(img.size[0] / 10)):
45+
for y in range(0, img.size[1], int(img.size[1] / 10)):
46+
if mask:
47+
pixel = px[x, y]
48+
assert isinstance(pixel, tuple)
49+
for ix, elt in enumerate(mask):
50+
if elts_per_pixel == 1:
51+
assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
52+
else:
53+
assert (
54+
pixel[ix]
55+
== arr[(y * img.width + x) * elts_per_pixel + elt].as_py()
56+
)
57+
else:
58+
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())
59+
60+
61+
def _test_img_equals_int32_pyarray(
62+
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
63+
) -> None:
64+
assert img.height * img.width * elts_per_pixel == len(arr)
65+
px = img.load()
66+
assert px is not None
67+
if mask is None:
68+
# have to do element-wise comparison when we're comparing
69+
# flattened rgba in an uint32 to a pixel.
70+
mask = list(range(elts_per_pixel))
71+
for x in range(0, img.size[0], int(img.size[0] / 10)):
72+
for y in range(0, img.size[1], int(img.size[1] / 10)):
73+
pixel = px[x, y]
74+
assert isinstance(pixel, tuple)
75+
arr_pixel_int = arr[y * img.width + x].as_py()
76+
arr_pixel_tuple = (
77+
arr_pixel_int % 256,
78+
(arr_pixel_int // 256) % 256,
79+
(arr_pixel_int // 256**2) % 256,
80+
(arr_pixel_int // 256**3),
81+
)
82+
if is_big_endian():
83+
arr_pixel_tuple = arr_pixel_tuple[::-1]
84+
85+
for ix, elt in enumerate(mask):
86+
assert pixel[ix] == arr_pixel_tuple[elt]
87+
88+
89+
fl_uint8_4_type = DataType.list(Field("_", DataType.uint8()).with_nullable(False), 4)
90+
91+
92+
@pytest.mark.parametrize(
93+
"mode, dtype, mask",
94+
(
95+
("L", DataType.uint8(), None),
96+
("I", DataType.int32(), None),
97+
("F", DataType.float32(), None),
98+
("LA", fl_uint8_4_type, [0, 3]),
99+
("RGB", fl_uint8_4_type, [0, 1, 2]),
100+
("RGBA", fl_uint8_4_type, None),
101+
("RGBX", fl_uint8_4_type, None),
102+
("CMYK", fl_uint8_4_type, None),
103+
("YCbCr", fl_uint8_4_type, [0, 1, 2]),
104+
("HSV", fl_uint8_4_type, [0, 1, 2]),
105+
),
106+
)
107+
def test_to_array(mode: str, dtype: DataType, mask: list[int] | None) -> None:
108+
img = hopper(mode)
109+
110+
# Resize to non-square
111+
img = img.crop((3, 0, 124, 127))
112+
assert img.size == (121, 127)
113+
114+
arr = Array(img)
115+
_test_img_equals_pyarray(img, arr, mask)
116+
assert arr.type == dtype
117+
118+
reloaded = Image.fromarrow(arr, mode, img.size)
119+
assert_image_equal(img, reloaded)
120+
121+
122+
def test_lifetime() -> None:
123+
# valgrind shouldn't error out here.
124+
# arrays should be accessible after the image is deleted.
125+
126+
img = hopper("L")
127+
128+
arr_1 = Array(img)
129+
arr_2 = Array(img)
130+
131+
del img
132+
133+
assert compute.sum(arr_1).as_py() > 0
134+
del arr_1
135+
136+
assert compute.sum(arr_2).as_py() > 0
137+
del arr_2
138+
139+
140+
def test_lifetime2() -> None:
141+
# valgrind shouldn't error out here.
142+
# img should remain after the arrays are collected.
143+
144+
img = hopper("L")
145+
146+
arr_1 = Array(img)
147+
arr_2 = Array(img)
148+
149+
assert compute.sum(arr_1).as_py() > 0
150+
del arr_1
151+
152+
assert compute.sum(arr_2).as_py() > 0
153+
del arr_2
154+
155+
img2 = img.copy()
156+
px = img2.load()
157+
assert px # make mypy happy
158+
assert isinstance(px[0, 0], int)
159+
160+
161+
class DataShape(NamedTuple):
162+
dtype: DataType
163+
# Strictly speaking, elt should be a pixel or pixel component, so
164+
# list[uint8][4], float, int, uint32, uint8, etc. But more
165+
# correctly, it should be exactly the dtype from the line above.
166+
elt: Any
167+
elts_per_pixel: int
168+
169+
170+
UINT_ARR = DataShape(
171+
dtype=fl_uint8_4_type,
172+
elt=[1, 2, 3, 4], # array of 4 uint8 per pixel
173+
elts_per_pixel=1, # only one array per pixel
174+
)
175+
176+
UINT = DataShape(
177+
dtype=DataType.uint8(),
178+
elt=3, # one uint8,
179+
elts_per_pixel=4, # but repeated 4x per pixel
180+
)
181+
182+
UINT32 = DataShape(
183+
dtype=DataType.uint32(),
184+
elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000
185+
elts_per_pixel=1, # one per pixel
186+
)
187+
188+
INT32 = DataShape(
189+
dtype=DataType.uint32(),
190+
elt=0x12CDEF45, # one packed int
191+
elts_per_pixel=1, # one per pixel
192+
)
193+
194+
195+
@pytest.mark.parametrize(
196+
"mode, data_tp, mask",
197+
(
198+
("L", DataShape(DataType.uint8(), 3, 1), None),
199+
("I", DataShape(DataType.int32(), 1 << 24, 1), None),
200+
("F", DataShape(DataType.float32(), 3.14159, 1), None),
201+
("LA", UINT_ARR, [0, 3]),
202+
("LA", UINT, [0, 3]),
203+
("RGB", UINT_ARR, [0, 1, 2]),
204+
("RGBA", UINT_ARR, None),
205+
("CMYK", UINT_ARR, None),
206+
("YCbCr", UINT_ARR, [0, 1, 2]),
207+
("HSV", UINT_ARR, [0, 1, 2]),
208+
("RGB", UINT, [0, 1, 2]),
209+
("RGBA", UINT, None),
210+
("CMYK", UINT, None),
211+
("YCbCr", UINT, [0, 1, 2]),
212+
("HSV", UINT, [0, 1, 2]),
213+
),
214+
)
215+
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
216+
(dtype, elt, elts_per_pixel) = data_tp
217+
218+
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
219+
if dtype == fl_uint8_4_type:
220+
tmp_arr = Array(elt * (ct_pixels * elts_per_pixel), type=DataType.uint8())
221+
arr = fixed_size_list_array(tmp_arr, 4)
222+
else:
223+
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
224+
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
225+
226+
_test_img_equals_pyarray(img, arr, mask, elts_per_pixel)
227+
228+
229+
@pytest.mark.parametrize(
230+
"mode, mask",
231+
(
232+
("LA", [0, 3]),
233+
("RGB", [0, 1, 2]),
234+
("RGBA", None),
235+
("CMYK", None),
236+
("YCbCr", [0, 1, 2]),
237+
("HSV", [0, 1, 2]),
238+
),
239+
)
240+
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
241+
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
242+
(dtype, elt, elts_per_pixel) = data_tp
243+
244+
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
245+
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
246+
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
247+
248+
_test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel)
249+
250+
251+
@pytest.mark.parametrize(
252+
"mode, metadata",
253+
(
254+
("LA", ["L", "X", "X", "A"]),
255+
("RGB", ["R", "G", "B", "X"]),
256+
("RGBX", ["R", "G", "B", "X"]),
257+
("RGBA", ["R", "G", "B", "A"]),
258+
("CMYK", ["C", "M", "Y", "K"]),
259+
("YCbCr", ["Y", "Cb", "Cr", "X"]),
260+
("HSV", ["H", "S", "V", "X"]),
261+
),
262+
)
263+
def test_image_metadata(mode: str, metadata: list[str]) -> None:
264+
img = hopper(mode)
265+
266+
arr = Array(img)
267+
268+
assert arr.type.value_field
269+
assert arr.type.value_field.metadata
270+
assert arr.type.value_field.metadata[b"image"]
271+
272+
parsed_metadata = json.loads(arr.type.value_field.metadata[b"image"].decode("utf8"))
273+
274+
assert "bands" in parsed_metadata
275+
assert parsed_metadata["bands"] == metadata

0 commit comments

Comments
 (0)