Pull to refresh

Comments 67

Что меня радует: Это не кусок кода, опубликованный на Stack Overflow. Это одна строчка импорта библиотеки

99% хабра это не радует, а однозначно огорчает.

Если вы в состоянии внедрить в свой проект кусок кода на Си со Stack Overflow, то можете сделать то же самое с кодом с гитхаба, разве нет? (раз, два).

Выглядит круто. И всё же почему бы не добавить поддержку SIMD в основной Pillow — динамически определять доступность SIMD и переключаться между реализацией на лету?

Для компиляции SSE4 и AVX2 кода нужны разные флаги компиляции у отдельных файлов. Сейчас система сборки бинарных питоновских модулей недостаточно гибкая, чтобы так сделать. Подробнее есть в readme.

А разве эти флаги влияют на доступные интринсики?

Конечно. Нужны -msse4 либо -mavx2.

А если ассемблерные вставки напрямую?

Можно к примеру положить оптимизированную версию в отдельный модуль, который будет собираться с нужными флагами. А в pillow добавить от него зависимость.
Для gcc можно использовать target атрибуты. И не надо ни каких флагов компиляции.
Но могут быть проблемы с LTO, если использовать одно имя функции с разными реализациями и атрибутами target.
Без lto, должно быть все хорошо. Либо можно обойти проблему использую функции с уникальными именами и собственную реализацию выбора функции на основе поддержки разных видов simd (я пока такое не делал)

Пример в wiki: https://gcc.gnu.org/wiki/FunctionMultiVersioning

Из недостатков, clang такое не поддерживает и надо обкладывать все через #ifdef/#define

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

Не подскажите, где нибудь есть мануал, как собрать под windows? В нескольких проектах у меня как раз нужен быстрый резайс.

Не пробовал собирать последние версии, но ранние собирал, там флаги те же, функции те же. Так что смотрите папку winbuild оригинального репозитория.

Если что-то не компилируется, дайте знать в issue pillow-simd. Чаще всего майкросовтовскому компилятору не нравится порядок объявления переменных.

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

Почему-то сразу вспоминаю про сервера, которые майнят биткоин.
Круто, поздравляю! С нетерпением жду продолжения!
Однако было бы корректнее привести результаты замеров времени не на единственной системе MacBook Pro, а на нескольких разных компьютерах.
Кроме того, однажды был у меня с оптимизацией такой случай: качество ухудшилось по сравнению с референсной реализацией. Вы бы показали/рассказали, что сравнение с результатами ImageMagic/OpenCV совпадает бит в бит или визуально совпадает, но численно отличается не более чем 1 градацию для 8 bpp, и т.п.
Однако было бы корректнее привести результаты замеров времени не на единственной системе MacBook Pro, а на нескольких разных компьютерах.

Там как минимум три системы. Если у кого-то есть возможность, прошу протестировать на процессорах AMD. Я не смог найти ни одного живого хостинга на этом процессоре.


Вы бы показали/рассказали, что сравнение с результатами ImageMagic/OpenCV совпадает бит в бит

Планирую добавить какую-то метрику отклонения, да.

Попробовал прогнать на домашней машине. Тикет завёл, как просили.

Готово!


В целом все довольно предсказуемо и мало отличается от Интела. Заметно, что ImageMagick и Pillow 2.6 (до всех оптимизаций) тормозят сильнее, чем на Интеловских процессорах. Скорее всего это сказано с ложной зависимостью по данным, о которой будет сказано с следующей статье, и которая была исправлена в GCC 4.9. Если же у вас GCC 4.9 или старше, то можно предположить, что фикс, который устраняет зависимость по данным на Интелах, не делает этого на AMD, что конечно очень печально.


Так же примечательно, что при повороте на 90° и 270° в Pillow 2.6 должно быть сильное проседание из-за неэффективного использования кеша, но его нет. Точнее оно совсем слабое. Такое же слабое проседание видно на серверных процессорах, но у них кеш по 20 мегабайт, они могут себе это позволить. А вот у вашего процессора всего 2 мегабайта на ядро, поэтому я не до конца понимаю, как ему это удается.

Версия gcc 6.3.1, крайне свежая. Если нужно еще что проверить(например с другой версией gcc) или еще какая-то информация — обращайтесь.
UFO just landed and posted this here

В статье нет таких слов.

а с чего вдруг такой пафос?
быстрый — суждение оценочное, на надо быть самым быстрым, это ни к чему, нужно быть достаточно быстрым чтобы решать поставленными перед языком задачами, и Python с ними справляется, последнее время все успешнее и успешнее, а если нет, то существует возможность добавить ему скорости и весьма существенно, иными словами он для своих задач вполне хорош, так как если к вам в руки попал молоток не все превратилось в гвозди
Теперь можно так же заняться ускорением экспорта в Jpeg. Например здесь было такое решение несколько лет назад https://habrahabr.ru/post/139970/, но это с CUDA.

Возможно я чего-то не понял, но я взял opencv и просто вызвал resize:
2560 x 1600 --> 320 x 200 bicubic = 3 ms
А у вас SIMD SSE4 — 7.7 ms, а SIMD AVX2 — 5.7 ms, то есть в 2-2.5 раза медленнее.

Судя по всему opencv не делает нормальный resampling после bicubic, сравните результат после opencv и после pillow bicubuc тут https://python-pillow.org/pillow-perf/#resampling. Там же описаны причины почему нету opencv в этом бенчмарке.
К тому же приводить свои результаты только одной либы и сравнивать с результатами автора это дурной тон, у вас с автором разное железо и разные условия тестирования.
Возможное объяснение здесь: «In these benchmarks, we measure the throughput on single CPU core, not minimum achievable execution time.» Т.е., условно говоря, автор делает измерения в одном потоке, а OpenCV на вашем компьютере решает задачу в 4 потока.

Дело в том, что в opencv для ресайза не используется метод сверток. Там используется тот же метод, что и в Pillow до версии 2.7 для bilinear и bicubic и тот же метод, что используется в элементе canvas в браузерах. Отсылаю вас почитать статью Ресайз картинок в браузере. Все очень плохо. Отличие минимальное — при уменьшении изображения, окно не увеличивается на коэффициент уменьшения, как в свертках. Но это радикально влияет на качество и скорость. Для примера я возьму ту же картинку, что в статье (7000 × 2926 → 512 × 214).


Вот код:


im = cv2.imread('pixar.jpg')
im = cv2.resize(im, (512, 214), interpolation=cv2.INTER_CUBIC)
cv2.imwrite('pixar.cv2.png', im)

Вот результат:


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

Спасибо вам за познавательную, и главное, понятную статью) Метод изложен очень доходчиво и наглядно, очень классно написано.

Я был бы, однако, очень благодарен, если бы вы подсказали мне какую-то ссылку, где так же понятно описывается принцип упрощения свёртки до двух проходов. Потому что на данный момент мне не ясно, как это может давать идентичный результат (мы ведь по сути не можем при таком сценарии учесть влияние диагональных пикселей из окна фильтра).
Каждый пиксель после горизонтальной свертки становится комбинацией нескольких соседних пикселей по горизонтали. Последующий проход вертикальной свертки комбинирует между собой уже не пиксели исходного изображения, а эти «комбинации по несколько пикселей». Скажем в фильтре 5x5 если нас интересует угол (-2, 2) то на этапе горизонтальной свертки мы посчитаем пиксель p_horizontal[2] = weighted_sum( [-2,2], [-1,2], [0,2], [1,2], [2,2] ) а затем подставим в p = weighted_sum(p_horizontal[-2], [-1], [0], [1], [2]); несложно видеть что в эту сумму войдет и [-2.2] с каким-то весом.

Это работает не для всех возможных 2D сверток, а только для специального их подкласса называемого свертками с «сепарабельными» ядрами. К счастью для ресайза почти все ядра сепарабельные.

Но вообще статья нужна, да. Там много всего интересного.
Спасибо, кажется понял)

При вертикальной свёртке мы «добавим» пиксели из вертикальной «линейки» с нужными коэффициентами, в каждом из которых уже просуммирована его горизонтальная «линейка». И плюс «своя» линейка уже учтена. Да, всё супер.

Единственное что — если докопаться до коэффициентов, там может оказаться некоторый косяк.

Например, я хочу взять фильтр 3х3 для простоты, и крайние пиксели взять с коэффициентом 0.4. Тогда после горизонтальной свёртки мы добавим 0.4 для крайних элементов в нижней линейке, а при вертикальной мы добавим уже 0.4*0.4=0.16, то есть квадрат коэффициента у нас будет. Тогда как логичнее, наверное, использовать квадратный корень (типа пропорционально расстоянию). Если фильтр не билинейный, то там конечно расстоянию вовсе не пропорционально, но я к тому, что всё равно коэффициенты этой матрицы надо бы пересчитать при таком методе будет :)
И ещё касаемо экономии ресурсов хотел бы высказать мнение: даже если опустить тот момент, что такое большое разрешение обычно не нужно, и призвано оно когда-то было скорее сгладить визуально недостатки изображения в случае плохой камеры (для современных моделей смартфонов уже не особо актуально) — всё равно более рационально сжимать изображение до загрузки, а не после. Таким образом, мы экономим и ресурсы сервера, и сетевой канал (который, кстати, не всегда достаточно хороший, чтобы быстро загружать фотографии по 12 мегапикселей).

