Introdução ao processamento de imagens Python em fotografia computacional

Publicados: 2022-03-11

A fotografia computacional trata de aprimorar o processo fotográfico com computação. Embora normalmente tenhamos a tendência de pensar que isso se aplica apenas ao pós-processamento do resultado final (semelhante à edição de fotos), as possibilidades são muito mais ricas, pois a computação pode ser ativada em todas as etapas do processo fotográfico - começando com a iluminação da cena, continuando com a lente e, eventualmente, até mesmo na exibição da imagem capturada.

Isso é importante porque permite fazer muito mais e de maneiras diferentes do que pode ser alcançado com uma câmera normal. Também é importante porque o tipo de câmera mais prevalente hoje em dia - a câmera móvel - não é particularmente poderosa em comparação com sua irmã maior (a DSLR), mas consegue fazer um bom trabalho aproveitando o poder de computação disponível no dispositivo .

Vamos dar uma olhada em dois exemplos em que a computação pode aprimorar a fotografia - mais precisamente, veremos como simplesmente tirar mais fotos e usar um pouco de Python para combiná-las pode criar bons resultados em duas situações em que o hardware da câmera móvel não realmente brilham - pouca luz e alta faixa dinâmica.

Fotografia com pouca luz

Digamos que queremos tirar uma fotografia com pouca luz de uma cena, mas a câmera tem uma pequena abertura (lente) e tempo de exposição limitado. Esta é uma situação típica para câmeras de telefones celulares que, em uma cena com pouca luz, podem produzir uma imagem como esta (tirada com uma câmera do iPhone 6):

Imagem de alguns brinquedos em um ambiente com pouca luz

Se tentarmos melhorar o contraste o resultado é o seguinte, o que também é bastante ruim:

A mesma imagem acima, muito mais brilhante, mas com um ruído visual perturbador

O que acontece? De onde vem todo esse barulho?

A resposta é que o ruído vem do sensor – o dispositivo que tenta determinar quando a luz o atinge e quão intensa é essa luz. Com pouca luz, no entanto, ele precisa aumentar muito sua sensibilidade para registrar qualquer coisa, e essa alta sensibilidade significa que ele também começa a detectar falsos positivos – fótons que simplesmente não existem. (Como observação lateral, esse problema não afeta apenas os dispositivos, mas também nós humanos: da próxima vez que você estiver em um quarto escuro, reserve um momento para perceber o ruído presente em seu campo visual.)

Alguma quantidade de ruído sempre estará presente em um dispositivo de imagem; no entanto, se o sinal (informação útil) tiver alta intensidade, o ruído será insignificante (alta relação sinal/ruído). Quando o sinal é baixo—como com pouca luz—o ruído se destacará (baixo sinal para ruído).

Ainda assim, podemos superar o problema do ruído, mesmo com todas as limitações da câmera, para obter fotos melhores do que a acima.

Para isso, precisamos levar em conta o que acontece ao longo do tempo: O sinal permanecerá o mesmo (mesma cena e assumimos que é estático) enquanto o ruído será completamente aleatório. Isso significa que, se tirarmos muitas fotos da cena, elas terão versões diferentes do ruído, mas as mesmas informações úteis.

Portanto, se calcularmos a média de muitas imagens tiradas ao longo do tempo, o ruído será cancelado enquanto o sinal não será afetado.

A ilustração a seguir mostra um exemplo simplificado: Temos um sinal (triângulo) afetado por ruído e tentamos recuperar o sinal calculando a média de várias instâncias do mesmo sinal afetadas por ruído diferente.

Uma demonstração do triângulo em quatro painéis, uma imagem dispersa representando o triângulo com ruído adicional, uma espécie de triângulo irregular representando a média de 50 instâncias e a média de 1000 instâncias, que parece quase idêntica ao triângulo original.

Vemos que, embora o ruído seja forte o suficiente para distorcer completamente o sinal em qualquer instância, a média reduz progressivamente o ruído e recuperamos o sinal original.

Vamos ver como esse princípio se aplica às imagens: Primeiro, precisamos tirar várias fotos do assunto com a exposição máxima que a câmera permite. Para obter melhores resultados, use um aplicativo que permita o disparo manual. É importante que as fotos sejam tiradas do mesmo local, então um tripé (improvisado) ajudará.

Mais fotos geralmente significam melhor qualidade, mas o número exato depende da situação: quanta luz há, quão sensível é a câmera etc. Um bom alcance pode estar entre 10 e 100.

Assim que tivermos essas imagens (em formato bruto, se possível), podemos lê-las e processá-las em Python.

Para aqueles que não estão familiarizados com o processamento de imagens em Python, devemos mencionar que uma imagem é representada como uma matriz 2D de valores de byte (0-255) - ou seja, para uma imagem monocromática ou em tons de cinza. Uma imagem colorida pode ser pensada como um conjunto de três dessas imagens, uma para cada canal de cor (R, G, B), ou efetivamente uma matriz 3D indexada pela posição vertical, posição horizontal e canal de cor (0, 1, 2). .

