Например: Программист
0,2
рейтинг
4 марта 2015 в 16:52

Разработка → Ресайз картинок в браузере. Все очень плохо

Если вы когда-нибудь сталкивались с задачей ресайза картинок в браузере, то вы наверное знаете, что это очень просто. В любом современном браузере есть такой элемент, как холст (<canvas>). На него можно нанести изображение нужных размеров. Пять строчек кода и картинка готова:

function resize(img, w, h) {
  var canvas = document.createElement('canvas');
  canvas.width = w;
  canvas.height = h;
  canvas.getContext('2d').drawImage(img, 0, 0, w, h);
  return canvas;
}

Из холста картинку можно сохранить в JPEG и, например, отправить на сервер. Можно было на этом закончить статью, но сперва давайте взглянем на результат. Если вы поставите рядом такой холст и обычный элемент <img>, в который загружена та же картинка (исходник, 4 Мб), то вы увидите разницу.

img

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

Конечно же картинку в таком виде нельзя показывать приличным людям. И не удивительно, что вопрос качества ресайза с помощью холста часто задается на stackoverflow. Самый распространенный совет — уменьшать картинку в несколько шагов. И действительно, если сильное уменьшение изображения захватывает не все пиксели, то почему бы не уменьшить изображение несильно. А потом еще раз и еще раз, пока не получим желаемый размер. Как в этом примере.

Бесспорно, этот метод дает намного лучший результат, ведь все точки исходного изображения учитываются в конечном. Другой вопрос, как именно они учитываются. Это уже зависит от размера шага, размера начального и размера конечного изображения. Например, если взять размер шага ровно 2, эти уменьшения будут эквивалентны суперсемплингу. А вот последний шаг — как повезет. Если совсем повезет, то последний шаг тоже будет равен 2. Но может совсем не повезти, когда на последнем шаге изображение нужно будет уменьшить на один пиксель, и картинка получится мыльная. Сравните, отличие размера всего в один пиксель, а какова разница (исходник, 4 Мб):

img

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

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

function resizeImage(image, width, height) {
  var cIn = document.createElement('canvas');
  cIn.width = image.width;
  cIn.height = image.height;
  var ctxIn = cIn.getContext('2d');

  ctxIn.drawImage(image, 0, 0);
  var dataIn = ctxIn.getImageData(0, 0, image.width, image.heigth);
  var dataOut = ctxIn.createImageData(width, heigth);
  resizePixels(dataIn, dataOut);

  var cOut = document.createElement('canvas');
  cOut.width = width;
  cOut.height = height;
  cOut.getContext('2d').putImageData(dataOut, 0, 0);
  return cOut;
}

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

Давайте поговорим, для чего вообще может понадобиться ресайз на клиенте. У меня была задача уменьшить размер выбранных фотографий перед отправкой на сервер, таким образом экономя трафик пользователя. Это наиболее актуально на мобильных устройствах с медленным соединением и платным трафиком. А какие фотографии чаще всего загружают на таких устройствах? Снятые на камеры этих мобильных устройств. Разрешение камеры, например, Айфона — 8 мегапикселей. Но с помощью неё можно снять панораму в 25 мегапикселей (на iPhone 6 даже больше). На Андроидах и Виндусфонах разрешения камер бывают еще выше. И тут мы сталкиваемся с ограничениями этих мобильных устройств. К сожалению, в iOS нельзя создать холст больше 5 мегапикселей.

Эпл можно понять, им приходится следить за нормальной работой своих устройств с ограниченными ресурсами. В самом деле, в представленной выше функции вся картинка будет занимать память три раза! Один раз — буфер, связанный с объектом Image, куда распаковывается изображение, второй раз — пиксели холста, и третий — типизированный массив в ImageData. Для картинки в 8 мегапикселей понадобится 8 × 3 × 4 = 96 мегабайт памяти, для 25 мегапикселей — 300.

Но в процессе тестирования я сталкивался с проблемами не только в iOS. Хром на Маке с некоторой вероятностью начинал рисовать вместо одного большого изображения несколько маленьких, а под Виндой просто выдавал белый лист.

Но раз нельзя получить все пиксели сразу, может можно получить их по частям? Можно подгружать картинку в холст по кускам, ширина которых равна ширине исходного изображения, а высота намного меньше. Сначала подгружаем первые 5 мегапикселей, потом еще, потом сколько останется. Или даже по 2 мегапикселя, что еще более сократит использование памяти. К счастью, в отличие от двухпроходного ресайза свертками, метод ресайза суперсемплингом однопроходный. Т.е. можно не только получать изображение порциями, но и отдавать на обработку одну порцию за раз. Память понадобится только под элемент Image, холст (например, 2 мегапикселя) и типизированный массив. Т.е. для картинки 8 мегапикселей (8 + 2 + 2) × 4 = 48 мегабайт, что в 2 раза меньше.

