# 7. Tạo một ROS Publisher

Bạn có thể bắt đầu với tutorial [Writing a Simple Publisher and Subscriber (Python)](http://wiki.ros.org/ROS/Tutorials/WritingPublisherSubscriber%28python%29) 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](https://robodev.blog/nhung-khai-niem-co-ban-trong-ros). 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 &gt; Manage &gt; Virtual Machine Settings...** hoặc **Ctrl + D**.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1670446255141/k7nve4iHU.jpg align="center")

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1670447769469/B9RBwGIrx6.jpg align="center")

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1670448489176/ng1QkjItE.jpg align="center")

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1670449224068/42BVA0AMm.jpg align="center")

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 &gt; 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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1670482874189/00xwQvGRJ.jpg align="center")

# 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](https://github.com/TrinhNC/my_cam) 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

```python
#!/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](https://en.wikipedia.org/wiki/Shebang_(Unix)) 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`.

```python
#!/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](http://wiki.ros.org/rospy) là Python API để tạo các ứng dụng trong ROS bằng Python. `cv2` là [OpenCV](https://en.wikipedia.org/wiki/OpenCV), 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](http://wiki.ros.org/cv_bridge) đượ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](http://wiki.ros.org/sensor_msgs)) vì dữ liệu được publish ở đây là hình ảnh.

```python
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](http://wiki.ros.org/rospy/Overview/Publishers%20and%20Subscribers#Choosing_a_good_queue_size).

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

Tạo một object CvBridge:

```python
bridge = CvBridge()
```

Tạo [video capture object](https://docs.opencv.org/4.x/dd/d43/tutorial_py_video_display.html) 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` và `/dev/video1` nhưng chỉ `/dev/video0` hoạt động.

```python
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](https://robodev.blog/tao-ros-publisher#heading-phu-luc), 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`.

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

```python
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

```bash
sudo chmod +x image_publisher.py
```

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1670879724888/n8DziyKWL.jpg align="center")

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](https://robodev.blog/tao-ros-publisher#heading-chay-node-bang-roslaunch) bên dưới.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1670880437417/2wCd3vorL.jpg align="center")

## 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](https://robodev.blog/write-a-ros-subscriber) 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 &gt; Visualization &gt; 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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1670880937685/PrS86GF2k.png align="center")

# 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](https://en.wikipedia.org/wiki/RGB_color_model). 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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672004602847/6724a545-2555-4ac1-86f1-b684dbcacde3.png align="center")

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672004523101/808080f8-6966-4d21-8b3c-c7527096d0f0.jpeg align="center")

## 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](https://github.com/TrinhNC/my_cam/blob/main/launch/my_cam.launch):

```xml
<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](https://github.com/TrinhNC/my_cam/blob/main/src/image_publisher_launch.py). File này nội dung gần giống y hệt như [image\_publisher.py](https://github.com/TrinhNC/my_cam/blob/main/src/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:

```python
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`:

```bash
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](http://wiki.ros.org/roslaunch/XML) và [ở đây](http://wiki.ros.org/roslaunch).

# Trích dẫn

1. Ảnh bìa: [http://wiki.ros.org/hydro](http://wiki.ros.org/hydro)
