안녕하세요! 너굴맨 입니다.
이번 OpenCV과정은 내용이 길어서 저번 시간에는 기본셋업, 그리고 카메라 테스트까지 했습니다.
이번 시간에는 자율주행 전에 OpenCV를 조금 더 알아보겠습니다.
이번에는 '라인트레이서'
즉, 카메라를 통해서 자동차가 한 라인을 따라가게 만드는 방법을 시도해보겠습니다.
OpenCV의 무게중심, 이미지 처리 등등의 기능을 통해서 딥러닝 없이 사용해 볼 수 있습니다.
실험환경 구성
저 같은 경우에는 검은색 하드보드지(400원)에 하얀색 테이프를 부착해서 테스트 했습니다.
이전에 실험할 때는, 바닥에 테이프를 붙였었는데, 테이프를 떼는 과정에서 바닥 장판이 까지는 바람에;;
여러분도 하실거면 따로 만드시는 것을 추천드립니다.
그리고 이후에 다시 설명을 하겠지만, 라인과 바닥은 색깔차이가 명확하게 차이나는 것이 좋습니다.
위와같이 하얀색 검은색이 제일 좋습니다.
왜냐하면 이후 빛이나 색차이로 구별하게 되는데 ,위와 같이 베이지색의 바닥에 하얀 테이프를 깔면
인식을 잘 하지 못할 가능성이 큽니다.. ㅠㅠ
테스트 시작
화면 자르기
import cv2 #OpenCV를 사용하기위해 import해줍니다. import numpy as np #파이썬의 기본 모듈중 하나인 numpy def main(): camera = cv2.VideoCapture(-1) #카메라를 비디오 입력으로 사용. -1은 기본설정이라는 뜻 camera.set(3,160) #띄울 동영상의 가로사이즈 160픽셀 camera.set(4,120) #띄울 동영상의 세로사이즈 120픽셀 while( camera.isOpened() ): #카메라가 Open되어 있다면, ret, frame = camera.read() #카메라를 읽어서 image값에 넣습니다. frame = cv2.flip(frame,-1) #카메라 이미지를 flip, 뒤집습니다. -1은 180도 뒤집는다 cv2.imshow( 'normal' , frame) #'normal'이라는 이름으로 영상을 출력 crop_img =frame[60:120, 0:160] #세로는 60~120픽셀, 가로는 0~160픽셀로 crop(잘라냄)한다. cv2.imshow('crop' ,crop_img) #crop이라는 이름으로 영상을 출력 if cv2.waitKey(1) == ord('q'): #만약 q라는 키보드값을 읽으면 종료합니다. break cv2.destroyAllWindows() #이후 openCV창을 종료합니다. if __name__ == '__main__': main() |
위의 코드를 준비해 줍니다.
import numpy as np |
이전 시간과 다른 부분은, 이번부터는 이미지처리를 위해서 파이썬의 기본 모듈중 하나인 numpy를 사용합니다.
저는 이전에 OpenCV를 설치할때 한번에 설치했지만, 설치를 안했다면, 'pip install numpy==1.20.2' 와 같이 설치해줍니다. 만약 numpy오류가 발생하면 OpenCV와 numpy의 버전차이 문제이므로 맞춰 설치해주시면 됩니다.
crop_img =frame[60:120, 0:160] |
그리고 이번에 crop_img를 하나 설정해 주었습니다.
왼쪽 이미지가 'normal'이라고 출력한 120x160의 영상이고 오른쪽이 'crop_img' 입니다.
위와 같이 해준 이유는, crop_img같이 트랙, 즉 라인만 보이는 것이 인식하는데 매우 좋기 때문입니다.
왼쪽과 같은 영상을 그대로 하게되면 주변 배경이 너무 많이 보이면 인식에 문제가 생깁니다.
그래서 frame[60:120, 0:160] 명령어를 이용합니다.
이미지는 위에서부터 0~ 픽셀 입니다. 그러므로 60:120, 60~120픽셀을 자르면 아래쪽의 절반으로 잘려진
오른쪽과 같은 화면을 얻을 수 있습니다.
이미지 처리
import cv2 #OpenCV를 사용하기위해 import해줍니다. import numpy as np #파이썬의 기본 모듈중 하나인 numpy def main(): camera = cv2.VideoCapture(-1) #카메라를 비디오 입력으로 사용. -1은 기본설정이라는 뜻 camera.set(3,160) #띄울 동영상의 가로사이즈 160픽셀 camera.set(4,120) #띄울 동영상의 세로사이즈 120픽셀 while( camera.isOpened() ): #카메라가 Open되어 있다면, ret, frame = camera.read() #비디오의 한 프레임씩 읽습니다. ret값이 True, 실패하면 False, fram에 읽은 프레임이 나옴 frame = cv2.flip(frame,-1) #카메라 이미지를 flip, 뒤집습니다. -1은 180도 뒤집는다 cv2.imshow( 'normal' , frame) #'normal'이라는 이름으로 영상을 출력 crop_img =frame[60:120, 0:160] #세로는 60~120픽셀, 가로는 0~160픽셀로 crop(잘라냄)한다. gray = cv2.cvtColor(crop_img, cv2.COLOR_BGR2GRAY) #이미지를 회색으로 변경 blur = cv2.GaussianBlur(gray, (5,5) , 0) #가우시간 블러로 블러처리를 한다. ret,thresh1 = cv2.threshold(blur, 123, 255, cv2.THRESH_BINARY_INV) #임계점 처리로, 123보다 크면, 255로 변환 #123밑의 값은 0으로 처리한다. 흑백으로 색을 명확하게 처리하기 위해서 cv2.imshow('thresh1' ,thresh1) #처리된 영상인 thresh1을 출력한다. if cv2.waitKey(1) == ord('q'): #만약 q라는 키보드값을 읽으면 종료합니다. break cv2.destroyAllWindows() #이후 openCV창을 종료합니다. if __name__ == '__main__': main() |
위의 코드를 준비해줍니다.
이번에는 라인트레이서를 위해 이미지 처리를 해보았습니다.
gray = cv2.cvtColor(crop_img, cv2.COLOR_BGR2GRAY) #이미지를 회색으로 변경 blur = cv2.GaussianBlur(gray, (5,5) , 0) #가우시간 블러로 블러처리를 한다. |
위의 코드는 gray, blur처리 입니다.
위의 이미지 처리를 하는 이유는 조금 더 깔끔한 이미지를 얻기 위함입니다.
gray의 경우, OpenCV의 cvtColor라는 명령어를 통해, crop_img를, BGR2GRAY는 Blue, Green, Red 채널 이미지를 단일 채널, 그레이스케일 이미지로 변경합니다.
blur는 2차원 가우시안 분포를 이용해 블러링을 한다. 중심의 픽셀에 높은 가중치를 부여하고, 5x5의 크기에 맞춰서 계산을 해줍니다.
ret,thresh1 = cv2.threshold(blur, 123, 255, cv2.THRESH_BINARY_INV) |
위의 코드는 임계점 처리로, 영상을 흑/백으로 분류하기 위해서 사용합니다.
123보다 크면, 255로 변환, 123밑의 값은 0으로 처리합니다.
123부분은 임계점 정도를 조절할 수 있는데, 빛이나 배경에 따라 다릅니다. 숫자가 낮아질수록 노이즈가 심해지고, 숫자가 커질 수록 블러처리가 심해집니다.
때문에 자신의 환경에 맞게 조절을 조금 해주셔야합니다. 어려운 부분은 아니에요.
그러면 위의 이미지를 얻을 수 있습니다.
오른쪽의 'thresh1'의 영상을 보면 라인이 검은색으로 명확하게 보이는 것이 확인 되시죠!
이러면 인식하기 좋은 이미지로 변환이 되었습니다.
무게중심 구하기
이제 선의 무게중심을 구해보겠습니다.
무게중심을 왜 구하냐고요?
이번 라인트레이서는 선의 무게중심을 이용해서 자동차가 움직일 겁니다.
만약 가운데가 0이고 왼쪽으로 갈수록 -50 오른쪽으로 갈수록 +50이라면
if문을 사용해서 -50쪽으로 가면 오른쪽으로 가도록, +50쪽으로 가면 왼쪽으로 가게해서 선을 따라갈 수 있도록 만드는 것이지요
import cv2 #OpenCV를 사용하기위해 import해줍니다. import numpy as np #파이썬의 기본 모듈중 하나인 numpy def main(): camera = cv2.VideoCapture(-1) #카메라를 비디오 입력으로 사용. -1은 기본설정이라는 뜻 camera.set(3,160) #띄울 동영상의 가로사이즈 160픽셀 camera.set(4,120) #띄울 동영상의 세로사이즈 120픽셀 while( camera.isOpened() ): #카메라가 Open되어 있다면, ret, frame = camera.read() #비디오의 한 프레임씩 읽습니다. ret값이 True, 실패하면 False, fram에 읽은 프레임이 나옴 frame = cv2.flip(frame,-1) #카메라 이미지를 flip, 뒤집습니다. -1은 180도 뒤집는다 cv2.imshow( 'normal' , frame) #'normal'이라는 이름으로 영상을 출력 crop_img =frame[60:120, 0:160] #세로는 60~120픽셀, 가로는 0~160픽셀로 crop(잘라냄)한다. gray = cv2.cvtColor(crop_img, cv2.COLOR_BGR2GRAY) #이미지를 회색으로 변경 blur = cv2.GaussianBlur(gray, (5,5) , 0) #가우시간 블러로 블러처리를 한다. ret,thresh1 = cv2.threshold(blur, 123, 255, cv2.THRESH_BINARY_INV) #임계점 처리 #이미지를 압축해서 노이즈를 없앤다. mask = cv2.erode(thresh1, None, iterations=2) mask = cv2.dilate(mask, None, iterations=2) cv2.imshow('mask',mask) #이미지의 윤곽선을 검출 contours,hierarchy = cv2.findContours(mask.copy(), 1, cv2.CHAIN_APPROX_NONE) #윤곽선이 있다면, max(가장큰값)을 반환, 모멘트를 계산한다. if len(contours) > 0: c = max(contours, key=cv2.contourArea) M = cv2.moments(c) #X축과 Y축의 무게중심을 구한다. cx = int(M['m10']/M['m00']) cy = int(M['m01']/M['m00']) #X축의 무게중심을 출력한다. cv2.line(crop_img,(cx,0),(cx,720),(255,0,0),1) cv2.line(crop_img,(0,cy),(1280,cy),(255,0,0),1) cv2.drawContours(crop_img, contours, -1, (0,255,0), 1) print(cx) #출력값을 print 한다. if cv2.waitKey(1) == ord('q'): #q값을 누르면 종료 break cv2.destroyAllWindows() #화면을 종료한다. if __name__ == '__main__': main() |
위의 코드를 준비해줍니다.
mask = cv2.erode(thresh1, None, iterations=2) mask = cv2.dilate(mask, None, iterations=2) |
일단 위의 마스크 처리를 통해서 노이즈를 줄여줍니다.
#이미지의 윤곽선을 검출 contours,hierarchy = cv2.findContours(mask.copy(), 1, cv2.CHAIN_APPROX_NONE) #윤곽선이 있다면, max(가장큰값)을 반환, 모멘트를 계산한다. if len(contours) > 0: c = max(contours, key=cv2.contourArea) M = cv2.moments(c) #X축과 Y축의 무게중심을 구한다. cx = int(M['m10']/M['m00']) cy = int(M['m01']/M['m00']) #X축의 무게중심을 출력한다. cv2.line(crop_img,(cx,0),(cx,720),(255,0,0),1) cv2.line(crop_img,(0,cy),(1280,cy),(255,0,0),1) cv2.drawContours(crop_img, contours, -1, (0,255,0), 1) print(cx) #출력값을 print 한다. |
그리고 위의 코드로, 윤곽선을 이용해서 무게중심을 구합니다.
이 코드로는 cx, 즉 x축의 무게중심을 사용합니다.
실행하게 되면..
위와같이 shell에는 print(cx)로 인해 무게중심값이 계속 나오게 됩니다.
중심에 있을때는 무게중심 값이 120~124 정도 나오네요.
무게중심값을 기록해 두는 것이 좋습니다.
오른쪽으로 갈때는 34~62 정도의 값이 나옵니다.
왼쪽으로 치우칠수록 80~120 정도의 값이 나오는 것을 확인할 수 있었습니다.
무게중심을 알았으니, 이제 움직여보면 됩니다!
import cv2 import numpy as np import RPi.GPIO as GPIO PWMA = 18 AIN1 = 22 AIN2 = 27 PWMB = 23 BIN1 = 25 BIN2 = 24 def motor_go(speed): L_Motor.ChangeDutyCycle(speed) GPIO.output(AIN2,True)#AIN2 GPIO.output(AIN1,False) #AIN1 R_Motor.ChangeDutyCycle(speed) GPIO.output(BIN2,True)#BIN2 GPIO.output(BIN1,False) #BIN1 def motor_right(speed): L_Motor.ChangeDutyCycle(speed) GPIO.output(AIN2,True)#AIN2 GPIO.output(AIN1,False) #AIN1 R_Motor.ChangeDutyCycle(0) GPIO.output(BIN2,False)#BIN2 GPIO.output(BIN1,True) #BIN1 def motor_left(speed): L_Motor.ChangeDutyCycle(0) GPIO.output(AIN2,False)#AIN2 GPIO.output(AIN1,True) #AIN1 R_Motor.ChangeDutyCycle(speed) GPIO.output(BIN2,True)#BIN2 GPIO.output(BIN1,False) #BIN1 GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(AIN2,GPIO.OUT) GPIO.setup(AIN1,GPIO.OUT) GPIO.setup(PWMA,GPIO.OUT) GPIO.setup(BIN1,GPIO.OUT) GPIO.setup(BIN2,GPIO.OUT) GPIO.setup(PWMB,GPIO.OUT) L_Motor= GPIO.PWM(PWMA,100) L_Motor.start(0) R_Motor = GPIO.PWM(PWMB,100) R_Motor.start(0) def main(): camera = cv2.VideoCapture(0) camera.set(3,160) camera.set(4,120) while( camera.isOpened() ): ret, frame = camera.read() frame = cv2.flip(frame,-1) cv2.imshow('normal',frame) crop_img =frame[60:120, 0:160] gray = cv2.cvtColor(crop_img, cv2.COLOR_BGR2GRAY) blur = cv2.GaussianBlur(gray,(5,5),0) ret,thresh1 = cv2.threshold(blur,130,255,cv2.THRESH_BINARY_INV) mask = cv2.erode(thresh1, None, iterations=2) mask = cv2.dilate(mask, None, iterations=2) cv2.imshow('mask',mask) contours,hierarchy = cv2.findContours(mask.copy(), 1, cv2.CHAIN_APPROX_NONE) if len(contours) > 0: c = max(contours, key=cv2.contourArea) M = cv2.moments(c) cx = int(M['m10']/M['m00']) cy = int(M['m01']/M['m00']) if cx >= 95 and cx <= 125: print("Turn Left!") motor_left(40) elif cx >= 39 and cx <= 65: print("Turn Right") motor_right(40) else: print("go") motor_go(40) if cv2.waitKey(1) == ord('q'): break cv2.destroyAllWindows() if __name__ == '__main__': main() GPIO.cleanup() |
위의 코드를 준비해 줍니다.
위의 내용은 앞에서 다루었던 자동차를 움직이는 함수들을 만든 부분이니 설명은 생략하겠습니다.
if cx >= 80 and cx <= 120: print("Turn Left!") motor_left(40) elif cx >= 34 and cx <= 62: print("Turn Right") motor_right(40) else: print("go") motor_go(40) |
중요한 것은 위의 추가된 부분입니다.
아까 구한 값을 이용합니다.
80~120의 무게중심 값이 나오면, 왼쪽으로 갑니다.
34~62의 무게중심 값이 나오면 오른쪽으로 갑니다.
그리고 나머지 값의 경우 앞으로 갑니다.
실행결과
앞으로 움찔움찔 거리면서 자동으로 잘 가는 모습을 보여줍니다.
하지만, 이러면 그냥 앞으로 가는거 아니야? 라고 하실 수 있기때문에 비틀어진 방향에서도 실행해봤습니다.
비틀어진 방향에서도 알아서 잘 찾아가는 모습을 보여줍니다!
워후! 성공!
훨씬 더 긴 트랙으로 보여드리면 더 확실하게 보여드릴 수 있겠지만,
저희가 최종적으로 할건 트랙을 자동으로 따라가는 자율주행 차량이니까요!
라인트레이서는 OpenCV를 활용한 방법으로 간단하게만 넘어갑니다.
다음시간 부터는 본격적으로 자율주행 차량에 대해서 시작하게 됩니다.
긴긴내용이 시작되겠네요.
빠이!!
AI 인공지능 자율주행 자동차
#해당 프로젝트는 앤써북의 'AI 인공지능 자율주행 자동차' 를 참고하고 있습니다!
https://book.naver.com/bookdb/book_detail.naver?bid=20861845
'AutoDriving > AutoDriving RC_Car' 카테고리의 다른 글
자율주행 차량 만들기 -완 (라즈베리파이 + OpenCV) 2022 (10) | 2023.02.21 |
---|---|
라즈베리파이 자율주행 자동차 - (6) OpenCV를 이용한 카메라 사용 1 (0) | 2022.04.30 |
라즈베리파이 자율주행 자동차 - (5) 블루투스를 사용해보자! (0) | 2022.04.22 |
라즈베리파이 자율주행 자동차 - (4) 부저와 모터를 움직여보자! (0) | 2022.03.08 |
라즈베리파이 자율주행 자동차 - (3) LED, 버튼 테스트 (0) | 2022.03.06 |