Por qué funciona: Unidad 1.2: Captura de la imagen, Unidad 1.3: La imagen como datos

Histogramas de imagen en OpenCV

Cuenta cuántos píxeles hay en cada brillo con cv2.calcHist, dibuja el histograma y lee en él que la foto oscura tiene el 79,3% de sus píxeles en el cuarto inferior y el 7,8% quemado.

Luis Condados · · 4 min de lectura Principiante

Traducción asistida por máquina del original en inglés. Los números, el código y las referencias se comprobaron contra él; el texto no fue revisado por un hablante nativo. Leer el original. Los labs interactivos y el código están en inglés.

El histograma de la foto oscura: un montón a la izquierda, un pico en 255.
El histograma de la foto oscura: un montón a la izquierda, un pico en 255.

Un histograma cuenta cuántos píxeles tienen cada brillo, y eso te dice de un vistazo si una foto está demasiado oscura, demasiado clara o las dos cosas.

La foto oscura de la lámpara junto a su histograma, que se amontona en el extremo izquierdo con un pico fino en el extremo derecho.
La foto oscura y su histograma. La mayoría de los píxeles se amontona a la izquierda (oscuro); el pico fino en el borde derecho es la lámpara, quemada en 255. Fotograma del HDR Photographic Survey [3].

Dónde ya lo has visto: el histograma en la pantalla de reproducción de una cámara y en el modo pro de la cámara del celular; el panel de histograma de Lightroom, de darktable y el diálogo Niveles de GIMP.

TL;DR. Un histograma muestra dónde está el brillo de una foto, así que ves una mala exposición antes de corregirla. En la foto oscura, el 79,3% de los píxeles está en el cuarto inferior del rango y el 7,8% en 250 o más, lo que significa que está demasiado oscura y quemada a la vez.

El problema

La lección 1 resumió la foto en un solo número, un brillo medio de 56,6. Una media oculta la forma: una foto mitad negra y mitad blanca también tiene una media cercana a 128. Para decidir cómo corregir la exposición necesitas la distribución completa.

Cuenta los píxeles de cada brillo

cv2.calcHist recibe una lista de imágenes, el canal a contar, una máscara opcional, el número de bins y el rango de valores [1]. Con 256 bins sobre [0, 256], cada bin es un nivel de gris.

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), "%")
Salida
darkest quarter (0-63): 79.3 %
250 or above: 7.8 %
ArgumentoAquíQué significa
[grey]una imagenuna lista, porque puede contar sobre varias imágenes
[0]canal 0el único canal de una imagen en gris
Nonesin máscarapasa una máscara para contar solo una parte de la imagen
[256]256 binsuno por nivel de gris
[0, 256]el rangoel extremo superior no se incluye

Casi cuatro de cada cinco píxeles están en el cuarto más oscuro. Esa es la subexposición. El 7,8% en 250 o más es la lámpara y la mancha de luz debajo de ella, que ya estaban demasiado claras en esta exposición: ningún aclarado va a devolver detalle ahí.

Dibújalo

Un histograma se lee mejor como imagen. La función de abajo dibuja una barra por nivel de gris con cv2.line y luego pone el histograma de la foto oscura junto al de la exposición objetivo.

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)]))
Salida
dark (left) and reference (right)
dark (left) and reference (right)

El histograma del objetivo se reparte por todo el rango. El de la foto oscura queda aplastado en el extremo izquierdo. Las correcciones de las lecciones 3, 4 y 5 son tres formas distintas de estirar el histograma de la izquierda hacia el de la derecha, y esta imagen es lo que todas intentan alcanzar.

Rómpelo

El rango es semiabierto: [0, 256] cuenta de 0 a 255. Escribir [0, 255], la versión que parece natural, descarta sin avisar todos los píxeles en 255.

Rómpelo
# El rango excluye su extremo 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]))
Salida
pixels at 255, range [0, 255]: 0
pixels at 255, range [0, 256]: 41503

41.503 píxeles, la lámpara quemada, desaparecen del conteo, y nada te avisa. Es un error fácil de copiar del propio tutorial de OpenCV, cuyo texto da el rango como [0,255] [1] mientras su código de ejemplo pasa (0, 256) con el comentario “the upper boundary is exclusive” [4].

Tu turno

La foto sobreexpuesta, bright.webp, se tomó a 20 segundos. ¿Qué porcentaje de sus píxeles en gris está en 250 o más? Guarda la respuesta en clipped.

Tu turno
# porcentaje de píxeles en gris con 250 o más en bright.webp
clipped = 0.0
print(clipped)

Por qué funciona

Un histograma es la distribución de los valores de intensidad [2]. No dice nada sobre dónde están los píxeles en el fotograma, solo cuántos hay de cada valor, y por eso dos fotos muy distintas pueden tener el mismo. Por qué la lámpara se quema en un extremo mientras la mesa se pierde en la oscuridad en el otro es una propiedad del sensor: Noise and dynamic range (en inglés) mide qué tan amplio es el rango que cabe en una exposición, y High dynamic range (en inglés) muestra cómo se combinan varias exposiciones de esta misma escena para abarcarlo todo.

Errores comunes

  • [0, 255] como rango. Descarta el valor 255, como muestra la celda Rómpelo.
  • Pasar la imagen en lugar de una lista. calcHist(grey, …) falla; tiene que ser calcHist([grey], …).
  • Una imagen en color con el canal [0]. Eso cuenta solo el canal azul. Convierte a gris primero, o cuenta cada canal a propósito.
  • Leer la altura de las barras como porcentajes. calcHist devuelve conteos. Divide entre grey.size para obtener fracciones.
  • Indexar el resultado como hist[v, 0]. OpenCV 4.11 devuelve una columna de forma (256, 1) y OpenCV 5.0 un array plano de forma (256,), así que ese índice funciona en uno y falla en el otro (ambos comprobados el 30 de septiembre de 2026). Llama a .ravel() antes, como hacen las celdas de aquí.

Referencias

[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