PyPortrait: Simulating Bokeh with Python

In photography, bokeh is the aesthetic quality of the blur produced in the out-of-focus parts of an image produced by a lens. Bokeh has been defined as "the way the lens renders out-of-focus points of light".

IPhone 7 plus has an additional camera that produces this effect. But its portrait camera's mode enhances via software with incredible results.

Comparison of bokeh (synthetic) and Gaussian blur - BenFrantzDale Wikipedia CC BY-SA 3.0


In [5]:
import numpy as np
import matplotlib.pyplot as plt

from skimage import measure
from skimage import data

from skimage import io

%matplotlib inline

lenna = io.imread('https://upload.wikimedia.org/wikipedia/en/2/24/Lenna.png')


plt.imshow(lenna)
plt.axis('off')

lenna.shape # lenna is a 512-by-512 pixel image with three channels (red, green, and blue)


Out[5]:
(512, 512, 3)

That's a nice out-of-the-focus background (bo-keh!!).The first thing we are going to do is to remove complexity converting the image to black and white. There are different ways to do that, but let's use the one by default in scikit-image that preserves luminosity


In [6]:
from skimage.color import rgb2gray

lenna_gray = rgb2gray(lenna)

plt.imshow(lenna_gray, cmap=plt.cm.gray)
plt.axis('off')

lenna_gray.shape


Out[6]:
(512, 512)

Alright! Now we are ready to detect the sharpness of the picture calculating the gradient of the image:


In [20]:
#https://github.com/scikit-image/scikit-image/blob/master/skimage/filters/rank/generic.py#L279

from skimage.morphology import disk
from skimage.filters.rank import gradient, gradient_percentile

selection_element = disk(25) # matrix of n pixels with a disk shape

lenna_sharpness = gradient(lenna_gray, selection_element)

plt.imshow(lenna_sharpness, cmap="viridis")
plt.axis('off')
plt.colorbar()

plt.show()


C:\Users\SERVER\Miniconda3\lib\site-packages\skimage\util\dtype.py:110: UserWarning: Possible precision loss when converting from float64 to uint8
  "%s to %s" % (dtypeobj_in, dtypeobj))

That's cool, now whe have a quantitative number that measures the sharpness. So if we compute the gradient selecting more pixels we might obtain a scalable non-detail map where we can apply more blur (bokeh!)


In [21]:
from skimage.filters import gaussian

selection_element = disk(50) # matrix of n pixels with a disk shape

lenna_sharpness = gradient(lenna_gray, selection_element)

lenna_sharpness_std = (lenna_sharpness - lenna_sharpness.min())/(lenna_sharpness.max()-lenna_sharpness.min())

# (optional) Removes sharp edges of disk
lenna_sharpness_std = gaussian(lenna_sharpness_std, sigma=5)

plt.imshow(lenna_sharpness_std, cmap="viridis")
plt.axis('off')
plt.colorbar()

plt.show()


C:\Users\SERVER\Miniconda3\lib\site-packages\skimage\util\dtype.py:110: UserWarning: Possible precision loss when converting from float64 to uint8
  "%s to %s" % (dtypeobj_in, dtypeobj))

And here is the grand finale. Let's apply a gaussian blur filter and a mean blur filter using a disk as a kernel the different effects we can get. Notice that we will use 'lenna_sharpness_std' as linear mask to blur only the background:


In [22]:
from skimage.filters import gaussian
from skimage.filters.rank import mean
from skimage.morphology import disk

from skimage import img_as_float


# Gaussian blur filter (as the iPhones 7 plus)
lenna_gray_gauss = gaussian(lenna_gray, sigma=5)

lenna_gray_gauss_final = lenna_gray*lenna_sharpness_std + \
                        + lenna_gray_gauss*(1-lenna_sharpness_std)


# Mean filter (more realistic bokeh!)
selection_element = disk(10) # matrix of n pixels with a disk shape
lenna_gray_mean = img_as_float(mean(lenna_gray, selection_element))

lenna_gray_mean_final = lenna_gray*lenna_sharpness_std + \
                       + lenna_gray_mean*(1-lenna_sharpness_std)


# Plotting code
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(14, 10))

axes[0].imshow(lenna_gray, cmap=plt.cm.gray)
axes[0].set_title('Original image', fontsize=10)
axes[0].set_axis_off()

axes[1].imshow(lenna_gray_gauss_final, cmap=plt.cm.gray)
axes[1].set_title('Gaussian blur filter \n(iPhone\'s 7 plus)', fontsize=10)
axes[1].set_axis_off()

