본문 바로가기
Robot/ROS

킴 - lane detector

by 9루트 2022. 3. 15.

결과

자율주행모드(Q)로 주행하니 커브길을 다음과 같이 SLIDE WINDOW로 차선을 인식하여 갈 수 있다.

특히 커브길에는 avg_val이 0이 되도록 PID 제어기에서 P GAIN이 동작한다.

여기서는 -0.1를 게인으로 설정했다.

커브 길을 지난 다음에는 차선 정 가운데에 위치하기 위해 dist_steer를 0으로 만들도록 코드가 작동한다.

server.py

openCV 가 아닌 ROS 자체에서 트랙바 기능을 코드로 구현

 

 

dynamic reconfigure

cfg : 트랙바에 추가할 내용

e_stop.py -> lidar_e_stop.cfg으로 값이 들어간다.

 

lidar2D로 바꿔서 출력하라

scan:=lidar2D

 

 

steering_publisher.py

바퀴로 차선 인식하는 코드

 

 

lidar.py

라이다로 차선 인식하는 코드

 


17. Lane Detection어 강의 교재

1. 기본 용어

픽셀

해상도:픽셀의 수

프레임: 동영상의 화면 하나 하나

FPS: 영상이 바뀌는 속도 , 화면의 부드러움


2. 차선 인식 과정

1) 차선 추출

ROI(REGION OF INTEREST)를 우선 정해서 필요한 영역을 지정한다.

 

2) Color Space Conversion

RGB, HSV, HLS, YUV 등 이미지의 스페이스를 선택한다.

H : 색상

S : 채도 (높을 수록 원색에 가까고 낮을 수록 흰색에 가깝다.) - 흰색이나 검은색(무채색)은 어둡게, 하늘색, 노란색은 밝게 나온다.

V : 밝기 (높을 수록 밝은 초록색이 된다.) RGB와 가장 가까운 결과가 나오며 어두우면 어둡게 밝으면 밝게 나온다.

 

3) Image Threshold(Binary Image)

이미지를 이진화 시켜서 0-255 -> 0/1로만 바꾼다. 차선은 1, 그 외는 0으로 만들어 버린다.

 

4) Perspective Transform(Bird Eye View)

원근법을 일직선으로 바꾼다. 위쪽에서 차선을 본 듯한 이미지로 변경한다.

 

5) Sliding Window (옵션 항목)

곡선의 차선 추출

특정 Window를 정해서 순차적으로 이미지를 탐색하는 방식을 통해 차선 검출

이진화된 이미지 상에서 파란 창이 위에서부터 쭉 탐색해서 흰색 부분이 일정 이상이면 차선 부분만을 검출한다.

Window의 크기나, 한번에 몇 개의 윈도우가 지나가도록 할 지 결정


3. 차선 기반의 주행을 하는 방법

1) 차선을 검출한다.

(1) 거리 계산

Bird Eye View에서 왼, 오른쪽 차선과 차의 중심 선(카메라 렌즈의 기준)의 거리를 픽셀 단위로 계산할 수 있다.로주

 

(2) 기울기 계산

중심선을 0도로 두고 양쪽 차선의 기울기 계산

 

(3) 거리값과 기울기값을 이용하여 제어

왼쪽 거리와 오른쪽 거리의 부호를 반대로 두어

더했을 때 0이 되면 중앙이므로 주행하도록 한다.

기울기값도 (왼쪽 기울기 + 오른쪽 기울기) = 0 이 되도록 해야한다.

예를 들면 왼쪽 기울기가 30도 오른쪽 기울기가 50도라면 40도 정도 회전하도록 해야한다.

 

 

다이나믹 컨피규어

srv = Server(image_processingConfig, self.configure_cb)

 

시뮬레이션에서의 이미지를 받아온다.

        rospy.Subscriber("/image_jpeg/compressed", CompressedImage, self.Image_CB)

 

        self.frame_pub = rospy.Publisher("/wecar/frame", Image, queue_size=1)
        self.bev_pub = rospy.Publisher("/wecar/bev_frame", Image, queue_size=1)
        self.color_detect_pub = rospy.Publisher("/wecar/color_detected_frame", Image, queue_size=1)
        self.bev_result_pub = rospy.Publisher("/wecar/bev_result_frame", Image, queue_size=1)
        self.result_pub = rospy.Publisher("/wecar/result_frame", Image, queue_size=1)
        self.steering_pub = rospy.Publisher("/wecar/steering_angle", Float32, queue_size=5)

각 이미지 변환 과정을 보여준다.

 

/wecar/steering_angle 은 angular.z를 의미한다.

 

 

점 4개를 찍기

    def __init__(self):
        self.TARGET_PTS = np.float32([[0, 0], [640, 0], [0, 480], [640, 480]])

 

    def configure_cb(self, config, level):
        self.ORIGINAL_PTS = np.float32([[config.left_upper_x_pos, config.left_upper_y_pos],\
                                        [config.right_upper_x_pos, config.right_upper_y_pos],\
                                        [config.left_lower_x_pos, config.left_lower_y_pos],\
                                        [config.right_lower_x_pos, config.right_lower_y_pos]])

각 4개의 점을 (x, y)로 찍어낸다.

 

 

 

여기에선 HLS를 사용한다.

        self.YELLOW_LANE_LOW = np.array([config.yellow_h_low, config.yellow_l_low, config.yellow_s_low]) 
        self.YELLOW_LANE_HIGH = np.array([config.yellow_h_high, config.yellow_l_high, config.yellow_s_high])
        self.WHITE_LANE_LOW = np.array([config.white_h_low, config.white_l_low, config.white_s_low]) 
        self.WHITE_LANE_HIGH = np.array([config.white_h_high, config.white_l_high, config.white_s_high])

 

 

여기까지 실시간으로 변경되는 다이나믹 reconfigure 값이다.

 

 


실습 시작

 

시뮬레이션을 연다.

kw-cobot@kwcobot-HGDT-Series:~$ roslaunch scout_sim_launch simulater.launch 

 

1. 차선 일직선화 하기

플러그인 - 이미지뷰 - compressed(왼쪽)

플로그인 - 이미지뷰 - frame(오른쪽)

플로그인 - configuration - dynamic reconfigure (아래)

위 처럼 환경을 조성해준다.

 

 

 

이렇게 차선을 일직선으로 펴준다.

 

 

이후 위 코드에 값을 넣어준다.

 

 

 

lane_detection.launch

<?xml version="1.0"?>
<launch>
   <node name="steering_node" pkg="lane_detection" type="steering_publisher.py" output="screen">
      <rosparam file="$(find lane_detection)/params/lane_detection.yaml" command="load" />
   </node>
   <node name="object_detector" pkg="lane_detection" type="e_stop.py" output="log">
      <rosparam file="$(find lane_detection)/params/e_stop.yaml" command="load" />
   </node>
<!--    <node name="control_wecar" pkg="lane_detection" type="control_wecar.py" output="screen">
      <rosparam file="$(find lane_detection)/params/control.yaml" command="load" />
   </node> -->
</launch>

주석 처리 해준다.

 

불러오면서 차선의 일직선화가 실행되도록 한다.

kw-cobot@kwcobot-HGDT-Series:~$ roslaunch lane_detection lane_detection.launch

 

 

 

HLV 설정 기존에 설정했으므로 변경할 필요 없다.

 

 

2. 이미지 콜백함수

이미지 타입 변경해주기

frame = self.cvbridge.compressed_imgmsg_to_cv2(img, "bgr8")

오픈시브이에서 처리하는 이미지를 ROS에서 처리 가능하도록 바꿔준다.

 

 

BEV = BIRD EYE VIEW

    def bird_eye(self, frame):
        result = cv2.warpPerspective(frame, self.matrix, (640, 480))
        return result

프레임을 matrix를 가지고 640x480 이미지로 변경해준다. 탑 뷰 이미지 로 변환해준다.

    def configure_cb(self, config, level):
        self.ORIGINAL_PTS = np.float32([[config.left_upper_x_pos, config.left_upper_y_pos],\
                                        [config.right_upper_x_pos, config.right_upper_y_pos],\
                                        [config.left_lower_x_pos, config.left_lower_y_pos],\
                                        [config.right_lower_x_pos, config.right_lower_y_pos]])
        self.YELLOW_LANE_LOW = np.array([config.yellow_h_low, config.yellow_l_low, config.yellow_s_low]) 
        self.YELLOW_LANE_HIGH = np.array([config.yellow_h_high, config.yellow_l_high, config.yellow_s_high])
        self.WHITE_LANE_LOW = np.array([config.white_h_low, config.white_l_low, config.white_s_low]) 
        self.WHITE_LANE_HIGH = np.array([config.white_h_high, config.white_l_high, config.white_s_high])
        self.matrix = cv2.getPerspectiveTransform(self.ORIGINAL_PTS, self.TARGET_PTS)
        self.inv_matrix = cv2.getPerspectiveTransform(self.TARGET_PTS, self.ORIGINAL_PTS)
        self.REF_LINE_X = config.ref_line_x
        return config

 

 

 

 

color_detected_frame

    def color_detect(self, img):
        # print(self.YELLOW_LANE_LOW)
        # print(self.YELLOW_LANE_HIGH)
        hls = cv2.cvtColor(img, cv2.COLOR_BGR2HLS)
        mask_white = cv2.inRange(hls, self.WHITE_LANE_LOW, self.WHITE_LANE_HIGH)
        mask_yellow = cv2.inRange(hls, self.YELLOW_LANE_LOW, self.YELLOW_LANE_HIGH)
        mask = cv2.bitwise_or(mask_white, mask_yellow)
        # cv2.imshow("hls", hls)
        # cv2.imshow("mask_yellow", mask_yellow)
        # cv2.imshow("mask_white", mask_white)
        # cv2.imshow("h", hls[:,:,0])
        # cv2.imshow("s", hls[:,:,1])
        # cv2.imshow("v", hls[:,:,2])
        # cv2.waitKey(1)
        # return mask_white
        # return mask_yellow
        return mask

하얀색과 노란색을 모두 검출되도록 한다.

 

 

 


슬라이딩 작업

bev_result_frame

    def sliding_left(self, img):
        left_list = []
        '''
        row: starting from y=179 to y=460, moving by 40
        col: starting from x=19 to x=300, moving by 5
        '''
        for j in range(179, img.shape[0] - 20, 40): 
            j_list = []
            for i in range(19, int(img.shape[1]/2) - 20, 5):
                num_sum = np.sum(img[j - 19:j + 21, i - 19:i + 21]) #window size is 20*20
                if num_sum > 100000: #pick (i,j) where its num_sum is over 100000
                    j_list.append(i)
            try:
                len_list = [] 
                #cluster if a gap between elements in the list is over 5
                result = np.split(j_list, np.where(np.diff(j_list) > 5)[0] + 1) 
                for k in range(0, len(result)):
                    len_list.append(len(result[k])) #append the lengths of each cluster
                largest_integer = max(len_list)
            
                for l in range(0, len(result)):
                    if len(result[l]) == largest_integer: 
                        avg = int(np.sum(result[l]) / len(result[l])) #average
                        left_list.append((avg, j)) #avg points of left side 
            except:
                continue
        return left_list

100000 = 255 * 400을 의미한다.

40 * 40 * 255 중에서 400 *255만 흰색이면 윈도우 상에 표시하는 코드

        self.sliding_list_left = self.sliding_left(colored_image)
        self.sliding_list_right = self.sliding_right(colored_image)
        print("left : {}".format(self.sliding_list_left))
        print("right : {}".format(self.sliding_list_right))

터미널을 보면 슬라이딩 점이 출력된다.

left : [(126, 179), (126, 219), (126, 259), (126, 299), (121, 339), (121, 379), (121, 419), (121, 459)]
right : [(485, 179), (485, 219), (487, 259), (487, 299), (490, 339), (490, 379), (490, 419), (490, 459)]

 

 

제어해주기산

거리 계산

    def steering(self, frame, sliding_list_left, sliding_list_right):
        x_left = []
        x_right = []
        self.REF_LINE_X = 320
        if len(sliding_list_left) == 0:
            left_distance = 0
            left_x = 0
        else:
            left_x = sliding_list_left[-1][0]
            left_distance = self.REF_LINE_X - sliding_list_left[-1][0] #should be positive
            # print("slidng_lisjlklljkljkljkljkljkljkljljukljkljkljklkjlt_left[-1][0] : {}".format(sliding_list_left[-1][0]))
        if len(sliding_list_right) == 0:
            right_distance = 0
            right_x = 0
        else:
            right_x = sliding_list_right[-1][0]
            right_distance = self.REF_LINE_X - sliding_list_right[-1][0] #should be negative
            # print("sliding_list_right[-1][0] : {}".format(sliding_list_right[-1][0]))
        # print("left_distance : {}".format(left_distance))
        # print("right_distance : {}".format(right_distance))
        #extract x coordinates from sliding_list_left
        if len(sliding_list_left) == 0:
            left_distance = 0
            left_x = 0

