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

1""" 

2pyclesperanto GPU processor for OpenHCS. 

3 

4This processor uses pyclesperanto for GPU-accelerated image processing, 

5providing excellent compatibility with OpenCL devices and seamless integration 

6with OpenHCS patterns. 

7""" 

8 

9import logging 

10import os 

11from typing import List, Optional, Union 

12 

13# Import OpenHCS decorator 

14from openhcs.core.memory.decorators import pyclesperanto as pyclesperanto_func 

15 

16# Set up logging 

17logger = logging.getLogger(__name__) 

18 

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") 

35 

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") 

40 

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") 

45 

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 

49 

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) 

54 

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) 

58 

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. 

69 

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). 

74 

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 

81 

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") 

88 

89 # Build result by concatenating pairs of slices 

90 result_slices = [] 

91 

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 

95 

96 # Calculate min/max range for normalization (pure GPU) 

97 p_low, p_high = _gpu_minmax_normalize_range(gpu_slice, low_percentile, high_percentile) 

98 

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 

106 

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) 

110 

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 ) 

116 

117 result_slices.append(gpu_normalized) 

118 

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]) 

123 

124 return result 

125 

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. 

134 

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). 

138 

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 

145 

146 Returns: 

147 Normalized 3D pyclesperanto Array of shape (Z, Y, X) with dtype uint16 

148 """ 

149 _check_pyclesperanto_available() 

150 

151 # Validate 3D array 

152 if len(image.shape) != 3: 

153 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array") 

154 

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) 

157 

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 

163 

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) 

167 

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 ) 

173 

174 return gpu_normalized 

175 

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. 

184 

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. 

188 

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 

193 

194 Returns: 

195 Sharpened 3D pyclesperanto Array of shape (Z, Y, X) 

196 """ 

197 _check_pyclesperanto_available() 

198 

199 # Validate 3D array 

200 if len(image.shape) != 3: 

201 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array") 

202 

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) 

205 

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) 

210 

211 # Clip to valid range 

212 gpu_clipped = cle.clip(gpu_sharpened, min_intensity=0, max_intensity=65535) 

213 

214 return gpu_clipped 

215 

216@pyclesperanto_func 

217def max_projection(stack: "cle.Array") -> "cle.Array": 

218 """ 

219 Create a maximum intensity projection from a Z-stack - GPU accelerated. 

220 

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. 

224 

225 Args: 

226 stack: 3D pyclesperanto Array of shape (Z, Y, X) 

227 

228 Returns: 

229 3D pyclesperanto Array of shape (1, Y, X) 

230 """ 

231 _check_pyclesperanto_available() 

232 

233 # Validate 3D array 

234 if len(stack.shape) != 3: 

235 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array") 

236 

237 # Create max projection (stays on GPU) 

238 gpu_projection_2d = cle.maximum_z_projection(stack) 

239 

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 

244 

245 return result 

246 

247@pyclesperanto_func 

248def mean_projection(stack: "cle.Array") -> "cle.Array": 

249 """ 

250 Create a mean intensity projection from a Z-stack - GPU accelerated. 

251 

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. 

255 

256 Args: 

257 stack: 3D pyclesperanto Array of shape (Z, Y, X) 

258 

259 Returns: 

260 3D pyclesperanto Array of shape (1, Y, X) 

261 """ 

262 _check_pyclesperanto_available() 

263 

264 # Validate 3D array 

265 if len(stack.shape) != 3: 

266 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array") 

267 

268 # Create mean projection (stays on GPU) 

269 gpu_projection_2d = cle.mean_z_projection(stack) 

270 

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 

275 

276 return result 

277 

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. 

284 

285 TRUE 3D OPERATION: Dispatcher function that calls the appropriate projection 

286 method. All projection methods collapse the Z-dimension. 

287 

288 Args: 

289 stack: 3D pyclesperanto Array of shape (Z, Y, X) 

290 method: Projection method (max_projection, mean_projection) 

291 

292 Returns: 

293 3D pyclesperanto Array of shape (1, Y, X) 

294 """ 

295 _check_pyclesperanto_available() 

296 

297 # Validate 3D array 

298 if len(stack.shape) != 3: 

299 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array") 

300 

301 if method == "max_projection": 

302 return max_projection(stack) 

303 

304 if method == "mean_projection": 

305 return mean_projection(stack) 

306 

307 # FAIL FAST: No fallback projection methods 

308 raise ValueError(f"Unknown projection method: {method}. Valid methods: max_projection, mean_projection") 

309 

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. 

320 

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. 

324 

325 Implementation: Downsamples entire stack, applies 2D top-hat per slice, 

326 calculates background, upsamples background, then subtracts from original. 

327 

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 

334 

335 Returns: 

336 Filtered 3D pyclesperanto Array of shape (Z, Y, X) 

337 """ 

338 _check_pyclesperanto_available() 

339 

340 # Validate 3D array 

341 if len(image.shape) != 3: 

