Home [데브코스] 7주차 - ROS wraping transformation and lane detection by sliding window
Post
Cancel

[데브코스] 7주차 - ROS wraping transformation and lane detection by sliding window


Warping

사전적으로 뒤틀림이라는 의미로, 영상에서 말하는 warping은 이동, 회전, 크기 변환 등을 말한다. 찌그러진 이미지를 복원할 수도 있다.

ROS 환경 설정

1.패키지 생성

  • sliding_drive 패키지를 생성
1
$ catkin_create_pkg sliding_drive std_msgs rospy


2.src 폴더 아래 girl.png, chess.png 파일 복사


Translation 변환

1.평행이동

  • 이미지를 이동하려면 원래 있던 좌표에 이동시키련느 거리만큼 더하면 된다.
    • x_new = x_old + d1
    • y_new = y_old + d2

이를 변환 행렬로 표현하면

따라서

  • x_old + d1 = 1 * x_old + 0 * y_old + d1
  • y_old + d2 = 0 * x_old + 1 * y_old + d2


dst = cv2.warpAffine(src, matrix, dsize, dst, flags, borderMode, borderValue)

  • src : 원본 이미지, numpy 배열
  • matrix : 2x3 변환 행렬, dtype=float32
  • dsize : 결과 이미지의 크기
  • dst : option,결과 이미지
  • flags : option, 보간법 알고리즘 플래그
    • cv2.INTER_LINEAR : default, 인접한 4개 픽셀 값에 거리 가중치 사용
    • cv2.INTER_NEAREST : 가장 가까운 픽셀 값 아용
    • cv2.INTER_AREA : 픽셀 영역 관계를 이용한 재샘플링
    • cv2.INTER_CUBIC : 인접한 16개 픽셀 값에 거리 가중치 사용
  • borderMode : option, 외곽영역 보정 플래그
    • cv2.BORDER_CONSTANT : 고정 색상 값
    • cv2.BORDER_REPLICATE : 가장자리 복제
    • cv2.BORDER_WRAP : 반복
    • cv2.BORDER_REFLECT : 반사
  • borderValue : option, 외곽영역 보정 플래그가 cv2.BORDER_CONSTANT일경우 사용할 색상 값


  • translation.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import cv2
import numpy as np

img = cv2.imread('girl.png')0
rows, cols = img.shape[0:2]

dx,dy=100,50

mtrx = np.float32([[1,0,dx],[0,1,dy]])

dst = cv2.warpAffine(img,mtrx, (cols+dx, rows+dy))

dst2 = cv2.warpAffine(img,mtrx, (cols+dx, rows+dy),None, cv2_INTER_LENEAR, cv2_BORDER_CONSTANT, (255,0,0))

dst3 = cv2.warpAffine(img,mtrx, (cols+dx, rows+dy),None, cv2_INTER_LENEAR, cv2_BORDER_RELECT)

cv2.imshow("src",img)
cv2.imshow("dst",dst)
cv2.imshow("constant",dst2)
cv2.imshow("reflect",dst3)
cv2.waitKey(0)
cv2.destroyAllWindows()


  • 실행
    1
    
    $ python translation.py
    


2.확대축소

일정 비율로 확대 및 축소

기존 좌표에 특정 값을 곱하면 된다.

  • x_new = a1 * x_old = a1 * x_old + 0 * y_old + 0 * 1
  • y_new = 0 * x_old = a1 * x_old + a2 * y_old + 0 * 1

이를 변환 행렬로 표현하면 다음과 같다.


  • scaling.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
import cv2
import numpy as np

img = cv2.imread('girl.png')0
rows, cols = img.shape[0:2]

mtrx_small = np.float32([[0.5,0,0],[0,0.5,0]])
mtrx_big = np.float32([[2,0,0],[0,2,0]])

dst = cv2.warpAffine(img,mtrx, m_small, (int(height*0.5), int(width*0.5)))

dst2 = cv2.warpAffine(img,mtrx, m_small, (int(height*0.5), int(width*0.5)), None, cv2_INTER_AREA)

