Coverage for src/ufig/mask_utils.py: 94%
59 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-07 15:17 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-07 15:17 +0000
1# Copyright (C) 2019 ETH Zurich, Institute for Particle and Astrophysics
3"""
4Created on Feb 5, 2019
5author: Joerg Herbel
6"""
8import numba as nb
9import numpy as np
12def decimal_integer_to_binary(n_bits, arr_decimal, dtype_out):
13 """
14 Transform a one-dimensional array of decimal integers into its binary
15 representation. Returns an array with the shape (len(arr_decimal), n_bits). To
16 reconstruct arr_decimal[i], one would perform the operation
18 arr_decimal[i] = np.sum(2**np.arange(n_bits) * arr_binary[i])
20 This means that this binary representation is reversed with respect to what is
21 normally used. For example, normally,
23 1100 = 1 * 2**3 + 1 * 2**2 + 0 * 2**1 + 0 * 2**0.
25 However, here we have
27 1100 = 1 * 2**0 + 1 * 2**1 + 0 * 2**2 + 0 * 2**3.
29 ATTENTION: THIS FUNCTION MODIFIES ARR_DECIMAL IN PLACE!
30 """
31 arr_binary = np.zeros((len(arr_decimal), n_bits), dtype=dtype_out)
32 arr_decimal = arr_decimal.astype(dtype_out)
33 for i in range(n_bits):
34 arr_binary[:, i] = arr_decimal % np.uint(2)
35 arr_decimal //= np.uint(2)
37 return arr_binary
40def get_binary_mask_dtype(n_bits):
41 for dtype in [np.uint8, np.uint16, np.uint32, np.uint64]: 41 ↛ 45line 41 didn't jump to line 45 because the loop on line 41 didn't complete
42 if np.iinfo(dtype).max >= n_bits: 42 ↛ 41line 42 didn't jump to line 41 because the condition on line 42 was always true
43 return dtype
45 raise ValueError(
46 f"Number of bits {n_bits} exceeding maximum possible value such that np.uint"
47 " dtype can be used"
48 )
51def set_masked_pixels(pixel_mask, maskbits_keep, n_bits):
52 """
53 Set pixels which are masked according to input bits. This function modifies
54 pixel_mask in-place.
56 :param pixel_mask: mask
57 :param maskbits_keep: mask bits to keep
58 :param n_bits: ...
59 :return: mask with only bits to keep switched on (same shape as input mask)
60 """
62 pixel_mask = pixel_mask.copy()
64 # special case of no masking
65 if len(maskbits_keep) == 0: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true
66 return pixel_mask * 0
68 # transform to binary representation
69 pixel_mask_bin = decimal_integer_to_binary(
70 n_bits, np.ravel(pixel_mask), dtype_out=get_binary_mask_dtype(n_bits)
71 )
73 # select bits to keep
74 pixel_mask_bin = pixel_mask_bin[:, maskbits_keep]
76 # transform back to original shape
77 pixel_mask_keep = np.sum(pixel_mask_bin, axis=1).reshape(pixel_mask.shape)
79 return pixel_mask_keep
82@nb.jit(nopython=True)
83def select_off_mask(xs, ys, rs, mask):
84 # Array holding results
85 select_off = np.ones(len(xs), dtype=np.int8)
87 # Compute coordinates to be checked
88 x_min = np.floor(xs - rs).astype(np.int32)
89 y_min = np.floor(ys - rs).astype(np.int32)
90 x_max = np.ceil(xs + rs).astype(np.int32)
91 y_max = np.ceil(ys + rs).astype(np.int32)
93 x_min = np.maximum(x_min, 0)
94 y_min = np.maximum(y_min, 0)
95 x_max = np.minimum(x_max, mask.shape[1])
96 y_max = np.minimum(y_max, mask.shape[0])
98 rs_sq = rs**2
100 for i in range(len(xs)):
101 x = xs[i]
102 y = ys[i]
103 r_sq = rs_sq[i]
105 for xi in range(x_min[i], x_max[i]):
106 for yi in range(y_min[i], y_max[i]):
107 delta = (xi - x) ** 2 + (yi - y) ** 2
108 if delta < r_sq and mask[int(yi), int(xi)] > 0:
109 select_off[i] = 0
110 break
111 if select_off[i] == 0:
112 break
114 return select_off
117def pixel_mask_to_catalog_mask(pixel_mask, cat, off_mask_radius):
118 x = cat["XWIN_IMAGE"] - 0.5
119 y = cat["YWIN_IMAGE"] - 0.5
120 r = off_mask_radius * cat["FLUX_RADIUS"]
121 r[r < 0] = 5
123 return select_off_mask(x, y, r, pixel_mask).astype(bool)
126def pixel_mask_to_ucat_catalog_mask(pixel_mask, cat, off_mask_radius, r50_offset=5):
127 x = cat["x"]
128 y = cat["y"]
129 r = off_mask_radius * cat["r50"] + r50_offset
130 r[r < 0] = 5
132 return select_off_mask(x, y, r, pixel_mask).astype(bool)