Рисуем светом: длинная выдержка на Android

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Всем привет, меня зовут Дмитрий, и я Android-разработчик в компании «MEL Science». Сегодня я хочу рассказать, как можно реализовать поддержку длинной выдержки на смартфонах, да так, чтобы получающуюся картинку можно было наблюдать прямо в процессе создания. А для заинтересовавшихся в конце статьи я подготовил ссылку на тестовое приложение - чтобы вы могли сами сделать крутое фото с длинной выдержкой.

Длинная выдержка

Выдержка - термин из мира фотографии, который определяет время открытия затвора при съемке. Чем дольше открыт затвор, тем дольше свет экспонирует светочувствительную матрицу. Проще говоря, делает фотографию более яркой. Современные фотоаппараты используют выдержки длинной в 1/2000 cекунды, что позволяет получить освещенную, но при этом не пересвеченную фотографию. Длинная выдержка подразумевает открытие затвора на секунду и больше. При верно выбранной сцене это позволяет получать фантастические фотографии, способные запечатлеть движение света в объективе камеры. Причем фотографировать можно что угодно: ночные улицы с мчащимися машинами или маятник, с укрепленным на нем фонариком, позволяющим выписывать фигуры Лиссажу. А можно вообще рисовать светом самому и получать целые картины-фотографии.

Улицы города, сфотографированные с использованием длинной выдержки
Улицы города, сфотографированные с использованием длинной выдержки

Теория

Для создания эффекта длинной выдержки можно использовать два подхода:

  • аппаратный - состоит в управлении физическим открытием и закрытием затвора

  • программный - эмулирует длинную выдержку за счет объединения большого числа обычных кадров

Главным недостатком аппаратного подхода является отсутствие возможности наблюдать за процессом появления фотографии онлайн - результат будет виден лишь после закрытия затвора и формирования изображения. Нарисовать что-либо светом человеку без опыта в таком режиме вряд ли удастся. Еще одним недостатком становится ограничение смартфонов на максимальное время выдержки - на Android оно составляет 30 секунд.

Программная эмуляция позволяет избавиться от всех недостатков ценой усложнения кода.

Практика

Для реализации работы с камерой смартфона будем использовать API CameraX. Это обусловлено ее гибкостью и лаконичностью. Также для программного подхода нам потребуется OpenGL ES для работы с изображениями. Данный выбор был сделан так как, это позволит работать напрямую с изображениями в видео памяти и обеспечить минимальную задержку при записи, так как вся обработка изображений происходит в реальном времени.

Аппаратный подход

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

Для тонкой настройки камеры, используем Camera2Interop который позволяет устанавливать флаги настройки камеры вручную, как в Camera2API. Для активации длинной выдержки помимо установки времени выдержки необходимо также отключить автоэкспозицию, т.к. иначе выдержка будет управляться камерой самостоятельно.

val imageCaptureBuilder = ImageCapture.Builder()
Camera2Interop.Extender(imageCaptureBuilder).apply {  
  setCaptureRequestOption(
    CaptureRequest.CONTROL_AE_MODE,
    CaptureRequest.CONTROL_AE_MODE_OFF
  )
  setCaptureRequestOption(
    CaptureRequest.SENSOR_EXPOSURE_TIME,
    EXPOSURE_TIME_SEC * NANO_IN_SEC
  )
}

Кстати, для каждой камеры диапазон допустимого интервала выдержки может быть разным, чтобы его узнать необходимо запросить cameraCharacteristics

val manager = getSystemService(CAMERA_SERVICE) as CameraManager
for (cameraId in manager.cameraIdList) {
  val chars = manager.getCameraCharacteristics(cameraId)
  val range = chars.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE)  
  Log.e("CameraCharacteristics", "Camera $cameraId range: ${range.toString()}")
}

Программный подход

Для начала определимся с общей идеей нашей реализации.

  1. Нам потребуется буффер для хранения формирующегося изображения, а также регулярно обновляемое изображение с камеры.

  2. Каждый новый кадр будем объединять с хранящимся в буффере, обрабатывая попиксельно и оставляя в буфере пиксель с наибольшей яркостью.

  3. Для того чтобы придать нашим картинкам эффект постепенного исчезновения света можем долго неизменяемые пиксели постепенно затемнять.

Я не буду приводить здесь весь код рендеринга, так как это займет слишком много места. Для желающих код можно найти здесь. Представлю лишь блок схему, объясняющую последовательность действий для получения очередного кадра на экране. 

Как видно из схемы, основная магия происходит при объединении 2х кадров: сохраненного в фреймбуфере и полученного с камеры. Рассмотрим шейдер для этой задачи подробнее.

#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform mat4 stMatrix;
uniform texType0 tex_sampler;
uniform texType1 old_tex_sampler;
varying vec2 v_texcoord;
void main() {    
    vec4 color = texture2D(tex_sampler, (stMatrix * vec4(v_texcoord.xy, 0, 1)).xy);
    vec4 oldColor = texture2D(old_tex_sampler, v_texcoord);  
    float oldBrightness = oldColor.r * 0.2126 + oldColor.g * 0.7152 + oldColor.b * 0.0722 + oldColor.a; 
    float newBrightness = color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722 + color.a;
  // объединяем пиксели
}

Работа шейдера состоит из нескольких этапов:

  1. К текстуре камеры мы применяем матрицу для получения верной ориентации изображения.

  2. Затем вычисляем яркость пикселя на обоих кадрах

  3. Объединяем пиксели

Для того чтобы понять, как правильно объединить пиксели, необходимо вспомнить принцип работы длинной выдержки - “длинная выдержка позволяет свету попадать на матрицу камеры длительное время и сохраняться до конца съемки”. То есть, если в течение процесса фотографии, в разных местах кадра будет появляться свет, то он должен оставаться в кадре до конца. При этом, длительность освещения пикселя не важна, т.к. однажды попавший в кадр свет сохранится до завершения съемки, даже если все остальное время свет на этот пиксель попадать не будет. 

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

Тогда объединение пикселей будет выглядеть вот так:

if (newBrightness > oldBrightness) {
  gl_FragColor = color;
} else {
  gl_FragColor = oldColor;
}

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

Длинная выдержка
Длинная выдержка

Однако любая ошибка при рисовании требует перезапуска камеры, т.к. один раз попавший на нее свет уже нельзя стереть! Такое поведение приемлемо при выполнении каких-то заранее спланированных фотографий, но что если мы хотим просто рисовать светом и сохранять изображение, лишь когда нам действительно понравился результат? Перезапускать постоянно камеру совсем неудобно. Значит свет все-таки должен пропадать через какое то время. Этого можно добиться с помощью постепенного затухания ярких пикселей. Чтобы добиться такого эффекта достаточно просто на каждом новом шаге добавлять к каждому пикселю немного черного цвета (чтобы сохранять корректность картинки мы будем добавлять не черный цвет, а просто более темный пиксель из доступных - это позволит и эффект угасания получить и сохранить гамму цветов). Тогда объединение пикселей будет выглядеть следующим образом 

if (newBrightness > oldBrightness) {  
  gl_FragColor = mix(color, oldColor, 0.01);
} else { 
  gl_FragColor = mix(oldColor, color, 0.01);
}
Вот несколько примеров с разными коэффициентами и временем затухания света.
Коэффициент 0.001
Коэффициент 0.001
Коэффициент 0.01
Коэффициент 0.01
Коэффициент 0.5
Коэффициент 0.5

Заключение

 Вот несколько примеров, что умеет получившееся приложение. 
Все-таки я не художник :( А что нарисуете вы?
Вот несколько примеров, что умеет получившееся приложение. Все-таки я не художник :( А что нарисуете вы?

На этом на сегодня все. Для желающих попробовать самому полный код приложения и apk можно найти здесь.

Источник: https://habr.com/ru/post/558728/


Интересные статьи

Интересные статьи

Последние 6 лет я работаю экспертом по информационной безопасности в Одноклассниках и отвечаю за безопасность приложений. Мой доклад сегодня — о механизмах межпроцессного взаимодействия...
У некоторых бизнес-тренеров в области е-коммерса и консультантов по увеличению интернет-продаж на многие вопросы часто можно слышать универсальную отмазку — «надо тестировать» или другую (чтобы не...
Привет, Хабр! Представляю вашему внимаю перевод статьи из журнала APC. Машинное обучение и интеллектуальный анализ данных – это практические разработки ИИ, благодаря которым появляются прилож...
Но если для интернет-магазина, разработанного 3–4 года назад «современные» ошибки вполне простительны потому что перед разработчиками «в те далекие времена» не стояло таких задач, то в магазинах, сдел...
Привет, Хабр! Напоминаем, что у нас уже открыт предзаказ на долгожданную книгу о языке Kotlin из знаменитой серии Big Nerd Ranch Guides. Сегодня мы решили предложить вашему вниманию перевод ст...