# 6\. ROS+OpenCV Course
## 6.1 Color Threshold Adjustment
Various light sources can affect colors differently, leading to discrepancies in recognition. To address this issue, you can utilize LABTOOL to adjust the color threshold, ensuring more accurate and consistent color recognition.
### 6.1.1 Launching and Closing LAB TOOL
The input command should be case sensitive, and the keywords can be complemented by the **Tab** key.
1. Please strictly follow the steps below to open the LAB TOOL. Otherwise, the tool cannot be opened.
2. Power on the robot and connect it to the remote control software NoMachine. For instructions on setting up the remote desktop connection, refer to the section [1.7.2 AP Mode Connection Steps](https://docs.hiwonder.com/projects/ROSOrin/en/latest/docs/1_ROSOrin_User_Manual.html#ap-mode-connection-steps) in the user manual.
3. Click the terminal icon
in the system desktop to open a command-line window.
4. Enter the following command and press **Enter** to stop the app auto-start service.
```bash
sudo systemctl stop start_app_node.service
```
5. Enter the command to start the camera node:
```bash
ros2 launch peripherals depth_camera.launch.py
```
6. Then open a new terminal, run the LAB Tool by entering the following command, and press **Enter**:
```bash
python3 /home/ubuntu/software/lab_tool/main.py
```
7. See the next section for an introduction to the interface and instructions for the buttons. Click the **Quit** button at the bottom right to close it.
8. After closing the LAB Tool, enter the command to restart the app auto-start service.
```bash
sudo systemctl restart start_app_node.service
```
> [!NOTE]
>
> **If the app auto-start service is not running, some app functions may not work properly. If the auto-start command has not been executed, restarting the robot will also automatically restart the APP auto-start service.**
### 6.1.2 LAB TOOL Interface Introduction
LABTOOL is divided into two parts, including the image display area and the recognition adjustment area.
1. Image display area: The processed camera feed is shown on the left, and the raw feed is shown on the right.
> [!NOTE]
>
> **If the video feed fails to display, indicating a camera connection error, check the camera cable for a secure connection or reconnect it.**
2. Adjustment Panel: Allows color-threshold tuning. The functions of each button are listed below.
| **Icon**| **Function**|
|----------|----------|
|
| Sliders L, A, and B are respectively used to adjust L, A, and B components of the camera returned image. The left sliders set the **minimum** values for each channel, and the right sliders set the **maximum** values.|
|
| Select the target color for threshold tuning.|
|
| Delete the selected color.|
|
| Add a new detectable color.|
|
| Save the adjusted value.|
|
| Switch the camera between the depth camera and the monocular camera.|
|
| Exit from the LAB TOOL.|
2. Modify all **min** values of L, A, and B to **0**, and **max** values to **255**.
3. Put the red block within the camera frame. According to the LAB color space, adjust L, A, and B components to approach the zone of the target color.
If the red area appears close to **+a**, increase the A component. Keep the A component’s **max** value unchanged and raise its **min** value until the target object in the left display turns white while all other areas turn black.
4. Based on the environment, modify the value of L and B. Increase the L component’s **min** value if the red tone is too light, or reduce its **max** value if it is too dark. Increase the B component’s **min** value if the red tone is too warm, or reduce its **max** value if it is too cool.
LAB Threshold Adjustment Parameter
| **Color Component**| **Range**| **Corresponding Color Zone**|
|----------|----------|----------|
| L| 0~255| Black-White(-L ~ +L)|
| A| 0~255| Green-Red(-a ~ +a)|
| B| 0~255| Blue-Yellow(-b ~ +b)|
5. Click **Save** to save the adjusted color threshold parameter.
### 6.1.4 Add New Color for Detection
In addition to the built-in detectable colors, additional colors can be added. Yellow is used as an example below. The steps are as follows:
1. Open the LAB TOOL and click the **Add** button.
2. Then fill in **yellow**, and click **OK**.
3. Select the newly added color from the color list.
4. Face the camera at the yellow block. And drag the sliders of L, A, and B to adjust the color threshold till the object on the left turns white and the other area turns black.
> [!NOTE]
>
> **For instructions on adjusting color thresholds, refer to section [6.1.3 Adjust Color Threshold](#anther6.1.3).**
5. Click **Save** to save the adjusted color threshold parameter.
Once the adjustment is completed, press **Ctrl+C** to disable the camera service, then click **Quit** to exit the interface.
6.2 Color Recognition
---
This section demonstrates the detection of red, green, and blue objects using OpenCV, with the results displayed on the video feed. Before starting the feature, please prepare three objects in these colors: red, green, and blue.
### 6.2.1 Recognition Process
Firstly, the program will acquire the RGB image of the camera, scale the image, and perform Gaussian filtering on the image to convert the image color space from RGB to LAB.
Based on this, color thresholds are used to identify the color of the object within the circle. A mask is then applied to the image, which involves selecting parts of the image, graphics, or objects to globally or locally block out areas in the image for processing.
After masking, morphological operations, including opening and closing, are performed on the object image to refine the results.
Opening operation: Involves erosion followed by dilation. Effect: Removes small objects, smooths object boundaries, and does not affect object area.
It can remove small particle noise and separate objects that are stuck together.
Erosion: Removes boundary pixels of an object, causing the edges to shrink inward. This operation can eliminate objects smaller than the structuring element.
Dilation: Expands the boundary pixels of an object by merging surrounding background pixels that are in contact with the object, causing the edges to grow outward.
Finally, the recognition results are overlaid on the return image.
### 6.2.2 Operation
> [!NOTE]
>
> **Commands must be entered with correct capitalization. The Tab key can be used to auto-complete keywords.**
1. Power on the robot and connect it via the NoMachine remote control software. For detailed information on connecting to a remote desktop, please refer to the section [1.7.2 AP Mode Connection Steps](https://docs.hiwonder.com/projects/ROSOrin/en/latest/docs/1_ROSOrin_User_Manual.html#ap-mode-connection-steps) in the user manual.
2. Click the terminal icon
in the system desktop to open a command-line window.
3. Enter the command to disable the app auto-start service.
```bash
sudo systemctl stop start_app_node.service
```
4. Enter the command to start the camera node:
```bash
ros2 launch peripherals depth_camera.launch.py
```
5. Open a new terminal, navigate to the program directory, and start the color detection feature by entering the following command:
```bash
cd ~/ros2_ws/src/example/example/color_detect && python3 color_detect_demo.py
```
6. The program will launch the camera image interface. For details on the detection steps, please refer to section [6.2.3 Project Outcome](#anther6.2.3) in this document.
7. To exit the feature, press **Ctrl+C** in the terminal. If the program does not close successfully, try pressing **Ctrl+C** again.
### 6.2.3 Project Outcome
> [!NOTE]
>
> **After the feature starts, please ensure there is no other object containing the recognition color except the target object within the camera view. Otherwise, the recognition result will be affected.**
After the feature starts, put the objects within the camera’s view. When the target object is detected, the program will highlight it with a circle of the corresponding color and display the color name in the lower-left corner of the window. The program can detect objects in red, blue, and green.
### 6.2.4 Program Analysis
The feature's source code is located at: **/home/ubuntu/ros2_ws/src/example/example/color_detect/color_detect_demo.py**
```python
# Color detection
range_rgb = {
'red': (0, 0, 255),
'blue': (255, 0, 0),
'green': (0, 255, 0),
'black': (0, 0, 0),
'white': (255, 255, 255),
}
def get_yaml_data(yaml_file):
yaml_file = open(yaml_file, 'r', encoding='utf-8')
file_data = yaml_file.read()
yaml_file.close()
data = yaml.load(file_data, Loader=yaml.FullLoader)
return data
lab_data = get_yaml_data("/home/ubuntu/software/lab_tool/lab_config.yaml")
```
* **Main Function**
Initializes the ROS node and creates the color detection node named `color_detect_node`, subscribes to the image topic `/depth_cam/rgb/image_raw` with `image_callback` as its callback function, runs the color detection `main` function using multi-threading, and waits for the node to shut down.
```python
if __name__ == '__main__':
rclpy.init()
node = rclpy.create_node('color_detect_node')
node.create_subscription(Image, '/depth_cam/rgb/image_raw', image_callback, 1)
threading.Thread(target=main, daemon=True).start()
rclpy.spin(node)
```
* **Functions**
1. The camera callback function is primarily used to read the video stream from the topic and push it into a queue, ensuring real-time display.
```python
def image_callback(ros_image):
cv_image = bridge.imgmsg_to_cv2(ros_image, "bgr8")
bgr_image = np.array(cv_image, dtype=np.uint8)
if image_queue.full():
# If the queue is full, discard the oldest image
image_queue.get()
# Put the image into the queue
image_queue.put(bgr_image)
```
2. Color Detection main Function
The images in the queue are read and passed into the `run` function to obtain the color-recognized frames, which are then displayed using `cv2`.
```python
def main():
running = True
while running:
try:
image = image_queue.get(block=True, timeout=1)
except queue.Empty:
if not running:
break
else:
continue
image = run(image)
cv2.imshow('image', image)
key = cv2.waitKey(1)
if key == ord('q') or key == 27: # Press Q or Esc to exit
break
```
3. YAML File Reading Function
```python
def get_yaml_data(yaml_file):
yaml_file = open(yaml_file, 'r', encoding='utf-8')
file_data = yaml_file.read()
yaml_file.close()
data = yaml.load(file_data, Loader=yaml.FullLoader)
return data
```
4. Maximum Contour Function
The function takes the contours detected by OpenCV as input, identifies the largest contour based on area, and returns both the contour and its area.
```python
# Find the contour with the largest area
# The parameter is a list of contours to be compared
def getAreaMaxContour(contours):
contour_area_temp = 0
contour_area_max = 0
area_max_contour = None
for c in contours: # Iterate through all contours
contour_area_temp = math.fabs(cv2.contourArea(c)) # Calculate the area of the contour
if contour_area_temp > contour_area_max:
contour_area_max = contour_area_temp
if contour_area_temp > 50: # Only when the area is greater than 50, the contour with the largest area is valid. This allows to filter out interference)
area_max_contour = c
return area_max_contour, contour_area_max # Return the contour with the largest area)
```
5. Color Recognition Function
The function takes an image as input and resizes it to the specified size (320, 240) to facilitate detection. The image is then processed with a Gaussian filter and converted to the LAB color space.
```python
def run(img):
global draw_color
global color_list
global detect_color
img_copy = img.copy()
img_h, img_w = img.shape[:2]
frame_resize = cv2.resize(img_copy, size, interpolation=cv2.INTER_NEAREST)
frame_gb = cv2.GaussianBlur(frame_resize, (3, 3), 3)
frame_lab = cv2.cvtColor(frame_gb, cv2.COLOR_BGR2LAB) # Convert image to LAB space
max_area = 0
color_area_max = None
areaMaxContour_max = 0
for i in ['red', 'green', 'blue']:
frame_mask = cv2.inRange(frame_lab, tuple(lab_data['lab']['Stereo'][i]['min']), tuple(lab_data['lab']['Stereo'][i]['max'])) #Perform bitwise operation on the original image and the mask
eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #Erode)
dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #Dilate
contours = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2] #Find contours
areaMaxContour, area_max = getAreaMaxContour(contours) #Find the largest contour
if areaMaxContour is not None:
if area_max > max_area:#Find the maximum area
max_area = area_max
color_area_max = i
areaMaxContour_max = areaMaxContour
```
The function loops through the predefined color list to perform color recognition and outputs the color corresponding to the largest detected contour.
```python
for i in ['red', 'green', 'blue']:
frame_mask = cv2.inRange(frame_lab, tuple(lab_data['lab']['Stereo'][i]['min']), tuple(lab_data['lab']['Stereo'][i]['max'])) #Perform bitwise operation on the original image and the mask
eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #Erode
dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #Dilate
contours = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2] #Find contours
areaMaxContour, area_max = getAreaMaxContour(contours) #Find the largest contour
if areaMaxContour is not None:
if area_max > max_area:#Find the maximum area
max_area = area_max
color_area_max = i
areaMaxContour_max = areaMaxContour
```
Only contours with an area greater than 200 are considered, smaller contours are filtered out. For contours larger than 200, the function calculates the contour coordinates and highlights the target color’s position in the frame based on the largest contour’s color.
```python
if max_area > 200: # The maximum area is found
((centerX, centerY), radius) = cv2.minEnclosingCircle(areaMaxContour_max) # Obtain the minimum circumscribed circle
centerX = int(common.val_map(centerX, 0, size[0], 0, img_w))
centerY = int(common.val_map(centerY, 0, size[1], 0, img_h))
radius = int(common.val_map(radius, 0, size[0], 0, img_w))
cv2.circle(img, (centerX, centerY), radius, range_rgb[color_area_max], 2)# Draw circle
if color_area_max == 'red': # Red has the maximum area
color = 1
elif color_area_max == 'green': # Green has the maximum area
color = 2
elif color_area_max == 'blue': # Blue has the maximum area
color = 3
else:
color = 0
color_list.append(color)
if len(color_list) == 3: # Determine multiple times
# Averaging
color = int(round(np.mean(np.array(color_list))))
color_list = []
if color == 1:
detect_color = 'red'
draw_color = range_rgb["red"]
elif color == 2:
detect_color = 'green'
draw_color = range_rgb["green"]
elif color == 3:
detect_color = 'blue'
draw_color = range_rgb["blue"]
else:
detect_color = 'None'
draw_color = range_rgb["black"]
else:
detect_color = 'None'
draw_color = range_rgb["black"]
cv2.putText(img, "Color: " + detect_color, (10, img.shape[0] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.65, draw_color, 2)
return img
```
6.3 QR Code Creation and Recognition
---
This lesson is divided into two parts. The first part explains how to create a QR code, and the second part focuses on recognizing the created QR code and decoding its information via the terminal.
### 6.3.1 QR Code Generation
* **Generation Process**
First, create an instance of the QR code tool and set its detailed parameters.
Next, obtain the input data and populate it into the QR code.
Finally, generate a QR code image based on the data, display it in a window, and save it to the specified path.
* **Creation Steps**
> [!NOTE]
>
> **Commands must be entered with correct capitalization. The Tab key can be used to auto-complete keywords.**
1. Power on the robot and connect it via the NoMachine remote control software. For detailed information on connecting to a remote desktop, please refer to the section [1.7.2 AP Mode Connection Steps](https://docs.hiwonder.com/projects/ROSOrin/en/latest/docs/1_ROSOrin_User_Manual.html#ap-mode-connection-steps) in the user manual.
2. Click the terminal icon
in the system desktop to open a command-line window.
3. Enter the command to disable the app auto-start service.
```bash
sudo systemctl stop start_app_node.service
```
4. Enter the following command to switch to the program directory and start the QR code creation program:
```bash
cd ~/ros2_ws/src/example/example/qrcode && python3 qrcode_creater.py
```
After launching the program, enter characters in the terminal to generate a QR code. For example, type **ubuntu**.
Press **Enter**, and a QR code containing the input data will be displayed.
5. To exit the QR code window, press the **ESC** key or the **q** key.
6. Returning to the command-line terminal, the message displayed below confirms that the QR code has been successfully generated and saved.
7. Next, click the taskbar on the left side of the system interface, then click
to open the file manager. Navigate to the directory highlighted in the red box below to find that the generated QR code has been exported to the host system.
8. Drag the image to the PC desktop using NoMachine by dragging with the mouse. The image can then be printed or transferred to a mobile photo album.
* **Program Analysis**
The source code for the program is located at: **/home/ubuntu/ros2_ws/src/example/example/qrcode/qrcode_creater.py**
```python
# encoding: utf-8
import os
import cv2
import qrcode
import numpy as np
def create_qrcode(data, file_name):
'''
version:An integer value between 1 and 40 that controls the size of the QR code. The minimum value is 1, which indicates a 12x12 matrix.
If you want the program to determine the size automatically, set the value to None and use the fit parameter.
error_correction:It controls the error correction capability of the QR code, and can take one of the following four constants.
ERROR_CORRECT_L:Can correct about 7% or fewer errors.
ERROR_CORRECT_M(default):Can correct about 15% or fewer errors.
ROR_CORRECT_H:Can correct about 30% or fewer errors.
box_size:Controls the number of pixels in each small square in the QR code
border:Controls the number of squares in the border, the distance between the QR code and the image boundary. Default is 4, which is the minimum value specified by the relevant standard.
'''
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_H,
box_size=5,
border=4)
# Add data
qr.add_data(data)
# Fill data
qr.make(fit=True)
# Generate image
img = qr.make_image(fill_color=(0, 0, 0), back_color=(255, 255, 255))
```
- **Creating a QR Code Tool Object**
Use the `qrcode` module to create the required object and set the parameters for the QR code.
```python
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_H,
box_size=5,
border=4)
# Add data
qr.add_data(data)
```
The parameters of the function shown above are as follows:
The first parameter, `version`, is an integer from 1 to 40 that controls the size of the QR code. To let the program determine the size automatically, set this parameter to `None` and use the **fit** parameter.
The second parameter, `error_correction`, controls the error correction level of the QR code and can take the following values:
1. `ERROR_CORRECT_L` allows approximately 7% or less of errors to be corrected.
2. `ERROR_CORRECT_M` is the default value and allows approximately 15% or less of errors to be corrected.
3. `ROR_CORRECT_H` allows approximately 30% or less of errors to be corrected.
`box_size` controls the number of pixels contained in each module of the QR code.
`border` controls the number of modules included in the border and defines the distance between the QR code and the image edge. The default value is 4, which is the minimum required by the relevant standard.
- **Generating a QR Code**
Data is added using the `add_data` and `make` functions, and the QR code image is generated using the `make_image` function.
```python
# Add data
qr.add_data(data)
# Fill data
qr.make(fit=True)
# Generate image
img = qr.make_image(fill_color=(0, 0, 0), back_color=(255, 255, 255))
opencv_img = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)
```
The parameters of the `make_image` function are as follows:
`fill_color=(0, 0, 0)` sets the fill color of the QR code, which is black in this case.
`back_color=(255, 255, 255)` sets the background color of the QR code, which is white.
- **Displaying the Image**
Convert the image’s color space using the `cvtColor` function, then display it in a window using the `imshow` function.
```python
while True:
cv2.imshow('img', opencv_img)
k = cv2.waitKey(1)
if k != -1:
break
cv2.imwrite(file_name, opencv_img)
print('save', data, file_name)
```
- **Saving the Image**
Use the `imwrite` function to save the generated QR code image and print the relevant information.
```python
cv2.imwrite(file_name, opencv_img)
print('save', data, file_name)
```
The parameters of the imwrite function are:
`file_name` specifies the storage path of the image.
`opencv_img` is the image to be saved.
### 6.3.2 QR Code Recognition
In the previous section, a QR code is created, which will be used to demonstrate recognizing the content of a QR code.
* **Recognition Process**
First, create an instance of the QR code detector and load the required network structure and model weight files for detection.
Next, capture the video stream from the camera and perform detection on each frame.
Finally, when a QR code is detected, it will be highlighted with a bounding box, and the content of the QR code will be printed.
* **QR Code Recognition Steps**
> [!NOTE]
>
> **Commands must be entered with correct capitalization. The Tab key can be used to auto-complete keywords.**
1. Power on the robot and connect it via the NoMachine remote control software. For detailed information on connecting to remote desktop, please refer to section [1.7.2 AP Mode Connection Steps](https://docs.hiwonder.com/projects/ROSOrin/en/latest/docs/1_ROSOrin_User_Manual.html#ap-mode-connection-steps) in the user manual.
2. Click the terminal icon
in the system desktop to open a command-line window.
3. Enter the command to disable the app auto-start service.
```bash
sudo systemctl stop start_app_node.service
```
4. Enter the command to start the camera node:
```bash
ros2 launch peripherals depth_camera.launch.py
```
5. Open a new ROS2 terminal, navigate to the program directory, and start the QR code recognition program by entering the following command:
```bash
cd ~/ros2_ws/src/example/example/qrcode && python3 qrcode_detecter.py
```
To exit the feature, press **Ctrl+C** in the terminal. If the program does not close successfully, try pressing **Ctrl+C** again.
* **Project Outcome**
After starting the feature, the QR code appearing in the returned video frame is detected, highlighted with a red box, and its content is printed.
* **Program Analysis**
The source code for the program is located at: **/home/ubuntu/ros2_ws/src/example/example/qrcode/qrcode_detecter.py**
```python
#!/usr/bin/env python3
# encoding: utf-8
import os
import cv2
import queue
import rclpy
import threading
import numpy as np
from rclpy.node import Node
from cv_bridge import CvBridge
from sensor_msgs.msg import Image
class QRCodeDetectNode(Node):
def __init__(self, name):
rclpy.init()
super().__init__(name)
self.running = True
self.bridge = CvBridge()
self.model_path = os.path.join(os.path.abspath(os.path.split(os.path.realpath(__file__))[0]), 'model/detector.tflite')
self.image_queue = queue.Queue(maxsize=2)
self.image_sub = self.create_subscription(Image, '/depth_cam/rgb0/image_raw', self.image_callback, 1)
self.qcd = cv2.QRCodeDetector()
threading.Thread(target=self.main, daemon=True).start()
def image_callback(self, ros_image):
cv_image = self.bridge.imgmsg_to_cv2(ros_image, "bgr8")
bgr_image = np.array(cv_image, dtype=np.uint8)
```
1. Main Function
Initialize the `QRCodeDetectNode` and set the node name to **qrcode_detect**.
Wait for the ROS node to shut down.
If the node shuts down, unregister the current node.
```python
def main():
node = QRCodeDetectNode('qrcode_detect')
try:
rclpy.spin(node)
except KeyboardInterrupt:
node.destroy_node()
rclpy.shutdown()
print('shutdown')
finally:
print('shutdown finish')
if __name__ == "__main__":
main()
```
2. `QRCodeDetectNode` Class
The class initializes the node, defines parameters, and starts the `self.main` function.
Parameters:
`self.running` determines whether detection is enabled.
`self.model_path` is the path to the detection model.
`self.image_queue` is the queue for storing images.
`self.image_sub` subscribes to the camera video stream.
`self.qcd` initializes the QR code detection model.
```python
super().__init__(name)
self.running = True
self.bridge = CvBridge()
self.model_path = os.path.join(os.path.abspath(os.path.split(os.path.realpath(__file__))[0]), 'model/detector.tflite')
self.image_queue = queue.Queue(maxsize=2)
self.image_sub = self.create_subscription(Image, '/depth_cam/rgb0/image_raw', self.image_callback, 1)
self.qcd = cv2.QRCodeDetector()
threading.Thread(target=self.main, daemon=True).start()
def image_callback(self, ros_image):
cv_image = self.bridge.imgmsg_to_cv2(ros_image, "bgr8")
bgr_image = np.array(cv_image, dtype=np.uint8)
if self.image_queue.full():
# If the queue is full, discard the oldest image
self.image_queue.get()
# Put the image into the queue
self.image_queue.put(bgr_image)
def main(self):
while self.running:
try:
image = self.image_queue.get(block=True, timeout=1)
except queue.Empty:
if not self.running:
break
else:
continue
ret_qr, decoded_info, points, _ = self.qcd.detectAndDecodeMulti(image)
if ret_qr:
for s, p in zip(decoded_info, points):
if s:
print(s)
color = (0, 255, 0)
else:
color = (0, 0, 255)
image = cv2.polylines(image, [p.astype(int)], True, color, 8)
cv2.imshow('image', image)
key = cv2.waitKey(1)
if key == ord('q') or key == 27: # Press q or esc to exit
break
```
Functions:
The camera callback function reads the camera video stream and places the frames into the queue `self.image_queue`, automatically updating
and discarding outdated frames.
```python
def image_callback(self, ros_image):
cv_image = self.bridge.imgmsg_to_cv2(ros_image, "bgr8")
bgr_image = np.array(cv_image, dtype=np.uint8)
if self.image_queue.full():
# If the queue is full, discard the oldest image
self.image_queue.get()
# Put the image into the queue
self.image_queue.put(bgr_image)
```
The main function handles QR code detection. It checks the `self.running` parameter to determine whether to start detection.
If True, it reads images from the `self.image_queue`, inputs them into the initialized detection model `self.qcd`, and finally prints the recognized content and draws the detection boxes based on the output.
```python
def main(self):
while self.running:
try:
image = self.image_queue.get(block=True, timeout=1)
except queue.Empty:
if not self.running:
break
else:
continue
ret_qr, decoded_info, points, _ = self.qcd.detectAndDecodeMulti(image)
if ret_qr:
for s, p in zip(decoded_info, points):
if s:
print(s)
color = (0, 255, 0)
else:
color = (0, 0, 255)
image = cv2.polylines(image, [p.astype(int)], True, color, 8)
cv2.imshow('image', image)
key = cv2.waitKey(1)
if key == ord('q') or key == 27: # Press q or esc to exit
break
```
## 6.4 Autonomous Patrolling
### 6.4.1 Recognition Process
Firstly, the program will acquire the RGB image of the camera, scale the image, and perform Gaussian filtering on the image to convert the image color space from RGB to LAB.
Based on this, color thresholds are used to identify the color of the object within the circle. A mask is then applied to the image, which involves selecting parts of the image, graphics, or objects to globally or locally block out areas in the image for processing.
After masking, morphological operations, including opening and closing, are performed on the object image to refine the results.
Opening operation: Involves erosion followed by dilation. Effect: Removes small objects, smooths object boundaries, and does not affect object area. It can remove small particle noise and separate objects that are stuck together.
Erosion: Removes boundary pixels of an object, causing the edges to shrink inward. This operation can eliminate objects smaller than the structuring element.
Dilation: Expands the boundary pixels of an object by merging surrounding background pixels that are in contact with the object, causing the edges to grow outward.
Finally, the recognition results are overlaid on the return image.
### 6.4.2 Operation Steps
> [!NOTE]
>
> **Commands must be entered with correct capitalization. The Tab key can be used to auto-complete keywords.**
1. Power on the robot and connect it via the NoMachine remote control software. For detailed information on connecting to a remote desktop, please refer to section [1.7.2 AP Mode Connection Steps](https://docs.hiwonder.com/projects/ROSOrin/en/latest/docs/1_ROSOrin_User_Manual.html#ap-mode-connection-steps) in the user manual.
2. Click the terminal icon
in the system desktop to open a command-line window.
3. Enter the command to disable the app auto-start service.
```bash
sudo systemctl stop start_app_node.service
```
4. Enter the command to start the line-following node:
```bash
ros2 launch app line_following_node.launch.py debug:=true
```
5. Open a new terminal, navigate to the program directory, and start the autonomous patrolling feature by entering the following command:
```bash
ros2 service call /line_following/enter std_srvs/srv/Trigger {}
```
6. Next, in the current terminal, switch to the program directory and start the autonomous patrolling feature by entering the following command:
```bash
ros2 service call /line_following/set_running std_srvs/srv/SetBool "{data: True}"
```
7. Click on the line in the interface, select the corresponding color of the track, and the robot will start following the line.
8. To stop this feature, return to the terminal and enter the following command:
```bash
ros2 service call /line_following/enter std_srvs/srv/Trigger {}
```
To exit the feature, press **Ctrl+C** in the terminal. If the program does not close successfully, try pressing **Ctrl+C** again.
### 6.4.3 Program Analysis
* **Launch Analysis**
The program file corresponding to this section is located at **/home/ubuntu/ros2_ws/src/app/launch/line_following_node.launch.py**
1. Reading Paths
```python
compiled = os.environ['need_compile']
debug = LaunchConfiguration('debug', default='false')
debug_arg = DeclareLaunchArgument('debug', default_value=debug)
if compiled == 'True':
controller_package_path = get_package_share_directory('controller')
```
The package path is obtained using `get_package_share_directory`.
2. Launch
```python
peripherals_package_path = '/home/ubuntu/ros2_ws/src/peripherals'
line_following_node = GroupAction([
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(peripherals_package_path, 'launch/lidar.launch.py')),
condition=IfCondition(debug),
),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(peripherals_package_path, 'launch/depth_camera.launch.py')),
condition=IfCondition(debug),
),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(controller_package_path, 'launch/controller.launch.py')),
condition=IfCondition(debug),
),
Node(
package='app',
executable='line_following',
output='screen',
parameters=[{'debug': debug}],
),
```
`controller_launch` starts the motion control launch file.
`depth_camera_launch` starts the depth camera launch file.
`lidar_launch` starts the LiDAR launch file.
3. None
`line_following_node` starts the line following node.
```python
package='app',
executable='line_following',
output='screen',
```
* **Source Code Analysis**
The source code for this demo is located at **/home/ubuntu/ros2_ws/src/app/app/line_following.py**
> [!NOTE]
>
> **Before modifying the program, backup the original factory code. Do not modify the source code file directly to avoid robot malfunction due to incorrect parameter changes.**
Functions
`Main`:
```python
def main():
node = LineFollowingNode('line_following')
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
```
Starts the line-following node.
Class:
`LineFollower`:
```python
class LineFollower:
def __init__(self, color, node):
self.node = node
```
`Init`:
```python
def __init__(self, color, node):
self.node = node
self.target_lab, self.target_rgb = color
if 'Mecanum' in os.environ['MACHINE_TYPE'] or 'Acker' in os.environ['MACHINE_TYPE']:
self.rois = ((0.90, 0.92, 0, 1, 0.7), (0.78, 0.80, 0, 1, 0.2), (0.66, 0.68, 0, 1, 0.1))
else:
self.rois = ((0.81, 0.83, 0, 1, 0.7), (0.69, 0.71, 0, 1, 0.2), (0.57, 0.59, 0, 1, 0.1))
self.weight_sum = 1.0
```
Sets the line-following color and ROI list.
`get_area_max_contour`:
```python
def get_area_max_contour(contours, threshold=100):
'''
Get the contour of the largest area
:param contours:
:param threshold:
:return:
'''
contour_area = zip(contours, tuple(map(lambda c: math.fabs(cv2.contourArea(c)), contours)))
contour_area = tuple(filter(lambda c_a: c_a[1] > threshold, contour_area))
if len(contour_area) > 0:
max_c_a = max(contour_area, key=lambda c_a: c_a[1])
return max_c_a
return None
```
Gets the contour with the largest area.
`Call`:
```python
def __call__(self, image, result_image, threshold, color=None, use_color_picker=True):
centroid_sum = 0
h, w = image.shape[:2]
if use_color_picker:
min_color = [int(self.target_lab[0] - 50 * threshold * 2),
int(self.target_lab[1] - 50 * threshold),
int(self.target_lab[2] - 50 * threshold)]
max_color = [int(self.target_lab[0] + 50 * threshold * 2),
int(self.target_lab[1] + 50 * threshold),
int(self.target_lab[2] + 50 * threshold)]
target_color = self.target_lab, min_color, max_color
lowerb = tuple(target_color[1])
upperb = tuple(target_color[2])
else:
lowerb = tuple(color['min'])
upperb = tuple(color['max'])
for roi in self.rois:
blob = image[int(roi[0]*h):int(roi[1]*h), int(roi[2]*w):int(roi[3]*w)] # Intercept roi
img_lab = cv2.cvtColor(blob, cv2.COLOR_RGB2LAB) # Convert rgb into lab
img_blur = cv2.GaussianBlur(img_lab, (3, 3), 3) # Perform Gaussian filtering to reduce noise
mask = cv2.inRange(img_blur, lowerb, upperb) # Image binarization
eroded = cv2.erode(mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) # Corrode
dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) # Dilate
# cv2.imshow('section:{}:{}'.format(roi[0], roi[1]), cv2.cvtColor(dilated, cv2.COLOR_GRAY2BGR))
contours = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_L1)[-2] # Find the contour
max_contour_area = self.get_area_max_contour(contours, 30) # Get the contour corresponding to the largest contour
if max_contour_area is not None:
rect = cv2.minAreaRect(max_contour_area[0]) # Minimum circumscribed rectangle
box = np.intp(cv2.boxPoints(rect)) # Four corners
for j in range(4):
box[j, 1] = box[j, 1] + int(roi[0]*h)
cv2.drawContours(result_image, [box], -1, (0, 255, 255), 2) # Draw the rectangle composed of four points
```
Performs color recognition based on the set color and provides feedback on the recognized image and angle.
`LineFollowingNode`:
```python
class LineFollowingNode(Node):
def __init__(self, name):
rclpy.init()
super().__init__(name, allow_undeclared_parameters=True, automatically_declare_parameters_from_overrides=True)
self.name = name
self.color = ''
self.set_above = False
self.set_callback = False
self.is_running = False
```
`Init`:
```python
def __init__(self, name):
rclpy.init()
super().__init__(name, allow_undeclared_parameters=True, automatically_declare_parameters_from_overrides=True)
self.name = name
self.color = ''
self.set_above = False
self.set_callback = False
self.is_running = False
self.color_picker = None
self.follower = None
self.scan_angle = math.radians(45)
self.pid = pid.PID(0.01, 0.0, 0.0)
self.empty = 0
self.count = 0
self.stop = False
self.threshold = 0.5
```
Initializes parameters required for the program, calls the chassis and camera nodes, and starts services such as enter, exit, start, set color, get color, and set threshold.
`get_node_state`:
```python
def get_node_state(self, request, response):
response.success = True
return response
```
Sets the current node state.
`Main`:
```python
def main(self):
while True:
try:
image = self.image_queue.get(block=True, timeout=1)
except queue.Empty:
continue
result = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
# cv2.imshow("image", result)
cv2.imshow("image", cv2.resize(result, (display_size[0], display_size[1])))
if self.debug and not self.set_callback:
self.set_callback = True
# Set callback function for mouse clicking event
cv2.setMouseCallback("image", self.mouse_callback)
k = cv2.waitKey(1)
if k != -1:
break
```
Reads the image and uses the mouse for color selection.
`mouse_callback`:
```python
def mouse_callback(self, event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
self.get_logger().info("x:{} y{}".format(x, y))
msg = SetPoint.Request()
if self.image_height is not None and self.image_width is not None:
msg.data.x = x / self.image_width
msg.data.y = y / self.image_height
self.set_target_color_srv_callback(msg, SetPoint.Response())
```
Mouse color selection callback function to obtain the pixel coordinates of the current mouse position.
`enter_srv_callback`:
```python
def enter_srv_callback(self, request, response):
self.get_logger().info('\033[1;32m%s\033[0m' % "line following enter")
with self.lock:
self.color = ''
self.stop = False
self.is_running = False
self.color_picker = None
self.pid = pid.PID(1.1, 0.0, 0.0)
self.follower = None
self.threshold = 0.5
self.empty = 0
if self.image_sub is None:
if 'ROSOrin' in self.machine_type:
self.camera_type = 'Stereo'
self.image_sub = self.create_subscription(Image, '/depth_cam/rgb0/image_raw', self.image_callback, 1) # Subscribe to the camera
else:
self.camera_type = 'Mono'
self.image_sub = self.create_subscription(Image, '/usb_cam/image_raw', self.image_callback, 1) # Subscribe to the camera
if self.lidar_sub is None:
qos = QoSProfile(depth=1, reliability=QoSReliabilityPolicy.BEST_EFFORT)
self.lidar_sub = self.create_subscription(LaserScan, '/scan_raw', self.lidar_callback, qos) # Subscribe to Lidar data
```
Starts autonomous patrolling, initializes robot motion parameters, and subscribes to camera and LiDAR topics.
`exit_srv_callback`:
```python
def exit_srv_callback(self, request, response):
self.get_logger().info('\033[1;32m%s\033[0m' % "line following exit")
try:
if self.image_sub is not None:
self.destroy_subscription(self.image_sub)
self.image_sub = None
if self.lidar_sub is not None:
self.destroy_subscription(self.lidar_sub)
self.lidar_sub = None
except Exception as e:
self.get_logger().error(str(e))
with self.lock:
self.is_running = False
self.color_picker = None
self.pid = pid.PID(0.01, 0.0, 0.0)
self.follower = None
self.threshold = 0.5
self.mecanum_pub.publish(Twist())
response.success = True
response.message = "exit"
return response
```
Exits the autonomous patrolling service, shuts down all active subscriptions, resets the PID controller, and stops line following.
`set_target_color_srv_callback`:
```python
def set_target_color_srv_callback(self, request, response):
self.get_logger().info('\033[1;32m%s\033[0m' % "set_target_color")
with self.lock:
self.use_color_picker = True
x, y = request.data.x, request.data.y
self.get_logger().info(f'{request.data.x},{request.data.y}')
# request.data.x = request.data.x
# request.data.y =
self.follower = None
if x == -1 and y == -1:
self.color_picker = None
else:
self.color_picker = ColorPicker(request.data, 5)
self.mecanum_pub.publish(Twist())
response.success = True
response.message = "set_target_color"
return response
```
Service to set the target line-following color.
`get_target_color_srv_callback`:
```python
def get_target_color_srv_callback(self, request, response):
self.get_logger().info('\033[1;32m%s\033[0m' % "get_target_color")
response.success = False
response.message = "get_target_color"
with self.lock:
if self.follower is not None:
response.success = True
rgb = self.follower.target_rgb
response.message = "{},{},{}".format(int(rgb[0]), int(rgb[1]), int(rgb[2]))
return response
```
Service to get the target line-following color.
`set_running_srv_callback`:
```python
def set_running_srv_callback(self, request, response):
self.get_logger().info('\033[1;32m%s\033[0m' % "set_running")
with self.lock:
self.is_running = request.data
self.empty = 0
if not self.is_running:
self.mecanum_pub.publish(Twist())
response.success = True
response.message = "set_running"
return response
```
Service to start the autonomous patrolling feature.
`set_threshold_srv_callback`:
```python
def set_threshold_srv_callback(self, request, response):
self.get_logger().info('\033[1;32m%s\033[0m' % "set threshold")
with self.lock:
self.threshold = request.data
response.success = True
response.message = "set_threshold"
return response
```
Service to set the color threshold.
`lidar_callback`:
```python
def lidar_callback(self, lidar_data):
# Data size= scanning angle/ the increased angle per scan
if self.lidar_type != 'G4':
min_index = int(math.radians(MAX_SCAN_ANGLE / 2.0) / lidar_data.angle_increment)
max_index = int(math.radians(MAX_SCAN_ANGLE / 2.0) / lidar_data.angle_increment)
left_ranges = lidar_data.ranges[:max_index] # Left data
right_ranges = lidar_data.ranges[::-1][:max_index] # Right data
elif self.lidar_type == 'G4':
min_index = int(math.radians((360 - MAX_SCAN_ANGLE) / 2.0) / lidar_data.angle_increment)
max_index = int(math.radians(180) / lidar_data.angle_increment)
left_ranges = lidar_data.ranges[min_index:max_index][::-1] # The left data
right_ranges = lidar_data.ranges[::-1][min_index:max_index][::-1] # The right data
# Get data according to settings
angle = self.scan_angle / 2
angle_index = int(angle / lidar_data.angle_increment + 0.50)
left_range, right_range = np.array(left_ranges[:angle_index]), np.array(right_ranges[:angle_index])
left_nonzero = left_range.nonzero()
right_nonzero = right_range.nonzero()
left_nonan = np.isfinite(left_range[left_nonzero])
right_nonan = np.isfinite(right_range[right_nonzero])
# Take the nearest distance left and right
min_dist_left_ = left_range[left_nonzero][left_nonan]
min_dist_right_ = right_range[right_nonzero][right_nonan]
if len(min_dist_left_) > 0 and len(min_dist_right_) > 0:
min_dist_left = min_dist_left_.min()
min_dist_right = min_dist_right_.min()
if min_dist_left < self.stop_threshold or min_dist_right < self.stop_threshold:
self.stop = True
else:
self.count += 1
if self.count > 5:
self.count = 0
self.stop = False
```
The Lidar callback function processes the received data and determines whether to stop moving.
`image_callback`:
```python
def image_callback(self, ros_image):
cv_image = self.bridge.imgmsg_to_cv2(ros_image, "rgb8")
rgb_image = np.array(cv_image, dtype=np.uint8)
rgb_image = cv2.resize(rgb_image, (640,480))
self.image_height, self.image_width = rgb_image.shape[:2]
result_image = np.copy(rgb_image) # The image used to display the result
with self.lock:
if self.use_color_picker:
# Color picker and line recognition are exclusive. If there is color picker, start picking
if self.color_picker is not None: # Color picker exists
try:
target_color, result_image = self.color_picker(rgb_image, result_image)
if target_color is not None:
self.color_picker = None
self.follower = LineFollower(target_color, self)
self.get_logger().info("target color: {}".format(target_color))
except Exception as e:
self.get_logger().error(str(e))
else:
twist = Twist()
twist.linear.x = 0.15
if self.follower is not None:
try:
result_image, deflection_angle = self.follower(rgb_image, result_image, self.threshold)
if deflection_angle is not None and self.is_running and not self.stop:
self.pid.update(deflection_angle)
if 'Acker' in self.machine_type:
steering_angle = common.set_range(-self.pid.output, -math.radians(322/2000*180), math.radians(322/2000*180))
if steering_angle != 0:
R = 0.17706/math.tan(steering_angle)
twist.angular.z = twist.linear.x/R
else:
twist.angular.z = common.set_range(-self.pid.output, -1.0, 1.0)
self.mecanum_pub.publish(twist)
elif self.stop:
self.mecanum_pub.publish(Twist())
else:
self.pid.clear()
except Exception as e:
self.get_logger().error(str(e))
else:
twist = Twist()
if self.color in common.range_rgb:
twist.linear.x = 0.15
self.follower = LineFollower([None, common.range_rgb[self.color]], self)
result_image, deflection_angle = self.follower(rgb_image, result_image, self.threshold, self.lab_data['lab'][self.camera_type][self.color], False)
if deflection_angle is not None and self.is_running and not self.stop:
self.pid.update(deflection_angle)
if 'Acker' in self.machine_type:
steering_angle = common.set_range(-self.pid.output, -math.radians(322/2000*180), math.radians(322/2000*180))
if steering_angle != 0:
R = 0.17706/math.tan(steering_angle)
twist.angular.z = twist.linear.x/R
else:
twist.angular.z = common.set_range(-self.pid.output, -1.0, 1.0)
self.mecanum_pub.publish(twist)
elif self.stop:
self.mecanum_pub.publish(Twist())
else:
self.pid.clear()
else:
self.mecanum_pub.publish(twist)
if self.debug:
if self.image_queue.full():
# If the queue is full, discard the oldest image
self.image_queue.get()
```
The camera callback function uses the received data to invoke the color picker and controls the robot’s movement along the recognized line using PID.