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

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 

10from typing import List, Optional, Union 

11 

12# Import OpenHCS decorator 

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

14 

15# Set up logging 

16logger = logging.getLogger(__name__) 

17 

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

26 

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

31 

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

36 

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 

40 

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) 

45 

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) 

49 

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. 

60 

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

65 

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 

72 

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

79 

80 # Build result by concatenating pairs of slices 

81 result_slices = [] 

82 

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 

86 

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

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

89 

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 

97 

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) 

101 

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 ) 

107 

108 result_slices.append(gpu_normalized) 

109 

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

114 

115 return result 

116 

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. 

125 

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

129 

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 

136 

137 Returns: 

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

139 """ 

140 _check_pyclesperanto_available() 

141 

142 # Validate 3D array 

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

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

145 

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) 

148 

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 

154 

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) 

158 

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 ) 

164 

165 return gpu_normalized 

166 

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. 

175 

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. 

179 

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 

184 

185 Returns: 

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

187 """ 

188 _check_pyclesperanto_available() 

189 

190 # Validate 3D array 

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

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

193 

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) 

196 

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) 

201 

202 # Clip to valid range 

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

204 

205 return gpu_clipped 

206 

207@pyclesperanto_func 

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

209 """ 

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

211 

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. 

215 

216 Args: 

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

218 

219 Returns: 

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

221 """ 

222 _check_pyclesperanto_available() 

223 

224 # Validate 3D array 

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

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

227 

228 # Create max projection (stays on GPU) 

229 gpu_projection_2d = cle.maximum_z_projection(stack) 

230 

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 

235 

236 return result 

237 

238@pyclesperanto_func 

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

240 """ 

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

242 

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. 

246 

247 Args: 

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

249 

250 Returns: 

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

252 """ 

253 _check_pyclesperanto_available() 

254 

255 # Validate 3D array 

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

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

258 

259 # Create mean projection (stays on GPU) 

260 gpu_projection_2d = cle.mean_z_projection(stack) 

261 

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 

266 

267 return result 

268 

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. 

275 

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

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

278 

279 Args: 

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

281 method: Projection method (max_projection, mean_projection) 

282 

283 Returns: 

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

285 """ 

286 _check_pyclesperanto_available() 

287 

288 # Validate 3D array 

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

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

291 

292 if method == "max_projection": 

293 return max_projection(stack) 

294 

295 if method == "mean_projection": 

296 return mean_projection(stack) 

297 

298 # FAIL FAST: No fallback projection methods 

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

300 

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. 

311 

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. 

315 

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

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

318 

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 

325 

326 Returns: 

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

328 """ 

329 _check_pyclesperanto_available() 

330 

331 # Validate 3D array 

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

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

334 

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 ) 

345 

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 

357 

358 # 3) Calculate background on small image 

359 gpu_background_small = cle.subtract_images(gpu_small, result_small) 

360 

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 ) 

370 

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) 

374 

375 return gpu_result 

376 

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. 

381 

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) 

385 

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) 

389 

390 Returns: 

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

392 """ 

393 _check_pyclesperanto_available() 

394 

395 # Validate 3D image 

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

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

398 

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 ) 

405 

406 # Create result array on GPU 

407 result = cle.create_like(image) 

408 

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 

416 

417 return result 

418 

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 ) 

425 

426 # Apply mask directly (both stay on GPU) 

427 return cle.multiply_images(image, mask) 

428 

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

432 

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. 

439 

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. 

443 

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. 

447 

448 Returns: 

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

450 """ 

451 _check_pyclesperanto_available() 

452 

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

456 

457 n_slices, height, width = stack.shape 

458 

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

469 

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] 

475 

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

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

478 

479 # Initialize with zeros 

480 cle.set(result, 0.0) 

481 

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 

487 

488 # Multiply slice by its weight 

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

490 

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) 

495 

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

497 result[0] = result_slice 

498 

499 return result 

500 

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. 

511 

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. 

515 

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 

522 

523 Returns: 

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

525 """ 

526 _check_pyclesperanto_available() 

527 

528 # Validate 3D array 

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

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

531 

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

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

534 

535 # Clip to valid range using pure pyclesperanto 

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

537 

538 return gpu_clipped 

539 

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. 

550 

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. 

554 

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 

561 

562 Returns: 

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

564 """ 

565 _check_pyclesperanto_available() 

566 

567 # Validate 3D array 

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

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

570 

571 # Create result array 

572 result = cle.create_like(stack) 

573 

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 

578 

579 # Apply 2D CLAHE to this slice only 

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

581 

582 # Clip to valid range 

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

584 

585 # Assign result directly 

586 result[z] = gpu_clipped_slice 

587 

588 return result 

589 

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. 

599 

600 COMPATIBILITY FUNCTION: Alias for equalize_histogram_3d to maintain API compatibility 

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

602 

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 

608 

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) 

614 

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

616# but delegate to the more accurately named implementations 

617 

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. 

628 

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) 

633 

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. 

644 

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) 

649 

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. 

653 

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) 

659 

660 # Fill coordinate arrays 

661 cle.set_ramp_y(y_coords) 

662 cle.set_ramp_x(x_coords) 

663 

664 # Calculate margin sizes 

665 margin_h = int(height * margin_ratio) 

666 margin_w = int(width * margin_ratio) 

667 

668 # Create weight mask starting with ones 

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

670 cle.set(mask, 1.0) 

671 

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) 

677 

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) 

685 

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) 

691 

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) 

699 

700 return mask 

701 

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

703 """ 

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

705 

706 Args: 

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

708 margin_ratio: Ratio of image size to use as margin 

709 

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

715 

716 height, width = shape 

717 return create_linear_weight_mask(height, width, margin_ratio) 

718