dst3 = cv2.warpAffine(img,mtrx, m_big, (int(height*2), int(width*2)))

dst4 = cv2.warpAffine(img,mtrx, m_big, (int(height*2), int(width*2)),None, cv2_INTER_CUBIC)

cv2.imshow("src",img)
cv2.imshow("small",dst)
cv2.imshow("small_inter_area",dst2)
cv2.imshow("big",dst3)
cv2.imshow("big_inter_cubic",dst4)
cv2.waitKey(0)
cv2.destroyAllWindows()

이미지를 축소할 때는 INTER_AREA를 권장하고, 확대할 때는 INTER_LINEAR 또는 CUBIC을 추천한다.



affine 이외에 다른 함수를 이용해서도 크기를 조정할 수 있다.

cv2.resize(src,dsize,dst,fx,fy,interpolation)

  • src : 입력 원본 이미지
  • dsize : 출력 영상 크기, 지정하지 않으면 fx,fy 배율을 적용
  • fx,fy : 크기 배율, dsize를 입력하면 dsize를 우선 적용함
  • interpolation : 보간법 알고리즘
  • dst : 결과 이미지


  • resizing.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cv2
import numpy as np

img = cv2.imread('girl.png')
height, width = img.shape[0:2]

dst1 = cv2.resize(img, (int(width*0.5), int(height*0.5)), interpolation=cv2.INTER_AREA)

dst2 = cv2.resize(img, None, None, 0.5,1.5, interpolation=cv2.INTER_CUBIC)

cv2.imshow("src",img)
cv2.imshow("small",dst)
cv2.imshow("small_inter_area",dst2)
cv2.waitKey(0)
cv2.destroyAllWindows()


  • 실행
    1
    
    $ python resizing.py
    


3.회전

이미지 회전을 위한 변환 행렬식은 다음과 같다.

이 2x2 행렬은 2x3을 요구하는 affine행렬에서는 사용할 수 없고, 사용히려면 2x3행렬로 변환해야 한다. 변환해주는 cv2.getRotationMatrix2D 함수가 존재한다.

회전 행렬을 직접 구현한 코드이다.

  • rotation1.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import cv2
import numpy as np

img = cv2.imread('girl.png')
rows, cols = img.shape[0:2]

d45 = 45.0 * np.pi / 180
d90 = 90.0 * np.pi / 180

mtrx1 = np.float32([[np.cos(d45), -1*np.sin(d45),cols//2],[np.sin(d45),np.cos(d45),-1*rows//4]]) 

mtrx2 = np.float32([[np.cos(d90), -1*np.sin(d90),cols],[np.sin(d90),np.cos(d90),0]])

dst1 = cv2.warpAffine(img,mtrx1,(cols,rows))
dst2 = cv2.warpAffine(img,mtrx2,(cols,rows))

cv2.imshow("src",img)
cv2.imshow("45",dst)
cv2.imshow("90",dst2)
cv2.waitKey(0)
cv2.destroyAllWindows()

컴퓨터는 반시계를 +로 잡기 때문에 시계방향으로 돌리기 위해 -1을 곱했고, 원점을 화면의 중간으로 잡기 위해 마지막 인자를 주었다.


회전 행렬을 함수를 사용하여 구현한 코드이다.

mtrx = cv2.getRotationMatrix2D(center, angle, scale)

  • Center : 회전축 중심 좌표
  • angle : 회전할 각도, 60진법
  • scale : 확대 축소 비율


  • rotation2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import cv2
import numpy as np

img = cv2.imread('girl.png')
rows, cols = img.shape[0:2]

d45 = 45.0 * np.pi / 180
d90 = 90.0 * np.pi / 180

mtrx1 = cv2.getRotationMatrix2D((cols/2,rows/2), 45, 0.5)

mtrx2 = cv2.getRotationMatrix2D((cols/2,rows/2), 90, 1.5)

dst1 = cv2.warpAffine(img,mtrx1,(cols,rows))
dst2 = cv2.warpAffine(img,mtrx2,(cols,rows))

cv2.imshow("src",img)
cv2.imshow("45",dst)
cv2.imshow("90",dst2)
cv2.waitKey(0)
cv2.destroyAllWindows()


Affine 변환

Affine 변환 행렬은 2x3 행렬로 cv2.getAffineTranform 함수를 통해서 얻을 수 있다.

  • affine.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('chess.png')
rows, cols = img.shape[0:2]

pts1 = np.float32([[50,50],[200,50],[50,200]])

pts2 = np.float32([[10,100],[200,50],[100,250]])

mtrx = cv2.getAffineTransform(pts1,pts2)

dst = cv2.warpAffine(img,mtrx,(cols,rows))

plt.subplot(121),plt.imshow(img), plt.title('input'))
plt.subplot(122),plt.imshow(dst), plt.title('output'))
plt.show()


Perspective 변환

원근법을 적용한 변환으로 직선의 성질만 유지가 되고 선의 평행성은 유지되지 않는 변환이다. 즉 원근법이 되어 있는 이미지를 평평하게 펴준다. 반대의 변환도 가능하다.

cv2.getPerspectiveTransform 함수를 통해서 얻을 수 있다. 이동할 4개의 점의 좌표가 필요하다. 결과값은 3x3 행렬이다. cv2.warpPerpective()함수에 변환 행렬값을 적용하면 이미지가 변환된다. 점의 순서는 좌상단부터 시작하여 반시계방향이다.


  • perspective.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('chess.png')
rows, cols = img.shape[0:2]

pts1 = np.float32([[20,20],[20,280],[380,20],[380,280]])

pts2 = np.float32([[100,20],[20,280],[300,20],[380,280]])

cv2.circle(img,(20,20),20, (255,0,0),-1)
cv2.circle(img,(20,280),20, (0,255,0),-1)
cv2.circle(img,(380,20),20, (0,0,255),-1)
cv2.circle(img,(380,380),20, (255,255,255),-1)

mtrx = cv2.getPerspectiveTransform(pts1, pts2)

dst = cv2.warpPerspective(img, mtrx, (400,300))

plt.subplot(121),plt.imshow(img), plt.title('input'))
plt.subplot(122),plt.imshow(dst), plt.title('output'))
plt.show()



이 변환을 차선 검출에 적용한다면 perspective 변환을 적용할 것이다. 이렇게 변환한 영상을 bird eye view라고 한다. 이렇게 변환한다면 차선을 찾기가 편하게 된다.



원근 변환을 이용한 차선 이미지 영상처리

perspective 변환을 통해 차선을 찾는다.

원근 변환과 슬라이딩 윈도우

