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.
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.
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.

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.
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), "%")darkest quarter (0-63): 79.3 % 250 or above: 7.8 %
| Argumento | Aquí | Qué significa |
|---|---|---|
[grey] | una imagen | una lista, porque puede contar sobre varias imágenes |
[0] | canal 0 | el único canal de una imagen en gris |
None | sin máscara | pasa una máscara para contar solo una parte de la imagen |
[256] | 256 bins | uno por nivel de gris |
[0, 256] | el rango | el 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.
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)]))
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.
# 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]))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.
# 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 sercalcHist([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.
calcHistdevuelve conteos. Divide entregrey.sizepara 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