Skip to content

Commit 0632e1d

Browse files
committed
Restructure sourcefinding code, add barycentre calculations.
1 parent d6eba20 commit 0632e1d

6 files changed

Lines changed: 540 additions & 109 deletions

File tree

‎docs/basic_sourcefinder_example.ipynb‎

Lines changed: 378 additions & 38 deletions
Large diffs are not rendered by default.

‎fastimgproto/sourcefind/detect.py‎

Lines changed: 0 additions & 48 deletions
This file was deleted.

‎fastimgproto/sourcefind/fit.py‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
1-
import astropy.modeling.models as models
2-
3-
4-
models.Gaussian2D
51

62

‎fastimgproto/sourcefind/image.py‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import astropy.stats
2+
import numpy as np
3+
from scipy import ndimage
4+
from attr import attributes, attr
5+
6+
7+
@attributes
8+
class IslandParams(object):
9+
"""
10+
Data structure for representing source 'islands'
11+
12+
Initialized with parent image, label index, and peak-pixel value.
13+
14+
Attributes:
15+
parent (SourceFindImage): The image the island was detected from.
16+
label_idx (int): Index of region in label-map of source image.
17+
peak_val (float): Peak pixel value
18+
peak_x_idx (int): Peak pixel x-index
19+
peak_y_idx (int): Peak pixel y-index
20+
xbar (float): Barycentric centre in x-pixel index
21+
ybar(float): Barycentric centre in y-pixel index
22+
"""
23+
parent = attr()
24+
label_idx = attr()
25+
peak_val = attr()
26+
peak_x_idx = attr(default=None)
27+
peak_y_idx = attr(default=None)
28+
xbar = attr(default=None)
29+
ybar = attr(default=None)
30+
31+
def calculate_params(self):
32+
"""
33+
Analyses an 'island' to extract further parameters
34+
"""
35+
self.data = np.ma.MaskedArray(
36+
self.parent.data,
37+
mask=_label_mask(self.parent.label_map, self.label_idx),
38+
39+
)
40+
self.peak_y_idx, self.peak_x_idx = _max_pixel_index(self.data)
41+
sum = np.ma.sum(self.data)
42+
self.xbar = np.ma.sum(self.parent.xgrid * self.data) / sum
43+
self.ybar = np.ma.sum(self.parent.ygrid * self.data) / sum
44+
45+
46+
def _label_mask(labels_map, label_num):
47+
return ~(labels_map == label_num)
48+
49+
50+
def _max_pixel_index(masked_image):
51+
"""
52+
Returns max pixel index in np array ordering, i.e. (y_max, x_max)
53+
"""
54+
return np.unravel_index(np.ma.argmax(masked_image),
55+
masked_image.shape)
56+
57+
58+
class SourceFindImage(object):
59+
"""
60+
Data structure for collecting intermediate results from source-extraction.
61+
62+
This can be useful for verifying / debugging the sourcefinder results,
63+
and intermediate results can also be reused to save recalculation.
64+
65+
Args:
66+
data (array_like): numpy.ndarray or numpy.ma.MaskedArray containing
67+
image data.
68+
detection_n_sigma (float): Detection threshold as multiple of RMS
69+
analysis_n_sigma (float): Analysis threshold as multiple of RMS
70+
rms_est (float): RMS estimate (may be `None`, in which case RMS is
71+
estimated from the image data via sigma-clipping).
72+
"""
73+
74+
def __init__(self, data, detection_n_sigma, analysis_n_sigma,
75+
rms_est=None):
76+
self.data = data
77+
self.detection_n_sigma = detection_n_sigma
78+
self.analysis_n_sigma = analysis_n_sigma
79+
80+
self.rms_est = rms_est
81+
if self.rms_est is None:
82+
self.rms_est = _estimate_rms(self.data)
83+
self.bg_level = np.ma.median(self.data)
84+
self.analysis_thresh = self.bg_level + self.analysis_n_sigma * self.rms_est
85+
self.detection_thresh = self.bg_level + self.detection_n_sigma * self.rms_est
86+
87+
self.ygrid, self.xgrid = np.indices(self.data.shape)
88+
89+
# Label connected regions
90+
self.label_map, label_maxvals = _label_detection_islands(
91+
self.data, self.analysis_thresh, self.detection_thresh
92+
)
93+
94+
self.islands = []
95+
for l_idx, l_maxval in label_maxvals.items():
96+
self.islands.append(
97+
IslandParams(parent=self, label_idx=l_idx, peak_val=l_maxval))
98+
for island in self.islands:
99+
island.calculate_params()
100+
101+
102+
def _estimate_rms(image):
103+
clipped_data = astropy.stats.sigma_clip(image)
104+
clipped_stddev = np.ma.std(clipped_data)
105+
return clipped_stddev
106+
107+
108+
def _label_detection_islands(data, analysis_thresh, detection_thresh):
109+
"""
110+
Find regions which are above analysis_thresh and peak above detection_thresh.
111+
112+
Args:
113+
data (array_like): Raw data (may be masked)
114+
analysis_thresh (float): Analysis ('single connected region') threshold
115+
detection_thresh (float): Detections threshold
116+
117+
Returns:
118+
tuple (array_like, dict): Tuple of `(label_map, valid_labels)`.
119+
`label_map` is an ndarray containing the pixel labels.
120+
`valid_labels` is a dict mapping valid label numbers to the maximum
121+
pixel-value in that label-region.
122+
123+
"""
124+
analysis_map = data > analysis_thresh
125+
label_map, n_labels = ndimage.label(analysis_map)
126+
127+
label_maxvals = ndimage.maximum(data, label_map,
128+
index=range(1, n_labels + 1))
129+
130+
valid_label_maxvals = {}
131+
# Delabel islands that don't meet detection threshold:
132+
for zero_idx, maxval in enumerate(label_maxvals):
133+
label = zero_idx + 1
134+
if maxval < detection_thresh:
135+
label_map[label_map == label] = 0.
136+
else:
137+
valid_label_maxvals[label] = maxval
138+
return label_map, valid_label_maxvals