차선이 안 잡히면 0만큼 이동해라

맨 아래에 있는, 즉 가장 차에 가까이 있는 점을 [-1][0]으로 처리

레퍼런스 라인을 기준으로 왼쪽, 오른쪽을 나눴다.

 

distance 두 값을 더하면 0이 되도록 제어한다.

 

 

기울기 계산

        left_diff_arr = np.diff(x_left)
        divider = len(left_diff_arr)
        if divider == 0:
            divider = 1
        left_diff_sum = np.sum(left_diff_arr)
        left_avg = int(left_diff_sum/divider)
        
        # print("left_diff_arr{}".format(left_diff_arr))
        # print("left_diff_sum{}".format(left_diff_sum))
        # print("left_avg{}".format(left_avg))

        #remove inappropriate cases 
        for i in range(0, len(left_diff_arr)): 
            if (left_diff_arr[i] > (left_avg +80)) or (left_diff_arr[i] < (left_avg - 80)): #the value 80 can be modified
                self.sliding_list_left = []
                left_diff_sum = 0

왼쪽  기울기값의 평균와 오른쪽 기울기 값의 평균을 구한다.

 

        #remove inappropriate cases 
        for i in range(0, len(left_diff_arr)): 
            if (left_diff_arr[i] > (left_avg +80)) or (left_diff_arr[i] < (left_avg - 80)): #the value 80 can be modified
                self.sliding_list_left = []
                left_diff_sum = 0

차선은 연속적이므로 평균 기울기에 대해 편차가 크지 않아야 한다.

즉, 차선 기울기에 너무 큰 변화량이 있으면 버리겠다는 코드이다.

        if left_distance == 0 and right_distance != 0:
            # dist_steer = right_distance - right_dist_th 
            dist_steer = 150

왼쪽 차선은 안 보이고, 오른쪽 차선이 보일 때 핸들을 꺾도록 제어한다.

 

왼, 오른쪽 차선 모두 보이는 일반적인 상황일 때,

       else:
            dist_steer = right_distance + left_distance
        avg_val = float((left_diff_sum + right_diff_sum)/2) *-1
        if avg_val > 100:
            avg_val = 100
        elif avg_val < -100:
            avg_val = -100
        avg_val /= 100
        avg_val *= 15
        print("avg_val : {}".format(avg_val * (-0.6)))
        print("dist_steer : {}".format(dist_steer))
        self.steering_pub.publish(avg_val*(-1.2) + dist_steer * 0.07)
        # self.steering_pub.publish(avg_val*(-0.5))
        # self.steering_pub.publish(dist_steer * 0.06)
    
        return frame #return frame is for visualization

왼, 오 각각의 기울기 평균값의 평균값을 구한다.

        if avg_val > 100:
            avg_val = 100
        elif avg_val < -100:
            avg_val = -100

범위를 벗어나지 않도록 정리해준다.

 

 

 

 

Result_frame

윈도우를 원을 이용하여 점으로 찍어주기.

            # 2.list points
            for i, pos in enumerate(self.sliding_list_left):
                cv2.circle(bird_eye_image, pos, 5,(255,i*20,0), -1)
            for i, pos in enumerate(self.sliding_list_right):
                cv2.circle(bird_eye_image, pos, 5,(i*20,255,0), -1)

 

        avg_val /= 100
        avg_val *= 15
        print("avg_val : {}".format(avg_val * (-0.6)))

dist_steer 음수이면 왼쪽 차선과 가까운 상태, 양수이면 오른쪽 차선과 가까운 상태이다

양수 --> 오른쪽 차선과 가까운 상태 --> 좌회전이 필요 (angualr.z가 양수)

음수 --> 왼쪽 차선과 가까운 상태 --> 우회전이 필요

 

 

avg_val은 우회전이 필요할 때 음수, 좌회전이 필요할 때는 양수가 나온다.

좌회전 -- > angular.z 양수, avg_val 음수

 

 

 

 