Я реализовал описанный выше подход и замерил время выполнения каждой части. Самому протестировать можно здесь. Вот что получилось у меня для картинки разрешением 10800×2332 пикселей (панорама с Айфона).
Браузер Safari 8 Chrome 40 Firefox 35 IE 11
Image load 24 ms 27 28 76
Draw to canvas 1 348 278 387
Get image data 304 299 165 320
JS Resize 233 135 138 414
Put data back 1 1 3 5
Get image blob 10 16 21 19
Total 576 833 641 1243

Это очень интересная таблица, давайте остановимся на ней подробно. Отличная новость в том, что сам ресайз на яваскрипте не является узким местом. Да, в Сафари он в 1,7 раз медленнее, чем в Хроме и Фаерфоксе, а в IE в 3 раза медленнее, но во всех браузерах время на загрузку картинки и получение данных все равно больше.

Второй примечательный момент — ни в одном браузере картинка не декодируется к событию image.onload. Декодирование откладывается на момент, когда это действительно необходимо — отображение на экране или вывод на холст. А в Сафари изображение не декодируется, даже когда нанесено на холст, ведь холст также не отображается на экране. А декодируется только когда пиксели извлекаются из холста.

В таблице приведено общее время рисования и получения данных, тогда как на самом деле эти операции делаются для каждых 2-х мегапикселей, и скрипт по приведенной выше ссылке выводит время каждой итерации отдельно. И если рассматривать эти показатели, можно увидеть, что несмотря на то, что общее время получения данных для Сафари, Хрома и IE примерно одинаково, в Сафари почти все время занимает только первый вызов, в котором и происходит декодирование картинки, тогда как в Хроме и IE время одинаково для всех вызовов и говорит об общей тормознутости получения данных. То же самое касается и Фаерфокса, но в меньшей степени.

Пока что такой подход выглядит перспективным. Давайте протестируем на мобильных устройствах. Под рукой у меня оказались iPhone 4s (i4s), iPhone 5 (i5), Meizu MX4 Pro (A) и я попросил Олега Корсунского протестировать на Windows Phone, у него оказался HTC 8x (W).
Браузер Safari i4s Safari i5 Chrome i4s Chrome A Chrome A Firefox A IE W
Image load 517 ms 137 650 267 220 81 437
Draw to canvas 2 706 959 2 725 1 108 6 954 1 007 1 019
Get image data 678 250 734 373 543 406 1 783
JS Resize 2 939 1 110 96 320 491 458 418 2 299
Put data back 9 5 315 6 4 14 24
Get image blob 98 46 187 37 41 80 33
Total 6 985 2 524 101 002 2 314 8 242 2 041 5 700

Первое, что бросается в глаза — «выдающийся» результат Хрома на iOS. Действительно, до недавнего времени в iOS все сторонние браузеры могли работать только с версией движка без jit-компиляции. В iOS 8 появилась возможность использовать jit, но Хром еще не успели адаптировать.

Другая странность — два результата у Хрома на Андроиде, радикально отличающиеся временем рисования и почти идентичные во всем остальном. Это не ошибка в таблице, Хром действительно может вести себя по-разному. Я уже говорил, что браузеры загружают картинки лениво, в момент, когда посчитают нужным. Так вот, ничего не мешает браузеру освобождать память, занятую картинкой, когда он считает, что картинка больше не нужна. Естественно, когда картинка снова понадобится при следующем рисовании на холсте, придется снова её декодировать. В данном случае картинка декодировалась 7 раз. Это хорошо видно по времени рисования отдельных чанков (напомню, в таблице только суммарное время). В таких условиях время декодирования становится непредсказуемым.

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

На этом месте я решил плюнуть на это дело. Был совсем сумасшедший вариант не только ресайзить, но и декодировать jpeg на клиенте. Минусы: только jpeg, плохое время Хрома под iOS еще сильнее ухудшится. Плюсы: предсказуемость в Хроме под Андроидом, нет лимитов на размер, нужно меньше памяти (нет бесконечного копирования на холст и обратно). На этот вариант я не решился, хотя и существует декодер jpeg на чистом javascript.

Часть 2. Вернемся к началу


Помните, как в самом начале мы получили хороший результат при последовательном уменьшении в 2 раза в лучшем случае, и мыльный — в худшем? А что, если попытаться избавиться от худшего варианта, не слишком изменив подход? Напомню, что мыло получается, если на последнем шаге нужно уменьшить картинку на совсем чуть-чуть. Что если последний шаг сделать первым, уменьшая сначала в какое-то неопределенное число раз, а потом только строго в 2 раза? Попутно надо учесть, чтобы первый шаг был не больше 5 мегапикселей по площади и 4096 пикселей по любой ширине. В таком варианте и код получается явно проще, чем ручной ресайз.

img

Слева изображение, уменьшенное за 4 шага, справа за 5, а разницы почти нет. Почти победа. К сожалению, разница между двумя и тремя шагами (не говоря о разнице между одним и двумя шагами) все равно видна достаточно сильно:

img

Хотя мыла и значительно меньше, чем было в самом начале. Я бы даже сказал, что изображение справа (полученное за 3 шага) выглядит немного приятнее левого, которое слишком резкое.

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

Часть 3. Много фоточек подряд


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

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

