История Оптимизации Альфа_Композита В Pillow 2.0

Недавно вышла вторая версия библиотеки Python для работы с изображениями.

Подушка .

Как многие знают, это форк известной библиотеки PIL, которая, несмотря на солидный возраст, до недавнего времени оставалась самым вменяемым способом работы с изображениями в Python. Авторы Pillow наконец решили не только сохранить старый код, но и добавить новые функции.

И одной из таких функций была функция Alpha_composite().

Эта функция реализует смешивание двух полупрозрачных изображений.

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

Вы можете прочитать о смешивании полупрозрачных изображений в Википедия .

я об этом уже давно писал статья .

С тех пор я смог писать больше для своих нужд. оптимальная реализация , чем указано в конце этой статьи.

И оказалось, что эта реализация быстрее, чем alpha_composite() из новой версии Pillow, написанной на C. Мне это, конечно, польстило, но я все же решил попробовать улучшить реализацию из Pillow. Для начала вам понадобится испытательный стенд. скамейка.

py

  
  
  
  
  
  
  
  
  
   

from PIL import Image from timeit import repeat from image import paste_composite im1 = Image.open('in1.png') im1.load() im2 = Image.open('in2.png') im2.load() print repeat(lambda: paste_composite(im1.copy(), im2), number=100) print repeat(lambda: Image.alpha_composite(im1, im2), number=100) out1 = im1.copy() paste_composite(out1, im2) out1.save('out1.png') out2 = Image.alpha_composite(im1, im2) out2.save('out2.png')

Скрипт берет файлы in1.png и in2.png, расположенные в текущей папке, и перемешивает их сто раз, сначала с помощью функции Paste_composite(), затем с помощью функции Alpha_composite().

Файлы я взял из предыдущей статьи ( один раз , два ).

Мое время работы Paste_composite() составило в среднем 235 мс.

Время работы оригинального метода Alpha_composite() составляло 400 мс.

Не потребовалось много времени, чтобы догадаться, почему функция Alpha_composite() работает так медленно.

Смотря на выполнение , понятно, что функция выполняет все вычисления с числами с плавающей запятой.

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

Первое, что я сделал, это перевел все для работы с целыми числами:

typedef struct { UINT8 r; UINT8 g; UINT8 b; UINT8 a; } rgba8; Imaging ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { Imaging imOut = ImagingNew(imDst->mode, imDst->xsize, imDst->ysize); for (int y = 0; y < imDst->ysize; y++) { rgba8* dst = (rgba8*) imDst->image[y]; rgba8* src = (rgba8*) imSrc->image[y]; rgba8* out = (rgba8*) imOut->image[y]; for (int x = 0; x < imDst->xsize; x ++) { if (src->a == 0) { *out = *dst; } else { UINT16 blend = dst->a * (255 - src->a); UINT8 outa = src->a + blend / 255; UINT8 coef1 = src->a * 255 / outa; UINT8 coef2 = blend / outa; out->r = (src->r * coef1 + dst->r * coef2) / 255; out->g = (src->g * coef1 + dst->g * coef2) / 255; out->b = (src->b * coef1 + dst->b * coef2) / 255; out->a = outa; } dst++; src++; out++; } } return imOut; }

И я сразу получил прибавку в 5,5 раз.

Тест завершился за 75 мс.

Хороший результат, но нужно было проверить, насколько качественная картинка.

Я не придумал ничего лучше, чем использовать результат Фотошопа в качестве эталона.

Получившиеся два изображения выглядели неотличимо от того, что было получено в Photoshop, поэтому мне пришлось придумать, как точнее визуализировать различия.

diff.py

#!/usr/bin/env python import sys from PIL import Image, ImageMath def chanel_diff(c1, c2): c = ImageMath.eval('127 + c1 - c2', c1=c1, c2=c2).

convert('L') return c.point(lambda c: c if 126 <= c <= 128 else 127 + (c - 127) * 10) im1 = Image.open(sys.argv[1]) im2 = Image.open(sys.argv[2]) diff = map(chanel_diff, im1.split(), im2.split()) Image.merge('RGB', diff[:-1]).

save('diff.png', optimie=True) diff[-1].

convert('RGB').

save('diff.alpha.png', optimie=True)

Скрипт принимает на вход 2 имени файла с картинками.

Между каждой парой каналов этих картинок есть различия: сначала просто ищется разница и добавляется к серому фону (значение 127), чтобы можно было увидеть отклонения в обе стороны.

