Перейти к основному содержимому

Глава 16: Баланс белого и цвет

Вы освоили яркость (экспозиция) и резкость (фокус). Теперь пришло время управлять видомцветовым тоном изображения.

Когда вы фотографируете белый лист бумаги под лампой накаливания, ее теплый оранжевый свет падает на бумагу, и сенсор видит ее оранжевой. Ваш мозг мгновенно корректирует это и всё равно видит «белую бумагу», но сырые данные сенсора фиксируют правду: она оранжевая.

Баланс белого (White Balance, WB) — это процесс компенсации цвета источника света камерой, чтобы нейтральные белые объекты выглядели нейтральными. Ошибитесь здесь — и всё фото получит нежелательный оттенок (слишком оранжевый, синий или зеленый).

Приложение Android Camera Parameters демонстрирует все пресеты AWB в виде сетки и предоставляет слайдеры ручного усиления — откройте приложение, перейдите на панель White Balance, и вы сможете увидеть в действии именно то, что мы реализуем в этой главе.


Цветовая температура: Спектр от теплого к холодному

Источники света описываются их цветовой температурой в Кельвинах (К). Шкала описывает температуру теоретического «абсолютно черного тела», которое светится таким же цветом.

Парадоксальное правило: Теплый свет = низкое число Кельвинов (свеча 1800К = очень оранжевая). Холодный свет = высокое число Кельвинов (небо 10000К = очень синее). Ваши глаза привыкают к этому с детства; вашему коду нужно помнить об этом явно.

СценаТипичная цветовая темп.Оттенок, если выбран WB Daylight
Ужин при свечах1800–2200KГустой оранжевый / янтарный
Домашняя лампа2700–3000KОранжевый / желтый
Восход / Закат3000–4000KТеплый золотистый (часто желаемый!)
Люминесцентная лампа4000–5000KЗеленоватый оттенок
Полдень, солнце5200–5800KНейтральный правильный
Фотовспышка5500–6000KНейтральный (соответствует дню)
Сильная облачность6000–7500KСлегка синеватый
Тень (без солнца)7000–9000KСиний оттенок
Чистое синее небо9000–12000KГлубокий синий

Работа автобаланса белого (AWB): определить наиболее вероятный источник света по статистике сцены, а затем вычесть цветовой оттенок, чтобы нейтральные объекты выглядели нейтральными.


Режимы автоматического баланса белого (AWB) в Camera2

Устанавливаются через CaptureRequest.CONTROL_AWB_MODE:

Режим (CONTROL_AWB_MODE_*)ЭффектСлучай использования
OFFТолько ручной баланс белого. Используйте COLOR_CORRECTION_GAINS или _TRANSFORM явно.Режим Pro, кастомная цветокоррекция, RAW
AUTOПо умолчанию. ISP непрерывно анализирует сцену.Обычная фотография
INCANDESCENT~2800K. Сильное синее усиление для компенсации ламп накаливания.Дом, сцена, вечерний интерьер
FLUORESCENT~4500K. Усиления для офисных ламп (склонны к зеленому).Офис / школа
WARM_FLUORESCENT~3200K. Компенсация «теплых» люминесцентных трубок.Дом, современные эконом-лампы
DAYLIGHT~5500K. Стандартный профиль полуденного солнца.Улица, солнечный день, вспышка
CLOUDY_DAYLIGHT~6500K. Небольшое «утепление» для компенсации пасмурной погоды.Пасмурный день
TWILIGHTПрофиль «золотого часа» (~4500K).Закат, сумерки, теплый пейзаж
SHADE~7500K. Сильное красное усиление против глубокой синей тени.Портрет в тени, городская тень

Сначала проверьте поддержку режимов: Не каждое устройство поддерживает все 9 пресетов. Флагманы обычно поддерживают; бюджетные модели могут предлагать только AUTO + OFF.

val availableAwbModes = characteristics.get(
CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES
) ?: intArrayOf()
Log.d("AWB", "Доступные режимы: ${availableAwbModes.toList()}")

Состояния AWB (как у AF, но менее «разговорчивые»)

Конечный автомат AWB концептуально похож на AF, но проще — у него меньше состояний:

Состояние AWBЗначение
CONTROL_AWB_STATE_INACTIVEAWB выключен (AWB_MODE = OFF)
CONTROL_AWB_STATE_SEARCHINGПоиск правильного осветителя (цвет может «плавать»)
CONTROL_AWB_STATE_CONVERGEDСтабильный осветитель найден — цвет зафиксирован
CONTROL_AWB_STATE_LOCKEDЯвно заблокирован через CONTROL_AWB_LOCK = true

