Skip to content

Commit c0a18f5

Browse files
committed
Add new tests, example scripts, and test improvements
- Add test/test_feature_flags.py (test rawpy.flags dict) - Add test/test_examples.py (run example scripts as subprocess) - Add test/test_user_cblack.py regression test for color balance - Improve test_basic.py: X3F skip helper, assertion improvements - Add examples/basic_process.py - Add examples/bad_pixel_repair.py - Add examples/thumbnail_extract.py
1 parent 46ede3b commit c0a18f5

File tree

7 files changed

+384
-5
lines changed

7 files changed

+384
-5
lines changed

examples/bad_pixel_repair.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
Bad Pixel Repair Example
3+
4+
Demonstrates:
5+
- Using rawpy.enhance module for bad pixel detection/repair
6+
- Loading pre-computed bad pixel coordinates
7+
- Repairing bad pixels using median interpolation
8+
9+
Note: In practice, you would first detect bad pixels using:
10+
bad_pixels = rawpy.enhance.find_bad_pixels(['image1.NEF', 'image2.NEF', ...])
11+
12+
Usage:
13+
python examples/bad_pixel_repair.py
14+
"""
15+
16+
import numpy as np
17+
import rawpy
18+
import rawpy.enhance
19+
import imageio.v3 as iio
20+
import os
21+
import sys
22+
import tempfile
23+
24+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
25+
REPO_ROOT = os.path.dirname(SCRIPT_DIR)
26+
TEST_IMAGE = os.path.join(REPO_ROOT, "test", "iss030e122639.NEF")
27+
BAD_PIXELS_FILE = os.path.join(REPO_ROOT, "test", "bad_pixels.gz")
28+
29+
30+
def main():
31+
if not os.path.exists(TEST_IMAGE):
32+
print(f"Error: Test image not found at {TEST_IMAGE}")
33+
return 1
34+
35+
# Load bad pixel coordinates (if available)
36+
if not os.path.exists(BAD_PIXELS_FILE):
37+
print(f"Bad pixel file not found: {BAD_PIXELS_FILE}")
38+
print("Skipping repair demo. In practice, you would run:")
39+
print(" bad_pixels = rawpy.enhance.find_bad_pixels([...image paths...])")
40+
return 0
41+
42+
bad_pixels = np.loadtxt(BAD_PIXELS_FILE, dtype=int)
43+
44+
print(f"Loaded {len(bad_pixels)} bad pixel coordinates")
45+
print(f"Processing: {TEST_IMAGE}")
46+
47+
with rawpy.imread(TEST_IMAGE) as raw:
48+
# Repair bad pixels in-place before postprocessing
49+
rawpy.enhance.repair_bad_pixels(raw, bad_pixels, method="median")
50+
51+
# Now postprocess the repaired data
52+
rgb = raw.postprocess()
53+
54+
output_path = os.path.join(tempfile.gettempdir(), "rawpy_repaired.tiff")
55+
iio.imwrite(output_path, rgb)
56+
print(f"Saved repaired image to: {output_path}")
57+
58+
print("Done!")
59+
return 0
60+
61+
62+
if __name__ == "__main__":
63+
sys.exit(main())

examples/basic_process.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
Basic RAW Processing Example
3+
4+
Demonstrates:
5+
- Loading a RAW file with rawpy.imread()
6+
- Converting to RGB with postprocess()
7+
- Saving the result
8+
9+
Usage:
10+
python examples/basic_process.py
11+
"""
12+
13+
import rawpy
14+
import imageio.v3 as iio
15+
import os
16+
import sys
17+
import tempfile
18+
19+
# Locate test image (works from repo root or examples/ directory)
20+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
21+
REPO_ROOT = os.path.dirname(SCRIPT_DIR)
22+
TEST_IMAGE = os.path.join(REPO_ROOT, "test", "iss030e122639.NEF")
23+
24+
25+
def main():
26+
if not os.path.exists(TEST_IMAGE):
27+
print(f"Error: Test image not found at {TEST_IMAGE}")
28+
print("This example requires the test data from the repository.")
29+
return 1
30+
31+
print(f"Loading: {TEST_IMAGE}")
32+
33+
with rawpy.imread(TEST_IMAGE) as raw:
34+
print(f" Raw type: {raw.raw_type}")
35+
print(f" Image size: {raw.sizes.width}x{raw.sizes.height}")
36+
37+
# Convert RAW to RGB using default parameters
38+
rgb = raw.postprocess()
39+
print(f" Output shape: {rgb.shape}")
40+
41+
# Save to temp directory (avoids polluting repo)
42+
output_path = os.path.join(tempfile.gettempdir(), "rawpy_basic_output.tiff")
43+
iio.imwrite(output_path, rgb)
44+
print(f" Saved to: {output_path}")
45+
46+
print("Done!")
47+
return 0
48+
49+
50+
if __name__ == "__main__":
51+
sys.exit(main())