Тут я хочу сделать лирическое отступление про мобильный Сафари 8 (у меня нет данных по другим версиям). В нем выбор фотки в инпут тормозит браузер на пару секунд. Это связано либо с тем, что Сафари создает копию фотки с обрезанным EXIF, либо с тем, что он генерирует маленькую превьюшку, которая отображается непосредственно внутри инпута. Если для одной фотки это терпимо и даже, можно сказать, незаметно, то для множественного выбора это может превратиться в ад (зависит от количества выбранных фоток). И все это время страница остается не в курсе, что фотки выбраны, как и не в курсе, что вообще открыт диалог выбора файлов.

Засучив рукава я открыл страничку на айфоне и выбрал 20 фоток. Немного подумав, Сафари радостно отрапортовал: A problem occurred with this webpage so it was reloaded. Вторая попытка — тот же результат. В этом месте я вам завидую, дорогие читатели, потому что для вас следующий абзац пролетит за минуту, тогда как для меня это была ночь боли и страданий.

Итак, Сафари вылетает. Отладить его с помощью инструментов разработчика не представляется возможным — там нет ничего про расход памяти. Я с надеждой открыл страницу в iOS симуляторе — не падает. Глянул в Activity Monitor — о, а память-то растет с каждой картинкой и не освобождается. Ну хоть что-то. Стал экспериментировать. Чтобы вы понимали, что такое эксперимент в симуляторе: увидеть утечку памяти на одной картинке невозможно. На 4-5 затруднительно. Лучше всего брать штук 20. Перетащить или выбрать их с "шифтом" нельзя, нужно 20 раз кликнуть. После того как выбрал, надо смотреть в диспетчер задач и гадать: уменьшение расхода памяти на 50 мегабайт — это случайные флуктуации, или я что-то сделал правильно.

В общем, после большого количества проб и ошибок я пришел к простому, но очень важному выводу: за собой нужно все освобождать. Как можно раньше, любыми доступными способами. А выделять как можно позже. Полагаться на сборку мусора нельзя совершенно. Если создается холст, в конце его нужно занулить (сделать размером 1×1 пиксель), если картинка — в конце нужно ее выгрузить, присвоив src="about:blank". Просто удалить из DOM недостаточно. Если открывается файл через URL.createObjectURL, его нужно тут же закрывать через URL.revokeObjectURL.

После сильной переработки кода старый айфон с 512 Мб памяти стал переваривать и 50 фоток, и больше. Хром и Опера на Андроиде тоже стали вести себя значительно лучше — беспрецедентные 160 20-мегапиксельных фоточек дались хоть и медленно, но «без разрывов». Это же благотворно сказалось на потреблении памяти и десктопными браузерами — IE, Хром и Сафари стали кушать стабильно не более 200 мегабайт на вкладку во время работы. К сожалению, это не помогло Фаерфоксу — он как кушал примерно гигабайт на 25 тестовых картинок, так и продолжил. Про мобильный Фаерфокс и Дельфин под Андроидом ничего сказать нельзя — в них невозможно выбрать несколько файлов.

Часть 4. Что-то вроде заключения


Как видите, ресайзить картинки на клиенте чертовски увлекательно и больно. Получается этакий франкенштейн: отвратительный нативный ресайз многократно используется для получения хоть какого-то подобия качества. При этом нужно обходить недетектируемые лимиты различных платформ. И все равно остается много частных сочетаний исходного и конечного размера, когда картинка получается слишком мыльной или резкой.

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

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

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