Используйте тот же паттерн «ожидания схождения перед захватом», что и для AF, для критически важной по цвету съемки (товары для каталогов, репродукции).


Как работает коррекция баланса белого: Изнутри

AWB применяет две трансформации цвета, чтобы перейти от RGB сенсора к отображаемому sRGB. Понимание этого позволит вам полностью заменить AWB ручными значениями.

Шаг 1: Канальные усиления (Коррекция точки белого)

Сначала умножьте каждый цветовой канал на коэффициент (gain), чтобы нейтральная поверхность имела равные значения R, G, B:

Если сцена с лампой 3200К дает [R=200, G=150, B=100] с сенсора для серой цели, AWB применит коэффициенты примерно R: 1.0, G: 1.33, B: 2.0, чтобы нормализовать до [200, 200, 200].

В Camera2 это представлено как CaptureRequest.COLOR_CORRECTION_GAINS: массив из 4 чисел float в порядке [R, Geven, B, Godd].

Два зеленых канала (Geven, Godd) существуют потому, что многие сенсоры используют сетку Байера 2×2: чередующиеся строки GR / BG. Строки, начинающиеся с Green-R и Green-B, имеют немного разную спектральную чувствительность и требуют независимых цифровых усилений. Для обычной работы можно устанавливать оба зеленых в одно и то же значение.

// COLOR_CORRECTION_GAINS = [ R gain, G-even gain, B gain, G-odd gain ]
val warmGains = floatArrayOf(1.0f, 1.2f, 0.8f, 1.2f) // Теплый: больше R, меньше B
val coolGains = floatArrayOf(0.85f, 1.0f, 1.25f, 1.0f) // Холодный: больше B, меньше R
val neutralGains = floatArrayOf(1.0f, 1.0f, 1.0f, 1.0f) // Единичные усиления (сырой цвет)

Допустимый диапазон: Коэффициенты обычно ограничиваются HAL в диапазоне [0.0, 4.0]. Используйте значения от 0.5× до 3× для правдоподобных результатов.

Шаг 2: Матрица цветовой трансформации 3×3 (Маппинг охвата)

Канальные усиления корректируют только точку белого. Но разные сенсоры имеют разные нативные спектральные характеристики фильтров, а разные устройства отображения — разный цветовой охват (sRGB/Rec.709 против DCI-P3). Матрица цветокоррекции (CCM) 3×3 сопоставляет нативное цветовое пространство сенсора со стандартным выходным пространством.

Математически:

[ R' ] [ m11 m12 m13 ] [ R ]
[ G' ] = [ m21 m22 m23 ] [ G ]
[ B' ] [ m31 m32 m33 ] [ B ]

Или в коде: выход = M × вход, где M — матрица 3×3.

Camera2 предоставляет это через COLOR_CORRECTION_TRANSFORM, который устанавливается с помощью массива Rational[9] (построчно: m11, m12, m13, m21, m22, m23, m31, m32, m33). Единичная матрица = копирование входа без изменений:

// Единичная матрица 3x3: 1/1 по диагонали, 0/1 в остальных ячейках
val identityMatrix = arrayOf(
Rational(1,1), Rational(0,1), Rational(0,1),
Rational(0,1), Rational(1,1), Rational(0,1),
Rational(0,1), Rational(0,1), Rational(1,1)
)

Цветовые охваты Rec.709 и DCI-P3:

Цветовое пространствоОхватСлучай использования
Rec.709 (sRGB)~35% видимого светаТВ, веб, JPEG по умолчанию, 100% экранов до ~2020
DCI-P3~45% видимого светаЦифровое кино, 4K UHD, современные экраны флагманов

Экран P3 может показывать более насыщенные красные и зеленые цвета, чем Rec.709. Ваша выходная матрица CCM должна выбирать целевой охват, соответствующий ожиданиям экрана пользователя. В Android проверьте Display.isWideColorGamut() и используйте подходящую матрицу.

Практический совет: Если вы не пишете профессиональный RAW-проявитель или приложение для киносъемки, установите COLOR_CORRECTION_MODE = TRANSFORM_MATRIX и позвольте стандартной матрице производителя выполнять маппинг охвата. Большинство приложений режима Pro меняют только COLOR_CORRECTION_GAINS (4 коэффициента) и не трогают матрицу.


Полный пример 1: Фиксация AWB на пресете Daylight

Начнем с простого. Иногда вам не нужен полный ручной режим — вы просто хотите предотвратить «плавание» баланса белого между кадрами (например, в таймлапсе или видео со сменой сцен). Установка фиксированного пресета, такого как DAYLIGHT, гарантирует стабильность цвета.

Это простейшее ручное управление цветом.

class AwbPresetController(
private val characteristics: CameraCharacteristics,
private val captureSession: CameraCaptureSession,
private val previewSurface: Surface
) {
fun isModeSupported(mode: Int): Boolean {
val available = characteristics.get(
CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES
) ?: intArrayOf()
return mode in available
}

fun setPresetDaylightForWarmTintLock(): Boolean {
if (!isModeSupported(CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT)) {
Log.w("AWB", "Пресет DAYLIGHT не поддерживается")
return false
}

val request = captureSession.device.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW
).apply {
addTarget(previewSurface)

// Фиксируем баланс белого на DAYLIGHT (~5500K).
// Это заставит интерьерные сцены выглядеть теплыми/оранжевыми,
// что является "кинематографичным" стилем.
set(CaptureRequest.CONTROL_AWB_MODE,
CameraMetadata.CONTROL_AWB_MODE_DAYLIGHT)

set(CaptureRequest.CONTROL_MODE,
CameraMetadata.CONTROL_MODE_AUTO)
}

captureSession.setRepeatingRequest(request.build(),
object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
val awbState = result.get(CaptureResult.CONTROL_AWB_STATE)
Log.d("AWB", "Пресет DAYLIGHT применен, состояние AWB=$awbState")
}
}, null
)
return true
}
}

