Comments 67
Что меня радует: Это не кусок кода, опубликованный на Stack Overflow. Это одна строчка импорта библиотеки
99% хабра это не радует, а однозначно огорчает.
А разве эти флаги влияют на доступные интринсики?
Но могут быть проблемы с LTO, если использовать одно имя функции с разными реализациями и атрибутами target.
Без lto, должно быть все хорошо. Либо можно обойти проблему использую функции с уникальными именами и собственную реализацию выбора функции на основе поддержки разных видов simd (я пока такое не делал)
Пример в wiki: https://gcc.gnu.org/wiki/FunctionMultiVersioning
Из недостатков, clang такое не поддерживает и надо обкладывать все через #ifdef/#define
Не пробовал собирать последние версии, но ранние собирал, там флаги те же, функции те же. Так что смотрите папку winbuild оригинального репозитория.
Если бы такой эффект удалось экстраполировать до масштабов планеты и заменить весь код, занимающийся ресайзом картинок, на более эффективный, польза была бы огромной. Десятки тысяч сэкономленных серверов, сотни киловатт электричества. А это уже одна миллионная от мирового потребления. Да можно было бы спасти планету!
Почему-то сразу вспоминаю про сервера, которые майнят биткоин.
Однако было бы корректнее привести результаты замеров времени не на единственной системе 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 мегабайта на ядро, поэтому я не до конца понимаю, как ему это удается.
В статье нет таких слов.
быстрый — суждение оценочное, на надо быть самым быстрым, это ни к чему, нужно быть достаточно быстрым чтобы решать поставленными перед языком задачами, и Python с ними справляется, последнее время все успешнее и успешнее, а если нет, то существует возможность добавить ему скорости и весьма существенно, иными словами он для своих задач вполне хорош, так как если к вам в руки попал молоток не все превратилось в гвозди
Возможно я чего-то не понял, но я взял 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 для ресайза не используется метод сверток. Там используется тот же метод, что и в 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. В зависимости от задачи вы можете рассматривать такой алгоритм или считать его недопустимым.
Я был бы, однако, очень благодарен, если бы вы подсказали мне какую-то ссылку, где так же понятно описывается принцип упрощения свёртки до двух проходов. Потому что на данный момент мне не ясно, как это может давать идентичный результат (мы ведь по сути не можем при таком сценарии учесть влияние диагональных пикселей из окна фильтра).
Это работает не для всех возможных 2D сверток, а только для специального их подкласса называемого свертками с «сепарабельными» ядрами. К счастью для ресайза почти все ядра сепарабельные.
Но вообще статья нужна, да. Там много всего интересного.
При вертикальной свёртке мы «добавим» пиксели из вертикальной «линейки» с нужными коэффициентами, в каждом из которых уже просуммирована его горизонтальная «линейка». И плюс «своя» линейка уже учтена. Да, всё супер.
Единственное что — если докопаться до коэффициентов, там может оказаться некоторый косяк.
Например, я хочу взять фильтр 3х3 для простоты, и крайние пиксели взять с коэффициентом 0.4. Тогда после горизонтальной свёртки мы добавим 0.4 для крайних элементов в нижней линейке, а при вертикальной мы добавим уже 0.4*0.4=0.16, то есть квадрат коэффициента у нас будет. Тогда как логичнее, наверное, использовать квадратный корень (типа пропорционально расстоянию). Если фильтр не билинейный, то там конечно расстоянию вовсе не пропорционально, но я к тому, что всё равно коэффициенты этой матрицы надо бы пересчитать при таком методе будет :)
Я понимаю, что ваш сервис рассчитан на браузеры. Но может, это повод разработчикам современных мобильных браузеров задуматься об API для ресайза картинок? Или, как альтернативный вариант — попытаться использовать Canvas и JavaScript (и кто-то, кажется, на Хабре уже писал про оптимизацию ресайза ровно для тех же целей, разгрузить бэкенд и ускорить загрузку картинок в облако).
кто-то, кажется, на Хабре уже писал про оптимизацию ресайза ровно для тех же целей
Это был я
Ресайз картинок в браузере. Все очень плохо
Ресайз картинок в браузере. Все может стать еще хуже
Осталось только найти открытые кодеки для PNG, WEBP и JPEG на GPU. Потому что иначе еще нужно распакованные данные туда-сюда гонять, а сделать это быстрее скорости чтения памяти невозможно. А скорость ресайза с AVX2, запущенного ядрах так на восьми, уже больше скорости чтения памяти. И откуда тогда возьмется ускорение на порядок?
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 это уже процессоры для энтузиастовЭто почти любой серверный процессор.
Это называется метод ближайшего соседа. Картинка получается грубой, рваной, неприятной. Так происходит потому, что в конечном изображении была использована очень малая часть исходных пикселей
Быть может из-за эффекта наложения спектров?
Для уменьшения изображения — фильтруем сигнал ФНЧ, а потом прореживаем
Для увеличения — разбавляем сигнал нулями и потом фильтруем ФНЧ
Частота среза ФНЧ = ширине спектра сигнала после уменьшения / до увеличения
Свертка технически это и есть КИХ фильтр, АЧХ которого определяют коэффициенты свертки.
Если честно, я не понял ваш вопрос )
Я для примера описал самый примитивный способ ресайза — метод «ближайшего соседа». Что будет если фильтровать, прореживать, разбавлять, как вы предлагаете? Ну, очевидно будет какой-то другой метод, метод «не ближайшего соседа». Методов-то на самом деле больше двух, просто мне интересны именно свертки, потому что сейчас это самый распространенный метод качественного ресайза.
Большое спасибо. Замечательный проект и замечательный пост. Пара вопросов.
[1] cv2 умеет делать resize для картинок с большим количеством каналов > 3. PIL-SIMD так умеет или надо разрезать на куски по 3 слоя, менять размер, и склеивать обратно? (В задаче про спутниковые снимки на Kaggle приходится работать вплоть до 20 каналов.)
[2] cv2 автоматически распараллеливает операцию resize. PIL-SIMD так умеет?
[3] Рабоче-крестьянский вопрос — если забить на производительность, какой тип сверток субъективно обеспечивает наилучшее качество? Cubic?
Все же правильно Pillow-SIMD. PIL давно мертв.
Максимум 4 восьмибитных канала на изображение. Есть режимы 1 восьмибитный канал, 1 float и 1 канал 32-битный int, но ни один из них сейчас не ускорен SIMD.
По разным ядрам сейчас нет распараллеливания. Но даже из Питона распараллелить по потокам довольно просто. Дело в том, что Сишный код отпускает GIL и возможна работа в несколько потоков. Кроме того, если готовы вложить время, то есть очень старый пулреквест, в котором пробовали прикрутить OpenMP. Он на удивление просто прикручивается для GCC. Как и в любом опенсорсе, что-то делается если кому-то это нужно. В моем приложении, если честно, параллельная обработка не сильно нужна, я гонюсь за максимальным throughput.
- Субъективно? Мне бикубик больше нравится. Ланцош более резкий благодаря более глубоким отрицательным долям. Иногда эта резкость идет в плюс, иногда подпорчивает результат. А бикубик очень стабильный результат дает.
На сколько я понимаю, ни одна из библиотек не учитывает, что почти все изображения сохранены в пространстве sRGB, и не делают для них гамма коррекцию перед сверткой и после. Соответственно, изображения выглядят темнее, чем должны.
Совершенно верно, ни одна из библиотек не делает гамма-коррекции во время ресайза, потому что:
- Гамма-коррекция и ресайз — не связанные операции. В идеале ресайз должен проходить в линейном цветовом пространстве, поэтому ресайз не делает никаких преобразований. Все преобразования должны быть до и после.
- Вы правильно написали «почти все изображения». Главное слово тут «почти». И нужно делать преобразование не из sRGB в линейный и обратно, а полноценный менеджмент цветов используя тот цветовой профиль, который прикреплен к изображению.
- Гамма коррекция не является операцией преобразования даже из sRGB в линейный и обратно. Не говоря о других цветовых пространствах.
Так что этот вопрос не связан с ресайзом напрямую.
Если делать ресайз флотовых данных, то можно и не учитывать, а если конвертить во флоат не хочется или не можется, и входные и выходные данные в 8bpc, то придется. Иначе будут огромные потери в точности.
На самом деле для преобразования 8 bit sRGB
→ linear RGB
→ 8 bit sRGB
без потерь не нужен float, достаточно 12 бит. С 16 битами уже можно почти без потерь совместить premultiplied alpha и линейны RGB, там все ошибки будут при alpha < 12 (из 255).
16-битный режим в Pillow очень хотелось бы (и ресайз в 16 битах для этого далеко не самое сложное), но пока никто этим не занимался.
На скорость масштабирование может влияет не только скорость вычислений. Но и как часто вы промахиваетесь мимо кэша и предсказаний brunch prediction. Попробуйте разбить изображение на блоки разных размеров и сравнить скорости. Более того блоки получаются независимыми и легко параллелятся на разные ядра процессора.
Уверяю вас что "матан не закончен". При уменьшении изображений есть свои тонкости вот попробуйте свой алгоритм на такой картинке
Мой компьютер не поддерживает ни AVX2 ни AVX512, но это не мешает использовать CUDA.
50% 0°
50% 15°
50% 30°
62.5% 45°
/8 0°
/8 7°
*5.5 15°
Уверяю вас что "матан не закончен".
Смотрите ответ выше.
Мой компьютер не поддерживает ни AVX2 ни AVX512, но это не мешает использовать CUDA.
Так используйте, я не против. А SSE4 тоже не поддерживает?
Я правильно понимаю, что это bilinear все с тем же фиксированным окном, т.е. дающий хороший результат только при увеличении? Не смог до конца разобраться в коде.
вы сравнивали точность вычислений для своего подхода
https://github.com/uploadcare/pillow-simd/blob/simd/3.4.x/Tests/test_image_resample.py
и приведённых выше библиотек (ImageMagic, IPP, Skia)
Нет.
Вы считаете промежуточные вычисления через float для Bicubic и Lanczos интерполяций?
Нет.
результат практически идентичен
Это конечно неправда. Вы могли посмотреть какую-то конкретную картинку и не увидеть различий, но в общем случае различия очень существенные.
Слушайте, я попробовал и это гениально на самом деле. Понятно, что для качественного ресайза не годится и относится к «трюкам и оптимизациям частных случаев». Но для супердешевого ресайза для какой-нибудь последующей обработки самое то.
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
Визуальные алгоритмы сложно оценивать объективно. Но вот еще материал для субъективного сравнения:
Image.NEAREST (0.5 ms) cv2.INTER_CUBIC (2.6 ms)
ваш метод (1.66 ms) Image.BICUBIC (40 ms)
Это сильное уменьшение этого изображения. Как видите, по соотношению цена/качество очень хорошо. Еще ваш алгоритм можно тюнить, изначально уменьшая в другое число раз.
Опубликовал следующую часть.
Как я сделал самый быстрый ресайз изображений. Часть 0