Затем все отклонения больше одного значения усиливаются в 10 раз, чтобы они были визуально заметны.

Результат сравнения записывается в 2 файла: каналы RGB в diff.png, альфа-канал в виде оттенков серого в diff.alpha.png. Оказалось, что разница между результатом Alpha_composite() и Photoshop весьма существенна:

История оптимизации альфа_композита в Pillow 2.0

Еще раз взглянув на код, я понял, что забыл про округление: целые числа делятся, а остаток отбрасывается.

Если мы хотим получить округленный результат, нам нужно к делимому прибавить половину делителя:

UINT16 blend = dst->a * (255 - src->a); UINT8 outa = src->a + (blend + 127) / 255; UINT8 coef1 = src->a * 255 / outa; UINT8 coef2 = blend / outa; out->r = (src->r * coef1 + dst->r * coef2 + 127) / 255; out->g = (src->g * coef1 + dst->g * coef2 + 127) / 255; out->b = (src->b * coef1 + dst->b * coef2 + 127) / 255; out->a = outa;

Время работы увеличилось до 94 мс.

Но отличий от референса стало гораздо меньше.

Однако они не исчезли полностью.

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

Но проверив на всякий случай оригинальную версию Alpha_composite(), я обнаружил, что она практически не отличается от стандартной.

На этом невозможно было остановиться.

Очевидно, возникающие неточности являются результатом недостаточной точности коэффициентов при умножении.

Для каждой переменной можно сохранить дополнительные значащие биты, что повышает точность деления.

Переменная blend вычисляется без деления, с ней ничего делать не нужно.

Переменная outa получается делением blend на 255. При расчете мы сдвигаем значения blend и src-> a вверх на 4 бита.

В результате outa будет иметь 12 значащих битов.

Оба коэффициента делятся на outa, которое теперь на 4 бита больше.

Плюс сами коэффициенты будут на 4 бита больше.

В результате мы сдвигаем делимое на 8 бит.

UINT16 blend = dst->a * (255 - src->a); // 16 bit max UINT16 outa = (src->a << 4) + ((blend << 4) + 127) / 255; // 12 UINT16 coef1 = ((src->a * 255) << 8) / outa; // 12 UINT16 coef2 = (blend << 8) / outa; // 12 out->r = ((src->r * coef1 + dst->r * coef2 + 0x7ff) / 255) >> 4; out->g = ((src->g * coef1 + dst->g * coef2 + 0x7ff) / 255) >> 4; out->b = ((src->b * coef1 + dst->b * coef2 + 0x7ff) / 255) >> 4; out->a = (outa + 0x7) >> 4;

Разумеется, результат всех вычислений необходимо сдвинуть на те же 4 бита, прежде чем поместить его в пиксели изображения.

Этот вариант работает немного медленнее: 108 мс, но у него практически нет пикселей, отличающихся более чем на одно значение от стандартного.

Качественный результат достигнут, можно снова задуматься о продуктивности.

Очевидно, что такой код не является самым оптимальным с точки зрения процессора.

Деление — одна из самых сложных задач, и здесь для каждого пикселя выполняется целых 6 делений.

Оказалось, что избавиться от большинства из них довольно просто.

Тот же PIL в файле Paste.c описывает метод быстрого деления с округлением:

a + 127 / 255 ≈ ((a + 128) + ((a + 128) >> 8)) >> 8

В результате одно деление заменяется двумя сменами и одним сложением, что происходит быстрее на большинстве процессоров.

Сгруппировав одну из смен с существующей сменой на 4, я получил следующий код:

UINT16 blend = dst->a * (255 - src->a); UINT16 outa = (src->a << 4) + (((blend << 4) + (blend >> 4) + 0x80) >> 8); UINT16 coef1 = (((src->a << 8) - src->a) << 8) / outa; // 12 UINT16 coef2 = (blend << 8) / outa; // 12 UINT32 tmpr = src->r * coef1 + dst->r * coef2 + 0x800; out->r = ((tmpr >> 8) + tmpr) >> 12; UINT32 tmpg = src->g * coef1 + dst->g * coef2 + 0x800; out->g = ((tmpg >> 8) + tmpg) >> 12; UINT32 tmpb = src->b * coef1 + dst->b * coef2 + 0x800; out->b = ((tmpb >> 8) + tmpb) >> 12; out->a = (outa + 0x7) >> 4;

Он выполняется за 65 мс и не меняет результат по сравнению с предыдущей версией.

Всего скорость работы была улучшена чуть более чем в 6 раз, а в репозиторий отправлен пул-реквест. Можно было бы попытаться добиться большего сходства со стандартом.

Например, мне удалось совершенно точно воспроизвести альфа-канал, сгенерированный Photoshop, но при этом в пиксели было внесено больше искажений.

Большое обновление Я еще раз задумался о том, что натворил, и решил, что глупо полагаться на алгоритм Фотошопа, который неизвестно для каких целей создан и реализации которого даже не видно.

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

diff.py

#!/usr/bin/env python import sys from PIL import Image, ImageMath def chanel_diff(c1, c2): return ImageMath.eval('127 + c1 - c2', c1=c1, c2=c2).

convert('L') def highlight(c): return c.point(lambda c: c if 126 <= c <= 128 else 127 + (c - 127) * 10) im1 = Image.open(sys.argv[1]) im2 = Image.open(sys.argv[2]) diff = map(chanel_diff, im1.split(), im2.split()) if len(sys.argv) >= 4: highlight(Image.merge('RGB', diff[:-1])).

save('%s.png' % sys.argv[3]) highlight(diff[-1]).

convert('RGB').

save('%s.alpha.png' % sys.argv[3]) def stats(ch): return sorted((c, n) for n, c in ch.getcolors()) for ch, stat in zip(['red ', 'grn ', 'blu ', 'alp '], map(stats, diff)): print ch, ' '.

join('{}: {:>5}'.

format(c, n) for c, n in stat)

Я также подготовил очень серьезный тестовый пример из двух 16-мегапиксельных изображений, где каждая комбинация пикселей была уникальной.

make_testcase.py

def prepare_test_images(dim): """Plese, be careful with dim > 32. Result image is have dim ** 4 pixels (i.e. 1Mpx for 32 dim or 4Gpx for 256 dim).

""" i1 = bytearray(dim ** 4 * 2) i2 = bytearray(dim ** 4 * 2) res = 255.0 / (dim - 1) rangedim = range(dim) pos = 0 for l1 in rangedim: for l2 in rangedim: for a1 in rangedim: for a2 in rangedim: i1[pos] = int(res * l1) i1[pos + 1] = int(res * a1) i2[pos] = int(res * l2) i2[pos + 1] = int(res * a2) pos += 2 print '%s of %s' % (l1, dim) i1 = Image.frombytes('LA', (dim ** 2, dim ** 2), bytes(i1)) i2 = Image.frombytes('LA', (dim ** 2, dim ** 2), bytes(i2)) return i1.convert('RGBA'), i2.convert('RGBA') im1, im2 = prepare_test_images(63) im1.save('im1.png') im2.save('im2.png')

После этого я решил начать с другого конца: не пытаться повысить точность целочисленного алгоритма, а попытаться привести алгоритм с плавающей запятой к работе с целыми числами.

Вот с чего я начал:

double dsta = dst->a / 255.0; double srca = src->a / 255.0; double blend = dsta * (1.0 - srca); double outa = srca + blend; double coef1 = srca / outa; double coef2 = 1 - coef1; double tmpr = src->r * coef1 + dst->r * coef2; out->r = (UINT8) (tmpr + 0.5); double tmpg = src->g * coef1 + dst->g * coef2; out->g = (UINT8) (tmpg + 0.5); double tmpb = src->b * coef1 + dst->b * coef2; out->b = (UINT8) (tmpb + 0.5); out->a = (UINT8) (outa * 255.0 + 0.5);

И вот к чему я пришел:

UINT16 blend = dst->a * (255 - src->a); UINT16 outa255 = src->a * 255 + blend; // There we use 7 bits for precision. // We could use more, but we go beyond 32 bits. UINT16 coef1 = src->a * 255 * 255 * 128 / outa255; UINT16 coef2 = 255 * 128 - coef1; #define SHIFTFORDIV255(a)\ ((a >> 8) + a >> 8) UINT32 tmpr = src->r * coef1 + dst->r * coef2 + (0x80 << 7); out->r = SHIFTFORDIV255(tmpr) >> 7; UINT32 tmpg = src->g * coef1 + dst->g * coef2 + (0x80 << 7); out->g = SHIFTFORDIV255(tmpg) >> 7; UINT32 tmpb = src->b * coef1 + dst->b * coef2 + (0x80 << 7); out->b = SHIFTFORDIV255(tmpb) >> 7; out->a = SHIFTFORDIV255(outa255 + 0x80);

Теги: #оптимизация #PIL #Pillow #python #альфа-канал #python #C++

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.