7. Tạo một ROS Publisher

7. Tạo một ROS Publisher

Bạn có thể bắt đầu với tutorial Writing a Simple Publisher and Subscriber (Python) trên trang chủ của ROS nhưng nó không thú vị cho lắm vì chỉ in "hello world" trên màn hình của bạn. Trong bài này, bạn sẽ học cách tạo ra Node "Nhiếp Ảnh Gia" từ chương 6. Nếu bạn quên thì Nhiếp Ảnh Gia đại diện cho một Node dùng để đọc và publish hình ảnh từ webcam của bạn.

Kết nối Camera trong VMWare

Nếu đang sử dụng máy ảo (Virtual Machine - VM), bạn cần đảm bảo rằng webcam được kết nối với máy ảo thay vì máy chủ. Trong VMWare Workstation Player, chọn Player > Manage > Virtual Machine Settings... hoặc Ctrl + D.

Chọn tab USB Controller và chọn USB 3.1 trong mục USB compatibility.

Sau đó, đi tới Removable Devices, chọn webcam của bạn (như của mình thì webcam có tên là Sunplus Innovation Integrated_Webcam_HD) và chọn Connect (Disconnect from host). Nhấp vào OK trên bất kỳ hộp thoại bật lên nào sau đó.

Nếu có lỗi hiện lên báo kết nối không thành công thì một mẹo nhỏ là lập tức chuyển sang máy chủ của bạn (trong trường hợp của mình là Windows) và mở webcam của bạn bằng app Camera từ Start. Sau đó quay trở lại máy ảo và bạn sẽ thấy một hộp thoại hiện lên hỏi chọn kết nối webcam với máy chủ hay với máy ảo (xem hình bên dưới). Chọn Connect to a virtual machine.

Cuối cùng, hãy kiểm tra xem kết nối có thực sự được thiết lập hay không bằng cách vào Player > Removeable Devices một lần nữa và bạn sẽ thấy một dấu tích bên cạnh webcam của mình. Nếu không, hãy làm lại các bước trên.

Tạo một image publisher

Đầu tiên, trong thư mục my_cam, hãy tạo một tệp mới với tên image_publisher.py. Toàn bộ code có sẵn tại đây nhưng mình thực sự khuyên bạn làm theo lời giải thích bên dưới và tự code.

Code

#!/usr/bin/python3
import rospy
import cv2
from cv_bridge import CvBridge, CvBridgeError
from sensor_msgs.msg import Image

def publish_image():
    """Capture frames from a camera and publish it to the topic /image_raw
    """
    image_pub = rospy.Publisher("image_raw", Image, queue_size=10)
    bridge = CvBridge()
    capture = cv2.VideoCapture("/dev/video0")

    while not rospy.is_shutdown():
        # Capture a frame
        ret, img = capture.read()
        if not ret:
            rospy.ERROR("Could not grab a frame!")
            break
        # Publish the image to the topic image_raw
        try:
            img_msg = bridge.cv2_to_imgmsg(img, "bgr8")
            image_pub.publish(img_msg)
        except CvBridgeError as error:
            print(error)

if __name__=="__main__":
    rospy.init_node("my_cam", anonymous=True)
    print("Image is being published to the topic /image_raw ...")
    publish_image()
    try:
        rospy.spin()
    except KeyboardInterrupt:
        print("Shutting down!")

Giải thích Code

Dòng đầu tiên được gọi là dòng shebang dùng để xác định vị trí của trình thông dịch (intepreter). Ở đây, Python3 (phiên bản Python mặc định trong Ubuntu 20) được sử dụng và nó nằm trong thư mục /usr/bin.

#!/usr/bin/python3

Tiếp đó, tất cả các thư viện cần thiết cần phải được import (liên kết). rospy là Python API để tạo các ứng dụng trong ROS bằng Python. cv2OpenCV, một thư viện mã nguồn mở dành cho thị giác máy tính. Ở ví dụ này chúng ta sẽ sử dụng opencv để đọc và hiển thị ảnh từ webcam. CvBridge được sử dụng để chuyển đổi giữa ROS Image message và OpenCV Image vì chúng không cùng một loại (type) dữ liệu. Cuối cùng, bạn cần import loại message Image (thuộc package sensor_msgs) vì dữ liệu được publish ở đây là hình ảnh.

import rospy
import cv2
from cv_bridge import CvBridge, CvBridgeError
from sensor_msgs.msg import Image

Tiếp theo, tạo một hàm có tên publish_image. Trong hàm này, tạo một publisher tên image_raw có kiểu dữ liệu là Image mà mình đã nói ở trên và queue_size là 10 (kích thước của hàng đợi). Một ví dụ cho dễ hiểu khái niệm queue_size là nếu bạn publish hình ảnh ở tốc độ 20 Hz (hoặc 20 khung hình mỗi giây) nhưng queue_size của bạn bằng 10 thì chỉ có 10 hình ảnh được xử lý (đặt vào hàng đợi) trong khi 10 cái còn lại sẽ bị loại bỏ. Trong ví dụ của chúng ta thì bạn không phải quan tâm nhiều đến nó vì bạn chỉ phải xử lý một message. Đây là một bài viết hay về cách chọn queue_size thích hợp.

