HDR-фотографии: что находится внутри JPEG с gain map

На HDR-дисплее солнце или блик на фотографии иногда выглядит ярче белого фона страницы. Разберём без магии, откуда экран получает команду сделать отдельные пиксели ярче. Пример - файл IMG_20260816_183557.jpg с Xiaomi 17.

Фотография, снятая на Xiaomi 17 и сохранённая как HDR JPEG с gain map

HDR-эффект на этой фотографии виден только на HDR-экране в совместимом браузере.

Пиксель, канал и биты

Здесь канал означает компонент цифрового представления цвета. В модели RGB цвет пикселя задают три числа: красное R, зелёное G и синее B.

У 8-битного канала есть 2⁸ = 256 возможных кодов, от 0 до 255. Например, после декодирования программа может представить один пиксель так:

один пиксель в декодированном RGB-буфере

R: 11010110  = 214
G: 10011100  = 156       всего 8 + 8 + 8 = 24 бита
B: 00110101  =  53       без учёта прозрачности

После декодирования пиксели могут лежать в памяти как RGB, BGR или RGBA, но это не описывает устройство JPEG-файла, а даже BMP добавляет заголовки и собственные правила хранения.

RGB не является единственной схемой кодирования:

Схема Компоненты Где встречается
Grayscale одно значение серого маски, карты, чёрно-белые изображения
RGB / RGBA красный, зелёный, синий и необязательная прозрачность декодированные изображения, графика, интерфейсы
YCbCr яркостная Y и две цветоразностные Cb/Cr JPEG и видео, часто с меньшим разрешением цветовых компонентов

Для однозначного цвета нужны также цветовое пространство и передаточная функция. Чёрный в обычном RGB-примере кодируется как (0, 0, 0), белый как (255, 255, 255), но это коды цвета, а не значения яркости в нитах.

Больше битов дают больше промежуточных кодов на канал:

Разрядность Значений канала Вариантов RGB-пикселя
8 бит 256 16,7 млн
10 бит 1024 1,07 млрд
12 бит 4096 68,7 млрд

10 или 12 бит уменьшают ступеньки на градиентах и дают запас для обработки. Но именно поэтому 8-битный JPEG всё ещё может давать HDR: разрядность описывает число кодов внутри базового изображения, а яркость выше SDR-белого добавляют второе изображение с gain map и метаданные для его применения.

Как JPEG лежит в файле и в памяти

После декодирования программа действительно может держать изображение как массив RGB-пикселей. Для кадра 4096 × 3072 при 8 битах на три канала такой несжатый массив занимал бы:

4096 × 3072 × 3 байта = 37 748 736 байт ≈ 36 МиБ

JPEG на диске устроен иначе. При типичном кодировании RGB сначала преобразуется в яркостную составляющую Y и две цветоразностные Cb/Cr. Цветовые каналы часто уменьшаются в разрешении, изображение делится на блоки, значения переводятся в частотные коэффициенты дискретным косинусным преобразованием, округляются и кодируются без лишних повторов.

flowchart TB
    subgraph memory[В памяти редактора: около 36 МиБ]
        RGB[Пиксели RGB<br/>4096 × 3072 × 3 байта]
    end

    subgraph encoder[Сохранение JPEG]
        direction LR
        YCC[Преобразование<br/>RGB → YCbCr]
        SUB[Уменьшение<br/>цветовых каналов]
        DCT[Блоки и DCT]
        Q[Округление<br/>коэффициентов]
    end

    FILE[(Сжатый JPEG<br/>около 4,77 МБ)]
    DEC[Декодирование]
    SCREEN[Пиксели<br/>на экране]

    RGB --> YCC --> SUB --> DCT --> Q --> FILE
    FILE --> DEC --> SCREEN

Главная потеря качества происходит при округлении коэффициентов. Поэтому JPEG компактный, но повторное пересохранение может добавить артефакты. В исследуемом файле основной кадр занимает около 4,77 МБ вместо примерно 36 МиБ декодированного RGB-массива.

Число 8 в заголовке JPEG означает точность отсчётов базового изображения. Оно не запрещает приложить к файлу второе изображение и метаданные, которые вместе описывают HDR.

Из каких частей состоит этот файл

