|
2 | 2 |
|
3 | 3 | import itertools |
4 | 4 | import os |
| 5 | +import warnings |
5 | 6 | from collections.abc import Iterable |
6 | 7 | from typing import Union, Sequence, Optional, Callable |
7 | 8 |
|
8 | 9 | import numpy as np |
9 | 10 | from .boolfunc import BoolFunc |
10 | 11 | from .oop_cython import RLECy |
11 | 12 |
|
| 13 | +_UNSET = object() |
| 14 | + |
12 | 15 |
|
13 | 16 | class RLEMask: |
14 | 17 | """Run-length encoded mask. |
@@ -129,6 +132,9 @@ def from_array( |
129 | 132 | thresh128: deprecated, equivalent to threshold=128. |
130 | 133 | """ |
131 | 134 | if thresh128: |
| 135 | + warnings.warn( |
| 136 | + "The 'thresh128' parameter is deprecated, use 'threshold=128' instead.", |
| 137 | + DeprecationWarning, stacklevel=2) |
132 | 138 | threshold = 128 |
133 | 139 | result = RLEMask._init() |
134 | 140 | result.cy._i_from_array(mask_array, threshold, is_sparse) |
@@ -3063,52 +3069,145 @@ def to_dict(self, zlevel: Optional[int] = None) -> dict: |
3063 | 3069 | """ |
3064 | 3070 | return self.cy.to_dict(zlevel) |
3065 | 3071 |
|
3066 | | - def to_array(self, value: int = 1, order="F") -> np.ndarray: |
3067 | | - """Convert the RLE mask to a dense 2D uint8 numpy array. |
| 3072 | + def to_array( |
| 3073 | + self, fg_value=1, bg_value=0, dtype=np.uint8, order="F", *, value=_UNSET |
| 3074 | + ) -> np.ndarray: |
| 3075 | + """Convert the RLE mask to a dense numpy array. |
| 3076 | +
|
| 3077 | + Background pixels get ``bg_value`` and foreground pixels get ``fg_value``. |
| 3078 | +
|
| 3079 | + If either ``fg_value`` or ``bg_value`` is a tuple, list, or 1D array, the result is a |
| 3080 | + 3D HWC array with one channel per element. A scalar value for the other parameter is |
| 3081 | + broadcast to all channels. |
3068 | 3082 |
|
3069 | | - False (background) values become 0 and True (foreground) values become the specified value. |
3070 | 3083 | The RLE is internally stored for the Fortran order, so order='F' is faster, because |
3071 | 3084 | 'C' requires a transpose. To improve efficiency, the transpose is done either in RLE or |
3072 | 3085 | in dense form, depending on the sparseness of the mask. |
3073 | 3086 |
|
3074 | 3087 | Args: |
3075 | | - value: the "True" value to use in the resulting array |
| 3088 | + fg_value: the foreground value (scalar for 2D, tuple/list/array for HWC) |
| 3089 | + bg_value: the background value (scalar for 2D, tuple/list/array for HWC) |
| 3090 | + dtype: the numpy dtype of the resulting array (default: np.uint8) |
3076 | 3091 | order: the order of the array ('C' for row-major, 'F' for column-major) |
| 3092 | + value: deprecated alias for ``fg_value`` |
3077 | 3093 |
|
3078 | 3094 | Returns: |
3079 | | - An F or C-contiguous 2D numpy array of type uint8 representing the mask. |
| 3095 | + A 2D or 3D numpy array representing the mask. |
3080 | 3096 |
|
3081 | 3097 | See Also: |
3082 | 3098 | :meth:`__array__`, :meth:`from_array` |
3083 | 3099 | """ |
3084 | | - return self.cy._r_to_dense_array(value, order) |
| 3100 | + if value is not _UNSET: |
| 3101 | + warnings.warn( |
| 3102 | + "The 'value' parameter is deprecated, use 'fg_value' instead.", |
| 3103 | + DeprecationWarning, stacklevel=2) |
| 3104 | + fg_value = value |
| 3105 | + |
| 3106 | + fg_multi = isinstance(fg_value, (tuple, list)) or (isinstance(fg_value, np.ndarray) and fg_value.ndim == 1) |
| 3107 | + bg_multi = isinstance(bg_value, (tuple, list)) or (isinstance(bg_value, np.ndarray) and bg_value.ndim == 1) |
| 3108 | + |
| 3109 | + if not fg_multi and not bg_multi: |
| 3110 | + return self.cy._r_to_dense_array(fg_value, bg_value, order, np.dtype(dtype)) |
| 3111 | + |
| 3112 | + # Multi-valued: produce HWC array |
| 3113 | + if fg_multi: |
| 3114 | + fg_value = np.asarray(fg_value, dtype=dtype) |
| 3115 | + n_channels = len(fg_value) |
| 3116 | + if bg_multi: |
| 3117 | + bg_value = np.asarray(bg_value, dtype=dtype) |
| 3118 | + n_channels = len(bg_value) |
| 3119 | + if n_channels == 0: |
| 3120 | + raise ValueError("fg_value/bg_value must have at least one channel") |
| 3121 | + if fg_multi and bg_multi and len(fg_value) != len(bg_value): |
| 3122 | + raise ValueError( |
| 3123 | + f"fg_value length ({len(fg_value)}) != bg_value length ({len(bg_value)})") |
3085 | 3124 |
|
3086 | | - def decode_into(self, arr: np.ndarray, value: int = 1) -> None: # noqa: vulture |
3087 | | - """Decode the RLE mask into an existing array, only setting foreground pixels. |
| 3125 | + # Broadcast scalar to all channels |
| 3126 | + dtype = np.dtype(dtype) |
| 3127 | + if not fg_multi: |
| 3128 | + fg_value = np.full(n_channels, fg_value, dtype=dtype) |
| 3129 | + if not bg_multi: |
| 3130 | + bg_value = np.full(n_channels, bg_value, dtype=dtype) |
3088 | 3131 |
|
3089 | | - This method sets foreground pixels to the specified value while leaving |
3090 | | - background pixels unchanged. This is useful for overlaying multiple masks |
3091 | | - onto a single array, e.g., creating a label map or visualization. |
| 3132 | + h, w = self.shape |
| 3133 | + arr = np.empty((h, w, n_channels), dtype=dtype, order='C') |
| 3134 | + arr[:] = bg_value # broadcasts along last axis |
| 3135 | + if dtype == np.uint8: |
| 3136 | + self.cy._decode_into(arr, fg_value) |
| 3137 | + elif dtype == np.float32 or dtype == np.float64: |
| 3138 | + self.cy._decode_typed_multi_into(arr, np.asarray(fg_value, dtype=dtype)) |
| 3139 | + else: |
| 3140 | + mask01 = self.cy._r_to_dense_array(1, 0, 'C') |
| 3141 | + arr[mask01 != 0] = fg_value |
| 3142 | + |
| 3143 | + if order == 'F' and not arr.flags.f_contiguous: |
| 3144 | + arr = np.asfortranarray(arr) |
| 3145 | + return arr |
| 3146 | + |
| 3147 | + def decode_into( # noqa: vulture |
| 3148 | + self, arr: np.ndarray, fg_value=None, bg_value=None, |
| 3149 | + *, value=_UNSET) -> None: |
| 3150 | + """Decode the RLE mask into an existing array. |
| 3151 | +
|
| 3152 | + Writes ``fg_value`` to foreground pixels and/or ``bg_value`` to background pixels. |
| 3153 | + Pixels whose corresponding parameter is ``None`` are left unchanged. |
| 3154 | +
|
| 3155 | + Supports uint8, float32, and float64 arrays (2D or 3D HWC). |
3092 | 3156 |
|
3093 | 3157 | Args: |
3094 | | - arr: A 2D uint8 numpy array with shape matching the mask. |
3095 | | - Must be either C-contiguous or Fortran-contiguous. |
3096 | | - value: The value to assign to foreground pixels (default: 1). |
| 3158 | + arr: A numpy array with shape matching the mask. |
| 3159 | + Must be either C-contiguous or Fortran-contiguous (or strided for 2D uint8). |
| 3160 | + fg_value: The value to assign to foreground pixels (default: None, meaning unchanged). |
| 3161 | + bg_value: The value to assign to background pixels (default: None, meaning unchanged). |
| 3162 | + value: deprecated alias for ``fg_value`` |
3097 | 3163 |
|
3098 | 3164 | Raises: |
3099 | 3165 | ValueError: If array shape doesn't match mask shape or array is not contiguous. |
3100 | 3166 |
|
3101 | 3167 | Example: |
3102 | 3168 | >>> canvas = np.zeros((100, 100), dtype=np.uint8) |
3103 | | - >>> mask1.decode_into(canvas, value=1) |
3104 | | - >>> mask2.decode_into(canvas, value=2) |
3105 | | - >>> mask3.decode_into(canvas, value=3) |
| 3169 | + >>> mask1.decode_into(canvas, fg_value=1) |
| 3170 | + >>> mask2.decode_into(canvas, fg_value=2) |
| 3171 | + >>> mask3.decode_into(canvas, fg_value=3) |
3106 | 3172 | # canvas now contains 1, 2, 3 for respective mask regions |
3107 | 3173 |
|
3108 | 3174 | See Also: |
3109 | 3175 | :meth:`to_array` |
3110 | 3176 | """ |
3111 | | - self.cy._decode_into(arr, value) |
| 3177 | + if value is not _UNSET: |
| 3178 | + warnings.warn( |
| 3179 | + "The 'value' parameter is deprecated, use 'fg_value' instead.", |
| 3180 | + DeprecationWarning, stacklevel=2) |
| 3181 | + fg_value = value |
| 3182 | + |
| 3183 | + if bg_value is not None and fg_value is not None: |
| 3184 | + arr[:] = bg_value |
| 3185 | + self._decode_into_typed(arr, fg_value) |
| 3186 | + elif fg_value is not None: |
| 3187 | + self._decode_into_typed(arr, fg_value) |
| 3188 | + elif bg_value is not None: |
| 3189 | + self.complement()._decode_into_typed(arr, bg_value) |
| 3190 | + # else: both None, no-op |
| 3191 | + |
| 3192 | + def _decode_into_typed(self, arr, fg_value): |
| 3193 | + """Dispatch decode_into to the right Cython method based on array dtype.""" |
| 3194 | + if arr.dtype == np.uint8: |
| 3195 | + self.cy._decode_into(arr, fg_value) |
| 3196 | + elif arr.dtype == np.float32 or arr.dtype == np.float64: |
| 3197 | + if arr.ndim == 2: |
| 3198 | + if isinstance(fg_value, (tuple, list, np.ndarray)): |
| 3199 | + raise ValueError( |
| 3200 | + "fg_value must be scalar for 2D arrays; use a 3D HWC array for multi-channel values") |
| 3201 | + self.cy._decode_typed_into(arr, fg_value) |
| 3202 | + else: |
| 3203 | + fg_arr = np.asarray(fg_value, dtype=arr.dtype) |
| 3204 | + if fg_arr.ndim == 0: |
| 3205 | + fg_arr = np.full(arr.shape[2], fg_value, dtype=arr.dtype) |
| 3206 | + self.cy._decode_typed_multi_into(arr, fg_arr) |
| 3207 | + else: |
| 3208 | + order = 'F' if arr.flags.f_contiguous else 'C' |
| 3209 | + mask01 = self.cy._r_to_dense_array(1, 0, order) |
| 3210 | + arr[mask01 != 0] = fg_value |
3112 | 3211 |
|
3113 | 3212 | def __reduce__(self): |
3114 | 3213 | """Support for pickle serialization.""" |
@@ -3213,7 +3312,7 @@ def _forward_slice(slice_obj, length): |
3213 | 3312 | # cropped = x.crop(box_old, inplace=False) |
3214 | 3313 | # interp = cv2.INTER_LINEAR if fx > 1 and fy > 1 else cv2.INTER_AREA |
3215 | 3314 | # resized = cv2.resize( |
3216 | | -# cropped.to_array(order='C', value=255), |
| 3315 | +# cropped.to_array(order='C', fg_value=255), |
3217 | 3316 | # (box_new[2], box_new[3]), |
3218 | 3317 | # fx=fx, fy=fy, interpolation=interp) |
3219 | 3318 | # resized = RLEMask.from_array(resized, thresh128=True, is_sparse=x.density < 0.04) |
|
0 commit comments