Skip to content

Commit 2f5264f

Browse files
author
Grok Compression
committed
test: add script to test region decompress
1 parent a4a5d75 commit 2f5264f

1 file changed

Lines changed: 279 additions & 0 deletions

File tree

tests/python/region_verify.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
#!/usr/bin/env python3
2+
"""Verify region decompression against full-image decompression.
3+
4+
Decompresses the entire image into memory, then repeatedly decompresses random
5+
regions (with both dimensions <= max_dimension) and compares the region pixels
6+
against the corresponding pixels from the full image.
7+
8+
Usage (from the grok project root):
9+
10+
PYTHONPATH=build/bin LD_LIBRARY_PATH=build/bin:$LD_LIBRARY_PATH \\
11+
python tests/python/region_verify.py <image> <max_dim> [options]
12+
13+
Examples:
14+
15+
# Run until Ctrl+C, regions up to 256x256
16+
PYTHONPATH=build/bin LD_LIBRARY_PATH=build/bin:$LD_LIBRARY_PATH \\
17+
python tests/python/region_verify.py /path/to/image.jp2 256
18+
19+
# Run 50 iterations with a fixed seed for reproducibility
20+
PYTHONPATH=build/bin LD_LIBRARY_PATH=build/bin:$LD_LIBRARY_PATH \\
21+
python tests/python/region_verify.py /path/to/image.jp2 128 -n 50 --seed 42
22+
23+
# Max verbosity from grok library
24+
GRK_DEBUG=5 PYTHONPATH=build/bin LD_LIBRARY_PATH=build/bin:$LD_LIBRARY_PATH \\
25+
python tests/python/region_verify.py /path/to/image.jp2 64
26+
27+
Environment variables:
28+
GRK_DEBUG Grok log verbosity (1=errors, 2=+warnings, 3=+info, 5=max).
29+
Defaults to 2 if not set.
30+
31+
Arguments:
32+
image Path to a JPEG 2000 file (.jp2, .j2k, .j2c)
33+
max_dim Maximum width/height for random decode regions
34+
35+
Options:
36+
-n N Number of iterations (default: run until interrupted)
37+
--seed N Random seed for reproducibility
38+
"""
39+
40+
import argparse
41+
import ctypes
42+
import os
43+
import random
44+
import sys
45+
46+
import grok_core
47+
48+
# Enable grok warning/info logging by default (level 3 = error + warning + info)
49+
if "GRK_DEBUG" not in os.environ:
50+
os.environ["GRK_DEBUG"] = "2"
51+
52+
53+
def decompress_full(path):
54+
"""Decompress the full image synchronously. Returns (image, codec)."""
55+
stream = grok_core.grk_stream_params()
56+
stream.file = path
57+
58+
params = grok_core.grk_decompress_parameters()
59+
codec = grok_core.grk_decompress_init(stream, params)
60+
if codec is None:
61+
sys.exit(f"ERROR: failed to initialize decompressor for {path}")
62+
63+
header = grok_core.grk_header_info()
64+
if not grok_core.grk_decompress_read_header(codec, header):
65+
grok_core.grk_object_unref(codec)
66+
sys.exit("ERROR: failed to read header")
67+
68+
image = grok_core.grk_decompress_get_image(codec)
69+
if image is None:
70+
grok_core.grk_object_unref(codec)
71+
sys.exit("ERROR: failed to get image from codec")
72+
73+
if not grok_core.grk_decompress(codec, None):
74+
grok_core.grk_object_unref(codec)
75+
sys.exit("ERROR: decompression failed")
76+
77+
return image, codec
78+
79+
80+
def decompress_region(path, x0, y0, x1, y1):
81+
"""Decompress a window region. Returns (image, codec)."""
82+
stream = grok_core.grk_stream_params()
83+
stream.file = path
84+
85+
params = grok_core.grk_decompress_parameters()
86+
params.dw_x0 = float(x0)
87+
params.dw_y0 = float(y0)
88+
params.dw_x1 = float(x1)
89+
params.dw_y1 = float(y1)
90+
91+
codec = grok_core.grk_decompress_init(stream, params)
92+
if codec is None:
93+
return None, None
94+
95+
header = grok_core.grk_header_info()
96+
if not grok_core.grk_decompress_read_header(codec, header):
97+
grok_core.grk_object_unref(codec)
98+
return None, None
99+
100+
image = grok_core.grk_decompress_get_image(codec)
101+
if image is None:
102+
grok_core.grk_object_unref(codec)
103+
return None, None
104+
105+
if not grok_core.grk_decompress(codec, None):
106+
grok_core.grk_object_unref(codec)
107+
return None, None
108+
109+
return image, codec
110+
111+
112+
def get_component_array(comp):
113+
"""Return a ctypes array for a component's pixel data."""
114+
n_elements = comp.h * comp.stride
115+
if comp.data_type == grok_core.GRK_INT_16:
116+
ptr = ctypes.cast(
117+
int(comp.data), ctypes.POINTER(ctypes.c_int16 * n_elements)
118+
)
119+
else:
120+
ptr = ctypes.cast(
121+
int(comp.data), ctypes.POINTER(ctypes.c_int32 * n_elements)
122+
)
123+
return ptr.contents
124+
125+
126+
def compare_region(full_image, region_image, x0, y0):
127+
"""Compare region_image pixels against the corresponding area in full_image.
128+
129+
Returns (match, first_diff, stats) where first_diff is None if match is True,
130+
or (component, x, y, expected, actual) on first mismatch.
131+
stats is a dict with pixel value statistics for the region.
132+
"""
133+
min_val = None
134+
max_val = None
135+
n_nonzero = 0
136+
n_total = 0
137+
138+
for c in range(region_image.numcomps):
139+
full_comp = full_image.comps[c]
140+
reg_comp = region_image.comps[c]
141+
142+
full_arr = get_component_array(full_comp)
143+
reg_arr = get_component_array(reg_comp)
144+
145+
for ry in range(reg_comp.h):
146+
for rx in range(reg_comp.w):
147+
reg_val = reg_arr[ry * reg_comp.stride + rx]
148+
n_total += 1
149+
if reg_val != 0:
150+
n_nonzero += 1
151+
if min_val is None or reg_val < min_val:
152+
min_val = reg_val
153+
if max_val is None or reg_val > max_val:
154+
max_val = reg_val
155+
# Map region coords back to full image coords
156+
fx = x0 + rx
157+
fy = y0 + ry
158+
full_val = full_arr[fy * full_comp.stride + fx]
159+
if reg_val != full_val:
160+
stats = {"min": min_val, "max": max_val,
161+
"nonzero": n_nonzero, "total": n_total}
162+
return False, (c, fx, fy, full_val, reg_val), stats
163+
164+
stats = {"min": min_val, "max": max_val,
165+
"nonzero": n_nonzero, "total": n_total}
166+
return True, None, stats
167+
168+
169+
def display_first_diff(full_image, region_image, x0, y0, diff_info):
170+
"""Print info about the first differing pixel."""
171+
comp_idx, fx, fy, expected, actual = diff_info
172+
print(f" MISMATCH at component={comp_idx}, "
173+
f"full_image({fx},{fy}): expected={expected}, got={actual}")
174+
print(f" Region origin: ({x0}, {y0}), "
175+
f"region size: {region_image.comps[0].w}x{region_image.comps[0].h}")
176+
177+
178+
def main():
179+
parser = argparse.ArgumentParser(
180+
description="Verify region decompression against full-image decode."
181+
)
182+
parser.add_argument("image", help="Path to JPEG 2000 image")
183+
parser.add_argument("max_dim", type=int, help="Max dimension for random regions")
184+
parser.add_argument(
185+
"-n", "--iterations", type=int, default=0,
186+
help="Number of random regions to test (default: 0 = run until interrupted)"
187+
)
188+
parser.add_argument(
189+
"--seed", type=int, default=None,
190+
help="Random seed for reproducibility"
191+
)
192+
args = parser.parse_args()
193+
194+
if args.seed is not None:
195+
random.seed(args.seed)
196+
197+
grok_core.grk_initialize(None, 0, None)
198+
199+
print(f"Decompressing full image: {args.image}")
200+
full_image, full_codec = decompress_full(args.image)
201+
202+
img_w = full_image.comps[0].w
203+
img_h = full_image.comps[0].h
204+
num_comps = full_image.numcomps
205+
print(f"Image: {img_w}x{img_h}, {num_comps} components")
206+
207+
# Sanity check: verify full image has non-trivial data
208+
for c in range(num_comps):
209+
comp = full_image.comps[c]
210+
arr = get_component_array(comp)
211+
sample_vals = [arr[y * comp.stride + x]
212+
for y in range(0, comp.h, max(1, comp.h // 8))
213+
for x in range(0, comp.w, max(1, comp.w // 8))]
214+
n_nonzero = sum(1 for v in sample_vals if v != 0)
215+
mn = min(sample_vals)
216+
mx = max(sample_vals)
217+
print(f" Component {c}: sample min={mn}, max={mx}, "
218+
f"nonzero={n_nonzero}/{len(sample_vals)}")
219+
if mn == mx == 0:
220+
print(f" WARNING: Component {c} appears to be all zeros!")
221+
222+
if args.max_dim > img_w or args.max_dim > img_h:
223+
print(f"WARNING: max_dim ({args.max_dim}) exceeds image dimensions, "
224+
f"clamping to image size")
225+
226+
passed = 0
227+
failed = 0
228+
i = 0
229+
limit = args.iterations if args.iterations > 0 else None
230+
label = str(limit) if limit else "∞"
231+
232+
try:
233+
while limit is None or i < limit:
234+
i += 1
235+
# Generate random region dimensions
236+
rw = random.randint(1, min(args.max_dim, img_w))
237+
rh = random.randint(1, min(args.max_dim, img_h))
238+
239+
# Random origin ensuring region stays within image
240+
x0 = random.randint(0, img_w - rw)
241+
y0 = random.randint(0, img_h - rh)
242+
x1 = x0 + rw
243+
y1 = y0 + rh
244+
245+
region_image, region_codec = decompress_region(args.image, x0, y0, x1, y1)
246+
if region_image is None:
247+
print(f"[{i}/{label}] SKIP region ({x0},{y0})-({x1},{y1}) "
248+
f"- decompress failed")
249+
continue
250+
251+
match, diff_info, stats = compare_region(full_image, region_image, x0, y0)
252+
253+
if match:
254+
passed += 1
255+
all_zero = stats["nonzero"] == 0
256+
zero_warn = " [ALL ZEROS]" if all_zero else ""
257+
print(f"[{i}/{label}] OK region ({x0},{y0})-({x1},{y1}) "
258+
f"size {rw}x{rh} "
259+
f"[min={stats['min']} max={stats['max']} "
260+
f"nonzero={stats['nonzero']}/{stats['total']}]{zero_warn}")
261+
else:
262+
failed += 1
263+
print(f"[{i}/{label}] FAIL region ({x0},{y0})-({x1},{y1}) "
264+
f"size {rw}x{rh}")
265+
display_first_diff(full_image, region_image, x0, y0, diff_info)
266+
267+
grok_core.grk_object_unref(region_codec)
268+
except KeyboardInterrupt:
269+
print("\n\nInterrupted by user.")
270+
271+
grok_core.grk_object_unref(full_codec)
272+
273+
total = passed + failed
274+
print(f"\nResults: {passed} passed, {failed} failed out of {total}")
275+
sys.exit(1 if failed > 0 else 0)
276+
277+
278+
if __name__ == "__main__":
279+
main()

0 commit comments

Comments
 (0)