Skip to content

Week 4 : Lane controller Hardware Setup

lasithaya edited this page Apr 8, 2020 · 6 revisions

In this tutorial, we will develop a lane controller for our robot.

Hardware Setup

It is important that we fix a right-handed coordinate system for our robot. It should be as shown in the following figure with X-axis in the forward direction, Y-axis to the left and Z-axis to up (More). When positive velocity command is given, it should move forward and positive rotational velocity should rotate the robot in the anti-clockwise direction (Viewed from top). You can use the teleop_twist_keyboard (teleop_twist_keyboard) package to control your robot by connecting to your robot remotely with your laptop. If your robot is behaving differently then you might have to swap the wire connection of your robot's motors.

(source :Duckiebot Modeling)

Network Setup

We are going to subscribe to the camera images from the robot and run our lane controller on your laptop. This way it's easy to debug and tune your code. You can follow the instructions provided earlier on how to set up remote connections to the robot. I would highly recommend using a dedicated wireless router for your laptop-robot connection with the laptop connected to the router via an ethernet cable. This will minimize any latency issues and you should be able to view the video stream from the robot's camera in near realtime. This is important because the robot will react to the lane detected by the camera and any latency in the received camera images will cause absurd behavior with your robot.

Detecting left and Right Lanes

In our previous image processing tutorial, we developed the code for detecting line segments but in order to move the robot inside the two lanes we should be able to categorize these line segments into two lines, namely left and right lanes. Before doing so, let's take a look at a frame photo shown below. The left lane appears to be going upwards so it has a negative slope (remember the coordinate system start point is top left corner). In other words, the left lane line has x1 < x2 and y2 < y1 and the slope = (y2 - y1) / (x2 - x1) which will give a negative slope. Therefore, all lines with negative slopes are considered left lane points. The right lane is the complete opposite, we can see that the right lane is going downwards and will have a positive slope. The right lane has x2 > x1 and y2 > y1 which will give a positive slope. So, all lines with positive slope are considered right lane points. In the case of vertical lines (x1 = x2), the slope will be infinity. In this case, we will skip all vertical lines to prevent getting an error. We will calculate the slope of each line segment and based on the sign of the slope we will combine the line segments into two distinct lines.

#!/usr/bin/env python
import rospy
from sensor_msgs.msg import Image
from geometry_msgs.msg import Twist
from cv_bridge import CvBridge
from sensor_msgs.msg import CompressedImage
import cv2
import math
import numpy as np

bridge = CvBridge()
vel_pub=rospy.Publisher('/cmd_vel', Twist, queue_size=1)
curr_orientation_angle=0.0

def start_node():
    rospy.init_node('lane_controller')
    rospy.loginfo('image_subcriber node started')
    
    rospy.Subscriber("/duckiebot/camera_node/image/compressed", CompressedImage, process_image)
    
    rospy.spin()
    
def process_image(msg):
    try:
       # convert sensor_msgs/Image to OpenCV Image
       
       # orig = bridge.imgmsg_to_cv2(msg, "bgr8")
        np_arr = np.fromstring(msg.data, np.uint8)
        orig = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
        edge_filtered = detect_edges(orig)
        roi_filtered = region_of_interest(edge_filtered)
       #line detection 
        line_segments = detect_line_segments(roi_filtered)
        
        lane_lines = average_slope_intercept(orig, line_segments)
        #lane_lines = combine_lines (orig, line_segments)
       
       #draw detected lines on the origial image 
        lane_lines_image = display_lines(orig,lane_lines)
        
        
        
        
    except Exception as err:
        print err
        
        
    global curr_orientation_angle    
    robot_orientation_new=compute_orientation_angle(orig, lane_lines)
    image_heading = display_heading_line( lane_lines_image,robot_orientation_new)
    curr_orientation_angle= curr_orientation_angle*0.0+ robot_orientation_new*1.0
    
    lane_controller( curr_orientation_angle,len(lane_lines))    
    # show results
    show_image(image_heading )
    
def lane_controller(orientation,num_lane):
    
    vel_reducer=(1-orientation/30.0);
    
    if  vel_reducer < 0.0:
        vel_mul=0.0
        
    else:
        vel_mul=vel_reducer   
    
    if num_lane==2:
        forward_vel=0.1* vel_mul
        angular_vel=-0.015*float(orientation)
        
    elif num_lane==1:
        forward_vel=0.02 * vel_mul
        angular_vel=0.005*float(orientation)
    else  :
        forward_vel=0.00
        angular_vel=0.00
    
    publishing_vel( forward_vel, angular_vel)
    
    