kw-cobot@kwcobot-HGDT-Series:~$ rostopic echo /wer/steering_angle 
---
data: 2.02999997139
---
data: 2.02999997139
---
data: 2.02999997139
---
data: 2.02999997139
---
data: 2.02999997139
---
data: 2.02999997139
---
data: 2.02999997139
---
data: 2.02999997139
---
data: 2.02999997139

 

 

★ steering_angle을 구독해서 cmd_vel을 발행하는 코드 짜기

controller.py

#! /usr/bin/env python
# -*- coding: utf-8 -*-

# /wecar/steering_angle Topic Subscribe (std_msgs/Float32)
# /cmd_vel Topic Publish (geometry_msgs/Twist)
# steering_angle을 구독해서 cmd_velf로  발행하는 코드 짜기

## /wecar/steering_angle --> Twist.angular.z
## Twist.angular.z -->  /wecar/steering_angle

import rospy
from geometry_msgs.msg import Twist
from std_msgs.msg import Float32


class RobotController(object):
    def __init__(self):
        rospy.Subscriber("wecar/steering_angle", Float32, self.steering_angle_callback)
        self.drive_pub = rospy.Publisher("cmd_vel", Twist, queue_size=1)
        rospy.Timer(rospy.Duration(0.1), self.timer_callback)
    
    def steering_angle_callback(self, data):
        self.z_data = data.data
        # twist_data = Twist()
        # twist_data.linear.x = 0.3
        # twist_data.angular.z = data.data
        # self.drive_pub.publish(twist_data)
        # rospy.loginfo("data published")
        
    def timer_callback(self, event):
        twist_data = Twist()
        twist_data.linear.x = 0.3
        twist_data.angular.z = self.z_data
        self.drive_pub.publish(twist_data)
        rospy.loginfo("{} data published".format(self.z_data))
    
    
def run():
    rospy.init_node("robot_controller")
    robot_controller= RobotController()
    rospy.spin()
    
if __name__ == "__main__":
    run()

 

 

result_frame

다시 원본 형태로 바꿔준다.

 

 

 

자동차 제어하기 위한 튜닝 작업

        print("avg_val : {}".format(avg_val))
        print("dist_steer : {}".format(dist_steer))
        self.steering_pub.publish(avg_val*(-0.012) + dist_steer * 0.01)
        # self.steering_pub.publish(avg_val*(-0.5))
        # self.steering_pub.publish(dist_steer * -0.001)
    
        return frame #return frame is for visualization

 

controller.py에서

steering_publisher.py의 streering_angle값을 받아 cmd_vel의 augular.z 값을 변경시킨다.

즉, 시뮬레이터 상의 핸들이 자동으로 차선을 따라 가게 방향을 조정하게 해준다.

rospy.loginfo("{} data published".format(self.z_data))

 

 

 

속도를 올리면 gain값을 줄이는 방향으로 튜닝해야 한다.

 

차선이 두 개가 문제가 생김

-> 차선 한 쪽이 bev_result_frame 상에서 없어지지 않게 해야한다.

      #self.TARGET_PTS = np.float32([[0, 0], [640, 0], [0, 480], [640, 480]])
        self.TARGET_PTS = np.float32([[n, m], [640-n, m], [n, 480-m], [640-n, 480-m]])

으로 바꿔주고

코너 부분에서 avg_vel의 gain 값을 높여준다.

 

 

속도 2.0부터는 힘들다.

카메라 프레임이 속도가 못 따라가는 건 아닐까 싶다. 센서의 문제라고 생각한다.

 

 

터미널 상황

kw-cobot@kwcobot-HGDT-Series:~$ roslaunch lane_detection lane_detection.launch 
kw-cobot@kwcobot-HGDT-Series:~/catkin_ws/src/lane_detection/scripts$ rosrun lane_detection controller.py 
kw-cobot@kwcobot-HGDT-Series:~$ rqt
kw-cobot@kwcobot-HGDT-Series:~$ roslaunch scout_sim_launch simulater.launch 

 

 

결과물

steering_publisher.py

self.steering_pub.publish(avg_val*(-0.1) + dist_steer * 0.001)

 

'Robot > ROS' 카테고리의 다른 글

20강 ROS SLAM  (0) 2022.03.16
민 - ar_maker  (0) 2022.03.15
민 - ar_maker  (0) 2022.03.14
민 - [20강]  (0) 2022.03.14
킴 [이미지 처리]  (0) 2022.03.10