안녕하세요. 저번에 새로운 글로 이미지인식에 대한 코드를 설명드린다고 하였습니다.
먼저 저의 전체적인 이미지인식 코드는 다음과 같습니다.
''' 물체 추적 프로그램 / openCV with Python '''
# 1. Cansat 지상국 위치 : C:\CNSAT_GS
# 2. Cansat 로그파일 위치 : C:\CNSAT_GS\CansatLog
# 3. Cansat 이미지로그 위치 : C:\CNSAT_GS\CameraImage
# 4. Cansat openCV 좌표 로그파일 디렉토리 : C:\Users\hoeun\Desktop\cansat\processing_working_xx\CansatCVLog.csv
# 패키지 모듈 임포트
import numpy as np
import cv2
import csv
from datetime import datetime
def main():
# 메인 코드
''' 로그 파일 위치 다시 한 번 확인하기! '''
# 캔위성 로그 파일 디렉토리
f = open('C:\\CNSAT_GS\\CansatLog\\CansatLog_180809\\CansatLog_180809_11.csv', 'r', encoding='utf-8')
reader = csv.reader(f)
# CansatCVLog 파일 초기화
tempLog = "None"
# 마지막 로그 정보 초기화
LastLog = None
# 로그 파일 읽으며 진행
while True:
for line in reader:
if (line[1] == "ImageWrite"):
# 마지막으로 읽은 로그를 'LastLog'에다가 저장
LastLog = line[0]
# 로그 이미지 디렉토리
readimg = 'C:\\CNSAT_GS\\CameraImage\\Picture_180809\\' + line[3]
print("Now reading Log : ", readimg)
# 이미지 읽기
img = cv2.imread(readimg)
# 사진 테스트용 코드
# img = cv2.imread('C:\\CNSAT_GS\\CameraImage\\Picture_test\\test2.jpg')
# 사진 비율
rad = 2
# 사진 크기 변경
img = cv2.resize(img, None, fx=rad, fy=rad, interpolation=cv2.INTER_AREA)
# 사진 정보 읽어오기
height, width, channel = img.shape
# 사진 정보 출력
print("Size of Input Picture : ", height, "X", width)
# 이미지 추적점 연산 위해 초기화
Sumx = 0
Sumy = 0
time = 0
# 좌표 읽으며 추적점 검색
for y in range(0, height):
for x in range(0, width):
# RGB 세팅
B = img.item(y, x, 0)
G = img.item(y, x, 1)
R = img.item(y, x, 2)
# 현재 좌표가 가르키고 있는 점이 다음 색 안에 있다면
if ((B >= 5 and B < 165) and (G > 117 and G < 255) and (R > 208 and R < 255)) is True:
Sumx = Sumx + x
Sumy = Sumy + y
time = time + 1
# 추적점 수 출력
print("times of calculate : ", time)
if time is 0:
# 추적점이 없다면 원점으로 세팅
comx = 0
comy = 0
else:
# 추적점을 정수로 변환 후 출력
comx = (Sumx / time)
comy = (Sumy / time)
comx = int(comx)
comy = int(comy)
# 바운더리 정의 - '노란색' 추출하기
boundaries = [
([5, 117, 208], [165, 255, 255]) # 노랑색_최적화됨
]
# 바운더리의 최대 최소에 따라 색 추적
for (lower, upper) in boundaries:
lower = np.array(lower, dtype="uint8")
upper = np.array(upper, dtype="uint8")
# 색 추적 마스크 생성 후 이미지에 씌우기
mask = cv2.inRange(img, lower, upper)
output = cv2.bitwise_and(img, img, mask=mask)
# csv 파일 쓰기
now = datetime.now()
# 로그 쓰기 위한 날짜 받아오기
yearlog = str(now.year)
monthlog = str(now.month)
daylog = str(now.day)
hourlog = str(now.hour)
minutelog = str(now.minute)
secondlog = str(now.second)
# YYYY 형태를 YY로 변환
yearlog = yearlog[2:]
# 각 문자열의 길이가 1이면(X) 0을 붙여 2자리고 만듬(XX)
if len(monthlog) is 1:
monthlog = '0' + monthlog
else:
monthlog = str(monthlog)
if len(daylog) is 1:
daylog = '0' + daylog
else:
daylog = str(daylog)
if len(hourlog) is 1:
hourlog = '0' + hourlog
else:
hourlog = str(hourlog)
if len(minutelog) is 1:
minutelog = '0' + minutelog
else:
minutelog = str(minutelog)
if len(secondlog) is 1:
secondlog = '0' + secondlog
else:
secondlog = str(secondlog)
# 프로세싱을 위한 좌표 변환
comxP = comx - (width / 2)
comyP = comy - (height / 2)
# 로그 쓰기 위한 writer 세팅
timelog = [(yearlog + monthlog + daylog + '_' + hourlog + minutelog + secondlog), str(comxP), str(comyP)]
# CansatCVLog 파일 불러오기
j = open('C:\\CNSAT_GS\\CansatCVLog\\CansatCVLog.csv', 'w',
encoding='utf-8', newline='')
wr = csv.writer(j)
# CansatCVLog 파일 테스트
q = open('C:\\CNSAT_GS\\CansatCVLog\\CansatCVLog.csv', 'r',
encoding='utf-8')
rd = csv.reader(q)
# 현재 CansatCVLog 파일 상태 읽기
print("Reading CansatCVLog : ")
for lined in rd:
print(lined)
tempLog = lined
# 로그 작성
wr.writerow(tempLog) # 이전 로그
wr.writerow(timelog) # 현재 로그
j.close
# 평균점을 빨간 점으로 그리기
avmask_one = cv2.circle(output, (comx, comy), int(time/80), (0, 0, 255), 1)
avmask_two = cv2.circle(output, (comx, comy), 3, (0, 0, 255), -1)
# 사진 띄워 출력
cv2.imshow("Input Images", np.hstack([img, output]))
# 로그 파일 읽기 딜레이(밀리초)
cv2.waitKey()
f.close()
main()
생각보다 코드가 그렇게 길지는 않습니다. 파이썬이 프로그래밍 언어 중 무거운 편에 속해서 처리 속도가 느립니다. 여기다가 openCV까지 사용하고 csv 파일도 읽고 파싱하기 때문에 처리속도 향상을 위해 여러 번 검토와 실험을 거쳐 간결하게 작성한 코드입니다.
이제 코드에 대해 천천히 설명드릴게요.
import numpy as np
import cv2
import csv
from datetime import datetime
모듈을 임포트하는 코드입니다. openCV 모듈과 numpy 모듈, csv를 로그파일로 이용하기 때문에 csv 모듈, 그리고 로그를 작성할 때 시드를 만들 datetime 모듈입니다.
def main():
# 메인 코드
''' 로그 파일 위치 다시 한 번 확인하기! '''
# 캔위성 로그 파일 디렉토리
f = open('C:\\CNSAT_GS\\CansatLog\\CansatLog_180809\\CansatLog_180809_11.csv', 'r', encoding='utf-8')
reader = csv.reader(f)
# CansatCVLog 파일 초기화
tempLog = "None"
# 마지막 로그 정보 초기화
LastLog = None
위 코드는 csv 모듈을 이용해 로그 파일의 위치를 지정해서 읽기 모드로 설정하고 로그를 남길 때 쓸 변수를 선언해주었습니다.
# 로그 파일 읽으며 진행
while True:
for line in reader:
if (line[1] == "ImageWrite"):
# 마지막으로 읽은 로그를 'LastLog'에다가 저장
LastLog = line[0]
# 로그 이미지 디렉토리
readimg = 'C:\\CNSAT_GS\\CameraImage\\Picture_180809\\' + line[3]
print("Now reading Log : ", readimg)
# 이미지 읽기
img = cv2.imread(readimg)
# 사진 테스트용 코드
# img = cv2.imread('C:\\CNSAT_GS\\CameraImage\\Picture_test\\test2.jpg')
# 사진 비율
rad = 2
# 사진 크기 변경
img = cv2.resize(img, None, fx=rad, fy=rad, interpolation=cv2.INTER_AREA)
# 사진 정보 읽어오기
height, width, channel = img.shape
# 사진 정보 출력
print("Size of Input Picture : ", height, "X", width)
# 이미지 추적점 연산 위해 초기화
Sumx = 0
Sumy = 0
time = 0
여기까지 코드기 본격적으로 시작됩니다.
while문으로 무한반복문을 만들어줍니다. 사실 이 코드의 메인함수는 끝나지 않습니다. 무한반복문이기 때문에 로그를 계속 읽습니다. 로그가 없다고 하더라도 대기하게 됩니다.
for line in reader는 파이썬의 for문을 이용했습니다. reader는 로그파일을 읽으며 배열이 되는데요. 이 배열에 따라서 for문을 돌립니다.
LastLog 변수에다가 로그를 저장합니다.
그 위 if 조건문으로 조건을 달았습니다. 'line[1]이 ImageWrite라면'이라는 조건입니다.
캔위성에서 들어오는 로그는 csv 파일에 위와 같이 저장됩니다. csv는 콤마(,)로 구분되어있는 파일인데요. 엑셀을 이용하면 이렇게 간편하게 확인할 수 있습니다. 컴퓨터에서는 첫번째 열의 인덱스를 0으로 지정하고 시작합니다. 그래서 배열에서도 첫 칸은 0번 인덱스를 가지게 되는 것입니다.
위 로그를 보면 0번 열에는 로그시드(날짜+시간), 1번 열에는 수신된 데이터의 종류, 2번 열부터는 각종 데이터가 기록됩니다.
즉 line[1]이 ImageWrite라면 그 행은 들어온 사진에 대한 정보를 갖고 있는 것입니다. 그렇기에 사진이 들어왔을 때만 로그를 읽어들이라는 것입니다. 또한 line[0]이면 0번째 열인 로그 시드를 읽으라는 것입니다.
다음으로는 openCV를 이용해 사진을 불러옵니다. 이때 디렉토리 주소와 ImageWrite 행의 3번 인덱스에 파일명이 있어서 line[3]을 문자열 합연산을 하게되면 파일의 주소가 되는 것입니다.
이후 사진을 읽고 resize 함수로 사진의 크기를 늘려줍니다. 들어오는 사진이 너무 작기 때문입니다. 그리고 img.shape 함수로 사진의 가로, 세로, 채널을 읽습니다.
마지막 sumX, sumY, time은 사진에서 픽셀에 대한 정보를 저장할 때 사용하기 위해 선언 후 초기화해줍니다.
# 좌표 읽으며 추적점 검색
for y in range(0, height):
for x in range(0, width):
# RGB 세팅
B = img.item(y, x, 0)
G = img.item(y, x, 1)
R = img.item(y, x, 2)
# 현재 좌표가 가르키고 있는 점이 다음 색 안에 있다면
if ((B >= 5 and B < 165) and (G > 117 and G < 255) and (R > 208 and R < 255)) is True:
Sumx = Sumx + x
Sumy = Sumy + y
time = time + 1
# 추적점 수 출력
print("times of calculate : ", time)
if time is 0:
# 추적점이 없다면 원점으로 세팅
comx = 0
comy = 0
else:
# 추적점을 정수로 변환 후 출력
comx = (Sumx / time)
comy = (Sumy / time)
comx = int(comx)
comy = int(comy)
이제 for문을 이용해 사진의 왼쪽 위인 (0, 0)부터 오른쪽 아래인 (width, height)까지 읽습니다. 여러분들이 이미지인식을 할 것이라면 사진을 인덱스나 좌표에 RGB 값이 들어있는 하나의 2차원 배열이나 2차원 좌표평면으로 생각하면 더욱 도움이 될 것입니다. 그러하기에 for문으로 사진 즉 2차원 배열을 읽을 수 있는 것이죠.
RGB를 변수로 세팅합니다. 아래 조건문은 머신러닝을 찾은 최적화된(학습된) 바이너리 형태의 필터입니다. RGB 각각의 최솟값, 최댓값으로 이루어져 있습니다. 저 필터 내부에 걸린 점을 SumX와 SumY 변수에 좌표를 더하는 것입니다. time은 픽셀의 수를 기록합니다. 아마 눈치가 빠르신 분이라면 대충 감이 오시죠?
위 식은 여러 좌표의 평균점을 구하는 식입니다. 2차원 좌표쌍을 매개변수 형태로 나타낸 것입니다.
아래 time is 0이라는 것은 저 필터에 걸린 픽셀이 0개. 즉 검출이 되지 않았을 때는 점의 좌표를 0, 0으로 설정합니다.
else문으로 검출이 되었다면 Sum 변수를 time변수로 나누어 즉 전체좌표 합을 픽셀 수로 나누어 중심 좌표를 구하는 과정입니다. 연산 후 float 형태의 변수를 강제로 int형으로 형변환해줍니다.
# 바운더리 정의 - '노란색' 추출하기
boundaries = [
([5, 117, 208], [165, 255, 255]) # 노랑색_최적화됨
]
# 바운더리의 최대 최소에 따라 색 추적
for (lower, upper) in boundaries:
lower = np.array(lower, dtype="uint8")
upper = np.array(upper, dtype="uint8")
# 색 추적 마스크 생성 후 이미지에 씌우기
mask = cv2.inRange(img, lower, upper)
output = cv2.bitwise_and(img, img, mask=mask)
# csv 파일 쓰기
now = datetime.now()
바운더리 형태의 필터를 선언합니다. 아까 최솟값, 최댓값 RGB 그 배열과 같은 형태입니다.
아래 for문에서는 바운더리 필터에 걸린 부분을 체크하게 됩니다. 필터에 걸린 부분, 즉 탁구공으로 검출된 부분을 제외하고 검정색으로 마스크를 씌우는 것입니다. 이렇게 되면 화면에서는 노란 부분만 보이게 됩니다. output 변수에 최종 출력 이미지를 대입합니다.
now 변수에는 datetime 모듈에서 현재 시간을 받아옵니다.
# 로그 쓰기 위한 날짜 받아오기
yearlog = str(now.year)
monthlog = str(now.month)
daylog = str(now.day)
hourlog = str(now.hour)
minutelog = str(now.minute)
secondlog = str(now.second)
# YYYY 형태를 YY로 변환
yearlog = yearlog[2:]
# 각 문자열의 길이가 1이면(X) 0을 붙여 2자리고 만듬(XX)
if len(monthlog) is 1:
monthlog = '0' + monthlog
else:
monthlog = str(monthlog)
if len(daylog) is 1:
daylog = '0' + daylog
else:
daylog = str(daylog)
if len(hourlog) is 1:
hourlog = '0' + hourlog
else:
hourlog = str(hourlog)
if len(minutelog) is 1:
minutelog = '0' + minutelog
else:
minutelog = str(minutelog)
if len(secondlog) is 1:
secondlog = '0' + secondlog
else:
secondlog = str(secondlog)
위 코드는 길이는 길지만 별 건 아닙니다. 로그 시드를 작성하기 위한 단계인데요.
현재 년도, 달, 일, 시, 분, 초를 받아온 후 모두 두자리로 변환해줍니다.
예를 들면 2018년이면 18로, 7시이면 07로 변환해줍니다.
# 프로세싱을 위한 좌표 변환
comxP = comx - (width / 2)
comyP = comy - (height / 2)
# 로그 쓰기 위한 writer 세팅
timelog = [(yearlog + monthlog + daylog + '_' + hourlog + minutelog + secondlog), str(comxP), str(comyP)]
# CansatCVLog 파일 불러오기
j = open('C:\\CNSAT_GS\\CansatCVLog\\CansatCVLog.csv', 'w',
encoding='utf-8', newline='')
wr = csv.writer(j)
# CansatCVLog 파일 테스트
q = open('C:\\CNSAT_GS\\CansatCVLog\\CansatCVLog.csv', 'r',
encoding='utf-8')
rd = csv.reader(q)
# 현재 CansatCVLog 파일 상태 읽기
print("Reading CansatCVLog : ")
for lined in rd:
print(lined)
tempLog = lined
# 로그 작성
wr.writerow(tempLog) # 이전 로그
wr.writerow(timelog) # 현재 로그
j.close
프로세싱을 좌표를 넘기기 위해 좌표를 재지정해줍니다. 이 부분은 프로세싱을 하신다면 조금 이해가 쉽겠네요. 저희 프로그램은 openCV에서 받은 이전 좌표와 현재 좌표를 벡터연산해서 벡터를 구해내는 것입니다. 그래서 이전 좌표와 현재 좌표 모두 로그에 작성해야합니다.
timelog 변수에 아까 했던 문자열들을 합연산해줍니다.
아래 csv모듈에서 CansatCVLog 파일에 두번째 행에 적힌 것을 읽어들입니다. 1행에는 이전좌표, 2행에는 현재좌표가 기록되는데 이게 계속 업데이트 되며 정보를 전달하는 방식입니다.
그래서 새로운 좌표가 구해지면 이전에 현재 좌표를 이전 좌표로 옮겨야 하므로 2행의 로그를 1행으로 옮겨줍니다. 그리고 2행에는 로그 시드와 함께 좌표를 써주는 것입니다.
# 평균점을 빨간 점으로 그리기
avmask_one = cv2.circle(output, (comx, comy), int(time/80), (0, 0, 255), 1)
avmask_two = cv2.circle(output, (comx, comy), 3, (0, 0, 255), -1)
# 사진 띄워 출력
cv2.imshow("Input Images", np.hstack([img, output]))
# 로그 파일 읽기 딜레이(밀리초)
cv2.waitKey(10)
f.close()
main()
코드의 마지막 부분입니다. 이미지에 중심좌표에는 빨간 점을, 탁구공 테두리를 표시해줍니다.
imshow 함수로 원래 사진과 이미지인식된 사진을 함께 띄워줍니다. 아까 말씀드렸듯이 사진은 배열이라 hstack 함수로 배열을 합칠 수 있는 것입니다.
마지막 cv2.waitKey(10)은 대기 함수인데요. 10밀리초(ms)를 대기하라는 것입니다. 이때 프로그램은 다음 이미지가 들어올 때까지 자동으로 대기합니다.
이로써 메인함수는 무한반복하며 끝이 납니다.
위 코드를 작성하는데 약 두 달 가량의 시간이 걸렸습니다. 이미지인식 필터도 학습하고 처리 속도가 느려서 이를 보완하느라 시간이 많이 걸렸던 것 같습니다. 그래도 대회 당일 잘 작동하고 성공적으로 마무리되어서 정말 기분이 좋습니다.
혹시 코드에 대한 오류나 질문 있으시면 댓글로 달아주세요. 감사합니다!