Parte 6: Node de Detección de Objetos
El Código¶
Copia todo el código a continuación en tu archivo object_detection.py, ¡y asegúrate de leer las anotaciones!
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node # (1)!
import cv2
from cv_bridge import CvBridge, CvBridgeError # (2)!
from sensor_msgs.msg import Image # (3)!
from pathlib import Path # (4)!
class ObjectDetection(Node):
def __init__(self): # (5)!
super().__init__("object_detection")
self.camera_sub = self.create_subscription(
msg_type=Image,
topic="/camera/image_raw",
callback=self.camera_callback,
qos_profile=10
)
self.waiting_for_image = True # (6)!
def camera_callback(self, img_data): # (7)!
cvbridge_interface = CvBridge() # (8)!
try:
cv_img = cvbridge_interface.imgmsg_to_cv2(
img_data, desired_encoding="bgr8"
) # (9)!
except CvBridgeError as e:
self.get_logger().warning(f"{e}")
if self.waiting_for_image: # (10)!
height, width, channels = cv_img.shape
self.get_logger().info(
f"Obtained an image of height {height}px and width {width}px."
)
self.show_image(img=cv_img, img_name="step1_original")
self.waiting_for_image = False # (15)!
cv2.destroyAllWindows() # (16)!
def show_image(self, img, img_name, save_img=True): # (11)!
self.get_logger().info(f"Opening {img_name}...")
cv2.imshow(img_name, img) # (12)!
cv2.waitKey(4000) # (14)!
if save_img: # (13)!
self.save_image(img, img_name)
def save_image(self, img, img_name): # (17)!
self.get_logger().info(f"Saving the image...")
base_image_path = Path.home().joinpath("object_detection")
base_image_path.mkdir(parents=True, exist_ok=True) # (18)!
full_image_path = base_image_path.joinpath(
f"{img_name}.jpg") # (19)!
cv2.imwrite(str(full_image_path), img) # (20)!
self.get_logger().info(
f"\nSaved an image to '{full_image_path}'\n"
f" - image dims: {img.shape[0]}x{img.shape[1]}px\n"
f" - file size: {full_image_path.stat().st_size} bytes"
) # (21)!
def main(args=None):
rclpy.init(args=args)
node = ObjectDetection()
while node.waiting_for_image:
rclpy.spin_once(node) # (22)!
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
-
Nada nuevo aquí, continuamos...
-
Estamos importando la biblioteca OpenCV para Python (recuerda la API de Python de la que hablamos antes), que se llama
cv2, y también esa interface puente entre ROS y OpenCV de la que hablamos antes:cv_bridge.De
cv_bridgeestamos importando las clasesCvBridgeyCvBridgeErrorde la bibliotecacv_bridgeespecíficamente. -
Necesitamos subscribirnos a un topic de imagen para obtener los datos que se publican en él. Ya deberías haber identificado el tipo de interface que se publica en el topic
/camera/image_raw, así que importamos ese tipo de interface aquí (del packagesensor_msgs) para poder construir un subscriber al topic más adelante. -
También estamos importando la clase Python
Pathdel módulopathlib. Una herramienta muy útil para realizar operaciones de archivos. -
Inicializando nuestra Clase
ObjectDetection()(debería serte muy familiar a estas alturas):- Dando un nombre a nuestro node.
- Creando un subscriber al topic
/camera/image_raw, proporcionando el tipo de interface utilizado por el topic (sensor_msgs/msg/Image, como se importó anteriormente), y apuntándolo a una función callback (camera_callback, en este caso), para definir los procesos que deben realizarse cada vez que se obtiene un mensaje en este topic (en este caso, los mensajes serán nuestras imágenes de cámara)
-
Estamos creando una bandera para indicar si el node ha obtenido una imagen todavía o no. Para este ejercicio, solo queremos obtener una única imagen, así que estableceremos la bandera
waiting_for_imageenFalseuna vez que se haya obtenido y procesado una imagen, para evitar capturar más.Esta bandera se usará luego para cerrar el node cuando haya terminado su trabajo.
-
Aquí estamos definiendo una función callback para nuestro subscriber
self.camera_sub... -
Aquí creamos una instancia de la clase
CvBridgeque importamos anteriormente, y que usaremos más adelante para convertir datos de imagen ROS a un formato que OpenCV pueda entender. -
Estamos usando la interface CvBridge para tomar nuestros datos de imagen ROS y convertirlos a un formato que OpenCV pueda entender. En este caso especificamos la conversión (o "encoding") a un formato de imagen BGR (Azul-Verde-Rojo) de 8 bits:
"bgr8".Sin embargo, contenemos esto dentro de un bloque
try-except, que es el procedimiento recomendado al hacer esto. Aquí intentamos convertir una imagen usando el encoding deseado, y si se genera unCvBridgeErrorentonces imprimimos un mensaje de advertencia en el terminal. Si esto sucede, esta ejecución particular de la función callback de la cámara se detendrá. -
Luego verificamos la bandera
waiting_for_imagepara ver si esta es la primera imagen recibida por el node. Si es así, entonces:- Obtenemos la altura y el ancho de la imagen (en píxeles), así como el número de canales de color.
- Imprimimos un mensaje de registro con las dimensiones de la imagen.
- Pasamos los datos de la imagen a la función
show_image()(definida a continuación). También pasamos un nombre descriptivo para la imagen a esta función (img_name).
-
Este método de clase nos presenta la imagen en una ventana emergente y también llama a otro método que guarda la imagen en un archivo para nosotros.
-
Muestra la imagen real en una ventana emergente:
- Los datos de la imagen se pasan a la función a través del argumento
img, - Necesitamos darle un nombre a la ventana emergente, así que en este caso usamos el argumento
img_nameque se pasa a este método de clase.
- Los datos de la imagen se pasan a la función a través del argumento
-
El método de clase
show_image()tiene un argumentosave_img, que está establecido enTruepor defecto, de modo que esta condiciónifse activa, y se llama a otro método de clase para guardar la imagen en un archivo. -
Proporcionamos un valor de
4000aquí, lo que le dice a esta función que mantenga esta ventana abierta durante 4000 milisegundos (4 segundos) antes de cerrarla nuevamente.Una vez que la ventana se haya cerrado, la ejecución de nuestro código puede continuar...
-
Luego establecemos la bandera
waiting_for_imageenFalsepara que estas etapas de procesamiento solo se realicen una vez (solo queremos capturar una única imagen). Esto activará el buclewhileprincipal para que se detenga (ver abajo), causando que la ejecución general del node se detenga también. -
cv2.destroyAllWindows()garantiza que cualquier ventana emergente de imagen OpenCV que pueda seguir activa o en memoria sea destruida antes de que el método de clase salga (y el node se cierre). -
Este método de clase maneja el guardado de la imagen en un archivo usando herramientas de
cv2ypathlib. -
Aquí definimos una ubicación del sistema de archivos para guardar imágenes.
Queremos que exista en una carpeta llamada "
object_detection" en el directorio de inicio, así que podemos usarPath.home().joinpath(...)de Pathlib para definirla (una forma práctica de acceder al directorio de inicio del Usuario, sin necesidad de conocer el nombre del Usuario).Luego usamos el método
Path.mkdir()de Pathlib para crear este directorio si no existe todavía. -
Aquí se construye una ruta completa de archivo para la imagen (usando el método
Path.joinpath()), basada en:- El
base_image_pathque definimos anteriormente - Un nombre de imagen que se pasa a este método de clase a través del argumento
img_name.
- El
-
Esto guarda la imagen en un archivo
.jpg. Proporcionamos lafull_image_pathque se creó anteriormente, y también los datos reales de la imagen (self.cv_img) para que la función sepa qué imagen queremos guardar. -
Estamos imprimiendo un mensaje de registro en el terminal para informarnos de:
- Dónde se ha guardado la imagen
- Qué tan grande es la imagen (en términos de sus dimensiones en píxeles)
- Qué tan grande es el archivo de imagen (en bytes).
-
Estamos usando
spin_once()dentro de un buclewhileaquí para poder vigilar el valor de la banderawait_for_image, y dejar de hacer spin (es decir, salir del buclewhile) una vez que se vuelveFalse.
Dependencias¶
Modifica tu archivo package.xml para acomodar las diversas dependencias del node object_detection.py: