Pull to refresh

Простое наложение 2-х изображений

Reading time 3 min
Views 31K
Это занимательный рассказ о том, как одно изображение накладывается на другое. Если вы занимались растровой графикой, писали игры или графические редакторы, вы врядли найдете в статье что-то для себя. Всем остальным, надеюсь, будет интересно узнать, что эта задача не такая тривиальная, как кажется на первый взгляд.

Итак, у нас 2 картинки в формате RGBA (т.е. 3 цвета + альфаканал):

image image
Прим. 1: Критерием отбора картинок было наличие прозрачных, непрозрачных и полупрозрачных областей. Что на них изображено — не так важно.
Прим. 2: Картинки буду выглядеть так, как они видны в фотошопе, ссылки ведут на настоящие картинки.


Задача — собрать из них третью, в том же формате RGBA с правильной прозрачностью и цветами. Вот так с этим справляется фотошоп, примем это изображение за эталон:

image

Примеры я буду показывать на питоне с использованием PIL.
Итак, простейший код:

im1 = Image.open('im1.png')
im2 = Image.open('im2.png')
im1.paste(im2, (0,0), im2) # Последний параметр — альфаканал, используемый для наложения
im1.save('r1.png')


Мы смешиваем оба изображения, используя для этого альфаканал второго из них. При этом, каждый компонент каждого пикселя результирующего изображения (включая альфаканал) будет расчитан как X2×a + X1×(1-a), где a — значение альфы для этого пикселя из переданного альфаканала im2.

Код дает такой результат:

image
Сразу видны несоответствия — в месте, где красная галочка перекрывается второй картинкой, стало даже прозрачнее, чем на оригинале, хотя логика подсказывает, что когда один полупрозрачный предмет загораживает другой, результат должен быть более непрозрачным, чем оба предмета. Это произошло потому, что помимо смешивания цветов, смешались и альфаканалы. Интенсивность второго альфаканала была примерно 0,25, интенсивность первого 0,5. Мы смешали их с интенсивностью второго: 0,25×0,25 + 0,5×(1-0,25) = 0,44.

Если бы фоновая (первая) картинка была вообще непрозрачной, то после такого смешивания на ней появились бы полупрозрачные места, что противоречит всем законам физики.

Попробуем устранить это эффект, очистив альфаканал второй картинки перед смешиванием:
im1 = Image.open('im1.png')
im2 = Image.open('im2.png')
im1.paste(im2.convert('RGB'), (0,0), im2)
im1.save('r2.png')


Результат будет таким:

image
Прозрачность в месте наложения стала нормальной, но тень от фигуры, которая была едва заметна на оригинале, превратилась в хорошо различимые черные полосы. Это произошло потому, что при расчете цвета алгоритм наложения больше не учитывает то, что второе изображение в этом месте более интенсивное, чем первое (откуда ему взять эти данные, ведь у второго изображения больше нет альфаканала), поэтому смешивает цвета по честному, из переданного альфаканала второго изображения. А в альфанале в этом месте интенсивность примерно 0,5. Потому черный цвет, которого на самом деле совсем немного, участвует в равной доле с голубым.

Но довольно гадать, лучше подумаем, как выглядели бы реальные объекты с такими свойствами.

Представим 2 полупрозрачных стекла: одно синее, другое красное. Синее находится ближе чем красное. Тогда, общая непрозрачность обоих стекол равна сумме непрозрачность ближнего к нам стекла и прозрачность ближнего (т.е. 1-АС), помноженная на непрозрачность дальнего: АС + (1-АС)×АК.

А что же с цветом? Точнее интерес представляет не сам цвет, а доля цвета ближнего стекла. Она равна непрозрачности ближнего стекла, деленной на общую непрозрачность. Т.е. в общем случае значение непрозрачности, которое используется для вычисления цветов, не равно непрозрачности результирующего пикселя.

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

Если же реализовывать «правильную» прозрачность средствами питона, получится примерно так:

def alpha_composite(background, image):
    
    # get alphachanels
    alpha = image.split()[-1]
    background_alpha = background.split()[-1]
    
    new_alpha = list(background_alpha.getdata())
    new_blending = list(alpha.getdata())
    
    for i in xrange(len(new_alpha)):
        alpha_pixel = new_blending[i]
        if alpha_pixel == 0:
            new_alpha[i] = 0
        else:
            new_alpha[i] = alpha_pixel + (255 - alpha_pixel) * new_alpha[i] / 255
            new_blending[i] = alpha_pixel * 255 / new_alpha[i]
    
    alpha.putdata(new_alpha)
    background_alpha.putdata(new_blending)
 
    del new_alpha
    del new_blending
    
    result = background.copy()
    
    result.paste(image, (0, 0), background_alpha)
    result.putalpha(alpha)
    
    return result

im1 = Image.open('im1.png')
im2 = Image.open('im2.png')
alpha_composite(im1, im2).save('r3.png')


И, собственно, результат:

image
Он почти не отличается от результата, созданного в фотошопе. Кстати, если кто-то знает, возможно ли с помошью PIL получить такой же результат без попиксельной работы с изображением, сообщите.
Tags:
Hubs:
+16
Comments 37
Comments Comments 37

Articles