lsst.pipe.tasks gcf790cdeb6+441d86229e
Loading...
Searching...
No Matches
_lum_scale.py
Go to the documentation of this file.
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
FloatImagePlane __call__(self, FloatImagePlane intensities)