342 raise ValueError(f"Expected 3D array, got {len(image.shape)}D array") 

343 

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 ) 

354 

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 

366 

367 # 3) Calculate background on small image 

368 gpu_background_small = cle.subtract_images(gpu_small, result_small) 

369 

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 ) 

379 

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) 

383 

384 return gpu_result 

385 

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. 

390 

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) 

394 

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) 

398 

399 Returns: 

400 Masked 3D pyclesperanto Array of shape (Z, Y, X) 

401 """ 

402 _check_pyclesperanto_available() 

403 

404 # Validate 3D image 

405 if len(image.shape) != 3: 

406 raise ValueError(f"Expected 3D image array, got {len(image.shape)}D array") 

407 

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 ) 

414 

415 # Create result array on GPU 

416 result = cle.create_like(image) 

417 

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 

425 

426 return result 

427 

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 ) 

434 

435 # Apply mask directly (both stay on GPU) 

436 return cle.multiply_images(image, mask) 

437 

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}") 

441 

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. 

448 

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. 

452 

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. 

456 

457 Returns: 

458 Composite 3D pyclesperanto Array of shape (1, Y, X) 

459 """ 

460 _check_pyclesperanto_available() 

461 

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") 

465 

466 n_slices, height, width = stack.shape 

467 

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}") 

478 

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] 

484 

485 # Create result array with shape (1, Y, X) 

486 result = cle.create((1, height, width), dtype=stack.dtype) 

487 

488 # Initialize with zeros 

489 cle.set(result, 0.0) 

490 

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 

496 

497 # Multiply slice by its weight 

498 weighted_slice = cle.multiply_image_and_scalar(slice_i, scalar=weight) 

499 

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) 

504 

505 # Put it back (this might need adjustment based on pyclesperanto API) 

506 result[0] = result_slice 

507 

508 return result 

509 

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. 

520 

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. 

524 

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 

531 

532 Returns: 

533 Equalized 3D pyclesperanto Array of shape (Z, Y, X) 

534 """ 

535 _check_pyclesperanto_available() 

536 

537 # Validate 3D array 

538 if len(stack.shape) != 3: 

539 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array") 

540 

541 # Use 3D CLAHE with 3D tiles (cubes) 

542 gpu_equalized = cle.clahe(stack, tile_size=tile_size, clip_limit=clip_limit) 

543 

544 # Clip to valid range using pure pyclesperanto 

545 gpu_clipped = cle.clip(gpu_equalized, min_intensity=range_min, max_intensity=range_max) 

546 

547 return gpu_clipped 

548 

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. 

559 

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. 

563 

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 

570 

571 Returns: 

572 Equalized 3D pyclesperanto Array of shape (Z, Y, X) 

573 """ 

574 _check_pyclesperanto_available() 

575 

576 # Validate 3D array 

577 if len(stack.shape) != 3: 

578 raise ValueError(f"Expected 3D array, got {len(stack.shape)}D array") 

579 

580 # Create result array 

581 result = cle.create_like(stack) 

582 

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 

587 

588 # Apply 2D CLAHE to this slice only 

589 gpu_equalized_slice = cle.clahe(gpu_slice, tile_size=tile_size, clip_limit=clip_limit) 

590 

591 # Clip to valid range 

592 gpu_clipped_slice = cle.clip(gpu_equalized_slice, min_intensity=range_min, max_intensity=range_max) 

593 

594 # Assign result directly 

595 result[z] = gpu_clipped_slice 

596 

597 return result 

598 

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. 

608 

609 COMPATIBILITY FUNCTION: Alias for equalize_histogram_3d to maintain API compatibility 

610 with numpy processor. Uses 3D CLAHE for true 3D histogram equalization. 

611 

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 

617 

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) 

623 

624# API compatibility aliases - these maintain the original function names 

625# but delegate to the more accurately named implementations 

626 

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. 

637 

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) 

642 

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. 

653 

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) 

658 

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. 

662 

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) 

668 

669 # Fill coordinate arrays 

670 cle.set_ramp_y(y_coords) 

671 cle.set_ramp_x(x_coords) 

672 

673 # Calculate margin sizes 

674 margin_h = int(height * margin_ratio) 

675 margin_w = int(width * margin_ratio) 

676 

677 # Create weight mask starting with ones 

678 mask = cle.create((height, width), dtype=float) 

679 cle.set(mask, 1.0) 

680 

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) 

686 

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) 

694 

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) 

700 

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) 

708 

709 return mask 

710 

711def create_weight_mask(shape: tuple, margin_ratio: float = 0.1) -> "cle.Array": 

712 """ 

713 Create a weight mask for blending images - GPU accelerated. 

714 

715 Args: 

716 shape: Shape of the mask (height, width) 

717 margin_ratio: Ratio of image size to use as margin 

718 

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)") 

724 

725 height, width = shape 

726 return create_linear_weight_mask(height, width, margin_ratio) 

727