Coverage for python / lsst / pipe / tasks / prettyPictureMaker / _functors / _lum_scale.py: 46%

46 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-07 08:39 +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__ = ("LumCompressor",) 

25 

26import skimage 

27import numpy as np 

28import logging 

29 

30 

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

32from lsst.pex.config.configurableActions import ConfigurableAction 

33from lsst.pex.config import Field, ListField 

34from lsst.rubinoxide import rgb 

35 

36from .._equalizers import contrast_equalizer, tone_equalizer 

37 

38logger = logging.getLogger(__name__) 

39 

40 

41class LumCompressor(ConfigurableAction): 

42 """Compress and enhance luminance using multi-stage processing. 

43 

44 This class implements luminance compression for RGB image generation using 

45 a multi-stage algorithm that includes: 

46 

47 - Asinh stretching for non-linear brightness mapping 

48 - Linear contrast manipulation 

49 - Midtone adjustment 

50 - Optional Gaussian denoising 

51 - Optional contrast equalization 

52 - Optional tone adjustment 

53 

54 The configuration fields control the parameters for each stage of the 

55 processing pipeline. 

56 """ 

57 

58 stretch = Field[float](doc="The stretch of the luminance in asinh", default=400) 

59 max = Field[float](doc="The maximum allowed luminance on a 0 to 1 scale", default=0.85) 

60 floor = Field[float](doc="A value in nJy that is used to map luminances to a very dark gray", default=0.0) 

61 Q = Field[float]( 

62 doc="softening parameter", 

63 default=0.7, 

64 deprecated="This field is no longer used and will be removed after v31.", 

65 ) 

66 highlight = Field[float]( 

67 doc="The value of highlights in scaling factor applied to post asinh streaching", default=1.0 

68 ) 

69 shadow = Field[float]( 

70 doc="The value of shadows in scaling factor applied to post asinh streaching", default=0.0 

71 ) 

72 midtone = Field[float]( 

73 doc="The value of midtone in scaling factor applied to post asinh streaching", default=0.5 

74 ) 

75 equalizerLevels = ListField[float]( 

76 doc=( 

77 "A list of factors to modify the constrast in a scale-dependent way. " 

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

79 "Values large than 1 increase contrast, while less than 1 decreases. " 

80 "This adjustment is multiplicative. " 

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

82 "i.e. [1.1,0.9] modifies the first two [1.1,1,0.9] modifies the first three." 

83 ), 

84 optional=True, 

85 ) 

86 toneAdjustment = ListField[float]( 

87 doc=( 

88 "A list of length 10 that adjusts the brightness of the image ranging " 

89 "from dark regions to light. These 10 values represent control points along " 

90 "the lumanance interval 0-1, but the actual adjustments made are continuous " 

91 "and are calculated from these control points." 

92 ), 

93 length=10, 

94 optional=True, 

95 ) 

96 toneWidth = Field[float]( 

97 doc=( 

98 "This parameters controls how each tone control point affect the adjustment " 

99 "of the values in between. Increase the value to have a more continuous " 

100 "change between control points, decrease to make the control sharper. Value " 

101 "must be greater than zero." 

102 ), 

103 default=0.07, 

104 ) 

105 doDenoise = Field[bool](doc="Denoise the luminance image", default=False) 

106 

107 def __call__(self, intensities: FloatImagePlane) -> FloatImagePlane: 

108 """Compress and enhance luminance using multi-stage processing. 

109 

110 This method applies the configured luminance compression algorithm to 

111 the input image. The processing pipeline includes optional denoising, 

112 asinh stretching, linear contrast manipulation, midtone adjustment, 

113 contrast equalization, and tone adjustment. 

114 

115 Parameters 

116 ---------- 

117 intensities : `FloatImagePlane` 

118 Input image with pixel intensities. This FloatImagePlane should 

119 contain the luminance data to be compressed. 

120 

121 Returns 

122 ------- 

123 result : `FloatImagePlane` 

124 The processed image with luminance compression applied. Values 

125 are clipped to the range [0, 1]. 

126 

127 Notes 

128 ----- 

129 The processing pipeline consists of the following stages: 

130 

131 1. Optional wavelet denoising if doDenoise is True 

132 2. Asinh stretching with configurable stretch parameter 

133 3. Linear contrast adjustment using highlight, shadow parameters 

134 4. Midtone adjustment using midtone parameter 

135 5. Optional contrast equalization if equalizerLevels is configured 

136 6. Optional tone adjustment if toneAdjustment is configured 

137 7. Final clipping to [0, 1] range 

138 

139 The configuration fields (stretch, highlight, shadow, midtone, 

140 equalizerLevels, toneAdjustment, toneWidth) control the behavior 

141 of each processing stage. 

142 """ 

143 if self.doDenoise: 

144 intensities = skimage.restoration.denoise_wavelet(intensities) 

145 

146 # Scale the values with linear manipulation for contrast 

147 intensities = abs(intensities) 

148 nj_to_lum = rgb.RGB_to_Oklab( 

149 np.array([[[self.floor, self.floor, self.floor]]], dtype=float), (0.28, 0.28) 

150 )[0, 0, 0] 

151 top = np.arcsinh(self.stretch) 

152 bottom = (np.arcsinh(nj_to_lum * self.stretch) - 0.2 * top) / 0.8 

153 intensities = (np.arcsinh(intensities * self.stretch) - bottom) / (top - bottom) 

154 logger.debug("arcsinh max %.4f", intensities.max()) 

155 intensities = np.clip(intensities, 0, 1) 

156 intensities = (intensities - self.shadow) / ((self.highlight) - self.shadow) 

157 logger.debug("post lin stretch max %.4f", intensities.max()) 

158 intensities = ((self.midtone - 1) * intensities) / ( 

159 ((2 * self.midtone - 1) * intensities) - self.midtone 

160 ) 

161 logger.debug("midtone adjustment max %.4f", intensities.max()) 

162 intensities = np.clip(intensities, 0.0, self.max) 

163 

164 if self.equalizerLevels is not None: 

165 intensities = contrast_equalizer(intensities, self.equalizerLevels) 

166 logger.debug("equalizer max %.4f", intensities.max()) 

167 

168 if self.toneAdjustment is not None: 

169 intensities = np.clip(intensities, 0, self.max) 

170 intensities = tone_equalizer(intensities, self.toneAdjustment, self.toneWidth, 10, 5) 

171 

172 intensities = np.clip(intensities, 0, 1) 

173 

174 return intensities