Используем JPEG с прозрачностью

    Конечно же, формат JPEG не поддерживает прозрачность, но сама идея использовать JPEG вместо PNG для прозрачных текстур будоражит умы довольно давно. Камрад PaulZi не так давно предложил использовать для HTML формат SVG, в котором хранится само изображение и маска. Jim Studt предлагает использовать EXIF поля в JPEG и хранить там маски, а отображать на веб-странице с помощью Canvas.
    Оба метода относительно сложны для использования, да и рассчитаны на веб, потому я остановился на самом простом варианте: хранить отдельно lossy JPEG для RGB и lossless маску в PNG, а совмещать их на этапе получения UIImage в программе. Сразу хочу сказать, что пишу на MonoTouch, потому код привожу на C#, хотя в ObjC это делается почти точно так же, с учетом синтаксиса.


    Разделение каналов


    Для разделения я использую консольные утилиты ImageMagik.
    Эта команда отделяет альфа-канал:
       convert file.png -channel Alpha -separate file.mask.png

    Следующая команда создает JPEG файл, отбрасывая данные о прозрачности. Что характерно, некоторые другие утилиты (в т.ч. и Photoshop) при конверсии PNG файла в JPEG добавляют к нему некую одноцветную подложку и лишь затем сохраняют в RGB, что дает красивую, но неверную картинку с pre-multiplied alpha.
       convert file.png -quality 90 -alpha off file.jpg

    Качество полученного файла регурируется параметром quality 90. 90% качества для JPEG это больше, чем Apple ставит для скриншотов программ и фильмов. Думаю, каждый сможет подстроить под свой вкус это значение.

    Оптимизация


    Маска получается в виде 8-битного Grayscale PNG без прозрачности. Такой формат отлично сжимается через optipng или через веб-сайт www.tinypng.org.
    С JPEG ситуация интереснее. Можно было бы ограничиться только заданием качества, но недавно мне попалась замечательная утилита jpegrescan от Loren Merritt, одного из разработчиков ffmpeg и кодировщика x264 (на счет него же есть подозрения, что он является представителем инопланетного разума или кибернетического мозга).
    Утилита использует необычный подход: подбирает разные коэффициенты для Progressive сжатия и выбирает наиболее оптимальные. Выигрышь получается от 5 до 15% с идентичным качеством картинки. Собственной страницы у утилиты нет, только топик с обсуждением и сам perl-код: pastebin.com/f78dbc4bc

    Чтобы не вводить команды каждый раз вручную, я написал простенький скрипт на bash:
    #!/bin/bash
    basefile=${1##*/}
    maskfile="${basefile%.*}.mask.png"
    jpegfile="${basefile%.*}.jpg"
    convert $1 -channel Alpha -separate $maskfile
    convert $1 -quality 90 -alpha off $jpegfile
    optipng $maskfile
    jpegrescan $jpegfile $jpegfile
    


    Вот результат работы скрипта:

    В моем случае из файла в 1,8Мб получилось два файла на 380Кб и 35Кб.

    Склеивание


    Само склеивание делается очень просто — загружается две картинки в UIImage, затем создается на их основе CGImage методом WithMask (в ObjC это CGImageRef и initWithMask соответственно), а потом оборачивается в новый UIImage.
    		UIImage result;
    		using(UIImage uiimage = UIImage.FromFile(file))
    		using(UIImage mask = UIImage.FromFile(maskFile))
    		{
    			CGImage image = uiimage.CGImage;
    			image = image.WithMask(mask.CGImage);
    			result = UIImage.FromImage(image, uiimage.CurrentScale, uiimage.Orientation);
    		}


    В реальном проекте я сделал чуть сложнее и проверяю наличие файла *.mask.png и если он отсутствует, то возвращаю обычный UIImage.FromFile().

    Профит




    Визуально игра не изменилась никак. Задержка загрузки и склеивания текстур на глаз не заметна, потому я и не стал ее замерять. Сам же проект уменьшился на 6 (!!) мегабайт, как в .ipa виде, так и в iTunes и на устройстве.



    Скрин из игры в PNG. Никаких артефактов или проблем сжатия/прозрачности не видно.
    Немного смущает удвоенное количество картинок в папке проекта, но это можно пережить. Изменения кода минимальные. Для графики интерфейса этот метод не идеален из-за необходимости вручную присваивать UIImage, а не загружать из NIB/XIB, но для собственных контролов или текстур подходит вполне. Даже если JPEG сохранять с 100% качеством, размер полученных файлов может быть меньше, чем изначального PNG без потерь качества.

    P.S. ImageMagick и optipng ставятся из портов (MacPorts/Fink/Brew) и называются так же. Скрипт jpegrescan качается с pastebin и использует порт jpeg
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 34
    • +1
      браво! попробую применить на windows 8
      • 0
        Не думали склеенные файлы сохранять в каком-то своём формате? Это избавило бы от удвоения количества картинок и склеивания налету. Нужно только написать просмотрщик для нового типа файлов в вашем приложении.
        • 0
          Лучше тогда уже действительно в exif писать, чтобы не плодить форматов. Там, правда, ограничение на 64кб на поле, но это не смертельно.
          А уж если совсем на свой формат переходить, так правильнее уже и JPEG2000 использовать.
          Но и тогда о простоте подхода забыть придется.
        • 0
          Одним из вариантов решения проблемы с увеличившимся вдвое количеством ресурсов может быть настройка пост-билд экшена, которые копирует в бандл прозрачные маски с теми же именами, что и у jpg-файлов.
          • 0
            А не думали использовать что-то аналогичное хромакей? Реализация: При отображении вычислять маску автоматом исходя из того, что некоторый цвет (с небольшим оговоренным допуском) считаем прозрачным. Делаете подложку из зеленого цвета. Все пикселы, в которых уровень зеленого преобладает над уровнем красного и синего считаем прозрачными.
            Косяки: Нельзя использовать зеленый. Решение: В случае если картинка уже имеет зеленый цвет — можно оговаривать цвет заранее (например синий или красный). Также можно и с другими цветами но с другим алгоритмом проверки…
            Плюсы: нет удвоенного количества файлов. Стандартный формат. Адекватный размер. Простота создания файлов с картинками.
            Минусы: возможны появления артефактов в маске, как результат — шум возле картинки…
            • 0
              Вариант достойный обсуждения. К сожалению, JPEG хранит не RGB картинку, а YCbCr (как и MPEG, DXT1-DXT5 и др.), потому «зеленый» сильно смешается с другими цветами и артефакты перечеркнут всю идею… Ну и восстановление цветового баланса будет потом непростой задачей.
              • 0
                Ну просто исходя из увиденных скриншотов, я решил что маски нужны не с градациями, а в духе есть/нет. Такой вариант пробовал на веб сайтах — работало вполне кошерно. Про RGB говорил т.к. обрабатывал уже распакованную картинку (а она там была именно rgb)
                А баланс зачем восстанавливать? или это было сказано по поводу полупрозрачных картинок?
                • 0
                  Как раз маски нужны были с градациями. Баланс восстанавливать надо будет, если мы целиком избавимся от одного цвета и заменим его альфа-каналом.
                  Покажете свой вариант с веб сайтами?
                  • +3
                    ну на счет сайтов — это я приувеличил: просто попробовал реализовать эффект хромакей наткнувшись на описание работы с canvas — на том все и заглохло… Но в планах есть… Честно :-) вот на чем остановился
                    • 0
                      вполне неплохо! для GUI без теней и особого сглаживания вполне может подойти
            • +2
              А вы не пробовали оптимизировать PNG уменьшением цветов и так далее? С этим неплохо справляется Color Quantizer. Глядишь, и JPEG не понадобился бы.
              • +1
                Конечно пробовал. Размер 1.8Мб был получен при оптимальном соотношении цвета/качества. Без этого тестовый пример 2.5Мб занимал. Более того, есть методы, которые автоматическим утилитам и не снились, например, разбить изображение на блоки, квантовать каждый из них до 256 цветов и собрать все вместе.
                Но все-равно ~400кб размера для такой текстуры и без визуальных потерь только JPEG даст.
            • 0
              Круто
              А как выглядит джпег, полученный из png с помощью imagemagic?

              Для меня, непрограммиста, например загадка, как Флеш жмёт png: они сохраняют прозрачность, но при этом уменьшаются в размере и при диком сжатии покрываются характерными артефактами
              • +1
                Выглядит как джипег со странными областями вокруг объектов, где раньше была прозрачность (остатки работы дизайнера):
                JPEG:
                runserver.net/temp/data_2.jpg
                Маска:
                runserver.net/temp/data_2.mask.png
                Оригинал:
                runserver.net/temp/data_2.png
                • 0
                  Ага, спасибо. Ничего не зная про порты, я такие штуки разделял в ФШ с помощью экшена, который делал кучу клонов прозрачного слоя (чтобы все полупрозрачные пиксели стали 100% непрозрачности), а оказывается вот оно как можно.
              • +1
                Добавлю маленькую деталь о сжатии JPEG.
                ImageMagick позволяет указывать не только quality но еще и chroma subsampling. При его отключении (точнее, установке параметра -sampling-factor 4:4:4 ) смешение цветов при сжатии уменьшается, и параметр quality можно установить меньшим, сохранив менее размытые цвета при большем сжатии.
                • 0
                  нельзя туда TIFF пихать? Каналов — сколько угодно. Подержка как jpg, так и всяких zip копрессий
                  • 0
                    В фотошопе если для TIFF ставить галочку «Save Transparency», то JPEG сжатие становится неактивным. Может можно как-то это обойти со слоями и спец. утилитами, но надо еще изучать.

                    • 0
                      Прямым конвертированием — нельзя, при "-compress JPEG" ImageMagick ругается (точнее, libjpeg) на непригодные данные на входе, если скармливать ему png с прозрачностью. Однако при использовании сжатия JPEG2000, все конвертируется, с сохранением прозрачности. Аналогично, если использовать ZIP.
                      • 0
                        Я имел ввиду использовать TIFF/LZW-ZIP + Alpha напрямую в софте. В OS X поддержка TIFF есть изначально, как наследие NeXT. А вот есть ли она iOS?
                        • 0
                          Использовать можно, хотя и были некоторые репорты о проблемах. Но TIFF/LWZ и TIFF/ZIP менее выгодны, чем PNG, потому интересно только колдовать с TIFF/JPEG и слоями
                    • 0
                      В геймдеве подобная техника применяется уже давно.
                      Только файл создается один, где последовательно записаны данные обоих файлов и в начале заголовок о смещениях данных и их размере. Кроме того серую маску можно делать также в jpeg с очень сильным сжатием (20-40%), результат не хуже.
                      • 0
                        мне казалось, в геймдеве как раз лидирует DXT5 с explicit alpha и другие GPU-поддерживаемые форматы, не требующие перекодировки текстуры и пересоздания mipmaps. Хотя если Вы о мобильных играх, то действительно тут и текстур поменьше, и альтернативы своим решениям почти нет.
                        А вообще, смещение в заголовке сразу рубит на корню простоту и редактируемость, получается собственный формат без возможности просмотра, требующий отдельных утилит и пр. В таком случае можно действительно и JPEG2000 уже использовать.
                        • 0
                          В таком случае рекомендую для iOS формат PVRTC или просто PVR. Это своего рода аналог DDS/DXT но для чипов PowerVR, которые повсеместно стоят в яблочных девайсах.
                          • 0
                            Я потому и писал, что на мобильных альтернативы собственным решениям почти нет — PVRTC уж очень lossy, как и ETC. Чистый PVR с GZ сжатием хоть как-то еще дают приемлемые размеры, но все-равно для 2D игр, где не нужны mipmaps лучше таки потратить секунду-другую на загрузку PNG или реконструкцию текстуры из JPEG+маска, это будет в разы оптимальнее по качеству/размеру.
                            • 0
                              Полностью с вами согласен, если важен размер на диске. Если важна скорость загрузки, то у PVR нет конкурентов. Даже загрузка PVR из ZIP происходит быстрее загрузки из PNG.
                      • 0
                        Первый вопрос, как сторонника стандартов: почему шаманство с JPEG, а не JNG?
                        • 0
                          1. Разве iOS и другие моб. платформы нативно поддерживают JNG? С разумным временем загрузки, подхватыванием Retina тегов @2x и пр.? Я находил стороннюю библиотеку по работе с JNG на github, но не стал бы рисковать встраивать ее в рабочий и отлаженный проект, в то время как с CGImageRef::initWithMask изменение кода минимально и работает идеально.

                          2. Удобство использования. Можно просматривать видим в любом просмотрщике маску и RGB данные, ретушировать при необходимости, исправлять баги, менять степень сжатия.

                          3. Оптимизации. Утилит, аналогичных jpegrescan для JNG еще просто не существует, надо писать свои.

                          В целом, подход точно такой же, как в JNG формате — различные потоки для альфа и RGB канала, но без соединения в один контейнер. Если Apple/Google/MS/etc. начнут использовать JNG — можно перейти даже без особой переконверсии, склеиванием с формированием заголовка.
                          • 0
                            Ну, 1 пункт весомый. Хотя, libmng сложно портировать?
                            2. Не критично, всю разработку можно вести в PNG, а в JNG только финальный этап.
                            3. jpegrescan сделает поток JPEG его и надо будет «поместить» в JNG. Или просто подбор параметров.
                            Впрочем, п.1 достаточно.
                            • +1
                              Портировать наврядли очень сложно… В конце концов, можно и вариант с github вычистить, отладить до блеска и использовать в продакшене. Но все-таки это время и усилия, а отдача не очень большая по сравнению с велосипедами вроде JPEG+PNG.
                              В целом, хотелось бы нативной поддержки JNG или чего-то вроде от самой ОС. Я бы, как и наверняка многие разработчики, с удовольствием перевел все проекты на него с заметным уменьшением размеров картинок.
                              • 0
                                libmng написанно на c, собирается в gcc нормально, из зависимостей тянет за собой zlib, jpeglib
                                После статьи по jng на хабре — немного поигрался с этим делом.
                                • 0
                                  Я так понял, что сложности потом это скормить UIImage. Хотя CGImage под OSX создавал из буфера, а UIImage по идее потом на основе CGImage создать не проблема. Но, в любом случае, не 5 строчек кода уже получается. Хотя я бы предпочёл именно JNG.
                          • 0
                            Тут конечно не совсем то, про что автор пишет, но может кому сгодится. Работает и в ИЕ6.

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