|
| 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