Pull to refresh

Comments 90

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

image
Да, круги бывают. А зачем вы после ресайза так картинку растянули? Чтобы круги показать? А то если не растягивать, то круги вообще незаметны — они только подчёркивают резкость.
Да, чтобы показать круги — они (для меня) очень бросаются в глаза даже на не-растянутом варианте.
Хм, а что, если воспользоваться WebGL? Там вполне возможно задать способ масштабирования, да и на большинстве устройств оно должно на GPU рендериться, т.е. будет быстрее нативного js.
Большинство библиотек под WebGL, да хоть тот же three.js, уже содержат ряд функций для работы с изображениями.
Правда, есть подозрение, что на мобильных устройствах всё равно начнут возникать какие-нибудь проблемы.
Вы меня опередили. Действительно, ресайз на 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, конечно, просто будет заметно больше кода. Но для тестов можно и так попробовать.
Тут нужно ещё учесть занимаемую память. По идее, после загрузки изображения в WebGL можно удалить из памяти исходник, но интересно, как это работает на мобильных устройствах?
На мобильные браузеры я бы особо пока не надеялся. По опыту, довольно сносно WebGL работает на мобильном Хроме. На мобильном Windows 8 IE пока не умеет WebGL, обещают скоро добавить, видимо, в 10 винде. На десктопе IE11 тоже хорошо справляется. А вот FF почему-то отказывается работать в WebGL контексте как на мобильном у меня, так и на десктопе, несмотря даже на все настройки в about:config — при вызове PIXI.autoDetectRenderer в FF всегда выбирается медленный «canvas» вместо «webgl». Не знаю, может руки у меня кривые.
Не могли бы вы сделать рабочий пример на jsbin.com, где ресайзилась бы картинка из <input type="file">?
Чуда, к сожалению, не произошло. В Хроме и Мобильном хроме результат такой же как context.drawImage() на обычной канве. Я бы рекомендовал следить за этим обсуждением.
Логичней было бы реализовать нормальный метод ресайза на сях и сделать пулреквест в репозитории браузеров?
Логичней для чего? Мне нужно было сделать фичу, я ее сделал с горем пополам за неделю. Если просто заменить ресайз на нормальный — никто из браузеров не примет, станет же медленнее, чем у конкурентов! Нужны какие-то флаги, опции — это потребует долгого обсуждения. Через два года основные платформы обновятся, через три этим можно будет пользоваться. Так что я не вижу, что тут логичного.

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

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

<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 и отдает картинку правильного размера).
ImageMagick это же не слишком быстро. Кешируете результат навсегда? Прегенерируете основные размеры? Что-то ещё?
Прегенерируете основные размеры?

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

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

Если файл найден — всегда отдается файл. Только если его нет — ресайзим (создаем нужный файл по запрошеному пути). Плюс у нас CDN, так что после того, как файл найден и отдан нашим сервером — он уже будет доступен в CDN для всех следуюших запросов.
Sign up to leave a comment.

Articles