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 +19 -0
- gliders-0.1.0/PKG-INFO +35 -0
- gliders-0.1.0/README.md +43 -0
- gliders-0.1.0/pyproject.toml +26 -0
- gliders-0.1.0/setup.cfg +4 -0
- gliders-0.1.0/src/gliders/__init__.py +2 -0
- gliders-0.1.0/src/gliders/artifacts.py +349 -0
- gliders-0.1.0/src/gliders/background_suppressor.py +268 -0
- gliders-0.1.0/src/gliders/cleaning_configs.py +99 -0
- gliders-0.1.0/src/gliders/deblending.py +954 -0
- gliders-0.1.0/src/gliders/others.py +290 -0
- gliders-0.1.0/src/gliders/pipeline.py +536 -0
- gliders-0.1.0/src/gliders/star_filter.py +606 -0
- gliders-0.1.0/src/gliders/stars.py +642 -0
- gliders-0.1.0/src/gliders/utils.py +1447 -0
- gliders-0.1.0/src/gliders/z_queries/DESI_Query.py +66 -0
- gliders-0.1.0/src/gliders/z_queries/GAMA_Query.py +53 -0
- gliders-0.1.0/src/gliders/z_queries/NED_Query.py +115 -0
- gliders-0.1.0/src/gliders/z_queries/SDSS_Query.py +73 -0
- gliders-0.1.0/src/gliders/z_queries/__init__.py +0 -0
- gliders-0.1.0/src/gliders/z_queries/z_fetcher.py +252 -0
- gliders-0.1.0/src/gliders.egg-info/PKG-INFO +35 -0
- gliders-0.1.0/src/gliders.egg-info/SOURCES.txt +25 -0
- gliders-0.1.0/src/gliders.egg-info/dependency_links.txt +1 -0
- gliders-0.1.0/src/gliders.egg-info/requires.txt +8 -0
- gliders-0.1.0/src/gliders.egg-info/top_level.txt +1 -0
- gliders-0.1.0/tests/tester.py +108 -0
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
|
gliders-0.1.0/README.md
ADDED
|
@@ -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"]
|
gliders-0.1.0/setup.cfg
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
|
+
|