gliders 0.1.0__tar.gz

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-0.1.0/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Alejandro Erostegui Losantos
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
gliders-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: gliders
3
+ Version: 0.1.0
4
+ Summary: GaLaxy Image DEcontamination Scripts - GLIDErS: Galaxy image cleaning and deblending pipeline
5
+ Author-email: Alejandro Erostegui <alexeros90@gmail.com>
6
+ License: Copyright (c) 2026 Alejandro Erostegui Losantos
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ Requires-Python: >=3.9
26
+ License-File: LICENSE
27
+ Requires-Dist: numpy
28
+ Requires-Dist: scipy
29
+ Requires-Dist: astropy
30
+ Requires-Dist: photutils
31
+ Requires-Dist: pandas
32
+ Requires-Dist: scikit-image
33
+ Requires-Dist: opencv-python
34
+ Requires-Dist: sep
35
+ Dynamic: license-file
@@ -0,0 +1,43 @@
1
+ # GLIDErS (GaLaxy Image DEcontamination Scripts)
2
+
3
+ **GLIDErS** is a Python package for cleaning astronomical images while preserving the main source of interest, such as a galaxy, and reducing contamination from foreground stars, neighboring objects, and overexposure artifacts.
4
+
5
+ ## Features
6
+
7
+ - **Artifact removal:** Detects and cleans potential overexposure artifacts.
8
+ - **Star masking:** Identifies contaminating stars and applies masking or interpolation based on their overlap with the main source.
9
+ - **Source deblending:** Separates overlapping sources using configurable criteria, including redshift and photometric-type information.
10
+ - **Background subtraction:** Supports background subtraction.
11
+ - **Background suppression:** Reduces the background noise level improving the signal-to-noise ratio for detecting faint sources or features.
12
+ - **Quality flags:** Reports potential issues such as significant stellar contamination, source truncation at image boundaries, failed redshift queries, and insufficient source size.
13
+
14
+ ## Getting Started
15
+
16
+ The main entry point is `clean_image`, which combines the available cleaning operations into a configurable workflow.
17
+
18
+ ```python
19
+ from gliders.pipeline import clean_image
20
+
21
+ cleaned_image, main_source_segment, cleaning_mask, cleaning_flags = clean_image(
22
+ image,
23
+ source_pos=(x, y),
24
+ mask_stars=True,
25
+ deblend_sources=True,
26
+ clean_artifacts=True,
27
+ show_plots=False
28
+ )
29
+ ```
30
+
31
+ Here, `image` is a two-dimensional astronomical image, and `source_pos` specifies the approximate pixel coordinates `(x, y)` of the main source.
32
+
33
+ The function returns the cleaned image, the final main-source segmentation, a mask identifying pixels marked by selected cleaning operations, and flags summarizing potential issues encountered during processing.
34
+
35
+ GLIDErS supports configurable cleaning strategies, allowing individual operations to be enabled or disabled according to the scientific application.
36
+
37
+ ## Configuration
38
+
39
+ GLIDErS uses a configuration object to control source detection, masking, and deblending parameters. When no configuration is provided, the package loads its default configuration.
40
+
41
+ ## License
42
+
43
+ See the repository's `LICENSE` file for licensing information.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gliders"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Alejandro Erostegui", email="alexeros90@gmail.com" },
10
+ ]
11
+ description = "GaLaxy Image DEcontamination Scripts - GLIDErS: Galaxy image cleaning and deblending pipeline"
12
+ license = {file = "LICENSE"}
13
+ requires-python = ">=3.9"
14
+
15
+ dependencies = [
16
+ "numpy",
17
+ "scipy",
18
+ "astropy",
19
+ "photutils",
20
+ "pandas",
21
+ "scikit-image",
22
+ "opencv-python",
23
+ "sep"
24
+ ]
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ from .pipeline import clean_image
2
+ # __all__ = ["get_clean_image_and_segment"]
@@ -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
+