1. import rclpy
    2. from rclpy.node import Node
    3. from sensor_msgs.msg import Image
    4. import cv_bridge
    5. import numpy as np
    6. import cv2 as cv
    7. from geometry_msgs.msg import Twist
    8. import serial
    9. import struct
    10. import threading
    11. import time
    12. ## User-defined parameters: (Update these values to your liking)
    13. # Minimum size for a contour to be considered anything
    14. MIN_AREA = 500
    15. # Minimum size for a contour to be considered part of the track
    16. MIN_AREA_TRACK = 5000
    17. # Robot's speed when following the line
    18. LINEAR_SPEED = 0.2
    19. # Proportional constant to be applied on speed when turning
    20. # (Multiplied by the error value)
    21. KP = 1.5/100
    22. # If the line is completely lost, the error value shall be compensated by:
    23. LOSS_FACTOR = 1.2
    24. # Send messages every $TIMER_PERIOD seconds
    25. TIMER_PERIOD = 0.06
    26. # When about to end the track, move for ~$FINALIZATION_PERIOD more seconds
    27. FINALIZATION_PERIOD = 4
    28. # The maximum error value for which the robot is still in a straight line
    29. MAX_ERROR = 30
    30. # BGR values to filter only the selected color range
    31. lower_bgr_values = np.array([0, 0, 0])
    32. upper_bgr_values = np.array([180, 255, 46])
    33. finalization_countdown = None
    34. right_mark_count = 0
    35. class MyNode(Node):
    36. def __init__(self,node_name):
    37. super().__init__(node_name)
    38. # 线速度和角速度
    39. self.x = 0.0
    40. self.z = 0.0
    41. # 初始化摄像头配置
    42. self.init_camera()
    43. # 初始化串口
    44. self.init_serial()
    45. def init_serial(self):
    46. # 创建串口对象
    47. self.ser = serial.Serial("/dev/ttyS3",115200,timeout = 5)
    48. self.ser.flushInput()
    49. self.i = 0
    50. # 需要发送的头 类型和数据长度
    51. self.head = struct.pack('<b',0x7b)
    52. self.instruction = struct.pack('<b',0x00)
    53. self.length = struct.pack('<b',3)
    54. self.tail = struct.pack('<b',0x7a)
    55. self.t1 = threading.Thread(target=self.recv_speed)
    56. self.t2 = threading.Thread(target=self.send_speed)
    57. # 守护线程 主线程挂掉 子线程也挂掉 需要在start之前设置
    58. self.t1.setDaemon(True)
    59. self.t1.setDaemon(True)
    60. self.t1.start()
    61. self.t2.start()
    62. # ---------------------------- 串口 ---------------------------- #
    63. # TODO 发送真实的操作指令
    64. def send_speed(self):
    65. while True:
    66. print('send_speed')
    67. data = struct.pack('<f',self.x)+struct.pack('<f',0.0)+struct.pack('<f',self.z)
    68. # 校验和校验
    69. check_num = 0x00+3+self.x+0.0+self.z
    70. check = struct.pack('<f',check_num)
    71. # 需要发送的数据
    72. data = self.head+self.instruction+self.length+data+check+self.tail
    73. # 发送串口数据
    74. self.ser.write(data)
    75. time.sleep(0.06)
    76. def recv_speed(self):
    77. print('start')
    78. print(f'is_open:{self.ser.is_open}')
    79. # while True:
    80. # print(f'i:{self.i}')
    81. # self.i += 1
    82. # time.sleep(0.06)
    83. while True:
    84. self.i += 1
    85. # 获取需要接收的数据个数
    86. num = self.ser.inWaiting()
    87. # print(f'i:{self.i} start num:{num}')
    88. # num = self.ser.in_waiting()
    89. # if num<=0 or num != 48:
    90. # continue
    91. if num<=0 or num<48:
    92. continue
    93. if num>48:
    94. # 清除
    95. self.ser.read(num)
    96. continue
    97. # 正常的一帧数据
    98. print(f'num:{num}')
    99. data = self.ser.read(num)
    100. # 解析数据
    101. self.parse_speed(data)
    102. print('end')
    103. time.sleep(0.06)
    104. # TODO 通过获取到的数据进行决策
    105. def parse_speed(self,data):
    106. # 帧头
    107. head = struct.unpack('<B',data[:1])[0]
    108. if head!=122:
    109. return
    110. # 命令
    111. ins = struct.unpack('<B',data[1:2])[0]
    112. if ins!=1:
    113. return
    114. # 数据长度
    115. length = struct.unpack('<B',data[2:3])[0]
    116. if length!=10:
    117. return
    118. # 数据
    119. want_data = struct.unpack('<ffffffffff',data[3:43])
    120. # 校验位
    121. check = struct.unpack('<f',data[43:47])[0]
    122. # 帧尾
    123. tail = struct.unpack('<B',data[47:])[0]
    124. # self.get_logger().info(f'i:{self.i},帧头:{head},命令:{ins},数据长度:{length},数据:{want_data},校验位:{check},帧尾:{tail}')
    125. print(f'i:{self.i},帧头:{head},命令:{ins},数据长度:{length},数据:{want_data},校验位:{check},帧尾:{tail}')
    126. print(f'i:{self.i} end')
    127. # ---------------------------- 摄像头 ---------------------------- #
    128. def init_camera(self):
    129. # 摄像头数据
    130. self.image_input = None
    131. # 图像感兴趣区域
    132. # self.crop_h_start = -1
    133. # self.crop_h_stop = -1
    134. self.crop_w_start = -1
    135. # self.crop_w_stop = -1
    136. # 订阅图像信息
    137. self.subscription = self.create_subscription(Image, 'image_raw',
    138. self.image_callback,
    139. rclpy.qos.qos_profile_sensor_data)
    140. # cvbridge
    141. self.bridge = cv_bridge.CvBridge()
    142. # 设定时间回调
    143. self.timer = self.create_timer(0.06, self.timer_callback)
    144. def image_callback(self,msg):
    145. """获取摄像头图像"""
    146. self.image_input = self.bridge.imgmsg_to_cv2(msg,desired_encoding='bgr8')
    147. # cv.imshow('img',self.image_input)
    148. # cv.waitKey(20)
    149. def crop_size(self,height, width):
    150. """
    151. 裁剪感兴趣的区域
    152. """
    153. return (1*height//3, height, width//4, 3*width//4)
    154. def get_contour_data(self,mask, out):
    155. """
    156. Return the centroid of the largest contour in
    157. the binary image 'mask' (the line)
    158. and return the side in which the smaller camera_namecontour is (the track mark)
    159. (If there are any of these contours),
    160. and draw all contours on 'out' image
    161. """
    162. # 获取轮廓
    163. contours, _ = cv.findContours(mask, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_NONE)
    164. mark = {}
    165. line = {}
    166. for contour in contours:
    167. M = cv.moments(contour)
    168. # Search more about Image Moments on Wikipedia :)
    169. if M['m00'] > MIN_AREA:
    170. # if countor.area > MIN_AREA:
    171. if (M['m00'] > MIN_AREA_TRACK):
    172. # Contour is part of the track
    173. line['x'] = self.crop_w_start + int(M["m10"]/M["m00"])
    174. line['y'] = int(M["m01"]/M["m00"])
    175. # plot the area in light blue
    176. cv.drawContours(out, contour, -1, (255,255,0), 1)
    177. cv.putText(out, str(M['m00']), (int(M["m10"]/M["m00"]), int(M["m01"]/M["m00"])),
    178. cv.FONT_HERSHEY_PLAIN, 2, (255,255,0), 2)
    179. else:
    180. # Contour is a track mark
    181. if (not mark) or (mark['y'] > int(M["m01"]/M["m00"])):
    182. # if there are more than one mark, consider only
    183. # the one closest to the robot
    184. mark['y'] = int(M["m01"]/M["m00"])
    185. mark['x'] = self.crop_w_start + int(M["m10"]/M["m00"])
    186. # plot the area in pink
    187. cv.drawContours(out, contour, -1, (255,0,255), 1)
    188. cv.putText(out, str(M['m00']), (int(M["m10"]/M["m00"]), int(M["m01"]/M["m00"])),
    189. cv.FONT_HERSHEY_PLAIN, 2, (255,0,255), 2)
    190. if mark and line:
    191. # if both contours exist
    192. if mark['x'] > line['x']:
    193. mark_side = "right"
    194. else:
    195. mark_side = "left"
    196. else:
    197. mark_side = None
    198. return (line, mark_side)
    199. def timer_callback(self):
    200. """
    201. Function to be called when the timer ticks.
    202. According to an image 'image_input', determine the speed of the robot
    203. so it can follow the contour
    204. """
    205. global error
    206. # global image_input
    207. global just_seen_line
    208. global just_seen_right_mark
    209. global should_move
    210. global right_mark_count
    211. global finalization_countdown
    212. # Wait for the first image to be received
    213. if type(self.image_input) != np.ndarray:
    214. return
    215. # 获取宽度和高度
    216. height, width, _ = self.image_input.shape
    217. image = self.image_input.copy()
    218. # 裁剪感兴趣的区域
    219. crop_h_start, crop_h_stop, self.crop_w_start, crop_w_stop = self.crop_size(height, width)
    220. # 获取感兴趣的图片区域
    221. crop = image[crop_h_start:crop_h_stop, self.crop_w_start:crop_w_stop]
    222. # 获取二值化的图 非0表示直线
    223. # (过滤颜色值 只看到轮廓)
    224. mask = cv.inRange(crop, lower_bgr_values, upper_bgr_values)
    225. # get the centroid of the biggest contour in the picture,
    226. # and plot its detail on the cropped part of the output image
    227. #获取图片中最大轮廓的质心,
    228. #并在输出图像的裁剪部分绘制其细节
    229. output = image
    230. line, mark_side = self.get_contour_data(mask, output[crop_h_start:crop_h_stop, self.crop_w_start:crop_w_stop])
    231. # also get the side in which the track mark "is"
    232. message = Twist()
    233. if line:
    234. # if there even is a line in the image:
    235. # (as the camera could not be reading any lines)
    236. x = line['x']
    237. # error:= The difference between the center of the image
    238. # and the center of the line
    239. error = x - width//2
    240. message.linear.x = LINEAR_SPEED
    241. just_seen_line = True
    242. # plot the line centroid on the image
    243. cv.circle(output, (line['x'], crop_h_start + line['y']), 5, (0,255,0), 7)
    244. else:
    245. # There is no line in the image.
    246. # Turn on the spot to find it again.
    247. if just_seen_line:
    248. just_seen_line = False
    249. error = error * LOSS_FACTOR
    250. message.linear.x = 0.0
    251. if mark_side != None:
    252. print("mark_side: {}".format(mark_side))
    253. if (mark_side == "right") and (finalization_countdown == None) and \
    254. (abs(error) <= MAX_ERROR) and (not just_seen_right_mark):
    255. right_mark_count += 1
    256. if right_mark_count > 1:
    257. # Start final countdown to stop the robot
    258. finalization_countdown = int(FINALIZATION_PERIOD / TIMER_PERIOD) + 1
    259. print("Finalization Process has begun!")
    260. just_seen_right_mark = True
    261. else:
    262. just_seen_right_mark = False
    263. # Determine the speed to turn and get the line in the center of the camera.
    264. message.angular.z = float(error) * -KP
    265. print("Error: {} | Angular Z: {}, ".format(error, message.angular.z))
    266. # Plot the boundaries where the image was cropped
    267. cv.rectangle(output, (self.crop_w_start, crop_h_start), (crop_w_stop, crop_h_stop), (0,0,255), 2)
    268. # Uncomment to show the binary picture
    269. #cv2.imshow("mask", mask)
    270. # Show the output image to the user
    271. cv.imshow("output", output)
    272. # Print the image for 5milis, then resume execution
    273. cv.waitKey(5)
    274. # Check for final countdown
    275. if finalization_countdown != None:
    276. if finalization_countdown > 0:
    277. finalization_countdown -= 1
    278. elif finalization_countdown == 0:
    279. should_move = False
    280. self.x = message.linear.x
    281. self.z = -message.angular.z
    282. # Publish the message to 'cmd_vel'
    283. # if should_move:
    284. # publisher.publish(message)
    285. # else:
    286. # empty_message = Twist()
    287. # publisher.publish(empty_message)
    288. def main():
    289. # 初始化rclpy
    290. rclpy.init()
    291. # 参数:节点名称
    292. node = MyNode('control_node')
    293. rclpy.spin(node)
    294. # 节点代码执行完之后需要销毁节点
    295. node.destroy_node()
    296. # 关闭rclpy
    297. rclpy.shutdown()
    298. if __name__ == '__main__':
    299. main()