Source code for kwimage.im_alphablend

# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals
import numpy as np
from . import im_core


[docs]def overlay_alpha_layers(layers, keepalpha=True, dtype=np.float32): """ Stacks a sequences of layers on top of one another. The first item is the topmost layer and the last item is the bottommost layer. Args: layers (Sequence[ndarray]): stack of images keepalpha (bool): if False, the alpha channel is removed after blending dtype (np.dtype): format for blending computation (defaults to float32) Returns: ndarray: raster: the blended images References: http://stackoverflow.com/questions/25182421/overlay-numpy-alpha https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending Example: >>> import kwimage >>> keys = ['astro', 'carl', 'stars'] >>> layers = [kwimage.grab_test_image(k, dsize=(100, 100)) for k in keys] >>> layers = [kwimage.ensure_alpha_channel(g, alpha=.5) for g in layers] >>> stacked = overlay_alpha_layers(layers) >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> kwplot.imshow(stacked) >>> kwplot.show_if_requested() """ layer_iter = iter(layers) img1 = next(layer_iter) rgb1, alpha1 = _prep_rgb_alpha(img1, dtype=dtype) for img2 in layer_iter: rgb2, alpha2 = _prep_rgb_alpha(img2, dtype=dtype) rgb1, alpha1 = _alpha_blend_inplace(rgb1, alpha1, rgb2, alpha2) if keepalpha: raster = np.dstack([rgb1, alpha1[..., None]]) else: raster = rgb1 return raster
[docs]def overlay_alpha_images(img1, img2, keepalpha=True, dtype=np.float32, impl='inplace'): """ Places img1 on top of img2 respecting alpha channels. Works like the Photoshop layers with opacity. Args: img1 (ndarray): top image to overlay over img2 img2 (ndarray): base image to superimpose on keepalpha (bool): if False, the alpha channel is removed after blending dtype (np.dtype): format for blending computation (defaults to float32) impl (str, default=inplace): code specifying the backend implementation Returns: ndarray: raster: the blended images TODO: - [ ] Make fast C++ version of this function References: http://stackoverflow.com/questions/25182421/overlay-numpy-alpha https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending Example: >>> import kwimage >>> img1 = kwimage.grab_test_image('astro', dsize=(100, 100)) >>> img2 = kwimage.grab_test_image('carl', dsize=(100, 100)) >>> img1 = kwimage.ensure_alpha_channel(img1, alpha=.5) >>> img3 = overlay_alpha_images(img1, img2) >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> kwplot.imshow(img3) >>> kwplot.show_if_requested() """ rgb1, alpha1 = _prep_rgb_alpha(img1, dtype=dtype) rgb2, alpha2 = _prep_rgb_alpha(img2, dtype=dtype) # Perform the core alpha blending algorithm if impl == 'simple': rgb3, alpha3 = _alpha_blend_simple(rgb1, alpha1, rgb2, alpha2) elif impl == 'inplace': rgb3, alpha3 = _alpha_blend_inplace(rgb1, alpha1, rgb2, alpha2) elif impl == 'numexpr1': rgb3, alpha3 = _alpha_blend_numexpr1(rgb1, alpha1, rgb2, alpha2) elif impl == 'numexpr2': rgb3, alpha3 = _alpha_blend_numexpr2(rgb1, alpha1, rgb2, alpha2) else: raise ValueError('unknown impl={}'.format(impl)) if keepalpha: raster = np.dstack([rgb3, alpha3[..., None]]) # Note: if we want to output a 255 img we could do something like this # out = np.zeros_like(img1) # out[..., :3] = rgb3 # out[..., 3] = alpha3 else: raster = rgb3 return raster
[docs]def _prep_rgb_alpha(img, dtype=np.float32): img = im_core.ensure_float01(img, dtype=dtype, copy=False) img = im_core.atleast_3channels(img, copy=False) c = im_core.num_channels(img) if c == 4: # rgb = np.ascontiguousarray(img[..., 0:3]) # alpha = np.ascontiguousarray(img[..., 3]) rgb = img[..., 0:3] alpha = img[..., 3] else: rgb = img alpha = np.ones_like(img[..., 0]) return rgb, alpha
[docs]def _alpha_blend_simple(rgb1, alpha1, rgb2, alpha2): """ Core alpha blending algorithm SeeAlso: _alpha_blend_inplace - alternative implementation """ c_alpha1 = (1.0 - alpha1) alpha3 = alpha1 + alpha2 * c_alpha1 numer1 = (rgb1 * alpha1[..., None]) numer2 = (rgb2 * (alpha2 * c_alpha1)[..., None]) with np.errstate(invalid='ignore'): rgb3 = (numer1 + numer2) / alpha3[..., None] rgb3[alpha3 == 0] = 0 return rgb3, alpha3
[docs]def _alpha_blend_inplace(rgb1, alpha1, rgb2, alpha2): """ Uglier but faster(? maybe not) version of the core alpha blending algorithm using preallocation and in-place computation where possible. SeeAlso: _alpha_blend_simple - alternative implementation Example: >>> rng = np.random.RandomState(0) >>> rgb1, rgb2 = rng.rand(10, 10, 3), rng.rand(10, 10, 3) >>> alpha1, alpha2 = rng.rand(10, 10), rng.rand(10, 10) >>> f1, f2 = _alpha_blend_inplace(rgb1, alpha1, rgb2, alpha2) >>> s1, s2 = _alpha_blend_simple(rgb1, alpha1, rgb2, alpha2) >>> assert np.all(f1 == s1) and np.all(f2 == s2) >>> alpha1, alpha2 = np.zeros((10, 10)), np.zeros((10, 10)) >>> f1, f2 = _alpha_blend_inplace(rgb1, alpha1, rgb2, alpha2) >>> s1, s2 = _alpha_blend_simple(rgb1, alpha1, rgb2, alpha2) >>> assert np.all(f1 == s1) and np.all(f2 == s2) """ rgb3 = np.empty_like(rgb1) temp_rgb = np.empty_like(rgb1) alpha3 = np.empty_like(alpha1) temp_alpha = np.empty_like(alpha1) # hold (1 - alpha1) np.subtract(1, alpha1, out=temp_alpha) # alpha3 np.copyto(dst=alpha3, src=temp_alpha) np.multiply(alpha2, alpha3, out=alpha3) np.add(alpha1, alpha3, out=alpha3) # numer1 np.multiply(rgb1, alpha1[..., None], out=rgb3) # numer2 np.multiply(alpha2, temp_alpha, out=temp_alpha) np.multiply(rgb2, temp_alpha[..., None], out=temp_rgb) # (numer1 + numer2) np.add(rgb3, temp_rgb, out=rgb3) # removing errstate is actually a significant speedup with np.errstate(invalid='ignore'): np.divide(rgb3, alpha3[..., None], out=rgb3) if not np.all(alpha3): rgb3[alpha3 == 0] = 0 return rgb3, alpha3
[docs]def _alpha_blend_numexpr1(rgb1, alpha1, rgb2, alpha2): """ Alternative. Not well optimized """ import numexpr alpha1_ = alpha1[..., None] # NOQA alpha2_ = alpha2[..., None] # NOQA alpha3 = numexpr.evaluate('alpha1 + alpha2 * (1.0 - alpha1)') alpha3_ = alpha3[..., None] # NOQA rgb3 = numexpr.evaluate('((rgb1 * alpha1_) + (rgb2 * alpha2_ * (1.0 - alpha1_))) / alpha3_') rgb3[alpha3 == 0] = 0
[docs]def _alpha_blend_numexpr2(rgb1, alpha1, rgb2, alpha2): """ Alternative. Not well optimized """ import numexpr c_alpha1 = numexpr.evaluate('1.0 - alpha1') alpha3 = numexpr.evaluate('alpha1 + alpha2 * c_alpha1') c_alpha1_ = c_alpha1[..., None] # NOQA alpha1_ = alpha1[..., None] # NOQA alpha2_ = alpha2[..., None] # NOQA alpha3_ = alpha3[..., None] # NOQA numer1 = numexpr.evaluate('rgb1 * alpha1_') # NOQA numer2 = numexpr.evaluate('rgb2 * (alpha2_ * c_alpha1_)') # NOQA with np.errstate(invalid='ignore'): rgb3 = numexpr.evaluate('(numer1 + numer2) / alpha3_') rgb3[alpha3 == 0] = 0 return rgb3, alpha3
[docs]def ensure_alpha_channel(img, alpha=1.0, dtype=np.float32, copy=False): """ Returns the input image with 4 channels. Args: img (ndarray): an image with shape [H, W], [H, W, 1], [H, W, 3], or [H, W, 4]. alpha (float, default=1.0): default value for missing alpha channel dtype (type, default=np.float32): a numpy floating type copy (bool, default=False): always copy if True, else copy if needed. Returns: an image with specified dtype with shape [H, W, 4]. Raises: ValueError - if the input image does not have 1, 3, or 4 input channels or if the image cannot be converted into a float01 representation """ img = im_core.ensure_float01(img, dtype=dtype, copy=copy) c = im_core.num_channels(img) if c == 4: return img else: if isinstance(alpha, np.ndarray): alpha_channel = alpha else: alpha_channel = np.full(img.shape[0:2], fill_value=alpha, dtype=img.dtype) if c == 3: return np.dstack([img, alpha_channel]) elif c == 1: return np.dstack([img, img, img, alpha_channel]) else: raise ValueError( 'Cannot ensure alpha. Input image has c={} channels'.format(c))