examples/thumbnail_extract.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
Thumbnail Extraction Example
3+
4+
Demonstrates:
5+
- Extracting embedded JPEG thumbnails from RAW files
6+
- Handling different thumbnail formats (JPEG vs BITMAP)
7+
- Error handling for missing/unsupported thumbnails
8+
9+
Usage:
10+
python examples/thumbnail_extract.py
11+
"""
12+
13+
import rawpy
14+
import imageio.v3 as iio
15+
import os
16+
import sys
17+
import tempfile
18+
19+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
20+
REPO_ROOT = os.path.dirname(SCRIPT_DIR)
21+
TEST_IMAGE = os.path.join(REPO_ROOT, "test", "iss030e122639.NEF")
22+
23+
24+
def main():
25+
if not os.path.exists(TEST_IMAGE):
26+
print(f"Error: Test image not found at {TEST_IMAGE}")
27+
return 1
28+
29+
print(f"Extracting thumbnail from: {TEST_IMAGE}")
30+
31+
with rawpy.imread(TEST_IMAGE) as raw:
32+
try:
33+
thumb = raw.extract_thumb()
34+
except rawpy.LibRawNoThumbnailError:
35+
print("No thumbnail embedded in this file.")
36+
return 0
37+
except rawpy.LibRawUnsupportedThumbnailError:
38+
print("Thumbnail format not supported.")
39+
return 0
40+
41+
print(f" Thumbnail format: {thumb.format}")
42+
43+
output_dir = tempfile.gettempdir()
44+
45+
if thumb.format == rawpy.ThumbFormat.JPEG:
46+
output_path = os.path.join(output_dir, "rawpy_thumb.jpg")
47+
with open(output_path, "wb") as f:
48+
f.write(thumb.data)
49+
print(f" Saved JPEG to: {output_path}")
50+
51+
elif thumb.format == rawpy.ThumbFormat.BITMAP:
52+
output_path = os.path.join(output_dir, "rawpy_thumb.tiff")
53+
iio.imwrite(output_path, thumb.data)
54+
print(f" Saved TIFF to: {output_path}")
55+
56+
print("Done!")
57+
return 0
58+
59+
60+
if __name__ == "__main__":
61+
sys.exit(main())

test/test_basic.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616

1717
thisDir = os.path.dirname(__file__)
1818

19+
_x3f_supported = rawpy.flags is not None and rawpy.flags.get("X3FTOOLS", False)
20+
21+
def _open_x3f():
22+
"""Open X3F test file, skip test if format not supported by this LibRaw build."""
23+
if not _x3f_supported:
24+
pytest.skip("X3F format not supported by this LibRaw build")
25+
return rawpy.imread(raw4TestPath)
26+
1927
# Nikon D3S
2028
rawTestPath = os.path.join(thisDir, 'iss030e122639.NEF')
2129
badPixelsTestPath = os.path.join(thisDir, 'bad_pixels.gz')
@@ -73,7 +81,7 @@ def testFileOpenAndPostProcess():
7381
iio.imwrite('test_16daylight_linear.tiff', rgb)
7482

7583
def testFoveonFileOpenAndPostProcess():
76-
raw = rawpy.imread(raw4TestPath)
84+
raw = _open_x3f()
7785

7886
assert_array_equal(raw.raw_image.shape, [1531, 2304, 3])
7987
iio.imwrite('test_foveon_raw.tiff', raw.raw_image)
@@ -145,7 +153,7 @@ def testThumbExtractJPEG():
145153
assert_array_equal(img.shape, [2832, 4256, 3])
146154

147155
def testThumbExtractBitmap():
148-
with rawpy.imread(raw4TestPath) as raw:
156+
with _open_x3f() as raw:
149157
thumb = raw.extract_thumb()
150158
assert thumb.format == rawpy.ThumbFormat.BITMAP
151159
assert isinstance(thumb.data, np.ndarray)
@@ -171,11 +179,13 @@ def testBayerPattern():
171179
for path in [rawTestPath, raw2TestPath]:
172180
raw = rawpy.imread(path)
173181
assert_equal(raw.color_desc, expected_desc)
174-
assert_array_equal(raw.raw_pattern, [[0,1],[3,2]])
182+
assert raw.raw_pattern is not None
183+
assert_array_equal(raw.raw_pattern, np.array([[0,1],[3,2]], dtype=np.uint8))
175184

176185
raw = rawpy.imread(raw3TestPath)
177186
assert_equal(raw.color_desc, expected_desc)
178-
assert_array_equal(raw.raw_pattern, [[3,2],[0,1]])
187+
assert raw.raw_pattern is not None
188+
assert_array_equal(raw.raw_pattern, np.array([[3,2],[0,1]], dtype=np.uint8))
179189

180190
def testAutoWhiteBalance():
181191
# Test that auto_whitebalance returns None before postprocessing
@@ -281,7 +291,7 @@ def testCropSizeCanon():
281291
assert_equal(s.crop_height, 3744)
282292

283293
def testCropSizeSigma():
284-
with rawpy.imread(raw4TestPath) as raw:
294+
with _open_x3f() as raw:
285295
s = raw.sizes
286296
assert_equal(s.crop_left_margin, 0)
287297
assert_equal(s.crop_top_margin, 0)

test/test_examples.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""
2+
Test that example scripts run without errors.
3+
4+
Each example is executed as a subprocess so it runs exactly as a user would.
5+
"""
6+
7+
import os
8+
import subprocess
9+
import sys
10+
11+
import pytest
12+
13+
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14+
EXAMPLES_DIR = os.path.join(REPO_ROOT, "examples")
15+
TEST_IMAGE = os.path.join(REPO_ROOT, "test", "iss030e122639.NEF")
16+
17+
needs_test_image = pytest.mark.skipif(
18+
not os.path.exists(TEST_IMAGE),
19+
reason="test image not available",
20+
)
21+
22+
skip_examples = pytest.mark.skipif(
23+
os.environ.get("RAWPY_SKIP_EXAMPLES", "") == "1",
24+
reason="RAWPY_SKIP_EXAMPLES is set (e.g., slow QEMU emulation)",
25+
)
26+
27+
pytestmark = skip_examples
28+
29+
30+
def run_example(script_name: str) -> subprocess.CompletedProcess:
31+
"""Run an example script and return the result."""
32+
script_path = os.path.join(EXAMPLES_DIR, script_name)
33+
return subprocess.run(
34+
[sys.executable, script_path],
35+
capture_output=True,
36+
text=True,
37+
timeout=120,
38+
)
39+
40+
41+
@needs_test_image
42+
def test_basic_process():
43+
result = run_example("basic_process.py")
44+
assert result.returncode == 0, result.stderr
45+
46+
47+
@needs_test_image
48+
def test_thumbnail_extract():
49+
result = run_example("thumbnail_extract.py")
50+
assert result.returncode == 0, result.stderr
51+
52+
53+
@needs_test_image
54+
def test_bad_pixel_repair():
55+
result = run_example("bad_pixel_repair.py")
56+
assert result.returncode == 0, result.stderr

0 commit comments

Comments
 (0)