LSST Applications g063fba187b+cac8b7c890,g0f08755f38+6aee506743,g1653933729+a8ce1bb630,g168dd56ebc+a8ce1bb630,g1a2382251a+b4475c5878,g1dcb35cd9c+8f9bc1652e,g20f6ffc8e0+6aee506743,g217e2c1bcf+73dee94bd0,g28da252d5a+1f19c529b9,g2bbee38e9b+3f2625acfc,g2bc492864f+3f2625acfc,g3156d2b45e+6e55a43351,g32e5bea42b+1bb94961c2,g347aa1857d+3f2625acfc,g35bb328faa+a8ce1bb630,g3a166c0a6a+3f2625acfc,g3e281a1b8c+c5dd892a6c,g3e8969e208+a8ce1bb630,g414038480c+5927e1bc1e,g41af890bb2+8a9e676b2a,g7af13505b9+809c143d88,g80478fca09+6ef8b1810f,g82479be7b0+f568feb641,g858d7b2824+6aee506743,g89c8672015+f4add4ffd5,g9125e01d80+a8ce1bb630,ga5288a1d22+2903d499ea,gb58c049af0+d64f4d3760,gc28159a63d+3f2625acfc,gcab2d0539d+b12535109e,gcf0d15dbbd+46a3f46ba9,gda6a2b7d83+46a3f46ba9,gdaeeff99f8+1711a396fd,ge79ae78c31+3f2625acfc,gef2f8181fd+0a71e47438,gf0baf85859+c1f95f4921,gfa517265be+6aee506743,gfa999e8aa5+17cd334064,w.2024.51
LSST Data Management Base Package
Loading...
Searching...
No Matches
_colorMapper.py
Go to the documentation of this file.
1__all__ = ("mapUpperBounds", "latLum", "colorConstantSat", "lsstRGB", "mapUpperBounds")
2
3import logging
4import numpy as np
5import skimage
6import colour
7import cv2
8from scipy.special import erf
9from scipy.interpolate import pchip_interpolate
10
11from ._localContrast import localContrast, makeGaussianPyramid, makeLapPyramid, levelPadder
12from lsst.cpputils import fixGamutOK
13
14from numpy.typing import NDArray
15from typing import Callable, Mapping
16
17
19 values,
20 stretch: float = 400,
21 max: float = 85,
22 A: float = 1,
23 b0: float = 0.0,
24 minimum: float = 0,
25 floor: float = 0.00,
26 Q: float = 0.7,
27 doDenoise: bool = False,
28) -> NDArray:
29 """
30 Scale the input luminosity values to maximize the dynamic range visible.
31
32 Parameters
33 ----------
34 values : `NDArray`
35 The input image luminosity data of of.
36 stretch : `float`, optional
37 A parameter for the arcsinh function.
38 max : `float`, optional
39 Maximum value for intensity scaling.
40 A : `float`, optional
41 Linear scaling factor for the transformed intensities.
42 b0 : `float` optional
43 Offset term added to the arcsinh transformation.
44 minimum : `float`
45 Threshold below which pixel values are set to zero.
46 floor : `float`
47 A value added to each pixel in arcsinh transform, this ensures values in
48 the arcsinh transform will be no smaller than the supplied value.
49 Q : `float`
50 Another parameter for the arcsinh function and scaling factor for
51 softening.
52 doDenoise : `bool`
53 Denoise the image if desired.
54
55 Returns:
56 luminance : `NDArray`
57 The stretched luminosity data.
58 """
59
60 # De-noise the input image using wavelet de-noising.
61 if doDenoise:
62 values = skimage.restoration.denoise_wavelet(values)
63 values = abs(values)
64
65 # Scale values from 0-1 to 0-100 as various algorithm expect that range.
66 values *= 100
67
68 # Find what fraction of 100 the brightest pixel is. This is then used to
69 # re-normalize after all the non-linear scaling such that the brightest part
70 # of the image corresponds to the same absolute brightness.
71 maxRatio = values.max() / 100
72
73 # Calculate the slope for arcsinh transformation based on Q and stretch
74 # parameters.
75 slope = 0.1 * 100 / np.arcsinh(0.1 * Q)
76
77 # Apply the modified luminosity transformation using arcsinh function.
78 soften = Q / stretch
79 intensities = A * np.arcsinh((abs(values) * soften + floor) * slope) + b0
80
81 intensities /= intensities.max() / maxRatio
82 np.clip(intensities, 0, 1, out=intensities)
83 intensities *= 100
84
85 # Apply a specific tone cure to the luminocity defined by the below interpolant.
86 # This is calculated on the median of the image to smooth out pixel to pixel
87 # variations that are most likely due to noise. The sharpness of the image
88 # is preserved as we only apply this filter to the luminocity data.
89 # filtered = medfilt2d(intensities, 3)
90
91 control_points = (
92 [0, 0.5, 2, 5, 13.725490196078432, 25, 30, 55.294117647058826, 73.72549019607844, 98, 100],
93 [0, 10, 15, 20, 25.686274509803921, 40, 50, 80.35294117647058, 94.11764705882352, 98, 100],
94 )
95 scaled = pchip_interpolate(*control_points, intensities)
96 scaled[scaled == 0] = 1e-7
97 intensities = scaled
98 intensities[intensities > max] = max
99
100 # If values end up near 100 it's best to "bend" them a little to help
101 # the out of gamut fixer to appropriately handle luminosity and chroma
102 # issues. This is an empirically derived formula that returns
103 # scaling factors. For most of the domain it will return a value that
104 # is close to 1. Right near the upper part of the domain, it
105 # returns values slightly below 1 such that it scales a value of 100
106 # to a value near 95.
107 intensities *= (-1 * erf(-1 * (1 / intensities * 210))) ** 20
108 intensities[np.isnan(intensities)] = 0
109
110 # Reset the input array.
111 values /= 100
112
113 # Rescale the output array.
114 intensities /= 100
115
116 return intensities
117
118
120 img: NDArray, quant: float = 0.9, absMax: float | None = None, scaleBoundFactor: float | None = None
121) -> NDArray:
122 """Bound images to a range between zero and one.
123
124 Some images supplied aren't properly bounded with a maximum value of 1.
125 Either the images exceed the bounds of 1, or that no value seems to close,
126 implying indeterminate maximum value. This function determines an
127 appropriate maximum either by taking the value supplied in the absMax
128 argument or by scaling the maximum across all channels with the
129 supplied quant variable.
130
131 Parameters
132 ----------
133 img : `NDArray` like
134 Must have dimensions of y,x,3 where the channels are in RGB order
135 quant : `float`
136 Value to scale the maximum pixel value, in any channel, by to
137 determine the maximum flux allowable in all channels. Ignored
138 if absMax isn't None.
139 absMax : `float` or `None`
140 If this value is not None, use it as the maximum pixel value
141 for all channels, unless scaleBoundFactor is set in which case
142 it is only the maximum if the value determined from the image
143 and quant is larger than scaleBoundFactor times absMax. This is
144 to prevent individual frames in a mosaic from being scaled too
145 faint if absMax is too large for one region.
146 scaleBoundFactor : `float` or `None`
147 Factor used to compare absMax and the emperically determined
148 maximim. if emperical_max is less than scaleBoundFactor*absMax
149 then the emperical_max is used instead of absMax, even if it
150 is set. Set to None to skip this comparison.
151
152 Returns
153 -------
154 image : `NDArray`
155 The result of the remapping process
156 """
157 if np.max(img) == 1:
158 return img
159
160 r = img[:, :, 0]
161 g = img[:, :, 1]
162 b = img[:, :, 2]
163
164 turnover = np.max(np.vstack((r, g, b)))
165
166 # If scaleBoundFactor is not none and absMax is not None, check that the
167 # determined turnover is not less than the supplied absMax times the
168 # scaleBoundFactor. This fixes patches that may have max values much less
169 # than others for some processing reason.
170 if absMax is not None:
171 if scaleBoundFactor is not None and turnover < scaleBoundFactor * absMax:
172 scale = turnover * quant
173 else:
174 scale = absMax
175 else:
176 scale = turnover * quant
177
178 image = np.empty(img.shape)
179 image[:, :, 0] = r / scale
180 image[:, :, 1] = g / scale
181 image[:, :, 2] = b / scale
182
183 # Clip values that exceed the bound to ensure all values are within [0, absMax]
184 np.clip(image, 0, 1, out=image)
185
186 return image
187
188
190 oldLum: NDArray, luminance: NDArray, a: NDArray, b: NDArray, saturation: float = 1, maxChroma: float = 50
191) -> tuple[NDArray, NDArray]:
192 """
193 Adjusts the color saturation while keeping the hue constant.
194
195 This function adjusts the chromaticity (a, b) of colors to maintain a
196 consistent saturation level, based on their original luminance. It uses
197 the CIELAB color space representation and the `luminance` is the new target
198 luminance for all colors.
199
200 Parameters
201 ----------
202 oldLum : `NDArray`
203 Luminance values of the original colors.
204 luminance : `NDArray`
205 Target luminance values for the transformed colors.
206 a : `NDArray`
207 Chromaticity parameter 'a' corresponding to green-red axis in CIELAB.
208 b : `NDArray`
209 Chromaticity parameter 'b' corresponding to blue-yellow axis in CIELAB.
210 saturation : `float`, optional
211 Desired saturation level for the output colors. Defaults to 1.
212 maxChroma : `float`, optional
213 Maximum chroma value allowed for any color. Defaults to 50.
214
215 Returns
216 -------
217 new_a : NDArray
218 New a values representing the adjusted chromaticity.
219 new_b : NDArray
220 New b values representing the adjusted chromaticity.
221 """
222 # Calculate the square of the chroma, which is the distance from origin in
223 # the a-b plane.
224 chroma1_2 = a**2 + b**2
225 chroma1 = np.sqrt(chroma1_2)
226
227 # Calculate the hue angle, taking the absolute value to ensure non-negative
228 # angle representation.
229 chromaMask = chroma1 == 0
230 chroma1[chromaMask] = 1
231 sinHue = b / chroma1
232 cosHue = a / chroma1
233 sinHue[chromaMask] = 0
234 cosHue[chromaMask] = 0
235
236 # Compute a divisor for saturation calculation, adding 1 to avoid division
237 # by zero.
238 div = chroma1_2 + oldLum**2
239 div[div <= 0] = 1
240
241 # Calculate the square of the new chroma based on desired saturation
242 sat_original_2 = chroma1_2 / div
243 chroma2_2 = saturation * sat_original_2 * luminance**2 / (1 - sat_original_2)
244
245 # Cap the chroma to avoid excessive values that are visually unrealistic
246 chroma2_2[chroma2_2 > maxChroma**2] = maxChroma**2
247
248 # Compute new 'a' values using the square root of adjusted chroma and
249 # considering hue direction.
250 chroma2 = np.sqrt(chroma2_2)
251 new_a = chroma2 * cosHue
252
253 # Compute new 'b' values by scaling 'new_a' with the tangent of the sin
254 # angle.
255 new_b = chroma2 * sinHue
256
257 return new_a, new_b
258
259
261 Lab: NDArray,
262 colourspace: str = "Display P3",
263) -> None:
264 """Remap colors that fall outside an RGB color gamut back into it.
265
266 This function modifies the input Lab array in-place for memory reasons.
267
268 Parameters
269 ----------
270 Lab : `NDArray`
271 A NxMX3 array that contains data in the Lab colorspace.
272 colourspace : `str`
273 The target colourspace to map outlying pixels into. This must
274 correspond to an RGB colourspace understood by the colour-science
275 python package
276 """
277 # Convert back into the CIE XYZ colourspace.
278 xyz_prime = colour.Oklab_to_XYZ(Lab)
279
280 # And then back to the specified RGB colourspace.
281 rgb_prime = colour.XYZ_to_RGB(xyz_prime, colourspace=colourspace)
282
283 # Determine if there are any out of bounds pixels
284 outOfBounds = np.bitwise_or(
285 np.bitwise_or(rgb_prime[:, :, 0] > 1, rgb_prime[:, :, 1] > 1), rgb_prime[:, :, 2] > 1
286 )
287
288 # If all pixels are in bounds, return immediately.
289 if not np.any(outOfBounds):
290 logging.info("There are no out of gamut pixels.")
291 return
292
293 logging.info("There are out of gamut pixels, remapping colors")
294 results = fixGamutOK(Lab[outOfBounds])
295 logging.debug(f"The total number of remapped pixels is: {np.sum(outOfBounds)}")
296 Lab[outOfBounds] = results
297 return
298
299
300def _fuseExposure(images, sigma=0.2, maxLevel=3):
301 weights = np.zeros((len(images), *images[0].shape[:2]))
302 for i, image in enumerate(images):
303 exposure = np.exp(-((image[:, :, 0] - 0.5) ** 2) / (2 * sigma))
304
305 weights[i, :, :] = exposure
306 norm = np.sum(weights, axis=0)
307 np.divide(weights, norm, out=weights)
308
309 # loop over each image again to build pyramids
310 g_pyr = []
311 l_pyr = []
312 maxImageLevel = int(np.min(np.log2(images[0].shape[:2])))
313 if maxLevel is None:
314 maxLevel = maxImageLevel
315 if maxImageLevel < maxLevel:
316 raise ValueError(
317 f"The supplied max level {maxLevel} is is greater than the max of the image: {maxImageLevel}"
318 )
319 support = 1 << (maxLevel - 1)
320 padY_amounts = levelPadder(image.shape[0] + support, maxLevel)
321 padX_amounts = levelPadder(image.shape[1] + support, maxLevel)
322 for image, weight in zip(images, weights):
323 imagePadded = cv2.copyMakeBorder(
324 image, *(0, support), *(0, support), cv2.BORDER_REPLICATE, None, None
325 ).astype(image.dtype)
326 weightPadded = cv2.copyMakeBorder(
327 weight, *(0, support), *(0, support), cv2.BORDER_REPLICATE, None, None
328 ).astype(image.dtype)
329
330 g_pyr.append(list(makeGaussianPyramid(weightPadded, padY_amounts, padX_amounts, None)))
331 l_pyr.append(list(makeLapPyramid(imagePadded, padY_amounts, padX_amounts, None, None)))
332
333 # time to blend
334 blended = []
335 for level in range(len(padY_amounts)):
336 accumulate = np.zeros_like(l_pyr[0][level])
337 for img in range(len(g_pyr)):
338 for i in range(3):
339 accumulate[:, :, i] += l_pyr[img][level][:, :, i] * g_pyr[img][level]
340 blended.append(accumulate)
341
342 # time to reconstruct
343 output = blended[-1]
344 for i in range(-2, -1 * len(blended) - 1, -1):
345 upsampled = cv2.pyrUp(output)
346 upsampled = upsampled[
347 : upsampled.shape[0] - 2 * padY_amounts[i + 1], : upsampled.shape[1] - 2 * padX_amounts[i + 1]
348 ]
349 output = blended[i] + upsampled
350 return output[:-support, :-support]
351
352
354 img: NDArray,
355 scaleLum: Callable[..., NDArray] | None = latLum,
356 scaleLumKWargs: Mapping | None = None,
357 remapBounds: Callable | None = mapUpperBounds,
358 remapBoundsKwargs: Mapping | None = None,
359 doLocalContrast: bool = True,
360 sigma: float = 30,
361 highlights: float = -0.9,
362 shadows: float = 0.5,
363 clarity: float = 0.1,
364 maxLevel: int | None = None,
365 cieWhitePoint: tuple[float, float] = (0.28, 0.28),
366 bracket: float = 1,
367 psf: NDArray | None = None,
368):
369 # remap the bounds of the image if there is a function to do so.
370 if remapBounds is not None:
371 img = remapBounds(img, **(remapBoundsKwargs or {}))
372
373 # scale to the supplied bracket
374 img /= bracket
375
376 # Convert the starting image into the OK L*a*b* color space.
377 # https://en.wikipedia.org/wiki/Oklab_color_space
378
379 Lab = colour.XYZ_to_Oklab(
380 colour.RGB_to_XYZ(
381 img,
382 colourspace="CIE RGB",
383 illuminant=np.array(cieWhitePoint),
384 chromatic_adaptation_transform="bradford",
385 )
386 )
387 lum = Lab[:, :, 0]
388
389 # This works because lum must be between zero and one, so the max it the ratio of the max
390 maxRatio = lum.max()
391
392 # Enhance the contrast of the input image before mapping.
393 if doLocalContrast:
394 newLum = localContrast(lum, sigma, highlights, shadows, clarity=clarity, maxLevel=maxLevel)
395 # Sometimes at the faint end the shadows can be driven a bit negative.
396 # Take the abs to avoid black clipping issues.
397 newLum = abs(newLum)
398 # because contrast enhancement can change the maximum value, linearly
399 # rescale the image such that the maximum is at the same ratio as the
400 # original maximum.
401 newLum /= newLum.max() / maxRatio
402 else:
403 newLum = lum
404
405 # Scale the luminance channel if possible.
406 if scaleLum is not None:
407 lRemapped = scaleLum(newLum, **(scaleLumKWargs or {}))
408 else:
409 lRemapped = newLum
410
411 if psf is not None:
412 lRemapped = skimage.restoration.richardson_lucy(lRemapped, psf=psf, clip=False, num_iter=2)
413 return lRemapped, Lab
414
415
417 rArray: NDArray,
418 gArray: NDArray,
419 bArray: NDArray,
420 doLocalContrast: bool = True,
421 scaleLum: Callable[..., NDArray] | None = latLum,
422 scaleLumKWargs: Mapping | None = None,
423 scaleColor: Callable[..., tuple[NDArray, NDArray]] | None = colorConstantSat,
424 scaleColorKWargs: Mapping | None = None,
425 remapBounds: Callable | None = mapUpperBounds,
426 remapBoundsKwargs: Mapping | None = None,
427 cieWhitePoint: tuple[float, float] = (0.28, 0.28),
428 sigma: float = 30,
429 highlights: float = -0.9,
430 shadows: float = 0.5,
431 clarity: float = 0.1,
432 maxLevel: int | None = None,
433 psf: NDArray | None = None,
434 brackets: list[float] | None = None,
435) -> NDArray:
436 """Enhance the lightness and color preserving hue using perceptual methods.
437
438 Parameters
439 ----------
440 rArray : `NDArray`
441 The array used as the red channel
442 gArray : `NDArray`
443 The array used as the green channel
444 bArray : `NDArray`
445 The array used as the blue channel
446 doLocalContrast: `bool`
447 Apply local contrast enhancement algorithms to the luminance channel.
448 scaleLum : `Callable` or `None`
449 This is a callable that's passed the luminance values as well as
450 any defined scaleLumKWargs, and should return a scaled luminance array
451 the same shape as the input. Set to None for no scaling.
452 scaleLumKWargs : `Mapping` or `None`
453 Key word arguments that passed to the scaleLum function.
454 scaleColor : `Callable` or `None`
455 This is a callable that's passed the original luminance, the remapped
456 luminance values, the a values for each pixel, and the b values for
457 each pixel. The function is also passed any parameters defined in
458 scaleColorKWargs. This function is responsible for scaling chroma
459 values. This should return two arrays corresponding to the scaled a and
460 b values. Set to None for no modification.
461 scaleColorKWargs : `Mapping` or `None`
462 Key word arguments passed to the scaleColor function.
463 remapBounds : `Callable` or `None`
464 This is a callable that should remaps the input arrays such that each of
465 them fall within a zero to one range. This callable is given the
466 initial image as well as any parameters defined in the remapBoundsKwargs
467 parameter. Set to None for no remapping.
468 remapBoundsKwargs : `Mapping` or None
469 cieWhitePoint : `tuple` of `float`, `float`
470 This is the white point of the input of the input arrays in CIE XY
471 coordinates. Altering this affects the relative balance of colors
472 in the input image, and therefore also the output image.
473 sigma : `float`
474 The scale over which local contrast considers edges real and not noise.
475 highlights : `float`
476 A parameter that controls how local contrast enhances or reduces
477 highlights. Contrary to intuition, negative values increase highlights.
478 shadows : `float`
479 A parameter that controls how local contrast will deepen or reduce
480 shadows.
481 clarity : `float`
482 A parameter that relates to the local contrast between highlights and
483 shadow.
484 maxLevel : `int` or `None`
485 The maximum number of image pyramid levels to enhance the local contrast
486 over. Each level has a spatial scale of roughly 2^(level) pixels.
487 psf : `NDArray` or `None`
488 If this parameter is an image of a PSF kernel the luminance channel is
489 deconvolved with it. Set to None to skip deconvolution.
490 brackets : `list` of `float` or `None`
491 If a list brackets is supplied, an image will be generated at each of
492 the brackets and the results will be used in exposure fusioning to
493 increase the apparent dynamic range of the image. The image post bounds
494 remapping will be divided by each of the values specified in this list,
495 which can be used to create for instance an under, over, and ballanced
496 expoisure. Theese will then be fusioned into a final single exposure
497 selecting the proper elements from each of the images.
498
499 Returns
500 -------
501 result : `NDArray`
502 The brightness and color calibrated image.
503
504 Raises
505 ------
506 ValueError
507 Raised if the shapes of the input array don't match
508 """
509 if rArray.shape != gArray.shape or rArray.shape != bArray.shape:
510 raise ValueError("The shapes of all the input arrays must be the same")
511
512 # Construct a new image array in the proper byte ordering.
513 img = np.empty((*rArray.shape, 3))
514 img[:, :, 0] = rArray
515 img[:, :, 1] = gArray
516 img[:, :, 2] = bArray
517
518 # The image might contain pixels less than zero due to noise. The options
519 # for handling this are to either set them to zero, which creates weird
520 # holes in the scaled output image, throw an exception and have the user
521 # handle it, which they might not have to proper understanding to, or take
522 # the abs. Here the code uses the later, though this may have the effect of
523 # raising the floor of the image a bit, this isn't really a bad thing as
524 # it makes the background a grey color rather that pitch black which
525 # can cause perceptual contrast issues.
526 img = abs(img)
527
528 # If there are nan's in the image there is no real option other than to
529 # set them to zero or throw.
530 img[np.isnan(img)] = 0
531
532 if not brackets:
533 brackets = [1]
534
535 exposures = []
536 for im_num, bracket in enumerate(brackets):
537 tmp_lum, Lab = _handelLuminance(
538 img,
539 scaleLum,
540 scaleLumKWargs=scaleLumKWargs,
541 remapBounds=remapBounds,
542 remapBoundsKwargs=remapBoundsKwargs,
543 doLocalContrast=doLocalContrast,
544 sigma=sigma,
545 highlights=highlights,
546 clarity=clarity,
547 maxLevel=maxLevel,
548 cieWhitePoint=cieWhitePoint,
549 bracket=bracket,
550 psf=psf,
551 )
552 if scaleColor is not None:
553 new_a, new_b = scaleColor(
554 Lab[:, :, 0], tmp_lum, Lab[:, :, 1], Lab[:, :, 2], **(scaleColorKWargs or {})
555 )
556 # Replace the color information with the new scaled color information.
557 Lab[:, :, 1] = new_a
558 Lab[:, :, 2] = new_b
559 # Replace the luminance information with the new scaled luminance information
560 Lab[:, :, 0] = tmp_lum
561 exposures.append(Lab)
562 if len(brackets) > 1:
563 Lab = _fuseExposure(exposures)
564
565 # Fix any colors that fall outside of the RGB colour gamut.
567
568 # Transform back to RGB coordinates
569 result = colour.XYZ_to_RGB(colour.Oklab_to_XYZ(Lab), colourspace="Display P3")
570
571 # explicitly cut at 1 even though the mapping above was to map colors
572 # appropriately because the Z matrix transform can produce values above
573 # 1 and is a known feature of the transform.
574 result[result > 1] = 1
575 result[result < 0] = 0
576 return result
None fixOutOfGamutColors(NDArray Lab, str colourspace="Display P3")
_fuseExposure(images, sigma=0.2, maxLevel=3)
NDArray latLum(values, float stretch=400, float max=85, float A=1, float b0=0.0, float minimum=0, float floor=0.00, float Q=0.7, bool doDenoise=False)
NDArray mapUpperBounds(NDArray img, float quant=0.9, float|None absMax=None, float|None scaleBoundFactor=None)
tuple[NDArray, NDArray] colorConstantSat(NDArray oldLum, NDArray luminance, NDArray a, NDArray b, float saturation=1, float maxChroma=50)
NDArray lsstRGB(NDArray rArray, NDArray gArray, NDArray bArray, bool doLocalContrast=True, Callable[..., NDArray]|None scaleLum=latLum, Mapping|None scaleLumKWargs=None, Callable[..., tuple[NDArray, NDArray]]|None scaleColor=colorConstantSat, Mapping|None scaleColorKWargs=None, Callable|None remapBounds=mapUpperBounds, Mapping|None remapBoundsKwargs=None, tuple[float, float] cieWhitePoint=(0.28, 0.28), float sigma=30, float highlights=-0.9, float shadows=0.5, float clarity=0.1, int|None maxLevel=None, NDArray|None psf=None, list[float]|None brackets=None)
_handelLuminance(NDArray img, Callable[..., NDArray]|None scaleLum=latLum, Mapping|None scaleLumKWargs=None, Callable|None remapBounds=mapUpperBounds, Mapping|None remapBoundsKwargs=None, bool doLocalContrast=True, float sigma=30, float highlights=-0.9, float shadows=0.5, float clarity=0.1, int|None maxLevel=None, tuple[float, float] cieWhitePoint=(0.28, 0.28), float bracket=1, NDArray|None psf=None)