@@ -461,8 +461,48 @@ def min_false_distance_to_edge(mask: np.ndarray) -> Tuple[int, int]:
461461 return min (top_dist , bottom_dist ), min (left_dist , right_dist )
462462
463463
464+ def blurring_mask_required_shape_from (
465+ mask_2d : np .ndarray ,
466+ kernel_shape_native : Tuple [int , int ],
467+ ) -> Tuple [int , int ]:
468+ """
469+ Return the minimal shape the mask must be padded to so that a kernel with the given
470+ footprint can be applied without sampling beyond the array edge.
471+
472+ Parameters
473+ ----------
474+ mask_2d
475+ 2D boolean array where False is unmasked and True is masked.
476+ kernel_shape_native
477+ (ky, kx) footprint of the convolution kernel.
478+
479+ Returns
480+ -------
481+ required_shape
482+ The minimal (ny, nx) shape such that the minimum distance from any unmasked
483+ pixel to the array edge is at least (ky//2, kx//2).
484+ """
485+ mask_2d = np .asarray (mask_2d , dtype = bool )
486+
487+ ky , kx = kernel_shape_native
488+ if ky <= 0 or kx <= 0 :
489+ raise ValueError (
490+ f"kernel_shape_native must be positive, got { kernel_shape_native } ."
491+ )
492+
493+ pad_y , pad_x = ky // 2 , kx // 2
494+ y_distance , x_distance = min_false_distance_to_edge (mask_2d )
495+
496+ extra_y = max (0 , pad_y - y_distance )
497+ extra_x = max (0 , pad_x - x_distance )
498+
499+ return (mask_2d .shape [0 ] + 2 * extra_y , mask_2d .shape [1 ] + 2 * extra_x )
500+
501+
464502def blurring_mask_2d_from (
465- mask_2d : np .ndarray , kernel_shape_native : Tuple [int , int ]
503+ mask_2d : np .ndarray ,
504+ kernel_shape_native : Tuple [int , int ],
505+ allow_padding : bool = False ,
466506) -> np .ndarray :
467507 """
468508 Return the blurring mask for a 2D mask and kernel footprint.
@@ -472,51 +512,79 @@ def blurring_mask_2d_from(
472512 - True = masked (excluded)
473513
474514 The returned *blurring mask* is a mask where the blurring-region pixels are unmasked (False).
515+
516+ Parameters
517+ ----------
518+ mask_2d
519+ 2D boolean mask.
520+ kernel_shape_native
521+ (ky, kx) kernel footprint.
522+ allow_padding
523+ If False (default), raises an exception when the mask is too small
524+ for the kernel footprint.
525+ If True, pads the mask symmetrically with masked pixels (True) to the
526+ minimal required shape and emits a warning.
475527 """
476528 mask_2d = np .asarray (mask_2d , dtype = bool )
477529
478- ky , kx = kernel_shape_native
479- if ky <= 0 or kx <= 0 :
480- raise ValueError (
481- f"kernel_shape_native must be positive, got { kernel_shape_native } ."
530+ required_shape = blurring_mask_required_shape_from (mask_2d , kernel_shape_native )
531+
532+ if required_shape != mask_2d .shape :
533+
534+ if not allow_padding :
535+ raise exc .MaskException (
536+ "The input mask is too small for the kernel shape. "
537+ f"Current shape: { mask_2d .shape } , required shape: { required_shape } . "
538+ "Set allow_padding=True to pad automatically."
539+ )
540+
541+ warnings .warn (
542+ f"Mask padded from { mask_2d .shape } to { required_shape } "
543+ f"to support kernel footprint { kernel_shape_native } ." ,
544+ UserWarning ,
482545 )
483546
484- # Keep your existing guard (optional)
485- y_distance , x_distance = min_false_distance_to_edge (mask_2d )
486- if (y_distance < ky // 2 ) or (x_distance < kx // 2 ):
487- raise exc .MaskException (
488- "The input mask is too small for the kernel shape. "
489- "Please pad the mask before computing the blurring mask."
547+ dy = required_shape [0 ] - mask_2d .shape [0 ]
548+ dx = required_shape [1 ] - mask_2d .shape [1 ]
549+
550+ pad_top = dy // 2
551+ pad_bottom = dy - pad_top
552+ pad_left = dx // 2
553+ pad_right = dx - pad_left
554+
555+ mask_2d = np .pad (
556+ mask_2d ,
557+ pad_width = ((pad_top , pad_bottom ), (pad_left , pad_right )),
558+ mode = "constant" ,
559+ constant_values = True , # outside is masked
490560 )
491561
492- # Kernel footprint (support only)
562+ ky , kx = kernel_shape_native
563+ pad_y , pad_x = ky // 2 , kx // 2
493564 structure = np .ones ((ky , kx ), dtype = bool )
494565
495566 # Unmasked region (True where unmasked)
496567 unmasked = ~ mask_2d
497568
498- # Explicit padding: outside-of-array is masked => outside is NOT unmasked (False)
499- pad_y , pad_x = ky // 2 , kx // 2
569+ # Pad so outside behaves as masked
500570 unmasked_padded = np .pad (
501571 unmasked ,
502572 pad_width = ((pad_y , pad_y ), (pad_x , pad_x )),
503573 mode = "constant" ,
504574 constant_values = False ,
505575 )
506576
507- # Pixels within kernel footprint of any unmasked pixel
508577 near_unmasked_padded = binary_dilation (unmasked_padded , structure = structure )
578+
509579 near_unmasked = near_unmasked_padded [
510580 pad_y : pad_y + mask_2d .shape [0 ],
511581 pad_x : pad_x + mask_2d .shape [1 ],
512582 ]
513583
514- # Blurring REGION: masked pixels that are near unmasked pixels
515- blurring_region = mask_2d & near_unmasked # True on the ring (in region-space)
584+ blurring_region = mask_2d & near_unmasked
516585
517- # Convert region -> mask semantics: ring should be unmasked (False)
518- blurring_mask = np .ones_like (mask_2d , dtype = bool ) # start fully masked
519- blurring_mask [blurring_region ] = False # unmask the ring
586+ blurring_mask = np .ones_like (mask_2d , dtype = bool )
587+ blurring_mask [blurring_region ] = False
520588
521589 return blurring_mask
522590
0 commit comments