Художественное применение: Если вы снимаете закат с AWB_MODE = DAYLIGHT, свет заката (3000К) будет восприниматься как теплый относительно фиксированных 5500К — в итоге получатся насыщенные золотисто-оранжевые тона. Режим AUTO здесь бы нейтрализовал закат (в чем смысл?!), добавляя синего для компенсации золотого света. Пресеты сохраняют настроение.


Полный пример 2: Полный ручной AWB — Теплые закатные усиления

Для максимального контроля выключите AWB и задайте собственные усиления. Создадим стиль «теплый закат» — немного поднимем красный, приглушим синий и добавим каплю зеленого, чтобы избежать фиолетового оттенка.

class ManualColorGradingController(
private val characteristics: CameraCharacteristics,
private val captureSession: CameraCaptureSession,
private val previewSurface: Surface,
private val jpegReaderSurface: Surface
) {
object Presets {
val NEUTRAL = floatArrayOf(1.00f, 1.00f, 1.00f, 1.00f)
val WARM_SUNSET = floatArrayOf(1.00f, 1.20f, 0.80f, 1.20f) // Теплый янтарный
val COOL_MORNING = floatArrayOf(0.85f, 1.00f, 1.25f, 1.00f) // Холодный синий
val VINTAGE_KODAK = floatArrayOf(1.15f, 1.00f, 0.85f, 1.00f) // Пленочный стиль
}

fun applyManualGains(gains: FloatArray, includeMatrix: Boolean = true) {
val hwLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
val supportsManualColor = (hwLevel == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_3 ||
hwLevel == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL ||
isModeSupported(CameraMetadata.CONTROL_AWB_MODE_OFF))
if (!supportsManualColor) {
Log.e("AWB", "Это устройство уровня LEGACY не поддерживает ручные усиления")
return
}

val request = captureSession.device.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW
).apply {
addTarget(previewSurface)

// 1) ВЫКЛЮЧАЕМ AWB полностью
set(CaptureRequest.CONTROL_AWB_MODE, CameraMetadata.CONTROL_AWB_MODE_OFF)

// 2) Применяем 4 канальных усиления (R, Geven, B, Godd)
set(CaptureRequest.COLOR_CORRECTION_GAINS, gains)

// 3) Выбираем стратегию цветокоррекции
if (includeMatrix) {
// FAST: пусть HAL вычислит подходящую матрицу сам
// (матрица подбирается автоматически, пользователь управляет только усилениями)
set(CaptureRequest.COLOR_CORRECTION_MODE,
CameraMetadata.COLOR_CORRECTION_MODE_FAST)
} else {
// EXPERT: устанавливаем свою матрицу 3x3 + усиления вместе
set(CaptureRequest.COLOR_CORRECTION_MODE,
CameraMetadata.COLOR_CORRECTION_MODE_TRANSFORM_MATRIX)
set(CaptureRequest.COLOR_CORRECTION_TRANSFORM, identityMatrix())
}
}

captureSession.setRepeatingRequest(request.build(), null, null)
}

