Coverage for python / lsst / pipe / tasks / prettyPictureMaker / _functors / _color_scale.py: 37%

33 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-18 09:04 +0000

1# This file is part of pipe_tasks. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("ColorScaler",) 

25 

26import numpy as np 

27 

28from lsst.pipe.tasks.prettyPictureMaker.types import FloatImagePlane 

29from lsst.pex.config.configurableActions import ConfigurableAction 

30from lsst.pex.config import Field, ListField 

31 

32from .._equalizers import contrast_equalizer 

33 

34 

35class ColorScaler(ConfigurableAction): 

36 saturation = Field[float]( 

37 doc=( 

38 "The overall saturation factor with the scaled luminance between zero and one. " 

39 "A value of one is not recommended as it makes bright pixels very saturated" 

40 ), 

41 default=0.6, 

42 ) 

43 maxChroma = Field[float]( 

44 doc=( 

45 "The maximum chromaticity in the OKLCh color space, large " 

46 "values will cause bright pixels to fall outside the RGB gamut." 

47 ), 

48 default=0.4, 

49 ) 

50 brightFalloff = Field[float]( 

51 doc="The rate that saturation scaling should slow with brightness", default=800 

52 ) 

53 equalizer_levels = ListField[float]( 

54 doc=( 

55 "A list of factors to modify the color constrast in a scale dependent way. " 

56 "One coefficient for each spatial scale, starting from the largest. " 

57 "Values large than 1 increase contrast, while less than 1 decreases" 

58 "Only scales upto and including the largest to be modified need specified" 

59 "IE [1.1,0.9] modifieds the first two [1.1,1,0.9] modifies the first three." 

60 ), 

61 optional=True, 

62 ) 

63 

64 def __call__( 

65 self, old_lum: FloatImagePlane, new_lum: FloatImagePlane, a: FloatImagePlane, b: FloatImagePlane 

66 ) -> tuple[FloatImagePlane, FloatImagePlane]: 

67 """ 

68 Adjusts the color saturation while keeping the hue constant. 

69 

70 This function adjusts the chromaticity (a, b) of colors to maintain a 

71 consistent saturation level, based on their original luminance. It uses 

72 the CIELAB color space representation and the `luminance` is the new target 

73 luminance for all colors. 

74 

75 Parameters 

76 ---------- 

77 old_lum : `FloatImagePlane` 

78 Luminance values of the original colors. 

79 new_lum : `FloatImagePlane` 

80 Target luminance values for the transformed colors. 

81 a : `FloatImagePlane` 

82 Chromaticity parameter 'a' corresponding to green-red axis in CIELAB. 

83 b : `FloatImagePlane` 

84 Chromaticity parameter 'b' corresponding to blue-yellow axis in CIELAB. 

85 

86 Returns 

87 ------- 

88 new_a : `FloatImagePlane` 

89 New a values representing the adjusted chromaticity. 

90 new_b : `FloatImagePlane` 

91 New b values representing the adjusted chromaticity. 

92 """ 

93 # Calculate the square of the chroma, which is the distance from origin in 

94 # the a-b plane. 

95 chroma1_2 = a**2 + b**2 

96 chroma1 = np.sqrt(chroma1_2) 

97 

98 # Calculate the hue angle, taking the absolute value to ensure non-negative 

99 # angle representation. 

100 chromaMask = chroma1 == 0 

101 chroma1[chromaMask] = 1 

102 # Calculate sine and cosine of hue angle from chromaticity coordinates. 

103 # In CIELAB a-b plane: sin(hue) = b/chroma, cos(hue) = a/chroma 

104 # These are used below to preserve hue while adjusting chroma. 

105 sinHue = b / chroma1 

106 cosHue = a / chroma1 

107 sinHue[chromaMask] = 0 

108 cosHue[chromaMask] = 0 

109 

110 # Compute a divisor for saturation calculation, adding 1 to avoid division 

111 # by zero. 

112 div = chroma1_2 + old_lum**2 

113 div[div <= 0] = 1 

114 

115 # Calculate the square of the new chroma based on desired saturation 

116 sat_original_2 = chroma1_2 / div 

117 # chroma2_2 = self.saturation * sat_original_2 * new_lum**2 / (1 - sat_original_2) 

118 correction_factor = np.arcsinh(abs(new_lum - 1) * self.brightFalloff) / np.arcsinh(self.brightFalloff) 

119 chroma2_2 = ( 

120 self.saturation 

121 * correction_factor**2 

122 * sat_original_2 

123 * new_lum**2 

124 / (1 - sat_original_2 * correction_factor**2) 

125 ) 

126 

127 # Compute new 'a' values using the square root of adjusted chroma and 

128 # considering hue direction. 

129 chroma2 = np.sqrt(chroma2_2) 

130 if self.equalizer_levels is not None: 

131 chroma2 = contrast_equalizer(chroma2, self.equalizer_levels) 

132 # Cap the chroma to avoid excessive values that are visually unrealistic 

133 chroma2[chroma2 > self.maxChroma] = self.maxChroma 

134 

135 new_a = chroma2 * cosHue 

136 

137 # Compute new 'b' values using the root of the adjusted chroma and hue 

138 # direction. 

139 new_b = chroma2 * sinHue 

140 

141 return new_a, new_b