def publishing_vel( forward_vel, angular_vel):
    vel = Twist()
    vel.angular.x = 0.0
    vel.angular.y = 0.0
    vel.angular.z = angular_vel
    vel.linear.x = forward_vel
    vel.linear.y = 0.0
    vel.linear.z = 0.0
    vel_pub.publish(vel)     
    
def stabilize_orientation_angle(curr_steering_angle, new_steering_angle, num_of_lane_lines, max_angle_deviation_two_lines=5.0, max_angle_deviation_one_lane=1):
    """
    Using last steering angle to stabilize the steering angle
    This can be improved to use last N angles, etc
    if new angle is too different from current angle, only turn by max_angle_deviation degrees
    """
    if num_of_lane_lines == 2 :
        # if both lane lines detected, then we can deviate more
        max_angle_deviation = max_angle_deviation_two_lines
    else :
        # if only one lane detected, don't deviate too much
        max_angle_deviation = max_angle_deviation_one_lane
    
    angle_deviation = new_steering_angle - curr_steering_angle
    if abs(angle_deviation) > max_angle_deviation:
        stabilized_steering_angle = float(curr_steering_angle
                                        + max_angle_deviation * angle_deviation / abs(angle_deviation))
    else:
        stabilized_steering_angle = new_steering_angle
    #logging.info('Proposed angle: %s, stabilized angle: %s' % (new_steering_angle, stabilized_steering_angle))
    return stabilized_steering_angle
    
def detect_edges(frame):
    # filter for blue lane lines
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    lower_blue = np.array([80, 140, 40])
    upper_blue = np.array([120, 255, 255])
    mask = cv2.inRange(hsv, lower_blue, upper_blue)
    

    # detect edges
    edges = cv2.Canny(mask, 200, 400)

    return edges
    

def region_of_interest(edges):
    height, width = edges.shape
    mask = np.zeros_like(edges)

    # only focus bottom half of the screen
    polygon = np.array([[
        (0, height * 1 / 2),
        (width, height * 1 / 2),
        (width, height),
        (0, height),
    ]], np.int32)

    cv2.fillPoly(mask, polygon, 255)
    cropped_edges = cv2.bitwise_and(edges, mask)
    return cropped_edges
    
    
def detect_line_segments(cropped_edges):
    # tuning min_threshold, minLineLength, maxLineGap is a trial and error process by hand
    rho = 1  # distance precision in pixel, i.e. 1 pixel
    angle = np.pi / 180  # angular precision in radian, i.e. 1 degree
    min_threshold = 50  # minimal of votes
    line_segments = cv2.HoughLinesP(cropped_edges, rho, angle, min_threshold, 
                                    np.array([]), minLineLength=5, maxLineGap=10)                                    
    return line_segments
    
    
def average_slope_intercept(frame, line_segments):
    """
    This function combines line segments into one or two lane lines
    If all line slopes are < 0: then we only have detected left lane
    If all line slopes are > 0: then we only have detected right lane
    """
    lane_lines = []
    if line_segments is None:
        rospy.loginfo('No line_segment segments detected')
        return lane_lines

    height, width, _ = frame.shape
    left_fit = []
    right_fit = []

    boundary = 1/3
    left_region_boundary = width * (1 - boundary)  # left lane line segment should be on left 2/3 of the screen
    right_region_boundary = width * boundary # right lane line segment should be on left 2/3 of the screen

    for line_segment in line_segments:
        for x1, y1, x2, y2 in line_segment:
            if x1 == x2:
                #rospy.loginfo('skipping vertical line segment (slope=inf): %s' % line_segment)
                continue
            fit = np.polyfit((x1, x2), (y1, y2), 1)
            slope = fit[0]
            intercept = fit[1]
            if slope < 0:
                #if x1 < left_region_boundary and x2 < left_region_boundary:
               left_fit.append((slope, intercept))
            else:
                #if x1 > right_region_boundary and x2 > right_region_boundary:
                right_fit.append((slope, intercept))

    left_fit_average = np.average(left_fit, axis=0)
    if len(left_fit) > 0:
        lane_lines.append(make_points(frame, left_fit_average))

    right_fit_average = np.average(right_fit, axis=0)
    if len(right_fit) > 0:
        lane_lines.append(make_points(frame, right_fit_average))
    

    return lane_lines
    
    
    
