7. AI Visual Transporting & Kicking Ball Course
7.1 Locate the Ball
7.1.1 Program logic
Firstly, recognize the color of the ball. Firstly, convert the color space of the image from RGB into Lab. Then perform binaryzation, corrosion, dilation, etc., to obtain the contour only containing the target color and circle it.
Then use the robot body as a reference to establish a coordinate system, and then obtain the coordinate of the ball. Judge whether the ball is on the right or left through the coordinate of the x axis, and then control the corresponding foot to lift
7.1.2 Operation steps
The input command should be case sensitive and space sensitive.
(1) Boot up SpiderPi Pro, then remotely connect to Raspberry Pi desktop through VNC.
(2) Click
at upper left corner of desktop to open the Terminator.
(3) Enter the command and press “Enter” to navigate to the directory where the game program is located.
cd spiderpi/advanced
(4) Input the command, and then press Enter to start the game.
python3 ball_orientation.py
(5) If want to close this game, press “Ctrl+C” on LX terminal. If the game cannot be quit, please try again.
7.1.3 Project outcome
The default recognition color is green. If you want to modify it as other color, please check how to modify in “7.1.5 Function Extension”
When the green ball is recognized, the green ball will be circled and its coordinate will be displayed on the terminal. And then control the robot to lift the corresponding leg.
7.1.4 Program Analysis
The source code of this program lies in: /home/pi/spiderpi/advanced/ball_orientation.py
Import Function Library
4import sys
5import cv2
6import math
7import time
8import threading
9import numpy as np
10from calibration.camera import Camera
11from calibration.CalibrationConfig import *
12from common import misc
13from common import yaml_handle
14from common import kinematics
15from common.ros_robot_controller_sdk import Board
16import arm_ik.arm_move_ik as AMK
(1) Import the libraries related to OpenCV, time, math, and threads.
If want to call a function in library, you can use “library name+function name (parameter, parameter)”. For example:
139 time.sleep(2)
Call sleep function in “time” library. The function sleep () is used to delay. There are some built-in libraries in Python, so they can be called directly. For example, time, cv2 and math. You can also write a new library like yaml_handle.
(2) Instantiate Function Library
The name of function library is too long to memorize. For calling function easily, the library can be instantiated. For example:
13from common import yaml_handle
14from common import kinematics
15from common.ros_robot_controller_sdk import Board
16import arm_ik.arm_move_ik as AMK
After instantiating, you can directly input and call the function “Board.function name (parameter, parameter)”.
Main Function Analysis
The python program __name__ == '__main__:' is the main function of program. Firstly, the function init() is called to initialize. The initialization in this program includes: return the servo to the initial position, read the color threshold file. Generally there are also configurations for ports, peripherals, timing interrupts, etc., which are all done in the process of initialization.
214if __name__ == '__main__':
215 from sensor.ultrasonic_sensor import Ultrasonic
216 ultrasonic = Ultrasonic()
217
218 #加载参数(load parameter)
219 param_data = np.load(calibration_param_path + '.npz')
220
221 #获取参数(obtain parameter)
222 mtx = param_data['mtx_array']
223 dist = param_data['dist_array']
224 newcameramtx, _ = cv2.getOptimalNewCameraMatrix(mtx, dist, (640, 480), 0, (640, 480))
225 mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (640, 480), 5)
226
227 init_move()
228 load_config()
229 camera = Camera()
230 camera.camera_open()
Define Global Variables
34current_pos = [[-199.53, -177.73, -100.0],
35 [ -35.0, -211.27, -100.0],
36 [199.53, -177.73, -100.0],
37 [199.53, 177.73, -100.0],
38 [ -35.0, 211.27, -100.0],
39 [-199.53, 177.73, -100.0]]
40
41# 左右两只前脚与摄像头坐标零点对应的参数(The parameters of the left and right front legs corresponding to the coordinates zero point of the camera)
42left_pos = [-220, -0, -80]
43right_pos = [-220, 0, -80]
44
45
46range_rgb = {
47 'red': (0, 0, 255),
48 'blue': (255, 0, 0),
49 'green': (0, 255, 0),
50 'black': (0, 0, 0),
51 'white': (255, 255, 255)}
52
53# 变量定义(define variables)
54i = 0
55ultrasonic = None
56step = False
57size = (640, 480)
58old_x, old_y = 0, 0
59initial_coord = (0, 18, 10)
60world_x, world_y = -1, -1
61lab_data = None
62K,R,T = None,None,None
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.
167 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 variance around the average in Gaussian filtering. The larger the value, the larger the allowable variance around the average value; The smaller the value, the smaller the allowable variance around the average value.
(2) Binaryzation processing
Adopt inRange() function in cv2 library to perform binaryzation on the image.
169 frame_mask = cv2.inRange(frame_lab,
170 (lab_data[color]['min'][0],
171 lab_data[color]['min'][1],
172 lab_data[color]['min'][2]),
173 (lab_data[color]['max'][0],
174 lab_data[color]['max'][1],
175 lab_data[color]['max'][2])) #对原图像和掩模进行位运算(perform bitwise operation to original image and mask)
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 a value of 1, otherwise, 0.
(3) Corrosion and dilation
To reduce the interference and make the image smoother, it is necessary to perform corrosion and dilation on the image.
176 eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #腐蚀(erode)
177 dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #膨胀(dilate)
erode() function is used for corrosion. Take eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) for example. The meaning of the parameters in bracket are as follow.
The first parameter frame_mask is the input image The second parameter cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) is the structural element and kernel deciding the nature of the operation. And the first parameter in the parenthesis is the kernel shape and the second parameter is the kernel dimension.
dilate() function is used for image dilation. And the meaning of the parameters in parenthesis is the same as that of erode() function.
(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.
178 contours = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2] #找出轮廓(find contours)
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 larger than this value, the target contour is valid.
87def get_area_maxContour(contours):
88 contour_area_temp = 0
89 contour_area_max = 0
90 area_max_contour = None
91 max_area = 0
92
93 for c in contours: # 历遍所有轮廓(traverse through all contours)
94 contour_area_temp = math.fabs(cv2.contourArea(c)) # 计算轮廓面积(calculate contour area)
95 if contour_area_temp > contour_area_max:
96 contour_area_max = contour_area_temp
97 if contour_area_temp >= 100: # 只有在面积大于设定时,最大面积的轮廓才是有效的,以过滤干扰(Only when the area is greater than the set value, the contour with the maximum area is considered valid to filter out interference)
98 area_max_contour = c
99 max_area = contour_area_temp
100
101 return area_max_contour, max_area # 返回最大的轮廓, 面积(Return the largest contour and area)
Feedback
According to the coordinate of the X axis, decide which leg of the robot to lift.
129def move():
130 global step
131 global world_x, world_y
132
133 while True:
134 if step:
135 if world_x > 0: # 目标在右边(the target is on the right)
136 # 抬起右前脚(lift the right front leg)
137 current_pos[5] = [-300, 100, -50]
138 ik.stand(current_pos)
139 time.sleep(2)
140
141 else: # 目标在左边(the target is on the left)
142 # 抬起左前脚(lift the left front leg)
143 current_pos[0] = [-300, -100, -50]
144 ik.stand(current_pos)
145 time.sleep(2)
146
147 else:
148 # 回到初始姿态(return to the initial status)
149 current_pos[0] = [-199.53, -177.73, -100]
150 ik.stand(current_pos)
151 current_pos[5] = [-199.53, 177.73, -100]
152 ik.stand(current_pos)
153 time.sleep(1)
current_pos[5] = [-300, 100, -50] and current_pos[0] = [-300, -100, -50] are used to control the movement of the leg.
Take current_pos[5] = [-300, 100, -50] for example.
The first parameter 5 is used to control the position of the leg. Robot legs is numbered counterclockwise.
The second parameter [-300, 100, -50] is x, y, z coordinate of the end of the leg.
7.1.5 Function extension
The program defaults to recognize green ball and display its coordinate. If you want to modify the recognition color, like red, you can follow the below steps to operate.
(1) Input the command and press “Enter” to get to the catalog where the game programs are stored.
cd spiderpi/advanced
(2) Input command and press “Enter” to open the program file.
sudo vim ball_orientation.py
(3) Locate these codes.
(4) Press “i” key to enter the editing mode.
(5) Modify “green” of color='green' as “red”, as shown below.
(6) After successful modification, press “Esc” and then input “:wq” to save the file and exit the editor.
(7) Execute the steps in “7.1.2 Operation Steps” to locate the red ball.
7.2 Kick the Ball
In the experiment, a small “ball” is used for demonstration. A color block can also be used to achieve intelligent kicking of the block.
7.2.1 Program Logic
Firstly, program SpiderPi Pro to recognize the color. Convert RGB color space into Lab. Then perform binaryzation, corrosion, dilation, etc., to obtain the contour only containing the target color and circle it.
The robot will perform color recognition till the coordinate of the “ball” is obtained. Then it will approach the ball. Next, according to the X axis coordinate of the ball, control the front leg which is closer to the ball to kick.
7.2.2 Operation Steps
Note
The input command should be case sensitive and space sensitive.
(1) Boot up SpiderPi Pro, then remotely connect to Raspberry Pi desktop through VNC.
(2) Click
at upper left corner of desktop to open the Terminator.
(3) Enter the command and press “Enter” to navigate to the directory where the game program is located.
cd spiderpi/advanced
(4) Input the command and then press “Enter” to start the game.
python3 intelligent_kick.py
(5) If want to close this game, press “Ctrl+C” on LX terminal. If the game cannot be quit, please try again.
7.2.3 Project Outcome
Note
The default recognition color is green.
After the green ball is recognized, based on the position of the “ball”, SpiderPi Pro will use the front leg closer to the ball to kick the ball. Besides, the ball will be circled and the color of the “ball” will be printed on the camera returned image.
7.2.4 Program Analysis
The source codes of this program are stored in: /home/pi/spiderpi/advanced/intelligent_kick.py
Import Function Library
4import sys
5import cv2
6import math
7import numpy as np
8import time
9import threading
10from calibration.camera import Camera
11from calibration.CalibrationConfig import *
12from common import misc
13from common.pid import PID
14from common import yaml_handle
15from common import kinematics
16from common.ros_robot_controller_sdk import Board
17from sensor.ultrasonic_sensor import Ultrasonic
18import arm_ik.arm_move_ik as AMK
(1) Import the libraries related to OpenCV, time, math, and threads. If want to call a function in library, you can use “library name+function name (parameter, parameter)”. For example:
251 time.sleep(0.05)
Call sleep function in time library. The function sleep () is used to delay. There are some built-in libraries in Python, so they can be called directly. For example, time, cv2 and math. You can also write a new library like yaml_handle.
(2) Instantiate Function Library
The name of function library is too long to memorize. For calling function easily, the library can be instantiated. For example:
12from common import misc
13from common.pid import PID
14from common import yaml_handle
15from common import kinematics
16from common.ros_robot_controller_sdk import Board
After instantiating, you can directly input and call the function Board.function name (parameter, parameter).
Main Function Analysis
The python program __name__ == '__main__:' is the main function of program. Firstly, the function init() is called to initialize. The initialization in this program includes: return the servo to the initial position, read the color threshold file. Generally there are also configurations for ports, peripherals, timing interrupts, etc., which are all done in the process of initialization.
338if __name__ == '__main__':
339 from sensor.ultrasonic_sensor import Ultrasonic
340
341 ultrasonic = Ultrasonic()
342
343 #加载参数(load parameter)
344 param_data = np.load(calibration_param_path + '.npz')
345
346 #获取参数(obtain parameter)
347 mtx = param_data['mtx_array']
348 dist = param_data['dist_array']
349 newcameramtx, _ = cv2.getOptimalNewCameraMatrix(mtx, dist, (640, 480), 0, (640, 480))
350 mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (640, 480), 5)
351
352 init()
353 camera = Camera()
354 camera.camera_open()
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.
273 frame_gb = cv2.GaussianBlur(frame_resize, (5, 5), 5)
The meaning of the parameters in bracket is as follow
The first parameter frame_resize is the input image.
The second parameter (5, 5) is the size of Gaussian kernel.
The third parameter 5 is the allowable variance around the average in Gaussian filtering. The larger the value, the larger the allowable variance around the average value; The smaller the value, the smaller the allowable variance around the average value.
(2) Binaryzation processing
Adopt inRange() function in cv2 library to perform binaryzation on the image.
281 frame_mask = cv2.inRange(frame_lab,
282 (lab_data[i]['min'][0],
283 lab_data[i]['min'][1],
284 lab_data[i]['min'][2]),
285 (lab_data[i]['max'][0],
286 lab_data[i]['max'][1],
287 lab_data[i]['max'][2])) #对原图像和掩模进行位运算(perform bitwise operation to original image and mask)
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 a value of 1, otherwise, 0.
(3) Corrosion and dilation
To reduce the interference and make the image smoother, it is necessary to perform corrosion and dilation on the image.
288 eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #腐蚀(erode)
289 dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #膨胀(dilate)
erode() function is used for corrosion. Take eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) for example. The meaning of the parameters in bracket are as follow.
The first parameter frame_mask is the input image
The second parameter cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) is the structural element and kernel deciding the nature of the operation. And the first parameter in the parenthesis is the kernel shape and the second parameter is the kernel dimension.
dilate() function is used for image dilation. And the meaning of the parameters in parenthesis is the same as that of erode() function.
(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.
292 contours = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2] # 找出轮廓(find contours)
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 larger than this value, the target contour is valid.
66def get_area_maxContour(contours):
67 contour_area_temp = 0
68 contour_area_max = 0
69
70 area_max_contour = None
71 max_area = 0
72
73 for c in contours: # 历遍所有轮廓(traverse through all contours)
74 contour_area_temp = math.fabs(cv2.contourArea(c)) # 计算轮廓面积(calculate contour area)
75 if contour_area_temp > contour_area_max:
76 contour_area_max = contour_area_temp
77 if contour_area_temp > 50: # 只有在面积大于设定时,最大面积的轮廓才是有效的,以过滤干扰(Only when the area is greater than the set value, the contour with the maximum area is considered valid to filter out interference)
78 area_max_contour = c
79 max_area = contour_area_temp
80
81 return area_max_contour, max_area # 返回最大的轮廓(return the largest contour)
After the contour with the largest area is acquired, call minEnclosingCircle() and drawContours() functions in cv2 library to acquire the smallest circumscribed circle of the target contour and mark it.
295 if area_max > 50: # 有找到最大面积(the maximum area has been found)
296 (centerX, centerY), radius = cv2.minEnclosingCircle(areaMaxContour) #获取最小外接圆(obtain the minimum circumscribed circle)
297 centerX = int(misc.map(centerX, 0, size[0], 0, img_w))
298 centerY = int(misc.map(centerY, 0, size[1], 0, img_h))
299 radius = int(misc.map(radius, 0, size[0], 0, img_w))
300 cv2.circle(img, (int(centerX), int(centerY)), int(radius), range_rgb[detect_color], 2)
Acquire the coordinate
After the coordinate is confirmed, convert the coordinate into actual distance through calculation.
159 if abs(dx) > 5 or dy > 20 or dy < -10:
160 if dy < -10:
161 direction = 0
162 elif dy > 20 and abs(dx) < 15:
163 direction = 180
164 dx = -dx
165
166 if abs(dy) > 50:
167 amplitude = int(abs(dy)-abs(dx)*2)
168
169 elif abs(dy) < 20:
170 amplitude = int(abs(dy) + abs(dx)*1.5)
171
172 else:
173 amplitude = abs(dy)
174 if dx > 5:
175 ago_direction = 'right'
176 elif dx < -5:
177 ago_direction = 'left'
178 ik.setStepMode(ik.initial_pos, 2, 2, amplitude*1.5, dz, direction, 0, dx/10, p, o, 50, 1)
179 move_st = True
Kick the ball
Firstly, control the robot to approach the ball.
177 ago_direction = 'left'
178 ik.setStepMode(ik.initial_pos, 2, 2, amplitude*1.5, dz, direction, 0, dx/10, p, o, 50, 1)
179 move_st = True
180
181 else:
182 ik.setStepMode(ik.initial_pos, 2, 2, 70, 20, 0, 0, 0, p, o, 50, 2) # 朝前直走(go straight forward)
Judge the ball is closer to which front leg according to the X axis coordinate of the ball. And control the corresponding front leg to kick the ball.
182 ik.setStepMode(ik.initial_pos, 2, 2, 70, 20, 0, 0, 0, p, o, 50, 2) # 朝前直走(go straight forward)
183 ik.stand(ik.initial_pos, t=500)
184 if x_dis <= 510:
185 ik.stand(current_pos)
186 time.sleep(0.5)
187 # 抬起右前脚(lift right front leg)
188 current_pos[5] = [-199.53, 177.73, -60]
189 ik.stand(current_pos)
190 time.sleep(0.2)
191 # 右脚踢球(kick the ball with right leg)
192 current_pos[5] = [-340, -30, -80]
193 ik.stand(current_pos,200)
194 time.sleep(0.5)
195 # 回到初始姿态(return to initial position)
196 current_pos[5] = [-199.53, 177.73, -100]
197 ik.stand(current_pos)
198 ik.stand(ik.initial_pos, t=500)
199 else:
200 ik.stand(current_pos)
201 time.sleep(0.5)
202 # 抬起左前脚(lift left front leg)
203 current_pos[0] = [-199.53, 177.73, -60]
204 ik.stand(current_pos)
205 time.sleep(0.2)
206 # 左脚踢球(kick the ball with left leg)
207 current_pos[0] = [-340, 30, -80]
208 ik.stand(current_pos,200)
209 time.sleep(0.5)
210 # 回到初始姿态(return to initial position)
211 current_pos[0] = [-199.53, -177.73, -100]
212 ik.stand(current_pos)
213 ik.stand(ik.initial_pos, t=500)
The attitude data of the SpiderPi Pro is defined as a 3×6 array, and the array elements represent the X, Y, and Z coordinates of the ends of the six legs (unit is mm). The left front leg is the first leg, and the leg is sequenced counterclockwise, hence the right front leg is the sixth leg.
57current_pos = [[-199.53, -177.73, -100.0],
58 [ -35.0, -211.27, -100.0],
59 [199.53, -177.73, -100.0],
60 [199.53, 177.73, -100.0],
61 [ -35.0, 211.27, -100.0],
62 [-199.53, 177.73, -100.0]]
Modify the coordinate parameter of the corresponding leg, and call stand() function in kinematics.IK library to update the data, which can change the posture of SpiderPi Pro.
182 ik.setStepMode(ik.initial_pos, 2, 2, 70, 20, 0, 0, 0, p, o, 50, 2) # 朝前直走(go straight forward)
183 ik.stand(ik.initial_pos, t=500)
184 if x_dis <= 510:
185 ik.stand(current_pos)
186 time.sleep(0.5)
187 # 抬起右前脚(lift right front leg)
188 current_pos[5] = [-199.53, 177.73, -60]
189 ik.stand(current_pos)
190 time.sleep(0.2)
191 # 右脚踢球(kick the ball with right leg)
192 current_pos[5] = [-340, -30, -80]
193 ik.stand(current_pos,200)
194 time.sleep(0.5)
195 # 回到初始姿态(return to initial position)
196 current_pos[5] = [-199.53, 177.73, -100]
197 ik.stand(current_pos)
198 ik.stand(ik.initial_pos, t=500)
199 else:
200 ik.stand(current_pos)
201 time.sleep(0.5)
202 # 抬起左前脚(lift left front leg)
203 current_pos[0] = [-199.53, 177.73, -60]
204 ik.stand(current_pos)
205 time.sleep(0.2)
206 # 左脚踢球(kick the ball with left leg)
207 current_pos[0] = [-340, 30, -80]
208 ik.stand(current_pos,200)
209 time.sleep(0.5)
210 # 回到初始姿态(return to initial position)
211 current_pos[0] = [-199.53, -177.73, -100]
212 ik.stand(current_pos)
213 ik.stand(ik.initial_pos, t=500)
7.3 Position Calibration
7.3.1 Preface
Position calibration is to adjust where the SpiderPi Pro picks the object, which aims at accurate picking.
Before the game starts, you need to paste the included ID1 sticker on one block.
The overall implementation process of this lesson is as follows:
It’s necessary to import the required libraries and modules, load the calibration parameters of the camera (matrix mtx and distortion coefficient dist), and calculate the new camera matrix newcameramtx. Then, detect AprilTag markers through positioning, image segmentation, and contour finding. After contour positioning, detect quadrilaterals and fit lines to form a closed loop by obtaining the four corner points. Next, update the status, calculate the world coordinates, update the image, and perform calibration.
After modification, SpiderPi Pro can pick the target object accurately.
7.3.2 Operation steps
The input command should be case sensitive and space sensitive.
(1) Boot up SpiderPi Pro, then remotely connect to Raspberry Pi desktop through VNC.
(2) Click
at upper left corner of desktop, or press “Ctrl+Alt+T” to open LX terminal.
(3) Enter the command and press “Enter” to navigate to the directory where the game program is located.
cd spiderpi/functions
(4) Input command and press “Enter” to run calibration program.
python3 camera_cal_main.py
(5) Place the block with ID1 sticker right under the gripper.
(6) If you want to exit the game, press “Ctrl+C” to close the LX terminal interface. If it cannot be closed, you can try again.
(7) When you hear two beeps, start calibrating the position.
(8) 15s later, the robot will beep three times continuously. When the 5 black dots overlap the 5 colored dots, it means that the calibration is completed.
Note
If it beeps three times continuously but 5 black dots don’t coincide with the 5 colored dots, the calibration doesn’t work. And you can adjust the lighting again and then start the program of position calibration.
(9) Having calibrated, press “Ctrl+C” to close the LX terminal interface. If it cannot be closed, you can try again.
(10) Enter the command “cd ..” and press “Enter” to switch to the parent directory; enter the “cd advand/” commandand press “Enter” to get into the catalog of intelligent picking program; and place the red block under the gripper and input the command “python3 intelligent_fetch.py”, then press “Enter” to start the intelligent picking game.
cd ..
cd advanced/
python3 intelligent_fetch.py
(11) If the gripper is not at the accurate position during picking, as the picture shown, you can run the calibration program to make fine adjustment again.
(12) Enter commands “cd ..”, “cd functions”, and “python3 camera_cal_ main.py” in turn to run the calibration program.
cd ..
cd functions
python3 camera_cal_ main.py
(13) Place the tag block according to the picking effect in the game. For example, if the gripper stops in front of the red block not at the middle, place the tag block under the gripper not at the position of the red block.
(14) Having calibrated, run the intelligent picking program in the same way to check the calibration effect. You can see that the gripper can stop at the middle of the block and pick the block.
7.3.3 Program Analysis
The source code of the program is located in: /home/pi/spiderpi/functions/camera_cal_main.py
1#!/usr/bin/python3
2# coding=utf8
3import os
4import cv2
5import yaml
6import time
7import threading
8import numpy as np
9from common import apriltag
10from calibration.camera import Camera
11from calibration.CalibrationConfig import *
12from common import kinematics
13from common.ros_robot_controller_sdk import Board
14import arm_ik.arm_move_ik as AMK
15
16#加载参数(load parameters)
17param_data = np.load(calibration_param_path + '.npz')
18#获取参数(obtain parameters)
19mtx = param_data['mtx_array']
20dist = param_data['dist_array']
21newcameramtx, _ = cv2.getOptimalNewCameraMatrix(mtx, dist, (640, 480), 0, (640, 480))
22
23
24board = Board()
25ik = kinematics.IK(board)
26ak = AMK.ArmIK()
27
28
29at_detector = apriltag.Detector(apriltag.DetectorOptions(families='tag36h11'))
30marker_corners = np.asarray([[0, 0, 40],
31 [16.65, -16.65, 40], # TAG_SIZE = 33.30mm
32 [-16.65, -16.65, 40],
33 [-16.65, 16.65, 40],
34 [16.65, 16.65, 40]],
35 dtype=np.float64)
36marker_corners_block = np.copy(marker_corners)
37marker_corners_block[:, 2] = marker_corners_block[:, 2] - 40
Import Function Library
3import os
4import cv2
5import yaml
6import time
7import threading
8import numpy as np
9from common import apriltag
10from calibration.camera import Camera
11from calibration.CalibrationConfig import *
12from common import kinematics
13from common.ros_robot_controller_sdk import Board
14import arm_ik.arm_move_ik as AMK
(1) Import the libraries related to OpenCV, time, math, threads, and inverse kinematics. If want to call a function in library, you can use “library name+function name (parameter, parameter)”. For example:
174 time.sleep(3)
Call sleep function in time library. The function sleep () is used to delay.
There are some built-in libraries in Python, therefore they can be called directly. For example, time, cv2 and math. You can also write a new library like yaml_handle.
(2) Instantiate Function Library
The name of function library is too long to memorize. For calling function easily, the library can be instantiated. For example:
13from common.ros_robot_controller_sdk import Board
After instantiating, you can directly input and call the function Board.function name (parameter, parameter).
Main Function Analysis
The python program __name__ == '__main__:' is the main function of program. Firstly, the function init() is called to initialize. The initialization in this program includes: return the servo to the initial position, read the color threshold file. Generally there are also configurations for ports, peripherals, timing interrupts, etc., which are all done in the process of initialization.
189if __name__ == '__main__':
190
191 #mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (640, 480), 5)
192
193 debug = False
194 if debug:
195 print('Debug Mode')
196
197 initMove()
198 state = State()
199 camera = Camera()
200 camera.camera_open()
201 while True:
202 img = camera.frame
203 if img is not None:
204 frame = img.copy()
205 #frame = cv2.remap(frame, mapx, mapy, cv2.INTER_LINEAR) # 畸变矫正(distortion correction)
206 Frame = run(frame)
207 cv2.imshow('Frame', Frame)
208 key = cv2.waitKey(1)
209 if key == 27:
210 break
211 else:
212 time.sleep(0.01)
213 camera.camera_close()
214 cv2.destroyAllWindows()
(1) Initialize posture and set servo position
56def initMove():
57 ik.stand(ik.initial_pos)
58 board.bus_servo_set_position(1, [[25, 650]])
59 ak.setPitchRangeMoving((0, 15, 5), -90, -90, 100, 2)
(2) Convert image data to actual physical space coordinates
62def camera_to_world(cam_mtx, r, t, img_points):
63 inv_k = np.asmatrix(cam_mtx).I
64 r_mat = np.zeros((3, 3), dtype=np.float64)
65 cv2.Rodrigues(r, r_mat)
66 # invR * T
67 inv_r = np.asmatrix(r_mat).I # 3*3
68 transPlaneToCam = np.dot(inv_r, np.asmatrix(t)) # 3*3 dot 3*1 = 3*1
69 world_pt = []
70 coords = np.zeros((3, 1), dtype=np.float64)
71 for img_pt in img_points:
72 coords[0][0] = img_pt[0][0]
73 coords[1][0] = img_pt[0][1]
74 coords[2][0] = 1.0
75 worldPtCam = np.dot(inv_k, coords) # 3*3 dot 3*1 = 3*1
76 # [x,y,1] * invR
77 worldPtPlane = np.dot(inv_r, worldPtCam) # 3*3 dot 3*1 = 3*1
78 # zc
79 scale = transPlaneToCam[2][0] / worldPtPlane[2][0]
80 # zc * [x,y,1] * invR
81 scale_worldPtPlane = np.multiply(scale, worldPtPlane)
82 # [X,Y,Z]=zc*[x,y,1]*invR - invR*T
83 worldPtPlaneReproject = np.asmatrix(scale_worldPtPlane) - np.asmatrix(transPlaneToCam) # 3*1 dot 1*3 = 3*3
84 pt = np.zeros((3, 1), dtype=np.float64)
85 pt[0][0] = worldPtPlaneReproject[0][0]
86 pt[1][0] = worldPtPlaneReproject[1][0]
87 pt[2][0] = 0
88 world_pt.append(pt.T.tolist())
89 return world_pt
(3) Input image processing tag
92def run(img):
93 frame_gray = cv2.cvtColor(np.copy(img), cv2.COLOR_RGB2GRAY)
94 tags = at_detector.detect(frame_gray)
95 for tag in tags:
96 center = np.array(tag.center).astype(int)
97 if tag.tag_id == 1:
98 corners = tag.corners.reshape(1, -1, 2).astype(np.float32)
99 pts = np.insert(corners[0], 0, values=tag.center, axis=0)
100 with state.lock:
101 rtl, new_r, new_t = cv2.solvePnP(marker_corners, pts, state.K, None)
102 if rtl:
103 state.R = new_r if state.R is None else state.R * 0.95 + new_r * 0.05
104 state.T = new_t if state.T is None else state.T * 0.95 + new_t * 0.05
105 rtl, new_r, new_t = cv2.solvePnP(marker_corners_block, pts, state.K, None)
106 if rtl:
107 state.R_40 = new_r if state.R_40 is None else state.R_40 * 0.95 + new_r * 0.05
108 state.T_40 = new_t if state.T_40 is None else state.T_40 * 0.95 + new_t * 0.05
109 if state.R is not None:
110 img_pts, jac = cv2.projectPoints(marker_corners, state.R, state.T, state.K, None)
111 else:
112 img_pts = []
113 corners = corners.astype(int)
114
115 w = camera_to_world(state.K, state.R_40, state.T_40,
116 center.reshape((1, 1, 2)))[0][0]
117 print(center.reshape((1, 1, 2)))
118 cv2.circle(img, tuple(corners[0][0]), 5, (255, 0, 0), 12)
119 cv2.circle(img, tuple(corners[0][1]), 5, (0, 255, 0), 12)
120 cv2.circle(img, tuple(corners[0][2]), 5, (0, 0, 255), 12)
121 cv2.circle(img, tuple(corners[0][3]), 5, (0, 255, 255), 12)
122 cv2.circle(img, tuple(center), 5, (255, 255, 0), 12)
123 cv2.putText(img, "id:%d" % tag.tag_id,
124 (center[0], center[1] - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
125 for i, c in enumerate(img_pts):
126 cv2.circle(img, tuple(c.astype(int)[0]), 3, (0, 0, 0), 4)
127 else:
128 with state.lock:
129 if state.R_40 is not None:
130
131 w = camera_to_world(state.K, state.R_40, state.T_40,
132 center.reshape((1, 1, 2)))[0][0]
133 else:
134 w = 0, 0, 0
135 cv2.putText(img, "id:{} x:{:.2f} y:{:.2f}".format(tag.tag_id, w[0], w[1]),
136 (center[0] - 20, center[1] - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)
137 return img
(4) Grayscale Transformation
Convert an input color image to grayscale image using cv2.cvtColor.
93 frame_gray = cv2.cvtColor(np.copy(img), cv2.COLOR_RGB2GRAY)
(5) Tag Detection:
94 tags = at_detector.detect(frame_gray)
(6) Tag Processing:
97 if tag.tag_id == 1:
98 corners = tag.corners.reshape(1, -1, 2).astype(np.float32)
99 pts = np.insert(corners[0], 0, values=tag.center, axis=0)
100 with state.lock:
101 rtl, new_r, new_t = cv2.solvePnP(marker_corners, pts, state.K, None)
102 if rtl:
103 state.R = new_r if state.R is None else state.R * 0.95 + new_r * 0.05
104 state.T = new_t if state.T is None else state.T * 0.95 + new_t * 0.05
105 rtl, new_r, new_t = cv2.solvePnP(marker_corners_block, pts, state.K, None)
106 if rtl:
107 state.R_40 = new_r if state.R_40 is None else state.R_40 * 0.95 + new_r * 0.05
108 state.T_40 = new_t if state.T_40 is None else state.T_40 * 0.95 + new_t * 0.05
109 if state.R is not None:
110 img_pts, jac = cv2.projectPoints(marker_corners, state.R, state.T, state.K, None)
111 else:
112 img_pts = []
113 corners = corners.astype(int)
① Traverse all detected tags and obtain the center and corner coordinates for each tag.
② If the tag ID is 1, process it and perform pose estimation using cv2.solvePnP. This updates the state variables R and T.
(7) Coordinate transformation and visual marker:
115 w = camera_to_world(state.K, state.R_40, state.T_40,
116 center.reshape((1, 1, 2)))[0][0]
117 print(center.reshape((1, 1, 2)))
118 cv2.circle(img, tuple(corners[0][0]), 5, (255, 0, 0), 12)
119 cv2.circle(img, tuple(corners[0][1]), 5, (0, 255, 0), 12)
120 cv2.circle(img, tuple(corners[0][2]), 5, (0, 0, 255), 12)
121 cv2.circle(img, tuple(corners[0][3]), 5, (0, 255, 255), 12)
122 cv2.circle(img, tuple(center), 5, (255, 255, 0), 12)
123 cv2.putText(img, "id:%d" % tag.tag_id,
124 (center[0], center[1] - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
125 for i, c in enumerate(img_pts):
126 cv2.circle(img, tuple(c.astype(int)[0]), 3, (0, 0, 0), 4)
127 else:
128 with state.lock:
129 if state.R_40 is not None:
130
131 w = camera_to_world(state.K, state.R_40, state.T_40,
132 center.reshape((1, 1, 2)))[0][0]
133 else:
134 w = 0, 0, 0
135 cv2.putText(img, "id:{} x:{:.2f} y:{:.2f}".format(tag.tag_id, w[0], w[1]),
136 (center[0] - 20, center[1] - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)
137 return img
Use the camera_to_world function to convert the point from the camera coordinate system to the physical coordinate system.
Draw the corner points and the center point of the tag on the image, and use cv2.putText to display the tag ID and physical coordinates on the image.
The parameters in the function are as follows:
① The first parameter img is the input image;
② The second parameter str(tag_id) is the content to be displayed;
③ The third parameter (10, img.shape[0] - 30) is the display position;
④ The fourth parameter cv2.FONT_HERSHEY_SIMPLEX is the font type;
⑤ The fifth parameter 0.65 is the font size;
⑥ The sixth parameter [0, 255, 255] is the font color, in BGR order, which is yellow;
⑦ The seventh parameter 2 is the font thickness.
Finally, the function returns the image.
(8) Action Control
173def move():
174 time.sleep(3)
175 board.set_buzzer(2400, 0.1, 0.4, 1)
176 down_cb()
177 time.sleep(10)
178 board.set_buzzer(2400, 0.1, 0.4, 2)
179 up_cb()
180 time.sleep(15)
181 board.set_buzzer(2400, 0.1, 0.4, 3)
182 save_cb()
Upon observing that the five black dots overlapped with the five colored dots on the AprilTag block, SpiderPi Pro makes three beeps.
7.4 Block Picking
7.4.1 Program Logic
Firstly, program SpiderPi Pro to recognize colors through Lab color space. Convert the RGB color space to Lab, and then perform image binarization, expansion, corrosion and other operations in sequence to obtain an outline only containing the target color. Then, circle the color outline to realize object color recognition.
After color recognition, calculate based on the feedback of the image position, and control the robotic arm of SpiderPi Pro to approach the target block, and open the gripper. Then pick the block to the corresponding position.
7.4.2 Operation Steps
Note
The input command should be case sensitive and space sensitive.
(1) Boot up SpiderPi Pro, then remotely connect to Raspberry Pi desktop through VNC.
(2) Click
at upper left corner of desktop to open the Terminator.
(3) Enter the command and press “Enter” to navigate to the directory where the game program is located.
cd spiderpi/advanced
(4) Input the command, and then press “Enter” to start the game.
python3 block_fetch.py
(5) If want to close this game, press “Ctrl+C” on LX terminal. If the game cannot be quit, please try again.
7.4.3 Project Outcome
After the block is recognized, the robotic arm will approach the block and open the gripper. After the block is placed to the middle of gripper, the robotic arm will transfer it to the right side.
7.4.4 Program Analysis
The source code of this program lies in: /home/pi/spiderpi/advanced/block_fetch.py
Import Function Library
4import sys
5import cv2
6import math
7import time
8import threading
9import numpy as np
10from common import misc
11from common import yaml_handle
12from common import kinematics
13from common.ros_robot_controller_sdk import Board
14from calibration.camera import Camera
15from calibration.CalibrationConfig import *
16import arm_ik.arm_move_ik as AMK
17from sensor.ultrasonic_sensor import Ultrasonic
(1) Import the libraries related to OpenCV, time, math, and threads.
If want to call a function in library, you can use “library name+function name (parameter, parameter)”. For example:
119 time.sleep(0.5)
Call sleep function in “time” library. The function sleep () is used to delay. There are some built-in libraries in Python, so they can be called directly. For example, time, cv2 and math. You can also write a new library like yaml_handle.
(2) Instantiate Function Library
The name of function library is too long to memorize. For calling function easily, the library can be instantiated. For example:
10from common import misc
11from common import yaml_handle
12from common import kinematics
13from common.ros_robot_controller_sdk import Board
After instantiating, you can directly input and call the function Board.function name (parameter, parameter).
Main Function Analysis
The python program __name__ == '__main__:' is the main function of program. Firstly, the function init() is called to initialize. The initialization in this program includes: return the servo to the initial position, read the color threshold file. Generally there are also configurations for ports, peripherals, timing interrupts, etc., which are all done in the process of initialization.
215if __name__ == '__main__':
216 #加载参数(load parameter)
217 param_data = np.load(calibration_param_path + '.npz')
218
219 #获取参数(obtain parameter)
220 mtx = param_data['mtx_array']
221 dist = param_data['dist_array']
222 newcameramtx, _ = cv2.getOptimalNewCameraMatrix(mtx, dist, (640, 480), 0, (640, 480))
223 mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (640, 480), 5)
224
225 init_move()
226 load_config()
227 camera = Camera()
228 camera.camera_open()
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.
161 frame_gb = cv2.GaussianBlur(img, (3, 3),3)
The meaning of the parameters in bracket is as follow
The first parameter img is the input image.
The second parameter (3, 3) is the size of Gaussian kernel.
The third parameter 3 is the allowable variance around the average in Gaussian filtering. The larger the value, the larger the allowable variance around the average value; The smaller the value, the smaller the allowable variance around the average value.
(2) Binaryzation processing
Adopt inRange() function in cv2 library to perform binaryzation on the image.
162 frame_mask = cv2.inRange(frame_lab,
163 (lab_data[color]['min'][0],
164 lab_data[color]['min'][1],
165 lab_data[color]['min'][2]),
166 (lab_data[color]['max'][0],
167 lab_data[color]['max'][1],
168 lab_data[color]['max'][2])) #对原图像和掩模进行位运算(perform bitwise operation to original image and mask)
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 a value of 1, otherwise, 0.
(3) Corrosion and dilation
To reduce the interference and make the image smoother, it is necessary to perform corrosion and dilation on the image.
169 eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #腐蚀(erode)
170 dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #膨胀(dilate)
erode() function is used for corrosion. Take eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) for example. The meaning of the parameters in bracket are as follow.
The first parameter frame_mask is the input image
The second parameter cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) is the structural element and kernel deciding the nature of the operation. And the first parameter in the parenthesis is the kernel shape and the second parameter is the kernel dimension.
dilate() function is used for image dilation. And the meaning of the parameters in parenthesis is the same as that of erode() function.
(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.
171 contours = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2] #找出轮廓(find contours)
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 larger than this value, the target contour is valid.
61def get_area_maxContour(contours):
62 contour_area_temp = 0
63 contour_area_max = 0
64 area_max_contour = None
65 max_area = 0
66
67 for c in contours: # 历遍所有轮廓(traverse through all contours)
68 contour_area_temp = math.fabs(cv2.contourArea(c)) # 计算轮廓面积(calculate contour area)
69 if contour_area_temp > contour_area_max:
70 contour_area_max = contour_area_temp
71 if contour_area_temp >= 100: # 只有在面积大于设定时,最大面积的轮廓才是有效的,以过滤干扰(Only when the area is greater than the set value, the contour with the maximum area is considered valid to filter out interference)
72 area_max_contour = c
73 max_area = contour_area_temp
74
75 return area_max_contour, max_area # 返回最大的轮廓, 面积(Return the largest contour and area)
(5) Information feedback
After the contour with the largest area is obtained, mark the contour with circle() function in cv2 library, and display the color with putText() function.
180 cv2.circle(img, (centerX, centerY), radius, range_rgb[color], 2)#画圆(draw the circle)
181 cv2.putText(img, "Color: " + color, (10, img.shape[0] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.65, range_rgb[color], 2)
Acquire the coordinate
(1) Judge whether the position of the ball remains stable in order to avoid the influence on the robot’s piking performance.
183 if abs(centerX-old_x) < 8 and abs(centerY-old_y) < 8: # 判断目标坐标有没有变化(determine whether the target coordinate is changed)
184 num += 1
185 else:
186 num = 0
187 old_x, old_y = centerX, centerY
(2) After the coordinate is confirmed, convert the coordinate into actual distance through calculation.
189 if num > 10: # 多次判断,确定目标位置稳定(judge multiple times to ensure stable target position)
190 # 转换成现实距离(convert to realistic distance)
191 center = np.array([centerX,centerY])
192 w = camera_to_world(K, R, T, center.reshape((1, 1, 2)))[0][0]
193 world_x, world_y = int(-w[0])/10, int(-w[1])/10
Pick the block
Having gotten the location of the block, control the robotic arm of the SpiderPi Pro to move to the top of the block, and open its gripper. After the block is placed to the middle of the gripper, it will transfer it to the corresponding position.
117 while True:
118 if start:
119 time.sleep(0.5)
120 board.bus_servo_set_position(0.5, [[25, 120]]) # 张开爪子(open the gripper)
121 x,y = initial_coord[0]+world_x, initial_coord[1]+world_y # 转换成机械臂坐标(convert to robotic arm coordinate)
122 ak.setPitchRangeMoving((x, y, 5), -90, -90, 100, 1) # 移动到目标位置(move to the target position)
123 time.sleep(3)
124 board.bus_servo_set_position(0.5, [[25, 550]]) # 夹取目标(grasping target)
125 time.sleep(2)
126 ak.setPitchRangeMoving((12, 24, 5), -90, -90, 100, 1.5) # 抬起来(raise up)
127 time.sleep(1.5)
128 ak.setPitchRangeMoving((12, 24, -5), -90, -90, 100, 1) # 移动到放置位置(move to the placing position)
129 time.sleep(1)
130 board.bus_servo_set_position(0.5, [[25, 120]]) # 张开爪子(open the gripper)
131 time.sleep(0.5)
132 ak.setPitchRangeMoving((12, 24, 5), -90, -90, 100, 1) # 抬起来(raise up)
133 time.sleep(1)
134 ak.setPitchRangeMoving(initial_coord, -90, -90, 100, 2) # 回到检测色块姿态(return to the posture of color detecting)
135 time.sleep(2)
136 start = False
137 world_x, world_y = 666, 666
138
139 else:
140 board.set_buzzer(2400, 0.1, 0.4, 1) # 以2400Hz的频率,0.1秒开始响,0.4秒停止响,重复1次(The buzzer sounds at the frequency of 2400Hz starting from 0.1s followed by a pause of 0.4s, then it repeats this pattern once)
141 time.sleep(1)
bus_servo_set_position() function in Board library and setPitchRangeMoving() function in ArmIK library will be used in block picking.
bus_servo_set_position() function is used to drive the serial servo to rotate to the designated position. Take board.bus_servo_set_position(0.5, [[25, 120]]) for example. The meaning of the parameter in the bracket is as follow.
The first parameter 0.5 is the rotation time in the unit of s, which is 0.5s.
The second parameter 25 is the ID of the servo to be driven.
The third parameter 120 is the rotation position. It is obtained by angle conversion.
The rotation range of the serial bus servo is 0-1000 pulse width. The converted angle range is 0-240°, that is 1°is approximately equivalent to 4.2 pulse width. Formula: pulse width=4.2*Angle. Note: this formula is only for reference.
setPitchRangeMoving() function is used to control the robotic arm of the SpiderPi Pro to move to the given coordinate. Take AK.setPitchRangeMoving((x, y, -5), -90, -90, 100, 1) for example. The meaning of the parameters in bracket is as follow.
The first parameter (x, y, -5) is the given coordinate and the unit is cm. This parameter is passed in as tuple.
The second parameter -90 is the given pitch angle
The third parameter -90 and the fourth parameter 100 is the range of the pitch angle.
The fifth parameter 1 is the rotation time of the servo and the unit is s.
7.5 Color Sorting
7.5.1 Program Logic
Firstly, program SpiderPi Pro to recognize colors through Lab color space. Convert the RGB color space to Lab, and then perform image binarization, expansion, corrosion and other operations in sequence to obtain an outline only containing the target color.
Then, circle the target outline on the camera returned image. And the robotic arm will perform corresponding action after recognizing the color. After that, the robotic arm will return to the initial posture.
7.5.2 Operation Steps
The input command should be case sensitive and space sensitive.
(1) Boot up SpiderPi Pro, then remotely connect to Raspberry Pi desktop through VNC.
(2) Click
at upper left corner of desktop to open the Terminator.
(3) Enter the command and press “Enter” to navigate to the directory where the game program is located.
cd spiderpi/advanced
(4) Input the command and then press “Enter” to start the game.
python3 color_sorting.py
(5) If want to close this game, press “Ctrl+C” on LX terminal. If the game cannot be quit, please try again.
7.5.3 Project Outcome
After the game starts, the robotic arm will raise, and the camera is facing forward horizontally. Please place red, green and blue balls in sequence within the camera’s field of view. When the colored block is recognized, it will be circled and the color of the block will be printed on the camera returned image.
When recognizing the red block, SpiderPi Pro will shake its head. When recognizing green or blue block, the robotic arm will move forward and open its gripper. Please place the block in the middle of the gripper, and then the robotic arm will pick the block and place it to the corresponding position. SpiderPi Pro will place the green block to its left front side, and blue block to its right front side.
7.5.4 Program Analysis
The source codes of this program are stored in: /home/pi/spiderpi/advanced/color_sorting.py
Import Function Library
4import sys
5import cv2
6import math
7import time
8import threading
9import numpy as np
10from common import misc
11from common.pid import PID
12from common import yaml_handle
13from calibration.camera import Camera
14from calibration.CalibrationConfig import *
15from sensor.ultrasonic_sensor import Ultrasonic
16import arm_ik.arm_move_ik as AMK
17from common import kinematics
18from common.ros_robot_controller_sdk import Board
19from sensor.ultrasonic_sensor import Ultrasonic
20debug = False
21
22board = Board()
23ik = kinematics.IK(board)
24ultrasonic = Ultrasonic()
25ak = AMK.ArmIK()
(1) Import the libraries related to OpenCV, time, math, and threads. If want to call a function in library, you can use library name+function name (parameter, parameter). For example:
122 time.sleep(0.3)
Call sleep function in time library. The function sleep () is used to delay. There are some built-in libraries in Python, so they can be called directly. For example, time, cv2 and math. You can also write a new library like yaml_handle.
(2) Instantiate Function Library
The name of function library is too long to memorize. For calling function easily, the library can be instantiated. For example:
122 time.sleep(0.3)
After instantiating, you can directly input and call the function “Board.function name (parameter, parameter)”.
Main Function Analysis
The python program __name__ == '__main__:' is the main function of program. Firstly, the function init() is called to initialize. The initialization in this program includes: return the servo to the initial position, read the color threshold file. Generally there are also configurations for ports, peripherals, timing interrupts, etc., which are all done in the process of initialization.
252if __name__ == '__main__':
253
254 #加载参数(load parameter)
255 param_data = np.load(calibration_param_path + '.npz')
256
257 #获取参数(obtain parameter)
258 mtx = param_data['mtx_array']
259 dist = param_data['dist_array']
260 newcameramtx, _ = cv2.getOptimalNewCameraMatrix(mtx, dist, (640, 480), 0, (640, 480))
261 mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (640, 480), 5)
(1) Read the Captured Image
267 while True:
268 img = camera.frame
When the the game is started, store the image in “img”.
(2) Enter Image Processing
When the captured image is read, call run function to process the image.
269 if img is not None:
270 frame = img.copy()
271 frame = cv2.remap(frame, mapx, mapy, cv2.INTER_LINEAR) # 畸变矫正(distortion correction)
272 Frame = run(frame)
The function img.copy() is used to copy the content of img to frame.
The function run() is used to process image.
{lineno-start=}
size = (320, 240)
def run(img):
global draw_color
global color_list
global detect_color
global action_finish
img_copy = img.copy()
img_h, img_w = img.shape[:2]
if not __isRunning:
return img
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) # 将图像转换到LAB空间(convert the image to LAB space)
max_area = 0
color_area_max = None
areaMaxContour_max = 0
if action_finish:
for i in lab_data:
if i != 'black' and i != 'white':
frame_mask = cv2.inRange(frame_lab,
(lab_data[i]['min'][0],
lab_data[i]['min'][1],
lab_data[i]['min'][2]),
(lab_data[i]['max'][0],
lab_data[i]['max'][1],
lab_data[i]['max'][2])) #对原图像和掩模进行位运算(perform bitwise operation to original image and mask)
(3) Image resizing process
182 frame_resize = cv2.resize(img_copy, size, interpolation=cv2.INTER_NEAREST)
The first parameter img_copy is the input image.
The second parameter size is the size of the output image. The size can be set based on you own needs.
The third parameter interpolation=cv2.INTER_NEAREST is interpolation method.
INTER_NEAREST is the nearest Neighbour Interpolation.
INTER_LINEAR is the Bilinear Interpolation. If the last parameter is not modified, this method INTER_LINEAR will be used by default.
INTER_CUBIC: Bicubic interpolation within a 4x4 pixel neighborhood.
INTER_LANCZOS4: Lanczos interpolation within an 8x8 pixel neighborhood.
(4) Gaussian filtering
Noise is always present in image to influence the image quality to weaken the features. Select the filter methods according to the different noises and the command method includes Gaussian filter, Median filtering, Mean filter, etc. Gaussian filter is a linear filter also smooths an image and reduces noise, which is widely used in processing image to eliminate noise.
183 frame_gb = cv2.GaussianBlur(frame_resize, (3, 3), 3)
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 standard deviation of the Gaussian kernel in the X-axis direction.
(5) Convert image to LAB space
Use the cv2.cvtColor() function to convert the color space.
184 frame_lab = cv2.cvtColor(frame_gb, cv2.COLOR_BGR2LAB) # 将图像转换到LAB空间(convert the image to LAB space)
The first parameter frame_gb is the input image.
The second parameter cv2.COLOR_BGR2LAB specifies the conversion format. cv2.COLOR_BGR2LAB is used to convert BGR format to LAB format. If you want to convert to RGB, cv2.COLOR_BGR2RGB can be used.
(6) Binaryzation processing
Adopt inRange() function in cv2 library to perform binaryzation on the image.
193 frame_mask = cv2.inRange(frame_lab,
194 (lab_data[i]['min'][0],
195 lab_data[i]['min'][1],
196 lab_data[i]['min'][2]),
197 (lab_data[i]['max'][0],
198 lab_data[i]['max'][1],
199 lab_data[i]['max'][2])) #对原图像和掩模进行位运算(perform bitwise operation to original image and mask)
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 a value of 1, otherwise, 0.
(7) Corrosion and dilation
To reduce distraction and make the image smoother, perform corrosion and dilation on the image.
200 eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #腐蚀(erode)
201 dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #膨胀(dilate)
erode() function is used for image corrosion. Take eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) for example. The meaning of the parameters in bracket are as follow.
The first parameter frame_mask is the input image.
The second parameter cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) is the structural element or kernel deciding the operation nature. The first parameter in the bracket is the kernel shape and the second parameter is the kernel dimension.
dilate() is used to perform dilation on the image. The meaning of the parameters in the bracket is the same as that of erode() function.
(8) Acquire the maximum contour
After processing the image, acquire the contour of the target to be recognized, which involves findContours() function in cv2 library.
204 contours = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2] #找出轮廓(find contours)
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 larger than this value, the target contour is valid.
205 areaMaxContour, area_max = getAreaMaxContour(contours) #找出最大轮廓(find the largest contour)
206 if areaMaxContour is not None:
207 if area_max > max_area:#找最大面积(find the largest area)
(9) Obtain position information
Using the cv2.minEnclosingCircle function in the cv2 library to obtain the minimum enclosing circle of the target contour, and obtaining the center coordinates and radius of the minimum enclosing circle.
211 if max_area > 100: # 有找到最大面积(the maximum area has been found)
212 ((centerX, centerY), radius) = cv2.minEnclosingCircle(areaMaxContour_max) # 获取最小外接圆(obtain the minimum circumscribed circle)
213 centerX = int(misc.map(centerX, 0, size[0], 0, img_w))
214 centerY = int(misc.map(centerY, 0, size[1], 0, img_h))
215 radius = int(misc.map(radius, 0, size[0], 0, img_w))
216 cv2.circle(img, (centerX, centerY), radius, range_rgb[color_area_max], 2) #画圆(draw the circle)
Obtaining the color with the largest area in the image through conditional statements.
218 if color_area_max == 'red': #红色最大(red is the largest)
219 color = 1
220 elif color_area_max == 'green': #绿色最大(green is the largest)
221 color = 2
222 elif color_area_max == 'blue': #蓝色最大(blue is the largest)
223 color = 3
224 else:
225 color = 0
226 color_list.append(color)
227
228 if len(color_list) == 3: #多次判断(determine multiple times)
229 # 取平均值(obtain mean)
230 color = int(round(np.mean(np.array(color_list))))
231 color_list = []
232 if color == 1:
233 detect_color = 'red'
234 draw_color = range_rgb["red"]
235 elif color == 2:
236 detect_color = 'green'
237 draw_color = range_rgb["green"]
238 elif color == 3:
239 detect_color = 'blue'
240 draw_color = range_rgb["blue"]
241 else:
242 detect_color = 'None'
243 draw_color = range_rgb["black"]
244 else:
245 detect_color = 'None'
246 draw_color = range_rgb["black"]
Cal the putText() function to display the block color in the live camera feed.
248 cv2.putText(img, "Color: " + detect_color, (10, img.shape[0] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.65, draw_color, 2)
(10) Display Transmitted Image
272 Frame = run(frame)
273 cv2.imshow('Frame', Frame)
274 key = cv2.waitKey(1)
275 if key == 27:
276 break
The function cv2.imshow() is used to display an image in a window. 'frame' is the name of the window, and 'Frame' is the content to be displayed. It is important to include cv2.waitKey() after this function, otherwise the image won’t be displayed.
The function cv2.waitKey() is used to wait for a key input, and the parameter 1 is the delay time.
Subthread Analysis
Run the move() function of the robotic arm as a subthread. When a color is recognized, the move() function is executed.
105def move():
106 global draw_color
107 global detect_color
108 global action_finish
109
110 coord = {'blue': ( 8, 24, -5.5),
111 'green': (-8, 24, -5.5)}
112
113 while True:
114 if debug:
115 return
116 if __isRunning:
117 if detect_color != 'None':
118 action_finish = False
119 if detect_color == 'red':
120 for i in range(3): # 识别到红色左右摇头(If red is recognized, shake the head left and right)
121 board.bus_servo_set_position(0.3,[[21,560]])
122 time.sleep(0.3)
123 board.bus_servo_set_position(0.3,[[21,440]])
124 time.sleep(0.3)
125 board.bus_servo_set_position(0.1,[[21,500]])
126 time.sleep(0.1)
127 detect_color = 'None'
128 draw_color = range_rgb["black"]
129 time.sleep(1)
130 elif detect_color == 'green' or detect_color == 'blue': # 识别到蓝色或者绿色(blue or green is recognized)
131 board.bus_servo_set_position(0.6,[[25,120]])
132 time.sleep(0.6)
133 ak.setPitchRangeMoving((0, 18, 30), 0, -90, 100, 0.5) # 机械臂往前伸(extend the robotic arm forward)
134 time.sleep(2)
135 board.bus_servo_set_position(0.6,[[25,450]])
136 time.sleep(1)
137 x,y,z = coord[detect_color] # 读取放置坐标(read placing coordinate)
138 pul, _ = ak.setPitchRange((x,y,z), -90, -80) # 逆运动学求解(calculate with inverse kinematics)
139 if pul is not None:
140 servo21 = pul['servo21']
141 board.bus_servo_set_position(0.5,[[21,servo21]])
142 time.sleep(0.5)
143 ak.setPitchRangeMoving((x,y,-2), -90, -90, 100, 1.6) # 机械臂运动到目标放置坐标上方(robotic arm moves above the target placing coordinate)
144 time.sleep(1.6)
145 ak.setPitchRangeMoving((x,y,z), -90, -90, 100, 0.6) # 到达放置目标(reach the target place for placing)
146 time.sleep(0.6)
147 board.bus_servo_set_position(0.6,[[25,120]])
148 time.sleep(0.8)
149 ak.setPitchRangeMoving((0, 12, 30), 0, -90, 100, 1.5) # 机械臂回初始姿态(robotic arm returns to the initial status)
150 time.sleep(1)
151 board.bus_servo_set_position(0.5,[[25,1000]])
152 time.sleep(1)
153 detect_color = 'None'
154 draw_color = range_rgb["black"]
155 time.sleep(1)
(1) Set the coordinate for placing the block
110 coord = {'blue': ( 8, 24, -5.5),
111 'green': (-8, 24, -5.5)}
(8, 24, -5.5) is a given coordinate, passed in the form of a tuple.
(2) Recognize red
When recognizing the red block, call setBusServoPulse() function in Board library to control the pan-tilt of robotic arm to rotate around.
110 coord = {'blue': ( 8, 24, -5.5),
111 'green': (-8, 24, -5.5)}
bus_servo_set_position() function is used to drive the serial bus servo to the designated position. Take board.bus_servo_set_position(0.3,[[21,560]]) for example.
The first parameter 0.3 is the rotation time in the unit of s, which is 0.3s.
The second parameter 21 is the ID of servo to drive.
The third parameter 560 is the rotation position obtained by angle conversion.
The rotation range of the serial bus servo is 0-1000, corresponding to 0-240°. 1°is approximately equivalent to 4.2 pulse width. Formula: pulse width=4.2*angle. Note: this formula is only for reference.
(2) Recognize green or blue
When green or blue block is recognized, combine setBusServoPulse() function in Board library and setPitchRangeMoving() function in ArmIK library to control the robotic arm to pick and place the block.
130 elif detect_color == 'green' or detect_color == 'blue': # 识别到蓝色或者绿色(blue or green is recognized)
131 board.bus_servo_set_position(0.6,[[25,120]])
132 time.sleep(0.6)
133 ak.setPitchRangeMoving((0, 18, 30), 0, -90, 100, 0.5) # 机械臂往前伸(extend the robotic arm forward)
134 time.sleep(2)
135 board.bus_servo_set_position(0.6,[[25,450]])
136 time.sleep(1)
137 x,y,z = coord[detect_color] # 读取放置坐标(read placing coordinate)
138 pul, _ = ak.setPitchRange((x,y,z), -90, -80) # 逆运动学求解(calculate with inverse kinematics)
139 if pul is not None:
140 servo21 = pul['servo21']
141 board.bus_servo_set_position(0.5,[[21,servo21]])
142 time.sleep(0.5)
143 ak.setPitchRangeMoving((x,y,-2), -90, -90, 100, 1.6) # 机械臂运动到目标放置坐标上方(robotic arm moves above the target placing coordinate)
144 time.sleep(1.6)
145 ak.setPitchRangeMoving((x,y,z), -90, -90, 100, 0.6) # 到达放置目标(reach the target place for placing)
146 time.sleep(0.6)
147 board.bus_servo_set_position(0.6,[[25,120]])
148 time.sleep(0.8)
149 ak.setPitchRangeMoving((0, 12, 30), 0, -90, 100, 1.5) # 机械臂回初始姿态(robotic arm returns to the initial status)
150 time.sleep(1)
151 board.bus_servo_set_position(0.5,[[25,1000]])
152 time.sleep(1)
153 detect_color = 'None'
154 draw_color = range_rgb["black"]
155 time.sleep(1)
setPitchRangeMoving() function is used to control the robotic arm to move to the given coordinate. Take AK.setPitchRangeMoving((0, 18, 30), 0, -90, 100, 0.5) for example. The meaning of the parameters in bracket is as follow.
The first parameter (0, 18, 30) is the given coordinate which is passed in as tuple. Its unit is cm.
The second parameter 0 is the given pitch angle.
The third parameter -90 and the fourth parameter 100 is the range of the pitch angle.
The fifth parameter 0.5 is the rotation time of the servo in the unit of s.
7.6 Intelligent Picking
7.6.1 Program Logic
Firstly, program SpiderPi Pro to recognize colors through Lab color space. Convert the RGB color space to Lab, and then perform image binarization, expansion, corrosion and other operations in sequence to obtain an outline only containing the target color. Then, circle the color outline.
After color recognition, calculate based on the feedback of the image position, and control the robotic arm of SpiderPi Pro to pick the target block. Then detect human face. When the human face is detected, robotic arm will transfer the block to the corresponding position and place it down.
7.6.2 Operation Steps
The input command should be case sensitive and space sensitive.
(1) Boot up SpiderPi Pro, then remotely connect to Raspberry Pi desktop through VNC.
(2) Click
at upper left corner of desktop to open the Terminator.
(3) Enter the command and press “Enter” to navigate to the directory where the game program is located.
cd spiderpi/advanced
(4) Input the command, and then press “Enter” to start the game.
python3 intelligent_fetch.py
(5) If want to close this game, press “Ctrl+C” on LX terminal. If the game cannot be quit, please try again.
7.6.3 Project Outcome
After the game starts, place the red block within camera’s field of view. When recognizing the red block, SpiderPi Pro will bend the robotic arm down to pick the block.
SpiderPi Pro will start detecting human face. When human face is recognized, transfer the block to the designated position.
7.6.4 Program Analysis
The source codes of this program are stored in: /home/pi/spiderpi/advanced/intelligent_fetch.py
Import Function Library
4import sys
5import cv2
6import math
7import time
8import threading
9import numpy as np
10import mediapipe as mp
11from common import misc
12from common import yaml_handle
13from common import kinematics
14from common.ros_robot_controller_sdk import Board
15from calibration.camera import Camera
16from calibration.CalibrationConfig import *
17from sensor.ultrasonic_sensor import Ultrasonic
18import arm_ik.arm_move_ik as AMK
(1) Import the libraries related to OpenCV, time, math, and threads. If want to call a function in library, you can use “library name+function name (parameter, parameter)”. For example:
133 time.sleep(0.5)
Call sleep function in “time” library. The function sleep () is used to delay. There are some built-in libraries in Python, so they can be called directly. For example, time, cv2 and math. You can also write a new library like yaml_handle.
(2) Instantiate Function Library
The name of function library is too long to memorize. For calling function easily, the library can be instantiated. For example:
11from common import misc
12from common import yaml_handle
13from common import kinematics
14from common.ros_robot_controller_sdk import Board
After instantiating, you can directly input and call the function “Board.function name (parameter, parameter)”.
Main Function Analysis
The python program __name__ == '__main__:' is the main function of program. Firstly, the function init() is called to initialize. The initialization in this program includes: return the servo to the initial position, read the color threshold file. Generally there are also configurations for ports, peripherals, timing interrupts, etc., which are all done in the process of initialization.
301if __name__ == '__main__':
302 #加载参数(load parameter)
303 param_data = np.load(calibration_param_path + '.npz')
304
305 #获取参数(obtain parameter)
306 mtx = param_data['mtx_array']
307 dist = param_data['dist_array']
308 newcameramtx, _ = cv2.getOptimalNewCameraMatrix(mtx, dist, (640, 480), 0, (640, 480))
309 mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (640, 480), 5)
310
311 init_move()
312 load_config()
313 camera = Camera()
314 camera.camera_open()
(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.
245 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 variation range around the average in Gaussian filtering. The larger the value, the larger the allowable variation range around the average value; The smaller the value, the smaller the allowable variation range around the average value.
(2) Binaryzation processing
Adopt inRange() function in cv2 library to perform binaryzation on the image.
247 frame_mask = cv2.inRange(frame_lab,
248 (lab_data[color]['min'][0],
249 lab_data[color]['min'][1],
250 lab_data[color]['min'][2]),
251 (lab_data[color]['max'][0],
252 lab_data[color]['max'][1],
253 lab_data[color]['max'][2])) #对原图像和掩模进行位运算(perform bitwise operation to original image and mask)
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 a value of 1, otherwise, 0.
(3) Corrosion and dilation
To reduce the interference and make the image smoother, it is necessary to perform corrosion and dilation on the image.
254 eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #腐蚀(erode)
255 dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #膨胀(dilate)
erode() function is used for corrosion. Take eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) for example. The meaning of the parameters in bracket are as follow.
The first parameter frame_mask is the input image
The second parameter cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) is the structural element and kernel deciding the nature of the operation. And the first parameter in the parenthesis is the kernel shape and the second parameter is the kernel dimension.
dilate() function is used for image corrosion. And the meaning of the parameters in parenthesis is the same as that of erode() function.
(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.
256 contours = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2] #找出轮廓(find contours)
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 larger than this value, the target contour is valid.
72def get_area_max_contour(contours):
73 contour_area_temp = 0
74 contour_area_max = 0
75 area_max_contour = None
76 max_area = 0
77
78 for c in contours: # 历遍所有轮廓(traverse through all contours)
79 contour_area_temp = math.fabs(cv2.contourArea(c)) # 计算轮廓面积(calculate contour area)
80 if contour_area_temp > contour_area_max:
81 contour_area_max = contour_area_temp
82 if contour_area_temp >= 100: # 只有在面积大于设定时,最大面积的轮廓才是有效的,以过滤干扰(Only when the area is greater than the set value, the contour with the maximum area is considered valid to filter out interference)
83 area_max_contour = c
84 max_area = contour_area_temp
85
86 return area_max_contour, max_area # 返回最大的轮廓, 面积(return the area of the largest contour)
(5) Information feedback
After the contour with the largest area is obtained, mark the contour with circle() function in cv2 library, and display the color with putText() function.
265 cv2.circle(img, (centerX, centerY), radius, range_rgb[color], 2)#画圆(draw the circle)
266 cv2.putText(img, "Color: " + color, (10, img.shape[0] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.65, range_rgb[color], 2)
Acquire the coordinate
(1) Judge whether the position of the ball remains stable in order to avoid the influence on the robot’s piking performance.
268 if abs(centerX-old_x) < 8 and abs(centerY-old_y) < 8: # 判断目标坐标有没有变化(determine whether the target coordinate is changed)
269 num += 1
270 else:
271 num = 0
272 old_x, old_y = centerX, centerY
(2) After the coordinate is confirmed, convert the coordinate into actual distance through calculation.
274 if num > 10: # 多次判断,确定目标位置稳定(judge multiple times to ensure stable target position)
275 # 转换成现实距离(convert to realistic distance)
276 center = np.array([centerX,centerY])
277 w = camera_to_world(K, R, T, center.reshape((1, 1, 2)))[0][0]
278 world_x, world_y = int(-w[0])/10, int(-w[1])/10
279 print(world_x, world_y)
280 start = True
281 num = 0
Pick the block
Having gotten the location of the block, control the robotic arm of the SpiderPi Pro to move to the top of the block, and control the gripper to pick the block.
131 if step == 'color': # 夹取色块阶段(the phrase of grasping block)
132 if start:
133 time.sleep(0.5)
134 board.bus_servo_set_position(0.5, [[25, 120]])# 张开爪子(open the gripper)
135 x,y = initial_coord[0]+world_x, initial_coord[1]+world_y # 转换成机械臂坐标(convert to robotic arm coordinate)
136
137 ak.setPitchRangeMoving((x, y+2, -5), -90, -90, 100, 2) # 移动到目标位置(move to the target position)
138 time.sleep(2)
139 board.bus_servo_set_position(0.5, [[25, 600]]) # 夹取目标(grasping target)
140 time.sleep(0.5)
141 ak.setPitchRangeMoving((x, y, 8), -90, -90, 100, 1) # 抬起来(raise up)
142 time.sleep(1)
143 ak.setPitchRangeMoving((0, 15, 30), 0, -90, 100, 2) # 检测人脸姿态(detect face status)
144 step = 'face'
145 time.sleep(2)
146 start = False
147 world_x, world_y = 666, 666
bus_servo_set_position() function in Board library and setPitchRangeMoving() function in ArmIK library will be used in block picking.
bus_servo_set_position() function is used to drive the serial servo to rotate to the designated position. Take board.bus_servo_set_position(0.5, [[25, 120]]) for example. The meaning of the parameter in the bracket is as follow.
The first parameter 0.5 is the rotation time in the unit of s, which is 0.5s.
The second parameter 25 is the ID of the servo to drive.
The third parameter 120 is the rotation position. It is obtained by angle conversion.
The rotation range of the serial bus servo is 0-1000 pulse width. The converted angle range is 0-240°, that is 1° is approximately equivalent to 4.2 pulse width. Formula: pulse width=4.2*Angle. Note: this formula is only for reference.
setPitchRangeMoving() function is used to control the robotic arm of the SpiderPi Pro to move to the given coordinate. Take AK.setPitchRangeMoving((x, y, -5), -90, -90, 100, 1) for example. The meaning of the parameters in bracket is as follow.
The first parameter (x, y, -5) is the given coordinate and the unit is cm. This parameter is passed in as tuple.
The second parameter -90 is the given pitch angle
The third parameter -90 and the fourth parameter “100” is the range of the pitch angle.
The fifth parameter 1 is the rotation time of the servo and the unit is s.
Detect human face
After picking the block, detect human face. The detection parameters involved in this process are as follow.
189def face_detect(img):
190 global area
191 global center_x, center_y
192 global center_x, center_y, area
193 global FaceDetect
194 global action_finish
195
196
197 img_copy = img.copy()
198 img_h, img_w = img.shape[:2]
199
200 imgRGB = cv2.cvtColor(img_copy, cv2.COLOR_BGR2RGB) # 将BGR图像转为RGB图像(convert the BGR image to RGB image)
201 results = faceDetection.process(imgRGB) # 将每一帧图像传给人脸识别模块(transmit the image of each frame to face recognition module)
202
203 if results.detections: # 如果检测不到人脸那就返回None(If no face is detected, return Noun)
204
205 for index, detection in enumerate(results.detections): # 返回人脸索引index(第几张脸),和关键点的坐标信息(Return the face index (the number of the detected face) and the coordinates of the facial keypoints)
206 scores = list(detection.score)
207 if scores and scores[0] > 0.7:
208
209 bboxC = detection.location_data.relative_bounding_box # 设置一个边界框,接收所有的框的xywh及关键点信息(Set a bounding box to receive the xywh and keypoint information of all the received boxes)
210
211 # 将边界框的坐标点,宽,高从比例坐标转换成像素坐标(Convert the coordinates, width, and height of the bounding box from relative coordinates to pixel coordinates)
212 bbox = (
213 int(bboxC.xmin * img_w),
214 int(bboxC.ymin * img_h),
215 int(bboxC.width * img_w),
216 int(bboxC.height * img_h)
217 )
218 cv2.rectangle(img, bbox, (0, 255, 0), 2) # 在每一帧图像上绘制矩形框(draw rectangle box on the image of each frame)
219
220 # 获取识别框的信息, xy为左上角坐标点(Obtain the information of the detected box, where xy is the coordinate of the upper-left corner)
221 x, y, w, h = bbox
222 center_x = int(x + (w / 2))
223 center_y = int(y + (h / 2))
224 area = int(w * h)
225
226 FaceDetect = True
227
228
229 else:
230 center_x, center_y, area = -1, -1, 0
231
232 return img
(1) Information feedback
After completing face detection, use rectangle() function to draw a rectangle to frame the human face, as shown in the following figure.
218 cv2.rectangle(img, bbox, (0, 255, 0), 2) # 在每一帧图像上绘制矩形框(draw rectangle box on the image of each frame)
The first parameter img is the input image, here referring to the image of the drawn rectangle.
The second parameter bbox is the start and end coordinates of the rectangle.
The third parameter(0, 255, 0) is the color of the rectangle border, here indicating green. And the numbers in parentheses represent B, G, and R respectively.
The forth parameter 2 is the width of rectangle border. If it is set as -1, it means that the rectangle will be filled with the designated color.
Transfer the block
After recognizing the human face, the robotic arm will transfer the block to the corresponding coordinate and place it down.
152 elif step == 'face': # 检测人脸阶段(detect face phrase)
153 if FaceDetect:
154
155 w = ak.setPitchRange((0, 30, 30), 10, -10)[0] # 要递出去的位置,逆运动学求解,得到舵机脉宽值(Given the desired position, solve the inverse kinematics to obtain the servo pulse width value)
156 if w is not None:
157 for i in ServoID:
158 board.bus_servo_set_position(1.5, [[i, w['servo'+str(i)]]])# 驱动舵机(drive the servo)
159 else:
160 return
161 time.sleep(2)
162 board.bus_servo_set_position(0.5, [[25, 120]])# 张开爪子(open the gripper)
163 time.sleep(1)
164 ak.setPitchRangeMoving(initial_coord, -90, -90, 100, 2) # 回到检测色块姿态(return to the posture of color detecting)
165 time.sleep(2)
166 pulse21 = 500
167 step = 'color'
168 Face_Detect = False
169 world_x, world_y = 666, 666
170 time.sleep(1)
If not recognizing human face, the robotic arm will rotate around to search human face, as shown in the following picture.
173 else: # 左右转动,寻找人脸(rotate left and right to find the face)
174 if pulse21 > 700 or pulse21 < 300:
175 dn = 0 - dn
176 pulse21 += dn
177 board.bus_servo_set_position(0.02, [[21, pulse21]])# 张开爪子(open the gripper)
178 time.sleep(0.03)
7.7 Line Following and Transferring
7.7.1 Program Logic
This game is combined with Line Following and Color Sorting. SpiderPi Pro will pick the colored blocks and transfer them to the destination and then sort them.
The implementation process is as follow.
Firstly, program SpiderPi Pro to recognize colors through Lab color space. Convert the RGB color space to Lab, and then perform image binarization, corrosion, expansion and other operations to obtain an outline only containing the target color.
Then, after the red line is detected, control SpiderPi Pro to move along the red line. When SpiderPi Pro arrives the picking point, control it to recognize and pick the block.
Next, control SpiderPi Pro to move to the storage destination, and perform color sorting.
Lastly, control the robot to move back to the picking point and repeat the previous operation.
7.7.2 Operation Steps
The input command should be case sensitive and space sensitive.
(1) Boot up SpiderPi Pro, then remotely connect to Raspberry Pi desktop through VNC.
(2) Click
at upper left corner of desktop to open the Terminator.
(3) Enter the command and press “Enter” to navigate to the directory where the game program is located.
cd spiderpi/advanced
(4) Input the command, and then press “Enter” to start the game.
python3 cruise_carry.py
(5) If want to close this game, press “Ctrl+C” on LX terminal. If the game cannot be quit, please try again.
7.7.3 Project Outcome
SpiderPi Pro will crawl along the red line. After detecting the red horizontal line, it starts detecting green or blue block. Having found the block, it will transfer the block and turn right 180°to continue its movement along the red line. When detecting the red horizontal line in the other end, it will place the blocks to the corresponding position.
After placing the colored block, it will turn right 180° again to continue transferring other colored blocks.
7.7.4 Program Analysis
The source code of this program is stored in: /home/pi/spiderpi/advanced/cruise_carry.py
Set the detected color
In this game, SpiderPi Pro will detect green or blue block and red line.
42color_list = ('green','blue')
403 if step == 'move':
404 img = lineDetect(img, 'red')
405 elif step == 'detect' and not place:
406 img = ColorDetect(img, color_list[skip])
Parameter of red line detection
Red line detection mainly involves the following parameters.
(1) Before converting the image from RGB into LAB space, denoise the image and use GaussianBlur() function in cv2 library for Gaussian filtering.
281 frame_gb = cv2.GaussianBlur(img, (3, 3), 3)
The first parameter img is the input image.
The second parameter (3, 3) is the size of Gaussian kernel. Larger kernel will lead to greater filtering, obscurer output image and more complex computation.
The third parameter 3 is the standard deviation of Gaussian function along X direction, which is used to control the variation around the average. As this data increases, the allowable range of variation around the average expands. As this data decrease, the allowable range of variation around the average narrows down.
(2) Use inRange function to perform binaryzation on the input image, as shown in the following figure.
286 frame_mask = cv2.inRange(frame_lab,
287 (lab_data[color]['min'][0],
288 lab_data[color]['min'][1],
289 lab_data[color]['min'][2]),
290 (lab_data[color]['max'][0],
291 lab_data[color]['max'][1],
292 lab_data[color]['max'][2])) #对原图像和掩模进行位运算(perform bitwise operation to original image and mask)
(3) To reduce interference and make the image smoother, corrosion and dilation need to be performed on the image, as shown in the below figure.
293 eroded = cv2.erode(frame_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #腐蚀(erode)
294 dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))) #膨胀(dilate)
getStructuringElement function is used to generate structural elements of different forms.
The first parameter cv2.MORPH_RECT is the kernel shape and the current parameter represents rectangle.
The second parameter (3, 3) is the size of the rectangle. The current dimension of the rectangle is 3x3.
(4) Find out the maximum contour of the object.
71def getAreaMaxContour(contours):
72 contour_area_temp = 0
73 contour_area_max = 0
74 area_max_contour = None
75 max_area = 0
76
77 for c in contours: # 历遍所有轮廓(traverse through all contours)
78 contour_area_temp = math.fabs(cv2.contourArea(c)) # 计算轮廓面积(calculate contour area)
79 if contour_area_temp > contour_area_max:
80 contour_area_max = contour_area_temp
81 if contour_area_temp >= 100: # 只有在面积大于设定时,最大面积的轮廓才是有效的,以过滤干扰(Only when the area is greater than the set value, the contour with the maximum area is considered valid to filter out interference)
82 area_max_contour = c
83 max_area = contour_area_temp
84
85 return area_max_contour, max_area # 返回最大的轮廓, 面积(Return the area of the largest contour)
To avoid interference, use if contour_area_temp > 100 for command setting. Only when the area is larger than 100, the contour of maximum area is effective.
(5) When the red line is recognized by the robot, use cv2.drawContours() function to draw the contour of the colored object.
305 cv2.drawContours(img, [box], -1, (0, 255, 255), 2) #画出四个点组成的矩形(draw the rectangle composed of four points)
The first parameter img is the input image.
The second parameter [box] is the contour itself, and list in Python.
The third parameter -1 is the index of the contour. The value here represents that all the contours in the list will be drawn.
The fourth parameter (0, 255, 255) is the color of contour. These three values respectively corresponds to B, G and R, and the current color is yellow.
The fifth parameter 2 is the contour width. If it is -1, it represents that the contour will be filled in with designated color.
(6) After the robot recognizes the red line, use cv2.circle() function to draw the center of the red line on the camera returned image.
311 cv2.circle(img, (int(line_center_x), int(line_center_y)), 5, (0, 0, 255), -1) #画出中心点(draw the center point)
312 line_centerX = line_center_x
313 line_centerY = line_center_y
The first parameter img is the input image. Here is the image of recognized colored object.
The second parameter (int(line_center_x), int(line_center_y)) is the center coordinate of the drawn circle.
The third parameter 5 is the radius of the drawn circle.
The fourth parameter (0, 0, 255) is the color of the drawn circle. These three values respectively corresponds to B, G and R, and the current color is red.
The fifth parameter -1 represents fill in the circle with the color of parameter 4. If it is a number, this parameter represents the width of the drawn circle.
Parameters of line following
After the red line is detected, control the robot to move along the red line. Its source code can be checked in the below figure.
217 elif step == 'move': # 机器人巡线移动(robot line following movement)
218 if line_centerX >= 0 and line_width < 100: # 判断是否检测到线条(determine whether the line is detected)
219 if abs(line_centerX -img_center_x) < 30: # 判断是否偏离(check if there is any deviation)
220 ik.go_forward(ik.initial_pos, 2, 50, 60, 1)
221
222 elif line_centerX -img_center_x >= 30: #线条在画面右侧,右转(If the line is on the right side of the image, the robot turns right)
223 ik.turn_right(ik.initial_pos, 2, 5, 50, 1)
224
225 elif line_centerX -img_center_x <= -30: # 线条在画面左侧,左转(If the line is on the left side of the image, the robot turns left)
226 ik.turn_left(ik.initial_pos, 2, 5, 50, 1)
227
228 last_line_centerX = line_centerX
229 n = 0
Take ik.go_forward(ik.initial_pos, 2, 50, 60, 1) for example.
The first parameter ik.initial_pos represents the posture.
The second parameter 2 represents the game mode is Hexapod Robot mode.
The third parameter 50 represents the stride in mm. The unit is degree when it makes turn.
The fourth parameter 60 represents the time taken to control the SpiderPi Pro to move in the stride or angle set in parameter 3.
The fifth parameter 1 represents the execution times. When it is set as 0, the robot will execute the action at loop.
Parameters of colored block detection
The parameters of colored block detection are similar to that of red line detection. The codes shown in the below figure.
159 else: #夹取状态(grasping status)
160 if carry:
161 if world_x > 7:
162 ik.right_move(ik.initial_pos, 2, 30, 50, 1)
163 direction = 'right'
164 sp += 1
165 carry = False
166 elif world_x > 4 and world_y < -2:
167 ik.right_move(ik.initial_pos, 2, 30, 50, 1)
168 direction = 'right'
169 sp += 1
170 carry = False
171 elif world_x < -7:
172 ik.left_move(ik.initial_pos, 2, 30, 50, 1)
173 direction = 'left'
174 sp += 1
175 carry = False
176 elif world_x < -4 and world_y < -2:
177 ik.left_move(ik.initial_pos, 2, 30, 50, 1)
178 direction = 'left'
179 sp += 1
180 carry = False
181 elif world_y > 7:
182 ik.go_forward(ik.initial_pos, 2, 50, 60, 1)
183 carry = False
Parameters of intelligent transferring
According to the coordinate of the colored block, the robot will determine whether the block is on its right side or left side. Then it will approach the colored block automatically and pick the block. It will take the following steps to realize this action.
(1) Judge the position of the block, on its left or right. And approach the colored block.
159 else: #夹取状态(grasping status)
160 if carry:
161 if world_x > 7:
162 ik.right_move(ik.initial_pos, 2, 30, 50, 1)
163 direction = 'right'
164 sp += 1
165 carry = False
166 elif world_x > 4 and world_y < -2:
167 ik.right_move(ik.initial_pos, 2, 30, 50, 1)
168 direction = 'right'
169 sp += 1
170 carry = False
171 elif world_x < -7:
172 ik.left_move(ik.initial_pos, 2, 30, 50, 1)
173 direction = 'left'
174 sp += 1
175 carry = False
176 elif world_x < -4 and world_y < -2:
177 ik.left_move(ik.initial_pos, 2, 30, 50, 1)
178 direction = 'left'
179 sp += 1
180 carry = False
181 elif world_y > 7:
182 ik.go_forward(ik.initial_pos, 2, 50, 60, 1)
183 carry = False
(2) Use board.bus_servo_set_position function to control the robotic arm to open its gripper.
184 else:
185 ik.stand(ik.initial_pos)
186 time.sleep(0.5)
187 board.bus_servo_set_position(0.5,[[25,120]]) # 张开爪子(open the gripper)
The first parameter 0.5 is the running time of the servo in the unit of s.
The first parameter 25 is the servo ID.
The second parameter 120 is the servo value ranging from 0 to 1000.
(3) Use AK.setPitchRangeMoving function to control the robotic arm to move to the colored block.
189 ak.setPitchRangeMoving((x, y+1.5, 0), -90, -90, 100, 1) # 移动到目标位置上方(move above the target position)
The first parameter (x, y, -5) is the coordinate where the robotic should reach.
The second parameter -90 is the pitch angle
The third parameter -90 and the fourth parameter 100 are the range of the pitch angle, that is -90~100.
The fifth parameter 1 is the time taken for the robotic arm to move to the specified coordinate.
Note
AK.tPitchRangeMoving funcsetion finally will be converted into the values that control NO.21, 22, 23 and 24 servos on the robotic arm, which will involves the inverse kinematics of the robotic arm.
(4) Next, pick the colored block and lift the robotic arm. After that, program SpiderPi Pro to perform line following.
189 ak.setPitchRangeMoving((x, y+1.5, 0), -90, -90, 100, 1) # 移动到目标位置上方(move above the target position)
190 time.sleep(1)
191 ak.setPitchRangeMoving((x, y+1.5, -5), -90, -90, 100, 1) # 移动到目标位置(move to the target position)
192 time.sleep(1)
193 board.bus_servo_set_position(0.5,[[25,550]]) # 夹取目标(grasping target)
194 time.sleep(0.5)
195 ak.setPitchRangeMoving((x, y+1, 0), -90, -90, 100, 1) # 抬起来(raise up)
196 time.sleep(1)
197 ak.setPitchRangeMoving(line_coord, -60, -90, 90, 1.5) # 机械臂回到巡线姿态(robotic arm returns to the line following posture)
198 time.sleep(1.5)
(5) According to the position of the colored block, control the robotic arm to move left and right.
199 if direction == 'left':
200 ik.right_move(ik.initial_pos, 2, 30, 50, sp)
201 direction = None
202 sp = 0
203 elif direction == 'right':
204 ik.left_move(ik.initial_pos, 2, 30, 50, sp)
205 direction = None
206 sp = 0
207 place = True
208 carry = False
(6) Control the moving direction of the robot.
210 ik.back(ik.initial_pos, 2, 30, 50, 5) # 向后退几步(back a few steps)
211 ik.stand(ik.initial_pos, t=500) # 立正姿态(stand position)
212 ik.turn_left(ik.initial_pos, 2, 15, 50, 13) # 右转180°(turn right for 180°)
213 ik.stand(ik.initial_pos, t=500)
214 time.sleep(0.5)
215 line_width = 0
Parameters of block placing
After picking the block, the robot will transfer it to the designated position. The specific the operation is as follow.
(1) The robot starts crawling along the red line. The parameters of this part are illustrated in “7.7.4 Program Analysis -> Parameters of red line detection” and “7.7.4 Program Analysis -> Parameters of line following”
(2) Control the robot to move along the red line. When the red horizontal line is detected, transfer the colored block and place it to the corresponding position.Lastly, control it to turn around, and continue line following and transferring other blocks.
242 if line_width > 200: # 检测到横线(a horizontal line is detected)
243 dn += 1
244 time.sleep(0.01)
245 if dn >= 5: # 多次检测(detect multiple times)
246 ik.go_forward(ik.initial_pos, 2, 60, 60, 3) # 向前走两步(go forward two steps)
247 ik.stand(ik.initial_pos, t=500)
248 if place