Введение в обработку изображений Python в вычислительной фотографии

Опубликовано: 2022-03-11

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

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

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

Фотография при слабом освещении

Допустим, мы хотим сделать снимок сцены при слабом освещении, но камера имеет маленькую апертуру (объектив) и ограниченное время экспозиции. Это типичная ситуация для камер мобильных телефонов, которые при слабом освещении могли бы создать такое изображение (снято камерой iPhone 6):

Изображение пары игрушек в условиях низкой освещенности

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

То же изображение, что и выше, намного ярче, но с отвлекающим визуальным шумом.

Что просходит? Откуда весь этот шум?

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

Некоторое количество шума всегда будет присутствовать в устройстве обработки изображений; однако, если сигнал (полезная информация) имеет высокую интенсивность, шум будет незначительным (высокое отношение сигнал/шум). Когда сигнал слабый, например, при слабом освещении, шум будет выделяться (от низкого уровня сигнала к шуму).

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

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

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

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

Четырехпанельная демонстрация треугольника, разрозненное изображение, представляющее треугольник с добавленным шумом, своего рода зубчатый треугольник, представляющий среднее значение 50 экземпляров, и среднее значение 1000 экземпляров, которое выглядит почти идентично исходному треугольнику.

Мы видим, что, хотя шум достаточно силен, чтобы полностью исказить сигнал в любом отдельном случае, усреднение постепенно уменьшает шум, и мы восстанавливаем исходный сигнал.

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

Больше снимков, как правило, означает лучшее качество, но точное число зависит от ситуации: сколько света, насколько чувствительна камера и т. д. Хороший диапазон может быть где-то между 10 и 100.

Когда у нас есть эти изображения (если возможно, в необработанном формате), мы можем читать и обрабатывать их в Python.

Для тех, кто не знаком с обработкой изображений в Python, следует отметить, что изображение представляется в виде двумерного массива байтовых значений (0–255), то есть для монохромного изображения или изображения в градациях серого. Цветное изображение можно рассматривать как набор из трех таких изображений, по одному для каждого цветового канала (R, G, B), или фактически трехмерный массив, индексированный по вертикальному положению, горизонтальному положению и цветовому каналу (0, 1, 2). .

Мы будем использовать две библиотеки: NumPy (http://www.numpy.org/) и OpenCV (https://opencv.org/). Первый позволяет нам очень эффективно выполнять вычисления с массивами (с удивительно коротким кодом), в то время как OpenCV обрабатывает чтение/запись файлов изображений в этом случае, но гораздо более эффективен, предоставляя множество расширенных графических процедур, некоторые из которых мы рассмотрим ниже. использовать далее в статье.

 import os import numpy as np import cv2 folder = 'source_folder' # We get all the image files from the source folder files = list([os.path.join(folder, f) for f in os.listdir(folder)]) # We compute the average by adding up the images # Start from an explicitly set as floating point, in order to force the # conversion of the 8-bit values from the images, which would otherwise overflow average = cv2.imread(files[0]).astype(np.float) for file in files[1:]: image = cv2.imread(file) # NumPy adds two images element wise, so pixel by pixel / channel by channel average += image # Divide by count (again each pixel/channel is divided) average /= len(files) # Normalize the image, to spread the pixel intensities across 0..255 # This will brighten the image without losing information output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) # Save the output cv2.imwrite('output.png', output)

Результат (с применением автоконтрастности) показывает, что шум исчез, что является очень большим улучшением по сравнению с исходным изображением.

Исходная фотография игрушек, на этот раз более яркая и четкая, с очень небольшим заметным шумом.

Тем не менее, мы все еще замечаем некоторые странные артефакты, такие как зеленоватая рамка и сетчатый узор. На этот раз это не случайный шум, а фиксированный шаблонный шум. Что случилось?

Крупный план верхнего левого угла изображения выше.

Крупный план верхнего левого угла с зеленой рамкой и сеткой.

Опять же, мы можем обвинить в этом датчик. В этом случае мы видим, что разные части сенсора по-разному реагируют на свет, в результате чего получается видимый рисунок. Некоторые элементы этих паттернов являются регулярными и, скорее всего, связаны с подложкой сенсора (металл/кремний) и тем, как она отражает/поглощает входящие фотоны. Другие элементы, такие как белый пиксель, представляют собой просто дефектные сенсорные пиксели, которые могут быть чрезмерно чувствительными или чрезмерно нечувствительными к свету.

К счастью, есть способ избавиться и от этого типа шума. Это называется вычитанием темного кадра.

Для этого нам нужно изображение самого паттернового шума, и его можно получить, если мы сфотографируем темноту. Да, правильно — просто закройте отверстие камеры и сделайте много снимков (скажем, 100) с максимальной выдержкой и значением ISO и обработайте их, как описано выше.

При усреднении по множеству черных кадров (которые на самом деле не являются черными из-за случайного шума) мы получим фиксированный шаблонный шум. Мы можем предположить, что этот фиксированный шум останется постоянным, поэтому этот шаг необходим только один раз: полученное изображение можно повторно использовать для всех будущих снимков при слабом освещении.

Вот как выглядит верхняя правая часть фонового шума (с поправкой на контраст) для iPhone 6:

Шаблонный шум для части кадра, отображаемой на предыдущем изображении.

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

Получив значение этого шума темного кадра (в переменной average_noise ), мы можем просто вычесть его из нашего снимка, прежде чем нормализовать:

 average -= average_noise output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) cv2.imwrite('output.png', output)

