gliders 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of gliders might be problematic. Click here for more details.
- gliders/__init__.py +2 -0
- gliders/artifacts.py +349 -0
- gliders/background_suppressor.py +268 -0
- gliders/cleaning_configs.py +99 -0
- gliders/deblending.py +954 -0
- gliders/others.py +290 -0
- gliders/pipeline.py +536 -0
- gliders/star_filter.py +606 -0
- gliders/stars.py +642 -0
- gliders/utils.py +1447 -0
- gliders/z_queries/DESI_Query.py +66 -0
- gliders/z_queries/GAMA_Query.py +53 -0
- gliders/z_queries/NED_Query.py +115 -0
- gliders/z_queries/SDSS_Query.py +73 -0
- gliders/z_queries/__init__.py +0 -0
- gliders/z_queries/z_fetcher.py +252 -0
- gliders-0.1.0.dist-info/METADATA +35 -0
- gliders-0.1.0.dist-info/RECORD +21 -0
- gliders-0.1.0.dist-info/WHEEL +5 -0
- gliders-0.1.0.dist-info/licenses/LICENSE +19 -0
- gliders-0.1.0.dist-info/top_level.txt +1 -0
gliders/__init__.py
ADDED
gliders/artifacts.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .utils import interpolate_image, grow_mask, add_noise_to_region, sigma_clipping, shrink_mask, get_profile_gradient, get_profile_from_border
|
|
3
|
+
from photutils.segmentation import SourceCatalog, detect_sources
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def detect_and_clean_artifacts(image, config = None, img_bkg_lvl = None, pix_scale = 1, main_source_pos = None,
|
|
7
|
+
skip_flagged_artifacts = False, verbose = False, show_plots = False):
|
|
8
|
+
"""
|
|
9
|
+
Detect and optionally remove overexposure artifacts from an image.
|
|
10
|
+
|
|
11
|
+
Artifact candidates are first identified using
|
|
12
|
+
:func:`detect_artifacts`, which searches for bright structures with
|
|
13
|
+
characteristic intensity profiles. Detected artifacts are then
|
|
14
|
+
classified as either safe or unsafe according to their size and
|
|
15
|
+
proximity to the target source through
|
|
16
|
+
:func:`get_artifact_flag`.
|
|
17
|
+
|
|
18
|
+
If artifacts are detected and cleaning is allowed, the affected
|
|
19
|
+
regions are replaced using :func:`clean_artifacts`, including an
|
|
20
|
+
additional mask to account for bleeding around the artifact.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
image : ndarray
|
|
25
|
+
Input image to be inspected and cleaned.
|
|
26
|
+
|
|
27
|
+
config : object, optional
|
|
28
|
+
Configuration object containing the artifact-detection and
|
|
29
|
+
cleaning parameters. If None, the default cleaning configuration
|
|
30
|
+
is loaded from ``cleaning_configs``.
|
|
31
|
+
|
|
32
|
+
img_bkg_lvl : float, optional
|
|
33
|
+
Background level of the image. If None, it is estimated using
|
|
34
|
+
sigma clipping.
|
|
35
|
+
|
|
36
|
+
pix_scale : float, optional
|
|
37
|
+
Pixel scale of the image. Used when cleaning artifacts. Default is 1.
|
|
38
|
+
|
|
39
|
+
main_source_pos : tuple(float, float) or None, optional
|
|
40
|
+
Pixel coordinates (x, y) of the primary target source. Used to
|
|
41
|
+
determine whether a detected artifact lies within the protected
|
|
42
|
+
central region of the image. If None, the image center is used.
|
|
43
|
+
|
|
44
|
+
skip_flagged_artifacts : bool, optional
|
|
45
|
+
If True, artifacts classified as unsafe are not cleaned and the
|
|
46
|
+
original image is returned unchanged. This can be useful to
|
|
47
|
+
preserve images where an artifact significantly affects the
|
|
48
|
+
target source. Default is False.
|
|
49
|
+
|
|
50
|
+
show_plots : bool, optional
|
|
51
|
+
If True, display diagnostic plots showing the original image,
|
|
52
|
+
cleaned image, detected artifact mask, and final cleaning mask.
|
|
53
|
+
Default is False.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
clean_image : ndarray
|
|
58
|
+
The cleaned image. If no artifacts are detected, or if cleaning
|
|
59
|
+
is skipped, this will be identical to the input image.
|
|
60
|
+
|
|
61
|
+
cleaning_mask : ndarray of bool
|
|
62
|
+
Boolean mask identifying pixels affected by the artifact
|
|
63
|
+
cleaning process. This includes both the detected artifact
|
|
64
|
+
pixels and any additional bleeding region considered during
|
|
65
|
+
cleaning.
|
|
66
|
+
|
|
67
|
+
unsafe_artifact_flag : bool
|
|
68
|
+
True if a detected artifact overlaps the protected central
|
|
69
|
+
region and exceeds the configured relative size threshold.
|
|
70
|
+
False otherwise.
|
|
71
|
+
|
|
72
|
+
Notes
|
|
73
|
+
-----
|
|
74
|
+
The behavior of the detector and cleaner is controlled by the
|
|
75
|
+
following configuration parameters:
|
|
76
|
+
|
|
77
|
+
- ``npixels_artifact``
|
|
78
|
+
- ``artifact_delta_slope_threshold``
|
|
79
|
+
- ``artifact_segment_slack``
|
|
80
|
+
- ``artifact_candidate_ratio``
|
|
81
|
+
- ``artifact_rel_size_thresh``
|
|
82
|
+
- ``artifact_safe_region_radius``
|
|
83
|
+
- ``artifact_bleeding_factor``
|
|
84
|
+
|
|
85
|
+
When ``skip_flagged_artifacts=True``, images containing unsafe
|
|
86
|
+
artifacts are not modified, although the artifact mask and flag are
|
|
87
|
+
still returned.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
if config is None:
|
|
91
|
+
from .cleaning_configs import configs
|
|
92
|
+
config = configs["default"]
|
|
93
|
+
if img_bkg_lvl is None:
|
|
94
|
+
img_bkg_lvl, _ = sigma_clipping(image)
|
|
95
|
+
|
|
96
|
+
artifact_segmap = detect_artifacts(image, npixels_artifact=config.npixels_artifact,
|
|
97
|
+
delta_slope_threshold=config.artifact_delta_slope_threshold,
|
|
98
|
+
segment_slack=config.artifact_segment_slack,
|
|
99
|
+
hi_pass_threshold=img_bkg_lvl * config.artifact_candidate_ratio)
|
|
100
|
+
|
|
101
|
+
if artifact_segmap is None:
|
|
102
|
+
if verbose:
|
|
103
|
+
print("No overexposure artifacts were detected in the image.")
|
|
104
|
+
cleaning_mask = np.zeros_like(image, dtype=bool)
|
|
105
|
+
return image, cleaning_mask, False
|
|
106
|
+
|
|
107
|
+
artifact_mask = artifact_segmap.make_source_mask()
|
|
108
|
+
unsafe_artifact_flag = get_artifact_flag(artifact_mask, main_source_pos, config.artifact_rel_size_thresh, config.artifact_safe_region_radius, show_plots = show_plots)
|
|
109
|
+
|
|
110
|
+
if artifact_mask.any() and not (skip_flagged_artifacts and unsafe_artifact_flag):
|
|
111
|
+
clean_image, cleaning_mask = clean_artifacts(image, artifact_segmap, config.artifact_bleeding_factor,
|
|
112
|
+
config.artifact_interpolation_radius, pix_scale)
|
|
113
|
+
else:
|
|
114
|
+
clean_image = image
|
|
115
|
+
cleaning_mask = artifact_mask
|
|
116
|
+
|
|
117
|
+
if show_plots:
|
|
118
|
+
import matplotlib.pyplot as plt
|
|
119
|
+
fig, ax = plt.subplots(2,2)
|
|
120
|
+
ax[0,0].imshow(image, norm = "log", origin = "lower")
|
|
121
|
+
ax[0,0].set_title('Input image')
|
|
122
|
+
ax[0,1].imshow(clean_image, norm = "log", origin = "lower")
|
|
123
|
+
ax[0,1].set_title('Treated image')
|
|
124
|
+
ax[1,0].imshow(artifact_mask, origin = "lower")
|
|
125
|
+
ax[1,0].set_title('Detected artifacts')
|
|
126
|
+
ax[1,1].imshow(cleaning_mask, origin = "lower")
|
|
127
|
+
ax[1,1].set_title('Detected artifacts + bleeding')
|
|
128
|
+
plt.tight_layout()
|
|
129
|
+
plt.show()
|
|
130
|
+
|
|
131
|
+
return clean_image, cleaning_mask, unsafe_artifact_flag
|
|
132
|
+
|
|
133
|
+
def clean_artifacts(image, artifact_segmap = None, bleeding_factor = 1.0, interpolation_radius = 1, pix_scale = 1):
|
|
134
|
+
|
|
135
|
+
"""Remove artifacts based on interpolation over segmented artifacts.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
image : np.ndarray
|
|
140
|
+
Input image.
|
|
141
|
+
artifact_segmap : segmentation map, optional
|
|
142
|
+
Artifact segmentation map. If None, the image is returned unchanged.
|
|
143
|
+
bleeding_factor : float
|
|
144
|
+
Multiplier controlling how far the artifact mask is expanded.
|
|
145
|
+
interpolation_radius : float
|
|
146
|
+
Radius of the circle containing the pixels to be considered in the interpolation.
|
|
147
|
+
pix_scale : float
|
|
148
|
+
Scale factor of the noise pixels added.
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
image : np.ndarray
|
|
153
|
+
Cleaned image.
|
|
154
|
+
full_cleaning_mask : np.ndarray or None
|
|
155
|
+
Mask of pixels modified by the cleaning procedure.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
if artifact_segmap is None or (len(artifact_segmap.segments) < 1):
|
|
159
|
+
return image, np.zeros(image.shape, dtype=bool)
|
|
160
|
+
|
|
161
|
+
if bleeding_factor < 0:
|
|
162
|
+
raise ValueError("bleeding_factor must be non-negative")
|
|
163
|
+
|
|
164
|
+
if interpolation_radius < 1:
|
|
165
|
+
raise ValueError("interpolation_radius must be equal or greater than 1")
|
|
166
|
+
all_artifacts_mask = artifact_segmap.make_source_mask()
|
|
167
|
+
full_cleaning_mask = all_artifacts_mask.copy()
|
|
168
|
+
for artifact in artifact_segmap.segments:
|
|
169
|
+
artifact_mask = artifact_segmap.data == artifact.label
|
|
170
|
+
artifact_area = artifact_mask.sum()
|
|
171
|
+
bleeding_decay = 2 * artifact_area**(1/3) # Rounded from empiric formula based on DESI Legacy images.
|
|
172
|
+
bleeding_radius = round(bleeding_factor * bleeding_decay)
|
|
173
|
+
# Expanding detection mask to cover artifact bleeding
|
|
174
|
+
cleaning_mask = grow_mask(artifact_mask, pixels=bleeding_radius)
|
|
175
|
+
full_cleaning_mask |= cleaning_mask
|
|
176
|
+
|
|
177
|
+
image = interpolate_image(image, full_cleaning_mask, interpolation_radius, method="inpaint")
|
|
178
|
+
_, img_bkg_lvl_std = sigma_clipping(image)
|
|
179
|
+
image = add_noise_to_region(image, full_cleaning_mask, noise_center=0, noise_std=img_bkg_lvl_std, scale=pix_scale)
|
|
180
|
+
return image, full_cleaning_mask
|
|
181
|
+
|
|
182
|
+
def detect_artifacts(image, npixels_artifact=100, delta_slope_threshold = 2.7, segment_slack = 5, hi_pass_threshold = 1):
|
|
183
|
+
"""
|
|
184
|
+
Detect candidate overexposure artifacts based on their radial intensity profile.
|
|
185
|
+
|
|
186
|
+
The algorithm first identifies bright connected regions above a fixed
|
|
187
|
+
intensity threshold. For each candidate region, a series of concentric
|
|
188
|
+
layers is constructed around the detected segment and the mean pixel value
|
|
189
|
+
within each layer is measured. The resulting radial profile is analyzed
|
|
190
|
+
through its gradient and second-order variations (delta slopes).
|
|
191
|
+
|
|
192
|
+
Regions showing an abrupt change in profile slope are classified as
|
|
193
|
+
artifacts.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
image : ndarray
|
|
198
|
+
2D image to analyze.
|
|
199
|
+
|
|
200
|
+
npixels_artifact : int, optional
|
|
201
|
+
Minimum number of connected pixels required for a region to be
|
|
202
|
+
considered as an artifact candidate.
|
|
203
|
+
|
|
204
|
+
delta_slope_threshold : float, optional
|
|
205
|
+
Minimum peak value in the profile delta-slope sequence required to
|
|
206
|
+
classify a candidate as an artifact. Larger values make the detection
|
|
207
|
+
more conservative.
|
|
208
|
+
|
|
209
|
+
segment_slack : int, optional
|
|
210
|
+
Number of pixels by which each candidate segment is expanded when
|
|
211
|
+
computing the surrounding radial profile. Also sets the number of
|
|
212
|
+
profile layers evaluated.
|
|
213
|
+
|
|
214
|
+
hi_pass_threshold : float, optional
|
|
215
|
+
Pixel intensity threshold used to generate the initial binary mask of
|
|
216
|
+
artifact candidates.
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
-------
|
|
220
|
+
ndarray of bool
|
|
221
|
+
Segmentation map of all detected artifacts.
|
|
222
|
+
|
|
223
|
+
Notes
|
|
224
|
+
-----
|
|
225
|
+
Artifact identification relies on the assumption that overexposed features
|
|
226
|
+
often exhibit sharp transitions in their radial intensity profile, unlike
|
|
227
|
+
typical astronomical sources whose profiles vary more smoothly.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
hi_pass_mask = image > hi_pass_threshold
|
|
231
|
+
artifacts_segmap = detect_sources(hi_pass_mask, 0.5, npixels_artifact, connectivity=8)
|
|
232
|
+
if artifacts_segmap is None:
|
|
233
|
+
return artifacts_segmap
|
|
234
|
+
|
|
235
|
+
candidate_artifacts_mask = artifacts_segmap.data > 0
|
|
236
|
+
|
|
237
|
+
labels_to_remove = []
|
|
238
|
+
artifacts_cat = SourceCatalog(candidate_artifacts_mask, artifacts_segmap)
|
|
239
|
+
|
|
240
|
+
for artifact in artifacts_cat:
|
|
241
|
+
segment_mask = artifact.segment.astype(bool)
|
|
242
|
+
|
|
243
|
+
pad_top = min(segment_slack, artifact.bbox.iymin)
|
|
244
|
+
pad_bottom = min(segment_slack, image.shape[0] - artifact.bbox.iymax)
|
|
245
|
+
pad_left = min(segment_slack, artifact.bbox.ixmin)
|
|
246
|
+
pad_right = min(segment_slack, image.shape[1] - artifact.bbox.ixmax)
|
|
247
|
+
segment_mask_padded = np.pad(segment_mask, ((pad_top, pad_bottom), (pad_left, pad_right)),
|
|
248
|
+
mode="constant", constant_values=False)
|
|
249
|
+
|
|
250
|
+
segment_cutout = image[max(0, artifact.bbox.iymin - segment_slack):min(image.shape[0],
|
|
251
|
+
artifact.bbox.iymax + segment_slack),
|
|
252
|
+
max(0, artifact.bbox.ixmin - segment_slack):min(image.shape[1],
|
|
253
|
+
artifact.bbox.ixmax + segment_slack)]
|
|
254
|
+
|
|
255
|
+
mean_profile = get_profile_from_border(segment_cutout, segment_mask_padded, segment_slack)
|
|
256
|
+
|
|
257
|
+
if len(mean_profile) < 3:
|
|
258
|
+
labels_to_remove.append(artifact.label)
|
|
259
|
+
continue
|
|
260
|
+
_, profile_delta_slopes = get_profile_gradient(mean_profile)
|
|
261
|
+
max_delta_slope = np.max(profile_delta_slopes)
|
|
262
|
+
is_artifact = max_delta_slope >= delta_slope_threshold
|
|
263
|
+
if not is_artifact:
|
|
264
|
+
labels_to_remove.append(artifact.label)
|
|
265
|
+
artifacts_segmap.remove_labels(labels_to_remove)
|
|
266
|
+
|
|
267
|
+
return artifacts_segmap
|
|
268
|
+
|
|
269
|
+
def get_artifact_flag(artifact_mask, main_source_pos = None, relative_size_thresh = 0.005, safe_region_radius = 0.25, show_plots = False):
|
|
270
|
+
"""
|
|
271
|
+
Determine whether a detected artifact should be considered critical.
|
|
272
|
+
|
|
273
|
+
An artifact is flagged when both of the following conditions are met:
|
|
274
|
+
|
|
275
|
+
1. At least one artifact pixel lies inside a central circular region
|
|
276
|
+
of the image.
|
|
277
|
+
2. The total artifact area exceeds a minimum fraction of the area
|
|
278
|
+
of that central region.
|
|
279
|
+
|
|
280
|
+
The central region is defined as a circle centered on
|
|
281
|
+
``main_source_pos``. If no valid source position is provided, the
|
|
282
|
+
image center is used instead. The circle radius is computed as
|
|
283
|
+
``safe_region_radius * min(image_shape)``.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
artifact_mask : ndarray of bool
|
|
288
|
+
Boolean mask containing the detected artifact pixels.
|
|
289
|
+
True values indicate artifact locations.
|
|
290
|
+
|
|
291
|
+
main_source_pos : tuple(float, float) or None, optional
|
|
292
|
+
Pixel coordinates (x, y) of the target source. If None or if
|
|
293
|
+
the coordinates are not finite, the geometric center of the
|
|
294
|
+
image is used.
|
|
295
|
+
|
|
296
|
+
relative_size_thresh : float, optional
|
|
297
|
+
Minimum ratio between the total artifact area and the area of
|
|
298
|
+
the central circular region required to trigger the flag.
|
|
299
|
+
Default is 0.005.
|
|
300
|
+
|
|
301
|
+
safe_region_radius : float, optional
|
|
302
|
+
Radius of the central circular region expressed as a fraction of
|
|
303
|
+
the smallest image dimension. Values are clipped to the interval
|
|
304
|
+
[0, 1]. Default is 0.25.
|
|
305
|
+
|
|
306
|
+
show_plots : bool, optional
|
|
307
|
+
If True and a critical artifact is detected, display a diagnostic
|
|
308
|
+
plot showing the artifact mask and the central region. Default is
|
|
309
|
+
False.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
bool
|
|
314
|
+
True if the artifact overlaps the central region and is larger
|
|
315
|
+
than the specified relative size threshold. False otherwise.
|
|
316
|
+
|
|
317
|
+
Notes
|
|
318
|
+
-----
|
|
319
|
+
The artifact size is computed using the total number of pixels in
|
|
320
|
+
``artifact_mask`` and is not restricted to the portion lying within
|
|
321
|
+
the central region.
|
|
322
|
+
"""
|
|
323
|
+
ny, nx = artifact_mask.shape
|
|
324
|
+
if (main_source_pos is None or not np.all(np.isfinite(main_source_pos))):
|
|
325
|
+
center = (nx // 2, ny // 2)
|
|
326
|
+
else:
|
|
327
|
+
center = main_source_pos
|
|
328
|
+
safe_reg_prop_radius = np.clip(safe_region_radius, 0, 1)
|
|
329
|
+
radius = int(round(min((ny, nx)) * safe_reg_prop_radius))
|
|
330
|
+
# Create a mask for the central circular region
|
|
331
|
+
y, x = np.ogrid[:ny, :nx]
|
|
332
|
+
circle_mask = ((x - center[0]) ** 2 + (y - center[1]) ** 2 <= radius ** 2)
|
|
333
|
+
|
|
334
|
+
artifact_size = np.sum(artifact_mask)
|
|
335
|
+
inner_region_size = np.sum(circle_mask)
|
|
336
|
+
if inner_region_size == 0:
|
|
337
|
+
return False
|
|
338
|
+
big_artifact = (artifact_size / inner_region_size >= relative_size_thresh)
|
|
339
|
+
artifact_inside_region = np.any(artifact_mask & circle_mask)
|
|
340
|
+
artifact_flag = artifact_inside_region and big_artifact
|
|
341
|
+
|
|
342
|
+
if show_plots and artifact_flag:
|
|
343
|
+
import matplotlib.pyplot as plt
|
|
344
|
+
plt.imshow(artifact_mask.astype(int) + circle_mask.astype(int), origin = "lower")
|
|
345
|
+
plt.title("Artifact not-safe region")
|
|
346
|
+
plt.show()
|
|
347
|
+
|
|
348
|
+
return artifact_flag
|
|
349
|
+
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy.stats import norm
|
|
4
|
+
from scipy.signal import convolve2d
|
|
5
|
+
from .utils import sigma_clipping, normalize_data
|
|
6
|
+
from astropy.convolution import Gaussian2DKernel
|
|
7
|
+
|
|
8
|
+
def suppress_background(image, method ="simple", kernel = None, logit_alpha = 1.0):
|
|
9
|
+
"""
|
|
10
|
+
Suppress background-dominated pixels in an image using a noise-based
|
|
11
|
+
probability model.
|
|
12
|
+
|
|
13
|
+
The function assumes that image noise follows an approximately normal
|
|
14
|
+
distribution. A noise probability map is first computed from the image
|
|
15
|
+
intensity distribution and then refined using one of several methods.
|
|
16
|
+
The resulting probability map is applied as a weighting function,
|
|
17
|
+
enhancing statistically significant structures while attenuating pixels
|
|
18
|
+
consistent with the background noise.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
image : ndarray
|
|
23
|
+
Input image.
|
|
24
|
+
|
|
25
|
+
method : {'simple', 'logit', 'layers'}, optional
|
|
26
|
+
Method used to generate the probability map.
|
|
27
|
+
|
|
28
|
+
* ``'simple'``: spatial smoothing of the noise probability map.
|
|
29
|
+
* ``'logit'``: smoothing in logit space followed by a logistic
|
|
30
|
+
transformation.
|
|
31
|
+
* ``'layers'``: iterative probability refinement using an
|
|
32
|
+
additional intensity-weighted layer.
|
|
33
|
+
|
|
34
|
+
Default is ``'simple'``.
|
|
35
|
+
|
|
36
|
+
kernel : ndarray, optional
|
|
37
|
+
Convolution kernel used for local probability aggregation. If
|
|
38
|
+
``None``, a ``Gaussian2DKernel(2.0)`` is used.
|
|
39
|
+
|
|
40
|
+
logit_alpha : float, optional
|
|
41
|
+
Scaling factor applied to the logistic transform when
|
|
42
|
+
``method='logit'``. Larger values increase the contrast between
|
|
43
|
+
low- and high-probability regions.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
ndarray
|
|
48
|
+
Background-suppressed image rescaled to approximately match the
|
|
49
|
+
original image intensity range.
|
|
50
|
+
|
|
51
|
+
Notes
|
|
52
|
+
-----
|
|
53
|
+
For better performance it is recommended that the image background
|
|
54
|
+
is already flattened (background subtraction).
|
|
55
|
+
|
|
56
|
+
See Also
|
|
57
|
+
--------
|
|
58
|
+
get_p_simple
|
|
59
|
+
get_p_logit
|
|
60
|
+
get_p_layers
|
|
61
|
+
apply_p_map
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
if kernel is None:
|
|
65
|
+
kernel = Gaussian2DKernel(2.0) # Larger kernels yield higher noise level, but more symmetric noise distribution
|
|
66
|
+
|
|
67
|
+
image_noise_lvl, image_noise_std = sigma_clipping(image, robust=False)
|
|
68
|
+
|
|
69
|
+
# Probability of obtaining x or lower:
|
|
70
|
+
noise_p_map = norm.cdf(image, loc=image_noise_lvl, scale=image_noise_std) # sf = 1 - cdf = survival function
|
|
71
|
+
|
|
72
|
+
if method == "logit":
|
|
73
|
+
p_map = _get_p_logit(noise_p_map, kernel, alpha = logit_alpha)
|
|
74
|
+
elif method == "layers":
|
|
75
|
+
p_map = _get_p_layers(image, noise_p_map, kernel)
|
|
76
|
+
else: #simple
|
|
77
|
+
p_map = _get_p_simple(noise_p_map, kernel)
|
|
78
|
+
|
|
79
|
+
bkg_reduced_image_leveled = _apply_p_map(image, p_map)
|
|
80
|
+
|
|
81
|
+
return bkg_reduced_image_leveled
|
|
82
|
+
|
|
83
|
+
def _get_p_simple(p_map, kernel):
|
|
84
|
+
"""
|
|
85
|
+
Convolve probability map (p_map) with the input kernel.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
p_out = convolve2d(p_map, kernel, mode='same', boundary='symm')
|
|
89
|
+
return p_out
|
|
90
|
+
|
|
91
|
+
def _get_p_logit(p_map, kernel, alpha = 1.0):
|
|
92
|
+
"""
|
|
93
|
+
Transform probability map (p_map) into logits, convolve with the supplied
|
|
94
|
+
kernel, and transform back into a probability map.
|
|
95
|
+
|
|
96
|
+
alpha is the scaling factor applied to the aggregated logits before converting
|
|
97
|
+
back to probabilities. Values greater than one amplify local contrast,
|
|
98
|
+
while values less than one reduce it.
|
|
99
|
+
|
|
100
|
+
Notes
|
|
101
|
+
-----
|
|
102
|
+
Pixels with probability equal to one are clipped slightly below one
|
|
103
|
+
before the logit transformation to avoid numerical singularities.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
p_map[p_map >= 1] = 1-1e-9
|
|
107
|
+
logit = np.log(p_map / (1 - p_map))
|
|
108
|
+
L = convolve2d(logit, kernel, mode='same', boundary='symm') # weighted sum of logit
|
|
109
|
+
p_out = 1.0 / (1.0 + np.exp(-alpha * L))
|
|
110
|
+
return p_out
|
|
111
|
+
|
|
112
|
+
def _get_p_layers(image, p_map, kernel):
|
|
113
|
+
"""
|
|
114
|
+
Generate a probability map using a two-layer probability refinement
|
|
115
|
+
scheme.
|
|
116
|
+
|
|
117
|
+
An initial probability map (p_map) is first convolved with the provided kernel.
|
|
118
|
+
The image is then weighted by this smoothed map, and a second probability map is
|
|
119
|
+
estimated from the resulting intensity distribution. The final output
|
|
120
|
+
is obtained by combining the two probability layers.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
p_map_cnv = convolve2d(p_map, kernel, mode='same', boundary='symm')
|
|
124
|
+
image_p_map_cnv = image * p_map_cnv
|
|
125
|
+
image_p_map_cnv_noise = sigma_clipping(image_p_map_cnv, robust=False)
|
|
126
|
+
p_map_new_layer = norm.cdf(image_p_map_cnv, loc=image_p_map_cnv_noise[0], scale=image_p_map_cnv_noise[1])
|
|
127
|
+
p_map_new_layer_cnv = convolve2d(p_map_new_layer, kernel, mode='same', boundary='symm')
|
|
128
|
+
p_out = p_map_cnv * p_map_new_layer_cnv
|
|
129
|
+
return p_out
|
|
130
|
+
|
|
131
|
+
def _apply_p_map(image, p_map):
|
|
132
|
+
"""
|
|
133
|
+
Apply a probability map (p_map) to an image and rescale according to their maxima.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
image_p_map = image * p_map
|
|
137
|
+
image_p_map_leveled = image_p_map * image.max() / image_p_map.max()
|
|
138
|
+
return image_p_map_leveled
|
|
139
|
+
|
|
140
|
+
def show_all_methods(image, kernel = None, normalize = True, cmap = "plasma", display_norm = "log", image_bias = 0.001, show_distributions = False):
|
|
141
|
+
"""
|
|
142
|
+
Visualization of the different background-suppression methods available in the
|
|
143
|
+
package.
|
|
144
|
+
|
|
145
|
+
This utility function computes the probability maps and background-reduced
|
|
146
|
+
images obtained with the ``simple``, ``logit``, and ``layers`` methods,
|
|
147
|
+
and displays the results either as images or as intensity distributions.
|
|
148
|
+
It is primarily intended for visual inspection, debugging, and parameter
|
|
149
|
+
tuning.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
image : ndarray
|
|
154
|
+
Input image.
|
|
155
|
+
|
|
156
|
+
kernel : ndarray, optional
|
|
157
|
+
Convolution kernel used to smooth the probability maps. If ``None``,
|
|
158
|
+
a ``Gaussian2DKernel(2.0)`` is used.
|
|
159
|
+
|
|
160
|
+
normalize : bool, optional
|
|
161
|
+
If ``True``, all displayed images are normalized using
|
|
162
|
+
``normalize_data`` before visualization. Default is ``True``.
|
|
163
|
+
|
|
164
|
+
cmap : str, optional
|
|
165
|
+
Matplotlib colormap used for image display. Default is ``"plasma"``.
|
|
166
|
+
|
|
167
|
+
display_norm : str, optional
|
|
168
|
+
Normalization used for image visualization and histogram scaling.
|
|
169
|
+
Typical values are ``"log"`` and ``"linear"``. Default is ``"log"``.
|
|
170
|
+
|
|
171
|
+
image_bias : float, optional
|
|
172
|
+
Small positive value added to displayed images to avoid issues with
|
|
173
|
+
logarithmic visualization and zero-valued pixels. Default is
|
|
174
|
+
``0.001``.
|
|
175
|
+
|
|
176
|
+
show_distributions : bool, optional
|
|
177
|
+
If ``True``, display intensity histograms of the original and
|
|
178
|
+
processed images instead of image panels. Default is ``False``.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
None
|
|
183
|
+
The function generates matplotlib figures but does not return any
|
|
184
|
+
value.
|
|
185
|
+
|
|
186
|
+
See Also
|
|
187
|
+
--------
|
|
188
|
+
suppress_background
|
|
189
|
+
get_p_simple
|
|
190
|
+
get_p_logit
|
|
191
|
+
get_p_layers
|
|
192
|
+
apply_p_map
|
|
193
|
+
normalize_data
|
|
194
|
+
sigma_clipping
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
if kernel is None:
|
|
198
|
+
kernel = Gaussian2DKernel(2.0)
|
|
199
|
+
image_noise_lvl, image_noise_std = sigma_clipping(image, robust=False)
|
|
200
|
+
p_map = norm.cdf(image, loc=image_noise_lvl, scale=image_noise_std) # sf = 1 - cdf = survival function
|
|
201
|
+
|
|
202
|
+
p_simple = _get_p_simple(p_map, kernel)
|
|
203
|
+
p_logit = _get_p_logit(p_map, kernel)
|
|
204
|
+
p_layers = _get_p_layers(image, p_map, kernel)
|
|
205
|
+
|
|
206
|
+
bkg_reduced_image_simple = _apply_p_map(image, p_simple)
|
|
207
|
+
bkg_reduced_image_logit = _apply_p_map(image, p_logit)
|
|
208
|
+
bkg_reduced_image_layers = _apply_p_map(image, p_layers)
|
|
209
|
+
|
|
210
|
+
images = {
|
|
211
|
+
'im_original': image,
|
|
212
|
+
'im_simple': bkg_reduced_image_simple,
|
|
213
|
+
'im_logit': bkg_reduced_image_logit,
|
|
214
|
+
'im_layers': bkg_reduced_image_layers,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if normalize:
|
|
218
|
+
for key in images:
|
|
219
|
+
images[key] = normalize_data(images[key]) + image_bias
|
|
220
|
+
|
|
221
|
+
if show_distributions:
|
|
222
|
+
|
|
223
|
+
image_hist = images['im_original'].flatten()
|
|
224
|
+
bins = np.logspace(np.log10(np.min(image_hist[image_hist>0])),np.log10(np.max(image_hist)),100)
|
|
225
|
+
for key in images:
|
|
226
|
+
log_y = False
|
|
227
|
+
if display_norm == "log":
|
|
228
|
+
log_y = True
|
|
229
|
+
plt.hist(images[key].flatten(), bins = bins, alpha = 0.8, log = log_y, histtype='step', label = key, linewidth = 3, density = False)
|
|
230
|
+
plt.xscale(display_norm)
|
|
231
|
+
plt.legend()
|
|
232
|
+
plt.show()
|
|
233
|
+
|
|
234
|
+
else:
|
|
235
|
+
|
|
236
|
+
if not normalize:
|
|
237
|
+
for key in images:
|
|
238
|
+
images[key] += image_bias
|
|
239
|
+
|
|
240
|
+
figs, ax = plt.subplots(2, 4, layout="tight")
|
|
241
|
+
|
|
242
|
+
ax[1, 0].imshow(p_map, origin="lower")
|
|
243
|
+
ax[1, 0].set_title("raw p_map")
|
|
244
|
+
ax[0, 0].imshow(images["im_original"], origin="lower", norm=display_norm, cmap=cmap)
|
|
245
|
+
ax[0, 0].set_title("original")
|
|
246
|
+
|
|
247
|
+
ax[1, 1].imshow(p_simple, origin="lower")
|
|
248
|
+
ax[1, 1].set_title("simple")
|
|
249
|
+
ax[0, 1].imshow(images["im_simple"], origin="lower", norm=display_norm, cmap=cmap)
|
|
250
|
+
ax[0, 1].set_title("simple")
|
|
251
|
+
|
|
252
|
+
ax[1, 2].imshow(p_logit, origin="lower")
|
|
253
|
+
ax[1, 2].set_title("logit")
|
|
254
|
+
ax[0, 2].imshow(images["im_logit"], origin="lower", norm=display_norm, cmap=cmap)
|
|
255
|
+
ax[0, 2].set_title("logit")
|
|
256
|
+
|
|
257
|
+
ax[1, 3].imshow(p_layers, origin="lower")
|
|
258
|
+
ax[1, 3].set_title("layers")
|
|
259
|
+
ax[0, 3].imshow(images["im_layers"], origin="lower", norm=display_norm, cmap=cmap)
|
|
260
|
+
ax[0, 3].set_title("layers")
|
|
261
|
+
|
|
262
|
+
plt.show()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
quit()
|
|
267
|
+
|
|
268
|
+
|