axes[2].imshow(lenna_gray_mean_final, cmap=plt.cm.gray)
axes[2].set_title('Mean blur \n(convultion by an uniform disk)', fontsize=10)
axes[2].set_axis_off()

plt.show()


C:\Users\SERVER\Miniconda3\lib\site-packages\skimage\util\dtype.py:110: UserWarning: Possible precision loss when converting from float64 to uint8
  "%s to %s" % (dtypeobj_in, dtypeobj))

In [23]:
# TODO color version has some problems with brightness

In [24]:
from skimage.filters import gaussian

filtered_lenna = gaussian(lenna, sigma=10, multichannel=True)

# lenna_sharpness_std_color = np.tile(lenna_sharpness_std[..., None],[1,1,3])
lenna_sharpness_std_color = np.dstack((lenna_sharpness_std, ) * 3)

lenna_final = np.copy(lenna)
lenna_final[:,:,0] = lenna_sharpness_std*lenna[:,:,0] + (1-lenna_sharpness_std)*filtered_lenna[:,:,0]
lenna_final[:,:,1] = lenna_sharpness_std*lenna[:,:,1] + (1-lenna_sharpness_std)*filtered_lenna[:,:,1]
lenna_final[:,:,2] = lenna_sharpness_std*lenna[:,:,2] + (1-lenna_sharpness_std)*filtered_lenna[:,:,2]

# Not working properly
#lenna_final = lenna*lenna_sharpness_std_color + filtered_lenna*(1-lenna_sharpness_std_color) 

# Not working properly
plt.imshow(lenna_final)
plt.axis('off')

plt.show()


Well, to be honest in this image the bokeh in the background is so prominent that we hardly see a difference. Let's create a synthetic image to clarify the difference between gaus and mean blur:


In [12]:
import numpy as np

from skimage.morphology import disk
from skimage.filters.rank import mean
from skimage.filters import gaussian

import matplotlib.pyplot as plt

n = 20
l = 256
im = np.zeros((l, l))
points = l * np.random.random((2, n ** 2))

# Original
im[(points[0]).astype(np.int), (points[1]).astype(np.int)] = 1

# Gaussian filter (as in iPhone's)
im_gauss = gaussian(im, sigma=3) #gauss with a 5 pixels as std

# Mean filter (more realistic bokeh!)
selection_element = disk(5) # matrix of n pixels with a disk shape
im_mean = mean(im, selection_element)

# Plotting code
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 6))

axes[0].imshow(im, cmap=plt.cm.viridis)
axes[0].set_title('Original synthetic image', fontsize=10)
axes[0].set_axis_off()

axes[1].imshow(im_gauss, cmap=plt.cm.viridis)
axes[1].set_title('Gaussian blur filter \n(iPhone\'s 7 plus)', fontsize=10)
axes[1].set_axis_off()

axes[2].imshow(im_mean, cmap=plt.cm.viridis)
axes[2].set_title('Simulated bokeh \n(convultion by an uniform disk)', fontsize=10)
axes[2].set_axis_off()

plt.show()


C:\Users\SERVER\Miniconda3\lib\site-packages\skimage\util\dtype.py:110: UserWarning: Possible precision loss when converting from float64 to uint8
  "%s to %s" % (dtypeobj_in, dtypeobj))

We can even change the shape of our aperture (element)


In [13]:
import numpy as np
from skimage.morphology import diamond, disk, square
from skimage.filters.rank import mean, modal

from skimage.filters import gaussian
import matplotlib.pyplot as plt

n = 20
l = 256
im = np.zeros((l, l))
points = l * np.random.random((2, n ** 2))

# Original
im[(points[0]).astype(np.int), (points[1]).astype(np.int)] = 1

# Plotting code
fig, axes = plt.subplots(nrows=1, ncols=4, figsize=(12, 10))
axes[3].imshow(im, cmap=plt.cm.viridis)
axes[3].set_title('original image', fontsize=10)
axes[3].set_axis_off()

element_list = [diamond, disk, square]

for i, element in enumerate(element_list):
    selection_element = element(5) # matrix of n pixels with a disk shape
    im_mean = mean(im, selection_element)
    
    axes[i].imshow(im_mean, cmap=plt.cm.viridis)
    axes[i].set_title(element.__name__, fontsize=10)
    axes[i].set_axis_off()

plt.show()


C:\Users\SERVER\Miniconda3\lib\site-packages\skimage\util\dtype.py:110: UserWarning: Possible precision loss when converting from float64 to uint8
  "%s to %s" % (dtypeobj_in, dtypeobj))