Por qué funciona: Unidad 1.3: La imagen como datos

Crear una máscara de color con cv2.inRange en OpenCV

Convierte un rango HSV en una máscara en blanco y negro con cv2.inRange, inviértela con bitwise_not y aplícala con bitwise_and. En la mesa de billar una llamada marca el 79,3% de la foto como paño.

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.

La máscara del paño en la foto del billar: blanco donde el HSV del píxel cae dentro del rango.
La máscara del paño en la foto del billar: blanco donde el HSV del píxel cae dentro del rango.

Una máscara es una imagen en blanco y negro que marca qué píxeles pasan una prueba, y cv2.inRange crea una a partir de un rango de color en una sola llamada.

La foto del billar junto a la máscara del paño: el paño es blanco; las bolas, el taco, las bandas y la sala son negros.
Izquierda: la foto. Derecha: la máscara para tono 75–100, saturación 80–255, valor 40–255. Los píxeles blancos pasaron. Las bolas y el taco son los agujeros negros en ella. Foto de MarkBuckawicki, CC0 [4].

Dónde ya lo has visto: el filtro de chroma key de OBS y de los editores de video, que es una máscara sobre el verde; Seleccionar › Por color en GIMP y Selección › Gama de colores en Photoshop.

TL;DR. Una llamada convierte el rango de color de la lección anterior en un mapa de dónde está ese color, e invertir el mapa da todo lo demás. En la foto del billar el 79,3% de los píxeles cae en el rango del paño; el resto, el “no paño”, contiene todas las bolas, pero también las bandas, la sala y el taco.

El problema

La lección 1 encontró el rango del paño en HSV. En el proyecto las bolas son lo que no es paño, así que el siguiente paso es marcar cada píxel como paño o no, y luego quedarse con los otros.

Un rango, una máscara

Python · OpenCV
import cv2
import numpy as np

img = cv2.imread("pool.webp")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

lower = np.array([75, 80, 40])
upper = np.array([100, 255, 255])
cloth = cv2.inRange(hsv, lower, upper)

print(cloth.shape, cloth.dtype, np.unique(cloth))
print("cloth:", round(100 * np.mean(cloth > 0), 1), "% of pixels")
cv2.imshow("cloth", cloth)
Salida
(720, 960) uint8 [  0 255]
cloth: 79.3 % of pixels
cloth
cloth
LlamadaArgumentosQué hace
cv2.inRange(hsv, lower, upper)una imagen, y el valor mínimo y máximo permitidos en cada canal255 donde todos los canales están dentro de su rango, 0 en el resto
np.unique(cloth)un arraylos valores distintos que contiene: aquí solo 0 y 255

El resultado tiene un canal, no tres, y solo dos valores. Un píxel queda blanco solo si pasa las tres pruebas: tono entre 75 y 100, y saturación entre 80 y 255, y valor entre 40 y 255 [1].

Inviértela y luego aplícala

Las bolas son los agujeros del paño. bitwise_not intercambia negro y blanco, y bitwise_and con mask= conserva los píxeles de la foto solo donde la máscara es blanca:

Python · OpenCV
not_cloth = cv2.bitwise_not(cloth)
# conserva los colores de la foto donde la máscara es blanca
cut = cv2.bitwise_and(img, img, mask=not_cloth)
cv2.imshow("not cloth", cut)
Salida
not cloth
not cloth
LlamadaQué hace
cv2.bitwise_not(mask)255 pasa a 0 y 0 pasa a 255
cv2.bitwise_and(img, img, mask=m)img donde m es blanca, negro en el resto

Todas las bolas están ahí. También está todo lo que no es la mesa: las bandas, las troneras, la sala oscura y el taco. La máscara responde “¿este píxel es paño?”, y la sala no es paño. La lección 4 usa la máscara del paño por segunda vez para encontrar el contorno de la mesa, y así se puede descartar la sala. Antes de eso, la lección 3 limpia las motas.

Mueve el rango tú mismo

Los mismos seis números, en controles deslizantes, aplicados a la misma foto en un tamaño menor. Sube H min de 75 hacia 85 y la máscara empieza a romperse a lo largo del borde cercano, donde el tono del paño es más bajo; baja V min a 0 y trozos de la sala oscura se unen al paño, el fallo que mide la lección 5.

image
mask
result

Rómpelo

bitwise_and espera dos imágenes de la misma forma. La foto tiene tres canales y la máscara uno, así que pasar la máscara como segunda imagen, en lugar de como mask=, falla:

Rómpelo
# la máscara pasada como segunda imagen, no como mask=
cut = cv2.bitwise_and(img, not_cloth)
Salida
Traceback (most recent call last):
  File "<exec>", line 2, in <module>
cv2.error: OpenCV(4.11.0) /home/runner/work/pyodide-recipes/pyodide-recipes/packages/opencv-python/build/opencv-python-4.11.0.86/opencv/modules/core/src/arithm.cpp:214: error: (-209:Sizes of input arguments do not match) The operation is neither 'array op array' (where arrays have the same size and type), nor 'array op scalar', nor 'scalar op array' in function 'binary_op'

OpenCV avisa que los tamaños de las entradas no coinciden. Las máscaras van en el argumento mask=.

Tu turno

El blanco tiene cualquier tono, muy poca saturación y un valor alto. Crea una máscara llamada white que seleccione la bola blanca en x = 791, y = 311 y deje el paño en negro.

Tu turno
white = cv2.inRange(hsv, (0, 0, 0), (179, 255, 255))  # estrecha esto
cv2.imshow("white", white)

Por qué funciona

Una máscara es una imagen binaria: cada píxel tiene uno de dos valores, y los dos significan “sí” y “no”. inRange hace la misma pregunta de sí o no a cada píxel: si sus tres números caen dentro de una caja en el espacio HSV [1] [2]. Gonzalez y Woods tratan la segmentación por color en HSI, un espacio cercano a HSV que también separa el tono del brillo, en su capítulo sobre el color [3]. La sección final de Colour spaces: choosing axes you can threshold (en inglés) hace lo mismo con el rojo, cuyo tono da la vuelta en 0.

Errores comunes

  • Inferior y superior intercambiados. inRange no los ordena. Un límite inferior por encima del superior no coincide con nada, y no da error.
  • El rojo en un solo rango. El rojo está cerca de 0 y también de 179. Haz dos máscaras y únelas con cv2.bitwise_or.
  • La máscara como argumento de imagen. Pásala como mask=, como muestra la celda Rómpelo. Tiene que ser un array uint8 de un canal con el mismo alto y ancho que la imagen.
  • Esperar que “no paño” signifique “bolas”. Invertir una máscara selecciona todo lo demás, y en una foto eso incluye el fondo.

Referencias

[1] OpenCV. Thresholding Operations using inRange (tutorial, doc/tutorials/imgproc/threshold_inRange/threshold_inRange.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv

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

[3] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), section 7.7 “Using Color in Image Segmentation”, Segmentation in HSI Color Space, p. 576. Pearson.

[4] MarkBuckawicki. Billiards table 2.JPG (27 Feb 2014). Wikimedia Commons, CC0 1.0. commons.wikimedia.org