spanks 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 spanks might be problematic. Click here for more details.
- spanks/__init__.py +2 -0
- spanks/noise_suppressor1D.py +123 -0
- spanks/noise_suppressor2D.py +147 -0
- spanks/synthetic_sky.py +310 -0
- spanks/synthetic_spectra.py +262 -0
- spanks/utils.py +221 -0
- spanks-0.1.0.dist-info/METADATA +776 -0
- spanks-0.1.0.dist-info/RECORD +11 -0
- spanks-0.1.0.dist-info/WHEEL +5 -0
- spanks-0.1.0.dist-info/licenses/LICENSE +674 -0
- spanks-0.1.0.dist-info/top_level.txt +1 -0
spanks/__init__.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.stats import norm
|
|
3
|
+
from scipy.signal import convolve
|
|
4
|
+
from astropy.convolution import Gaussian1DKernel
|
|
5
|
+
from .utils import sigma_clipping
|
|
6
|
+
|
|
7
|
+
def suppress_noise1d(profile, method ="simple", kernel = None, logit_alpha = 1.0, abs_profile = False):
|
|
8
|
+
"""
|
|
9
|
+
Suppress background-dominated samples in a 1D profile using a noise-based
|
|
10
|
+
probability model.
|
|
11
|
+
|
|
12
|
+
The function assumes that the profile noise follows an approximately
|
|
13
|
+
normal distribution. A noise probability profile is first computed from
|
|
14
|
+
the profile intensity distribution and then refined using one of several
|
|
15
|
+
methods. The resulting probability profile is applied as a weighting
|
|
16
|
+
function, enhancing statistically significant features while attenuating
|
|
17
|
+
samples consistent with the background noise.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
profile : ndarray
|
|
22
|
+
Input one-dimensional profile.
|
|
23
|
+
|
|
24
|
+
method : {'simple', 'logit', 'layers'}, optional
|
|
25
|
+
Method used to generate the probability profile.
|
|
26
|
+
|
|
27
|
+
* ``'simple'``: local smoothing of the noise probability profile.
|
|
28
|
+
* ``'logit'``: smoothing in logit space followed by a logistic
|
|
29
|
+
transformation.
|
|
30
|
+
* ``'layers'``: iterative probability refinement using an
|
|
31
|
+
additional intensity-weighted layer.
|
|
32
|
+
|
|
33
|
+
Default is ``'simple'``.
|
|
34
|
+
|
|
35
|
+
kernel : ndarray, optional
|
|
36
|
+
Kernel used for local probability aggregation. If ``None``, a
|
|
37
|
+
Gaussian kernel with a width equivalent to
|
|
38
|
+
``Gaussian1DKernel(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
|
+
abs_profile : bool, optional
|
|
46
|
+
If True will use absolute values to make to probability profile,
|
|
47
|
+
else it will use squared values. Default is False.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
ndarray
|
|
52
|
+
Background-suppressed profile rescaled to approximately match the
|
|
53
|
+
original profile intensity range.
|
|
54
|
+
|
|
55
|
+
See Also
|
|
56
|
+
--------
|
|
57
|
+
_get_p_simple_1d
|
|
58
|
+
_get_p_logit_1d
|
|
59
|
+
_get_p_layers_1d
|
|
60
|
+
_apply_p_profile
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
if kernel is None:
|
|
64
|
+
kernel = Gaussian1DKernel(2.0) # Larger kernels yield higher noise level, but more symmetric noise distribution
|
|
65
|
+
|
|
66
|
+
profile_noise_lvl, profile_noise_std = sigma_clipping(profile, robust=False)
|
|
67
|
+
|
|
68
|
+
profile_centered = profile - profile_noise_lvl
|
|
69
|
+
|
|
70
|
+
# Probability of obtaining x or lower:
|
|
71
|
+
if abs_profile:
|
|
72
|
+
noise_p_profile = norm.cdf(np.abs(profile_centered), loc=0, scale=profile_noise_std)
|
|
73
|
+
else:
|
|
74
|
+
noise_p_profile = norm.cdf(profile_centered**2, loc=0, scale=profile_noise_std)
|
|
75
|
+
|
|
76
|
+
if method == "logit":
|
|
77
|
+
p_prof = _get_p_logit_1d(noise_p_profile, kernel, alpha = logit_alpha)
|
|
78
|
+
elif method == "layers":
|
|
79
|
+
p_prof = _get_p_layers_1d(profile, noise_p_profile, kernel)
|
|
80
|
+
else: #simple
|
|
81
|
+
p_prof = _get_p_simple_1d(noise_p_profile, kernel)
|
|
82
|
+
|
|
83
|
+
bkg_reduced_image_leveled = _apply_p_profile(profile_centered, p_prof) + profile_noise_lvl
|
|
84
|
+
|
|
85
|
+
return bkg_reduced_image_leveled
|
|
86
|
+
|
|
87
|
+
def _get_p_simple_1d(p_prof, kernel):
|
|
88
|
+
p_out = convolve(p_prof, kernel, mode='same')
|
|
89
|
+
return p_out
|
|
90
|
+
|
|
91
|
+
def _get_p_logit_1d(p_prof, kernel, alpha = 1.0):
|
|
92
|
+
p_prof[p_prof >= 1] = 1-1e-9
|
|
93
|
+
logit = np.log(p_prof / (1 - p_prof))
|
|
94
|
+
L = convolve(logit, kernel, mode='same') # weighted sum of logit
|
|
95
|
+
p_out = 1.0 / (1.0 + np.exp(-alpha * L))
|
|
96
|
+
return p_out
|
|
97
|
+
|
|
98
|
+
def _get_p_layers_1d(data, p_prof, kernel, abs_profile = False):
|
|
99
|
+
p_prof_cnv = convolve(p_prof, kernel, mode='same')
|
|
100
|
+
data_p_prof_cnv = data * p_prof_cnv
|
|
101
|
+
data_p_prof_cnv_noise_lvl, data_p_prof_cnv_noise_lvl_std = sigma_clipping(data_p_prof_cnv, robust=False)
|
|
102
|
+
data_p_prof_cnv_center = data_p_prof_cnv - data_p_prof_cnv_noise_lvl
|
|
103
|
+
if abs_profile:
|
|
104
|
+
p_prof_new_layer = norm.cdf(np.abs(data_p_prof_cnv_center), loc=0, scale=data_p_prof_cnv_noise_lvl_std)
|
|
105
|
+
else:
|
|
106
|
+
p_prof_new_layer = norm.cdf(data_p_prof_cnv_center**2, loc=0, scale=data_p_prof_cnv_noise_lvl_std)
|
|
107
|
+
p_prof_new_layer_cnv = convolve(p_prof_new_layer, kernel, mode='same')
|
|
108
|
+
p_out = p_prof_cnv * p_prof_new_layer_cnv
|
|
109
|
+
return p_out
|
|
110
|
+
|
|
111
|
+
def _apply_p_profile(data, p_prof):
|
|
112
|
+
"""
|
|
113
|
+
Apply a probability map (p_prof) to an image and rescale according to their maxima.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
data_p_prof = data * p_prof
|
|
117
|
+
data_p_prof_leveled = data_p_prof * data.max() / data_p_prof.max()
|
|
118
|
+
return data_p_prof_leveled
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
quit()
|
|
122
|
+
|
|
123
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.stats import norm
|
|
3
|
+
from scipy.signal import convolve2d
|
|
4
|
+
from astropy.convolution import Gaussian2DKernel
|
|
5
|
+
from .utils import sigma_clipping
|
|
6
|
+
|
|
7
|
+
def suppress_noise2d(image, method ="simple", kernel = None, logit_alpha = 1.0, symmetric = False, abs_profile = False):
|
|
8
|
+
"""
|
|
9
|
+
Suppress background-dominated pixels in an image using a noise-based
|
|
10
|
+
probability model.
|
|
11
|
+
|
|
12
|
+
The function assumes that image noise follows an approximately normal
|
|
13
|
+
distribution. A noise probability map is first computed from the image
|
|
14
|
+
intensity distribution and then refined using one of several methods.
|
|
15
|
+
The resulting probability map is applied as a weighting function,
|
|
16
|
+
enhancing statistically significant structures while attenuating pixels
|
|
17
|
+
consistent with the background noise.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
image : ndarray
|
|
22
|
+
Input image.
|
|
23
|
+
|
|
24
|
+
method : {'simple', 'logit', 'layers'}, optional
|
|
25
|
+
Method used to generate the probability map.
|
|
26
|
+
|
|
27
|
+
* ``'simple'``: spatial smoothing of the noise probability map.
|
|
28
|
+
* ``'logit'``: smoothing in logit space followed by a logistic
|
|
29
|
+
transformation.
|
|
30
|
+
* ``'layers'``: iterative probability refinement using an
|
|
31
|
+
additional intensity-weighted layer.
|
|
32
|
+
|
|
33
|
+
Default is ``'simple'``.
|
|
34
|
+
|
|
35
|
+
kernel : ndarray, optional
|
|
36
|
+
Convolution kernel used for local probability aggregation. If
|
|
37
|
+
``None``, a ``Gaussian2DKernel(2.0)`` is used.
|
|
38
|
+
|
|
39
|
+
logit_alpha : float, optional
|
|
40
|
+
Scaling factor applied to the logistic transform when
|
|
41
|
+
``method='logit'``. Larger values increase the contrast between
|
|
42
|
+
low- and high-probability regions.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
ndarray
|
|
47
|
+
Background-suppressed image rescaled to approximately match the
|
|
48
|
+
original image intensity range.
|
|
49
|
+
|
|
50
|
+
See Also
|
|
51
|
+
--------
|
|
52
|
+
_get_p_simple2d
|
|
53
|
+
_get_p_logit2d
|
|
54
|
+
_get_p_layers2d
|
|
55
|
+
_apply_p_map
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if kernel is None:
|
|
59
|
+
kernel = Gaussian2DKernel(2.0) # Larger kernels yield higher noise level, but more symmetric noise distribution
|
|
60
|
+
|
|
61
|
+
image_noise_lvl, image_noise_std = sigma_clipping(image, robust=False)
|
|
62
|
+
|
|
63
|
+
# Probability of obtaining x or lower:
|
|
64
|
+
if symmetric:
|
|
65
|
+
if abs_profile:
|
|
66
|
+
noise_p_map = norm.cdf(np.abs(image), loc=image_noise_lvl, scale=image_noise_std)
|
|
67
|
+
else:
|
|
68
|
+
noise_p_map = norm.cdf(image**2, loc=image_noise_lvl, scale=image_noise_std)
|
|
69
|
+
else:
|
|
70
|
+
noise_p_map = norm.cdf(image, loc=image_noise_lvl, scale=image_noise_std)
|
|
71
|
+
|
|
72
|
+
if method == "logit":
|
|
73
|
+
p_map = _get_p_logit2d(noise_p_map, kernel, alpha = logit_alpha)
|
|
74
|
+
elif method == "layers":
|
|
75
|
+
p_map = _get_p_layers2d(image, noise_p_map, kernel, symmetric, abs_profile)
|
|
76
|
+
else: #simple
|
|
77
|
+
p_map = _get_p_simple2d(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_simple2d(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_logit2d(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_layers2d(image, p_map, kernel, symmetric = False, abs_profile = False):
|
|
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_lvl, image_p_map_cnv_noise_std = sigma_clipping(image_p_map_cnv, robust=False)
|
|
126
|
+
if symmetric:
|
|
127
|
+
if abs_profile:
|
|
128
|
+
p_map_new_layer = norm.cdf(np.abs(image_p_map_cnv), loc=image_p_map_cnv_noise_lvl, scale=image_p_map_cnv_noise_std)
|
|
129
|
+
else:
|
|
130
|
+
p_map_new_layer = norm.cdf(image_p_map_cnv**2, loc=image_p_map_cnv_noise_lvl, scale=image_p_map_cnv_noise_std)
|
|
131
|
+
else:
|
|
132
|
+
p_map_new_layer = norm.cdf(image_p_map_cnv, loc=image_p_map_cnv_noise_lvl, scale=image_p_map_cnv_noise_std)
|
|
133
|
+
p_map_new_layer_cnv = convolve2d(p_map_new_layer, kernel, mode='same', boundary='symm')
|
|
134
|
+
p_out = p_map_cnv * p_map_new_layer_cnv
|
|
135
|
+
return p_out
|
|
136
|
+
|
|
137
|
+
def _apply_p_map(image, p_map):
|
|
138
|
+
"""
|
|
139
|
+
Apply a probability map (p_map) to an image and rescale according to their maxima.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
image_p_map = image * p_map
|
|
143
|
+
image_p_map_leveled = image_p_map * image.max() / image_p_map.max()
|
|
144
|
+
return image_p_map_leveled
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
quit()
|
spanks/synthetic_sky.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from astropy.modeling.models import Sersic2D
|
|
3
|
+
|
|
4
|
+
def _get_psf_sources(number_of_psf_sources, image_size, psf_fwhm_range, psf_dynamic_range, symmetric):
|
|
5
|
+
"""
|
|
6
|
+
Generate an image containing randomly positioned PSF-like sources.
|
|
7
|
+
|
|
8
|
+
Each source is modeled as a circular two-dimensional Gaussian profile
|
|
9
|
+
with a randomly selected position, width, and peak intensity. The
|
|
10
|
+
resulting source images are summed to produce a single image.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
number_of_psf_sources : int
|
|
15
|
+
Number of PSF sources to generate.
|
|
16
|
+
|
|
17
|
+
image_size : tuple of int
|
|
18
|
+
Output image size as ``(nx, ny)``.
|
|
19
|
+
|
|
20
|
+
psf_fwhm_range : tuple of float
|
|
21
|
+
Minimum and maximum full width at half maximum (FWHM) values for
|
|
22
|
+
the generated Gaussian sources.
|
|
23
|
+
|
|
24
|
+
psf_dynamic_range : tuple of float
|
|
25
|
+
Minimum and maximum absolute source amplitudes.
|
|
26
|
+
|
|
27
|
+
symmetric : bool
|
|
28
|
+
If ``True``, sources may have either positive or negative peak
|
|
29
|
+
amplitudes. If ``False``, all amplitudes are positive.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
ndarray
|
|
34
|
+
Image containing the sum of all generated PSF sources.
|
|
35
|
+
|
|
36
|
+
Notes
|
|
37
|
+
-----
|
|
38
|
+
The Gaussian standard deviation is computed from the FWHM using
|
|
39
|
+
|
|
40
|
+
``sigma = FWHM / 2.3548``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
all_psf_image = np.zeros(image_size, dtype = float)
|
|
44
|
+
y, x = np.mgrid[:image_size[1], :image_size[0]]
|
|
45
|
+
x_positions = np.random.random(number_of_psf_sources) * image_size[0]
|
|
46
|
+
y_positions = np.random.random(number_of_psf_sources) * image_size[1]
|
|
47
|
+
sources_fwhms = np.random.random(number_of_psf_sources) * (psf_fwhm_range[1] - psf_fwhm_range[0]) + psf_fwhm_range[0]
|
|
48
|
+
sources_stds = sources_fwhms/2.3548 # std = fwhm/2.3548
|
|
49
|
+
|
|
50
|
+
if symmetric:
|
|
51
|
+
peaks = (2 * np.random.random(number_of_psf_sources) - 1) * (psf_dynamic_range[1] - psf_dynamic_range[0]) #+ psf_dynamic_range[0]
|
|
52
|
+
peaks = peaks + psf_dynamic_range[0] * peaks / np.abs(peaks)
|
|
53
|
+
else:
|
|
54
|
+
peaks = (np.random.random(number_of_psf_sources)) * (psf_dynamic_range[1] - psf_dynamic_range[0]) + psf_dynamic_range[0]
|
|
55
|
+
sources = zip(x_positions, y_positions, sources_stds, peaks)
|
|
56
|
+
for this_source in sources:
|
|
57
|
+
sigma = this_source[2]
|
|
58
|
+
source_g = np.exp(-((x-this_source[0])**2 + (y-this_source[1])**2) / (2*sigma**2))
|
|
59
|
+
source_g = this_source[3] * source_g/source_g.max()
|
|
60
|
+
all_psf_image += source_g
|
|
61
|
+
return all_psf_image
|
|
62
|
+
|
|
63
|
+
def _get_sersic_sources(number_of_sersic_sources, image_size, r_eff_range, n_range, ellip_range, theta_range,
|
|
64
|
+
dynamic_range, symmetric = False):
|
|
65
|
+
"""
|
|
66
|
+
Generate an image containing randomly positioned Sérsic profile sources.
|
|
67
|
+
|
|
68
|
+
Each source is modeled using a two-dimensional Sérsic profile with
|
|
69
|
+
randomly selected structural parameters and position. The generated
|
|
70
|
+
source images are summed to produce a composite image.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
number_of_sersic_sources : int
|
|
75
|
+
Number of Sérsic sources to generate.
|
|
76
|
+
|
|
77
|
+
image_size : tuple of int
|
|
78
|
+
Output image size as ``(nx, ny)``.
|
|
79
|
+
|
|
80
|
+
r_eff_range : tuple of float
|
|
81
|
+
Minimum and maximum effective radius values.
|
|
82
|
+
|
|
83
|
+
n_range : tuple of float
|
|
84
|
+
Minimum and maximum Sérsic indices.
|
|
85
|
+
|
|
86
|
+
ellip_range : tuple of float
|
|
87
|
+
Minimum and maximum ellipticities.
|
|
88
|
+
|
|
89
|
+
theta_range : tuple of float
|
|
90
|
+
Minimum and maximum position angles in radians.
|
|
91
|
+
|
|
92
|
+
dynamic_range : tuple of float
|
|
93
|
+
Minimum and maximum source amplitudes.
|
|
94
|
+
|
|
95
|
+
symmetric : bool, optional
|
|
96
|
+
If ``True``, each source is randomly assigned either a positive
|
|
97
|
+
or negative sign. Default is ``False``.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
ndarray
|
|
102
|
+
Image containing the sum of all generated Sérsic sources.
|
|
103
|
+
|
|
104
|
+
See Also
|
|
105
|
+
--------
|
|
106
|
+
astropy.modeling.functional_models.Sersic2D
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
all_sersic_image = np.zeros(image_size, dtype = float)
|
|
111
|
+
y, x = np.mgrid[:image_size[1], :image_size[0]]
|
|
112
|
+
x_positions = np.random.random(number_of_sersic_sources) * image_size[0]
|
|
113
|
+
y_positions = np.random.random(number_of_sersic_sources) * image_size[1]
|
|
114
|
+
r_eff = np.random.random(number_of_sersic_sources) * (r_eff_range[1] - r_eff_range[0]) + r_eff_range[0]
|
|
115
|
+
n = np.random.random(number_of_sersic_sources) * (n_range[1] - n_range[0]) + n_range[0]
|
|
116
|
+
ellip = np.random.random(number_of_sersic_sources) * (ellip_range[1] - ellip_range[0]) + ellip_range[0]
|
|
117
|
+
theta = np.random.random(number_of_sersic_sources) * (theta_range[1] - theta_range[0]) + theta_range[0]
|
|
118
|
+
peaks = (np.random.random(number_of_sersic_sources)) * (dynamic_range[1] - dynamic_range[0]) + dynamic_range[0]
|
|
119
|
+
sources = zip(x_positions, y_positions, r_eff, n, ellip, theta, peaks)
|
|
120
|
+
for this_source in sources:
|
|
121
|
+
sersic_model = Sersic2D(amplitude=this_source[6], r_eff=this_source[2], n=this_source[3], x_0=this_source[0], y_0=this_source[1], ellip=this_source[4], theta=this_source[5])
|
|
122
|
+
galaxy = sersic_model(x, y)
|
|
123
|
+
galaxy = this_source[6] * galaxy/galaxy.max()
|
|
124
|
+
if symmetric:
|
|
125
|
+
rd = np.random.rand() - 0.5
|
|
126
|
+
galaxy = galaxy * (rd/np.abs(rd))
|
|
127
|
+
all_sersic_image += galaxy
|
|
128
|
+
return all_sersic_image
|
|
129
|
+
|
|
130
|
+
def get_synthetic_sky_image(image_size = None, noise_std = 0.125, number_of_psf_sources = 25, psf_fwhm_range = None,
|
|
131
|
+
psf_dynamic_range = None, number_of_sersic_sources = 8, r_eff_range = None, n_range = None,
|
|
132
|
+
ellip_range = None, theta_range = None, sersic_dynamic_range = None,
|
|
133
|
+
gradient_level = 0.5, gradient_slope = 1.5, symmetric = False):
|
|
134
|
+
"""
|
|
135
|
+
Generate a synthetic astronomical image containing noise,
|
|
136
|
+
background structure, point sources, and extended galaxies.
|
|
137
|
+
|
|
138
|
+
The generated image consists of four components:
|
|
139
|
+
|
|
140
|
+
* Gaussian-distributed background noise.
|
|
141
|
+
* A large-scale background gradient.
|
|
142
|
+
* PSF-like point sources represented by Gaussian profiles.
|
|
143
|
+
* Extended sources represented by Sérsic profiles.
|
|
144
|
+
|
|
145
|
+
This function is intended for testing and benchmarking image
|
|
146
|
+
processing, source detection, and background estimation algorithms.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
image_size : tuple of int, optional
|
|
151
|
+
Output image size as ``(nx, ny)``.
|
|
152
|
+
Default is ``(1000, 1000)``.
|
|
153
|
+
|
|
154
|
+
noise_std : float, optional
|
|
155
|
+
Standard deviation of the Gaussian background noise.
|
|
156
|
+
Default is ``0.125``.
|
|
157
|
+
|
|
158
|
+
number_of_psf_sources : int, optional
|
|
159
|
+
Number of PSF sources to generate.
|
|
160
|
+
Default is ``25``.
|
|
161
|
+
|
|
162
|
+
psf_fwhm_range : tuple of float, optional
|
|
163
|
+
Range of PSF FWHM values. Default is ``(1000, 1000)``.
|
|
164
|
+
|
|
165
|
+
psf_dynamic_range : tuple of float, optional
|
|
166
|
+
Range of PSF amplitudes. Default is ``(1, 4)``.
|
|
167
|
+
|
|
168
|
+
number_of_sersic_sources : int, optional
|
|
169
|
+
Number of Sérsic sources to generate.
|
|
170
|
+
Default is ``8``.
|
|
171
|
+
|
|
172
|
+
r_eff_range : tuple of float, optional
|
|
173
|
+
Range of effective radii for Sérsic profiles.
|
|
174
|
+
Default is ``(100, 170)``.
|
|
175
|
+
|
|
176
|
+
n_range : tuple of float, optional
|
|
177
|
+
Range of Sérsic indices.
|
|
178
|
+
Default is ``(1, 4)``.
|
|
179
|
+
|
|
180
|
+
ellip_range : tuple of float, optional
|
|
181
|
+
Range of Sérsic ellipticities.
|
|
182
|
+
Default is ``(0, 0.8)``.
|
|
183
|
+
|
|
184
|
+
theta_range : tuple of float, optional
|
|
185
|
+
Range of Sérsic position angles in radians.
|
|
186
|
+
Default is ``(0, pi)``.
|
|
187
|
+
|
|
188
|
+
sersic_dynamic_range : tuple of float, optional
|
|
189
|
+
Range of Sérsic source amplitudes.
|
|
190
|
+
Default is ``(1, 4)``.
|
|
191
|
+
|
|
192
|
+
gradient_level : float, optional
|
|
193
|
+
Constant background level of the smooth gradient component.
|
|
194
|
+
Default is ``0.5``.
|
|
195
|
+
|
|
196
|
+
gradient_slope : float, optional
|
|
197
|
+
Controls the strength of the background gradient.
|
|
198
|
+
Default is ``1.5``.
|
|
199
|
+
|
|
200
|
+
symmetric : bool, optional
|
|
201
|
+
If ``True``, generated sources may have positive or negative
|
|
202
|
+
amplitudes. Default is ``False``.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
ndarray
|
|
207
|
+
Synthetic image containing background noise, gradient,
|
|
208
|
+
PSF sources, and Sérsic sources.
|
|
209
|
+
|
|
210
|
+
See Also
|
|
211
|
+
--------
|
|
212
|
+
_get_psf_sources
|
|
213
|
+
_get_sersic_sources
|
|
214
|
+
_get_simple_gradient
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if image_size is None:
|
|
219
|
+
image_size = (1000, 1000)
|
|
220
|
+
if psf_fwhm_range is None:
|
|
221
|
+
psf_fwhm_range = (1000, 1000)
|
|
222
|
+
if psf_dynamic_range is None:
|
|
223
|
+
psf_dynamic_range = (1, 4)
|
|
224
|
+
if r_eff_range is None:
|
|
225
|
+
r_eff_range = (100, 170)
|
|
226
|
+
if n_range is None:
|
|
227
|
+
n_range = (1, 4)
|
|
228
|
+
if ellip_range is None:
|
|
229
|
+
ellip_range = (0, 0.8)
|
|
230
|
+
if theta_range is None:
|
|
231
|
+
theta_range = (0, np.pi)
|
|
232
|
+
if sersic_dynamic_range is None:
|
|
233
|
+
sersic_dynamic_range = (1, 4)
|
|
234
|
+
|
|
235
|
+
psfs_image = _get_psf_sources(number_of_psf_sources, image_size, psf_fwhm_range, psf_dynamic_range, symmetric)
|
|
236
|
+
sersic_image = _get_sersic_sources(number_of_sersic_sources, image_size, r_eff_range, n_range, ellip_range, theta_range, sersic_dynamic_range, symmetric)
|
|
237
|
+
full_map = np.zeros(image_size, dtype = float)
|
|
238
|
+
bkg_gradient = _get_simple_gradient(image_size, gradient_level, gradient_slope)
|
|
239
|
+
noise = np.random.normal(loc=0.0, scale=noise_std, size=(image_size[1], image_size[0]))
|
|
240
|
+
full_map += noise + bkg_gradient + psfs_image + sersic_image
|
|
241
|
+
|
|
242
|
+
return full_map
|
|
243
|
+
|
|
244
|
+
def _get_simple_gradient(image_size, level = 0.5, slope = 1.5):
|
|
245
|
+
"""
|
|
246
|
+
Generate a simple two-dimensional background gradient.
|
|
247
|
+
|
|
248
|
+
The gradient increases linearly along both image axes and is
|
|
249
|
+
normalized such that its minimum value is approximately equal to
|
|
250
|
+
``level``.
|
|
251
|
+
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
image_size : tuple of int
|
|
255
|
+
Output image size as ``(nx, ny)``.
|
|
256
|
+
|
|
257
|
+
level : float, optional
|
|
258
|
+
Baseline offset added to the gradient.
|
|
259
|
+
Default is ``0.5``.
|
|
260
|
+
|
|
261
|
+
slope : float, optional
|
|
262
|
+
Gradient strength. A value of ``0`` produces a flat image.
|
|
263
|
+
Default is ``1.5``.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
ndarray
|
|
268
|
+
Two-dimensional background gradient image.
|
|
269
|
+
|
|
270
|
+
Notes
|
|
271
|
+
-----
|
|
272
|
+
When ``slope == 0``, a zero-valued image with shape
|
|
273
|
+
``image_size`` is returned.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
y, x = np.mgrid[:image_size[1], :image_size[0]]
|
|
277
|
+
if slope != 0:
|
|
278
|
+
gradient = slope * (x / image_size[1] + y / image_size[0]) / 2
|
|
279
|
+
gradient = level + gradient/gradient.max()
|
|
280
|
+
else:
|
|
281
|
+
return np.zeros(image_size, dtype = float)
|
|
282
|
+
# gradient = 0.2 + 0.5 * x / image_size[1]
|
|
283
|
+
return gradient
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
import matplotlib.pyplot as plt
|
|
287
|
+
|
|
288
|
+
image_size = (1000, 1000)
|
|
289
|
+
symmetric = False
|
|
290
|
+
noise_std = 0.125
|
|
291
|
+
|
|
292
|
+
number_of_psf_sources = 25
|
|
293
|
+
psf_fwhm_range = (2, 14)
|
|
294
|
+
psf_dynamic_range = (1, 4)
|
|
295
|
+
|
|
296
|
+
number_of_sersic_sources = 8
|
|
297
|
+
r_eff_range = (100, 170)
|
|
298
|
+
n_range = (1, 4)
|
|
299
|
+
ellip_range = (0, 0.8)
|
|
300
|
+
theta_range = (0, np.pi)
|
|
301
|
+
sersic_dynamic_range = (1, 4)
|
|
302
|
+
|
|
303
|
+
gradient_level = 0.2
|
|
304
|
+
gradient_slope = 2
|
|
305
|
+
|
|
306
|
+
full_map = get_synthetic_sky_image(image_size, noise_std, number_of_psf_sources, psf_fwhm_range, psf_dynamic_range,
|
|
307
|
+
number_of_sersic_sources, r_eff_range, n_range, ellip_range, theta_range,
|
|
308
|
+
sersic_dynamic_range, gradient_level, gradient_slope, symmetric)
|
|
309
|
+
plt.imshow(full_map)
|
|
310
|
+
plt.show()
|