Я понимаю, что ваш сервис рассчитан на браузеры. Но может, это повод разработчикам современных мобильных браузеров задуматься об API для ресайза картинок? Или, как альтернативный вариант — попытаться использовать Canvas и JavaScript (и кто-то, кажется, на Хабре уже писал про оптимизацию ресайза ровно для тех же целей, разгрузить бэкенд и ускорить загрузку картинок в облако).
О, классно)) Первую я читал, и даже что-то запомнилось, как видите.
Сделайте еще с фильтром на основе функции smoothstep

Осталось только найти открытые кодеки для PNG, WEBP и JPEG на GPU. Потому что иначе еще нужно распакованные данные туда-сюда гонять, а сделать это быстрее скорости чтения памяти невозможно. А скорость ресайза с AVX2, запущенного ядрах так на восьми, уже больше скорости чтения памяти. И откуда тогда возьмется ускорение на порядок?

Интересно, к какому слову по вашему мнению относится материал по ссылке. К «открытые» или к «кодеки PNG, WEBP»?

Вот этот результат:
Scale 2560×1600 RGB image
to 5478x3424 lzs 2.04901 s 2.00 Mpx/s

Он Uncompress -> Resize -> Compress? Или просто Resize?
Ну и 8 ядер с AVX2 это уже процессоры для энтузиастов стоимостью >1000$
Он Uncompress -> Resize -> Compress? Или просто Resize?
Из статьи:
Ресемплинг производится над массивом 8-битных RGB пикселей в память, без учета декодирования и кодирования изображений, однако с учетом выделения памяти под конечное изображение и с учетом подготовки коэффициентов, необходимых для конкретной операции.

Ну и 8 ядер с AVX2 это уже процессоры для энтузиастов
Это почти любой серверный процессор.
Это называется метод ближайшего соседа. Картинка получается грубой, рваной, неприятной. Так происходит потому, что в конечном изображении была использована очень малая часть исходных пикселей

Быть может из-за эффекта наложения спектров?
Для уменьшения изображения — фильтруем сигнал ФНЧ, а потом прореживаем
Для увеличения — разбавляем сигнал нулями и потом фильтруем ФНЧ
Частота среза ФНЧ = ширине спектра сигнала после уменьшения / до увеличения
Свертка технически это и есть КИХ фильтр, АЧХ которого определяют коэффициенты свертки.

Если честно, я не понял ваш вопрос )


Я для примера описал самый примитивный способ ресайза — метод «ближайшего соседа». Что будет если фильтровать, прореживать, разбавлять, как вы предлагаете? Ну, очевидно будет какой-то другой метод, метод «не ближайшего соседа». Методов-то на самом деле больше двух, просто мне интересны именно свертки, потому что сейчас это самый распространенный метод качественного ресайза.

mkarev говорит о математике стоящей за ресайзом свертками. Ресайз сверткой и конкретные ядра использующиеся для нее — это просто практическая реализация некоторых принципов общей теории цифрового представления сигналов. В её рамках объяснение о «очень малом числе исходных пикселей» не релевантно; можно в частности легко сделать очень паршивый ресайз сверткой который будет давать, гм, своеобразную картинку используя при этом все пиксели изображения. А реальная проблема — это наложение спектра, «алиасинг» при дискретизации сигнала. Но в общем это тема отдельной статьи, математику ресайза очень мало кто знает и понимает.

Большое спасибо. Замечательный проект и замечательный пост. Пара вопросов.


[1] cv2 умеет делать resize для картинок с большим количеством каналов > 3. PIL-SIMD так умеет или надо разрезать на куски по 3 слоя, менять размер, и склеивать обратно? (В задаче про спутниковые снимки на Kaggle приходится работать вплоть до 20 каналов.)


[2] cv2 автоматически распараллеливает операцию resize. PIL-SIMD так умеет?


[3] Рабоче-крестьянский вопрос — если забить на производительность, какой тип сверток субъективно обеспечивает наилучшее качество? Cubic?

Все же правильно Pillow-SIMD. PIL давно мертв.


  1. Максимум 4 восьмибитных канала на изображение. Есть режимы 1 восьмибитный канал, 1 float и 1 канал 32-битный int, но ни один из них сейчас не ускорен SIMD.


  2. По разным ядрам сейчас нет распараллеливания. Но даже из Питона распараллелить по потокам довольно просто. Дело в том, что Сишный код отпускает GIL и возможна работа в несколько потоков. Кроме того, если готовы вложить время, то есть очень старый пулреквест, в котором пробовали прикрутить OpenMP. Он на удивление просто прикручивается для GCC. Как и в любом опенсорсе, что-то делается если кому-то это нужно. В моем приложении, если честно, параллельная обработка не сильно нужна, я гонюсь за максимальным throughput.


  3. Субъективно? Мне бикубик больше нравится. Ланцош более резкий благодаря более глубоким отрицательным долям. Иногда эта резкость идет в плюс, иногда подпорчивает результат. А бикубик очень стабильный результат дает.

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