private fun identityMatrix(): Array<Rational> = arrayOf(
Rational(1,1), Rational(0,1), Rational(0,1),
Rational(0,1), Rational(1,1), Rational(0,1),
Rational(0,1), Rational(0,1), Rational(1,1)
)

private fun isModeSupported(mode: Int): Boolean {
val available = characteristics.get(
CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES
) ?: intArrayOf()
return mode in available
}
}

COLOR_CORRECTION_MODE: FAST против TRANSFORM_MATRIX

Используйте эту таблицу для выбора:

СценарийВыбирайте COLOR_CORRECTION_MODE =
Мне нужны только ручные усиления; пусть OEM выберет матрицуFAST
Я применяю внешнюю LUT / матрицу, мне нужен нетронутый цвет сенсораTRANSFORM_MATRIX + единичная матрица
У меня есть кастомный профиль цвета (ICC / DCP) для этого сенсораTRANSFORM_MATRIX + своя матрица 3x3

Предупреждение: TRANSFORM_MATRIX с единичной матрицей дает сырой цвет сенсора без маппинга охвата производителя. На многих сенсорах это выглядит заметно ненасыщенным и слегка зеленоватым. Это нормальное поведение — это «сырой» выход, готовый к вашей кастомной обработке.


Ручной конвертер Кельвинов в коэффициенты (Слайдер температуры)

Профессиональные приложения (включая Android Camera Parameters) содержат слайдер температуры в Кельвинах. Поскольку Camera2 не принимает Кельвины напрямую, мы аппроксимируем кривую усилений R/B.

Простое приближение, работающее для большинства сенсоров:

class KelvinGainsConverter {
// Конвертация Кельвинов [2000..10000] → примерные усиления [R, Geven, B, Godd]
fun kelvinToRgbGains(kelvin: Int): FloatArray {
val k = kelvin.coerceIn(2000, 10000)
val temp = k / 100.0

// Красный (теплый при низких K)
val r = if (temp <= 66) 255.0 else (329.698 * Math.pow(temp - 60, -0.133)).coerceIn(0.0, 255.0)

// Зеленый
val g = if (temp <= 66) {
(99.47 * Math.log(temp) - 161.11).coerceIn(0.0, 255.0)
} else {
(288.12 * Math.pow(temp - 60, -0.075)).coerceIn(0.0, 255.0)
}

// Синий (холодный при высоких K)
val b = when {
temp >= 66 -> 255.0
temp <= 19 -> 0.0
else -> (138.51 * Math.log(temp - 10) - 305.04).coerceIn(0.0, 255.0)
}

// Нормализуем так, чтобы GREEN = 1.0, затем инвертируем: нам нужны УСИЛЕНИЯ для компенсации.
// Если выбрано 2800К (тепло), нам нужно БОЛЬШЕ синего усиления, чтобы убрать желтизну.
val rGain = (g / r).toFloat().coerceIn(0.3f, 3.0f)
val bGain = (g / b).toFloat().coerceIn(0.3f, 3.0f)
return floatArrayOf(rGain, 1.0f, bGain, 1.0f)
}
}

Резюме

Баланс белого и коррекция цвета в Camera2 завершают трилогию ручного управления:

  • Цветовая температура (К): Низкая (1800К) = тепло/оранжево; высокая (10000К) = холодно/сине. AWB компенсирует это, нейтрализуя оттенок.
  • Режимы AWB: 9 пресетов (INCANDESCENTSHADE) + AUTO + OFF.
  • Состояния AWB: SEARCHING → CONVERGED → LOCKED.
  • Ручное управление имеет два уровня:
    1. COLOR_CORRECTION_GAINS = 4 коэффициента — коррекция точки белого.
    2. COLOR_CORRECTION_TRANSFORM = матрица 3×3 — маппинг охвата (gamut).
  • Rec.709 против DCI-P3: Матрица сопоставляет цвет сенсора с целевым охватом дисплея.

Что дальше

Вы изучили экспозицию, фокус и баланс белого по отдельности. В главе 17: Конвейер 3A мы наконец объединим все три системы в единую связную последовательность захвата:

  • Полный цикл триггер AF → AF locked → precapture AE → AE сошлась со вспышкой → захват фото.
  • Режимы вспышки AE.
  • Состояния AE и последовательность триггера precapture.
  • Работа на Kotlin, реализующая оркестрацию 3A с диаграммой Mermaid.

Эта глава превратит разрозненные знания в работающее приложение камеры профессионального уровня.