diff --git a/rawpy/_rawpy.pyx b/rawpy/_rawpy.pyx index c7c5710..d89add4 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 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 @@ -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..cde053f --- /dev/null +++ b/test/test_user_cblack.py @@ -0,0 +1,76 @@ +"""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 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 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 + + # 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'])