Por que funciona: Unidade 1.2: Captura da imagem, Unidade 1.3: A imagem como dado
Corrigir uma foto mal exposta com OpenCV
O primeiro projeto de OpenCV na Prática: abrir uma foto tirada dois stops escura demais, ler seu histograma, testar cinco formas de corrigi-la e medir cada uma contra uma foto mais longa e bem exposta da mesma cena (o alvo). Multiplicar chega mais perto; as correções automáticas ficam mais longe.
Tradução assistida por máquina do original em inglês. Números, código e referências foram conferidos com ele; o texto não foi revisado por um falante nativo. Ler o original. Os labs interativos e o código estão em inglês.
Uma foto tirada com pouca luz muitas vezes pode ser clareada até parecer uma exposição mais longa da mesma cena, e este projeto mede qual forma de clarear chega mais perto.
A animação aumenta um único ajuste, o multiplicador aplicado a cada pixel, e mostra a foto corrigida ao lado de uma foto mais longa e bem exposta da mesma cena (o alvo), com a diferença entre as duas caindo.

- Abrir a foto. Carregar o arquivo numa grade de números, um por cor em cada ponto (lição 1).
- Ler o histograma. Contar quantos pixels há em cada brilho, o que mostra o quão escura a foto está e o que já estourou (lição 2).
- Corrigir a exposição. Mudar todos os valores por uma única regra: somar uma constante, multiplicar por um ganho, curvar com uma curva gama ou deixar um equalizador automático decidir (lições 3, 4 e 5).
- Medir a diferença. Comparar a foto corrigida com uma foto mais longa e bem exposta da mesma cena (o alvo), pixel a pixel, e tirar a média da diferença. Quanto menor, mais perto; 0 é idêntico.
- Recortar, redimensionar, salvar. Ficar com a parte que interessa, reduzir sem aliasing e gravar num arquivo (lição 6).
Onde você já viu isso: os controles de Exposição e Brilho nos editores de foto do celular e no Lightroom; o botão Automático ao lado deles; o histograma na tela de reprodução de uma câmera.
TL;DR. Clarear uma foto escura multiplicando seus valores chega bem mais perto da exposição-alvo do que somar a eles ou deixar uma ferramenta automática decidir, e nada traz de volta o que a câmera estourou. Com dois stops a menos (quase um quarto da luz que o alvo recebeu), um ganho de 2,4 deixa uma diferença média de 9,1 (de 255) até o alvo, somar brilho deixa 18,0 e a equalização de histograma 30,6. Com três stops a menos, a gama vence, 13,9 contra 15,1.
A cena e o alvo
Todas as lições trabalham sobre uma única cena: um abajur de mesa, um cartão e uma carta de cores, fotografados dezoito vezes com tempos de obturador de 1/800 s a 30 s para o HDR Photographic Survey de Mark Fairchild [1]. A unidade 1.2 da trilha de Fundamentos (em inglês) mede os mesmos dezoito arquivos raw. Este módulo usa quatro, revelados do mesmo jeito:
| Arquivo | Obturador | Cinza médio | Cinza ≥ 250 | Papel |
|---|---|---|---|---|
reference.webp | 5 s | 104.7 | 11.7% | o alvo contra o qual toda correção é avaliada |
dark.webp | 1.3 s | 56.6 | 7.8% | cerca de dois stops abaixo |
darker.webp | 1/2 s | 40.8 | 6.3% | mais de três stops abaixo |
bright.webp | 20 s | 173.8 | 27.2% | dois stops acima |
A distância usada do começo ao fim é a diferença absoluta média entre a foto corrigida e o alvo, sobre todos os pixels e canais de cor, na escala de 0 a 255.
A correção inteira, numa célula
import cv2
import numpy as np
img = cv2.imread("dark.webp")
ref = cv2.imread("reference.webp")
fixed = cv2.convertScaleAbs(img, alpha=2.4)
gap = np.abs(fixed.astype(int) - ref.astype(int)).mean()
print("distance to the reference:", round(gap, 1))
cv2.imwrite("fixed.jpg", fixed, [cv2.IMWRITE_JPEG_QUALITY, 90])
side = [cv2.resize(x, None, fx=0.5, fy=0.5) for x in (img, fixed, ref)]
cv2.imshow("dark | fixed | reference", np.hstack(side))distance to the reference: 9.1
O que cada correção consegue
Cada correção foi avaliada no seu melhor ajuste, encontrado testando o parâmetro em passos pequenos (valores entre parênteses).
| Correção | Escura, 1.3 s | Mais escura, 1/2 s | Clara, 20 s |
|---|---|---|---|
| nada | 47.0 | 62.3 | 68.4 |
| somar uma constante (valor) | 18.0 (50) | 25.7 (65) | 68.4 (0) |
| multiplicar (ganho) | 9.1 (2.4) | 15.1 (4.2) | 22.8 (0.5) |
| gama (γ) | 10.7 (0.55) | 13.9 (0.4) | 33.4 (2.35) |
equalizeHist no brilho | 30.6 | 31.9 | 30.3 |
| CLAHE no brilho | 35.5 | 51.3 | 58.8 |
A tabela mostra quatro coisas:
- Um ganho vence um deslocamento. Uma exposição mais longa multiplica a luz, então um multiplicador é a forma certa de correção, e ele corta pela metade a diferença que a soma deixa (lição 3). O próprio tutorial do OpenCV avisa que o deslocamento deixa um véu cinza [3].
- A gama passa o ganho à medida que a foto fica mais escura. Um ganho grande o bastante para levantar três stops de sombra joga tudo acima de 60 para o branco; a gama leva 0 em 0 e 255 em 255 em qualquer ajuste, então nunca satura [2] (lição 4).
- As correções automáticas ficam mais longe. A equalização distribui o brilho conforme a frequência de cada nível [2], e esta foto é quase toda parede e mesa escuras (lição 5).
- A superexposição é a direção difícil. Nada deixa a foto clara mais perto do que 22,8, porque 15,6% dos seus pixels estão em 250 ou acima onde o alvo ainda tem detalhe. Esses valores se perderam.
O que levar daqui
- Uma foto um ou dois stops escura: multiplique por um único ganho e confira contra algo em que você confia (um cartão cinza, uma segunda foto). Aqui isso levou 47,0 a 9,1.
- Bem mais escura que isso: use gama, ou um ganho e uma gama juntos, e aceite sombras mais chapadas. Aqui 62,3 foi para 13,9.
- Sem referência, e você só precisa enxergar detalhe: equalização ou CLAHE, sabendo que o resultado não vai se parecer com a cena.
- Superexposta: fotografe de novo ou faça bracketing. O software só levou a foto clara até 22,8, e 15,6% dela não pôde ser recuperada com nenhum ajuste. High dynamic range (em inglês) mostra como várias exposições desta cena são combinadas para manter tudo.
- Quando vale um modelo: este módulo não testou um realçador aprendido. O número a bater é 9,1 com dois stops a menos e 13,9 com três.
Limitações
- O alvo é uma escolha, não a verdade. A foto de 5 segundos já tem 11,7% dos pixels em 250 ou acima. A foto escura manteve detalhe no abajur que o alvo perdeu, então uma correção que o preserva é avaliada como pior.
- Os melhores ajustes foram escolhidos olhando a resposta. Cada ganho e cada gama acima foi escolhido para minimizar a distância até o alvo. Sem um alvo, você teria de escolher no olho ou por uma regra, e se sairia pior.
- Uma cena, uma câmera. Um abajur num quarto escuro é uma faixa de luz extrema. Uma cena mais plana favoreceria mais a equalização, e a ordem das correções poderia mudar.
- A distância pesa todos os pixels igualmente. A maior parte deste quadro é parede e mesa escuras, então a nota reflete principalmente as sombras. Uma métrica que pesasse mais o abajur e a carta classificaria as correções de outro jeito.
- As entradas são WebP com qualidade 90. Todos os números foram calculados a partir dos arquivos publicados, então o Rodar do leitor os reproduz, mas eles não são os valores brutos do sensor.
As lições
- Abrir uma imagem e ler seus pixels no OpenCV
- Histogramas de imagem no OpenCV
- Brilho e contraste no OpenCV
- Correção gama no OpenCV com uma tabela de consulta
- Equalização de histograma e CLAHE no OpenCV
- Recortar, redimensionar e salvar uma imagem no OpenCV
Todas as células do módulo também estão num notebook, para rodar na sua máquina com o OpenCV que o pip instala: abra no Colab. As saídas dele batem com as da página, célula por célula.
Como estes números foram feitos
| Dados | HDR Photographic Survey, “Luxo Double Checker” [1]: arquivos _MDF0010.NEF (1/2 s), _MDF0011.NEF (1.3 s), _MDF0013.NEF (5 s), _MDF0015.NEF (20 s). Termos de pesquisa e uso não comercial; baixados, não redistribuídos como raw |
| Revelação | rawpy 0.27.0 (LibRaw 0.22.1): balanço de branco da câmera, sem clareamento automático, meio tamanho, 8 bits; redimensionados para 960 px de largura com INTER_AREA; gravados como WebP qualidade 90 |
| Notas | Diferença absoluta média até reference.webp sobre todos os pixels e canais. Grades: soma de 0 a 150 em passos de 5; ganho de 0.2 a 6.0 em passos de 0.1; gama de 0.20 a 3.00 em passos de 0.05; CLAHE clipLimit=2.0, tileGridSize=(8, 8) |
| Na página | Pyodide 314.0.7, OpenCV 4.11.0, NumPy 2.4.6 (a saída exibida de cada célula é gerada por npm run pycells com este build) |
| Localmente | OpenCV 5.0.0, NumPy 2.5.2, Python 3.12; todos os números da tabela acima batem com os da página até uma casa decimal |
| Máquina | Intel Core i7-12700H, Ubuntu 22.04.5 LTS. Nada é cronometrado; os números são determinísticos |
| Comandos | uv run --extra sensing sensing-download, depois uv run --extra sensing practice-m1 --assets --media em fundamentals-lab; npm run pycells no repositório do site |
| Artefato | output/practice_m1_numbers.json |
Leitura complementar
- Gonzalez & Woods, Digital Image Processing, capítulo 3 [2], para transformações de intensidade e processamento de histograma com a teoria por trás de cada uma.
- O tutorial do OpenCV Changing the contrast and brightness of an image! [3], que corrige outra foto subexposta com α, β e γ.
- Image sensing (em inglês), a unidade de Fundamentos sobre esta mesma cena, para entender por que a câmera saturou onde saturou e como as dezoito exposições se juntam numa só.
Referências
[1] Fairchild, M. D. (2007). The HDR Photographic Survey. IS&T 15th Color Imaging Conference, pp. 233–238. doi:10.2352/CIC.2007.15.1.art00044
[2] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), chapter 3 “Intensity Transformations and Spatial Filtering”: section 3.2 “Some Basic Intensity Transformation Functions”, p. 136, and section 3.3 “Histogram Processing”, p. 147. Pearson.
[3] OpenCV. Changing the contrast and brightness of an image! (tutorial, doc/tutorials/core/basic_linear_transform/basic_linear_transform.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv