Skip to content

Commit 8d0726c

Browse files
authored
Merge pull request #611 from DavidT3/bug/massGenerationProblems
Bug/mass generation problems
2 parents cb1f376 + 2f77868 commit 8d0726c

File tree

7 files changed

+874
-39
lines changed

7 files changed

+874
-39
lines changed

xga/imagetools/bin.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# This code is a part of XMM: Generate and Analyse (XGA), a module designed for the XMM Cluster Survey (XCS).
2+
# Last modified by David J Turner (david.turner@sussex.ac.uk) 30/08/2021, 09:32. Copyright (c) David J Turner
3+
4+
from warnings import warn
5+
6+
import matplotlib.pyplot as plt
7+
import numpy as np
8+
from astropy.units import Quantity, UnitConversionError
9+
from tqdm import tqdm
10+
11+
from ..imagetools.misc import edge_finder, data_limits
12+
from ..products import RateMap
13+
14+
CONT_BIN_METRICS = ['counts', 'snr']
15+
MAX_VAL_UNITS = ['ct', '']
16+
17+
18+
def _fill_reg_mask(outline_mask: np.ndarray) -> np.ndarray:
19+
"""
20+
21+
22+
:param np.ndarray outline_mask:
23+
:return:
24+
:rtype: np.ndarray
25+
"""
26+
27+
raise NotImplementedError("This idea is still under development")
28+
29+
all_x = np.arange(0, outline_mask.shape[1])
30+
x_lims, y_lims = data_limits(outline_mask)
31+
32+
plt.figure(figsize=(10, 10))
33+
plt.imshow(outline_mask, origin='lower', cmap='gray')
34+
plt.xlim(x_lims)
35+
plt.ylim(y_lims)
36+
plt.show()
37+
38+
filled_mask = np.zeros(outline_mask.shape)
39+
for y in range(y_lims[0], y_lims[1]+1):
40+
x_bnds = np.where(outline_mask[y, :] == 1)[0]
41+
42+
if len(x_bnds) == 0:
43+
continue
44+
elif len(x_bnds) == 1:
45+
filled_line = x_bnds[0]
46+
else:
47+
filled_line = np.where((all_x >= x_bnds.min()) & (all_x <= x_bnds.max()))
48+
49+
filled_mask[y, filled_line] = 1
50+
51+
plt.figure(figsize=(10, 10))
52+
plt.imshow(filled_mask, origin='lower', cmap='gray')
53+
plt.xlim(x_lims)
54+
plt.ylim(y_lims)
55+
plt.show()
56+
import sys
57+
sys.exit()
58+
59+
60+
def contour_bin_masks(prod: RateMap, src_mask: np.ndarray = None, bck_mask: np.ndarray = None,
61+
start_pos: Quantity = None, max_masks: int = 20, metric: str = 'counts',
62+
max_val: Quantity = Quantity(1000, 'ct')) -> np.ndarray:
63+
"""
64+
This method implements different spins on Jeremy Sanders' contour binning
65+
method (https://doi.org/10.1111/j.1365-2966.2006.10716.x) to split the 2D ratemap into bins that
66+
are spatially and morphologically connected (in theory). This can make some nice images, and also allows
67+
you to use those new regions to measure projected spectral quantities (temperature, metallicity, density)
68+
and make a 2D projected property map. After the first bin is defined at start_pos, further bins will be
69+
started at the brightest pixels left.
70+
71+
Current allowable metric choices:
72+
* 'counts' - Stop adding to bin when total background subtracted counts are over max_val
73+
* 'snr' - Stop adding to bin when signal to noise is over max_val.
74+
75+
This function cannot be used on Images as they lack the exposure map information necessary to define the area of
76+
the the regions.
77+
78+
:param RateMap prod: The ratemap to apply the contour binning process to.
79+
:param np.ndarray src_mask: A mask that removes emission from regions not associated with the source you're
80+
analysing, including removing interloper sources. Default is None, in which case no mask will be applied.
81+
:param np.ndarray bck_mask: A mask defining the background region. Default is None in which case no background
82+
subtraction will be used.
83+
:param Quantity start_pos: The position at which to start the binning process, in units of degrees or pixels.
84+
This parameter is None by default, in which case the brightest pixel (after masks have been applied), will
85+
be used as the starting point.
86+
:param int max_masks: A simple cut off for the number of masks that can be produced by this
87+
function, default is 20.
88+
:param str metric: The metric by which to judge when to stop adding new pixels to a bin (see docstring).
89+
:param Quantity max_val: The max value for the chosen metric, above which a new bin is started.
90+
:return: A 3D array of bin masks, the first two dimensions are the size of the input image, the third is
91+
the number of regions that have been generated.
92+
:rtype: np.ndarray
93+
"""
94+
raise NotImplementedError("This function is not fully implemented yet!")
95+
96+
# First checks to make sure no illegal values have been passed for the metric, and that the max value is in the
97+
# right units for the chosen metric.
98+
if metric not in CONT_BIN_METRICS:
99+
cont_av = ", ".join(CONT_BIN_METRICS)
100+
raise ValueError("{m} is not a recognised contour binning metric, please use one of the "
101+
"following; {a}".format(m=metric, a=cont_av))
102+
elif max_val.unit.to_string() != MAX_VAL_UNITS[CONT_BIN_METRICS.index(metric)]:
103+
raise UnitConversionError("The {m} metric requires a max value in units of "
104+
"{u}".format(m=metric, u=MAX_VAL_UNITS[CONT_BIN_METRICS.index(metric)]))
105+
106+
# We use type here rather than isinstance because ExpMaps are also a subclass of Image, so would get through
107+
# that check
108+
if type(prod) != RateMap:
109+
raise TypeError("Only XGA RateMap products can be binned with this function.")
110+
111+
# I do warn the user in this case, because it seems like a strange choice
112+
if src_mask is None and bck_mask is None:
113+
warn("You have not passed a src or bck mask, the whole image will be binned and no background subtraction "
114+
"will be applied")
115+
116+
# If the src mask parameter is None, we assemble an array of ones, to allow all pixels to be considered
117+
if src_mask is None:
118+
src_mask = np.full(prod.shape, 1)
119+
120+
# If the background mask is None then the user doesn't wish to take background into
121+
# account, otherwise we measure a background level and create a background map
122+
if bck_mask is not None:
123+
# I don't know if this is the correct way to go about this, it'll be split off into its own entry in
124+
# imagetools anyway I think, but this will do for now
125+
im_bck_dat = prod.image.data.copy() * bck_mask
126+
ex_bck_dat = prod.expmap.data.copy() * bck_mask
127+
128+
# bck_rt = Quantity(im_bck_dat.sum()/ex_bck_dat.sum(), 'ct/s')
129+
#
130+
# print(bck_rt)
131+
# area = Quantity((bck_mask*prod.sensor_mask).sum(), 'pix^2')
132+
# bck_rt_per_pix = bck_rt / area
133+
#
134+
# print(bck_rt_per_pix)
135+
136+
# Not sure about this at all, but it'll do for now
137+
bck_rt_per_pix = im_bck_dat.sum() / ex_bck_dat.sum()
138+
# A map of the background COUNTS at each pixel
139+
bck_cnt_map = Quantity(prod.expmap.data.copy() * bck_rt_per_pix, 'ct')
140+
else:
141+
bck_cnt_map = Quantity(np.zeros(prod.shape), 'ct')
142+
143+
# If the user hasn't supplied us with somewhere to start, we just select the brightest pixel
144+
# remaining after masking, just using the Image or RateMap simple peak method
145+
if start_pos is None:
146+
start_pos = prod.simple_peak(src_mask, 'pix')[0].value
147+
else:
148+
# If the user has supplied a start position then we want it to be in pixels
149+
start_pos = prod.coord_conv(start_pos, 'pix').value
150+
151+
# While start_pos is the very first start position and is immutable, each region will need its own
152+
# start coordinate that is stored in this variable
153+
# We flip it so that it makes sense in the context of accessing numpy arrays
154+
cur_bin_sp = start_pos.copy()[[1, 0]]
155+
156+
# This variable controls the outermost while loop, and will be set to true when all bins are complete
157+
all_done = False
158+
# This is what will be output, the final set of masks, will be made into a 3D numpy array at the
159+
# end, rather than just a list of arrays
160+
all_masks = []
161+
# This list stores whether given masks reached their required value or were terminated for some other reason
162+
reached_max = []
163+
164+
# Making a copy of the data array and masking it so that only what the user wants the algorithm to consider remains
165+
prod_dat = prod.data.copy() * src_mask
166+
167+
# The various product type and metric combinations
168+
# There are many while loops in this process and they make me sad, but we don't know a priori how
169+
# many bins there will be when we're done
170+
if metric == 'counts':
171+
172+
# Here we create a total mask where all previously claimed parts of the image are marked
173+
# off. As bins are laid down this will be slowly blocked off, but it starts as all ones
174+
no_go = np.ones(prod_dat.shape)
175+
176+
with tqdm(desc="Generating Contour Bins", total=max_masks) as open_ended:
177+
while not all_done and len(all_masks) < max_masks:
178+
# Setup the mask for this region
179+
cur_mask = np.zeros(prod.shape)
180+
# We already know where we're starting, so that is the first point included in the mask
181+
cur_mask[cur_bin_sp[0], cur_bin_sp[1]] = 1
182+
183+
# Fetch the counts in the start pixel
184+
start_cnt = prod.get_count(Quantity([cur_bin_sp[1], cur_bin_sp[0]], 'pix'))
185+
# Fetch the background counts in the start pixel
186+
start_bck_cnt = bck_cnt_map[cur_bin_sp[0], cur_bin_sp[1]]
187+
start_bck_cnt = Quantity(20, 'ct')
188+
189+
# For this count based metric, this is were the total counts is totted up. We make sure that it can't
190+
# go below zero
191+
tot_cnts = Quantity([start_cnt-start_bck_cnt, Quantity(0, 'ct')]).max()
192+
193+
# Did this go all the way through the loop to the max val?
194+
max_val_reached = True
195+
196+
while tot_cnts < max_val:
197+
# This grabs the pixels around the edge of the current bin mask, we also don't want values other
198+
# than 0 or 1 in here, hence why keep_corners is False
199+
cand_mask = edge_finder(cur_mask, border=True, keep_corners=False)
200+
201+
# This allowed data takes into account the source mask (as prod_dat already has that applied),
202+
# the regions that have already been claimed by previous contour bins, (in no_go), and finally
203+
# the places around the edge of the current contour bin that the algorithm is allowed
204+
# to go next (in cand_mask)
205+
allowed_data = prod_dat*cand_mask*no_go
206+
207+
# Obviously if all of this array is zero, then we can't continue with this contour bin, as
208+
# there is nowhere else to go
209+
if np.all(allowed_data == 0):
210+
# We haven't reached the maximum value and the loop has to be prematurely terminated
211+
max_val_reached = False
212+
break
213+
214+
# Now we know there is still data to add to the bin, we can find the point with the
215+
# maximum value at least in the parts of the image/ratemap we're allowed to look in. The
216+
# background isn't taken into account here because my current implementation calculates a
217+
# single background countrate, its only relevant when finding the absolute counts in a pixel
218+
cur_bin_sp = np.unravel_index(np.argmax(allowed_data), prod_dat.shape)
219+
# That position in the current contour bin mask is then set to 1, to indicate its a part
220+
# of this contour bin now
221+
cur_mask[cur_bin_sp[0], cur_bin_sp[1]] = 1
222+
223+
cur_cnt = prod.get_count(Quantity([cur_bin_sp[1], cur_bin_sp[0]], 'pix'))
224+
cur_bck_cnt = bck_cnt_map[cur_bin_sp[0], cur_bin_sp[1]]
225+
# We make sure that it can't go below zero
226+
cnts = Quantity([cur_cnt-cur_bck_cnt, Quantity(0, 'ct')]).max()
227+
228+
# Then we update the total number of counts
229+
tot_cnts += cnts
230+
231+
# We have to do a check here that the mask doesn't just have one point (can happen in
232+
# un-smoothed ratemaps), and stop it from being the brightest pixel for another mask
233+
if cur_mask.sum() != 1:
234+
# And variable indicating whether we reached the maximum value is also stored
235+
reached_max.append(max_val_reached)
236+
237+
# Here I try and do something a little clever, but only really applicable to when this is applied
238+
# to un-smoothed images. If there are single pixels of the current region mask that are zero
239+
# valued and completely surrounded by ones, then this will fill them in
240+
# First find the limits in x and y where there is mask data (to save checking the whole array)
241+
x_lims, y_lims = data_limits(cur_mask)
242+
243+
# Then make array representations of the y and x coordinates of the mask array
244+
row_inds, col_inds = np.indices(cur_mask.shape)
245+
# Find points in the mask that are within the approximate x and y lims, and have a value of zero.
246+
yp, xp = np.where((row_inds < y_lims[1]) & (row_inds > y_lims[0]) & (col_inds < x_lims[1]) &
247+
(col_inds > x_lims[0]) & (cur_mask == 0))
248+
249+
# Convert the results of np.where back into the coordinate system of the mask, we now have a list
250+
# of coordinates to check further (split into separate x (cp) and y (rp) arrays
251+
rp, cp = row_inds[yp, xp], col_inds[yp, xp]
252+
253+
# Then we go looking for which of those zero valued points is surrounded by values of one, in
254+
# the mask. Should be relatively easy to see whats happening here
255+
dots = np.where((cur_mask[rp+1, cp] == 1) & (cur_mask[rp-1, cp] == 1) & (cur_mask[rp, cp+1] == 1)
256+
& (cur_mask[rp, cp-1] == 1))
257+
258+
# Then those points in the mask we've identified are set to one
259+
cur_mask[rp[dots], cp[dots]] = 1
260+
261+
# We've broken out of the inner while loop, for one reason or another, and now can consider
262+
# that last contour bin to be finished, so we store it in the all_masks list
263+
all_masks.append(cur_mask)
264+
265+
# I only update the progress bar if we accept the current region
266+
open_ended.update(1)
267+
268+
else:
269+
pass
270+
271+
# Here we invert the mask so it can be added to the no_go array. Elements that are 1 (indicating
272+
# that they are a part of the current contour bin) are switched to zero so that future contour bins
273+
# are not allowed to include them. Vice Versa for elements of 0.
274+
flipped = cur_mask.copy()
275+
flipped[flipped == 1] = -1
276+
flipped[flipped == 0] = 1
277+
flipped[flipped == -1] = 0
278+
279+
# The no_go mask is updated
280+
no_go *= flipped
281+
282+
# Now this particular bin is finished, we find the pixel at which we shall start the next one
283+
new_peak = prod.simple_peak(no_go*src_mask, 'pix')[0].value
284+
cur_bin_sp = new_peak[[1, 0]]
285+
286+
elif metric == 'snr':
287+
raise NotImplementedError("The signal to noise approach has not been implemented yet")
288+
289+
# This makes the list of arrays into a 3D array to be returned
290+
all_masks = np.dstack(all_masks)
291+
292+
return all_masks
293+

0 commit comments

Comments
 (0)