Skip to content

Commit 5fa29ff

Browse files
Copilotletmaik
andauthored
Expose per-channel black level corrections via user_cblack parameter (#278)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> Co-authored-by: Maik Riechert <letmaik@outlook.com>
1 parent fbfe66b commit 5fa29ff

File tree

2 files changed

+87
-1
lines changed

2 files changed

+87
-1
lines changed

rawpy/_rawpy.pyx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,9 @@ cdef class RawPy:
10001000
p.output_bps = params.output_bps
10011001
p.user_flip = params.user_flip
10021002
p.user_black = params.user_black
1003+
if params.user_cblack:
1004+
for i in range(4):
1005+
p.user_cblack[i] = params.user_cblack[i]
10031006
p.user_sat = params.user_sat
10041007
p.no_auto_bright = params.no_auto_bright
10051008
p.no_auto_scale = params.no_auto_scale
@@ -1160,7 +1163,7 @@ class Params(object):
11601163
noise_thr=None, median_filter_passes=0,
11611164
use_camera_wb=False, use_auto_wb=False, user_wb=None,
11621165
output_color=ColorSpace.sRGB, output_bps=8,
1163-
user_flip=None, user_black=None, user_sat=None,
1166+
user_flip=None, user_black=None, user_cblack=None, user_sat=None,
11641167
no_auto_bright=False, auto_bright_thr=None, adjust_maximum_thr=0.75,
11651168
bright=1.0, highlight_mode=HighlightMode.Clip,
11661169
exp_shift=None, exp_preserve_highlights=0.0, no_auto_scale=False,
@@ -1189,6 +1192,8 @@ class Params(object):
11891192
:param int user_flip: 0=none, 3=180, 5=90CCW, 6=90CW,
11901193
default is to use image orientation from the RAW image if available
11911194
:param int user_black: custom black level
1195+
:param list user_cblack: list of length 4 with per-channel corrections to user_black.
1196+
These are offsets applied on top of user_black for [R, G, B, G2] channels.
11921197
:param int user_sat: saturation adjustment (custom white level)
11931198
:param bool no_auto_scale: Whether to disable pixel value scaling
11941199
:param bool no_auto_bright: whether to disable automatic increase of brightness
@@ -1233,6 +1238,11 @@ class Params(object):
12331238
self.output_bps = output_bps
12341239
self.user_flip = user_flip if user_flip is not None else -1
12351240
self.user_black = user_black if user_black is not None else -1
1241+
if user_cblack is not None:
1242+
assert len(user_cblack) == 4
1243+
self.user_cblack = user_cblack
1244+
else:
1245+
self.user_cblack = [0, 0, 0, 0]
12361246
self.user_sat = user_sat if user_sat is not None else -1
12371247
self.no_auto_bright = no_auto_bright
12381248
self.no_auto_scale = no_auto_scale

test/test_user_cblack.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Tests for per-channel black level corrections (user_cblack parameter)."""
2+
from __future__ import division, print_function, absolute_import
3+
4+
import os
5+
import pytest
6+
import numpy as np
7+
from numpy.testing import assert_array_equal
8+
9+
import rawpy
10+
11+
thisDir = os.path.dirname(__file__)
12+
rawTestPath = os.path.join(thisDir, 'iss030e122639.NEF')
13+
14+
15+
def test_user_cblack_parameter_acceptance():
16+
"""Test that the user_cblack parameter is accepted in Params constructor."""
17+
# Test with valid 4-element list
18+
params = rawpy.Params(user_cblack=[100, 200, 150, 200])
19+
assert params.user_cblack == [100, 200, 150, 200]
20+
21+
# Test with None (default)
22+
params = rawpy.Params()
23+
assert params.user_cblack == [0, 0, 0, 0]
24+
25+
26+
def test_user_cblack_parameter_validation():
27+
"""Test that user_cblack parameter validates list length."""
28+
# Should raise assertion error for wrong length
29+
with pytest.raises(AssertionError):
30+
rawpy.Params(user_cblack=[100, 200])
31+
32+
with pytest.raises(AssertionError):
33+
rawpy.Params(user_cblack=[100, 200, 150])
34+
35+
with pytest.raises(AssertionError):
36+
rawpy.Params(user_cblack=[100, 200, 150, 200, 250])
37+
38+
39+
def test_user_cblack_postprocess():
40+
"""Test that user_cblack can be used in postprocessing without errors."""
41+
with rawpy.imread(rawTestPath) as raw:
42+
# Process with per-channel black levels
43+
rgb = raw.postprocess(user_cblack=[100, 100, 100, 100], no_auto_bright=True)
44+
assert rgb.shape[2] == 3 # RGB image
45+
46+
# Process with different per-channel values
47+
rgb2 = raw.postprocess(user_cblack=[50, 100, 150, 100], no_auto_bright=True)
48+
assert rgb2.shape[2] == 3
49+
50+
# Images should be different when different black levels are applied
51+
assert not np.array_equal(rgb, rgb2)
52+
53+
54+
def test_user_cblack_vs_user_black():
55+
"""Test that user_cblack and user_black can be used together in a single call.
56+
57+
user_cblack values are corrections/offsets applied on top of user_black.
58+
For example: user_black=100, user_cblack=[10, 20, 30, 20] results in
59+
effective black levels of [110, 120, 130, 120] for each channel.
60+
"""
61+
with rawpy.imread(rawTestPath) as raw:
62+
# Process with both user_black and user_cblack in a single call
63+
# user_cblack provides per-channel corrections on top of user_black base value
64+
rgb = raw.postprocess(
65+
user_black=100,
66+
user_cblack=[10, 20, 30, 20],
67+
no_auto_bright=True
68+
)
69+
assert rgb.shape[2] == 3 # RGB image
70+
71+
# Verify that using both parameters together works without errors
72+
# and produces a valid image
73+
assert rgb.dtype == np.uint8 # Default output_bps is 8
74+
75+
if __name__ == '__main__':
76+
pytest.main([__file__, '-v'])

0 commit comments

Comments
 (0)