@@ -272,7 +272,7 @@ def destin(
272272 foregroundAlpha : np .ndarray ,
273273 backgroundColour : np .ndarray ,
274274 foregroundColour : np .ndarray ,
275- ):
275+ ) -> tuple [ np . ndarray , np . ndarray ] :
276276 """'clip' composite mode.
277277
278278 All parts of 'layer above' which are alpha in 'layer below' will be made
@@ -300,7 +300,7 @@ def destout(
300300 foregroundAlpha : np .ndarray ,
301301 backgroundColour : np .ndarray ,
302302 foregroundColour : np .ndarray ,
303- ):
303+ ) -> tuple [ np . ndarray , np . ndarray ] :
304304 """Reverse 'Clip' composite mode.
305305
306306 All parts of 'layer below' which are alpha in 'layer above' will be made
@@ -323,7 +323,7 @@ def destatop(
323323 foregroundAlpha : np .ndarray ,
324324 backgroundColour : np .ndarray ,
325325 foregroundColour : np .ndarray ,
326- ):
326+ ) -> tuple [ np . ndarray , np . ndarray ] :
327327 """Place the layer below above the 'layer above' in places where the 'layer above' exists...
328328
329329 where 'layer below' does not exist, but 'layer above' does, place 'layer-above'
@@ -344,7 +344,7 @@ def srcatop(
344344 foregroundAlpha : np .ndarray ,
345345 backgroundColour : np .ndarray ,
346346 foregroundColour : np .ndarray ,
347- ):
347+ ) -> tuple [ np . ndarray , np . ndarray ] :
348348 """Place the layer below above the 'layer above' in places where the 'layer above' exists."""
349349 outAlpha = (foregroundAlpha * backgroundAlpha ) + (backgroundAlpha * (1 - foregroundAlpha ))
350350 with np .errstate (divide = "ignore" , invalid = "ignore" ):
@@ -459,34 +459,150 @@ def blendLayers(
459459 foreground : Image .Image ,
460460 blendType : BlendType | tuple [str , ...],
461461 opacity : float = 1.0 ,
462+ offsets : tuple [int , int ] = (0 , 0 ),
462463) -> Image .Image :
463- """Blend layers using numpy array.
464+ """Blend two layers (background, and foreground).
465+
466+ Note if the background is smaller than the foreground then some of the foreground will be cut
467+ off
464468
465469 Args:
466470 ----
467- background (Image.Image): background layer
468- foreground (Image.Image): foreground layer (must be same size as background)
469- blendType (BlendType): The blendtype
470- opacity (float): The opacity of the foreground image
471+ background (Image.Image): The background layer.
472+ foreground (Image.Image): The foreground layer (must be the same size as the background).
473+ blendType (BlendType): The blend type to be applied.
474+ opacity (float, optional): The opacity of the foreground image. Defaults to 1.0.
475+ offsets (Tuple[int, int], optional): Offsets for the foreground layer. Defaults to (0, 0).
471476
472477 Returns:
473478 -------
474- Image.Image: combined image
479+ Image.Image: The combined image.
480+
481+ Examples:
482+ --------
483+ # Blend two layers with default parameters
484+ combined_image = blendLayers(background_image, foreground_image, BlendType.NORMAL)
485+
486+ # Blend two layers with custom opacity and offsets
487+ combined_image = blendLayers(
488+ background_image,
489+ foreground_image,
490+ BlendType.MULTIPLY,
491+ opacity=0.7,
492+ offsets=(100, 50)
493+ )
475494 """
476- # Convert the Image.Image to a numpy array
477- npForeground : np .ndarray = imageIntToFloat (np .array (foreground .convert ("RGBA" )))
478- npBackground : np .ndarray = imageIntToFloat (np .array (background .convert ("RGBA" )))
495+ arr = blendLayersArray (
496+ background = background ,
497+ foreground = foreground ,
498+ blendType = blendType ,
499+ opacity = opacity ,
500+ offsets = offsets ,
501+ )
502+
503+ return Image .fromarray (np .uint8 (np .around (arr , 0 )))
504+
479505
480- # Get the alpha from the layers
481- backgroundAlpha = npBackground [:, :, 3 ]
482- foregroundAlpha = npForeground [:, :, 3 ] * opacity
483- combinedAlpha = backgroundAlpha * foregroundAlpha
506+ def blendLayersArray (
507+ background : np .ndarray | Image .Image ,
508+ foreground : np .ndarray | Image .Image ,
509+ blendType : BlendType ,
510+ opacity : float = 1.0 ,
511+ offsets : tuple [int , int ] = (0 , 0 ),
512+ ) -> np .ndarray :
513+ """Blend two layers (background, and foreground).
484514
485- # Get the colour from the layers
486- backgroundColor = npBackground [:, :, 0 :3 ]
487- foregroundColor = npForeground [:, :, 0 :3 ]
515+ Note if the background is smaller than the foreground then some of the foreground will be cut
516+ off
517+
518+ Args:
519+ ----
520+ background (np.ndarray | Image.Image): The background layer.
521+ foreground (np.ndarray | Image.Image): The foreground layer (must be the same size as the background).
522+ blendType (BlendType): The blend type to be applied.
523+ opacity (float, optional): The opacity of the foreground image. Defaults to 1.0.
524+ offsets (Tuple[int, int], optional): Offsets for the foreground layer. Defaults to (0, 0).
525+
526+ Returns:
527+ -------
528+ np.ndarray: The combined image.
529+
530+ Examples:
531+ --------
532+ # Blend two layers with default parameters
533+ combined_image = blendLayers(background_image, foreground_image, BlendType.NORMAL)
534+
535+ # Blend two layers with custom opacity and offsets
536+ combined_image = blendLayers(
537+ background_image,
538+ foreground_image,
539+ BlendType.MULTIPLY,
540+ opacity=0.7,
541+ offsets=(100, 50)
542+ )
543+ """
544+ # Convert the Image.Image to a numpy array if required
545+ if isinstance (background , Image .Image ):
546+ background = np .array (background .convert ("RGBA" ))
547+ if isinstance (foreground , Image .Image ):
548+ foreground = np .array (foreground .convert ("RGBA" ))
549+
550+ # do any offset shifting first
551+ if offsets [0 ] > 0 :
552+ foreground = np .hstack (
553+ (np .zeros ((foreground .shape [0 ], offsets [0 ], 4 ), dtype = np .float64 ), foreground )
554+ )
555+ elif offsets [0 ] < 0 :
556+ if offsets [0 ] > - 1 * foreground .shape [1 ]:
557+ foreground = foreground [:, - 1 * offsets [0 ] :, :]
558+ else :
559+ # offset offscreen completely, there is nothing left
560+ return np .zeros (background .shape , dtype = np .float64 )
561+ if offsets [1 ] > 0 :
562+ foreground = np .vstack (
563+ (np .zeros ((offsets [1 ], foreground .shape [1 ], 4 ), dtype = np .float64 ), foreground )
564+ )
565+ elif offsets [1 ] < 0 :
566+ if offsets [1 ] > - 1 * foreground .shape [0 ]:
567+ foreground = foreground [- 1 * offsets [1 ] :, :, :]
568+ else :
569+ # offset offscreen completely, there is nothing left
570+ return np .zeros (background .shape , dtype = np .float64 )
571+
572+ # resize array to fill small images with zeros
573+ if foreground .shape [0 ] < background .shape [0 ]:
574+ foreground = np .vstack (
575+ (
576+ foreground ,
577+ np .zeros (
578+ (background .shape [0 ] - foreground .shape [0 ], foreground .shape [1 ], 4 ),
579+ dtype = np .float64 ,
580+ ),
581+ )
582+ )
583+ if foreground .shape [1 ] < background .shape [1 ]:
584+ foreground = np .hstack (
585+ (
586+ foreground ,
587+ np .zeros (
588+ (foreground .shape [0 ], background .shape [1 ] - foreground .shape [1 ], 4 ),
589+ dtype = np .float64 ,
590+ ),
591+ )
592+ )
593+
594+ # crop the source if the backdrop is smaller
595+ foreground = foreground [: background .shape [0 ], : background .shape [1 ], :]
596+
597+ lower_norm = background / 255.0
598+ upper_norm = foreground / 255.0
599+
600+ upper_alpha = upper_norm [:, :, 3 ] * opacity
601+ lower_alpha = lower_norm [:, :, 3 ]
602+
603+ upper_rgb = upper_norm [:, :, :3 ]
604+ lower_rgb = lower_norm [:, :, :3 ]
488605
489- # Some effects require alpha
490606 alphaFunc = {
491607 BlendType .DESTIN : destin ,
492608 BlendType .DESTOUT : destout ,
@@ -495,28 +611,34 @@ def blendLayers(
495611 }
496612
497613 if blendType in alphaFunc :
498- return Image .fromarray (
499- imageFloatToInt (
500- np .clip (
501- np .dstack (
502- alphaFunc [blendType ](
503- backgroundAlpha , foregroundAlpha , backgroundColor , foregroundColor
504- )
505- ),
506- a_min = 0 ,
507- a_max = 1 ,
508- )
509- )
614+ out_rgb , out_alpha = alphaFunc [blendType ](lower_alpha , upper_alpha , lower_rgb , upper_rgb )
615+ else :
616+ out_rgb , out_alpha = alpha_comp_shell (
617+ lower_alpha , upper_alpha , lower_rgb , upper_rgb , blendType
510618 )
511619
512- # Get the colours and the alpha for the new image
513- colorComponents = (
514- (backgroundAlpha - combinedAlpha )[:, :, None ] * backgroundColor
515- + (foregroundAlpha - combinedAlpha )[:, :, None ] * foregroundColor
516- + combinedAlpha [:, :, None ] * blend (backgroundColor , foregroundColor , blendType )
517- )
518- alphaComponent = backgroundAlpha + foregroundAlpha - combinedAlpha
620+ return np .nan_to_num (np .dstack ((out_rgb , out_alpha )), copy = False ) * 255.0
519621
520- return Image .fromarray (
521- imageFloatToInt (np .clip (np .dstack ((colorComponents , alphaComponent )), a_min = 0 , a_max = 1 ))
522- )
622+
623+ def alpha_comp_shell (
624+ lower_alpha : np .ndarray ,
625+ upper_alpha : np .ndarray ,
626+ lower_rgb : np .ndarray ,
627+ upper_rgb : np .ndarray ,
628+ blendType : BlendType | tuple [str , ...],
629+ ) -> tuple [np .ndarray , np .ndarray ]:
630+ """
631+ Implement common transformations occurring in any blend or composite mode.
632+ """
633+
634+ out_alpha = upper_alpha + lower_alpha - (upper_alpha * lower_alpha )
635+
636+ blend_rgb = blend (lower_rgb , upper_rgb , blendType )
637+
638+ lower_rgb_part = np .multiply (((1.0 - upper_alpha ) * lower_alpha )[:, :, None ], lower_rgb )
639+ upper_rgb_part = np .multiply (((1.0 - lower_alpha ) * upper_alpha )[:, :, None ], upper_rgb )
640+ blended_rgb_part = np .multiply ((lower_alpha * upper_alpha )[:, :, None ], blend_rgb )
641+
642+ out_rgb = np .divide ((lower_rgb_part + upper_rgb_part + blended_rgb_part ), out_alpha [:, :, None ])
643+
644+ return out_rgb , out_alpha
0 commit comments