Faremos uso de duas bibliotecas: NumPy (http://www.numpy.org/) e OpenCV (https://opencv.org/). O primeiro nos permite realizar cálculos em arrays de forma muito eficaz (com código surpreendentemente curto), enquanto o OpenCV lida com a leitura/gravação dos arquivos de imagem neste caso, mas é muito mais capaz, fornecendo muitos procedimentos gráficos avançados - alguns dos quais veremos usar mais tarde no artigo.

 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)

O resultado (com auto-contraste aplicado) mostra que o ruído desapareceu, uma melhoria muito grande em relação à imagem original.

A fotografia original de brinquedos, desta vez mais brilhante e muito mais clara, com muito pouco ruído discernível

No entanto, ainda notamos alguns artefatos estranhos, como a moldura esverdeada e o padrão em forma de grade. Desta vez, não é um ruído aleatório, mas um ruído de padrão fixo. O que aconteceu?

Um close-up do canto superior esquerdo da imagem acima

Um close-up do canto superior esquerdo, mostrando a moldura verde e o padrão de grade

Novamente, podemos culpar o sensor. Nesse caso, vemos que diferentes partes do sensor reagem de maneira diferente à luz, resultando em um padrão visível. Alguns elementos desses padrões são regulares e provavelmente estão relacionados ao substrato do sensor (metal/silício) e como ele reflete/absorve fótons recebidos. Outros elementos, como o pixel branco, são simplesmente pixels de sensor defeituosos, que podem ser excessivamente sensíveis ou insensíveis à luz.

Felizmente, existe uma maneira de se livrar desse tipo de ruído também. Isso é chamado de subtração de quadro escuro.

Para isso, precisamos de uma imagem do próprio ruído padrão, e isso pode ser obtido se fotografarmos a escuridão. Sim, isso mesmo - apenas cubra o orifício da câmera e tire muitas fotos (digamos 100) com tempo máximo de exposição e valor ISO e processe-as conforme descrito acima.

Ao calcular a média de muitos quadros pretos (que não são de fato pretos, devido ao ruído aleatório), acabaremos com o ruído de padrão fixo. Podemos supor que esse ruído fixo permanecerá constante, portanto, essa etapa é necessária apenas uma vez: A imagem resultante pode ser reutilizada para todas as fotos futuras com pouca luz.

Aqui está como a parte superior direita do ruído padrão (contraste ajustado) se parece para um iPhone 6:

O ruído padrão para a parte do quadro exibida na imagem anterior

Novamente, notamos a textura em forma de grade, e até o que parece ser um pixel branco preso.

Uma vez que tenhamos o valor desse ruído de quadro escuro (na variável average_noise ), podemos simplesmente subtraí-lo de nossa foto até agora, antes de normalizar:

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

Segue nossa foto final:

Mais uma imagem da foto, desta vez sem absolutamente nenhuma evidência de ter sido tirada com pouca luz

Dinâmica de alto alcance

Outra limitação que uma câmera pequena (móvel) tem é sua pequena faixa dinâmica, o que significa que a faixa de intensidades de luz na qual ela pode capturar detalhes é bastante pequena.

Em outras palavras, a câmera é capaz de capturar apenas uma faixa estreita das intensidades de luz de uma cena; as intensidades abaixo dessa faixa aparecem como preto puro, enquanto as intensidades acima aparecem como branco puro, e quaisquer detalhes são perdidos nessas regiões.

No entanto, há um truque que a câmera (ou fotógrafo) pode usar – e é ajustar o tempo de exposição (o tempo que o sensor é exposto à luz) para controlar a quantidade total de luz que chega ao sensor, de forma eficaz. mudando o alcance para cima ou para baixo para capturar o alcance mais apropriado para uma determinada cena.

Mas isso é um compromisso. Muitos detalhes não chegam à foto final. Nas duas imagens abaixo, vemos a mesma cena capturada com tempos de exposição diferentes: uma exposição muito curta (1/1000 seg), uma exposição média (1/50 seg) e uma exposição longa (1/4 seg).

Três versões da mesma imagem de flores, uma tão escura que a maior parte da foto é preta, uma de aparência normal, embora com iluminação um pouco infeliz, e uma terceira com a luz tão alta que é difícil ver as flores no primeiro plano

Como você pode ver, nenhuma das três imagens é capaz de capturar todos os detalhes disponíveis: O filamento da lâmpada é visível apenas na primeira foto, e alguns dos detalhes da flor são visíveis no meio ou na última foto, mas não Ambas.

A boa notícia é que há algo que podemos fazer sobre isso e, novamente, envolve a construção de várias cenas com um pouco de código Python.

A abordagem que adotaremos é baseada no trabalho de Paul Debevec et al., que descreve o método em seu artigo aqui. O método funciona assim:

Primeiro, requer várias fotos da mesma cena (estacionária), mas com tempos de exposição diferentes. Novamente, como no caso anterior, precisamos de um tripé ou suporte para garantir que a câmera não se mova. Também precisamos de um aplicativo de disparo manual (se estiver usando um telefone) para que possamos controlar o tempo de exposição e evitar ajustes automáticos da câmera. O número de fotos necessárias depende da faixa de intensidades presentes na imagem (de três para cima), e os tempos de exposição devem ser espaçados nessa faixa para que os detalhes que estamos interessados ​​em preservar apareçam claramente em pelo menos uma foto.

Em seguida, um algoritmo é usado para reconstruir a curva de resposta da câmera com base na cor dos mesmos pixels nos diferentes tempos de exposição. Isso basicamente nos permite estabelecer um mapa entre o brilho real da cena de um ponto, o tempo de exposição e o valor que o pixel correspondente terá na imagem capturada. Usaremos a implementação do método Debevec da biblioteca 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)

A curva de resposta é mais ou menos assim:

Um gráfico exibindo a curva de resposta como exposição de pixel (log) sobre o valor de pixel

No eixo vertical temos o efeito cumulativo do brilho da cena de um ponto e o tempo de exposição, enquanto no eixo horizontal temos o valor (0 a 255 por canal) que o pixel correspondente terá.

Essa curva nos permite realizar a operação inversa (que é a próxima etapa do processo) – dado o valor do pixel e o tempo de exposição, podemos calcular o brilho real de cada ponto da cena. Esse valor de brilho é chamado de irradiância e mede a quantidade de energia luminosa que incide em uma unidade de área do sensor. Ao contrário dos dados de imagem, eles são representados usando números de ponto flutuante porque refletem uma faixa muito mais ampla de valores (portanto, alta faixa dinâmica). Assim que tivermos a imagem de irradiância (a imagem HDR), podemos simplesmente salvá-la:

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

Para aqueles de nós que têm a sorte de ter uma tela HDR (que está se tornando cada vez mais comum), pode ser possível visualizar essa imagem diretamente em toda a sua glória. Infelizmente, os padrões HDR ainda estão em sua infância, então o processo para fazer isso pode ser um pouco diferente para diferentes telas.

Para o resto de nós, a boa notícia é que ainda podemos aproveitar esses dados, embora uma exibição normal exija que a imagem tenha canais de valor de byte (0-255). Embora precisemos abrir mão de parte da riqueza do mapa de irradiância, pelo menos temos o controle sobre como fazê-lo.

Esse processo é chamado de mapeamento de tom e envolve a conversão do mapa de irradiância de ponto flutuante (com uma alta faixa de valores) em uma imagem de valor de byte padrão. Existem técnicas para fazer isso para que muitos dos detalhes extras sejam preservados. Apenas para dar um exemplo de como isso pode funcionar, imagine que antes de espremer o intervalo de ponto flutuante em valores de byte, aprimoramos (aguçamos) as bordas que estão presentes na imagem HDR. Aprimorar essas bordas ajudará a preservá-las (e implicitamente os detalhes que elas fornecem) também na imagem de faixa dinâmica baixa.

O OpenCV fornece um conjunto desses operadores de mapeamento de tons, como Drago, Durand, Mantiuk ou Reinhardt. Aqui está um exemplo de como um desses operadores (Durand) pode ser usado e do resultado que ele produz.

 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) 

O resultado do cálculo acima exibido como uma imagem

Usando Python, você também pode criar seus próprios operadores se precisar de mais controle sobre o processo. Por exemplo, este é o resultado obtido com um operador personalizado que remove intensidades que são representadas em poucos pixels antes de reduzir o intervalo de valores para 8 bits (seguido por uma etapa de contraste automático):

A imagem que resulta de seguir o processo acima

E aqui está o código para o operador acima:

 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)

Conclusão

Vimos como com um pouco de Python e algumas bibliotecas de suporte, podemos ultrapassar os limites da câmera física para melhorar o resultado final. Ambos os exemplos que discutimos usam várias fotos de baixa qualidade para criar algo melhor, mas existem muitas outras abordagens para diferentes problemas e limitações.

Embora muitos telefones com câmera tenham aplicativos de armazenamento ou integrados que abordam esses exemplos específicos, claramente não é difícil programá-los manualmente e aproveitar o nível mais alto de controle e compreensão que pode ser obtido.

Se você estiver interessado em cálculos de imagem em um dispositivo móvel, confira Tutorial OpenCV: Detecção de objetos em tempo real usando MSER no iOS pelo colega Toptaler e desenvolvedor de elite do OpenCV Altaibayar Tseveenbayar.