Skip to content

Commit 744e2ed

Browse files
committed
improve mask 2d util docs
1 parent 0290b84 commit 744e2ed

1 file changed

Lines changed: 145 additions & 66 deletions

File tree

autoarray/mask/mask_2d_util.py

Lines changed: 145 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,19 @@ def mask_2d_centres_from(
3434
3535
Examples
3636
--------
37-
centres_scaled = mask_2d_centres_from(shape_native=(5, 5), pixel_scales=(0.5, 0.5), centre=(0.0, 0.0))
37+
>>> centres_scaled = mask_2d_centres_from(shape_native=(5, 5), pixel_scales=(0.5, 0.5), centre=(0.0, 0.0))
38+
>>> print(centres_scaled)
39+
(0.0, 0.0)
3840
"""
39-
return (
40-
0.5 * (shape_native[0] - 1) - (centre[0] / pixel_scales[0]),
41-
0.5 * (shape_native[1] - 1) + (centre[1] / pixel_scales[1]),
42-
)
41+
42+
# Calculate scaled y-coordinate by centering and adjusting for pixel scale
43+
y_scaled = 0.5 * (shape_native[0] - 1) - (centre[0] / pixel_scales[0])
44+
45+
# Calculate scaled x-coordinate by centering and adjusting for pixel scale
46+
x_scaled = 0.5 * (shape_native[1] - 1) + (centre[1] / pixel_scales[1])
47+
48+
# Return the scaled (y, x) coordinates
49+
return (y_scaled, x_scaled)
4350

4451

4552
def mask_2d_circular_from(
@@ -70,16 +77,23 @@ def mask_2d_circular_from(
7077
7178
Examples
7279
--------
73-
mask = mask_2d_circular_from(shape_native=(10, 10), pixel_scales=(0.1, 0.1), radius=0.5, centre=(0.0, 0.0))
80+
>>> mask = mask_2d_circular_from(shape_native=(10, 10), pixel_scales=(0.1, 0.1), radius=0.5, centre=(0.0, 0.0))
7481
"""
82+
83+
# Get scaled coordinates of the mask center
7584
centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre)
7685

86+
# Create a grid of y, x indices for the mask
7787
y, x = np.ogrid[: shape_native[0], : shape_native[1]]
88+
89+
# Scale the y and x indices based on pixel scales
7890
y_scaled = (y - centres_scaled[0]) * pixel_scales[0]
7991
x_scaled = (x - centres_scaled[1]) * pixel_scales[1]
8092

93+
# Compute squared distances from the center for each pixel
8194
distances_squared = x_scaled**2 + y_scaled**2
8295

96+
# Return a mask with True for pixels outside the circle and False for inside
8397
return distances_squared >= radius**2
8498

8599

@@ -114,18 +128,25 @@ def mask_2d_circular_annular_from(
114128
115129
Examples
116130
--------
117-
mask = mask_2d_circular_annular_from(
118-
shape_native=(10, 10), pixel_scales=(0.1, 0.1), inner_radius=0.5, outer_radius=1.5, centre=(0.0, 0.0)
119-
)
131+
>>> mask = mask_2d_circular_annular_from(
132+
>>> shape_native=(10, 10), pixel_scales=(0.1, 0.1), inner_radius=0.5, outer_radius=1.5, centre=(0.0, 0.0)
133+
>>> )
120134
"""
135+
136+
# Get scaled coordinates of the mask center
121137
centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre)
122138

139+
# Create grid of y, x indices for the mask
123140
y, x = np.ogrid[: shape_native[0], : shape_native[1]]
141+
142+
# Scale the y and x indices based on pixel scales
124143
y_scaled = (y - centres_scaled[0]) * pixel_scales[0]
125144
x_scaled = (x - centres_scaled[1]) * pixel_scales[1]
126145

146+
# Compute squared distances from the center for each pixel
127147
distances_squared = x_scaled**2 + y_scaled**2
128148

149+
# Return the mask where pixels are unmasked between inner and outer radii
129150
return ~(
130151
(distances_squared >= inner_radius**2) & (distances_squared <= outer_radius**2)
131152
)
@@ -166,30 +187,31 @@ def mask_2d_elliptical_from(
166187
167188
Examples
168189
--------
169-
mask = mask_2d_elliptical_from(
170-
shape_native=(10, 10), pixel_scales=(0.1, 0.1), major_axis_radius=0.5, axis_ratio=0.5, angle=45.0, centre=(0.0, 0.0)
171-
)
190+
>>> mask = mask_2d_elliptical_from(
191+
>>> shape_native=(10, 10), pixel_scales=(0.1, 0.1), major_axis_radius=0.5, axis_ratio=0.5, angle=45.0, centre=(0.0, 0.0)
192+
>>> )
172193
"""
194+
195+
# Get scaled coordinates of the mask center
173196
centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre)
174197

