Coverage for openhcs/processing/backends/processors/pyclesperanto_processor.py: 14.9%
232 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +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
10import os
11from typing import List, Optional, Union
13# Import OpenHCS decorator
14from openhcs.core.memory.decorators import pyclesperanto as pyclesperanto_func
16# Set up logging
17logger = logging.getLogger(__name__)
19# Check if we're in subprocess runner mode and should skip GPU imports
20if os.getenv('OPENHCS_SUBPROCESS_NO_GPU') == '1': 20 ↛ 22line 20 didn't jump to line 22 because the condition on line 20 was never true
21 # Subprocess runner mode - skip GPU imports
22 PYCLESPERANTO_AVAILABLE = False
23 cle = None
24 logger.info("Subprocess runner mode - skipping pyclesperanto import")
25else:
26 # Normal mode - try to import pyclesperanto
27 try:
28 import pyclesperanto as cle
29 PYCLESPERANTO_AVAILABLE = True
30 logger.info("pyclesperanto available - GPU acceleration enabled")
31 except ImportError:
32 PYCLESPERANTO_AVAILABLE = False
33 cle = None
34 logger.warning("pyclesperanto not available - install with: pip install pyclesperanto")
36def _check_pyclesperanto_available():
37 """Check if pyclesperanto is available and raise error if not."""
38 if not PYCLESPERANTO_AVAILABLE:
39 raise ImportError("pyclesperanto is required but not available. Install with: pip install pyclesperanto")
41def _validate_3d_array(array) -> None:
42 """Validate that the input is a 3D array."""
43 if array.ndim != 3:
44 raise ValueError(f"Expected 3D array, got {array.ndim}D array")
46def _gpu_minmax_normalize_range(image: "cle.Array" ) -> tuple:
47 """Calculate normalization range using min/max instead of percentiles - pure GPU."""
48 import pyclesperanto as cle
50 # Use min/max instead of percentiles to stay purely on GPU
51 # This is similar to how CLAHE works internally
52 min_val = cle.minimum_of_all_pixels(image)
53 max_val = cle.maximum_of_all_pixels(image)
55 # For compatibility, we could apply a small margin based on percentile values
56 # but for pure GPU operation, we use full min/max range
57 return float(min_val), float(max_val)
59@pyclesperanto_func
60def per_slice_minmax_normalize(
61 image: "cle.Array",
62 low_percentile: float = 1.0,
63 high_percentile: float = 99.0,
64 target_min: int = 0,
65 target_max: int = 65535
66) -> "cle.Array":
67 """
68 Normalize image intensities using per-slice min/max values - GPU accelerated.
70 PER-SLICE OPERATION: Uses min/max values independently for each Z-slice,
71 then normalizes each slice to its own min/max range. Each slice gets
72 its own normalization parameters. Pure GPU implementation using min/max
73 instead of percentiles (pyclesperanto limitation).
75 Args:
76 image: 3D pyclesperanto Array of shape (Z, Y, X)
77 low_percentile: Ignored - kept for API compatibility
78 high_percentile: Ignored - kept for API compatibility
79 target_min: Minimum value in output range
80 target_max: Maximum value in output range
82 Returns:
83 Normalized 3D pyclesperanto Array of shape (Z, Y, X) with dtype uint16
84 """
85 # Validate 3D array
86 if len(image.shape) != 3:
87 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array")
89 # Build result by concatenating pairs of slices
90 result_slices = []
92 for z in range(image.shape[0]):
93 # Work directly with slice views - no copying needed
94 gpu_slice = image[z] # Direct slice access
96 # Calculate min/max range for normalization (pure GPU)
97 p_low, p_high = _gpu_minmax_normalize_range(gpu_slice, low_percentile, high_percentile)
99 # Avoid division by zero
100 if p_high == p_low:
101 # Fill slice with target_min
102 gpu_result_slice = cle.create_like(gpu_slice)
103 cle.set(gpu_result_slice, target_min)
104 result_slices.append(gpu_result_slice)
105 continue
107 # All normalization operations stay on GPU using pure pyclesperanto
108 gpu_clipped = cle.clip(gpu_slice, min_intensity=p_low, max_intensity=p_high)
109 gpu_shifted = cle.subtract_image_from_scalar(gpu_clipped, scalar=p_low)
111 scale_factor = (target_max - target_min) / (p_high - p_low)
112 gpu_normalized = cle.add_image_and_scalar(
113 cle.multiply_image_and_scalar(gpu_shifted, scalar=scale_factor),
114 scalar=target_min
115 )
117 result_slices.append(gpu_normalized)
119 # Concatenate slices back into 3D array using pairwise concatenation
120 result = result_slices[0]
121 for i in range(1, len(result_slices)):
122 result = cle.concatenate_along_z(result, result_slices[i])
124 return result
126@pyclesperanto_func
127def stack_minmax_normalize(
128 image: "cle.Array",
129 target_min: int = 0,
130 target_max: int = 65535
131) -> "cle.Array":
132 """
133 Normalize image intensities using global stack min/max values - GPU accelerated.
135 STACK-WIDE OPERATION: Uses global min/max values from the entire 3D stack
136 for normalization. All slices use the same global normalization parameters.
137 Pure GPU implementation using min/max instead of percentiles (pyclesperanto limitation).
139 Args:
140 image: 3D pyclesperanto Array of shape (Z, Y, X)
141 low_percentile: Ignored - kept for API compatibility
142 high_percentile: Ignored - kept for API compatibility
143 target_min: Minimum value in output range
144 target_max: Maximum value in output range
146 Returns:
147 Normalized 3D pyclesperanto Array of shape (Z, Y, X) with dtype uint16
148 """
149 _check_pyclesperanto_available()
151 # Validate 3D array
152 if len(image.shape) != 3:
153 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array")
155 # Calculate global min/max range from entire stack (pure GPU)
156 p_low, p_high = _gpu_minmax_normalize_range(image, low_percentile, high_percentile)
158 # Avoid division by zero
159 if p_high == p_low:
160 result = cle.create_like(image)
161 cle.set(result, target_min)
162 return result
164 # All normalization operations stay on GPU using pure pyclesperanto
165 gpu_clipped = cle.clip(image, min_intensity=p_low, max_intensity=p_high)
166 gpu_shifted = cle.subtract_image_from_scalar(gpu_clipped, scalar=p_low)
168 scale_factor = (target_max - target_min) / (p_high - p_low)
169 gpu_normalized = cle.add_image_and_scalar(
170 cle.multiply_image_and_scalar(gpu_shifted, scalar=scale_factor),
171 scalar=target_min
172 )
174 return gpu_normalized
176@pyclesperanto_func
177def sharpen(
178 image: "cle.Array",
179 radius: float = 1.0,
180 amount: float = 1.0
181) -> "cle.Array":
182 """
183 Apply unsharp mask sharpening to a 3D image - GPU accelerated.
185 PER-SLICE OPERATION: Applies 2D Gaussian blur (sigma_z=0) and unsharp masking
186 to each Z-slice independently. Each slice is sharpened using only its own
187 2D neighborhood, not considering adjacent Z-slices.
189 Args:
190 image: 3D pyclesperanto Array of shape (Z, Y, X)
191 radius: Gaussian blur radius for unsharp mask (applied in X,Y only)
192 amount: Sharpening strength
194 Returns:
195 Sharpened 3D pyclesperanto Array of shape (Z, Y, X)
196 """
197 _check_pyclesperanto_available()
199 # Validate 3D array
200 if len(image.shape) != 3:
201 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array")
203 # Apply 3D Gaussian blur (pyclesperanto handles Z-dimension efficiently)
204 gpu_blurred = cle.gaussian_blur(image, sigma_x=radius, sigma_y=radius, sigma_z=0)
206 # Unsharp mask: original + amount * (original - blurred)
207 # Use add_images_weighted for efficiency: result = 1*original + amount*(original - blurred)
208 gpu_diff = cle.subtract_images(image, gpu_blurred)
209 gpu_sharpened = cle.add_images_weighted(image, gpu_diff, factor1=1.0, factor2=amount)
211 # Clip to valid range
212 gpu_clipped = cle.clip(gpu_sharpened, min_intensity=0, max_intensity=65535)
214 return gpu_clipped
216@pyclesperanto_func
217def max_projection(stack: "cle.Array") -> "cle.Array":
218 """
219 Create a maximum intensity projection from a Z-stack - GPU accelerated.
221 TRUE 3D OPERATION: Collapses the Z-dimension by taking the maximum value
222 across all Z-slices for each (Y,X) position. Uses pyclesperanto's
223 maximum_z_projection function.
225 Args:
226 stack: 3D pyclesperanto Array of shape (Z, Y, X)
228 Returns:
229 3D pyclesperanto Array of shape (1, Y, X)
230 """
231 _check_pyclesperanto_available()
233 # Validate 3D array
234 if len(stack.shape) != 3:
235 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
237 # Create max projection (stays on GPU)
238 gpu_projection_2d = cle.maximum_z_projection(stack)
240 # Reshape to (1, Y, X) by creating a new 3D array
241 result_shape = (1, gpu_projection_2d.shape[0], gpu_projection_2d.shape[1])
242 result = cle.create(result_shape, dtype=gpu_projection_2d.dtype)
243 result[0] = gpu_projection_2d # Direct assignment
245 return result
247@pyclesperanto_func
248def mean_projection(stack: "cle.Array") -> "cle.Array":
249 """
250 Create a mean intensity projection from a Z-stack - GPU accelerated.
252 TRUE 3D OPERATION: Collapses the Z-dimension by taking the mean value
253 across all Z-slices for each (Y,X) position. Uses pyclesperanto's
254 mean_z_projection function.
256 Args:
257 stack: 3D pyclesperanto Array of shape (Z, Y, X)
259 Returns:
260 3D pyclesperanto Array of shape (1, Y, X)
261 """
262 _check_pyclesperanto_available()
264 # Validate 3D array
265 if len(stack.shape) != 3:
266 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
268 # Create mean projection (stays on GPU)
269 gpu_projection_2d = cle.mean_z_projection(stack)
271 # Reshape to (1, Y, X) by creating a new 3D array
272 result_shape = (1, gpu_projection_2d.shape[0], gpu_projection_2d.shape[1])
273 result = cle.create(result_shape, dtype=gpu_projection_2d.dtype)
274 result[0] = gpu_projection_2d # Direct assignment
276 return result
278@pyclesperanto_func
279def create_projection(
280 stack: "cle.Array", method: str = "max_projection"
281) -> "cle.Array":
282 """
283 Create a projection from a stack using the specified method - GPU accelerated.
285 TRUE 3D OPERATION: Dispatcher function that calls the appropriate projection
286 method. All projection methods collapse the Z-dimension.
288 Args:
289 stack: 3D pyclesperanto Array of shape (Z, Y, X)
290 method: Projection method (max_projection, mean_projection)
292 Returns:
293 3D pyclesperanto Array of shape (1, Y, X)
294 """
295 _check_pyclesperanto_available()
297 # Validate 3D array
298 if len(stack.shape) != 3:
299 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
301 if method == "max_projection":
302 return max_projection(stack)
304 if method == "mean_projection":
305 return mean_projection(stack)
307 # FAIL FAST: No fallback projection methods
308 raise ValueError(f"Unknown projection method: {method}. Valid methods: max_projection, mean_projection")
310@pyclesperanto_func
311def tophat(
312 image: "cle.Array",
313 selem_radius: int = 50,
314 downsample_factor: int = 4,
315 downsample_interpolate: bool = True,
316 upsample_interpolate: bool = False
317) -> "cle.Array":
318 """
319 Apply white top-hat filter to a 3D image for background removal - GPU accelerated.
321 PER-SLICE OPERATION: Applies 2D top-hat morphological filtering to each Z-slice
322 independently using a sequential loop. Each slice is processed with its own
323 2D structuring element, not considering adjacent Z-slices.
325 Implementation: Downsamples entire stack, applies 2D top-hat per slice,
326 calculates background, upsamples background, then subtracts from original.
328 Args:
329 image: 3D pyclesperanto Array of shape (Z, Y, X)
330 selem_radius: Radius of the 2D structuring element (applied per slice)
331 downsample_factor: Factor by which to downsample for processing
332 downsample_interpolate: Whether to use interpolation when downsampling
333 upsample_interpolate: Whether to use interpolation when upsampling
335 Returns:
336 Filtered 3D pyclesperanto Array of shape (Z, Y, X)
337 """
338 _check_pyclesperanto_available()
340 # Validate 3D array
341 if len(image.shape) != 3:
342 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array")
344 # 1) Downsample entire stack at once (more efficient)
345 scale_factor = 1.0 / downsample_factor
346 gpu_small = cle.scale(
347 image,
348 factor_x=scale_factor,
349 factor_y=scale_factor,
350 factor_z=1.0, # Don't scale Z dimension
351 resize=True,
352 interpolate=downsample_interpolate
353 )
355 # 2) Apply top-hat filter using sphere structuring element to entire stack
356 # Process slice by slice using direct array access
357 result_small = cle.create_like(gpu_small)
358 for z in range(gpu_small.shape[0]):
359 gpu_slice = gpu_small[z] # Direct slice access
360 gpu_tophat_slice = cle.top_hat_sphere(
361 gpu_slice,
362 radius_x=selem_radius // downsample_factor,
363 radius_y=selem_radius // downsample_factor
364 )
365 result_small[z] = gpu_tophat_slice # Direct assignment
367 # 3) Calculate background on small image
368 gpu_background_small = cle.subtract_images(gpu_small, result_small)
370 # 4) Upscale background to original size
371 gpu_background_large = cle.scale(
372 gpu_background_small,
373 factor_x=downsample_factor,
374 factor_y=downsample_factor,
375 factor_z=1.0, # Don't scale Z dimension
376 resize=True,
377 interpolate=upsample_interpolate
378 )
380 # 5) Subtract background and clip negative values (entire stack at once)
381 gpu_subtracted = cle.subtract_images(image, gpu_background_large)
382 gpu_result = cle.maximum_image_and_scalar(gpu_subtracted, scalar=0)
384 return gpu_result
386@pyclesperanto_func
387def apply_mask(image: "cle.Array", mask: "cle.Array") -> "cle.Array":
388 """
389 Apply a mask to a 3D image - GPU accelerated.
391 HYBRID OPERATION:
392 - If 3D mask: TRUE 3D OPERATION (direct element-wise multiplication)
393 - If 2D mask: PER-SLICE OPERATION (applies same 2D mask to each Z-slice via sequential loop)
395 Args:
396 image: 3D pyclesperanto Array of shape (Z, Y, X)
397 mask: 3D pyclesperanto Array of shape (Z, Y, X) or 2D pyclesperanto Array of shape (Y, X)
399 Returns:
400 Masked 3D pyclesperanto Array of shape (Z, Y, X)
401 """
402 _check_pyclesperanto_available()
404 # Validate 3D image
405 if len(image.shape) != 3:
406 raise ValueError(f"Expected 3D image array, got {len(image.shape)}D array")
408 # Handle 2D mask (apply to each Z-slice)
409 if len(mask.shape) == 2:
410 if mask.shape != image.shape[1:]:
411 raise ValueError(
412 f"2D mask shape {mask.shape} doesn't match image slice shape {image.shape[1:]}"
413 )
415 # Create result array on GPU
416 result = cle.create_like(image)
418 for z in range(image.shape[0]):
419 # Work directly with slice views
420 gpu_slice = image[z] # Direct slice access
421 # Apply mask (both stay on GPU)
422 gpu_masked = cle.multiply_images(gpu_slice, mask)
423 # Assign result directly
424 result[z] = gpu_masked
426 return result
428 # Handle 3D mask
429 elif len(mask.shape) == 3:
430 if mask.shape != image.shape:
431 raise ValueError(
432 f"3D mask shape {mask.shape} doesn't match image shape {image.shape}"
433 )
435 # Apply mask directly (both stay on GPU)
436 return cle.multiply_images(image, mask)
438 # If we get here, the mask is neither 2D nor 3D
439 else:
440 raise TypeError(f"mask must be a 2D or 3D pyclesperanto Array, got shape {mask.shape}")
442@pyclesperanto_func
443def create_composite(
444 stack: "cle.Array", weights: Optional[List[float]] = None
445) -> "cle.Array":
446 """
447 Create a composite image from a 3D stack where each slice is a channel - GPU accelerated.
449 TRUE 3D OPERATION: Performs element-wise weighted addition across slices
450 to create a composite. All mathematical operations are applied using
451 efficient pyclesperanto functions.
453 Args:
454 stack: 3D pyclesperanto Array of shape (N, Y, X) where N is number of channel slices
455 weights: List of weights for each slice. If None, equal weights are used.
457 Returns:
458 Composite 3D pyclesperanto Array of shape (1, Y, X)
459 """
460 _check_pyclesperanto_available()
462 # Validate input is 3D array
463 if len(stack.shape) != 3:
464 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
466 n_slices, height, width = stack.shape
468 # Default weights if none provided
469 if weights is None:
470 weights = [1.0 / n_slices] * n_slices
471 elif isinstance(weights, (list, tuple)):
472 # Convert tuple to list if needed
473 weights = list(weights)
474 if len(weights) != n_slices:
475 raise ValueError(f"Number of weights ({len(weights)}) must match number of slices ({n_slices})")
476 else:
477 raise TypeError(f"weights must be a list of values or None, got {type(weights)}: {weights}")
479 # Normalize weights to sum to 1
480 weight_sum = sum(weights)
481 if weight_sum == 0:
482 raise ValueError("Sum of weights cannot be zero")
483 normalized_weights = [w / weight_sum for w in weights]
485 # Create result array with shape (1, Y, X)
486 result = cle.create((1, height, width), dtype=stack.dtype)
488 # Initialize with zeros
489 cle.set(result, 0.0)
491 # Add each weighted slice
492 for i, weight in enumerate(normalized_weights):
493 if weight > 0.0:
494 # Get slice i from the stack
495 slice_i = stack[i] # This gives us a 2D slice
497 # Multiply slice by its weight
498 weighted_slice = cle.multiply_image_and_scalar(slice_i, scalar=weight)
500 # Add to result (need to handle 2D slice + 3D result)
501 # Extract the single slice from result for addition
502 result_slice = result[0]
503 result_slice = cle.add_images(result_slice, weighted_slice)
505 # Put it back (this might need adjustment based on pyclesperanto API)
506 result[0] = result_slice
508 return result
510@pyclesperanto_func
511def equalize_histogram_3d(
512 stack: "cle.Array",
513 tile_size: int = 8,
514 clip_limit: float = 0.01,
515 range_min: float = 0.0,
516 range_max: float = 65535.0
517) -> "cle.Array":
518 """
519 Apply 3D CLAHE histogram equalization to a volume - GPU accelerated.
521 TRUE 3D OPERATION: Uses 3D CLAHE (Contrast Limited Adaptive Histogram Equalization)
522 with 3D tiles (cubes) that consider voxel neighborhoods in X, Y, and Z dimensions.
523 Appropriate for Z-stacks where adjacent slices are spatially continuous.
525 Args:
526 stack: 3D pyclesperanto Array of shape (Z, Y, X)
527 tile_size: Size of 3D tiles (cubes) for adaptive equalization
528 clip_limit: Clipping limit for histogram equalization (0.0-1.0)
529 range_min: Minimum value for output range
530 range_max: Maximum value for output range
532 Returns:
533 Equalized 3D pyclesperanto Array of shape (Z, Y, X)
534 """
535 _check_pyclesperanto_available()
537 # Validate 3D array
538 if len(stack.shape) != 3:
539 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
541 # Use 3D CLAHE with 3D tiles (cubes)
542 gpu_equalized = cle.clahe(stack, tile_size=tile_size, clip_limit=clip_limit)
544 # Clip to valid range using pure pyclesperanto
545 gpu_clipped = cle.clip(gpu_equalized, min_intensity=range_min, max_intensity=range_max)
547 return gpu_clipped
549@pyclesperanto_func
550def equalize_histogram_per_slice(
551 stack: "cle.Array",
552 tile_size: int = 8,
553 clip_limit: float = 0.01,
554 range_min: float = 0.0,
555 range_max: float = 65535.0
556) -> "cle.Array":
557 """
558 Apply 2D CLAHE histogram equalization to each slice independently - GPU accelerated.
560 PER-SLICE OPERATION: Applies 2D CLAHE to each Z-slice independently using 2D tiles.
561 Each slice gets its own adaptive histogram equalization. Appropriate for stacks
562 of different images (different X,Y content) or when slices should be treated independently.
564 Args:
565 stack: 3D pyclesperanto Array of shape (Z, Y, X)
566 tile_size: Size of 2D tiles (squares) for adaptive equalization per slice
567 clip_limit: Clipping limit for histogram equalization (0.0-1.0)
568 range_min: Minimum value for output range
569 range_max: Maximum value for output range
571 Returns:
572 Equalized 3D pyclesperanto Array of shape (Z, Y, X)
573 """
574 _check_pyclesperanto_available()
576 # Validate 3D array
577 if len(stack.shape) != 3:
578 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array")
580 # Create result array
581 result = cle.create_like(stack)
583 # Apply 2D CLAHE to each slice independently
584 for z in range(stack.shape[0]):
585 # Work directly with slice views
586 gpu_slice = stack[z] # Direct slice access
588 # Apply 2D CLAHE to this slice only
589 gpu_equalized_slice = cle.clahe(gpu_slice, tile_size=tile_size, clip_limit=clip_limit)
591 # Clip to valid range
592 gpu_clipped_slice = cle.clip(gpu_equalized_slice, min_intensity=range_min, max_intensity=range_max)
594 # Assign result directly
595 result[z] = gpu_clipped_slice
597 return result
599@pyclesperanto_func
600def stack_equalize_histogram(
601 stack: "cle.Array",
602 bins: int = 65536,
603 range_min: float = 0.0,
604 range_max: float = 65535.0
605) -> "cle.Array":
606 """
607 Apply histogram equalization to a stack - GPU accelerated.
609 COMPATIBILITY FUNCTION: Alias for equalize_histogram_3d to maintain API compatibility
610 with numpy processor. Uses 3D CLAHE for true 3D histogram equalization.
612 Args:
613 stack: 3D pyclesperanto Array of shape (Z, Y, X)
614 bins: Number of bins for histogram computation (unused - CLAHE parameter)
615 range_min: Minimum value for histogram range
616 range_max: Maximum value for histogram range
618 Returns:
619 Equalized 3D pyclesperanto Array of shape (Z, Y, X)
620 """
621 # Delegate to the 3D version with default parameters
622 return equalize_histogram_3d(stack, range_min=range_min, range_max=range_max)
624# API compatibility aliases - these maintain the original function names
625# but delegate to the more accurately named implementations
627@pyclesperanto_func
628def percentile_normalize(
629 image: "cle.Array",
630 low_percentile: float = 1.0,
631 high_percentile: float = 99.0,
632 target_min: int = 0,
633 target_max: int = 65535
634) -> "cle.Array":
635 """
636 COMPATIBILITY ALIAS: Delegates to per_slice_minmax_normalize.
638 Note: Uses min/max normalization instead of true percentiles due to
639 pyclesperanto limitations. Kept for API compatibility with other processors.
640 """
641 return per_slice_minmax_normalize(image, low_percentile, high_percentile, target_min, target_max)
643@pyclesperanto_func
644def stack_percentile_normalize(
645 image: "cle.Array",
646 low_percentile: float = 1.0,
647 high_percentile: float = 99.0,
648 target_min: int = 0,
649 target_max: int = 65535
650) -> "cle.Array":
651 """
652 COMPATIBILITY ALIAS: Delegates to stack_minmax_normalize.
654 Note: Uses min/max normalization instead of true percentiles due to
655 pyclesperanto limitations. Kept for API compatibility with other processors.
656 """
657 return stack_minmax_normalize(image, target_min, target_max)
659def create_linear_weight_mask(height: int, width: int, margin_ratio: float = 0.1) -> "cle.Array":
660 """
661 Create a linear weight mask for blending images - GPU accelerated.
663 Pure pyclesperanto implementation using GPU operations only.
664 """
665 # Create coordinate arrays for X and Y positions
666 y_coords = cle.create((height, width), dtype=float)
667 x_coords = cle.create((height, width), dtype=float)
669 # Fill coordinate arrays
670 cle.set_ramp_y(y_coords)
671 cle.set_ramp_x(x_coords)
673 # Calculate margin sizes
674 margin_h = int(height * margin_ratio)
675 margin_w = int(width * margin_ratio)
677 # Create weight mask starting with ones
678 mask = cle.create((height, width), dtype=float)
679 cle.set(mask, 1.0)
681 # Apply fade from top edge: weight = min(1.0, y / margin_h)
682 if margin_h > 0:
683 top_weight = cle.multiply_image_and_scalar(y_coords, scalar=1.0/margin_h)
684 top_weight = cle.minimum_image_and_scalar(top_weight, scalar=1.0)
685 mask = cle.multiply_images(mask, top_weight)
687 # Apply fade from bottom edge: weight = min(1.0, (height - 1 - y) / margin_h)
688 if margin_h > 0:
689 bottom_coords = cle.subtract_image_from_scalar(y_coords, scalar=height - 1)
690 bottom_coords = cle.absolute(bottom_coords)
691 bottom_weight = cle.multiply_image_and_scalar(bottom_coords, scalar=1.0/margin_h)
692 bottom_weight = cle.minimum_image_and_scalar(bottom_weight, scalar=1.0)
693 mask = cle.multiply_images(mask, bottom_weight)
695 # Apply fade from left edge: weight = min(1.0, x / margin_w)
696 if margin_w > 0:
697 left_weight = cle.multiply_image_and_scalar(x_coords, scalar=1.0/margin_w)
698 left_weight = cle.minimum_image_and_scalar(left_weight, scalar=1.0)
699 mask = cle.multiply_images(mask, left_weight)
701 # Apply fade from right edge: weight = min(1.0, (width - 1 - x) / margin_w)
702 if margin_w > 0:
703 right_coords = cle.subtract_image_from_scalar(x_coords, scalar=width - 1)
704 right_coords = cle.absolute(right_coords)
705 right_weight = cle.multiply_image_and_scalar(right_coords, scalar=1.0/margin_w)
706 right_weight = cle.minimum_image_and_scalar(right_weight, scalar=1.0)
707 mask = cle.multiply_images(mask, right_weight)
709 return mask
711def create_weight_mask(shape: tuple, margin_ratio: float = 0.1) -> "cle.Array":
712 """
713 Create a weight mask for blending images - GPU accelerated.
715 Args:
716 shape: Shape of the mask (height, width)
717 margin_ratio: Ratio of image size to use as margin
719 Returns:
720 2D pyclesperanto Array of shape (Y, X)
721 """
722 if not isinstance(shape, tuple) or len(shape) != 2:
723 raise TypeError("shape must be a tuple of (height, width)")
725 height, width = shape
726 return create_linear_weight_mask(height, width, margin_ratio)