
Начнем с того, что сделаем свою первую миниатюру.
Приступая к работе, в первую очередь необходимо выбрать фотографию. Нет смысла пытаться обрабатывать изображения, которые не подходят изначально. Преимущественно для этого используются пейзажные, городские снимки, снятые с возвышения — это в первую очередь и помагает создать иллюзию маленького мира. Так же хорошо, если в кадре есть небольшие объекты, такие как транспорт и люди.

Основной эффект в подобных фотографиях достигается за счет поворота объектива, что приводит к тому, что плоскость фокуса становится неперпендикулярна оптической оси. Это, в свою очередь, приводит к образованию клиновидной глубины резкости. О том, как создать этот эффект с помощью графических редакторов, можно детально ознакомиться на любом сайте с уроками фотошопа, поэтому перейдем сразу к делу.
Для работы я выбрал эту фотографию:

После добавления насыщенности и применения фильтров в фотошопе получается такой результат:

Разумеется, искушенного читателя не проведешь, и настоящий оптический эффект довольно легко отличить от фильтров, но все же при достаточно грамотной постановке кадра подручными средствами можно добиться неплохих результатов.
По сути все, что пришлось сделать с изображением — наложить искусственное боке в нужных местах и откорректировать насыщенность цвета, которая помогает придать эффект пластиковых моделей объектам в кадре.
Перейдем к программированию.
Начнем с простого. Сначала нужно подготовить маску для изображения. Чтобы не загружать себя сразу лишней работой, сделаем ее в фотошопе. В темных местах величина альфа-слоя будет равна максимальному значению, в белых — нулю, то есть изображение в этих местах будет прозрачно.

В месте с черными пикселями наложения боке происходить не будет и сцена останется в фокусе. Маска была сделана методом “так примерно покатит”, это всего лишь демонстрация подхода.
Немного поковырявшись в python’е, получился такой небольшой и аккуратный скрипт:
- import Image
- import sys
- import numpy as np
- from scipy import ndimage
- import ImageEnhance
- # INIT
- blur_size = 6
- image_base = "/Users/Mango/Desktop/tiltshift_alpha.png"
- image_mask = "/Users/Mango/Desktop/mask_tiltshift.png"
- image_output = "/Users/Mango/Desktop/tiltshift_preview.png"
- # LOAD
- im_base = Image.open(image_base)
- im_mask = Image.open(image_mask)
- im_mask = im_mask.resize(im_base.size)
- # PROCESS
- enh = ImageEnhance.Color(im_base)
- im_base = enh.enhance(1.7)
- enh = ImageEnhance.Contrast(im_base)
- im_base = enh.enhance(1.2)
- im_blurred = np.array(im_base, dtype=float)
- im_blurred = ndimage.gaussian_filter(im_blurred, sigma=[blur_size,blur_size,0])
- im_blurred = Image.fromarray(np.uint8(im_blurred))
- im_mask = im_mask.convert("L")
- im_base = im_base.convert("RGBA")
- # MERGE AND SAVE
- im_base.paste(im_blurred,mask=im_mask)
- im_base.save(image_output)
Вначале получаем исходное изображение и маску, затем размер маски подгоняется под размер изображения и начинается обработка. С помощью модуля ImageEnhance регулируются такие показатели как цвет, яркость и контраст. После чего в im_blurred сохраняется копия изображения в виде массива. Для создания боке я использовал старый добрый фиьтр размытия Гаусса. Его результат отличается от того же Lens Blur в профессиональных редакторах, но для начала вполне неплохой результат.
На финальной стадии размытое изображение накладывается на наш оригинал, используя альфа-маску. Так же стоит учесть, что каждый слой должен иметь правильную палитру. Маска используется в монохромном режиме L, а исходному изображению при помощи convert(«RGBA») добавляется альфа-слой, который и позволяем с помощью маски накладывать второй слой.
Вот что получилось в итоге:

Теперь немного допилим полученный код и избавимся от некрасивого внешнего дополнения в виде вручную нарисованной маски. В этом нет ничего сложного, она представляет собой так называемый reflected gradient и задается двумя коллинеарными векторами.

В еще более упрощенном виде эту модель можно представить следующим образом:

Вся система задается несколькими параметрами: направление, длина вектора А, зоны фокусировки и вектора Б. Все расстояния для простоты задаем по оси y.
Построение градиента происходит следующим образом. Сначала по заданным расстояниям строится изображение шириной в один пиксель, оно будет выступать в роли паттерна. После чего разными манипуляциями можно придать ему любую форму и повернуть на нужный угол.
Немного поэкспериментировав у меня получилась такая функция:
- import Image
- import ImageDraw
- import ImageOps
- import math
- def draw_mask(angle,width,height,offset_init,offset_A,offset_focus,offset_B):
- offset = height*offset_init/100
- vectorA = offset+offset_A*height/100
- focus = vectorA+offset_focus*height/100
- vectorB = focus+offset_B*height/100
- mask = Image.new('L', (width,height))
- mask_1px = Image.new('L', (1,height))
- draw_1px = ImageDraw.Draw(mask_1px)
- for y in range (0,offset): # draw white zone
- draw_1px.point((0,y),255)
- for y in range (offset,vectorA): # draw vectorA
- draw_1px.point((0,y),(vectorA-y)*(255/(vectorA-offset)))
- for y in range (vectorA,focus): # draw white zone
- draw_1px.point((0,y),0)
- for y in range (focus,vectorB): # draw vectorB
- draw_1px.point((0,y),255-(vectorB-y)*(255/(vectorB-focus)))
- for y in range (vectorB,height): # draw white zone
- draw_1px.point((0,y),255)
- m_width,m_height = mask.size
- mask_1px = mask_1px.resize((int(m_width*3),m_height), Image.ANTIALIAS)
- mask_1px = ImageOps.invert(mask_1px)
- mask_top = mask_1px.rotate(angle,Image.NEAREST,1)
- mask_top = ImageOps.invert(mask_top)
- mask.convert("RGBA")
- n_width,n_height = mask_top.size
- mask.paste(mask_top,(-n_width/2,-(n_height/2-height/2)))
- mask.convert("L")
- return mask

Этот код учитывает угол поворота на случай, если захочется сделать инструмент более универсальным или прикрутить к веб-интерфейсу, чем я и собираюсь заняться в ближайшем будущем.
Если кому понадобятся исходники, финальная версия есть на github.