JPEG - последовательность секций, или сегментов. Диапазон маркеров APP0...APP15 задан в приложении B рекомендации ITU-T T.81, совместном тексте с ISO/IEC 10918-1, а правила применения APPn описаны в действующей рекомендации ITU-T T.86. Маркеры APP1, APP2 и другие позволяют помещать рядом со сжатыми пикселями служебные данные. Разбор файла Xiaomi дал такую карту сегментов в порядке следования. Для APPn в таблице указано значение двухбайтового поля длины JPEG-сегмента: оно включает само поле, но не включает двухбайтовый маркер FFEn.

Порядок JPEG Смещение Сегмент Длина / параметры
1 основной 2 APP1 XMP 948 Б
2 основной 952 APP3 ALGO_COMMON 22 672 Б
3 основной 23 626 APP2 ICC_PROFILE 648 Б
4 основной 24 276 APP4 XIAOMI_CUSTOMIZE 546 Б
5 основной 24 824 APP3 OISINFO 7 574 Б
6 основной 32 400 APP2 urn:iso:std:iso:ts:21496:-1 34 Б
7 основной 32 436 APP2 MPF 88 Б
8 основной 32 660 SOF0 4096 × 3072, 3 компонента, 8 бит
9 основной 4 773 110 EOI конец базового JPEG
10 gain map 4 773 114 APP1 XMP 549 Б
11 gain map 4 773 665 APP2 ISO 21496-1 91 Б
12 gain map 4 773 758 APP0 JFIF 16 Б
13 gain map 4 773 845 SOF0 2048 × 1536, 1 компонент, 8 бит

Старый декодер читает первый JPEG до маркера конца изображения и показывает нормальную SDR-фотографию. Совместимый декодер находит второе изображение, читает параметры gain map и строит HDR-представление. Поэтому расширение .jpg не противоречит HDR.

EXIF: чем и как снято

EXIF описан в официальной спецификации CIPA DC-008-Translation-2026, Exif Version 3.1. Это действительно актуальная редакция на дату статьи: CIPA опубликовала её 30 января 2026 года как пересмотр CIPA DC-008-2024. EXIF представляет собой стандартную таблицу фотографических метаданных: модель камеры, выдержка, диафрагма, ISO-чувствительность, ориентация, время и иногда GPS. Эти поля описывают происхождение снимка, но не содержат сами видимые пиксели.

У Xiaomi часть сведений находится в фирменном блоке камеры: модель Xiaomi 17 и Hdr: auto. Здесь Hdr: auto говорит, что HDR-режим камеры был автоматическим. Этого недостаточно, чтобы доказать наличие воспроизводимого HDR: производитель мог свести несколько экспозиций в обычный SDR JPEG. Доказательство находится в gain map и её метаданных.

ISO в EXIF и ISO 21496-1 - разные значения слова ISO. В EXIF ISO 100, 400 или 1600 означает настройку светочувствительности при съёмке. ISO 21496-1 - номер международного технического стандарта хранения gain map.

XMP: расширяемые инструкции

XMP - текстовые метаданные в формате, похожем на XML. EXIF имеет заранее определённый набор фотографических полей, а XMP допускает пространства имён производителей и программ. Поэтому Adobe может добавить поля hdrgm:*, не меняя сам JPEG-декодер.

Во втором изображении этого файла записано:

hdrgm:Version="1.0"
hdrgm:GainMapMin="0"
hdrgm:GainMapMax="2.32193"
hdrgm:Gamma="1"
hdrgm:OffsetSDR="0"
hdrgm:OffsetHDR="0"
hdrgm:HDRCapacityMin="0"
hdrgm:HDRCapacityMax="2.32193"
hdrgm:BaseRenditionIsHDR="False"

Метаданные сами по себе почти не занимают места. Основной дополнительный объём создаёт JPEG с картой усиления.

MPF: где искать вторую картинку

В Ultra HDR основным механизмом обнаружения gain map служит не MPF, а XMP-каталог Google Photos Container. Он лежит в APP1 XMP базового кадра. Container:Directory перечисляет основной элемент с Item:Semantic="Primary" и дополнительный с Item:Semantic="GainMap"; Item:Length задаёт длину добавленного изображения. В исследуемом файле каталог выглядит так:

<Container:Directory>
  <rdf:Seq>
    <rdf:li rdf:parseType="Resource">
      <Container:Item
        Item:Semantic="Primary"
        Item:Mime="image/jpeg"/>
    </rdf:li>
    <rdf:li rdf:parseType="Resource">
      <Container:Item
        Item:Semantic="GainMap"
        Item:Mime="image/jpeg"
        Item:Length="931559"/>
    </rdf:li>
  </rdf:Seq>
</Container:Directory>

MPF, Multi-Picture Format, идёт дополнением: его индекс в APP2 хранит размеры, смещения и типы нескольких JPEG-изображений. Это похоже на второе оглавление того же контейнера. Спецификация Ultra HDR требует после записи вторичного изображения добавить и MPF, и GContainer XMP. Ни один из них сам по себе не описывает математику яркости.

Рядом присутствует метка urn:iso:std:iso:ts:21496:-1 и бинарные данные ISO 21496-1. Стандарт определяет gain map как способ преобразования между двумя представлениями динамического диапазона. В этом файле есть Adobe XMP-поля и ISO-вариант метаданных; GContainer и MPF указывают расположение изображений.

Rendition: представление одной фотографии

Rendition здесь означает не новый сюжет и не копию другого размера, а одно визуальное представление той же сцены. Например:

  • SDR rendition рассчитано на обычный диапазон дисплея;
  • HDR rendition сохраняет более яркие света;
  • gain map содержит разницу, необходимую для перехода между ними.

BaseRenditionIsHDR="False" означает, что основной JPEG является SDR-представлением. Если программа игнорирует всё после него, фотография всё равно выглядит корректно.

Что кодирует gain map

Gain map, или карта усиления, похожа на чёрно-белую маску. Каждая её точка сообщает не готовую яркость, а множитель для соответствующего участка базового изображения:

Базовый SDR-кадр фотографии с Xiaomi 17

Базовый кадр: цветное SDR-представление, которое видят программы без поддержки gain map.

Извлечённая из фотографии одноканальная gain map

Gain map: светлые области требуют большего усиления, вплоть до ×5; тёмные остаются без усиления, то есть ×1.

flowchart LR
    SDR[SDR-база<br/>цвет и мелкие детали]
    MAP[Gain map<br/>тёмное: около ×1<br/>светлое: до ×5]
    META[Метаданные<br/>min · max · gamma<br/>capacity · offsets]
    HEAD[Запас дисплея<br/>HDR white / SDR white]
    DEC[HDR-декодер<br/>растянуть карту<br/>вычислить weight<br/>усилить линейный RGB]
    OUT[Представление для экрана<br/>SDR · промежуточное · полное HDR]

    SDR --> DEC
    MAP --> DEC
    META --> DEC
    HEAD --> DEC
    DEC --> OUT

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

Упрощённая математика для SDR-базы выглядит так:

map       = значение пикселя карты от 0 до 1
logGain   = lerp(GainMapMin, GainMapMax, map ^ (1 / Gamma))
boost     = log2(HDR-белый дисплея / SDR-белый дисплея)
weight    = clamp((boost - HDRCapacityMin) /
                  (HDRCapacityMax - HDRCapacityMin), 0, 1)
HDR       = (SDR + OffsetSDR) × 2 ^ (logGain × weight) - OffsetHDR

lerp(a, b, t) выбирает положение между минимумом и максимумом, а clamp ограничивает результат диапазоном 0...1. Если Gamma равна 1, как в этом файле, степень не меняет карту. Реальная операция выполняется над линейными цветовыми значениями, а не прямо над числами 0...255 из JPEG.

Например, возьмём линейное значение SDR-компонента 0,4, значение карты 0,5 и дисплей, у которого HDR-белый в 4 раза ярче SDR-белого. Для параметров этого файла расчёт проходит насквозь так:

logGain = lerp(0, 2,32193, 0,5) = 1,160965
boost   = log2(4) = 2
weight  = 2 / 2,32193 ≈ 0,86135
HDR     = 0,4 × 2 ^ (1,160965 × 0,86135)
        = 0,4 × 2¹ = 0,8

То есть этот участок получает двукратное усиление: не максимальные ×5 из метаданных, потому что значение карты находится посередине, а запас дисплея ограничен четырёхкратным отношением белого.