‎fastimgproto/tests/test_sourcefind/test_detection.py‎

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
gaussian_point_source,
44
uncorrelated_gaussian_noise_background
55
)
6-
import fastimgproto.sourcefind.detect as detect
6+
from fastimgproto.sourcefind.image import (SourceFindImage, _estimate_rms)
77
import numpy as np
88

99
ydim = 128
1010
xdim = 64
1111
rms = 1.0
12-
bright_src = gaussian_point_source(x_centre=48, y_centre=52, amplitude=10.0)
12+
bright_src = gaussian_point_source(x_centre=48.24, y_centre=52.66, amplitude=10.0)
1313
faint_src = gaussian_point_source(x_centre=32, y_centre=64, amplitude=3.5)
1414

1515

@@ -18,7 +18,7 @@ def test_rms_estimation():
1818
sigma=rms)
1919
img += evaluate_model_on_pixel_grid(img.shape, bright_src)
2020
img += evaluate_model_on_pixel_grid(img.shape, faint_src)
21-
rms_est= detect.estimate_rms(img)
21+
rms_est= _estimate_rms(img)
2222
# print "RMS EST:", rms_est
2323
assert np.abs((rms_est - rms) / rms) < 0.05
2424

@@ -31,25 +31,29 @@ def test_basic_source_detection():
3131
img += evaluate_model_on_pixel_grid(img.shape, bright_src)
3232
# img += evaluate_model_on_pixel_grid(img.shape, faint_src)
3333

34-
srcs = detect.extract_sources(img, detection_n_sigma=4,
35-
analysis_n_sigma=3,
36-
rms_estimate=rms)
37-
assert len(srcs) == 1
38-
pixel_idx, pixel_value = srcs[0]
39-
y_idx, x_idx = pixel_idx
40-
assert np.abs(y_idx - bright_src.y_mean) <1.1
41-
assert np.abs(x_idx - bright_src.x_mean) < 1.1
34+
sf = SourceFindImage(img, detection_n_sigma=4,
35+
analysis_n_sigma=3,
36+
rms_est=rms)
37+
assert len(sf.islands) == 1
38+
src = sf.islands[0]
39+
print bright_src
40+
print src
41+
assert np.abs(src.peak_x_idx - bright_src.x_mean) <0.5
42+
assert np.abs(src.peak_y_idx - bright_src.y_mean) <0.5
43+
assert np.abs(src.xbar - bright_src.x_mean) <0.1
44+
assert np.abs(src.ybar - bright_src.y_mean) <0.1
45+
4246

4347
# We expect to detect the bright source, but not the faint one.
4448
img += evaluate_model_on_pixel_grid(img.shape, faint_src)
45-
srcs = detect.extract_sources(img, detection_n_sigma=4,
46-
analysis_n_sigma=3,
47-
rms_estimate=rms)
48-
assert len(srcs) == 1
49+
sf = SourceFindImage(img, detection_n_sigma=4,
50+
analysis_n_sigma=3,
51+
rms_est=rms)
52+
assert len(sf.islands) == 1
4953
# Unless we add it again and effectively double the faint_src flux
5054
img += evaluate_model_on_pixel_grid(img.shape, faint_src)
51-
srcs = detect.extract_sources(img, detection_n_sigma=4,
52-
analysis_n_sigma=3,
53-
rms_estimate=rms)
54-
assert len(srcs) == 2
55+
sf = SourceFindImage(img, detection_n_sigma=4,
56+
analysis_n_sigma=3,
57+
rms_est=rms)
58+
assert len(sf.islands) == 2
5559

‎setup.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
install_requires = [
77
'astropy',
8+
'attrs',
89
'click',
910
'pandas',
1011
'pytest',

0 commit comments

Comments
 (0)