Coverage for python / lsst / pipe / tasks / prettyPictureMaker / _functors / _local_contrast.py: 71%
47 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 18:38 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 18:38 +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/>.
22from __future__ import annotations
24__all__ = ("DiffusionFunction", "LocalContrastEnhancer")
26from lsst.pipe.tasks.prettyPictureMaker.types import FloatImagePlane
27from lsst.pex.config.configurableActions import ConfigurableAction, ConfigurableActionField
28from lsst.pex.config import Field
29from lsst.rubinoxide import rgb
31from .._localContrast import localContrast
34class DiffusionFunction(ConfigurableAction):
35 """Apply anisotropic diffusion processing to enhance image details.
37 Anisotropic diffusion is a multi-scale image processing technique that
38 selectively smooths regions while preserving edges by using spatially
39 varying diffusion coefficients. This implementation uses wavelet-based
40 anisotropic diffusion with configurable anisotropy parameters to control
41 how different frequency components diffuse relative to their gradients.
43 The diffusion process works by:
44 - Applying multiple iterations of gradient-based diffusion
45 - Using different diffusion speeds for low and high frequency wavelets
46 - Controlling diffusion direction via anisotropy parameters
47 - Regularizing coefficients to detect and preserve edges
48 - Modulating response to low-variance regions via variance threshold
49 """
51 iterations = Field[int]("number of interations in the diffusion process", default=3)
52 anisotropy_first = Field[float](
53 "The diffusion direction of low-frequency wavelets relative to their own gradient orientation",
54 default=1,
55 )
56 anisotropy_second = Field[float](
57 "The diffusion direction of low-frequency wavelets relative to the high-frequency gradient.",
58 default=1,
59 )
60 anisotropy_third = Field[float](
61 "The diffusion direction of high-frequency wavelets relative to the low-frequency gradient.",
62 default=1,
63 )
64 anisotropy_fourth = Field[float](
65 "The diffusion direction of high-frequency wavelets relative to their own gradient orientation",
66 default=1,
67 )
68 regularization = Field[float]("Regularization of coefficients used to detect edges", default=2.94)
69 variance_threshold = Field[float](
70 doc=(
71 "The variance threshold modulates the filter's response to low-variance regions, with positive "
72 " values enhancing local contrast and negative values suppressing noise and blur in those areas"
73 ),
74 default=0.0,
75 )
76 radius_center = Field[float](
77 doc=(
78 "The diffusion scale parameter: zero diffuses fine details (deblurring/denoising), "
79 "while non-zero values selectively diffuse larger scales to enhance local contrast."
80 ),
81 default=0.0,
82 )
83 radius = Field[float](
84 doc="The diffusion span defines a radial band (center ± span) for detail modification.", default=5.0
85 )
86 first = Field[float](doc="Anisotropic diffusion speed of low-frequency wavelets", default=0.0065)
87 second = Field[float](
88 doc="Low-frequency wavelet diffusion speed along the 2nd-order anisotropy axis", default=-0.25
89 )
90 third = Field[float](
91 doc="High-frequency wavelet diffusion speed along the 3rd-order anisotropy axis", default=-0.25
92 )
93 fourth = Field[float](
94 doc="High-frequency wavelet diffusion speed along the 4th-order anisotropy axis.", default=-0.2774
95 )
96 sharpness = Field[float](
97 doc="Adjusts wavelet detail amplitude. Positive values sharpen, negative values blur.", default=0.0
98 )
100 def __call__(self, intensities: FloatImagePlane) -> FloatImagePlane:
101 """Apply anisotropic diffusion to the input intensity image.
103 Parameters
104 ----------
105 intensities : `FloatImagePlane`
106 The input intensity image to process.
108 Returns
109 -------
110 result : `FloatImagePlane`
111 The diffused intensity image with enhanced details.
113 Notes
114 -----
115 This method implements wavelet-based anisotropic diffusion:
117 1. Multi-scale decomposition: The image is analyzed across multiple
118 frequency bands using wavelet decomposition.
119 2. Directional diffusion: Low-frequency wavelets diffuse according to
120 their own gradient orientation (anisotropy_first) and high-frequency
121 gradients (anisotropy_second). High-frequency wavelets diffuse
122 relative to low-frequency gradients (anisotropy_third) and their
123 own gradients (anisotropy_fourth).
124 3. Speed control: Diffusion speeds are configured via `first`, `second`,
125 `third`, and `fourth` parameters for each anisotropy axis.
126 4. Edge preservation: Regularization prevents diffusion across edges.
127 Variance threshold modulates response to smooth regions.
128 5. Scale selection: `radius_center` and `radius` define which scales
129 are modified, enabling targeted enhancement or denoising.
131 The diffusion equation is solved iteratively for `iterations` steps,
132 with the `sharpness` parameter adjusting final detail amplitudes.
133 """
134 return rgb.diffuse_gray_image(
135 intensities,
136 iterations=self.iterations,
137 radius_center=self.radius_center,
138 radius=self.radius,
139 regularization=self.regularization,
140 anisotropy_first=self.anisotropy_first,
141 anisotropy_second=self.anisotropy_second,
142 anisotropy_third=self.anisotropy_third,
143 anisotropy_fourth=self.anisotropy_fourth,
144 first=self.first,
145 second=self.second,
146 third=self.third,
147 fourth=self.fourth,
148 variance_threshold=self.variance_threshold,
149 sharpness=self.sharpness,
150 )
153class LocalContrastEnhancer(ConfigurableAction):
154 """Multi-stage local contrast enhancement processor.
156 Notes
157 -----
158 This class implements a two-stage approach for enhancing image contrast:
160 1. **Local Contrast Enhancement**: Applies scale-space contrast enhancement
161 using a Laplacian pyramid approach. This adjusts highlights, shadows,
162 and clarity while operating on multiple resolution levels.
164 2. **Anisotropic Diffusion**: Optionally applies wavelet-based anisotropic
165 diffusion to further sharpen details and preserve edges. This stage
166 selectively smooths regions based on local gradient information.
168 The processing pipeline is configurable via parameters for both stages,
169 allowing fine-tuned control over the enhancement behavior.
170 """
172 doLocalContrast = Field[bool](
173 "Do apply local contrast",
174 default=True,
175 deprecated=(
176 "This will stop working in v31 and be removed in v32, please set doLocalContrast on"
177 " PrettyPictureConfig"
178 ),
179 )
180 highlights = Field[float](doc="Adjustment factor for the highlights", default=-0.9)
181 shadows = Field[float](doc="Adjustment factor for the shadows", default=0.5)
182 clarity = Field[float](doc="Amount of clarity to apply to contrast modification", default=0.1)
183 sigma = Field[float](
184 doc="The scale size of what is considered local in the contrast enhancement", default=30
185 )
186 maxLevel = Field[int](
187 doc="The maximum number of scales the contrast should be enhanced over, if None then all",
188 default=4,
189 optional=True,
190 )
191 skipLevels = Field[int]("Skip this many lowest levels in laplace pyramid", default=0)
192 doDiffusion = Field[bool]("Run the diffusion function or not", default=True)
193 diffusionFunction = ConfigurableActionField[DiffusionFunction](
194 doc="Diffusion function to enhance local contrast",
195 )
197 def setDefaults(self) -> None:
198 self.diffusionFunction.iterations = 2
199 self.diffusionFunction.radius_center = 280
200 self.diffusionFunction.radius = 400
201 self.diffusionFunction.regularization = 0.0
202 self.diffusionFunction.first = -1.25
203 self.diffusionFunction.third = 0.0
204 self.diffusionFunction.fourth = 0.0
206 def __call__(self, intensities: FloatImagePlane) -> FloatImagePlane:
207 """Apply multi-stage contrast enhancement to the input image.
209 Parameters
210 ----------
211 intensities : `FloatImagePlane`
212 The input intensity image to process.
214 Returns
215 -------
216 result : `FloatImagePlane`
217 The enhanced intensity image with improved local contrast.
219 Notes
220 -----
221 This method implements a two-stage enhancement pipeline:
223 1. **Local Contrast Enhancement** (via `localContrast`):
224 - Builds a Laplacian pyramid of the input image
225 - Applies scale-dependent contrast modifications
226 - Adjusts highlights and shadows via `highlights` and `shadows`
227 - Controls clarity and sharpness via `clarity` parameter
228 - Operates over `maxLevel` scales, skipping `skipLevels` lowest
229 - Uses `sigma` to define what is considered "local"
231 2. **Anisotropic Diffusion** (optional, via `diffusionFunction`):
232 - Applied only if `doDiffusion=True`
233 - Performs wavelet-based anisotropic diffusion
234 - Preserves edges while enhancing details
235 - Configurable via diffusionFunction parameters
237 The two stages are applied sequentially, with the diffusion stage
238 operating on the locally enhanced image to further refine details.
239 """
240 intensities = localContrast(
241 intensities,
242 sigma=self.sigma,
243 highlights=self.highlights,
244 shadows=self.shadows,
245 clarity=self.clarity,
246 maxLevel=self.maxLevel,
247 skipLevels=self.skipLevels,
248 )
249 if self.doDiffusion:
250 intensities = self.diffusionFunction(intensities)
251 return intensities