image_pub = rospy.Publisher("image_raw", Image, queue_size=10)

Tạo một object CvBridge:

bridge = CvBridge()

Tạo video capture object giúp bạn đọc hình ảnh từ webcam của mình. /dev/video0 là tên thiết bị. Nếu bạn sử dụng một máy ảnh ngoài mà không phải webcam tích hợp trong laptop thì tên này có thể khác. Bạn có thể kiểm tra bằng cách sử dụng lệnh sau: ls -ltrh /dev/video*. Trong trường hợp của mình, nó liệt kê cả /dev/video0/dev/video1 nhưng chỉ /dev/video0 hoạt động.

capture = cv2.VideoCapture("/dev/video0")

Sau đó, tạo một while loop. Vòng lặp này luôn kiểm tra xem Node có đang chạy hay không bằng cách check rospy.is_shutdown(). Nó bị ngắt nếu có gián đoạn, ví dụ như bạn dừng chương trình bằng cách sử dụng Ctrl + C. capture.read() trả về một biến boolean ret (True nếu ảnh được đọc chính xác và false nếu không) và một hình ảnh img. Ảnh này được mã hóa trong OpenCV ở định dạng 3 kênh (channel) Blue, Green và Red (hay BGR) và mỗi kênh là một ma trận (một mảng 2 chiều). Trong Phụ lục, bên dưới, mình giải thích chi tiết hơn cách một hình ảnh được mã hóa.

Nếu ret trả về False thì đã có lỗi (nhiều khả năng là không thể kết nối được với webcam). Bạn cần check trong terminal xem lỗi là gì và sửa. Trước khi có thể publish ảnh, bạn cần chuyển đổi nó thành ROS message dạng img_msg bằng hàm cv2_to_imgmsg (từ CvBridge) với mã hóa màu "bgr8"(để những subscriber biết thứ tự màu là gì, trong trường hợp này là blue-green-red và 8-bit). Cuối cùng, bạn dùng publisher image_pub mà bạn đã tạo để publish message img_msg bằng hàm publish.

    while not rospy.is_shutdown():
        # Capture a frame
        ret, img = capture.read()
        if not ret:
            rospy.ERROR("Could not grab a frame!")
            break
        # Publish the image to the topic image_raw
        try:
            img_msg = bridge.cv2_to_imgmsg(img, "bgr8")
            image_pub.publish(img_msg)
        except CvBridgeError as error:
            print(error)

Trong hàm main, trước tiên bạn khởi tạo một Node có tên là my_cam. Lưu ý: trong ROS, các Node được đặt tên duy nhất. Nếu hai Node có cùng tên được khởi tạo, thì Node khởi tạo trước sẽ bị tắt. Biến anonymous được đặt True để rospy sẽ chọn một tên duy nhất cho Node publisher của chúng ta (bằng cách thêm các số ngẫu nhiên vào cuối tên) để nhiều publihser có thể chạy đồng thời.

if __name__=="__main__":
    rospy.init_node("my_cam", anonymous=True)
    print("Image is being published to the topic /image_raw ...")
    publish_image()
    try:
        rospy.spin()
    except KeyboardInterrupt:
        print("Shutting down!")

Sau khi khởi tạo Node, hàm publish_image được gọi. Và bước cuối cùng là gọi rospy.spin(), thao tác này chỉ đơn giản là giữ Node của bạn không tắt cho đến khi Node bị dừng (ví dụ: bằng Ctrl + C). Và thế là xong! Publisher của bạn đã sẵn sàng để chạy.

Chạy Publisher

Để chạy image_publisher.py thì bạn phải làm cho nó execuatble (có thể thực thi) bằng cách sử dụng lệnh

sudo chmod +x image_publisher.py

hoặc nhấp chuột phải vào tệp, chọn Properties > Permissions > Allow executing files as program.

Sau đó, mở 2 terminal hoặc một terminator với 2 terminal. Trong terminal đầu tiên, chạy roscore để bắt đầu ROS Master. Trong terminal thứ hai, chạy rosrun my_cam image_publisher.py trong đó my_cam là tên gói và image_publisher.py là script. Một cách khác để chạy publisher là dùng roslaunch mình có đề cập ở phần Phụ lục bên dưới.

Hiển thị hình ảnh được publish

Bạn sẽ thấy đèn LED bên cạnh webcam sáng lên. Và nếu bạn mở một terminal khác và chạy: rostopic list, bạn sẽ thấy topic /image_raw được liệt kê. Bạn thậm chí có thể nhìn thấy những gì bên trong /image_raw bằng cách chạy lệnh này: rostopic echo /image_raw và nó sẽ in ra các mảng số đại diện cho hình ảnh. Để hiển thị hình ảnh, bạn cần viết một image subscriber (hay Người In Ấn trong ví dụ trước) mà chúng ta sẽ thực hiện trong chương tiếp theo TODO. ROS cũng cung cấp một công cụ để trực quan hóa các loại dữ liệu được publish trong đó có hình ảnh. Chạy rqt trong một terminal và một cửa sổ sẽ hiện ra. Tìm đến Plugin > Visualization > Image View, sau đó chọn /image_raw trong danh sách topic và bạn sẽ thấy hình ảnh trực tiếp từ webcam của bạn.