Вот наше финальное фото:

Еще одно изображение фотографии, на этот раз без каких-либо признаков того, что она была сделана при слабом освещении.

Расширенный динамический диапазон

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

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

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

Но это компромисс. Многие детали не попадают в финальную фотографию. На двух изображениях ниже мы видим одну и ту же сцену, снятую с разной выдержкой: очень короткая выдержка (1/1000 с), средняя выдержка (1/50 с) и длинная выдержка (1/4 с).

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

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

Хорошей новостью является то, что мы можем кое-что с этим поделать, и опять же это включает в себя построение нескольких кадров с небольшим количеством кода Python.

Подход, который мы выберем, основан на работе Пола Дебевека и др., который описывает этот метод в своей статье здесь. Метод работает следующим образом:

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

Затем используется алгоритм для восстановления кривой отклика камеры на основе цвета одних и тех же пикселей при разном времени экспозиции. Это в основном позволяет нам установить карту между реальной яркостью сцены точки, временем экспозиции и значением, которое будет иметь соответствующий пиксель в захваченном изображении. Мы будем использовать реализацию метода Дебевека из библиотеки OpenCV.

 # Read all the files with OpenCV files = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg'] images = list([cv2.imread(f) for f in files]) # Compute the exposure times in seconds exposures = np.float32([1. / t for t in [1000, 500, 100, 50, 10]]) # Compute the response curve calibration = cv2.createCalibrateDebevec() response = calibration.process(images, exposures)

Кривая отклика выглядит примерно так:

График, отображающий кривую отклика в зависимости от экспозиции пикселя (логарифм) в зависимости от значения пикселя.

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

Эта кривая позволяет нам выполнить обратную операцию (которая является следующим шагом в процессе) — учитывая значение пикселя и время экспозиции, мы можем вычислить реальную яркость каждой точки в сцене. Это значение яркости называется освещенностью и измеряет количество световой энергии, попадающей на единицу площади сенсора. В отличие от данных изображения, они представлены с использованием чисел с плавающей запятой, поскольку отражают гораздо более широкий диапазон значений (следовательно, высокий динамический диапазон). Получив изображение освещенности (изображение HDR), мы можем просто сохранить его:

 # Compute the HDR image merge = cv2.createMergeDebevec() hdr = merge.process(images, exposures, response) # Save it to disk cv2.imwrite('hdr_image.hdr', hdr)

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

Для остальных из нас хорошая новость заключается в том, что мы все еще можем использовать эти данные, хотя для нормального отображения требуется, чтобы изображение имело каналы со значением байта (0-255). Хотя нам нужно отказаться от части богатства карты освещенности, по крайней мере, у нас есть контроль над тем, как это сделать.

Этот процесс называется тональным отображением и включает в себя преобразование карты освещенности с плавающей запятой (с большим диапазоном значений) в изображение со стандартным байтовым значением. Существуют методы, позволяющие сохранить многие дополнительные детали. Просто чтобы дать вам пример того, как это может работать, представьте, что перед тем, как мы сжимаем диапазон с плавающей запятой в байтовые значения, мы улучшаем (заостряем) края, присутствующие в HDR-изображении. Улучшение этих краев поможет сохранить их (и неявно предоставляемую ими детализацию) также в изображении с низким динамическим диапазоном.

OpenCV предоставляет набор таких операторов отображения тонов, таких как Drago, Durand, Mantiuk или Reinhardt. Вот пример того, как можно использовать один из этих операторов (Дюран) и какой результат он дает.

 durand = cv2.createTonemapDurand(gamma=2.5) ldr = durand.process(hdr) # Tonemap operators create floating point images with values in the 0..1 range # This is why we multiply the image with 255 before saving cv2.imwrite('durand_image.png', ldr * 255) 

Результат вышеуказанного вычисления отображается в виде изображения

Используя Python, вы также можете создавать свои собственные операторы, если вам нужен больший контроль над процессом. Например, это результат, полученный с помощью пользовательского оператора, который удаляет интенсивности, представленные в очень небольшом количестве пикселей, перед сужением диапазона значений до 8 бит (за которым следует шаг автоконтрастности):

Изображение, полученное в результате выполнения описанного выше процесса

А вот код вышеуказанного оператора:

 def countTonemap(hdr, min_fraction=0.0005): counts, ranges = np.histogram(hdr, 256) min_count = min_fraction * hdr.size delta_range = ranges[1] - ranges[0] image = hdr.copy() for i in range(len(counts)): if counts[i] < min_count: image[image >= ranges[i + 1]] -= delta_range ranges -= delta_range return cv2.normalize(image, None, 0, 1, cv2.NORM_MINMAX)

Заключение

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

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

Если вас интересуют вычисления изображений на мобильном устройстве, ознакомьтесь с учебным пособием по OpenCV: Обнаружение объектов в реальном времени с использованием MSER в iOS, написанным товарищем по Toptaler и элитным разработчиком OpenCV Алтайбаяром Цевеенбаяром.