def combine_lines (frame, line_segments):  
    color_1=[255, 0, 0] 
    color_2=[0, 0, 255] 
    thickness=2
    lane_lines = []
    if line_segments is None:
        rospy.loginfo('No line_segment segments detected')
        return lane_lines
    
    # state variables to keep track of most dominant segment
    largestLeftLineSize = 0
    largestRightLineSize = 0
    largestLeftLine = (0,0,0,0)
    largestRightLine = (0,0,0,0)
    left_lane_flag= False
    right_lane_flag= False
    
    for line in line_segments:
        for x1,y1,x2,y2 in line:
            size = math.hypot(x2 - x1, y2 - y1)
            slope = ((y2-y1)/(x2-x1))
            # Filter slope based on incline and
            # find the most dominent segment based on length
            if (slope > 0.0): #right
                if (size > largestRightLineSize):
                    largestRightLine = (x1, y1, x2, y2)
                    right_lane_flag= True                  
               #cv2.line(frame, (x1, y1), (x2, y2), color_1, thickness)
            elif (slope < 0.0): #left
                if (size > largestLeftLineSize):
                    largestLeftLine = (x1, y1, x2, y2)
                    left_lane_flag= True
               #cv2.line(frame, (x1, y1), (x2, y2), color_2, thickness)
                
    if  left_lane_flag: 
        x1,y1,x2,y2 = largestLeftLine         
        fit_left = np.polyfit((x1,y1), (x2,y2), 1) 
        cv2.line(frame, (x1, y1), (x2, y2), color_1, thickness)
        lane_lines.append(make_points(frame, fit_left))   
    
    if  right_lane_flag:
        x1,y1,x2,y2 = largestRightLine    
        fit_right = np.polyfit((x1,y1), (x2,y2), 1) 
        cv2.line(frame, (x1, y1), (x2, y2), color_2, thickness)  
        lane_lines.append(make_points(frame, fit_right))
    
    return lane_lines
    
def make_points(frame, line):
    height, width, _ = frame.shape
    slope, intercept = line
    y1 = height  # bottom of the frame
    y2 = int(y1 * 1 / 2)  # make points from middle of the frame down

    # bound the coordinates within the frame
    x1 = max(-width, min(2 * width, int((y1 - intercept) / slope)))
    x2 = max(-width, min(2 * width, int((y2 - intercept) / slope)))
    return [[x1, y1, x2, y2]]
    
    
def compute_orientation_angle(frame, lane_lines):
    height, width, _ = frame.shape

    if len(lane_lines) == 2: # if two lane lines are detected
        _, _, left_x2, _ = lane_lines[0][0] # extract left x2 from lane_lines array
        _, _, right_x2, _ = lane_lines[1][0] # extract right x2 from lane_lines array
        mid = float(width / 2)
        x_offset = float( (left_x2 + right_x2) / 2 - mid)
        y_offset = float(height / 2)  
 
    elif len(lane_lines) == 1: # if only one line is detected
        x1, _, x2, _ = lane_lines[0][0]
        x_offset = float(x2 - x1)
        y_offset = float(height / 2)

    elif len(lane_lines) == 0: # if no line is detected
        x_offset = 0.0
        y_offset = float(height / 2)

    angle_to_mid_radian = math.atan(x_offset / y_offset)
    angle_to_mid_deg = float(angle_to_mid_radian * 180.0 / math.pi)  
    steering_angle = angle_to_mid_deg 
    rospy.loginfo("Steering angle %f :",angle_to_mid_deg)

    
    return steering_angle  
    
    
    
    
def display_lines(frame, lines, line_color=(0, 255, 0), line_width=2):
    line_image = np.zeros_like(frame)
    if lines is not None:
        for line in lines:
            for x1, y1, x2, y2 in line:
                cv2.line(line_image, (x1, y1), (x2, y2), line_color, line_width)
    line_image = cv2.addWeighted(frame, 0.8, line_image, 1, 1)
    return line_image
    
    
def display_heading_line(frame, steering_angle, line_color=(0, 0, 255), line_width=5 ):
    heading_image = np.zeros_like(frame)
    height, width, _ = frame.shape

    # figure out the heading line from steering angle
    # heading line (x1,y1) is always center bottom of the screen
    # (x2, y2) requires a bit of trigonometry

    # Note: the steering angle of:
    # 0-89 degree: turn left
    # 90 degree: going straight
    # 91-180 degree: turn right 
    steering_angle_radian = (steering_angle + 90.0) / 180.0 * math.pi 
    x1 = int(width / 2)
    y1 = height
    x2 = int(x1 - height / 2 / math.tan(steering_angle_radian))
    y2 = int(height / 2)

    cv2.line(heading_image, (x1, y1), (x2, y2), line_color, line_width)
    heading_image = cv2.addWeighted(frame, 0.8, heading_image, 1, 1)

    return heading_image

        
        
def show_image(img):
    cv2.imshow('ROI Filter', img)
    cv2.waitKey(1)

if __name__ == '__main__':
    try:
        start_node()
    except rospy.ROSInterruptException:
        pass

Clone this wiki locally