Кстати, вот еще несколько цифр: используя эту технику, 80 фотографий с Айфона 5, уменьшенных до разрешения 800×600, загружаются по сети 3G меньше чем за 2 минуты. Те же самые оригинальные фотографии могли бы загружаться 26 минут. Так что оно того стоило.
Александр Карпинский @homm
карма
79,8
рейтинг 0,2
Например: Программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (89)

  • +4
    Качественый пост, хорошая проделана работа!
    Насколько я знаю еще флеш умеет делать ресайз картинок на клиентской стороне — интересно сравнить с ним. Понятно что для мобильных платформ это не актуально, но всё же.
    • 0
      Респект автору за JS решение! Читал и плакал))) Все же флеш не так распространен…
      Странно, что качественный даунскейл не поддерживается в браузере. Почти на всех мобильных устройствах есть возможность аппаратно обрабатывать изображения, т.к. upscale/downscale и различные фильтрации в IP ядре поддерживающем OpenMAX и эти функции используются в пост обработке изображения с сенсора, видео декодера
      • +1
        Все же флеш не так распространен…

        А еще лет пять назад…
        • 0
          Это да. Хорошо что флеш был, иначе как бы мы жили без Масяни!
      • 0
        Но это же недоступно из браузера.
        • 0
          Пока да, так же как когда-то не был досупен OpenGL а сейчас это WebGL
  • 0
    У Flash есть варианты:
    — работать через пиксели (используя класс BitmapData)
    — простой скейл визуального объекта (для внешней картинки это будет Loader)
    Тоже есть проблемы с sharpness, которые решаются подбором bimtap фильтра.
  • +7
    Спасибо за пост, но от перевода «canvas» как «канва» у меня лопается эмаль на зубах.
    • +3
      Не раз слышал выражение «канва произведения», мне казалось, что это оно и есть. А как вы хотите, «холст»?
      • +18
        Разумеется. «Холст» — это в том числе технический термин, применяемый в графической индустрии.

        Но я бы просто оставил «canvas». Вы же не переводите «span» как «пядь», «li» как «э. с.» (элемент списка)… так зачем переводить «canvas»?
        • 0
          Переводить как холст надо, как минимум потому, что именно так это слово и переводится. Какой смысл оставлять английские слова или англицизмы? Первый вариант говорит «подскажите мне адекватный перевод», а второй — «я не понимаю, что значит это слово, но, думать не хочу, поэтому несу чушь с умным видом».

          Со span и li — думаю, они не так часто используются, чтобы париться их переводом. Тот же p называют параграфом потому, что в этом есть смысл.
          • 0
            вообще-то, канва — вполне обрусевшее слово
            • +2
              >Канва: сквозная бумажная, сильно проклеенная ткань для вышивания по ней узоров цветной бумагой, шерстью или шелками.
              Обрусевшее, только по холсту можно писать красками, а на канве нужно вышивать. В программном canvas всё необходимое для рисования есть, а для вышивания ничего нет. Тут всего лишь выбор более подходящего слова.
            • +1
              Холст годится) Canvas однозначен) Давайте ценить и любить родную речь) А если для Вас русский, как иностранный, то ещё прошу уважения к языку)
      • +2
        Канва вполне адекватный вариант, по крайней мере, в среде delphi-девелоперов нулевых. Тэги иронии проставить на свое усмотрение.
        • +2
          TКанва :)
  • 0
    В самом деле, в представленной выше функции вся картинка будет занимать память три раза! Один раз — буфер, связанный с объектом Image, куда распаковывается изображение, второй раз — пиксели канвы, и третий — типизированный массив в ImageData.
    Я как-то надеялся, что imageData — псевдомассив и он отдаёт пиксели из буфера канваса.
    • +1
      Но ведь тогда любые изменения на холсте будут отображаться в этом массиве. Да и сам хост постепенно переезжает в видеопамять, а imageData должен находиться в оперативной.
    • 0
      А еще ImageData может не совпадать по размерам с холстом, а доступ к его элементам одномерный. В общем, нет ни одной причины полагать, что это отображение пикселей холста.
      • 0
        То, что доступ одномерный, напротив — аргумент в пользу моей надежды.
        • 0
          Но как это может быть, если ширина холста и imageData не совпадает?

          Ну, т.е. теоретически конечно у imageData может быть виртуальная адресация с нехитрой арифметикой отражения в реальные пиксели буфера холста. Но это был бы сильный удар по производительности.
  • +1
    > Все смартфоны снимают изображение с матрицы в одной и той же ориентации, а настоящую ориентацию записывают в EXIF

    JIC: Не все, как минимум OnePlus oneplus.net транспонирует матрицу.

    • 0
      Спасибо за информацию. А вы уверены, что транспонирование происходит именно в момент съемки? У Meizu MX4 Pro, например, фотографии делаются с ориентацией в EXIF, но при выборе фото из «галлереи», фотография транспонируется и большая часть EXIF (включая геопозицию) обрезается. Причем вырезается из самого файла и насовсем. А вот если выбирать из «документов», то загружается оригинальный файл.

      Кстати, bolk, на обычном MX4 так же?
      • 0
        Да я что-то не проверял.
      • +1
        Да, уверен. Я сам не был к этому готов и минут пять пытался понять, что за ерунда.

        Мне как раз нужен EXIF, поэтому я тащу фотографию на страницу не через IMG, а через BLOB, прямым запросом, и рисую на канве (чтобы GEO показать тут же, и не делать два запроса). Ну поворачивать в таком сценарии надо самому, понятно. Взял у жены телефон, покрутил, пощелкал — все четыре кадра показываются нормально, ногами вниз, а в EXIF у всех 1. Чуть не поседел :)
  • +1
    Давно еще я читал рекомендацию масштабировать картинки в 3 шага: первый и последний в 2 раза, промежуточный — сколько получится.
    Причем писалось это применительно к ФШ, но мои эксперименты показали, что там это очень несущественно. Ну вот может для canvas пригодится.

    И я не понял — в каких единицах заполнены таблицы? Для секунд слишком медленно, для миллисекунд вроде быстровато.
    • 0
      Вполне возможно, что таким образом автор рекомендации пытался избавиться от каких-то неприятных эффектов конкретных фильтров. В отрыве от метода или фильтра, которым там рекомендовалось воспользоваться, эта рекомендация не имеет смысла. А вообще, в Фотошопе нет проблем, с которыми я борюсь в этой статье — там используются свертки или Фурье.

      В таблицах миллисекунды, сейчас добавлю.
    • 0
      Вообще это правильно, главное только после каждого ресайза шарп добавлять.
  • –3
    Ситуация напоминает резайз картинок на первых PC-ках. До сих пор помню эти фотки «гламурных» девчонок с искореженными пропорциями в 256-цветном gif. Но ведь человечество уже давно придумало кучу щадящих алгоритмов resize. Например вот habrahabr.ru/post/243285/
    Неужели ничего из этого не реализовано в виде JS библиотек? Или Вам хотелось «нативного» браузерного ресайза?
    • +5
      Кажется, вы не читали статью :(
      Я про все написал и ссылка на ту мою статью есть в тексте этой три раза.
      • –2
        Да вроде читал. Статья о расходе ресурсов при ресайзе. Вот это все очень похоже на первопроходство:

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

        Поэтому я спросил, неужели нет каких то уже написанных кроссбраузных библиотек которые умеют правильно сжимать и убирать за собой мусор? Ведь как то реализуются граф. редакторы в браузерах. Неужели только через флеш?
        • +4
          Статья о расходе ресурсов при ресайзе.
          Это только третья часть статьи.

          библиотек, которые умеют правильно сжимать
          А остальные части о том, что «правильно» сжимать сложно или невозможно. Приходится искать компромисс.

          Ведь как то реализуются граф. редакторы в браузерах.
          Я посмотрел Picozu и Adobe Aviary, оба ресайзят нативным canvas.drawImage() с паршивым качеством.
          • 0
            Как насчет Pixlr?
            • 0
              Он же на флеше.
  • 0
    А что если первым шагом увеличить изображение, чтобы оно было кратным 2, а затем уже уменьшать? На огромных фотографиях вряд ли будет заметна разница, когда из ~4200px надо сделать ~500px допустим, а вот из 900px > 500px может будет и качественнее.
    • +1
      У нас и так ограничение в 5 мегапикселей из-за iOS, больше картинки не получится сделать еще больше. Что же касается маленьких, 900px → 1000px → 500px даст больше мыла, чем 900px → 500px (что, кстати, очень хороший случай).
  • 0
    Расскажите про реализацию resizePixels, пожалуйста.
    • +1
      На js она такая же как на любом другом языке. Изначально у меня была идея сделать суперсемплинг, он быстрее сверток и однопроходный, но для тестов я портировал только версию с округлением (в чем разница). Она есть во втором примере. Портировал я со своего кода на си.

      Есть такой проект pica, там очень хорошая реализация ресайза на свертках (демо), но поддержки мобильных и больших картинок в IE там нет.

      Так же вы можете найти разные реализации сверток на stackoverflow по запросу в гугле «canvas resize image quality» и подобным. Например такой.
  • +1
    А почему не взять комбинированный метод? Суперсэмплингом уменьшить, условно, с 20 мегапикселей до 5, а дальше уже работать с тем, что уже нужно?
    Да, качество будет хуже. Но работать будет ощутимо шустрее, если я правильно понимаю
    • 0
      Первая часть статьи заканчивается тем, что я столкнулся с проблемами обработки больших изображений: медленная скорость в Хроме под iOS и сложность работы в IE. Это вынудило вернуться к пляскам с бубном вокруг нативных аффинных преобразований. Поэтому суперсэмплингом уменьшить с 20 мегапикселей до 5 не получится. А если бы получалось, то нет проблем сразу суперсемплить до скольки нужно.
      • +1
        Видимо, плохо объяснил :)

        canvas.drawImage(img, 0,0, img.width / 2, img.height / 2) — разве тут не вышел бы суперсэмплинг?
        • 0
          Понятно, под «с 20 мегапикселей до 5» вы имели в виду «уменьшить ровно в 4 раза», а я прочел как «уменьшить строго до 5 мегапикселей». В вашем прочтении не получится уменьшить картинки больше 20 мегапикселей на iOS, в моем будет не суперсемплинг.

          Однако это простая и хорошая идея — если картинка больше 5 мегапикселей или шире 4096, уменьшать её с помощью canvas.drawImage(), а потом применять суперсемплинг вручную. Правда от тормозов в Хроме под iOS это не спасет.
          • 0
            Да, именно, чистый суперсэмплинг — 2х по каждой из сторон.
            От тормозов не спасет, но памяти точно жрать будет куда меньше)
            А в сторону webGL с шейдерами не пытались смотреть, кстати?
  • 0
    Автор, что мешало вам сначала заблурить картинку, и потом ресайзить в один проход (для ресайза точно *0.5 — вообще тупо отбросить каждую вторую строку и каждый второй столбец, т.е. проредить)? Это правильный с точки зрения теории способ понижения частоты дискретизации — сначала low-pass фильтрация для устранения алисинга, а затем прореживание.
    • 0
      Первая часть статьи заканчивается тем, что я столкнулся с проблемами обработки больших изображений: медленная скорость в Хроме под iOS и сложность работы в IE. Эти же проблемы помешали бы заблурить картинку. Кроме того, блюр всегда медленнее ресайза.
      • –1
        Да блюр является частью ресайза, если этот ресайз реализован корректно! Просто обычно функции резайза пишут так, что в явном виде блюр там не выделяется. Это делается для оптимизации, чтобы не хранить промежуточный результат и т.д.

        Вы столкнулись с тем, что встроенный браузерный ресайз реализован некорректно — он не выполняет подавление высоких частот перед пересчётом картинки. Ну так значит вам придётся сделать это за него. И блюр в данном случае может быть тривиальным, например, каждый пиксел брать не непосредственно, а среднее из четырёх (сам пиксел, сосед слева, сосед сверху и сосед слева сверху) — скорее всего вам уже этого хватит, чтобы подавить алиасинг.
        • 0
          Еще раз: если бы было возможно сдедать блюр, было бы возможно сделать и ресайз. Делать блюр и ресайз отдельно нет никакого смысла.

          То, что вы описываете со средним по четырем пикселям (фиксированное число) — и есть ресайз аффинными преобразованиями, который и используется в браузерах.
      • 0
        Кстати, я ещё не понимаю, почему вы удивляетесь, что большие изображения обрабатываются медленно. А попробуйте сделать такой же ресайз в фотошопе или чём-то другом не примитивном, корректно и с таким настройками, чтобы вам результат нравился. Скорее всего, это будет бикубический или фильтр ланцоша. И посмотрите, сколько он времени будет считать такие преобразования. (Я попробовал: GIMP 8mpix изображение с моего телефона ресайзит бикубиком от половины до целой секунды.) Браузер быстрее и так же качественно в любом случае не сможет — тут типичный trade-off — быстро или качественно, и разработчики браузера выбрали быстро.
  • +2
    А ещё мне непонятно, что за «метод аффинных преобразований». Вы вообще знаете, что такое аффинное преобразование? Любой ресайз (а также поворот, перекос) — аффинное преобразование.
    • 0
      Когда я писал «Ликбез: методы ресайза изображений» я долго думал, как назвать этот странный метод, который обычно никак не называют, когда для каждой конечной точки берется фиксированное число исходных. В Pillow, библиотеке для работы с изображениями для Питона, где до версии 2.7 был этот метод, функция ресайза как раз была частным случаем аффинных преобразований. Я не придумал ничего лучше, чем так его и обозвать.

      С тех пор я по прежнему не встретил для него названия в литературе и не придумал ничего лучше. А как бы вы его назвали?
      • 0
        Если я правильно понял ваше описание в статье, это — "билинейная интерполяция", в граф.редакторах так и называется обычно — bilinear resize.
        • 0
          Ну нет же, билинейная интерполяция — это фильтр. Можно взять 16 точек таким же методом, будет бикубическая. В то же время, и билинейную, и бикубическую, и много что еще можно сделать на свертках.
          • 0
            Не совсем так просто, но близко к этому. А ещё существует интерполяция сплайнами, sinc (ланцоша) и т. д.

            Всё это применяется для ресемплинга оцифрованных данных. Я всё-таки не понимаю, почему вас удивляет наличие фильтра в ресайзе. Он там есть, он там должен быть. Просто он должен быть адекватный, а билинейный фильтр фактически не подходит для изображений с большим количеством высоких частот, потому, что он их плохо подавляет.

            Вон ссылка правильная в комментарии ниже.
            • 0
              Я всё-таки не понимаю, почему вас удивляет наличие фильтра в ресайзе.
              Я тоже не понимаю, почему меня это удивляет.

              Вы опять про фильтры, когда я спрашиваю, как бы вы назвали метод. Я вам все же очень рекомендую прочитать статью Ликбез: методы ресайза изображений, там именно про методы. А выбор фильтра внутри метода не дает таких принципиальных различий, как выбор метода.
  • +1
    Меня что-то смущают эти алгоритмы в духе «давайте уменьшим в N раз, а затем несколько раз в 2 раза». Хорошо они будут работать только при размерах изображения вида N * 2K. В то же время известны алгоритмы с доказанным качеством для произвольного случая. А в случае, если это у вас маленький мобильный телефон, и вам нужно уменьшенную картинку только показать, то можно использовать ещё более качественные алгоритмы, вроде придуманных wizzard0. Даунсемплинг на клиенте это не очень хорошая идея, потому что вы никогда не знаете, как будет вести себя код на клиенте.

    • 0
      Не самый лучший алгоритим. Обратите внимание на паразитный муар на картинках с концентрическими кругами — это и есть алиасинг, то есть, искажение низких частот из-за того, что перед ресемплингом недостаточно подавили высокие. На этом примере этот «новый алгоритм» явно облажался. Да и вообще, фотошоповский ресайз там не хуже в первом варианте (различий между картинками практически нет), лучше для концентрических кругов и не хуже (точнее, такой же плохой) для примера с рассчёской — по совокупности «новый алгоритм» проигрывает даже голимому бикубику!
      Кстати, ещё странно, почему он там сравнивал с бикубиком, и не сравнил с ланцошем.

      P.S. Да там оригинал кругов уже с муаром. Конечно, garbage in -> garbage out.
      • +1
        > паразитный муар

        TLDR: идеального ресайза нет, смотрите, куда потом картинку использовать будете.

        есть две основных, хм, «школы» ресайза картинок

        а) давить частоты до полного отсутствия алиасинга, тогда у нас максимальная частота на квадратной решетке пикселей — 1/sqrt(2) линий на пиксель («линия толщиной не менее 1.4 пикселя»)

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

        б) добиваться максимального использования разрешающей способности (например, для шрифтов так ClearType работает), это означает что по горизонтали у нас получается 3 линий на пиксель («линия толщиной в 1 субпиксель»), и 1 линия по вертикали.

        однако, картинку после этого нельзя ресайзить ВООБЩЕ, только показывать на RGB LCD мониторе 1:1, иначе будет уебище.
        также уебище будет, если монитор не калиброван и гамма-кривые разных цветов расходятся, что чаще всего демонстрирует тот пример с кругами.
        (ну и, строго говоря, я неправильно там сделал energy diffusion, но на ч/б картинке оно не влияет)

        у меня на сайте dmnd.io есть переключатель между этими двумя вариантами, пробуйте :)
        • +1
          Есть где-то реализация или хотя бы описание вашего алгоритма? А то я не понимаю о чем речь вообще.
          • +1
            Там просто честный гамма-корректный ресэмплинг, где рассчитывается, какую площадь каких исходных пикселов накрывает результирующий пиксель (или субпиксель). Край прямоугольника либо резкий, либо взвешенный гауссиан.

            Конечно, для увеличения оно выглядит как nearest neighour :)

            При субпиксельном сглаживании соответственно рисуются честные прямоугольные пиксели с (примерной, к сожалению) развесовкой по яркостям.

            Все пересчитывается в double, потом dither'ится. Характерная фича — если увеличить картинку (без субпиксельного сглаживания), а потом уменьшить — результат часто бит-в-бит совпадает.
            • +1
              Понятно. Это называется суперсэмплинг.
              • +1
                В случае без гауссиана — да. В случае с гауссианом — не берусь сказать, как это правильно называется %)
      • +3
        ланцош, sinc и прочие делают вокруг высококонтрастных элементов круги (в случае белого на сером может получиться темно-серая кайма), они меня раздражают столь же сильно, сколь многих раздражает алиасинг :)

        image
        • 0
          Да, круги бывают. А зачем вы после ресайза так картинку растянули? Чтобы круги показать? А то если не растягивать, то круги вообще незаметны — они только подчёркивают резкость.
          • 0
            Да, чтобы показать круги — они (для меня) очень бросаются в глаза даже на не-растянутом варианте.
  • +3
    Хм, а что, если воспользоваться WebGL? Там вполне возможно задать способ масштабирования, да и на большинстве устройств оно должно на GPU рендериться, т.е. будет быстрее нативного js.
    Большинство библиотек под WebGL, да хоть тот же three.js, уже содержат ряд функций для работы с изображениями.
    Правда, есть подозрение, что на мобильных устройствах всё равно начнут возникать какие-нибудь проблемы.
    • 0
      Вы меня опередили. Действительно, ресайз на GPU проходит заметно быстрее при довольно хорошем качестве. Рекомендую всем желающим попробовать:
      1. Подключаете PIXI.js для простоты работы и создаете сцену (var stage = new PIXI.Stage(0xffffff))
      2. Высчитываете пропорции сцены и коэффициенты масштабирования по каждой стороне
      3. Создаете текстуру с оригинальным изображением (var texture = new PIXI.Texture.fromImage('path/to/image'))
      4. Создаете спрайт с этой текстурой (var sprite = new PIXI.Sprite(texture)), выставляете коэффициенты масштабирования (sprite.scale = {x:… ,y: ...})
      5. Добавляете спрайт на сцену (stage.addChild(sprite)) и рендерите изображение (renderer = PIXI.autoDetectRenderer(newWidth, newHeight); renderer.render(stage);)

      Можно и без PIXI, конечно, просто будет заметно больше кода. Но для тестов можно и так попробовать.
      • 0
        Тут нужно ещё учесть занимаемую память. По идее, после загрузки изображения в WebGL можно удалить из памяти исходник, но интересно, как это работает на мобильных устройствах?
        • 0
          На мобильные браузеры я бы особо пока не надеялся. По опыту, довольно сносно WebGL работает на мобильном Хроме. На мобильном Windows 8 IE пока не умеет WebGL, обещают скоро добавить, видимо, в 10 винде. На десктопе IE11 тоже хорошо справляется. А вот FF почему-то отказывается работать в WebGL контексте как на мобильном у меня, так и на десктопе, несмотря даже на все настройки в about:config — при вызове PIXI.autoDetectRenderer в FF всегда выбирается медленный «canvas» вместо «webgl». Не знаю, может руки у меня кривые.
      • 0
        Не могли бы вы сделать рабочий пример на jsbin.com, где ресайзилась бы картинка из <input type="file">?
        • 0
          Пробуйте тут (Хром, ИЕ11): jsbin.com/mevehayica/2/edit?html,js,output
          • 0
            Чуда, к сожалению, не произошло. В Хроме и Мобильном хроме результат такой же как context.drawImage() на обычной канве. Я бы рекомендовал следить за этим обсуждением.
  • 0
    Логичней было бы реализовать нормальный метод ресайза на сях и сделать пулреквест в репозитории браузеров?
    • +3
      Логичней для чего? Мне нужно было сделать фичу, я ее сделал с горем пополам за неделю. Если просто заменить ресайз на нормальный — никто из браузеров не примет, станет же медленнее, чем у конкурентов! Нужны какие-то флаги, опции — это потребует долгого обсуждения. Через два года основные платформы обновятся, через три этим можно будет пользоваться. Так что я не вижу, что тут логичного.

      Ну и к тому же, во всех браузерах кроме эксплорера картинки в <img> нормально интерполируются, но почему-то на канве все равно используется фиговый метод.
      • –1
        Ну с позиции несения добра в массы. :)
  • 0
    А я помнится решил проблему с помощью WebGL (для большей простоты еще и с PIXI.js). Конечно, это не для всех браузеров, но в своей админке я обычно хозяйничал сам и заходил только под Хромом. Тесты производительности не делал, но проблем и подвисаний даже с огромными изображениями не было.
  • +2
    Почитал статью про мучения, а в голову пришла сумасшедшая идея, жаль что в данный момент у меня нет возможности протестировать.
    Суть:
    Выбираем картинку, через FileReader выводим ее в html нужного размера, а потом с помощью html2canvas (возможно откорректированного под нужды) lделаем скриншот страницы и сохранем его в канвас с уже нужным размером, выходит за ресайз будет отвечать браузер сам, размер подогнать с помощью тега img.
    • 0
      У меня не получилось заставить его работать. Пытался вот так. Да и по описанию принципа работы понятно, что ничего не получится, он же просто разбирает DOM и рисует на канву тем, чем может.
  • –3
    становится просто прозрачными пикселями черного цвета

    Пять красных линий, часть прозрачным цветом и часть зелёным
    • +1
      rbga(0,0,0,0)
    • +1
      А что вас смутило? Никогда не видели #00000000?
      • –2
        Нет. А вы видите такие вот пиксели? И давно это у вас? =)
  • –1
    А можно уточнить, в табличке у вас два Chrome A — это разные версии андроида?
    • +2
      Нельзя. Читайте статью :)
  • –1
    И часть картинки за этими пределами становится просто прозрачными пикселями черного цвета.
  • 0
    Спасибо за интересную статью. Мы в свое вермя решили не заморачиваться, а использовать следующую конструкцию:

    <picture data-alt="Eine neue Versus Karte" class="picture loading"> 
        <!--[if IE 9]><video class="is-hidden"<![endif]--> 
        <!-- Generic wide screen -->
        <source media="(min-width: 1024px)" 
                srcset="/files/teaser-hero/_946x/18022015-Hero49.jpg, /files/teaser-hero/_1888x18022015-Hero49.jpg 2x">
        <\/source> 
        <!-- iPad lanscape --> 
        <source media="(min-device-width: 769px) and (max-device-width: 1024px)" 
                srcset="/files/teaser-hero/_944x/18022015-Hero49.jpg, /files/teaser-hero/_1888x/18022015-Hero49.jpg 2x">
        <\/source>
        <!-- iPad portrait --> 
        <source media="(min-device-width: 481px) and (max-device-width: 768px)" 
                srcset="/files/teaser-hero/_768x/18022015-Hero49.jpg, /files/teaser-hero/_1536x/18022015-Hero49.jpg 2x">
        <\/source>
        <!-- iPhone 3-4 landscape --> 
        <source media="(min-device-width: 321px) and (max-device-width: 480px)" 
                srcset="/files/teaser-hero/_480x/18022015-Hero49.jpg, /files/teaser-hero/_944x/18022015-Hero49.jpg 2x">
        <\/source>
        <!-- iPhone 3-4 portrait --> 
        <source media="(max-device-width: 320px)" 
                srcset="/files/teaser-hero/_280x/18022015-Hero49.jpg, /files/teaser-hero/_560x/18022015-Hero49.jpg 2x">
        <\/source>
        <!--[if IE 9]></video><![endif]--> 
        <img alt="Eine neue Versus Karte" 
                srcset="/files/teaser-hero/_1888x/18022015-Hero49.jpg 2x" 
                src="/files/teaser-hero/_946x/18022015-Hero49.jpg"> 
    </picture>
    

    а на сервере перенаправялем все запросы по /files/* на скрипт ресайза (который использует ImageMagic и отдает картинку правильного размера).
    • 0
      ImageMagick это же не слишком быстро. Кешируете результат навсегда? Прегенерируете основные размеры? Что-то ещё?
      • 0
        Прегенерируете основные размеры?

        Нет, только по реквесту.

        Кешируете результат навсегда?

        Если файл найден — всегда отдается файл. Только если его нет — ресайзим (создаем нужный файл по запрошеному пути). Плюс у нас CDN, так что после того, как файл найден и отдан нашим сервером — он уже будет доступен в CDN для всех следуюших запросов.
        • 0
          Ясно, спасибо.
  • 0
    Кое-что скоро может стать получше:

    html.spec.whatwg.org/multipage/scripting.html#image-smoothing

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