198+
# Create grid of y, x indices for the mask
175199
y, x = np.ogrid[: shape_native[0], : shape_native[1]]
200+
201+
# Scale the y and x indices based on pixel scales
176202
y_scaled = (y - centres_scaled[0]) * pixel_scales[0]
177203
x_scaled = (x - centres_scaled[1]) * pixel_scales[1]
178204

179-
# Rotate the coordinates by the angle (counterclockwise)
180-
205+
# Compute the rotated coordinates and elliptical radius
181206
r_scaled = np.sqrt(x_scaled**2 + y_scaled**2)
182-
183207
theta_rotated = np.arctan2(y_scaled, x_scaled) + np.radians(angle)
184-
185208
y_scaled_elliptical = r_scaled * np.sin(theta_rotated)
186209
x_scaled_elliptical = r_scaled * np.cos(theta_rotated)
187-
188-
# Compute the elliptical radius
189210
r_scaled_elliptical = np.sqrt(
190211
x_scaled_elliptical**2 + (y_scaled_elliptical / axis_ratio) ** 2
191212
)
192213

214+
# Return the mask where pixels are outside the elliptical region
193215
return ~(r_scaled_elliptical <= major_axis_radius)
194216

195217

@@ -231,7 +253,7 @@ def mask_2d_elliptical_annular_from(
231253
The rotation angle of the outer ellipse within which pixels are unmasked, (counter-clockwise from the
232254
positive x-axis).
233255
centre
234-
The centre of the elliptical annuli used to mask pixels.
256+
The centre of the elliptical annuli used to mask pixels.
235257
236258
Returns
237259
-------
@@ -240,95 +262,119 @@ def mask_2d_elliptical_annular_from(
240262
241263
Examples
242264
--------
243-
mask = mask_elliptical_annuli_from(
244-
shape=(10, 10), pixel_scales=(0.1, 0.1),
245-
inner_major_axis_radius=0.5, inner_axis_ratio=0.5, inner_phi=45.0,
246-
outer_major_axis_radius=1.5, outer_axis_ratio=0.8, outer_phi=90.0,
247-
centre=(0.0, 0.0))
265+
>>> mask = mask_elliptical_annuli_from(
266+
>>> shape=(10, 10), pixel_scales=(0.1, 0.1),
267+
>>> inner_major_axis_radius=0.5, inner_axis_ratio=0.5, inner_phi=45.0,
268+
>>> outer_major_axis_radius=1.5, outer_axis_ratio=0.8, outer_phi=90.0,
269+
>>> centre=(0.0, 0.0)
270+
>>> )
248271
"""
272+
273+
# Get scaled coordinates of the mask center
249274
centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre)
250275

276+
# Create grid of y, x indices for the mask
251277
y, x = np.ogrid[: shape_native[0], : shape_native[1]]
278+
279+
# Scale the y and x indices based on pixel scales
252280
y_scaled = (y - centres_scaled[0]) * pixel_scales[0]
253281
x_scaled = (x - centres_scaled[1]) * pixel_scales[1]
254282

255-
# Rotate the coordinates for the inner annulus
283+
# Compute and rotate coordinates for inner annulus
256284
r_scaled_inner = np.sqrt(x_scaled**2 + y_scaled**2)
257285
theta_rotated_inner = np.arctan2(y_scaled, x_scaled) + np.radians(inner_phi)
258286
y_scaled_elliptical_inner = r_scaled_inner * np.sin(theta_rotated_inner)
259287
x_scaled_elliptical_inner = r_scaled_inner * np.cos(theta_rotated_inner)
260-
261-
# Compute the elliptical radius for the inner annulus
262288
r_scaled_elliptical_inner = np.sqrt(
263289
x_scaled_elliptical_inner**2
264290
+ (y_scaled_elliptical_inner / inner_axis_ratio) ** 2
265291
)
266292

267-
# Rotate the coordinates for the outer annulus
293+
# Compute and rotate coordinates for outer annulus
268294
r_scaled_outer = np.sqrt(x_scaled**2 + y_scaled**2)
269295
theta_rotated_outer = np.arctan2(y_scaled, x_scaled) + np.radians(outer_phi)
270296
y_scaled_elliptical_outer = r_scaled_outer * np.sin(theta_rotated_outer)
271297
x_scaled_elliptical_outer = r_scaled_outer * np.cos(theta_rotated_outer)
272-
273-
# Compute the elliptical radius for the outer annulus
274298
r_scaled_elliptical_outer = np.sqrt(
275299
x_scaled_elliptical_outer**2
276300
+ (y_scaled_elliptical_outer / outer_axis_ratio) ** 2
277301
)
278302

303+
# Return the mask where pixels are outside the inner and outer elliptical annuli
279304
return ~(
280305
(r_scaled_elliptical_inner >= inner_major_axis_radius)
281306
& (r_scaled_elliptical_outer <= outer_major_axis_radius)
282307
)
283308

284309

285310
def mask_2d_via_pixel_coordinates_from(
286-
shape_native: Tuple[int, int], pixel_coordinates: [list], buffer: int = 0
311+
shape_native: Tuple[int, int], pixel_coordinates: list, buffer: int = 0
287312
) -> np.ndarray:
288313
"""
289-
Returns a mask where all unmasked `False` entries are defined from an input list of list of pixel coordinates.
314+
Returns a mask where all unmasked `False` entries are defined from an input list of 2D pixel coordinates.
290315
291-
These may be buffed via an input ``buffer``, whereby all entries in all 8 neighboring directions by this
316+
These may be buffed via an input `buffer`, whereby all entries in all 8 neighboring directions are buffed by this
292317
amount.
293318
294319
Parameters
295320
----------
296-
shape_native (int, int)
297-
The (y,x) shape of the mask in units of pixels.
298-
pixel_coordinates : [[int, int]]
299-
The input lists of 2D pixel coordinates where `False` entries are created.
321+
shape_native
322+
The (y, x) shape of the mask in units of pixels.
323+
pixel_coordinates
324+
The input list of 2D pixel coordinates where `False` entries are created.
300325
buffer
301-
All input ``pixel_coordinates`` are buffed with `False` entries in all 8 neighboring directions by this
326+
All input `pixel_coordinates` are buffed with `False` entries in all 8 neighboring directions by this
302327
amount.
303-
"""
304328
305-
mask_2d = np.full(shape=shape_native, fill_value=True)
329+
Returns
330+
-------
331+
np.ndarray
332+
The 2D mask array where all entries in the input pixel coordinates are set to `False`, with optional buffering
333+
applied to the neighboring entries.
306334
307-
for y, x in pixel_coordinates:
335+
Examples
336+
--------
337+
mask = mask_2d_via_pixel_coordinates_from(
338+
shape_native=(10, 10),
339+
pixel_coordinates=[[1, 2], [3, 4], [5, 6]],
340+
buffer=1
341+
)
342+
"""
343+
mask_2d = np.full(
344+
shape=shape_native, fill_value=True
345+
) # Initialize mask with all True values
346+
347+
for (
348+
y,
349+
x,
350+
) in (
351+
pixel_coordinates
352+
): # Loop over input coordinates to set corresponding mask entries to False
308353
mask_2d[y, x] = False
309354

310-
if buffer == 0:
355+
if buffer == 0: # If no buffer is specified, return the mask directly
311356
return mask_2d
312-
return buffed_mask_2d_from(mask_2d=mask_2d, buffer=buffer)
357+
return buffed_mask_2d_from(mask_2d=mask_2d, buffer=buffer) # Apply buf
313358

314359

315360
import numpy as np
316361

317362

318363
def min_false_distance_to_edge(mask: np.ndarray) -> Tuple[int, int]:
319364
"""
320-
Compute the minimum 1D distance in the y and x directions from any False value at the mask's extreme positions
365+
Compute the minimum 1D distance in the y and x directions from any `False` value at the mask's extreme positions
321366
(leftmost, rightmost, topmost, bottommost) to its closest edge.
322367
323368
Parameters
324369
----------
325370
mask
326-
A 2D boolean array where False represents the unmasked region.
371+
A 2D boolean array where `False` represents the unmasked region.
327372
328373
Returns
329374
-------
330-
The smallest distances of any extreme False value to the nearest edge in the vertical (y) and horizontal (x)
331-
directions.
375+
Tuple[int, int]
376+
The smallest distances of any extreme `False` value to the nearest edge in the vertical (y) and horizontal (x)
377+
directions.
332378
333379
Examples
334380
--------
@@ -341,27 +387,40 @@ def min_false_distance_to_edge(mask: np.ndarray) -> Tuple[int, int]:
341387
>>> min_false_distance_to_edge(mask)
342388
(1, 1)
343389
"""
344-
false_indices = np.column_stack(np.where(mask == False))
390+
false_indices = np.column_stack(
391+
np.where(mask == False)
392+
) # Find all coordinates where mask is False
345393

346394
if false_indices.size == 0:
347-
raise ValueError("No False values found in the mask.")
348-
349-
leftmost = false_indices[np.argmin(false_indices[:, 1])]
350-
rightmost = false_indices[np.argmax(false_indices[:, 1])]
351-
topmost = false_indices[np.argmin(false_indices[:, 0])]
352-
bottommost = false_indices[np.argmax(false_indices[:, 0])]
353-
354-
height, width = mask.shape
395+
raise ValueError(
396+
"No False values found in the mask."
397+
) # Raise error if no False values
398+
399+
leftmost = false_indices[
400+
np.argmin(false_indices[:, 1])
401+
] # Find the leftmost False coordinate
402+
rightmost = false_indices[
403+
np.argmax(false_indices[:, 1])
404+
] # Find the rightmost False coordinate
405+
topmost = false_indices[
406+
np.argmin(false_indices[:, 0])
407+
] # Find the topmost False coordinate
408+
bottommost = false_indices[
409+
np.argmax(false_indices[:, 0])
410+
] # Find the bottommost False coordinate
411+
412+
height, width = mask.shape # Get the height and width of the mask
355413

356414
# Compute distances to respective edges
357415
left_dist = leftmost[1] # Distance to left edge (column index)
358416
right_dist = width - 1 - rightmost[1] # Distance to right edge
359417
top_dist = topmost[0] # Distance to top edge (row index)
360418
bottom_dist = height - 1 - bottommost[0] # Distance to bottom edge
361419

362-
# Return the minimum distance to an edge
420+
# Return the minimum distance to both edges
363421
return min(top_dist, bottom_dist), min(left_dist, right_dist)
364422

423+
365424
def blurring_mask_2d_from(
366425
mask_2d: np.ndarray, kernel_shape_native: Tuple[int, int]
367426
) -> np.ndarray:
@@ -397,27 +456,47 @@ def blurring_mask_2d_from(
397456
398457
"""
399458

400-
y_distance, x_distance = min_false_distance_to_edge(mask_2d)
459+
# Get the distance from False values to edges
460+
y_distance, x_distance = min_false_distance_to_edge(
461+
mask_2d
462+
)
401463

402-
y_kernel_distance = (kernel_shape_native[0]) // 2
403-
x_kernel_distance = (kernel_shape_native[1]) // 2
464+
# Compute kernel half-size in y and x direction
465+
y_kernel_distance = (
466+
kernel_shape_native[0]
467+
) // 2
468+
x_kernel_distance = (
469+
kernel_shape_native[1]
470+
) // 2
404471

472+
# Check if mask is too small for the kernel size
405473
if (y_distance < y_kernel_distance) or (x_distance < x_kernel_distance):
406-
407474
raise exc.MaskException(
408475
"The input mask is too small for the kernel shape. "
409476
"Please pad the mask before computing the blurring mask."
410477
)
411478

412-
kernel = np.ones(kernel_shape_native, dtype=np.uint8)
479+
# Create a kernel with the given PSF shape
480+
kernel = np.ones(
481+
kernel_shape_native, dtype=np.uint8
482+
)
413483

414-
convolved_mask = convolve(mask_2d.astype(np.uint8), kernel, mode="reflect", cval=0)
484+
# Convolve mask with kernel producing non-zero values around mask False values
485+
convolved_mask = convolve(
486+
mask_2d.astype(np.uint8), kernel, mode="reflect", cval=0
487+
)
415488

416-
result_mask = convolved_mask == np.prod(kernel_shape_native)
489+
# Identify pixels that are non-zero and fully covered by kernel
490+
result_mask = convolved_mask == np.prod(
491+
kernel_shape_native
492+
)
417493

418-
blurring_mask = ~mask_2d + result_mask
494+
# Create the blurring mask by removing False values in original mask
495+
blurring_mask = (
496+
~mask_2d + result_mask
497+
)
419498

420-
return blurring_mask
499+
return blurring_mask
421500

422501

423502
@numba_util.jit()

0 commit comments

Comments
 (0)