# 24. ROS2-AI Stair Recognition and Negotiating Course
## 24.1 Stair Recognition
### 24.1.1 Program Logic
Before the game, we need to DIY a stair. We can use 20mm height items as stair, such as board and sturdy box. And paste the red electrical tape on the border, because PuppyPi recognize the stair through recognizing the color.
Stair recognition process is as follow.
Firstly, program to recognize the color of line. Use Lab color space to convert the image from RGB into Lab. Then, perform binaryzation, corrosion, dilation, etc., on the image to obtain the contour which contains the target color. Next, mark the contour with rectangle.
Then, acquire the diagonal point of the rectangle and draw the center of the line.
Lastly, display the information about the line center on the terminal.
### 24.1.2 Operation Steps
:::{Note}
The input command should be case and space sensitive.
:::
(1) Power on the robot, then follow the steps in [3.3 Docker Container Introduction and Entry](3.Remote_Tool_Installation_Connection.md#docker-container-introduction-and-entry) and [3.4 ROS Version Switch Tool Guide](3.Remote_Tool_Installation_Connection.md#ros-version-switch-tool-guide) to connect via the VNC remote control software and switch to the ROS 2 environment.
(2) Click
to open Terminator ROS2 terminal.
(3) Input the command below and press Enter to start the game.
```bash
ros2 launch example negotiate_stairs_demo.launch.py model:=0
```
(4) If want to close this game, we can press "**Ctrl+C**". If it fails to close the game, please try again.
### 24.1.3 Program Outcome
The program is default to detect red.
Paste the red electrical tape on the boarder of the stair, and then place the stair in front of PuppyPi. After the game starts, PuppyPi will recognize the line. When recognized, it will be framed on the camera returned image and its center will be drawn.
### 24.1.4 Program Analysis
The source code of this program is stored in **[ros2_ws/src/example/example/advanced_functions/negotiate_stairs_demo.py](../_static/source_code/ros2/advanced_functions.zip)**
(1) Import Function Package
{lineno-start=3}
```py
import sys
import cv2
import time
import math
import numpy as np
from enum import Enum
import yaml
from sdk import Misc
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
from sensor_msgs.msg import Image
from std_srvs.srv import Empty, SetBool
from interfaces.srv import *
from puppy_control_msgs.msg import Velocity, Pose, Gait
from puppy_control_msgs.srv import SetRunActionName
from cv_bridge import CvBridge
```
Using import statements to import the required modules: math provides a range of mathematical functions and constants for related calculations; rospy is used for ROS communication. `from object_tracking.srv import *` imports services related to object tracking. `from puppy_control.msg import Velocity, Pose, Gait` imports services for controlling and transmitting the velocity, pose, and gait of the robot.
* **Image Processing**
(1) Gaussian Filtering
Before converting the image from RGB into Lab space, denoise the image and use `GaussianBlur()` function in cv2 library for Gaussian filtering.
{lineno-start=176}
```py
frame_gb = cv2.GaussianBlur(frame_resize, (3, 3), 3)
```
The meaning of the parameters in bracket is as follow:
① The first parameter `frame_resize` is the input image
② The second parameter `(3, 3)` is the size of Gaussian kernel
③ The third parameter `3` is the allowable range of variation around the average in Gaussian filtering. The larger the value, the larger the allowable range of variation
(2) Binaryzation Processing
Adopt `inRange()` function in cv2 library to perform binaryzation on the image.
{lineno-start=189}
```py
frame_mask = cv2.inRange(
frame_lab,
tuple(self.color_range_list[detect_color]['min']),
tuple(self.color_range_list[detect_color]['max'])
)
```
The first parameter in the bracket is the input image. The second and the third parameters respectively are the lower limit and upper limit of the threshold. When the RGB value of the pixel is between the upper limit and lower limit, the pixel is assigned 1, otherwise, 0.
(3) Open Operation and Close Operation
To reduce interference and make the image smoother, it is necessary to process the image.
{lineno-start=195}
```py
opened = cv2.morphologyEx(frame_mask, cv2.MORPH_OPEN, np.ones((6, 6), np.uint8))
closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, np.ones((6, 6), np.uint8))
```
cv2.MORPH_OPEN refers to open operation where corrosion will be conducted first, then dilation. cv2.MORPH_CLOSE indicates close operation where dilation will be conducted first, then corrosion.
Take `opened = cv2.morphologyEx(frame_mask, cv2.MORPH_OPEN, np.ones((6, 6), np.uint8))` for example. The meaning of the parameters in bracket is as follow.
① The first parameter `frame_mask` is the input image.
② The second parameter `cv2.MORPH_OPEN` refers to processing method, open operation.
③ The third parameter `np.ones((6, 6), np.uint8)` is frame size.
(4) Acquire the Maximum Contour
After processing the image, acquire the contour of the target to be recognized, which involves `findContours()` function in cv2 library.
{lineno-start=198}
```py
if detect_color:
cnts = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_L1)[-2]
cnt_large, area_max = getAreaMaxContour(cnts)
```
The first parameter in parentheses is the input image; the second parameter is the retrieval mode of the contour; the third parameter is the approximation method of the contour.
Find the contour of the maximum area among the obtained contours. To avoid interference, please set a minimum value. Only when the area is greater than this value, the target contour is valid.
{lineno-start=202}
```py
if cnt_large is not None:
rect = cv2.minAreaRect(cnt_large)
box = np.int0(cv2.boxPoints(rect))
centerX = rect[0][0]
centerY = rect[0][1]
centerX = int(Misc.map(centerX, 0, self.image_size[0], 0, img_w))
centerY = int(Misc.map(centerY, 0, self.image_size[1], 0, img_h))
for i in range(4):
box[i, 1] = int(Misc.map(box[i, 1], 0, self.image_size[1], 0, img_h))
box[i, 0] = int(Misc.map(box[i, 0], 0, self.image_size[0], 0, img_w))
cv2.drawContours(img, [box], -1, (0, 0, 255, 255), 2)
self.target_centre_point = [centerX, centerY]
cv2.circle(img, (centerX, centerY), 5, (0, 0, 255), -1)
# 打印目标颜色和中心点
self.get_logger().info(f"检测到目标颜色: {detect_color}, 中心点: {self.target_centre_point}")
```
* **Acquire the Position**
(1) Frame the Line
Call `drawContours()` function to set the rectangle pattern and frame the line.
{lineno-start=213}
```py
cv2.drawContours(img, [box], -1, (0, 0, 255, 255), 2)
```
(2) Draw the Center
Next, acquire the diagonal points of the rectangle, and draw the line center through `circle()` function.
{lineno-start=213}
```py
cv2.drawContours(img, [box], -1, (0, 0, 255, 255), 2)
self.target_centre_point = [centerX, centerY]
cv2.circle(img, (centerX, centerY), 5, (0, 0, 255), -1)
# 打印目标颜色和中心点
self.get_logger().info(f"检测到目标颜色: {detect_color}, 中心点: {self.target_centre_point}")
else:
self.target_centre_point = None
self.get_logger().info(f"未检测到目标颜色: {detect_color}")
return img
```
and run the following command to enter the directory containing the program:
```bash
cd ~/ros2_ws/src/example/example/advanced_functions
```
(2) Use the vim editor to open the program file:
```bash
vim negotiate_stairs_demo.py
```
(3) Find the line of code responsible for setting the target color. Refer to the image below for guidance.
:::{Note}
In vim, you can jump to a specific line by typing the line number, then pressing Shift + G.(Note: The line number shown in the image is for reference only. Please locate the actual line in your file.)
:::
(4) Press the i key to enter insert mode, and change the line to: self.set_target('green')
(5) After editing, press the Esc key, then type the following to save and exit:
```bash
:wq
```
(6) Run the command below to restart the program and observe the updated behavior:
```bash
ros2 launch example negotiate_stairs_demo.launch.py model:=0
```
* **Adding a New Recognizable Color**
In addition to the default colors configured in the program, users can add custom colors for recognition. This section provides step-by-step instructions for adding **yellow** as a new detectable color:
(1) Open a terminal
and run the following command to enter the directory containing the program:
```bash
~/.stop_ros.sh
```
(2) Start the USB camera node with the following command:
```bash
ros2 launch peripherals usb_cam.launch.py
```
(3) Open a new terminal
and navigate to the LAB Tool directory:
```bash
cd /home/ubuntu/software/lab_tool
```
(4) Run the PC software:
```bash
python3 main.py
```
(5) Click the **"Add"** button located at the bottom right corner of the interface.
(6) In the popup dialog, enter **"yellow"** as the new color name and click **"OK"**.
(7) From the color selection panel at the bottom right, select **"yellow"**.
(8) Place a yellow object within the camera's field of view. Adjust the **L**, **A**, and **B** sliders until the yellow area appears **white** on the left side of the interface, while all other areas appear **black**.
(9) Once you're satisfied with the result, click the **"Save"** button to store the LAB threshold values. Then, close the LAB Tool.
(10) To ensure the new color settings were saved correctly, open the configuration file using the following commands:
```bash
cd software/lab_tool/ && vim lab_config.yaml
```
(11) To test the updated settings, restart the program with the command below:
```bash
ros2 launch example negotiate_stairs_demo.launch.py model:=0
```
## 24.2 Stair Negotiating
### 24.2.1 Program Logic
Before the game, we need to DIY a stair. We can use 20mm height items as stair, such as board and sturdy box. And paste the red electrical tape on the border. Then PuppyPi will recognize the red tape and approach it to finish negotiating the stair.
Stair negotiating process is as follow.
Firstly, program to recognize the color of line. Use Lab color space to convert the image from RGB into Lab. Then, perform binaryzation, corrosion, dilation, etc., on the image to obtain the contour which contains the target color. Next, mark the contour with rectangle. And draw the center of line with red dot.
Next, control PuppyPi to approach the line center to come closer to the stair.
Lastly, control PuppyPi to negotiate the stair.
### 24.2.2 Operation Steps
:::{Note}
The input command should be case and space sensitive.
:::
(1) Power on the robot, then follow the steps in [3.3 Docker Container Introduction and Entry](3.Remote_Tool_Installation_Connection.md#docker-container-introduction-and-entry) and [3.4 ROS Version Switch Tool Guide](3.Remote_Tool_Installation_Connection.md#ros-version-switch-tool-guide) to connect via the VNC remote control software and switch to the ROS 2 environment.
(2) Click
to open Terminator ROS2 terminal. Enter the commands to activate the feature (two lines of commands, entered one line at a time):
```bash
ros2 launch example negotiate_stairs_demo.launch.py model:=1
```
(3) To exit this feature, press **"Ctrl+C"** in the LX terminal interface. If the shutdown fails, try pressing **"Ctrl+C"** multiple times.
### 24.2.3 Program Outcome
:::{Note}
The program is default to detect red.
:::
Paste the red electrical tape on the boarder of the stair, and then place the stair in front of PuppyPi. After the game starts, PuppyPi will recognize the line, and then approach the center of the line to negotiate the stair.
### 24.2.4 Program Analysis
The source code for this program is located within the Docker container.
**[ros2_ws/src/example/example/advanced_functions/negotiate_stairs_demo.py](../_static/source_code/ros2/advanced_functions.zip)**
* **Approach the Stair**
In the first lesson, we introduced how to identify stairs. Next, you can control the robot to approach the stairs based on the center coordinate information of the lines, as shown in the diagram below:
In the code, an enumerated class named **PuppyStatus** is defined, which includes five states. These states represent different actions or events, such as:
`LOOKING_FOR`: Searching for the stairs
`FOUND_TARGET`: Target stairs found
`DOWN_STAIRS`: Descending the stairs
`STOP`: Stopping
`END`: Ending
The **getAreaMaxContour** function is used to find the largest contour by area from a set of contours. The function iterates through all contours, calculates the area of each, and records the largest contour and its area. If a contour's area is greater than or equal to 5, it is considered valid and returned as the result.
* **Negotiate the Stair**
After PuppyPi approaches the stair, call the built-in action group "**up_stairs_2cm.d6ac**" to control it to negotiate the stair.
{lineno-start=103}
```python
def move(self):
if not self.is_running:
return
with self.lock:
if self.model == 0:
# model=0:仅识别,不控制运动
return
# 状态机逻辑
if self.puppyStatus == PuppyStatus.LOOKING_FOR:
if self.target_centre_point and self.target_centre_point[1] > 400:
self.puppyStatus = PuppyStatus.FOUND_TARGET
velocity_msg = Velocity(x=0.0, y=0.0, yaw_rate=0.0)
self.velocity_publisher.publish(velocity_msg)
self.up_stairs_time = time.time()
self.action_group_executed = False
self.get_logger().info('发现目标,准备执行上台阶动作')
else:
velocity_msg = Velocity(x=10.0, y=0.0, yaw_rate=0.0)
self.velocity_publisher.publish(velocity_msg)
self.get_logger().info('未发现目标,继续向前移动')
elif self.puppyStatus == PuppyStatus.FOUND_TARGET:
if not self.action_group_executed:
self.run_action_group('up_stairs_2cm.d6ac', True)
self.action_group_executed = True
self.get_logger().info('执行上台阶动作组')
if time.time() - self.up_stairs_time > 25:
self.puppyStatus = PuppyStatus.DOWN_STAIRS
pose_msg = Pose(
stance_x=0.0, stance_y=0.0, x_shift=0.0,
height=0.3, roll=0.0, pitch=0.0, yaw=0.0, run_time=500
)
self.pose_publisher.publish(pose_msg)
self.get_logger().info('超时,切换到下台阶状态')
elif self.puppyStatus == PuppyStatus.DOWN_STAIRS:
velocity_msg = Velocity(x=14.0, y=0.0, yaw_rate=0.0)
self.velocity_publisher.publish(velocity_msg)
self.get_logger().info('正在下台阶')
self.puppyStatus = PuppyStatus.END
elif self.puppyStatus == PuppyStatus.END:
velocity_msg = Velocity(x=0.0, y=0.0, yaw_rate=0.0)
self.velocity_publisher.publish(velocity_msg)
self.get_logger().info('任务完成,停止移动')
# 状态变化日志
if self.puppyStatusLast != self.puppyStatus:
self.get_logger().info(f'当前状态: {self.puppyStatus}')
self.puppyStatusLast = self.puppyStatus
```
**Publishing Speed Messages**
In the `move` method, speed messages are published using the command `self.velocity_publisher.publish(velocity_msg)`. This is similar to the example where `self.rect = self.rect.move(self.speed)` is used to move an object by updating its speed. Both approaches rely on publishing speed messages to control movement.
**State Transitions:** The code determines the next action by checking the value of `self.puppyStatus`. This is comparable to how the `move` function in the example uses the current state to adjust the ball's movement.
**Logging:** The code utilizes `self.get_logger().info()` to log information, which is consistent with the logging approach mentioned in the example.
Behavior Based on `self.puppyStatus`:
① When `self.puppyStatus` is `PuppyStatus.LOOKING_FOR`:
- If `self.target_centre_point` exists and its `y` coordinate is greater than 400, the robot's status is updated to `PuppyStatus.FOUND_TARGET`. A speed message is then published to stop the robot.
- If `self.target_centre_point` does not exist or its `y` coordinate is not greater than 400, a speed message is published to move the robot with an `x` velocity of 10.0.
② When `self.puppyStatus` is `PuppyStatus.FOUND_TARGET`:
- If `self.action_group_executed` is `False`, the robot executes an action group named `'up_stairs_2cm.d6ac'` and sets `self.action_group_executed` to `True`.
③ When `self.puppyStatus` is `PuppyStatus.DOWN_STAIRS`:
- A speed message is published to move the robot dog forward with an `x` velocity of 14.0.
- The robot's status is then updated to `PuppyStatus.END`, and a log message is recorded.
### 24.2.5 Function Extension
* **Close Debugging Interface**
As the continuous refresh of debugging interface will occupy CPU of Raspberry Pi, we can close debugging interface to tackle choppy running.
(1) Input the command below and press Enter to edit the stair negotiating program.
```bash
cd ros2_ws/src/example/example/advanced_functions/
```
(2) Next, enter the following command to open the program file using the Vim editor.
```bash
vim negotiate_stairs_demo.py
```
(3) Next, jump to this line of code.
:::{Note}
we can input the line code and press **"Shift+G"** to jump to the corresponding line. The numbering of code positions in the diagram is for reference only, please refer to the actual positions.
:::
(4) Press "**i**" key to enter editing mode. Then add "**#**" in front of the codes in the red frame to comment.
(5) After modification, press "**Esc**" and input "**:wq**" and press Enter to save and exit editing.
```bash
:wq
```
(6) Input the following command to restart the game and check PuppyPi's performance.
```bash
ros2 launch example negotiate_stairs_demo.launch.py model:=1
```
(7) If you need to view the debugging screen again (real-time feedback from the camera), you can uncomment the content boxed in step 3), i.e., remove the "**#**" in front of the code, then save, as shown in the following figure:
* **Change Target Recognized Color**
The default color of the step edge lines in the program is red. To change the color—for example, to black—please refer to section [24.1.5 Function Extension](#anchor_24_1_5)
After completing the modification, run the program to see the updated effect.
```bash
ros2 launch example negotiate_stairs_demo.launch.py model:=1
```