Por qué funciona: Unidad 1.3: La imagen como datos
Encontrar el rango HSV de un objeto en OpenCV
Lee los píxeles de una mesa de billar en HSV con cv2.cvtColor y encuentra el rango que ocupa el paño: su valor de verde cae casi diez veces entre el extremo iluminado y una sombra, mientras su tono se queda entre 86 y 90.
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.
Para separar un objeto de una foto por su color necesitas el rango de números que ocupa ese color, y la forma más rápida de encontrarlo es leer algunos de sus píxeles en HSV.

Dónde ya lo has visto: el filtro de chroma key (pantalla verde) de OBS y de los editores de video; Seleccionar › Por color en GIMP y Gama de colores en Photoshop; las clasificadoras ópticas que descartan granos y frutas de otro color.
TL;DR. Un color nos parece el mismo a la luz y a la sombra, pero los números que la cámara guarda para él cambian mucho, y uno de ellos, el tono, casi no se mueve, así que ese es el número que hay que probar. En la mesa de billar el valor de verde del paño cae casi diez veces, de 201 en el extremo iluminado a 21 en la sombra de una bola, mientras su tono se queda entre 86 y 90 en la escala 0–179 de OpenCV.
El problema
Este módulo encuentra todas las bolas de una mesa de billar (el proyecto). Las bolas vienen en todos los colores, pero el paño de debajo tiene uno solo, así que el plan es encontrar el paño y quedarse con lo que no es paño. Para eso hace falta una regla para “paño” que una computadora pueda probar en cada píxel.
Lee los píxeles en HSV
cv2.cvtColor convierte una imagen entera de un espacio de color a otro. Con COLOR_BGR2HSV cada píxel se vuelve tres números nuevos: tono (qué color, 0–179), saturación (qué tan puro, 0–255) y valor (qué tan claro, 0–255) [1].
import cv2
import numpy as np
img = cv2.imread("pool.webp")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# (x, y) de cuatro puntos de la foto
points = {"cloth, near": (400, 240),
"cloth, far": (700, 95),
"shadow": (790, 336),
"green ball": (727, 155)}
for name, (x, y) in points.items():
print(f"{name:12} BGR {img[y, x]} HSV {hsv[y, x]}")cloth, near BGR [198 201 9] HSV [ 90 244 201] cloth, far BGR [32 37 2] HSV [ 86 241 37] shadow BGR [18 21 0] HSV [ 86 255 21] green ball BGR [ 86 117 5] HSV [ 82 244 117]
| Llamada | Qué hace |
|---|---|
cv2.cvtColor(img, cv2.COLOR_BGR2HSV) | una imagen nueva del mismo tamaño, con H, S y V en lugar de B, G y R |
img[y, x] | los tres números de un píxel; primero la fila (y), luego la columna (x) |
Los tres puntos del paño nos parecen un solo color. En BGR están lejos entre sí: el canal verde cae de 201 cerca de la lámpara a 37 en el extremo del fondo y 21 en la sombra de la bola blanca, y el azul y el verde caen juntos, así que ningún rango único en ningún canal cubre los tres sin cubrir también la mayoría de los demás colores. En HSV el tono es 90, 86 y 86 y solo cambia el valor. En esa propiedad se va a apoyar la máscara: el brillo tiene su propio número, así que un rango en el tono ignora la luz y la sombra [2].
La bola verde es la advertencia. Su tono es 82, a cuatro pasos del paño del fondo. Un rango lo bastante ancho para el paño con cualquier luz puede tragársela; la lección 5 cuenta cuánto cuesta eso.
Encuentra el rango en una zona
Un píxel solo puede tener mala suerte. La esquina inferior derecha de la foto es solo paño, así que su dispersión es el rango desde el que empezar:
# una zona de la foto que es solo paño
patch = hsv[550:, 650:]
for i, name in enumerate(["H", "S", "V"]):
lo, hi = np.percentile(patch[..., i], [2, 98])
print(f"{name}: {lo:.0f} to {hi:.0f}")
cv2.imshow("hue (0-179)", hsv[..., 0])H: 85 to 88 S: 233 to 253 V: 118 to 163
Los percentiles descartan el 2% extremo de cada lado, para que una mota suelta no fije el rango. En esta zona el tono va de 85 a 88 y el valor de 118 a 163. El extremo del fondo de la mesa y las sombras son más oscuros que cualquier cosa de la zona (el paño del fondo estaba en 37), así que el rango de valor tiene que bajar bastante por debajo de 118. El módulo empieza con una ventana amplia, que la lección 2 convierte en una máscara:
| H | S | V | |
|---|---|---|---|
| inferior | 75 | 80 | 40 |
| superior | 100 | 255 | 255 |
La imagen del tono muestra el paño como un solo gris de adelante hacia atrás. La sala negra sobre la mesa es ruido: cuando un píxel es casi negro, los tres canales están todos cerca de 0 y la fórmula divide diferencias pequeñas entre un número pequeño, así que su tono es casi aleatorio [2].
Rómpelo
cvtColor confía en el flag que le pasas. cv2.imread devuelve BGR, y el flag para eso es COLOR_BGR2HSV. Pasa COLOR_RGB2HSV y el rojo y el azul se intercambian antes de la conversión:
# RGB2HSV en una imagen BGR: rojo y azul intercambiados
wrong = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
x, y = points["cloth, near"]
print("right:", hsv[y, x], " wrong:", wrong[y, x])
cv2.imshow("hue, wrong flag", wrong[..., 0])right: [ 90 244 201] wrong: [ 30 244 201]
El tono del paño sale como 30, que es amarillo en la escala de OpenCV, y una máscara construida sobre él no seleccionaría nada en la mesa. No hay ningún error que te avise.
Tu turno
Lee la bola morada en x = 414, y = 109 y guarda su tono en hue.
hue = None # reemplaza esta línea
print(hue)Por qué funciona
Una cámara registra cuánta luz roja, verde y azul llegó a cada píxel, así que una superficie en sombra da valores menores en los tres canales a la vez. HSV toma el mayor de los tres como valor y describe el color por la posición de los tres entre sí, que no cambia cuando los tres se reducen juntos [2]. Gonzalez y Woods derivan la misma idea para el modelo HSI, muy cercano, y lo usan para segmentar por color [3]. Colour spaces: choosing axes you can threshold (en inglés) hace la conversión paso a paso sobre un solo parche medido de una carta de color, y how close is your colour (en inglés) mide cuánto se alejan los colores de una cámara de los reales.
Errores comunes
- Tono en grados. Los selectores de color dan el tono en 0–360. OpenCV lo divide entre dos para que quepa en un byte: 0–179 para imágenes de 8 bits [2]. El paño en 88 queda en unos 176 grados en GIMP.
- El flag de conversión equivocado.
imreaddevuelve BGR; usaCOLOR_BGR2HSV, como muestra la celda Rómpelo. - Confiar en el tono de píxeles oscuros o grises. Cuando la saturación o el valor están cerca de 0, el tono es ruido. La bola blanca, blanco puro, tiene saturación 11: su tono no dice nada.
- El rojo da la vuelta. El rojo está en los dos extremos de la escala de tono, cerca de 0 y cerca de 179, así que un solo rango no lo contiene. Colour spaces muestra la solución con dos máscaras.
- Leer un solo píxel. Toma una zona y sus percentiles, y mira tanto la parte iluminada como la sombreada.
Referencias
[1] OpenCV. Changing Colorspaces (Python tutorial, doc/py_tutorials/py_imgproc/py_colorspaces/py_colorspaces.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv
[2] OpenCV. Color conversions, section “RGB ↔ HSV” (modules/imgproc/doc/colors.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv
[3] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), section 7.2 “Color Models”, The HSI Color Model, p. 541, and section 7.7 “Using Color in Image Segmentation”, p. 575. Pearson.
[4] MarkBuckawicki. Billiards table 2.JPG (27 Feb 2014). Wikimedia Commons, CC0 1.0. commons.wikimedia.org