Есть и цена в байтах. Совместимая JPEG-база заканчивается на смещении 4 773 112, а весь файл занимает 5 704 671 байт. Значит, второй JPEG с gain map и его служебными данными добавляет 931 559 байт: 931 559 / 4 773 112 ≈ 19,5% к размеру обычной базы.
Поля означают следующее:

Поле Смысл в декодировании Значение в файле
GainMapMin минимальный логарифм усиления 0, то есть 2⁰ = ×1
GainMapMax максимальный логарифм усиления 2,32193, то есть 2²·³²¹⁹³ ≈ ×5
Gamma показатель кодирования карты, который декодер обращает степенью 1 / Gamma 1, без дополнительного изгиба
OffsetSDR/HDR малые смещения до и после умножения, полезные около чёрного 0 и 0
HDRCapacityMin/Max при каком запасе дисплея начинать и заканчивать применение карты от 0 до 2,32193 ступени
BaseRenditionIsHDR является ли базовый JPEG HDR-стороной преобразования False, база SDR

Числа заданы в ступенях экспозиции, то есть как логарифмы по основанию 2. Поэтому GainMapMax="2.32193" кодирует верхнюю границу примерно ×5, а не 2,32-кратное усиление. Это не обещает, что любой пиксель станет в пять раз ярче: значение зависит от самой карты, а weight ограничивает эффект возможностями экрана.

Почему Gamma - не та же gamma экрана

Слово gamma используется в двух местах:

  1. hdrgm:Gamma изгибает значения внутри gain map, чтобы точнее распределить её ограниченные коды;
  2. передаточная функция изображения, например приближённая gamma sRGB, связывает код цвета с линейным светом.

Это разные этапы. В данном файле ImageMagick сообщает gamma базового изображения около 0.4545, а поле gain map равно 1. Первое относится к интерпретации цвета, второе - к декодированию карты усиления. Gain map также не равна обычной gamma-коррекции: gamma применяет одну кривую ко всем пикселям с одинаковым входным значением, а карта может задать разное усиление двум участкам одинаковой исходной яркости.

Обычный белый интерфейса служит точкой отсчёта SDR. HDR-дисплей имеет headroom, или запас яркости выше этой точки. Декодер узнаёт доступный запас, вычисляет weight, восстанавливает HDR-пиксели по карте и передаёт их оконной системе. Поэтому блик может физически излучать больше света, хотя базовый JPEG всё ещё ограничен значением 255.

Нужны одновременно три условия:

  1. файл содержит базу, gain map и корректные метаданные;
  2. приложение умеет их декодировать;
  3. дисплей и режим системы предоставляют HDR-запас.

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

Почему не просто 10-битный AVIF, HEIC или JPEG XL

Эти форматы тоже умеют хранить HDR: спецификация AVIF поддерживает HDR и разрядности AV1, Apple описывает 10-битный HEIF для ISO HDR, а JPEG Committee указывает для JPEG XL HDR и высокую разрядность. Но 10-битный HDR-кадр сам по себе не даёт старому JPEG-декодеру готовую SDR-версию и не кодирует две художественно согласованные версии для разного запаса яркости. Ultra HDR решает именно эту задачу: обычный 8-битный JPEG остаётся безопасной SDR-базой, а gain map позволяет плавно получить промежуточный или полный HDR.

Это не противопоставление JPEG и HEIF. Apple Adaptive HDR использует ту же стандартизованную в ISO 21496-1 модель SDR + gain map и определяет её хранение как в HEIF, так и в JPEG. Начиная с iOS 18, iPhone 15 и iPhone 15 Pro перешли на Adaptive HDR; в HEIC gain map хранится как дополнительное изображение HEIF, а не как второй JPEG. Поэтому выбор контейнера и выбор gain map являются разными решениями.

Где gain map действительно работает

Поддержка состоит из двух уровней: программа должна сохранить и прочитать gain map, а система с дисплеем должны дать ей HDR-запас. На дату статьи подтверждены следующие сценарии:

Что срезает HDR, тоже определяется не расширением файла, а трактом обработки. Google Photos предупреждает, что неподдерживаемая правка сохраняет результат как SDR, а highlight video всегда использует SDR-версии. Обычный декодер, который прочитал базовый JPEG и затем заново закодировал пиксели, не переносит автоматически GContainer XMP, MPF, второй JPEG и ISO-метаданные. Напротив, стандартные операции Android вроде кадрирования и поворота могут сохранить Ultra HDR, если выполняются gain-map-aware API, поэтому нельзя утверждать, что любая обработка обязательно уничтожает карту.

Инструменты

В таблицу вошли только возможности, подтверждённые документацией самих проектов.

Инструмент Что умеет Отдаёт ли JPEG с gain map
Extra Bright Images Локально в браузере строит HDR из обычного изображения; настройки Make brights brighter, Gain и Highlight threshold действуют автоматически по всему кадру Да - кнопка Download Ultra HDR отдаёт Ultra HDR JPEG; отдельно доступен Native HDR PNG
Lightroom Импортирует, редактирует и экспортирует HDR; при сохранении JPEG с HDR-правками сохраняет SDR-изображение и HDR-детали в gain map Да - JPEG с HDR-правками сохраняется с gain map
Adobe Camera Raw Открывает и редактирует HDR, поддерживает локальные маски и сохраняет HDR в JPEG; отдельная справка по Gain Map описывает представление base + gain map + metadata Да - JPEG указан среди поддерживаемых HDR-форматов сохранения
libultrahdr CLI Эталонный кодек Ultra HDR: кодирует из HDR/SDR-представлений, собирает готовые SDR JPEG и gain map, декодирует и выводит метаданные в probe-режиме Да - ultrahdr_app по умолчанию создаёт out.jpeg с базой, gain map и метаданными
exiftool Читает XMP и MPF, показывает параметры вложенных изображений и позволяет извлечь дополнительный JPEG; документация помечает MPF-теги как недоступные для записи Нет - это инструмент анализа метаданных, не энкодер JPEG с gain map

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

Как не потерять HDR при публикации

Обычный обработчик изображений может прочитать только первый JPEG, уменьшить его и сохранить новый файл. Тогда MPF, XMP, ISO-метаданные и сама gain map исчезнут. Наличие .jpg и даже копирование EXIF не гарантируют сохранение HDR.

Проверить метаданные и извлечь карту можно воспроизводимыми командами из корня проекта:

# Показать Adobe HDR gain map, MPF и Google Photos Container.
exiftool -G1 -s -XMP-hdrgm:All -MPF:All -XMP-GContainer:All \
  content/files/IMG_20260816_183557.jpg

# Извлечь второе изображение из MPF без декодирования и повторного JPEG-сжатия.
exiftool -b -MPImage2 content/files/IMG_20260816_183557.jpg \
  > content/files/IMG_20260816_183557-gainmap.jpg

Если установленный ExifTool не отдаёт MPImage2, короткий Python-фолбэк находит второй маркер начала JPEG FF D8 и сохраняет данные до конца файла:

from pathlib import Path

src = Path("content/files/IMG_20260816_183557.jpg")
dst = Path("content/files/IMG_20260816_183557-gainmap.jpg")
data = src.read_bytes()
second_soi = data.find(b"\xff\xd8", 2)

if second_soi < 0 or not data.endswith(b"\xff\xd9"):
    raise SystemExit("second JPEG not found")

dst.write_bytes(data[second_soi:])
print(f"wrote {dst}: {len(data) - second_soi} bytes from offset {second_soi}")

Этот фолбэк подходит для данного файла, где второй JPEG занимает весь хвост. Для произвольного контейнера надёжнее использовать MPF или GContainer, потому что после дополнительного изображения могут находиться другие данные.

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

1. Есть ли внутри второй JPEG?
2. Остались ли MPF и метка ISO 21496-1 или XMP hdrgm?
3. Совпадает ли SDR-вид в старом просмотрщике?
4. Появляются ли дополнительные света в HDR-просмотрщике?

Именно поэтому снимок в этой статье подключён как исходный статический JPEG, а не как автоматически созданная WebP-миниатюра.

Короткий итог

Исследуемый файл содержит не «8-битный HDR-пиксель». Он содержит совместимый 8-битный SDR JPEG, второй одноканальный JPEG с пространственной картой усиления и инструкции по их объединению. EXIF рассказывает о съёмке, XMP и ISO 21496-1 описывают преобразование, GContainer XMP объявляет основной кадр и gain map, MPF дополняет его индексом изображений, а HDR-декодер собирает из них rendition под возможности конкретного дисплея.

Источники для дальнейшего чтения: