From 1b0ed5012a2a25625e78230721e113e1646a5268 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:05:00 +0000 Subject: [PATCH 1/5] Initial plan From 80027dcdd191ae544211017f75299e7d684fe906 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:13:11 +0000 Subject: [PATCH 2/5] Add user_cblack parameter for per-channel black level corrections Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- rawpy/_rawpy.pyx | 12 +++++- test/test_user_cblack.py | 83 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 test/test_user_cblack.py diff --git a/rawpy/_rawpy.pyx b/rawpy/_rawpy.pyx index c7c5710..30b1064 100644 --- a/rawpy/_rawpy.pyx +++ b/rawpy/_rawpy.pyx @@ -941,6 +941,9 @@ cdef class RawPy: p.output_bps = params.output_bps p.user_flip = params.user_flip p.user_black = params.user_black + if params.user_cblack: + for i in range(4): + p.user_cblack[i] = params.user_cblack[i] p.user_sat = params.user_sat p.no_auto_bright = params.no_auto_bright p.no_auto_scale = params.no_auto_scale @@ -1101,7 +1104,7 @@ class Params(object): noise_thr=None, median_filter_passes=0, use_camera_wb=False, use_auto_wb=False, user_wb=None, output_color=ColorSpace.sRGB, output_bps=8, - user_flip=None, user_black=None, user_sat=None, + user_flip=None, user_black=None, user_cblack=None, user_sat=None, no_auto_bright=False, auto_bright_thr=None, adjust_maximum_thr=0.75, bright=1.0, highlight_mode=HighlightMode.Clip, exp_shift=None, exp_preserve_highlights=0.0, no_auto_scale=False, @@ -1130,6 +1133,8 @@ class Params(object): :param int user_flip: 0=none, 3=180, 5=90CCW, 6=90CW, default is to use image orientation from the RAW image if available :param int user_black: custom black level + :param list user_cblack: list of length 4 with per-channel black levels for [R, G, B, G2]. + If provided, this overrides user_black. :param int user_sat: saturation adjustment (custom white level) :param bool no_auto_scale: Whether to disable pixel value scaling :param bool no_auto_bright: whether to disable automatic increase of brightness @@ -1174,6 +1179,11 @@ class Params(object): self.output_bps = output_bps self.user_flip = user_flip if user_flip is not None else -1 self.user_black = user_black if user_black is not None else -1 + if user_cblack is not None: + assert len(user_cblack) == 4 + self.user_cblack = user_cblack + else: + self.user_cblack = [0, 0, 0, 0] self.user_sat = user_sat if user_sat is not None else -1 self.no_auto_bright = no_auto_bright self.no_auto_scale = no_auto_scale diff --git a/test/test_user_cblack.py b/test/test_user_cblack.py new file mode 100644 index 0000000..2c8874d --- /dev/null +++ b/test/test_user_cblack.py @@ -0,0 +1,83 @@ +"""Tests for per-channel black level corrections (user_cblack parameter).""" +from __future__ import division, print_function, absolute_import + +import os +import pytest +import numpy as np +from numpy.testing import assert_array_equal + +import rawpy + +thisDir = os.path.dirname(__file__) +rawTestPath = os.path.join(thisDir, 'iss030e122639.NEF') + + +def test_user_cblack_parameter_acceptance(): + """Test that the user_cblack parameter is accepted in Params constructor.""" + # Test with valid 4-element list + params = rawpy.Params(user_cblack=[100, 200, 150, 200]) + assert params.user_cblack == [100, 200, 150, 200] + + # Test with None (default) + params = rawpy.Params() + assert params.user_cblack == [0, 0, 0, 0] + + +def test_user_cblack_parameter_validation(): + """Test that user_cblack parameter validates list length.""" + # Should raise assertion error for wrong length + with pytest.raises(AssertionError): + rawpy.Params(user_cblack=[100, 200]) + + with pytest.raises(AssertionError): + rawpy.Params(user_cblack=[100, 200, 150]) + + with pytest.raises(AssertionError): + rawpy.Params(user_cblack=[100, 200, 150, 200, 250]) + + +def test_user_cblack_postprocess(): + """Test that user_cblack can be used in postprocessing without errors.""" + with rawpy.imread(rawTestPath) as raw: + # Process with per-channel black levels + rgb = raw.postprocess(user_cblack=[100, 100, 100, 100], no_auto_bright=True) + assert rgb.shape[2] == 3 # RGB image + + # Process with different per-channel values + rgb2 = raw.postprocess(user_cblack=[50, 100, 150, 100], no_auto_bright=True) + assert rgb2.shape[2] == 3 + + # Images should be different when different black levels are applied + assert not np.array_equal(rgb, rgb2) + + +def test_user_cblack_vs_user_black(): + """Test that user_cblack and user_black can both be used.""" + with rawpy.imread(rawTestPath) as raw: + # Process with single black level + rgb_single = raw.postprocess(user_black=100, no_auto_bright=True) + + # Process with per-channel black levels (all same value) + rgb_multi = raw.postprocess(user_cblack=[100, 100, 100, 100], no_auto_bright=True) + + # When all channels have the same value, results should be similar + # (might not be exactly equal due to processing differences) + assert rgb_single.shape == rgb_multi.shape + + +def test_user_cblack_with_other_params(): + """Test that user_cblack works correctly with other parameters.""" + with rawpy.imread(rawTestPath) as raw: + # Combine user_cblack with various other parameters + rgb = raw.postprocess( + user_cblack=[100, 150, 100, 150], + no_auto_bright=True, + output_bps=16, + use_camera_wb=True + ) + assert rgb.dtype == np.uint16 + assert rgb.shape[2] == 3 + + +if __name__ == '__main__': + pytest.main([__file__, '-v']) From 0db69907c9f0479a35a084b75546053463077547 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Tue, 27 Jan 2026 23:22:33 +0000 Subject: [PATCH 3/5] Apply suggestion from @letmaik --- rawpy/_rawpy.pyx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rawpy/_rawpy.pyx b/rawpy/_rawpy.pyx index 30b1064..ee2b3e0 100644 --- a/rawpy/_rawpy.pyx +++ b/rawpy/_rawpy.pyx @@ -1133,8 +1133,7 @@ class Params(object): :param int user_flip: 0=none, 3=180, 5=90CCW, 6=90CW, default is to use image orientation from the RAW image if available :param int user_black: custom black level - :param list user_cblack: list of length 4 with per-channel black levels for [R, G, B, G2]. - If provided, this overrides user_black. + :param list user_cblack: list of length 4 with per-channel corrections to user_black. :param int user_sat: saturation adjustment (custom white level) :param bool no_auto_scale: Whether to disable pixel value scaling :param bool no_auto_bright: whether to disable automatic increase of brightness From d64ef5c4024e2bedc973af605fc9017650a7a963 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Tue, 27 Jan 2026 23:26:35 +0000 Subject: [PATCH 4/5] Update test/test_user_cblack.py --- test/test_user_cblack.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/test/test_user_cblack.py b/test/test_user_cblack.py index 2c8874d..2c2fa58 100644 --- a/test/test_user_cblack.py +++ b/test/test_user_cblack.py @@ -64,20 +64,5 @@ def test_user_cblack_vs_user_black(): # (might not be exactly equal due to processing differences) assert rgb_single.shape == rgb_multi.shape - -def test_user_cblack_with_other_params(): - """Test that user_cblack works correctly with other parameters.""" - with rawpy.imread(rawTestPath) as raw: - # Combine user_cblack with various other parameters - rgb = raw.postprocess( - user_cblack=[100, 150, 100, 150], - no_auto_bright=True, - output_bps=16, - use_camera_wb=True - ) - assert rgb.dtype == np.uint16 - assert rgb.shape[2] == 3 - - if __name__ == '__main__': pytest.main([__file__, '-v']) From 262d6216a98f511777b3b4058e16dc80f5057ca2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:29:55 +0000 Subject: [PATCH 5/5] Update test to use user_black and user_cblack together; clarify documentation Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- rawpy/_rawpy.pyx | 1 + test/test_user_cblack.py | 26 +++++++++++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/rawpy/_rawpy.pyx b/rawpy/_rawpy.pyx index ee2b3e0..d89add4 100644 --- a/rawpy/_rawpy.pyx +++ b/rawpy/_rawpy.pyx @@ -1134,6 +1134,7 @@ class Params(object): default is to use image orientation from the RAW image if available :param int user_black: custom black level :param list user_cblack: list of length 4 with per-channel corrections to user_black. + These are offsets applied on top of user_black for [R, G, B, G2] channels. :param int user_sat: saturation adjustment (custom white level) :param bool no_auto_scale: Whether to disable pixel value scaling :param bool no_auto_bright: whether to disable automatic increase of brightness diff --git a/test/test_user_cblack.py b/test/test_user_cblack.py index 2c2fa58..cde053f 100644 --- a/test/test_user_cblack.py +++ b/test/test_user_cblack.py @@ -52,17 +52,25 @@ def test_user_cblack_postprocess(): def test_user_cblack_vs_user_black(): - """Test that user_cblack and user_black can both be used.""" + """Test that user_cblack and user_black can be used together in a single call. + + user_cblack values are corrections/offsets applied on top of user_black. + For example: user_black=100, user_cblack=[10, 20, 30, 20] results in + effective black levels of [110, 120, 130, 120] for each channel. + """ with rawpy.imread(rawTestPath) as raw: - # Process with single black level - rgb_single = raw.postprocess(user_black=100, no_auto_bright=True) - - # Process with per-channel black levels (all same value) - rgb_multi = raw.postprocess(user_cblack=[100, 100, 100, 100], no_auto_bright=True) + # Process with both user_black and user_cblack in a single call + # user_cblack provides per-channel corrections on top of user_black base value + rgb = raw.postprocess( + user_black=100, + user_cblack=[10, 20, 30, 20], + no_auto_bright=True + ) + assert rgb.shape[2] == 3 # RGB image - # When all channels have the same value, results should be similar - # (might not be exactly equal due to processing differences) - assert rgb_single.shape == rgb_multi.shape + # Verify that using both parameters together works without errors + # and produces a valid image + assert rgb.dtype == np.uint8 # Default output_bps is 8 if __name__ == '__main__': pytest.main([__file__, '-v'])