@@ -32,10 +32,10 @@ def add_weighted_simsimd(img1: np.ndarray, weight1: float, img2: np.ndarray, wei
32
32
original_dtype = img1 .dtype
33
33
34
34
if img2 .dtype != original_dtype :
35
- img2 = clip (img2 .astype (original_dtype ), original_dtype , inplace = True )
35
+ img2 = clip (img2 .astype (original_dtype , copy = False ), original_dtype , inplace = True )
36
36
37
37
return np .frombuffer (
38
- ss .wsum (img1 .reshape (- 1 ), img2 .astype (original_dtype ).reshape (- 1 ), alpha = weight1 , beta = weight2 ),
38
+ ss .wsum (img1 .reshape (- 1 ), img2 .astype (original_dtype , copy = False ).reshape (- 1 ), alpha = weight1 , beta = weight2 ),
39
39
dtype = original_dtype ,
40
40
).reshape (
41
41
original_shape ,
@@ -51,7 +51,7 @@ def multiply_by_constant_simsimd(img: np.ndarray, value: float) -> np.ndarray:
51
51
52
52
53
53
def add_constant_simsimd (img : np .ndarray , value : float ) -> np .ndarray :
54
- return add_weighted_simsimd (img , 1 , (np .ones_like (img ) * value ).astype (img .dtype ), 1 )
54
+ return add_weighted_simsimd (img , 1 , (np .ones_like (img ) * value ).astype (img .dtype , copy = False ), 1 )
55
55
56
56
57
57
def create_lut_array (
@@ -92,11 +92,12 @@ def apply_lut(
92
92
93
93
if isinstance (value , (int , float )):
94
94
lut = create_lut_array (dtype , value , operation )
95
- return sz_lut (img , clip (lut , dtype ) , inplace )
95
+ return sz_lut (img , clip (lut , dtype , inplace = False ), False )
96
96
97
97
num_channels = img .shape [- 1 ]
98
- luts = create_lut_array (dtype , value , operation )
99
- return cv2 .merge ([sz_lut (img [:, :, i ], clip (luts [i ], dtype , inplace = False ), inplace ) for i in range (num_channels )])
98
+
99
+ luts = clip (create_lut_array (dtype , value , operation ), dtype , inplace = False )
100
+ return cv2 .merge ([sz_lut (img [:, :, i ], luts [i ], inplace ) for i in range (num_channels )])
100
101
101
102
102
103
def prepare_value_opencv (
@@ -135,14 +136,14 @@ def _prepare_array_value(
135
136
operation : Literal ["add" , "multiply" ],
136
137
) -> np .ndarray :
137
138
if value .dtype == np .float64 :
138
- value = value .astype (np .float32 )
139
+ value = value .astype (np .float32 , copy = False )
139
140
if value .ndim == 1 :
140
141
value = value .reshape (1 , 1 , - 1 )
141
142
value = np .broadcast_to (value , img .shape )
142
143
if operation == "add" and img .dtype == np .uint8 :
143
144
if np .all (value >= 0 ):
144
- return clip (value , np .uint8 )
145
- return np .trunc (value ).astype (np .float32 )
145
+ return clip (value , np .uint8 , inplace = False )
146
+ return np .trunc (value ).astype (np .float32 , copy = False )
146
147
return value
147
148
148
149
@@ -154,7 +155,7 @@ def apply_numpy(
154
155
if operation == "add" and img .dtype == np .uint8 :
155
156
value = np .int16 (value )
156
157
157
- return np_operations [operation ](img .astype (np .float32 ), value )
158
+ return np_operations [operation ](img .astype (np .float32 , copy = False ), value )
158
159
159
160
160
161
def multiply_lut (img : np .ndarray , value : np .ndarray | float , inplace : bool ) -> np .ndarray :
@@ -165,7 +166,7 @@ def multiply_lut(img: np.ndarray, value: np.ndarray | float, inplace: bool) -> n
165
166
def multiply_opencv (img : np .ndarray , value : np .ndarray | float ) -> np .ndarray :
166
167
value = prepare_value_opencv (img , value , "multiply" )
167
168
if img .dtype == np .uint8 :
168
- return cv2 .multiply (img .astype (np .float32 ), value )
169
+ return cv2 .multiply (img .astype (np .float32 , copy = False ), value )
169
170
return cv2 .multiply (img , value )
170
171
171
172
@@ -223,7 +224,10 @@ def add_opencv(img: np.ndarray, value: np.ndarray | float, inplace: bool = False
223
224
)
224
225
225
226
if needs_float :
226
- return cv2 .add (img .astype (np .float32 ), value if isinstance (value , (int , float )) else value .astype (np .float32 ))
227
+ return cv2 .add (
228
+ img .astype (np .float32 , copy = False ),
229
+ value if isinstance (value , (int , float )) else value .astype (np .float32 , copy = False ),
230
+ )
227
231
228
232
# Use img as the destination array if inplace=True
229
233
dst = img if inplace else None
@@ -272,14 +276,14 @@ def add(img: np.ndarray, value: ValueType, inplace: bool = False) -> np.ndarray:
272
276
273
277
274
278
def normalize_numpy (img : np .ndarray , mean : float | np .ndarray , denominator : float | np .ndarray ) -> np .ndarray :
275
- img = img .astype (np .float32 )
279
+ img = img .astype (np .float32 , copy = False )
276
280
img -= mean
277
281
return img * denominator
278
282
279
283
280
284
@preserve_channel_dim
281
285
def normalize_opencv (img : np .ndarray , mean : float | np .ndarray , denominator : float | np .ndarray ) -> np .ndarray :
282
- img = img .astype (np .float32 )
286
+ img = img .astype (np .float32 , copy = False )
283
287
mean_img = np .zeros_like (img , dtype = np .float32 )
284
288
denominator_img = np .zeros_like (img , dtype = np .float32 )
285
289
@@ -290,7 +294,7 @@ def normalize_opencv(img: np.ndarray, mean: float | np.ndarray, denominator: flo
290
294
denominator = np .full (img .shape , denominator , dtype = np .float32 )
291
295
292
296
# Ensure the shapes match for broadcasting
293
- mean_img = (mean_img + mean ).astype (np .float32 )
297
+ mean_img = (mean_img + mean ).astype (np .float32 , copy = False )
294
298
denominator_img = denominator_img + denominator
295
299
296
300
result = cv2 .subtract (img , mean_img )
@@ -343,7 +347,7 @@ def power_opencv(img: np.ndarray, value: float) -> np.ndarray:
343
347
return cv2 .pow (img , value )
344
348
if img .dtype == np .uint8 and isinstance (value , float ):
345
349
# For uint8 images, convert to float32, apply power, then convert back to uint8
346
- img_float = img .astype (np .float32 )
350
+ img_float = img .astype (np .float32 , copy = False )
347
351
return cv2 .pow (img_float , value )
348
352
349
353
raise ValueError (f"Unsupported image type { img .dtype } for power operation with value { value } " )
@@ -368,7 +372,7 @@ def power(img: np.ndarray, exponent: ValueType, inplace: bool = False) -> np.nda
368
372
369
373
370
374
def add_weighted_numpy (img1 : np .ndarray , weight1 : float , img2 : np .ndarray , weight2 : float ) -> np .ndarray :
371
- return img1 .astype (np .float32 ) * weight1 + img2 .astype (np .float32 ) * weight2
375
+ return img1 .astype (np .float32 , copy = False ) * weight1 + img2 .astype (np .float32 , copy = False ) * weight2
372
376
373
377
374
378
@preserve_channel_dim
@@ -430,7 +434,7 @@ def multiply_add_opencv(img: np.ndarray, factor: ValueType, value: ValueType) ->
430
434
if isinstance (value , (int , float )) and value == 0 and isinstance (factor , (int , float )) and factor == 0 :
431
435
return np .zeros_like (img )
432
436
433
- result = img .astype (np .float32 )
437
+ result = img .astype (np .float32 , copy = False )
434
438
result = (
435
439
cv2 .multiply (result , np .ones_like (result ) * factor , dtype = cv2 .CV_64F )
436
440
if factor != 0
@@ -473,7 +477,7 @@ def multiply_add(img: np.ndarray, factor: ValueType, value: ValueType, inplace:
473
477
474
478
@preserve_channel_dim
475
479
def normalize_per_image_opencv (img : np .ndarray , normalization : NormalizationType ) -> np .ndarray :
476
- img = img .astype (np .float32 )
480
+ img = img .astype (np .float32 , copy = False )
477
481
eps = 1e-4
478
482
479
483
if img .ndim == MONO_CHANNEL_DIMENSIONS :
@@ -486,7 +490,7 @@ def normalize_per_image_opencv(img: np.ndarray, normalization: NormalizationType
486
490
mean = np .full_like (img , mean )
487
491
std = np .full_like (img , std )
488
492
normalized_img = cv2 .divide (cv2 .subtract (img , mean ), std )
489
- return normalized_img .clip (- 20 , 20 )
493
+ return np .clip (normalized_img , - 20 , 20 , out = normalized_img )
490
494
491
495
if normalization == "image_per_channel" :
492
496
mean , std = cv2 .meanStdDev (img )
@@ -498,7 +502,7 @@ def normalize_per_image_opencv(img: np.ndarray, normalization: NormalizationType
498
502
std = np .full_like (img , std )
499
503
500
504
normalized_img = cv2 .divide (cv2 .subtract (img , mean ), std , dtype = cv2 .CV_32F )
501
- return normalized_img .clip (- 20 , 20 )
505
+ return np .clip (normalized_img , - 20 , 20 , out = normalized_img )
502
506
503
507
if normalization == "min_max" or (img .shape [- 1 ] == 1 and normalization == "min_max_per_channel" ):
504
508
img_min = img .min ()
@@ -513,14 +517,19 @@ def normalize_per_image_opencv(img: np.ndarray, normalization: NormalizationType
513
517
img_min = np .full_like (img , img_min )
514
518
img_max = np .full_like (img , img_max )
515
519
516
- return cv2 .divide (cv2 .subtract (img , img_min ), (img_max - img_min + eps ), dtype = cv2 .CV_32F ).clip (- 20 , 20 )
520
+ return np .clip (
521
+ cv2 .divide (cv2 .subtract (img , img_min ), (img_max - img_min + eps ), dtype = cv2 .CV_32F ),
522
+ - 20 ,
523
+ 20 ,
524
+ out = img ,
525
+ )
517
526
518
527
raise ValueError (f"Unknown normalization method: { normalization } " )
519
528
520
529
521
530
@preserve_channel_dim
522
531
def normalize_per_image_numpy (img : np .ndarray , normalization : NormalizationType ) -> np .ndarray :
523
- img = img .astype (np .float32 )
532
+ img = img .astype (np .float32 , copy = False )
524
533
eps = 1e-4
525
534
526
535
if img .ndim == MONO_CHANNEL_DIMENSIONS :
@@ -530,23 +539,23 @@ def normalize_per_image_numpy(img: np.ndarray, normalization: NormalizationType)
530
539
mean = img .mean ()
531
540
std = img .std () + eps
532
541
normalized_img = (img - mean ) / std
533
- return normalized_img .clip (- 20 , 20 )
542
+ return np .clip (normalized_img , - 20 , 20 , out = normalized_img )
534
543
535
544
if normalization == "image_per_channel" :
536
545
pixel_mean = img .mean (axis = (0 , 1 ))
537
546
pixel_std = img .std (axis = (0 , 1 )) + eps
538
547
normalized_img = (img - pixel_mean ) / pixel_std
539
- return normalized_img .clip (- 20 , 20 )
548
+ return np .clip (normalized_img , - 20 , 20 , out = normalized_img )
540
549
541
550
if normalization == "min_max" :
542
551
img_min = img .min ()
543
552
img_max = img .max ()
544
- return ( img - img_min ) / (img_max - img_min + eps )
553
+ return np . clip (( img - img_min ) / (img_max - img_min + eps ), - 20 , 20 , out = img )
545
554
546
555
if normalization == "min_max_per_channel" :
547
556
img_min = img .min (axis = (0 , 1 ))
548
557
img_max = img .max (axis = (0 , 1 ))
549
- return ( img - img_min ) / (img_max - img_min + eps )
558
+ return np . clip (( img - img_min ) / (img_max - img_min + eps ), - 20 , 20 , out = img )
550
559
551
560
raise ValueError (f"Unknown normalization method: { normalization } " )
552
561
@@ -579,7 +588,7 @@ def normalize_per_image_lut(img: np.ndarray, normalization: NormalizationType) -
579
588
img_min = img .min ()
580
589
img_max = img .max ()
581
590
lut = (np .arange (0 , max_value + 1 , dtype = np .float32 ) - img_min ) / (img_max - img_min + eps )
582
- return cv2 .LUT (img , lut )
591
+ return cv2 .LUT (img , lut ). clip ( - 20 , 20 )
583
592
584
593
if normalization == "min_max_per_channel" :
585
594
img_min = img .min (axis = (0 , 1 ))
@@ -604,15 +613,15 @@ def normalize_per_image(img: np.ndarray, normalization: NormalizationType) -> np
604
613
def to_float_numpy (img : np .ndarray , max_value : float | None = None ) -> np .ndarray :
605
614
if max_value is None :
606
615
max_value = get_max_value (img .dtype )
607
- return (img / max_value ).astype (np .float32 )
616
+ return (img / max_value ).astype (np .float32 , copy = False )
608
617
609
618
610
619
@preserve_channel_dim
611
620
def to_float_opencv (img : np .ndarray , max_value : float | None = None ) -> np .ndarray :
612
621
if max_value is None :
613
622
max_value = get_max_value (img .dtype )
614
623
615
- img_float = img .astype (np .float32 )
624
+ img_float = img .astype (np .float32 , copy = False )
616
625
617
626
num_channels = get_num_channels (img )
618
627
@@ -638,7 +647,7 @@ def to_float_lut(img: np.ndarray, max_value: float | None = None) -> np.ndarray:
638
647
639
648
def to_float (img : np .ndarray , max_value : float | None = None ) -> np .ndarray :
640
649
if img .dtype == np .float64 :
641
- return img .astype (np .float32 )
650
+ return img .astype (np .float32 , copy = False )
642
651
if img .dtype == np .float32 :
643
652
return img
644
653
if img .dtype == np .uint8 :
@@ -657,17 +666,17 @@ def from_float_opencv(img: np.ndarray, target_dtype: np.dtype, max_value: float
657
666
if max_value is None :
658
667
max_value = get_max_value (target_dtype )
659
668
660
- img_float = img .astype (np .float32 )
669
+ img_float = img .astype (np .float32 , copy = False )
661
670
662
671
num_channels = get_num_channels (img )
663
672
664
673
if num_channels > MAX_OPENCV_WORKING_CHANNELS :
665
674
# For images with more than 4 channels, create a full-sized multiplier
666
675
max_value_array = np .full_like (img_float , max_value )
667
- return clip (np .rint (cv2 .multiply (img_float , max_value_array )), target_dtype )
676
+ return clip (np .rint (cv2 .multiply (img_float , max_value_array )), target_dtype , inplace = False )
668
677
669
678
# For images with 4 or fewer channels, use scalar multiplication
670
- return clip (np .rint (img * max_value ), target_dtype )
679
+ return clip (np .rint (img * max_value ), target_dtype , inplace = False )
671
680
672
681
673
682
def from_float (img : np .ndarray , target_dtype : np .dtype , max_value : float | None = None ) -> np .ndarray :
@@ -695,7 +704,7 @@ def from_float(img: np.ndarray, target_dtype: np.dtype, max_value: float | None
695
704
return img
696
705
697
706
if target_dtype == np .float64 :
698
- return img .astype (np .float32 )
707
+ return img .astype (np .float32 , copy = False )
699
708
700
709
if img .dtype == np .float32 :
701
710
return from_float_opencv (img , target_dtype , max_value )
@@ -710,6 +719,9 @@ def hflip_numpy(img: np.ndarray) -> np.ndarray:
710
719
711
720
@preserve_channel_dim
712
721
def hflip_cv2 (img : np .ndarray ) -> np .ndarray :
722
+ # OpenCV's flip function has a limitation of 512 channels
723
+ if img .ndim > 2 and img .shape [2 ] > 512 :
724
+ return _flip_multichannel (img , flip_code = 1 )
713
725
return cv2 .flip (img , 1 )
714
726
715
727
@@ -719,6 +731,9 @@ def hflip(img: np.ndarray) -> np.ndarray:
719
731
720
732
@preserve_channel_dim
721
733
def vflip_cv2 (img : np .ndarray ) -> np .ndarray :
734
+ # OpenCV's flip function has a limitation of 512 channels
735
+ if img .ndim > 2 and img .shape [2 ] > 512 :
736
+ return _flip_multichannel (img , flip_code = 0 )
722
737
return cv2 .flip (img , 0 )
723
738
724
739
@@ -731,6 +746,48 @@ def vflip(img: np.ndarray) -> np.ndarray:
731
746
return vflip_cv2 (img )
732
747
733
748
749
+ def _flip_multichannel (img : np .ndarray , flip_code : int ) -> np .ndarray :
750
+ """Process images with more than 512 channels by splitting into chunks.
751
+
752
+ OpenCV's flip function has a limitation where it can only handle images with up to 512 channels.
753
+ This function works around that limitation by splitting the image into chunks of 512 channels,
754
+ flipping each chunk separately, and then concatenating the results.
755
+
756
+ Args:
757
+ img: Input image with many channels
758
+ flip_code: OpenCV flip code (0 for vertical, 1 for horizontal, -1 for both)
759
+
760
+ Returns:
761
+ Flipped image with all channels preserved
762
+ """
763
+ # Get image dimensions
764
+ height , width = img .shape [:2 ]
765
+ num_channels = 1 if img .ndim == 2 else img .shape [2 ]
766
+
767
+ # If the image has 2 dimensions or fewer than 512 channels, use cv2.flip directly
768
+ if img .ndim == 2 or num_channels <= 512 :
769
+ return cv2 .flip (img , flip_code )
770
+
771
+ # Process in chunks of 512 channels
772
+ chunk_size = 512
773
+ result_chunks = []
774
+
775
+ for i in range (0 , num_channels , chunk_size ):
776
+ end_idx = min (i + chunk_size , num_channels )
777
+ chunk = img [:, :, i :end_idx ]
778
+ flipped_chunk = cv2 .flip (chunk , flip_code )
779
+
780
+ # Ensure the chunk maintains its dimensionality
781
+ # This is needed when the last chunk has only one channel and cv2.flip reduces the dimensions
782
+ if flipped_chunk .ndim == 2 and img .ndim == 3 :
783
+ flipped_chunk = np .expand_dims (flipped_chunk , axis = 2 )
784
+
785
+ result_chunks .append (flipped_chunk )
786
+
787
+ # Concatenate the chunks along the channel dimension
788
+ return np .concatenate (result_chunks , axis = 2 )
789
+
790
+
734
791
def float32_io (func : Callable [..., np .ndarray ]) -> Callable [..., np .ndarray ]:
735
792
"""Decorator to ensure float32 input/output for image processing functions.
736
793
0 commit comments