단계

  1. camera calibration
  2. bird`s eye view
  3. 이미지 임계값 및 이진화
  4. 슬라이딩 윈도우로 차선 위치 파악
  5. 파악된 차선 위치 원본이미지에 표시


1. Camera Calibration(카메라 보정)

카메라는 곡면 렌즈를 사용해서 이미지를 형성하기 때문에 왜곡되어 보인다. 왜곡됨으로 인해 물체의 크기, 모양이 변경되기도 하고, 시야의 위치에 따라 모양이 변경되기도 한다. 따라서 이 왜곡을 변화시켜야 한다.

또한, 렌즈-이미지 센서와의 거리, 렌즈와 이미지 센서가 이루는 각도 등에 의해서도 왜곡이 발생할 수 있다. 실제적으로 보이게 보정하는 것을 camera calibration이라 한다.

체스보드는 정사각형으로 이루어져 있으므로 컴퓨터가 인식하기 쉽다. 그래서 체스판을 사용해서 왜곡을 보정할 수 있다.


1-1. 자동으로 체스판을 찾아서 패턴을 매핑

  • cv2.findchessboardCorners(), cv2.drawchessboardcorners() 함수 사용
    • cv2.findchessboardCorners() : 체스 판의 코너들을 찾는다.
    • cv2.drawchessboardcorners() : 찾은 체스 판의 코너들을 그린다.

1-2. 교정 및 undistortion 계산

  • cv2.calibrateCamera(), cv2.undistort() 함수 사용
    • cv2.calibrateCamera() : camera matrix, 왜곡 계수, 회전/변환 벡터들을 리턴
    • cv2.undistort() : 이미지를 펴서 왜곡을 없어지게 한다.


체스판을 통해 얻어진 calibrate를 통해 차선이미지를 calibration을 진행할 수 있다.

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
int main()
{
	vector<String> img_paths;
	glob("./data/*.bmp", img_paths);

	for (const auto& path : img_paths) {
		Mat img = imread(path);

		if (img.empty()) {
			cout << "Image load failed: " << path << endl;
			continue;
		}

		img_size = img.size();

		Mat gray;
		cvtColor(img, gray, COLOR_BGR2GRAY);

		vector<Point2f> corners;
		bool found = findChessboardCorners(gray, pattern_size, corners);

		if (found) {
			TermCriteria criteria(TermCriteria::Type::EPS | TermCriteria::Type::MAX_ITER, 30, 0.001);
			cornerSubPix(gray, corners, Size(11, 11), Size(-1, -1), criteria);

			obj_pts.push_back(objp);
			img_pts.push_back(corners);

			drawChessboardCorners(img, pattern_size, corners, found);
			imshow("img", img);
			waitKey();
		}
	}

    cv::destroyAllWindows();

	// Calculate intrinsic_matrix, distortion_coeffs
	Mat intrinsic_matrix, distortion_coeffs;

	calibrateCamera(obj_pts, img_pts, img_size, intrinsic_matrix, distortion_coeffs, // 왜곡 계수
		noArray(), noArray(), CALIB_ZERO_TANGENT_DIST | CALIB_FIX_PRINCIPAL_POINT);

    // save them in a XML file
    FileStorage fs("intrinsics.xml", FileStorage::WRITE);
}

저장된 정보는 불러올 수 있다.

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
int main()
{
	// Load intrinsic_matrix, distortion_coeffs from a XML file
	FileStorage fs("intrinsics.xml", FileStorage::READ);
    Mat intrinsic_matrix, distortion_coeffs;
	fs["camera_matrix"] >> intrinsic_matrix;
	fs["distortion_coefficients"] >> distortion_coeffs;

    Mat map1, map2;     
	initUndistortRectifyMap(intrinsic_matrix, distortion_coeffs, noArray(),
		intrinsic_matrix, img_size, CV_16SC2, map1, map2);

    Mat frame;

	while (true) {
		cap >> frame;
		if (frame.empty()) break;

		Mat dst;
		remap(frame, dst, map1, map2, INTER_LINEAR);

        imshow("frame", frame);
		imshow("dst", dst);

		if (waitKey(10) == 27) break;
    }
}



2. Bird’s eye View

왜곡이 없는 차선 이미지를 bird’s eye view 구도로 변환한다. 원근 변환을 하는 것이므로 cv2.getPerspectiveTransform(src,dst)를 사용하여 원근 변환 행렬을 얻는다.


원래 이미지 -\> 조감도 -\> 원래 이미지 의 방식으로 두 번 변환해야 한다.


원근 변환을 위한 4개의 점을 어떻게 얻을까?

위에서 도로를 내려다 볼때 직사각형을 나타내야 하므로 사다리꼴 모양의 4개 점을 찾아야 한다. 색상이나 속성 등을 분석해서 선택한다.


3. 이미지 이진화

임계값을 주어 이진화를 진행한다. 그를 위해 이미지를 grayscale로 변환한 후 이진화한다.

  • 색상 표현
  1. HSV
    • H: 색조, S: 채도, V: 명도
    • 명도가 낮을 수록 검은색, 명도가 낮고 채도가 낮을 수록 흰색
  2. LAB
    • 사람 눈이 감지할 수 있는 색차와 색공간에서 수치로 표현한 색차를 거의 일치시킬 수 있는 색공간
    • L: 명도, A: Red and Green, B: Yellow and Blue
    • 노란색 차선을 인식할 때 B를 사용하면 좋은 성능을 낼 수 있다고 한다.
  3. HLS
    • 색상 균형, HSV의 V를 L로 바꾼 것
    • H: 색조, L: 밝기, S: 채도
    • 밝기가 낮을 수록 검은색, 밝기가 높을 수록 흰색
    • 흰색 차선을 인식할 때 L을 사용하면 좋은 성능을 낸다고 한다.


4. 슬라이딩 윈도우

차선을 찾는 방법

  • 히스토그램

도로 이미지에 보정, 임계값 및 원근 변화을 적용하여 차선이 두드러지는 이진 이미지를 얻은 후 어떤 픽셀이 라인의 일부이고 이게 왼쪽인지 오른쪽인지를 결정해야 한다. 이를 위해 히스토그램을 사용한다. 각 열에 따라 픽셀 개수를 더하면 히스토그램에서 가장 눈에 띄는 두 개의 peak, 즉 높은 막대기 2개가 생성되고, 이 2개의 위치가 차선의 x위치라 할 수 있다.


  • 슬라이딩 윈도우 선 중심 주변에 배치된 슬라이딩 윈도우를 사용해서 프레임 상단까지 선을 찾아 따라가게 하여 곡선을 찾는다. 방금 찾은 히스토그램의 차선 x위치의 시작점, 즉 제일 하단점을 이용하여 첫 윈도우의 위치를 정한다. 그 위도우안의 점들의 평균점을 기준으로 바로 위에 쌓는다. 그렇게 최상단까지 쌓다보면 곡선을 찾을 수 있다.

파악을 한 후 윈도우의 중심을 통해 선을 그리는데, polyfit 함수를 사용해서 2차원을 찾아준다. 이 선은 원본 이미지에 차선을 그리기 위한 선이다. 이 2차원 함수 2개사이의 영역을 polygon 함수를 통해 다각형을 원본 이미지에 그린다.


  • 실행 결과



차선 인식 구현

summary 1. Image read 2. warping (원근 변환) 3. gaussian blur (노이즈 제거) 4. threshold (이진 이미지 변환) 5. histogram (차선 위치 추출) 6. sliding window (윈도우 생성) 7. polyfit (2차 함수 그래프로 차선 그리기) 8. 차선 영역 표시 (원본 이미지에 영역 오버레이) 9. 핸들 조향각 결정 10. 핸들 조종



1. 작업 환경 설정

필요한 파일

  • sliding_drive 패키지에서 작업
  • sliding_find.py
  • car_track1.avi,road_video1.mp4,road_video2.mp4


  • sliding_find.py
  1. 영상 프레임 추출
  2. 카메라 calibration 설정값을 통해 이미지 보정
  3. 원근 변환을 통해 bird’s eye view로 변환
  4. 노이즈 제거, HLS포맷으로 변경, 이진화 처리
  5. 히스토그램을 통해 좌우 차선 시작 위치 파악
  6. 슬라이딩 윈도우를 좌우 9개씩 쌓아 올리기
  7. 9개 윈도우 안의 중앙점을 모두 지나는 2차함수 찾기
  8. 원본 이미지에 차선 영역을 표시하는 영역 오버레이


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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
import cv2, random, math, copy

width = 640
height = 480

cap = cv2.VideoCapture("xycar_track1.mp4")
window_title = 'camera'

# 1.
warp_img_w = 320 # w * 1/2
warp_img_h = 240 # h * 1/2

warpx_margin = 20
warpy_margin = 3

# 2.
nwindows = 9

# 3.
margin = 12

# 4.
minpixel = 5

lane_bin_th = 145

# 5.
warp_src  = np.array([
    [230-warpx_margin, 300-warpy_margin],  
    [45-warpx_margin,  450+warpy_margin],
    [445+warpx_margin, 300-warpy_margin],
    [610+warpx_margin, 450+warpy_margin]
], dtype=np.float32)

# 6.
warp_dst = np.array([
    [0,0],
    [0,warp_img_h],
    [warp_img_w,0],
    [warp_img_w, warp_img_h]
], dtype=np.float32)

# 7.
calibrated = True
if calibrated:
    mtx = np.array([
        [422.037858, 0.0, 245.895397], 
        [0.0, 435.589734, 163.625535], 
        [0.0, 0.0, 1.0]
    ])
    dst = np.array([-0.289296, 0.061035, 0.001786, 0.015238, 0.0])
    cal_mtx, cal_roi = cv2.getOptimalNewCameraMatrix(mtx, dst, (width, height), 1, (width, height))

# 8.
def calibrate_image(frame):
    global width, height
    global mtx, dst
    global cal_mtx, cal_roi
    
    tf_image = cv2.undistort(frame, mtx, dst, None, cal_mtx)
    x, y, w, h = cal_roi
    tf_image = tf_image[y:y+h, x:x+w]

    return cv2.resize(tf_image, (width, height))

# 9.
def warp_image(img, src, dst, size):
    m_to_dst = cv2.getPerspectiveTransform(src, dst)
    m_to_src = cv2.getPerspectiveTransform(dst, src)
    warp_img = cv2.warpPerspective(img, m_to_dst, size, flags=cv2.INTER_LINEAR)

    return warp_img, m_to_dst, m_to_src

# 10.
def warp_process_image(img):
    global nwindows
    global margin
    global minpixel
    global lane_bin_th

    # 11.
    blur = cv2.GaussianBlur(img,(5, 5), 0)

    # take the value, only B(yellow and blue) to detect yellow line easily
    # but, in video, there is no yellow line, so in this code, do not take B
    # _, _, B = cv2.split(cv2.cvtColor(blur, cv2.COLOR_BGR2LAB))

    # 12.
    _, L, _ = cv2.split(cv2.cvtColor(blur, cv2.COLOR_BGR2HLS))

    # 13.
    _, lane = cv2.threshold(L, lane_bin_th, 255, cv2.THRESH_BINARY)

    # 14.
    histogram = np.sum(lane[lane.shape[0]//2:,:], axis=0)

    # 15.
    midpoint = np.int(histogram.shape[0]/2)

    # 16.
    leftx_current = np.argmax(histogram[:midpoint])

    # 17.
    rightx_current = np.argmax(histogram[midpoint:]) + midpoint

    # 18.
    window_height = np.int(lane.shape[0]/nwindows)
    nz = lane.nonzero()

    left_lane_inds = []
    right_lane_inds = []
    
    lx, ly, rx, ry = [], [], [], []

    # 19.
    out_img = np.dstack((lane, lane, lane))*255

    for window in range(nwindows):

        # 20.
        win_yl = lane.shape[0] - (window+1)*window_height
        win_yh = lane.shape[0] - window*window_height

        # 21.
        win_xll = leftx_current - margin
        win_xlh = leftx_current + margin
        win_xrl = rightx_current - margin
        win_xrh = rightx_current + margin

        # 22.
        cv2.rectangle(out_img,(win_xll,win_yl),(win_xlh,win_yh),(0,255,0), 2) 
        cv2.rectangle(out_img,(win_xrl,win_yl),(win_xrh,win_yh),(0,255,0), 2)

        # 23.
        good_left_inds = ((nz[0] >= win_yl)&(nz[0] < win_yh)&(nz[1] >= win_xll)&(nz[1] < win_xlh)).nonzero()[0]
        good_right_inds = ((nz[0] >= win_yl)&(nz[0] < win_yh)&(nz[1] >= win_xrl)&(nz[1] < win_xrh)).nonzero()[0]

        left_lane_inds.append(good_left_inds)
        right_lane_inds.append(good_right_inds)

        # 24.
        if len(good_left_inds) > minpixel:
            leftx_current = np.int(np.mean(nz[1][good_left_inds]))
        if len(good_right_inds) > minpixel:        
            rightx_current = np.int(np.mean(nz[1][good_right_inds]))

        # 25.
        lx.append(leftx_current)
        ly.append((win_yl + win_yh)/2)
        rx.append(rightx_current)
        ry.append((win_yl + win_yh)/2)

    # 26.
    left_lane_inds = np.concatenate(left_lane_inds)
    right_lane_inds = np.concatenate(right_lane_inds)

    #left_fit = np.polyfit(nz[0][left_lane_inds], nz[1][left_lane_inds], 2)
    #right_fit = np.polyfit(nz[0][right_lane_inds] , nz[1][right_lane_inds], 2)

    # 27.
    lfit = np.polyfit(np.array(ly),np.array(lx),2)
    rfit = np.polyfit(np.array(ry),np.array(rx),2)

    # 28.
    out_img[nz[0][left_lane_inds], nz[1][left_lane_inds]] = [255, 0, 0]
    out_img[nz[0][right_lane_inds] , nz[1][right_lane_inds]] = [0, 0, 255]
    cv2.imshow("viewer", out_img)
    
    #return left_fit, right_fit
    return lfit, rfit

def draw_lane(image, warp_img, m_to_src, left_fit, right_fit):
    global width, height
    ymax = warp_img.shape[0]
    ploty = np.linspace(0, ymax - 1, ymax)

    # 29.
    color_warp = np.zeros_like(warp_img).astype(np.uint8)

    # 30.
    left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
    right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]

    # 31.
    pts_left = np.array([np.transpose(np.vstack([left_fitx, ploty]))])
    pts_right = np.array([np.flipud(np.transpose(np.vstack([right_fitx, ploty])))]) 
    pts = np.hstack((pts_left, pts_right))

    # 31.
    color_warp = cv2.fillPoly(color_warp, np.int_([pts]), (0, 255, 0))
    newwarp = cv2.warpPerspective(color_warp, m_to_src, (width, height))

    return cv2.addWeighted(image, 1, newwarp, 0.3, 0)

def start():
    global width, height, cap

    _, frame = cap.read()
    while not frame.size == (width*height*3):
        _, frame = cap.read()
        continue

    print("start")

    while cap.isOpened():
        
        _, frame = cap.read()

        image = calibrate_image(frame)
        warp_img, m_to_dst, m_to_src = warp_image(image, warp_src, warp_dst, (warp_img_w, warp_img_h))
        left_fit, right_fit = warp_process_image(warp_img)
        lane_img = draw_lane(image, warp_img, m_to_src, left_fit, right_fit)

        cv2.imshow(window_title, lane_img)

        cv2.waitKey(1)

if __name__ == '__main__':
    start()

1.기존의 크기보다 1/2 크기의 출력 영상에 대한 크기

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
2.슬라이딩 윈도우 개수

3.슬라이딩 윈도우 넓이

4.선을 그리기 위해 최소한 있어야할 점의 개수, lane_binary_threshold(선을 판단하기 위한 이진화의 임계값)

5.변환 전 좌표 행렬

6.변환 후 좌표 행렬

7.자이카 카메라로 촬영된 동영상이므로 자이카 카메라의 calibration보정값을 사용한다.

8.위에서 구한 보정 행렬값을 적용하여 이미지를 반듯하게 수정하는 함수

9.변환 전과 후의 4개 점 좌표를 전달받아서 이미지를 원근 변환 처리하여 새로운 이미지를 만든다.

10.이미지를 처리하는 함수

11.5x5 kernel filter을 적용한 가우시안 블러링

12.흰색선을 잘 찾기 위해 HLS로 변환한 후 L값만 추출, 영상에서 노란색선이 없어서 LAB으로 변환은 하지 않았으나 있다면 LAB로 변환하여 B값만 추출한다.

13.임계값을 이용하여 이진화한다. lane은 이진화된 조감도 이미지의 행렬(320,240)일 것이다.

14.히스토그램의 x,y축을 정의한다. x축 : 픽셀의 x좌표값, y축 : 특정 x좌표값을 갖는 모든 흰색 픽셀의 개수

15.x축을 반으로 나누어 왼쪽 차선과 오른쪽 차선을 구분한다.

16.왼쪽 절반 구역에서 흰색 픽셀의 개수가 가장 많은 위치를 슬라이딩 윈도우 중심 좌표로 두어 왼쪽 윈도우의 시작 위치로 잡는다. 시작 위치에서만 이 값을 사용하고 나머지는 아래의 평균값을 사용한다.

17.픽셀의 개수가 가장 많은 위치를 오른쪽 윈도우의 시작 위치를 잡는다.

18.1개의 윈도우의 높이를 구함

19.

20.각 윈도우의 상단, 하단 y좌표

21.윈도우 넓이를 구하는데, win_xlh - win_xll = 윈도우 가로 값, xrh-xrl = 윈도우 세로 값

22.윈도우마다 사각형 그리기, (dst, 좌상단 좌표,우하단 좌표,색상,두께)

23.슬라이딩 윈도우 박스 하나 안에 있는 흰색 픽셀의 x좌표들을 모두 모든다. 왼쪽 오른쪽은 따로 계산한다. zero가 아닌 것들에 대한 점들을 다 구하고 x좌표만 추출한다.

24.위에서 구한 x좌표 리스트에서 흰색점이 5개 이상인 경우에 한해서 x좌표의 평균값을 구한다. 이 값을 위에 쌓을 슬라이딩 윈도우의 중심점으로 사용한다.

25.슬라이딩 윈도우의 중심점을 저장한다. 이는 2차함수 그릴 때 사용한다.

26.

27.슬라이딩 윈도우의 중심 좌표 9개를 가지고 2차함수를 만들어낸다.

28.기존 하얀색 차선 픽셀을 왼쪽 오른쪽 각각 파란색과 빨간색으로 색상 변경

29.zeros matrix, warp_img 크기의 0으로 된 행렬을 생성한다.

30.

31.구해놓은 2차함수의 상수들을 이용해서 사다리꼴 이미지 외곽선 픽셀 좌표를 계산한다.

32.사다리꼴 이미지를 칼라로 그리고 거꾸로 원근 변환하여 원본 이미지와 오버레이 한다. >



ROS에서 슬라이딩 윈도우 기반 차선 인식 주행

  • sliding_drive.launch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<launch>
  <!-- 노드 실행 : 자이카 모터 제어기 구동 -->
  <include file="$(find xycar_motor)/launch/xycar_motor.launch" />

  <!-- 노드 실행 : 자이카 카메라 구동 -->
  <node name="usb_cam" output="screen" pkg="usb_cam" type="usb_cam_node">
    <param name="video_device" value="/dev/videoCAM" />
    <param name="autoexposure" value="false" />
    <param name="exposure" value="50" />
    <param name="image_width" value="640" />
    <param name="image_height" value="480" />
    <param name="pixel_format" value="yuyv" />
    <param name="camera_frame_id" value="usb_cam" />
    <param name="io_method" value="mmap" />
  </node>

  <!-- 노드 실행 : 슬라이딩 윈도우 기반 주행 프로그램인 sliding_drive.py 실행 -->
  <node name="auto_drive" output="screen" pkg="sliding_drive" type="sliding_drive.py" />

</launch>



  • sliding_drive.py

작업 흐름도

  1. 카메라 노드가 보내는 토픽에서 영상 프레임 추출
  2. 카메라 calibration 설정값으로 이미지 보정
  3. 원근 변환으로 차선 이미지를 Bird’s eye view로 변환
  4. openCV영상처리
    • Gaussian blur (노이즈 제거)
    • cvtColor (BGR2HLS)
    • threshold (이진화 처리)
  5. 히스토그램을 사용해서 좌우 차선의 시작 위치 파악
  6. 슬라이딩 윈도우를 좌우 9개씩 쌓아 올리기
  7. 왼쪽 오른쪽 차선의 위치 찾기
  8. 적절한 조향값 계산하고, 모터 제어 토픽 발행


차선에 대한 2차함수가 쌓여 있고, 윈도우가 총 9개 있을 때, 내가 보고자 하는 윈도우의 번호에 있는 x좌표를 구해서 두개의 평균을 구한다. 그리고 그것과 이미지의 중앙값을 비교하여 조향각을 결정한다.

/usb_cam -\> /usb_cam/image_raw/ -\> /sliding_drive -\> /xycar_motor -\> /xycar_motor

This post is licensed under CC BY 4.0 by the author.