Phụ lục

Cách máy tính "nhìn" một bức ảnh

Trong thị giác máy tính, một bức ảnh thường được biểu thị bằng Mô hình màu RGB. RGB là viết tắt của Red, Green và Blue, 3 màu cơ bản mà từ đó bạn có thể tạo ra bất kỳ màu nào khác.

Mỗi kênh màu (color channel) được mã hóa trong một ma trận pixel (điểm ảnh). Nếu các pixel ở dạng 8 bit, thì giá trị của chúng nằm trong khoảng từ 0 đến 255 (hoặc 2^8 - 1). Trong ví dụ bên dưới, bạn có thể thấy cách một bức ảnh (với chiều cao 140 pixel và chiều rộng 140 pixel) được phân tách thành 3 kênh khác nhau và mỗi kênh thực chất là một ma trận 140x140. Các kênh này được kết hợp thành một ma trận 3D mà mỗi phần tử chứa các pixel Blue (Xanh lam), Green (Xanh lục), Red (Đỏ). Thứ tự BGR này là thứ tự màu mặc định của OpenCV và nó có thể khác với các thư viện khác.

Chạy Node bằng roslaunch

Ngoài rosrun, một cách phổ biến được dùng nhiều hơn để chạy Node là roslaunch. Những hạn chế của rosrun so với roslaunch:

  • rosrun chỉ có thể chạy một lúc một Node từ một package trong khi roslaunch có thể chạy nhiều Node từ nhiều package.

  • roslaunch tự khởi động roscore (nếu chưa có) còn rosrun thì không.

  • Để dùng được roslaunch cần một tệp .launch và người dùng có thể tùy biến file này như gọi các launch file khác, hoặc thay đổi biến của các hàm, v.v trong khi rosrun không có lựa chọn này.

Một tệp .launch thực chất là một tệp XML nên cách tạo cũng khá dễ dàng. Đầu tiên bạn tạo một folder tên launch rồi tạo một file text đặt tên là my_cam.launch. Nội dung của file này như sau:

<launch>
    <arg name="camera_name" default="/dev/video0"/>
    <node name="my_webcam" pkg="my_cam" type="image_publisher_launch.py" output="screen">
        <param name="camera_name" type="string" value="$(arg camera_name)" />
    </node>
</launch>

Giải thích: Một tệp launch bắt đầu bằng tag <launch> và kết thúc bằng tag </launch>. Đầu tiên mình tạo một biến (argument) có tên camera_name và giá trị mặc định của nó là /dev/video0. Mình sẽ giải thích cách dùng của nó ở bên dưới. Khi muốn dùng biến này trong launch file thì gọi nó bằng $(arg camera_name) như ở dòng thứ 4. Dòng tiếp theo định nghĩa Node mà mình muốn chạy, trong đó name là tên của Node (tên này sẽ được dùng nếu nó khác với tên bạn đặt trong tệp Python), pkg là tên package (ở đây là my_cam), type là tên file (ở đây là image_publisher_launch.py), output là để chọn cách hiển thị các thông tin (nếu là screen thì thông tin sẽ được print ra màn hình, còn là log thì sẽ được lưu trong một file log). Cuối cùng, tạo một tham số (parameter) có tên camera_name với type string và giá trị là giống với biến camera_name bên trên. Bạn có thể sử dụng tham số này trong code và vì thế không cần phải sửa code mỗi khi cần thay đổi một thông tin nào đó. Cụ thể, mình đã tạo một file image_publisher_launch.py. File này nội dung gần giống y hệt như image_publisher.py chỉ khác ở chỗ mình cho nó thành một class và khi khởi tạo class thì có dòng:

self.capture = cv2.VideoCapture(rospy.get_param("my_webcam/camera_name"))

Ở đây thay vì cho một giá trị mặc định như trước thì bạn dùng hàm rospy.get_param để lấy tham số từ launch file. Vì thế nếu ví dụ bạn dùng một camera có tên khác, thì bạn chỉ cần thay đổi launch file. Hoặc cách hay hơn là khi chạy roslaunch bạn có thể dùng biến (argument) đã được tạo ở đây <arg name="camera_name" default="/dev/video0"/>

Chạy roslaunch:

roslaunch my_cam my_cam.launch camera_name:="/dev/video0"

Nếu bạn sử dụng giá trị mặc định của các biến bạn chỉ cần chạy roslaunch my_cam my_cam.launch. Nhưng nếu bạn dùng một camera khác hoặc tên của nó không phải là /dev/video0 thì bạn chỉ cần thay đổi tên ở lệnh bên trên. Rất tiện lợi đúng không?

Ngoài ra, còn nhiều tag khác mà mình không list ra được hết. Các bạn có thể tham khảo thêm ở đâyở đây.

Trích dẫn

  1. Ảnh bìa: http://wiki.ros.org/hydro

Did you find this article valuable?

Support Trinh Nguyen by becoming a sponsor. Any amount is appreciated!