Coverage for openhcs/processing/backends/processors/pyclesperanto_processor.py: 14.0%
226 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""
2pyclesperanto GPU processor for OpenHCS.
4This processor uses pyclesperanto for GPU-accelerated image processing,
5providing excellent compatibility with OpenCL devices and seamless integration
6with OpenHCS patterns.
7"""
9import logging
10from typing import List, Optional, Union
12# Import OpenHCS decorator
13from openhcs.core.memory.decorators import pyclesperanto as pyclesperanto_func
15# Set up logging
16logger = logging.getLogger(__name__)
18# Try to import pyclesperanto
19try:
20 import pyclesperanto as cle
21 PYCLESPERANTO_AVAILABLE = True
22 logger.info("pyclesperanto available - GPU acceleration enabled")
23except ImportError:
24 PYCLESPERANTO_AVAILABLE = False
25 logger.warning("pyclesperanto not available - install with: pip install pyclesperanto")
27def _check_pyclesperanto_available():
28 """Check if pyclesperanto is available and raise error if not."""
29 if not PYCLESPERANTO_AVAILABLE:
30 raise ImportError("pyclesperanto is required but not available. Install with: pip install pyclesperanto")
32def _validate_3d_array(array) -> None:
33 """Validate that the input is a 3D array."""
34 if array.ndim != 3:
35 raise ValueError(f"Expected 3D array, got {array.ndim}D array")
37def _gpu_minmax_normalize_range(image: "cle.Array" ) -> tuple:
38 """Calculate normalization range using min/max instead of percentiles - pure GPU."""
39 import pyclesperanto as cle
41 # Use min/max instead of percentiles to stay purely on GPU
42 # This is similar to how CLAHE works internally
43 min_val = cle.minimum_of_all_pixels(image)
44 max_val = cle.maximum_of_all_pixels(image)
46 # For compatibility, we could apply a small margin based on percentile values
47 # but for pure GPU operation, we use full min/max range
48 return float(min_val), float(max_val)
50@pyclesperanto_func
51def per_slice_minmax_normalize(
52 image: "cle.Array",
53 low_percentile: float = 1.0,
54 high_percentile: float = 99.0,
55 target_min: int = 0,
56 target_max: int = 65535
57) -> "cle.Array":
58 """
59 Normalize image intensities using per-slice min/max values - GPU accelerated.
61 PER-SLICE OPERATION: Uses min/max values independently for each Z-slice,
62 then normalizes each slice to its own min/max range. Each slice gets
63 its own normalization parameters. Pure GPU implementation using min/max
64 instead of percentiles (pyclesperanto limitation).
66 Args:
67 image: 3D pyclesperanto Array of shape (Z, Y, X)
68 low_percentile: Ignored - kept for API compatibility
69 high_percentile: Ignored - kept for API compatibility
70 target_min: Minimum value in output range
71 target_max: Maximum value in output range
73 Returns:
74 Normalized 3D pyclesperanto Array of shape (Z, Y, X) with dtype uint16
75 """
76 # Validate 3D array
77 if len(image.shape) != 3:
78 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array")
80 # Build result by concatenating pairs of slices
81 result_slices = []
83 for z in range(image.shape[0]):
84 # Work directly with slice views - no copying needed
85 gpu_slice = image[z] # Direct slice access
87 # Calculate min/max range for normalization (pure GPU)
88 p_low, p_high = _gpu_minmax_normalize_range(gpu_slice, low_percentile, high_percentile)
90 # Avoid division by zero
91 if p_high == p_low:
92 # Fill slice with target_min
93 gpu_result_slice = cle.create_like(gpu_slice)
94 cle.set(gpu_result_slice, target_min)
95 result_slices.append(gpu_result_slice)
96 continue
98 # All normalization operations stay on GPU using pure pyclesperanto
99 gpu_clipped = cle.clip(gpu_slice, min_intensity=p_low, max_intensity=p_high)
100 gpu_shifted = cle.subtract_image_from_scalar(gpu_clipped, scalar=p_low)
102 scale_factor = (target_max - target_min) / (p_high - p_low)
103 gpu_normalized = cle.add_image_and_scalar(
104 cle.multiply_image_and_scalar(gpu_shifted, scalar=scale_factor),
105 scalar=target_min
106 )
108 result_slices.append(gpu_normalized)
110 # Concatenate slices back into 3D array using pairwise concatenation
111 result = result_slices[0]
112 for i in range(1, len(result_slices)):
113 result = cle.concatenate_along_z(result, result_slices[i])
115 return result
117@pyclesperanto_func
118def stack_minmax_normalize(
119 image: "cle.Array",
120 target_min: int = 0,
121 target_max: int = 65535
122) -> "cle.Array":
123 """
124 Normalize image intensities using global stack min/max values - GPU accelerated.
126 STACK-WIDE OPERATION: Uses global min/max values from the entire 3D stack
127 for normalization. All slices use the same global normalization parameters.
128 Pure GPU implementation using min/max instead of percentiles (pyclesperanto limitation).
130 Args:
131 image: 3D pyclesperanto Array of shape (Z, Y, X)
132 low_percentile: Ignored - kept for API compatibility
133 high_percentile: Ignored - kept for API compatibility
134 target_min: Minimum value in output range
135 target_max: Maximum value in output range
137 Returns:
138 Normalized 3D pyclesperanto Array of shape (Z, Y, X) with dtype uint16
139 """
140 _check_pyclesperanto_available()
142 # Validate 3D array
143 if len(image.shape) != 3:
144 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array")
146 # Calculate global min/max range from entire stack (pure GPU)
147 p_low, p_high = _gpu_minmax_normalize_range(image, low_percentile, high_percentile)
149 # Avoid division by zero
150 if p_high == p_low:
151 result = cle.create_like(image)
152 cle.set(result, target_min)
153 return result
155 # All normalization operations stay on GPU using pure pyclesperanto
156 gpu_clipped = cle.clip(image, min_intensity=p_low, max_intensity=p_high)
157 gpu_shifted = cle.subtract_image_from_scalar(gpu_clipped, scalar=p_low)
159 scale_factor = (target_max - target_min) / (p_high - p_low)
160 gpu_normalized = cle.add_image_and_scalar(
161 cle.multiply_image_and_scalar(gpu_shifted, scalar=scale_factor),
162 scalar=target_min
163 )
165 return gpu_normalized
167@pyclesperanto_func
168def sharpen(
169 image: "cle.Array",
170 radius: float = 1.0,
171 amount: float = 1.0
172) -> "cle.Array":
173 """
174 Apply unsharp mask sharpening to a 3D image - GPU accelerated.
176 PER-SLICE OPERATION: Applies 2D Gaussian blur (sigma_z=0) and unsharp masking
177 to each Z-slice independently. Each slice is sharpened using only its own
178 2D neighborhood, not considering adjacent Z-slices.
180 Args:
181 image: 3D pyclesperanto Array of shape (Z, Y, X)
182 radius: Gaussian blur radius for unsharp mask (applied in X,Y only)
183 amount: Sharpening strength
185 Returns:
186 Sharpened 3D pyclesperanto Array of shape (Z, Y, X)
187 """
188 _check_pyclesperanto_available()
190 # Validate 3D array
191 if len(image.shape) != 3:
192 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array")
194 # Apply 3D Gaussian blur (pyclesperanto handles Z-dimension efficiently)
195 gpu_blurred = cle.gaussian_blur(image, sigma_x=radius, sigma_y=radius, sigma_z=0)
197 # Unsharp mask: original + amount * (original - blurred)
198 # Use add_images_weighted for efficiency: result = 1*original + amount*(original - blurred)
199 gpu_diff = cle.subtract_images(image, gpu_blurred)
200 gpu_sharpened = cle.add_images_weighted(image, gpu_diff, factor1=1.0, factor2=amount)
202 # Clip to valid range
203 gpu_clipped = cle.clip(gpu_sharpened, min_intensity=0, max_intensity=65535)
205 return gpu_clipped
207@pyclesperanto_func
208def max_projection(stack: "cle.Array") -> "cle.Array":
209 """
210 Create a maximum intensity projection from a Z-stack - GPU accelerated.
212 TRUE 3D OPERATION: Collapses the Z-dimension by taking the maximum value
213 across all Z-slices for each (Y,X) position. Uses pyclesperanto's
214 maximum_z_projection function.
216 Args:
217 stack: 3D pyclesperanto Array of shape (Z, Y, X)
219 Returns:
220 3D pyclesperanto Array of shape (1, Y, X)
221 """
222 _check_pyclesperanto_available()
224 # Validate 3D array
225 if len(stack.shape) != 3:
226 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
228 # Create max projection (stays on GPU)
229 gpu_projection_2d = cle.maximum_z_projection(stack)
231 # Reshape to (1, Y, X) by creating a new 3D array
232 result_shape = (1, gpu_projection_2d.shape[0], gpu_projection_2d.shape[1])
233 result = cle.create(result_shape, dtype=gpu_projection_2d.dtype)
234 result[0] = gpu_projection_2d # Direct assignment
236 return result
238@pyclesperanto_func
239def mean_projection(stack: "cle.Array") -> "cle.Array":
240 """
241 Create a mean intensity projection from a Z-stack - GPU accelerated.
243 TRUE 3D OPERATION: Collapses the Z-dimension by taking the mean value
244 across all Z-slices for each (Y,X) position. Uses pyclesperanto's
245 mean_z_projection function.
247 Args:
248 stack: 3D pyclesperanto Array of shape (Z, Y, X)
250 Returns:
251 3D pyclesperanto Array of shape (1, Y, X)
252 """
253 _check_pyclesperanto_available()
255 # Validate 3D array
256 if len(stack.shape) != 3:
257 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
259 # Create mean projection (stays on GPU)
260 gpu_projection_2d = cle.mean_z_projection(stack)
262 # Reshape to (1, Y, X) by creating a new 3D array
263 result_shape = (1, gpu_projection_2d.shape[0], gpu_projection_2d.shape[1])
264 result = cle.create(result_shape, dtype=gpu_projection_2d.dtype)
265 result[0] = gpu_projection_2d # Direct assignment
267 return result
269@pyclesperanto_func
270def create_projection(
271 stack: "cle.Array", method: str = "max_projection"
272) -> "cle.Array":
273 """
274 Create a projection from a stack using the specified method - GPU accelerated.
276 TRUE 3D OPERATION: Dispatcher function that calls the appropriate projection
277 method. All projection methods collapse the Z-dimension.
279 Args:
280 stack: 3D pyclesperanto Array of shape (Z, Y, X)
281 method: Projection method (max_projection, mean_projection)
283 Returns:
284 3D pyclesperanto Array of shape (1, Y, X)
285 """
286 _check_pyclesperanto_available()
288 # Validate 3D array
289 if len(stack.shape) != 3:
290 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
292 if method == "max_projection":
293 return max_projection(stack)
295 if method == "mean_projection":
296 return mean_projection(stack)
298 # FAIL FAST: No fallback projection methods
299 raise ValueError(f"Unknown projection method: {method}. Valid methods: max_projection, mean_projection")
301@pyclesperanto_func
302def tophat(
303 image: "cle.Array",
304 selem_radius: int = 50,
305 downsample_factor: int = 4,
306 downsample_interpolate: bool = True,
307 upsample_interpolate: bool = False
308) -> "cle.Array":
309 """
310 Apply white top-hat filter to a 3D image for background removal - GPU accelerated.
312 PER-SLICE OPERATION: Applies 2D top-hat morphological filtering to each Z-slice
313 independently using a sequential loop. Each slice is processed with its own
314 2D structuring element, not considering adjacent Z-slices.
316 Implementation: Downsamples entire stack, applies 2D top-hat per slice,
317 calculates background, upsamples background, then subtracts from original.
319 Args:
320 image: 3D pyclesperanto Array of shape (Z, Y, X)
321 selem_radius: Radius of the 2D structuring element (applied per slice)
322 downsample_factor: Factor by which to downsample for processing
323 downsample_interpolate: Whether to use interpolation when downsampling
324 upsample_interpolate: Whether to use interpolation when upsampling
326 Returns:
327 Filtered 3D pyclesperanto Array of shape (Z, Y, X)
328 """
329 _check_pyclesperanto_available()
331 # Validate 3D array
332 if len(image.shape) != 3:
333 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array")
335 # 1) Downsample entire stack at once (more efficient)
336 scale_factor = 1.0 / downsample_factor
337 gpu_small = cle.scale(
338 image,
339 factor_x=scale_factor,
340 factor_y=scale_factor,
341 factor_z=1.0, # Don't scale Z dimension
342 resize=True,
343 interpolate=downsample_interpolate
344 )
346 # 2) Apply top-hat filter using sphere structuring element to entire stack
347 # Process slice by slice using direct array access
348 result_small = cle.create_like(gpu_small)
349 for z in range(gpu_small.shape[0]):
350 gpu_slice = gpu_small[z] # Direct slice access
351 gpu_tophat_slice = cle.top_hat_sphere(
352 gpu_slice,
353 radius_x=selem_radius // downsample_factor,
354 radius_y=selem_radius // downsample_factor
355 )
356 result_small[z] = gpu_tophat_slice # Direct assignment
358 # 3) Calculate background on small image
359 gpu_background_small = cle.subtract_images(gpu_small, result_small)
361 # 4) Upscale background to original size
362 gpu_background_large = cle.scale(
363 gpu_background_small,
364 factor_x=downsample_factor,
365 factor_y=downsample_factor,
366 factor_z=1.0, # Don't scale Z dimension
367 resize=True,
368 interpolate=upsample_interpolate
369 )
371 # 5) Subtract background and clip negative values (entire stack at once)
372 gpu_subtracted = cle.subtract_images(image, gpu_background_large)
373 gpu_result = cle.maximum_image_and_scalar(gpu_subtracted, scalar=0)
375 return gpu_result
377@pyclesperanto_func
378def apply_mask(image: "cle.Array", mask: "cle.Array") -> "cle.Array":
379 """
380 Apply a mask to a 3D image - GPU accelerated.
382 HYBRID OPERATION:
383 - If 3D mask: TRUE 3D OPERATION (direct element-wise multiplication)
384 - If 2D mask: PER-SLICE OPERATION (applies same 2D mask to each Z-slice via sequential loop)
386 Args:
387 image: 3D pyclesperanto Array of shape (Z, Y, X)
388 mask: 3D pyclesperanto Array of shape (Z, Y, X) or 2D pyclesperanto Array of shape (Y, X)
390 Returns:
391 Masked 3D pyclesperanto Array of shape (Z, Y, X)
392 """
393 _check_pyclesperanto_available()
395 # Validate 3D image
396 if len(image.shape) != 3:
397 raise ValueError(f"Expected 3D image array, got {len(image.shape)}D array")
399 # Handle 2D mask (apply to each Z-slice)
400 if len(mask.shape) == 2:
401 if mask.shape != image.shape[1:]:
402 raise ValueError(
403 f"2D mask shape {mask.shape} doesn't match image slice shape {image.shape[1:]}"
404 )
406 # Create result array on GPU
407 result = cle.create_like(image)
409 for z in range(image.shape[0]):
410 # Work directly with slice views
411 gpu_slice = image[z] # Direct slice access
412 # Apply mask (both stay on GPU)
413 gpu_masked = cle.multiply_images(gpu_slice, mask)
414 # Assign result directly
415 result[z] = gpu_masked
417 return result
419 # Handle 3D mask
420 elif len(mask.shape) == 3:
421 if mask.shape != image.shape:
422 raise ValueError(
423 f"3D mask shape {mask.shape} doesn't match image shape {image.shape}"
424 )
426 # Apply mask directly (both stay on GPU)
427 return cle.multiply_images(image, mask)
429 # If we get here, the mask is neither 2D nor 3D
430 else:
431 raise TypeError(f"mask must be a 2D or 3D pyclesperanto Array, got shape {mask.shape}")
433@pyclesperanto_func
434def create_composite(
435 stack: "cle.Array", weights: Optional[List[float]] = None
436) -> "cle.Array":
437 """
438 Create a composite image from a 3D stack where each slice is a channel - GPU accelerated.
440 TRUE 3D OPERATION: Performs element-wise weighted addition across slices
441 to create a composite. All mathematical operations are applied using
442 efficient pyclesperanto functions.
444 Args:
445 stack: 3D pyclesperanto Array of shape (N, Y, X) where N is number of channel slices
446 weights: List of weights for each slice. If None, equal weights are used.
448 Returns:
449 Composite 3D pyclesperanto Array of shape (1, Y, X)
450 """
451 _check_pyclesperanto_available()
453 # Validate input is 3D array
454 if len(stack.shape) != 3:
455 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
457 n_slices, height, width = stack.shape
459 # Default weights if none provided
460 if weights is None:
461 weights = [1.0 / n_slices] * n_slices
462 elif isinstance(weights, (list, tuple)):
463 # Convert tuple to list if needed
464 weights = list(weights)
465 if len(weights) != n_slices:
466 raise ValueError(f"Number of weights ({len(weights)}) must match number of slices ({n_slices})")
467 else:
468 raise TypeError(f"weights must be a list of values or None, got {type(weights)}: {weights}")
470 # Normalize weights to sum to 1
471 weight_sum = sum(weights)
472 if weight_sum == 0:
473 raise ValueError("Sum of weights cannot be zero")
474 normalized_weights = [w / weight_sum for w in weights]
476 # Create result array with shape (1, Y, X)
477 result = cle.create((1, height, width), dtype=stack.dtype)
479 # Initialize with zeros
480 cle.set(result, 0.0)
482 # Add each weighted slice
483 for i, weight in enumerate(normalized_weights):
484 if weight > 0.0:
485 # Get slice i from the stack
486 slice_i = stack[i] # This gives us a 2D slice
488 # Multiply slice by its weight
489 weighted_slice = cle.multiply_image_and_scalar(slice_i, scalar=weight)
491 # Add to result (need to handle 2D slice + 3D result)
492 # Extract the single slice from result for addition
493 result_slice = result[0]
494 result_slice = cle.add_images(result_slice, weighted_slice)
496 # Put it back (this might need adjustment based on pyclesperanto API)
497 result[0] = result_slice
499 return result
501@pyclesperanto_func
502def equalize_histogram_3d(
503 stack: "cle.Array",
504 tile_size: int = 8,
505 clip_limit: float = 0.01,
506 range_min: float = 0.0,
507 range_max: float = 65535.0
508) -> "cle.Array":
509 """
510 Apply 3D CLAHE histogram equalization to a volume - GPU accelerated.
512 TRUE 3D OPERATION: Uses 3D CLAHE (Contrast Limited Adaptive Histogram Equalization)
513 with 3D tiles (cubes) that consider voxel neighborhoods in X, Y, and Z dimensions.
514 Appropriate for Z-stacks where adjacent slices are spatially continuous.
516 Args:
517 stack: 3D pyclesperanto Array of shape (Z, Y, X)
518 tile_size: Size of 3D tiles (cubes) for adaptive equalization
519 clip_limit: Clipping limit for histogram equalization (0.0-1.0)
520 range_min: Minimum value for output range
521 range_max: Maximum value for output range
523 Returns:
524 Equalized 3D pyclesperanto Array of shape (Z, Y, X)
525 """
526 _check_pyclesperanto_available()
528 # Validate 3D array
529 if len(stack.shape) != 3:
530 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
532 # Use 3D CLAHE with 3D tiles (cubes)
533 gpu_equalized = cle.clahe(stack, tile_size=tile_size, clip_limit=clip_limit)
535 # Clip to valid range using pure pyclesperanto
536 gpu_clipped = cle.clip(gpu_equalized, min_intensity=range_min, max_intensity=range_max)
538 return gpu_clipped
540@pyclesperanto_func
541def equalize_histogram_per_slice(
542 stack: "cle.Array",
543 tile_size: int = 8,
544 clip_limit: float = 0.01,
545 range_min: float = 0.0,
546 range_max: float = 65535.0
547) -> "cle.Array":
548 """
549 Apply 2D CLAHE histogram equalization to each slice independently - GPU accelerated.
551 PER-SLICE OPERATION: Applies 2D CLAHE to each Z-slice independently using 2D tiles.
552 Each slice gets its own adaptive histogram equalization. Appropriate for stacks
553 of different images (different X,Y content) or when slices should be treated independently.
555 Args:
556 stack: 3D pyclesperanto Array of shape (Z, Y, X)
557 tile_size: Size of 2D tiles (squares) for adaptive equalization per slice
558 clip_limit: Clipping limit for histogram equalization (0.0-1.0)
559 range_min: Minimum value for output range
560 range_max: Maximum value for output range
562 Returns:
563 Equalized 3D pyclesperanto Array of shape (Z, Y, X)
564 """
565 _check_pyclesperanto_available()
567 # Validate 3D array
568 if len(stack.shape) != 3:
569 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
571 # Create result array
572 result = cle.create_like(stack)
574 # Apply 2D CLAHE to each slice independently
575 for z in range(stack.shape[0]):
576 # Work directly with slice views
577 gpu_slice = stack[z] # Direct slice access
579 # Apply 2D CLAHE to this slice only
580 gpu_equalized_slice = cle.clahe(gpu_slice, tile_size=tile_size, clip_limit=clip_limit)
582 # Clip to valid range
583 gpu_clipped_slice = cle.clip(gpu_equalized_slice, min_intensity=range_min, max_intensity=range_max)
585 # Assign result directly
586 result[z] = gpu_clipped_slice
588 return result
590@pyclesperanto_func
591def stack_equalize_histogram(
592 stack: "cle.Array",
593 bins: int = 65536,
594 range_min: float = 0.0,
595 range_max: float = 65535.0
596) -> "cle.Array":
597 """
598 Apply histogram equalization to a stack - GPU accelerated.
600 COMPATIBILITY FUNCTION: Alias for equalize_histogram_3d to maintain API compatibility
601 with numpy processor. Uses 3D CLAHE for true 3D histogram equalization.
603 Args:
604 stack: 3D pyclesperanto Array of shape (Z, Y, X)
605 bins: Number of bins for histogram computation (unused - CLAHE parameter)
606 range_min: Minimum value for histogram range
607 range_max: Maximum value for histogram range
609 Returns:
610 Equalized 3D pyclesperanto Array of shape (Z, Y, X)
611 """
612 # Delegate to the 3D version with default parameters
613 return equalize_histogram_3d(stack, range_min=range_min, range_max=range_max)
615# API compatibility aliases - these maintain the original function names
616# but delegate to the more accurately named implementations
618@pyclesperanto_func
619def percentile_normalize(
620 image: "cle.Array",
621 low_percentile: float = 1.0,
622 high_percentile: float = 99.0,
623 target_min: int = 0,
624 target_max: int = 65535
625) -> "cle.Array":
626 """
627 COMPATIBILITY ALIAS: Delegates to per_slice_minmax_normalize.
629 Note: Uses min/max normalization instead of true percentiles due to
630 pyclesperanto limitations. Kept for API compatibility with other processors.
631 """
632 return per_slice_minmax_normalize(image, low_percentile, high_percentile, target_min, target_max)
634@pyclesperanto_func
635def stack_percentile_normalize(
636 image: "cle.Array",
637 low_percentile: float = 1.0,
638 high_percentile: float = 99.0,
639 target_min: int = 0,
640 target_max: int = 65535
641) -> "cle.Array":
642 """
643 COMPATIBILITY ALIAS: Delegates to stack_minmax_normalize.
645 Note: Uses min/max normalization instead of true percentiles due to
646 pyclesperanto limitations. Kept for API compatibility with other processors.
647 """
648 return stack_minmax_normalize(image, target_min, target_max)
650def create_linear_weight_mask(height: int, width: int, margin_ratio: float = 0.1) -> "cle.Array":
651 """
652 Create a linear weight mask for blending images - GPU accelerated.
654 Pure pyclesperanto implementation using GPU operations only.
655 """
656 # Create coordinate arrays for X and Y positions
657 y_coords = cle.create((height, width), dtype=float)
658 x_coords = cle.create((height, width), dtype=float)
660 # Fill coordinate arrays
661 cle.set_ramp_y(y_coords)
662 cle.set_ramp_x(x_coords)
664 # Calculate margin sizes
665 margin_h = int(height * margin_ratio)
666 margin_w = int(width * margin_ratio)
668 # Create weight mask starting with ones
669 mask = cle.create((height, width), dtype=float)
670 cle.set(mask, 1.0)
672 # Apply fade from top edge: weight = min(1.0, y / margin_h)
673 if margin_h > 0:
674 top_weight = cle.multiply_image_and_scalar(y_coords, scalar=1.0/margin_h)
675 top_weight = cle.minimum_image_and_scalar(top_weight, scalar=1.0)
676 mask = cle.multiply_images(mask, top_weight)
678 # Apply fade from bottom edge: weight = min(1.0, (height - 1 - y) / margin_h)
679 if margin_h > 0:
680 bottom_coords = cle.subtract_image_from_scalar(y_coords, scalar=height - 1)
681 bottom_coords = cle.absolute(bottom_coords)
682 bottom_weight = cle.multiply_image_and_scalar(bottom_coords, scalar=1.0/margin_h)
683 bottom_weight = cle.minimum_image_and_scalar(bottom_weight, scalar=1.0)
684 mask = cle.multiply_images(mask, bottom_weight)
686 # Apply fade from left edge: weight = min(1.0, x / margin_w)
687 if margin_w > 0:
688 left_weight = cle.multiply_image_and_scalar(x_coords, scalar=1.0/margin_w)
689 left_weight = cle.minimum_image_and_scalar(left_weight, scalar=1.0)
690 mask = cle.multiply_images(mask, left_weight)
692 # Apply fade from right edge: weight = min(1.0, (width - 1 - x) / margin_w)
693 if margin_w > 0:
694 right_coords = cle.subtract_image_from_scalar(x_coords, scalar=width - 1)
695 right_coords = cle.absolute(right_coords)
696 right_weight = cle.multiply_image_and_scalar(right_coords, scalar=1.0/margin_w)
697 right_weight = cle.minimum_image_and_scalar(right_weight, scalar=1.0)
698 mask = cle.multiply_images(mask, right_weight)
700 return mask
702def create_weight_mask(shape: tuple, margin_ratio: float = 0.1) -> "cle.Array":
703 """
704 Create a weight mask for blending images - GPU accelerated.
706 Args:
707 shape: Shape of the mask (height, width)
708 margin_ratio: Ratio of image size to use as margin
710 Returns:
711 2D pyclesperanto Array of shape (Y, X)
712 """
713 if not isinstance(shape, tuple) or len(shape) != 2:
714 raise TypeError("shape must be a tuple of (height, width)")
716 height, width = shape
717 return create_linear_weight_mask(height, width, margin_ratio)