Совершенно верно, ни одна из библиотек не делает гамма-коррекции во время ресайза, потому что:


  • Гамма-коррекция и ресайз — не связанные операции. В идеале ресайз должен проходить в линейном цветовом пространстве, поэтому ресайз не делает никаких преобразований. Все преобразования должны быть до и после.
  • Вы правильно написали «почти все изображения». Главное слово тут «почти». И нужно делать преобразование не из sRGB в линейный и обратно, а полноценный менеджмент цветов используя тот цветовой профиль, который прикреплен к изображению.
  • Гамма коррекция не является операцией преобразования даже из sRGB в линейный и обратно. Не говоря о других цветовых пространствах.

Так что этот вопрос не связан с ресайзом напрямую.

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

На самом деле для преобразования 8 bit sRGBlinear RGB8 bit sRGB без потерь не нужен float, достаточно 12 бит. С 16 битами уже можно почти без потерь совместить premultiplied alpha и линейны RGB, там все ошибки будут при alpha < 12 (из 255).


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

На скорость масштабирование может влияет не только скорость вычислений. Но и как часто вы промахиваетесь мимо кэша и предсказаний brunch prediction. Попробуйте разбить изображение на блоки разных размеров и сравнить скорости. Более того блоки получаются независимыми и легко параллелятся на разные ядра процессора.


Уверяю вас что "матан не закончен". При уменьшении изображений есть свои тонкости вот попробуйте свой алгоритм на такой картинке
image


Мой компьютер не поддерживает ни AVX2 ни AVX512, но это не мешает использовать CUDA.


Так выглядит результат афинных преобразований вообще без фильтров

image 50% 0°
image 50% 15°
image 50% 30°
image 62.5% 45°


image /8 0°
image /8 7°
image *5.5 15°

Уверяю вас что "матан не закончен".

Смотрите ответ выше.


Мой компьютер не поддерживает ни AVX2 ни AVX512, но это не мешает использовать CUDA.

Так используйте, я не против. А SSE4 тоже не поддерживает?

У меня MIPS SIMD в тв-приставке и ARM NEON в телефоне и планшете, в одном из ноутов SSE4+FMA4, в другом SSE4.2+AVX, даже есть один с SSE2.

У меня есть небольшая собственная библиотечка по обработке изображений. Тоже есть оптимизации под практически все расширения x86, Arm, PowerPC. В ней есть функция SimdResizeBilinear. Конечно сравнивать напряму нельзя. Но при ресайзе изображения (1920х1080 -> 1728x972) получается для одного потока: (Gray-8 — 0.426 мс, BGR-24 — 2.477 мс, BGRA-32 — 2.063 мс).

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

bilinear это линейная интерполяция по 4 соседям (см. SimdBaseResizeBilinear.cpp)
А при увеличении хороший результат bspline для фоток и точный вариант по площадям для сканов текстов, чертежей и карт. Билинейное при увеличении даёт не естественное размытие.

homm, скажите, вы сравнивали точность вычислений для своего подхода и приведённых выше библиотек (ImageMagic, IPP, Skia)?
Вы считаете промежуточные вычисления через float для Bicubic и Lanczos интерполяций?
вы сравнивали точность вычислений для своего подхода

https://github.com/uploadcare/pillow-simd/blob/simd/3.4.x/Tests/test_image_resample.py


и приведённых выше библиотек (ImageMagic, IPP, Skia)

Нет.


Вы считаете промежуточные вычисления через float для Bicubic и Lanczos интерполяций?

Нет.

UFO just landed and posted this here
результат практически идентичен

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

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


from PIL import Image
im = Image.open('pixar.jpg')
im.resize((1024, 428), Image.NEAREST).resize((512, 214), Image.BICUBIC).save('pixar.2x.bic.png')


По скорости очень впечатляет:


In [3]: %timeit im.resize((1024, 428), Image.NEAREST).resize((512, 214), Image.BICUBIC)
100 loops, best of 3: 2.99 ms per loop

In [4]: %timeit im.resize((512, 214), Image.BICUBIC)
10 loops, best of 3: 31.1 ms per loop
UFO just landed and posted this here

Визуальные алгоритмы сложно оценивать объективно. Но вот еще материал для субъективного сравнения:


Image.NEAREST (0.5 ms)  cv2.INTER_CUBIC (2.6 ms)
ваш метод (1.66 ms)     Image.BICUBIC (40 ms)



Это сильное уменьшение этого изображения. Как видите, по соотношению цена/качество очень хорошо. Еще ваш алгоритм можно тюнить, изначально уменьшая в другое число раз.

UFO just landed and posted this here
UFO just landed and posted this here
Это очень похоже на трюк с draft для jpeg.
Sign up to leave a comment.

Articles