-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathorange_line_follower.py
executable file
·151 lines (122 loc) · 5.17 KB
/
orange_line_follower.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/env python3
import rospy
import cv2
import numpy as np
from cv_bridge import CvBridge, CvBridgeError
from sensor_msgs.msg import Image
from std_msgs.msg import Float32
from geometry_msgs.msg import Vector3
import math
def normalize_angle(angle):
"""
Normalize angle to the range [-180, 180] degrees.
"""
while angle > 180:
angle -= 360
while angle < -180:
angle += 360
return angle
class LineFollowerNode:
def __init__(self):
rospy.init_node('line_follower', anonymous=True)
# Subscribers
rospy.Subscriber('/usb_cam_node/image_raw', Image, self.camera_callback)
rospy.Subscriber('/sensors/orientation', Vector3, self.imu_callback)
# Publishers
self.angle_pub = rospy.Publisher('/line/angle_difference', Float32, queue_size=10)
self.image_pub = rospy.Publisher('/processed_image', Image, queue_size=10)
# Variables
self.yaw = 0.0 # IMU yaw
self.bridge = CvBridge()
# Line smoothing variables
self.prev_line_params = None
self.smoothing_factor = 0.2 # Adjust this to control smoothing (lower is smoother)
def imu_callback(self, msg):
"""
Callback to handle IMU data and update yaw.
"""
self.yaw = msg.z # Assuming `orientation.z` is in degrees (0 to 360)
def smooth_line(self, params):
"""
Smooth the line parameters using exponential moving average (EMA).
"""
if self.prev_line_params is None:
self.prev_line_params = params
return params
smoothed = [
self.smoothing_factor * new + (1 - self.smoothing_factor) * old
for new, old in zip(params, self.prev_line_params)
]
self.prev_line_params = smoothed
return smoothed
def camera_callback(self, msg):
"""
Callback to process camera image, detect line, and calculate angle difference.
"""
try:
# Convert ROS image to OpenCV image
cv_image = self.bridge.imgmsg_to_cv2(msg, "bgr8")
except CvBridgeError as e:
rospy.logerr(f"CvBridge Error: {e}")
return
# Preprocess the image
hsv = cv2.cvtColor(cv_image, cv2.COLOR_BGR2HSV)
lower_orange = np.array([10, 100, 100]) # Adjust HSV thresholds for orange
upper_orange = np.array([25, 255, 255])
mask = cv2.inRange(hsv, lower_orange, upper_orange)
masked_image = cv2.bitwise_and(cv_image, cv_image, mask=mask)
# Convert to grayscale and threshold
gray = cv2.cvtColor(masked_image, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 50, 255, cv2.THRESH_BINARY)
# Detect line using contours
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
# Find the largest contour (assuming it's the line)
largest_contour = max(contours, key=cv2.contourArea)
# Fit a line to the contour
[vx, vy, x, y] = cv2.fitLine(largest_contour, cv2.DIST_L2, 0, 0.01, 0.01)
line_angle = math.degrees(math.atan2(vy, vx))
# Normalize line angle to [0, 360]
if line_angle < 0:
line_angle += 360
# Calculate angle difference
angle_difference = normalize_angle(line_angle - self.yaw)
# Publish the angle difference
self.angle_pub.publish(angle_difference)
# Smooth the line parameters to reduce jitter
smoothed_line = self.smooth_line([vx, vy, x, y])
vx, vy, x, y = smoothed_line
# Draw the green line (detected path)
rows, cols = cv_image.shape[:2]
lefty = int((-x * vy / vx) + y)
righty = int(((cols - x) * vy / vx) + y)
# Ensure line coordinates are within image bounds
lefty = max(0, min(rows - 1, lefty))
righty = max(0, min(rows - 1, righty))
cv2.line(cv_image, (cols - 1, righty), (0, lefty), (0, 255, 0), 2)
else:
rospy.logwarn("No orange line detected.")
angle_difference = None
# Draw the yaw axis and heading text
rows, cols = cv_image.shape[:2]
yaw_x = int(cols // 2 + 100 * math.cos(math.radians(self.yaw)))
yaw_y = int(rows // 2 - 100 * math.sin(math.radians(self.yaw)))
cv2.line(cv_image, (cols // 2, rows // 2), (yaw_x, yaw_y), (0, 0, 255), 2)
cv2.putText(cv_image, f"Yaw: {self.yaw:.1f} deg", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2)
if angle_difference is not None:
cv2.putText(cv_image, f"Angle Diff: {angle_difference:.1f} deg", (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2)
# Publish the processed image
self.image_pub.publish(self.bridge.cv2_to_imgmsg(cv_image, "bgr8"))
# Optional: Display for debugging purposes
cv2.imshow("Line Detection", cv_image)
cv2.waitKey(1)
def run(self):
rospy.spin()
if __name__ == '__main__':
try:
node = LineFollowerNode()
node.run()
except rospy.ROSInterruptException:
pass
finally:
cv2.destroyAllWindows()