Por que funciona: Unidade 1.2: Captura da imagem, Unidade 1.3: A imagem como dado

Histogramas de imagem no OpenCV

Conte quantos pixels há em cada brilho com cv2.calcHist, desenhe o histograma e leia nele que a foto escura tem 79,3% dos pixels no quarto inferior e 7,8% estourados.

Luis Condados · · 4 min de leitura Iniciante

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.

O histograma da foto escura: um monte à esquerda, um pico em 255.
O histograma da foto escura: um monte à esquerda, um pico em 255.

Um histograma conta quantos pixels têm cada brilho, o que mostra num relance se uma foto está escura demais, clara demais ou as duas coisas.

A foto escura do abajur ao lado do seu histograma, que se acumula na ponta esquerda com um pico fino na extremidade direita.
A foto escura e seu histograma. A maioria dos pixels se acumula à esquerda (escuro); o pico fino na borda direita é o abajur, estourado em 255. Quadro do HDR Photographic Survey [3].

Onde você já viu isso: o histograma na tela de reprodução de uma câmera e no modo pro da câmera do celular; o painel de histograma do Lightroom, do darktable e a janela Níveis do GIMP.

TL;DR. Um histograma mostra onde está o brilho de uma foto, então você enxerga uma exposição ruim antes de corrigi-la. Na foto escura, 79,3% dos pixels ficam no quarto inferior da faixa e 7,8% estão em 250 ou acima, o que quer dizer que ela está escura demais e estourada ao mesmo tempo.

O problema

A lição 1 resumiu a foto num único número, um brilho médio de 56,6. Uma média esconde a forma: uma foto metade preta e metade branca também tem média perto de 128. Para decidir como corrigir a exposição, você precisa da distribuição inteira.

Conte os pixels em cada brilho

cv2.calcHist recebe uma lista de imagens, o canal a contar, uma máscara opcional, o número de bins e o intervalo de valores [1]. Com 256 bins sobre [0, 256], cada bin é um nível de cinza.

Python · OpenCV
import cv2
import numpy as np

img = cv2.imread("dark.webp")
grey = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hist = cv2.calcHist([grey], [0], None, [256], [0, 256])

low = 100 * hist[:64].sum() / grey.size
high = 100 * hist[250:].sum() / grey.size
print("darkest quarter (0-63):", round(low, 1), "%")
print("250 or above:", round(high, 1), "%")
Saída
darkest quarter (0-63): 79.3 %
250 or above: 7.8 %
ArgumentoAquiO que significa
[grey]uma imagemuma lista, porque ele pode contar em várias imagens
[0]canal 0o único canal de uma imagem em cinza
Nonesem máscarapasse uma máscara para contar só parte da imagem
[256]256 binsum por nível de cinza
[0, 256]o intervaloa ponta superior não entra

Quase quatro em cada cinco pixels estão no quarto mais escuro. Essa é a subexposição. Os 7,8% em 250 ou acima são o abajur e a mancha de luz embaixo dele, que já estavam claros demais nesta exposição: nenhum clareamento vai trazer detalhe de volta ali.

Desenhe

Um histograma é mais fácil de ler como imagem. A função abaixo desenha uma barra por nível de cinza com cv2.line e depois coloca o histograma da foto escura ao lado do histograma da exposição-alvo.

Python · OpenCV
def draw_histogram(grey, height=200):
    hist = cv2.calcHist([grey], [0], None, [256], [0, 256]).ravel()
    canvas = np.full((height, 256, 3), 255, np.uint8)
    for value, count in enumerate(hist / hist.max()):
        top = int(height * (1 - count))
        cv2.line(canvas, (value, height), (value, top), (80, 80, 80))
    return canvas

ref = cv2.cvtColor(cv2.imread("reference.webp"), cv2.COLOR_BGR2GRAY)
cv2.imshow("dark (left) and reference (right)",
           np.hstack([draw_histogram(grey), draw_histogram(ref)]))
Saída
dark (left) and reference (right)
dark (left) and reference (right)

O histograma do alvo se espalha pela faixa inteira. O da foto escura fica espremido na ponta esquerda. As correções das lições 3, 4 e 5 são três formas diferentes de esticar o histograma da esquerda em direção ao da direita, e esta imagem é o que todas elas tentam alcançar.

Quebre

O intervalo é semiaberto: [0, 256] conta de 0 a 255. Escrever [0, 255], a versão que parece natural, descarta sem aviso todos os pixels em 255.

Quebre
# O intervalo exclui a ponta superior: [0, 255] descarta 255
wrong = cv2.calcHist([grey], [0], None, [256], [0, 255]).ravel()
right = cv2.calcHist([grey], [0], None, [256], [0, 256]).ravel()
print("pixels at 255, range [0, 255]:", int(wrong[255]))
print("pixels at 255, range [0, 256]:", int(right[255]))
Saída
pixels at 255, range [0, 255]: 0
pixels at 255, range [0, 256]: 41503

41.503 pixels, o abajur estourado, somem da contagem, e nada te avisa. É um erro fácil de pegar do próprio tutorial do OpenCV, cujo texto dá o intervalo como [0,255] [1] enquanto o código de exemplo passa (0, 256) com o comentário “the upper boundary is exclusive” [4].

Sua vez

A foto superexposta, bright.webp, foi tirada com 20 segundos. Que porcentagem dos seus pixels em cinza está em 250 ou acima? Guarde a resposta em clipped.

Sua vez
# porcentagem de pixels em cinza com 250 ou mais em bright.webp
clipped = 0.0
print(clipped)

Por que funciona

Um histograma é a distribuição dos valores de intensidade [2]. Ele não diz nada sobre onde os pixels estão no quadro, só quantos há de cada valor, e por isso duas fotos muito diferentes podem ter o mesmo histograma. O motivo de o abajur estourar numa ponta enquanto a mesa some no escuro na outra é uma propriedade do sensor: Noise and dynamic range (em inglês) mede a largura da faixa que uma exposição consegue guardar, e High dynamic range (em inglês) mostra como várias exposições desta mesma cena são combinadas para guardar tudo.

Erros comuns

  • [0, 255] como intervalo. Descarta o valor 255, como a célula Quebre mostra.
  • Passar a imagem em vez de uma lista. calcHist(grey, …) falha; tem de ser calcHist([grey], …).
  • Uma imagem colorida com o canal [0]. Isso conta só o canal azul. Converta para cinza antes, ou conte cada canal de propósito.
  • Ler a altura das barras como porcentagem. calcHist devolve contagens. Divida por grey.size para ter frações.
  • Indexar o resultado como hist[v, 0]. O OpenCV 4.11 devolve uma coluna de forma (256, 1), o OpenCV 5.0 um array plano de forma (256,), então esse índice funciona num e falha no outro (os dois conferidos em 30 de setembro de 2026). Chame .ravel() antes, como fazem as células aqui.

Referências

[1] OpenCV. Histogram Calculation (tutorial, doc/tutorials/imgproc/histograms/histogram_calculation/histogram_calculation.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv

[2] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), section 3.3 “Histogram Processing”, p. 147. Pearson.

[3] 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

[4] OpenCV. samples/python/tutorial_code/Histograms_Matching/histogram_calculation/calcHist_Demo.py, line 27. OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv