Project/Proj-Nova Vision

[Nova-Vision] 육계 체중측정 시스템 시제품 제작과정

dohyeon2 2023. 10. 27. 17:04

이번 글에서는 "육계 체중측정 시스템 시제품(설치형)" 제작과정에서 마주했던 문제들과 해결방법에 대해 정리하였습니다.

(전북대학교 시제품 제작 지원사업, 2023.05~2023.09)

 

AI model 배포과정: AI 모델 Serving은 어떻게 하는걸까?

 

Github: https://github.com/dohyeonYoon/Nova-Vision

 

GitHub - dohyeonYoon/Nova-Vision: This code is for custom stereo depth estimation.

This code is for custom stereo depth estimation. Contribute to dohyeonYoon/Nova-Vision development by creating an account on GitHub.

github.com


프로젝트 개요

2023년 1월 아래 그림 1과 같이 휴대형(3D카메라 + Edge Computer)으로 동작하는 Nova-Vision MVP 제품을 제작하였습니다. 기존 시제품의 체중측정 방식은 3D 깊이 카메라에서 촬영한 이미지를 Edge Computer에 저장하고 깊이 데이터를 계산한 뒤, 서버에 전송하여 체중을 추론하는 방식이었습니다. 그러나 제품을 이와같이 구성할 경우, 한 대의 디바이스마다 카메라(stereolabs Zed2i) + 컴퓨터(jetson nano)가 필요해서 제품 단가가 100만원에 달하는 문제점이 있었습니다. 규모가 큰 농장의 경우, 많게는 30대의 카메라가 설치되는데 이런 경우 설치비가 3,000만원 가량 발생하였습니다. 저희팀의 비즈니스 모델은 카메라 장비는 무상으로 제공하고, 월 구독료를 통해 수익을 창출하는 방식이기 때문에 장비의 단가를 낮추는 것은 수익창출을 위해 매우 중요하였습니다. 결론적으로 3D Depth 카메라와 임베디드보드를 자체개발하여 제품 단가를 낮추고자 프로젝트를 시작하게 되었습니다.

 

 

그림 1. Nova-vision 초기 MVP

프로젝트 기획

앞서 설명드린대로 이번 프로젝트의 목표는 기존 제품과 동일한 기능을 하면서 제품의 단가를 낮추는 것입니다. 많은 회의끝에 기존 제품에 탑재되는 3D 카메라와 임베디드보드를 자체개발하고, 모든 데이터 저장 및 추론작업은 클라우드 서버에서 진행되도록 구조를 변경하기로 하였습니다. 새롭게 개발된 시제품의 서비스 개략도는 그림 2와 같습니다.

 

[Service overview]

그림 2. Nova-vision 서비스 overview

 

[Architecture overview]

 

그림 3. Nova-vision 시제품(설치형) 개략도

 

새로운 시제품의 전체적인 Architecture를 최대한 간단하게 설명하면 다음과 같습니다.

  1. 3D Depth 카메라에서 획득한 데이터(RGB image pair, camera parameter) 추론서버로 전송
  2. 추론서버에서 전송받은 데이터를 이용하여 (RGB image pair, camera parameter를 이용하여) 3D Depth 계산
  3. RGB, Depth 정보를 이용하여 가축의 체중을 측정하고 체중정보를 DB에 저장
  4. 모니터링 서버에서 DB에 저장된 데이터를 시각화

(이번 글에서는 3D Depth 카메라와 임베디드보드를 개발하는 과정에 대해 설명합니다.)

 

S/W 개발

스테레오방식 3D Depth 카메라의 내부동작 코드 개발의 목표는 왼쪽,오른쪽 렌즈에서 촬영한 2장의 이미지만으로 각 픽셀까지의 실제 거리 정보를 획득하는것 입니다.

 

[Final goal]

스테레오 카메라를 이용하여 촬영된 2장의 이미지로 실제 거리 정보를 획득하는 방법은 다음과 같이 진행됩니다. 

1. 카메라 캘리브레이션

2. 이미지 Rectification

3. Disparity Map 생성

4. Depth Estimation

5. Point cloud 생성

 

1. 카메라 Calibration

카메라 캘리브레이션 과정은 스테레오 카메라 캘리브레이션을 기준으로 설명합니다

1.1 Camera Calibration이란?

영상 왜곡보정에 사용되는 카메라의 내부 파라미터를 계산하는 과정을 말하며, 이중 Stereo camera calibration 과정은 카메라의 내부 파라미터뿐만 아니라 Image rectification 과정에 사용되는 카메라 외부 파라미터까지 계산하는 과정을 뜻합니다. 

 

Intrinsic parameter(내부파라메터)
Focal length(fx, fy) - 초점거리
Principal point(cx, cy) - 광학중심
Distortion(K1, K2, P1, P2, K3) - 일반켈리브레이션에 적합,
Distortion_Ext(K1, K2, P1, P2, K3, K4, K5, K6) - 광각이나 일반켈이브레이션보다 정밀도를 원하는 경우에 적합
Distortion_Fisheye(K1, K2, K3, K4) - 초광각의 경우에 적합

Extrinsic parameter(외부파라메터)
Rotation matrix(R) - 두 카메라간의 회전 매트릭스

Translation matrix(T) - 두 카메라간의 변환 매트릭스

 

1.2 Camera calibration 방법

Step 1. 캘리브레이션 데이터셋(스테레오 카메라의 왼쪽 오른쪽 렌즈에서 체스보드판을 촬영한 이미지) 준비

: 데이터셋은 정확한 카메라 파라미터 도출을 위해 다음과 같은 조건을 만족해야합니다.

  • 카메라는 고정되있는 상태에서 체크보드판을 다양한 거리, 상하좌우 각도, 회전 각도에서 촬영
  • 각 카메라 당 50장 이상의 이미지 촬영(본인은 각 75장 사용)
  • 햇빛이나 형광등에 의한 흰색 빛번짐이 없는 환경에서 촬영(야간에 실내에서 하는 것을 추천)

chessboard image from each camera

 

Step 2. 개별 카메라 캘리브레이션(opencv library)

: 카메라 캘리브레이션 과정은 일반적으로 이미 사이즈를 알고있는 chess board의 샘플이미지를 입력받습니다.

예를들어 10x7 grid, 18mm 정사각형 크기 체스보드판을 이용하여 캘리브레이션을 진행한다면, 체스보드판 내 정사각형 코너의 3차원 월드좌표와 2차원 픽셀좌표만 알아내면 캘리브레이션을 진행할 수 있습니다. 이때, 우리는 이미 체스보드의   3차원 월드좌표를 알고있습니다. 그러면 2차원 픽셀좌표만 알아내면 되는데 이는 opencv findChessboard() 함수를 이용하여 입력받은 샘플이미지에서 직접 찾을 수 있습니다! opencv calibration code를 기반으로 순서대로 설명하겠습니다.

  • 캘리브레이션 데이터셋 불러오기
for i in tqdm(range(0,75)):
	imgL = cv2.imread(pathL+f"left_{i}.png")
	imgR = cv2.imread(pathR+f"right_{i}.png")
	imgL_gray = cv2.imread(pathL+f"left_{i}.png",0)
	imgR_gray = cv2.imread(pathR+f"right_{i}.png",0)
  • 체스보드판 내 정사각형 코너의 3차원 월드좌표 정의
    :체스보드판의 실제 크기는 우리가 이미 알고있기 때문에 이를 objP라는 변수에 저장합니다. objP를 출력해보면 아래와 같이 체스보드판 내 사각형 코너의 실제 3차원 좌표를 출력합니다. (원래 (0,0,0), (1,0,0) ...(9,0,0) ....(9,6,0)와 같이 단위가 없는 값이 저장되었지만 chessboard 정사각형의 실제크기를 곱해줌으로서 아래와 같이 출력됩니다). 
objp[:,:2] = np.mgrid[0:10,0:7].T.reshape(-1,2) * 18 # chessboard의 정사각형 크기(mm단위) 곱해줄 것
print(objp)

 

----------------------------------------------------------------------------------------------
촬영에 쓰인 체스보드판 크기(10x7 grid,18mm 정사각형)
objP =[[  0.   0.   0.],
 [ 18.   0.   0.],
 [ 36.   0.   0.],
...
 [162. 108.   0.]] # 총 70개의 object Point(월드좌표계) 

  • 체스보드판 이미지 내 정사각형의 코너감지
	retR, cornersR =  cv2.findChessboardCorners(outputR,(10,7),None)
	retL, cornersL = cv2.findChessboardCorners(outputL,(10,7),None)
  • 감지된 코너의 정확도 향상을 위한 sub pixel 수준에서 코너 재감지
    if retR and retL:
        obj_pts.append(objp)
        cornersR2 = cv2.cornerSubPix(imgR_gray,cornersR,(11,11),(-1,-1),criteria)
        cornersL2 = cv2.cornerSubPix(imgL_gray,cornersL,(11,11),(-1,-1),criteria)

findChessboard() 함수로 그려진 정사각형 코너의 2차원 픽셀좌표

 코너를 감지한 결과를 출력해보면 다음과 같이 2차원 픽셀좌표를 출력합니다.

 CornerR2 = [[368.      346.     ]]
 [[405.,      342.5    ]]
 [[441.5     339.5    ]]
 [[479.3437  334.8 ]]
 [[512.5     333.5    ]]
 [[546.5     330.5    ]]

....

 [[379.5     415.5    ]]

  • 카메라 내부 파라미터 계산
# Calibrating left camera
rmsL, camera_mtxL, distL, rvecsL, tvecsL = cv2.calibrateCamera(obj_pts,img_ptsL,imgL_gray.shape[::-1],None,None)
hL,wL= imgL_gray.shape[:2]
new_mtxL, roiL= cv2.getOptimalNewCameraMatrix(mtxL,distL,(wL,hL),0,(wL,hL))
print('왼쪽 카메라 메트릭트', new_mtxL)

# Calibrating right camera
rmsR, camera_mtxR, distR, rvecsR, tvecsR = cv2.calibrateCamera(obj_pts,img_ptsR,imgR_gray.shape[::-1],None,None)
hR,wR= imgR_gray.shape[:2]
new_mtxR, roiR= cv2.getOptimalNewCameraMatrix(mtxR,distR,(wR,hR),0,(wR,hR))
print('오른쪽 카메라 메트릭트', new_mtxR)

 이때, camera_mtxL, distL은 각각 왼쪽 카메라의 camera matrix, distortion coefficients를 말하며, 이를 통해 각 카메라의   내부 파라미터를 계산할 수 있습니다. 

camera intrinsic parameter

Step 3. 스테레오 카메라 캘리브레이션(opencv library)

이 과정을 통해 카메라의 외부 파라미터를 계산할 수 있습니다.

  • 회전, 변환 매트릭스 계산
# This step is performed to transformation between the two cameras and calculate Essential and Fundamenatl matrix
rmsS, new_mtxL, distL, new_mtxR, distR, Rot, Trns, Emat, Fmat = cv2.stereoCalibrate(obj_pts,
                                                          img_ptsL,
                                                          img_ptsR,
                                                          new_mtxL,
                                                          distL,
                                                          new_mtxR,
                                                          distR,
                                                          imgL_gray.shape[::-1],
                                                          criteria_stereo,
                                                          flags)

 

Step 4. 캘리브레이션 오차(재투영 오차) 확인

: 카메라의 캘리브레이션 오차는 Reprojection RMSE(Root Mean Square Error)라는 지표로 평가합니다. Reprojection RMSE란 투영된 3D 포인트가 주어진 2D 이미지 포인트와 몇 픽셀만큼 차이나는지를 의미하는 재투영 오류를 말합니다. 왼쪽, 오른쪽, 스테레오 캘리브레이션 각 과정에서 RMSE를 평가하며, RMSE값은 작으면 작을수록 좋지만, 0.1~1.0 픽셀 사이의 값으로 수렴하면 충분합니다. 개별 카메라의 RMSE값은 cv2.calibrateCamera 함수의 output 첫번째 인자 rmsL, rmsR이며 스테레오 카메라의 RMSE값은 cv2.stereoCalibrate 함수의 output 첫번째 인자 rmsS를 출력하면 확인하실 수 있습니다.

문제해결 1

재투영 오차 확인과정에서 개별 카메라의 RMSE값은 0.251, 0.313으로 수렴하였지만 스테레오 RMSE값이 20.31로 매우 크게 나타났습니다. 결론적으로 이는 왼쪽 이미지와 오른쪽 이미지에 표기되는 패턴의 순서가 달라서 발생하는 문제였습니다.

이렇게 되면 포인트는 서로 일치하지 않기 때문에 stereoCalibrate RMSE값이 매우 높게 나오게됩니다.

Left 이미지의 패턴은 오른쪽에서 왼쪽으로, Right 이미지의 패턴은 왼쪽에서 오른쪽으로

이를 해결하기 위해 캘리브레이션 데이터셋에서 패턴의 순서가 다른 이미지는 데이터셋에서 제외하였습니다. 그 결과 stereoCalibrate RMSE값은 0.293으로 수렴하였습니다.

reference: https://stackoverflow.com/questions/23826541/opencv-stereocalibrate-returns-high-rms-error

 

문제해결 2

마지막에 생성되는 point cloud의 (x,y,z)값을 확인하였을 때 실제 거리와 완전히 다른 값을 얻게되었습니다. 결론적으로 이는 카메라 캘리브레이션 단계에서 단위를 설정하지 않아서 발생한 문제였습니다. 저는 생성되는 point cloud의 단위를 실제 월드좌표계의 mm단위를 얻고자 하였습니다. 

objp = np.zeros((10*7,3), np.float32)
objp[:,:2] = np.mgrid[0:10,0:7].T.reshape(-1,2) * 18 # chessboard의 정사각형 크기(mm단위) 곱해줄 것

이를 해결하기 위해 체스보드 직사각형 코너의 실제 3차원 위치를 저장하는 objp 변수에 체스보드판 직사각형의 실제 사이즈를 곱해주었습니다. 그 결과 마지막에 생성되는 point cloud (x,y,z)값을 mm단위로 얻을 수 있었습니다.

reference: https://stackoverflow.com/questions/41708833/python-opencv-stereo-calibrate-object-points 

 

2. 이미지 Rectification

카메라 캘리브레이션 과정을 통해 얻은 내부 파라미터를 이용하여 이미지의 왜곡을 보정하고, 외부 파라미터를 이용하여 Epipolar line을 정렬하는 것을 Image rectification이라고 합니다. 이렇게 설명하면 이해가 쉽지 않을 것 같은데요. 이미지 Rectification을 이해하기 위해서는 이에앞서 Epipolar 제한조건에 대해서 알아보겠습니다.

 

2.1 에피폴라 제한조건(Epipolar Constraint)

기존에 우리는 왼쪽 이미지상의 한점에 대응되는 점을 찾기 위해서 오른쪽 이미지의 모든 픽셀을 뒤져야 했습니다. 제가 입력으로 사용하는 FHD(1920x1080) 이미지를 예로들면 다음과 같이 엄청난 연산이 필요합니다.

  1. 왼쪽 이미지에서 한 점을 선택
  2. 해당 점에 대응하는 점을 오른쪽 이미지에서 1920x1080번 반복하여 탐색
  3. 다시 왼쪽 이미지에서 한점을 선택하고 (2)를 반복
    (이러한 반복은 1920x1080번 반복됩니다)

실시간성을 필요로하지 않는 서비스의 경우 위 연산과정을 수행하더라도 문제가 없겠지만, 실시간성을 요하는 서비스의 경우 분명히 문제가 발생할 것입니다. 그렇다면 위와 같은 반복적인 연산과정을 좀더 단순화 할 수 있는 방법이 궁금할텐데 이 방법이 바로 에피폴라 제한조건입니다. 에피폴라 제한조건을 적용하면 2D Serching 문제를 1D Searching 문제로 바꿔 연산속도를 증가시킬 수 있습니다. 이 글에서는 에피폴라 제한조건을 그림을 통해 설명해보겠습니다. 

 

출처: https://computervision.tistory.com/4

 

그림과 같이 3차원 공간상에 X 라는 점이 있습니다. 이 점은 왼쪽 카메라에서는 이미지센서 O와 연결하는 선상에 놓이게 되므로 x로 투영됩니다. 왼쪽 카메라의 이미지평면 상에서 본 x라는 점은 3차원 공간 상에 위치한 X이기도 하지만 xX 선분 상에 있는 어떠한 점도 왼쪽 카메라의 이미지 평면 상에서는 x로 투영됩니다. 따라서 왼쪽 카메라 이미지 상에서 x라는 점은 3차원 공간 상에서는 xX 선분에 있는 어떠한 점도 x가 될 수 있기때문에, x에 대응되는 오른쪽 카메라 이미지평면 상에서의 후보군은 여러개의 점으로 이루어진 선분 형태로 나타납니다. 이 때 이 선분이 하나로 만나는 점이 생깁니다. 바로 왼쪽 카메라와 오른쪽 카메라의 원점(OO')을 연결한 직선이 이미지를 관통하는 점 e와 e'입니다. 이때 원점을 연결한 직선이 이미지를 관통하는 점 e를 에피폴(epipole)이라고 부르고 에피폴을 지라는 후보군 선분을 에피폴라 라인(epipolar line)이라고 합니다. 그리고 이러한 일련의 조건들을 에피폴라 제한조건이라고 합니다.

위 내용을 요약하자면 다음과 같습니다. 

  1. 카메라로부터 물체까지 실제 거리를 알기위해 Disparity를 알아야 함
  2. Disparity를 알기 위해서 블록 매칭을 해야 하는데, 매칭은 매우 큰 연산량이 필요함
  3. 왼쪽 이미지에서 한점은 3차원 공간 상에서 후보군이 한 직선으로 나타나고, 이는 오른족 카메라에서 특정한 라인으로 표시되는데 그 라인은 항상 에피폴을 지나감
  4. 이제 왼쪽 이미지상의 한점에 대응되는 점을 찾기 위해 오른쪽 이미지의 모든 픽셀을 뒤질 필요없이 에피폴 라인에 위치하는 픽셀만 뒤지면 됨
  5. 에피폴은 카메라 배치에 따라 정해지는 것이므로 카메라 캘리브레이션 과정을 통해 에피폴을 알아낼 수 있음

 

2.2 Image Rectification 이란?

앞선 글에서 카메라 캘리브레이션을 마쳤기 때문에 우리는 epipole의 위치를 알 수 있고, 왼쪽 이미지 한 점에 대응되는 오른쪽 이미지의 epipolar line을 알 수 있습니다. Image rectification 전 이미지를 살펴보면 아래 그림처럼 epipolar line이 사선으로 나타나는 것을 볼 수 있습니다. Epipolar line만 뒤지면 대응되는 점을 찾을 수 있다고 했지만, Epipolar line이 x축으로 평행선상에 위치하지 않기 때문에 block maching하는데 있어서 여전히 계산상으로 매우 큰 cost가 발생합니다. 

Before rectification

이 문제를 단순화하기 위해 epipolar line이 평행하도록 이미지를 변환하는 과정을 Image rectification이라고 합니다. 위 이미지에서 카메라 캘리브레이션 파라미터를 이용하여 rectification한 결과는 아래 그림과 같습니다. 

 

After rectification

 

이렇게 epipolar line이 평행선상에 위치하게되면, 왼쪽 이미지의 한점은 오른쪽 이미지에서 평행한 line에 위치한 한줄만 검색하면 되기때문에 계산상으로 매우 큰 이득이 생깁니다. 또한, block maching 정확도 측면에서도 이점이 발생하는데 아래는 실제 rectification을 적용하기 전과 후 disparity map 생성 정확도를 비교한 그림입니다.

위와 같이 Image rectification을 하고 안하고 disparity map의 정확도 차이는 분명하기 때문에 stereo camera를 구성할 때 rectification 과정은 필수입니다.

 

2.3 Image rectification process

Image rectification 과정은 아래 그림과 같이 진행됩니다.

a. 원본 이미지 입력

b. 캘리브레이션 파라미터를 이용한 이미지 왜곡보정

c. 캘리브레이션 파라미터를 이용하여 epipolar line이 평행하도록 이미지 변환

d. 변환과정에서 0(검은색)으로 패딩된 픽셀영역을 제외하고 유효한 픽셀영역만을 crop

Image rectification process

 

앞선 글에서 스테레오 캘리브레이션 작업까지 완료되었다는 가정하에 code로 설명드리겠습니다.

사실 코드상에서는 b,c과정이 동시에 진행됩니다.

  • 보정된 스테레오 카메라의 각 렌즈에 대한 정류변환 계산
# Once we know the transformation between the two cameras we can perform stereo rectification
rectify_scale= 0.0 # if 0 image croped, if 1 image not croped
rect_l, rect_r, proj_mat_l, proj_mat_r, Q, roiL, roiR= cv2.stereoRectify(new_mtxL, distL, new_mtxR, distR,
									  imgL_gray.shape[::-1], Rot, Trns, alpha=rectify_scale)
  • 비왜곡 및 정류변환 맵 계산
# Use the rotation matrixes for stereo rectification and camera intrinsics for undistorting the image
# Compute the rectification map (mapping between the original image pixels and
# their transformed values after applying rectification and undistortion) for left and right camera frames
Left_Stereo_Map= cv2.initUndistortRectifyMap(new_mtxL, distL, rect_l, proj_mat_l,
                                             imgL_gray.shape[::-1], cv2.CV_16SC2)
Right_Stereo_Map= cv2.initUndistortRectifyMap(new_mtxR, distR, rect_r, proj_mat_r,
                                              imgR_gray.shape[::-1], cv2.CV_16SC2)

 

  • 왜곡보정 및 정류(rectification)
Left_Stereo_Map_x = Left_Stereo_Map[0]
Left_Stereo_Map_y = Left_Stereo_Map[1]
Right_Stereo_Map_x = Right_Stereo_Map[0]
Right_Stereo_Map_y = Right_Stereo_Map[1]

Left_nice= cv2.remap(imgL_gray,
					Left_Stereo_Map_x,
					Left_Stereo_Map_y,
					cv2.INTER_LANCZOS4,
					cv2.BORDER_CONSTANT,
					0)

# Applying stereo image rectification on the right image
Right_nice= cv2.remap(imgR_gray,
					Right_Stereo_Map_x,
					Right_Stereo_Map_y,
					cv2.INTER_LANCZOS4,
					cv2.BORDER_CONSTANT,
					0)

이렇게 되면 아래 그림과 같이 원본이미지를 왜곡보정하고 정류한 이미지를 얻을 수 있습니다.

 

문제해결 1

왜곡보정 및 정류 과정을 통해 얻게되는 왜곡보정된 이미지는 위 그림의 초록색 box로 표시된 영역이었으나, 실제로 얻어진 이미지는 검정색 dead pixel이 포함된 이상한 이미지를 얻게되었습니다. 처음 저는 카메라 내부파라미터값에 문제가 있어서 왜곡보정이 잘못된줄 알고 캘리브레이션과정을 수차례 재검토하였지만 문제가 없었습니다. 결론적으로 opencv document를 살펴본 결과 아래 border mode가 BORDER_TRANSPARENT(1값 입력)인 경우 dead pixel까지 포함하여 이미지를 정류한다는 사실을 확인하였습니다. 따라서 border mode를 BORDER_CONSTANT로 변경(0값 입력)하여 문제를 해결하였습니다.

opencv document cv2.remap 함수 설명자료

결과적으로 앞서 기대했던 정류(왜곡보정 및 ROI Crop)된 이미지를 획득할 수 있었습니다.

reference: https://docs.opencv.org/3.4/da/d54/group__imgproc__transform.html   

 

3. Disparity map 계산

앞서 스테레오 블록매칭 과정에서 발생하는 계산상의 Cost를 낮추기 위한 이미지 Rectification에 대해서 알아보았습니다. 이번에는 이미지 Rectification(Distortion 및 Epipolar line 보정) 과정을 마친 2장의 이미지를 입력으로 받아 Disparity Map을 생성하고, 생성된 disparity Map을 이용하여 실제 거리를 계산하는 방법을 설명합니다.

 

보정을 마친 두 장의 이미지로 Depth map을 생성하는 과정은 다음과 같습니다.

1. openCV block matching algorithm을 이용하여 Disparity map(각 픽셀의 disparity value) 계산

2. 계산된 Disparity map과 카메라 파라미터를 이용하여 각 픽셀의 depth map(각 픽셀의 depth value) 계산 

(이때, depth value는 카메라 원점(카메라 이미지센서 중심)으로부터 해당 물체의 위치까지 떨어진 z방향 실제거리를 뜻합니다.)

 

3.1 물체거리와 Disparity 사이의 관계

 

우리는 두장의 이미지만으로 어떻게 물체까지 거리를 알 수 있을까요? 구글에 "스테레오 카메라 원리"를 검색해보면 가장 많이 나와있는 설명은 "삼각측량법을 사용한다" 입니다. 하지만 저는 수학적인 내용은 최대한 배제하고 좀더 직관적으로 설명해보겠습니다. 우리가 구하고자 하는 Disparity(시차)는 스테레오 카메라로 촬영한 왼쪽, 오른쪽 이미지에서 두 대응 지점 사이의 픽셀거리를 말합니다. 

먼저 물체가 카메라와 가까이 있을 때, 3차원상의 물체가 왼쪽 카메라의 이미지 평면상에 투영된 지점은 A이고 오른쪽 카메라의 이미지 평면상에 투영된 지점은 B라고 하겠습니다. 왼쪽 그림과 같이 물체가 가까이 있을 때, A와 B 사이의 픽셀거리는 커집니다. 

반면에 물체가 카메라와 멀리 있을때, 3차원상의 물체가 왼쪽 카메라의 이미지 평면상에 투영된 지점은 A'이고 오른쪽 카메라의 이미지 평면상에 투영된 지점은 B'입니다. 오른쪽 그림과 같이 물체가 멀리 있을 때 , A'와 B' 사이의 픽셀거리는 작아집니다.

이렇게 물체가 카메라와 가까우면 Disparity value가 커지고, 물체가 카메라와 멀면 Disparity value가 작아지는 원리와 카메라의 파라미터 정보를 이용하면 우리는 카메라로부터 물체까지 떨어진 실제 거리를 계산할 수 있습니다.

 

3.2 Disparity map 계산

openCV에서 제공하는 block matching algorithm은 크게 2가지가 존재합니다. stereoBM 알고리즘과 stereoSGBM 알고리즘입니다. 간략하게 그려본 stereoSGBM의 동작원리는 아래 그림과 같습니다.

 

- stereoSGBM 알고리즘의 동작원리

openCV stereoSGBM 알고리즘 원리 모식도

 

  • 1개의 픽셀을 총 256(16x16)개의 subpixel로 나눈다.
  • 가로, 세로, 대각선 각각 2개씩 총 8개의 방향에서 왼쪽 픽셀과 매칭되는 픽셀을 찾는 block matching 과정을 거친다.(opencv stereoSGBM 함수의 HH mode 사용)
  • disparity value 계산 (이때, 매칭에 실패한 무효픽셀: -16, 매칭에 성공한 유효픽셀: 0~ 16 x numDisparity, 자료형: int16)
  • 자료형 변환 및 깊이정보 계산을 위해 측정된 disparity value를 16으로 나누기(이때, 매칭에 실패한 무효픽셀: -1.0, 매칭에 성공한 유효픽셀: 0~ numDisparity, 변경된 자료형: float32)                                

- Block matching 알고리즘 성능 비교

stereoBM, stereoSGBM 성능비교

 

stereoBM과 stereoSGBM 알고리즘을 통해 생성된 Disparity map을 살펴보면 위 그림과 같이 파라미터튜닝 전/후 stereoSGBM 알고리즘이 더 정확한 Disparity map을 생성한 것을 볼 수 있었습니다. 두대의 카메라로 촬영한 이미지를 통해 실시간으로 생성되는 Disparity map의 FPS는 stereoBM이 10.9, stereoSGBM이 3.1이었습니다. 이와같이 속도 측면에서는 stereoBM이 우수했지만, 저의 경우 실시간성을 보장하지 않아도 되었기 때문에 정확도가 더 높은 stereoSGBM 알고리즘을 선택하였습니다.

 

- Disparity map 후처리(filtering)

openCV StereoSGBM을 통해 생성된 Disparity map도 꽤 훌륭하지만 중간중간 검정색으로 나타난 dead pixel이 존재하였습니다. 이는 block maching 과정에서 매칭에 실패한 무효픽셀로서 정확한 거리계산을 위해 꼭 해결해야 할 문제점입니다. 저는 이러한 dead pixel 문제를 해결하기 위해 Disparity map 후처리 과정에서 주로 쓰이는 WLS filtering, Bilateral filtering을 적용해보고 confidence map을 그려 각 픽셀의 신뢰도를 분석해봤습니다.

 

Disparity map post-processing 결과

 

원본 Disparity map에 WLS filter, Bilateral filter를 적용한 결과 위 그림과 같이 WLS filter가 가장 RGB 이미지와 boundary가 유사하고, dead pixel 문제도 대부분 해결된 것을 확인하였습니다. 그 다음 openCV getConfidence() 함수를 사용하여 계산된 각 픽셀의 disaprity value 신뢰도를 계산하였습니다. 신뢰도는 0~255 범위의 1채널 CV32F 자료형으로 나타나는데, 간단하게 설명하면 어두운 부분이 신뢰도가 낮은픽셀, 밝은 부분이 신뢰도가 높은 픽셀이라고 생각하면 됩니다. 결과적으로 대부분의 영역에서 높은 신뢰도로 Disparity가 계산된 것을 확인하였습니다.

 

4. Depth map 계산

후처리까지 마친 Disparity map을 이용하여 Depth map을 생성할 수 있습니다. 앞서 생성된 각 픽셀의 Disparity value는 float32 자료형의 -1~numDisparity 범위의 값입니다(이때, 유효한 픽셀값 범위는 0~numDisparity). 이 값을 이용해 Depth map을 계산하는 방법은 다음과 같습니다.

 

4.1 각 픽셀의 Depth 계산방법

depth map 계산 모식도

 

  • 왼쪽 카메라 렌즈 기준 각 픽셀의 disparity value를 array 형태로 변환
  •  array에서 값이 0보다 작은(매칭에 실패한 무효픽셀) 픽셀의 disparity값을 None으로 변경
  • 아래 공식에 의해 각 픽셀이 카메라로부터 떨어진 거리를 계산
Depth = Baseline x Focal_length_L / Disparity

Depth = 카메라 원점으로부터 해당 픽셀에 투영된 물체까지 떨어진 z방향 실제거리

Baseline = 왼쪽과 오른쪽 카메라 렌즈 중심점 사이의 거리

Focal_length_L = 왼쪽 카메라 렌즈의 초점거리

Disparity = 각 픽셀의 disparity value

 

4.2 Depth 정확도 측정 실험

앞서 설명한 방법대로 개발한 코드의 거리측정 정확도를 실험을 통해 분석해보았습니다.

0.5~2m 범위 정확도 측정실험 결과

실험을 통해 측정된 카메라로부터 박스까지의 거리는 위 그림과 같이 1m 까지는 5% 범위의 오차율을 보였지만 1m가 넘어가면서 꽤 큰 측정오차를 보였습니다.

 

4.3 RGB, Depth frame 1:1 정렬

최종 목표인 RGB 이미지에서 segmentation된 특정 픽셀까지의 z축 거리를 계산하기 위해서는 RGB frame과 Depth frame이 1:1 정렬되어야 했습니다. 그래서 저는 아래 그림과 같이 왼쪽 카메라 센서에서 촬영된 RGB 이미지에서 numDisparity값만큼 픽셀을 Crop하여 RGB frame과 Depth frame을 1:1 mapping시켰습니다. 이를 통해 RGB 이미지에서 segmentation된 특정 픽셀들까지의 실제 Z축 거리를 계산할 수 있었습니다.

RGB, Depth frame 1:1 정렬방법

 

 

5. Point Cloud 생성

앞서 측정한 Depth 정확도 결과를 정성적으로 확인하고자 Depth 정보와 Camera Matrix(초점거리, 주점 등)를 이용하여 3D point cloud 형태의 데이터 생성하였습니다.

Depth 정보와 Camera Matrix로 3D point cloud를 생성하는 공식은 다음과 같습니다. 

Z = depth value of the pixel (u,v) from the depth map
X = (( u - c_x) * Z) / (f_x)
Y = (( v - c_y) * Z) / (f_y)

u: 2D 픽셀좌표계에서 x방향 픽셀좌표

v: 2D 픽셀좌표계에서 y방향 픽셀좌표

f_x: x방향 초점거리(픽셀단위)

f_y: y방향 초점거리(픽셀단위)

c_x: x방향 주점

c_y: y방향 주점

 

Nova Vision 카메라로 생성된 3D point cloud 데이터 예시

 

위 공식에 의해 재구성된 3D point cloud입니다. 전통적인 삼각측량 방식으로도 꽤 깔끔한 3D point cloud 데이터를 생성할 수 있었습니다. 

 

5.1 Future work

논문 및 구글링을 통해 찾아본 결과, 스테레오 방식 Depth Estimation 기술의 정확도 개선을 위해 실험해볼 수 있는 몇가지 방법이 있었습니다.

  • Raw data 입력: 일반적으로 카메라로 촬영되어 저장되는 이미지 file은 jpg(손실압축), png(무손실 압축) 방법 등이 있지만 카메라 capture 단계에서 Raw data format(YUV2)으로 저장하면 이미지 인코딩 과정에서 왜곡되는 RGB값 문제를 해결하여 더 정확한 거리측정이 가능하다는 글을 보았습니다. 앞으로 기능 구현 및 실험 예정입니다.
  • 컬러/흑백 이미지 입력: 앞서 Disparity map 생성과정에서 입력되는 이미지를 컬러로 입력했을때보다 흑백으로 입력했을때 더 정확한 Disparity map이 생성되었습니다. stackoverflow에서 찾아본 결과 컬러 이미지는 block maching과정에서 3개 채널(RGB)에서 각각 block maching을 진행한 뒤 이를 평균내어 Disparity map을 생성하고, 흑백 이미지는 1개 채널에서 block maching을 진행하기 때문에 상황에 따라 컬러/흑백으로 만들어진 Disparity map의 결과가 달라질 수 있다고 확인하였습니다. 따라서 위 두가지 경우중에 어떤 방식이 우리 카메라에 더 적합할 지 실험을 통해 비교해볼 예정입니다.
  • Deep-Learning 측정방식: 딥러닝 방식으로 각각의 카메라로 촬영된 여러 이미지를 학습하고 이를 바탕으로 Depth map을 생성하는 방식이 최근 많이 사용되는 것을 확인하였습니다. 기존에 사용하던 stereolabs ZED2i camera 역시 neural depth mode라는 기능을 제공하는데 전통적인 방식보다 매우 정확한 측정결과를 제공하였기 때문에 이를 직접 기능구현 해보고 실험을 통해 전통적인 방식과 비교해볼 예정입니다.

 H/W 개발 - 임베디드보드

1. 필요기능 정의

: 초기 기획한 임베디드보드에 필요한 기능은 다음과 같습니다.

- 카메라 연결 : 카메라와 보드가 연결되는 방식은 USB 2.0 혹은 FFC 케이블 방식 둘중 하나를 지원 필요

- 촬영 데이터 송신: 일정시간마다 촬영된 데이터를 보드에서 중앙서버로 FTP or SFTP 방식으로 송신해야 하므로 이를 위한 칩셋 필요

- 네트워크 연결: 네트워크는 농장 상황에 따라 달라지므로 이더넷 연결방식과 Wifi 방식 모두를 지원 필요

- 장치 동작상태 알림: 장치 동작상태를 외부에서 확인할 수 있도록 보드에 LED 모듈 탑재 필요

- 카메라 제어: FTP 방식으로 카메라의 해상도, 밝기, 명도 등 카메라를 외부에서 제어할 수 있는 기능 필요

 

2. 임베디드보드 설계 업체선정

: 임베디드보드 설계를 위해 개발업체 선정을 진행하였습니다. 업체 선정은 크몽이라는 아웃소싱 플랫폼을 통해 진행하였고 최종적으로 

"주식회사 힙" 이라는 업체를 선정하였습니다.

 

3. 카메라 모듈 선정

제가 카메라 모듈을 선정할 때 고려한 사항은 다음과 같습니다.

  • 카메라 해상도: stereo depth estimation 방식은 좌우 카메라에서 촬영한 이미지를 block matching 알고리즘을 통해 disparity를 계산하고 이를 통해 해당 픽셀까지의 실제 거리를 계산합니다. 이러한 방식은 해상도가 높을수록 더 정확한 block matching이 가능하기 때문에 가능한 높은 해상도를 지원해야 했습니다. 이때 최대 2K 이상의 해상도를 지원하는 칩셋은 대부분 Aplication Processor(라즈베리파이 등 고성능 칩셋)에 속했고, 최대 FHD급의 해상도를 지원하는 칩셋은 Micro Processor unit에 속했습니다. 이번 프로젝트의 목표는 카메라 및 임베디드보드의 생산단가가 10만원이라는 제한조건이 있었기 때문에 MCU를 선택하였고, 결과적으로 카메라 해상도를 FHD(1920x1080)로 결정하였습니다.
  • 칩셋 호환성: 초기에 업체측에서 제안해주신 칩셋은 JENO 사의 "ESP 32"라는 칩셋이었습니다. 하지만 해당 칩셋이 지원하는 최대 해상도는 1600x1200 이어서 구글링을 통해 OO사의 "OO" 이라는 MCU 칩셋이 최대 FHD 해상도를 지원하여 해당 칩셋을 선택하게 되었습니다.
  • 카메라 화각(Field of View): 한대의 카메라로 가능한 넓은 범위의 농장을 촬영해야 했기 때문에 화각이 넓은 카메라 모듈이 필요했습니다. 단순히 광각렌즈로 화각을 넓힌 카메라 모듈이 아닌 왜곡되지 않는 선에서 가능한 화각이 넓은 카메라 렌즈를 탑재한 카메라 모듈을 찾아보았습니다.

그림 3. 알리익스프레스에서 구매 및 테스트한 카메라 모듈 리스트

 

위 첨부한 다양한 카메라 모듈과 기존 보유하고 있던 카메라 모듈을 테스트해본 결과 화각 95도를 갖는 카메라 모듈이 왜곡이 발생하지 않는 선에서 가장 큰 화각을 보유한 카메라였습니다.

  • 카메라 선명도: 카메라의 선명도는 이미지 센서의 성능에 의해 결정됩니다. 예를들어 동일한 해상도를 지원하는 카메라라고 하더라도 이미지 센서의 성능에 따라 아래 그림과 같이 선명도의 차이가 발생합니다. 예산과 이미지 센서의 성능을 고려하여 적당한 카메라 모듈을 선택하였습니다.

그림 4. 이미지 센서별 카메라 선명도 테스트

 

4. 임베디드 보드 설계

개발될 스테레오 카메라는 다양한 농장 천장에 설치되기 때문에 낮게는 3m 높게는 7m 농장 천장에 설치됩니다. 이에따라 최소 0.4m~최대 10m 범위 내에서 카메라가 동작하도록 설계를 진행하였습니다.

 

스테레오 카메라 거리 측정범위 결정방법 모식도

 

위 공식은 수학적으로 스테레오 카메라의 동작 범위를 계산하는 공식입니다. 스테레오 카메라의 깊이측정 범위는 카메라 렌즈 사이 거리, 카메라 화각, 카메라 해상도 3가지에 의해 결정됩니다. 예를들어 저희와 같이 1920x1080 해상도에 카메라 가로방향 해상도가 82.7996도일 때 0.4m의 최소 동작범위를 설정하기 위해서는 카메라 렌즈 사이 거리가 80mm가 필요합니다. 이에따라 임베디드보드에 탑재될 두 개의 카메라 렌즈 사이의 거리는 80mm로 결정하였습니다.

 

Issue 1 - 네트워크 연결문제

: 초기 테스트용(설계 변경 전) 샘플보드를 받아보고 천안에 위치한 협업농가에 설치 및 테스트를 진행하였습니다.

 

네트워크 연결 테스트용 샘플보드

 

테스트 과정에서 공유기와 거리가 먼 몇몇 카메라에서 네트워크 연결 문제가 발생하였는데 결론적으로 wifi 모듈이 최대 20m 거리까지 연결될 것으로 예상하였으나 농장 천장에 설치된 철제 H빔이 wifi 신호를 차단한다는 사실을 알게되었습니다.

 

협업농가 농장 내부사진

 

단지 해당 농가만 문제가 된다면 이더넷 연결로 대체하면 되겠지만, 지금까지 방문해본 5개의 농가 천장은 위 사진과 같이 대부분 H빔(철제 구조물)이 천장을 받치는 구조로 되어있었습니다. 따라서 임베디드보드에서 wifi 기능없이 이더넷 연결방식만으로 네트워크를 지원하도록 설계를 변경하였습니다.

 

Issue 2 - 전원 연결문제

농장 내부에 카메라를 설치하고 개별 카메라마다 전원, 이더넷 2개의 케이블이 필요로해서 생각보다 설치과정이 번거로웠습니다.

이에대한 해결책으로 POE 방식을 통해 하나의 랜 케이블로 전원, 네트워크 둘다 커버할 수 있도록 하고자 하였습니다.

임베디드보드 개발업체와 미팅한 결과 카메라 및 칩셋의 동작을 위해 5V 가량의 전압이 필요하다고 하였고 POE 방식으로 전원을 공급할 수 있다고 확인받아 위 방식대로 설계를 변경하기로 결정했습니다.

 

 H/W 개발 - 제품 하우징

제품 하우징 설계 및 샘플 제작을 위해 다음과 같은 순서로 작업을 진행하였습니다.

 

1. 필요기능 정의

초기 기획한 하우징에 필요한 기능은 다음과 같습니다.

  • 케이블 배선: 임베디드보드에 필요한 전원 케이블, 이더넷 케이블 2개 port 필요 
  • 방진 및 방수 설계: 개발될 하우징은 축산농장과 같이 분진 및 습기가 매우 많은 환경에서 사용해야 하므로 IP55(분진 및 분사되는 물로부터 보호) 등급을 충족해야 함
  • 카메라 마운트: 카메라는 바닥을 바라보는 방향으로 설치되므로 하우징 후면부가 천장과 마운트될 수 있도록 마운트 홀 위치 고려해야 함
  • 디자인: 디자인적인 요소를 고려해야함(레퍼런스 제공예정)
  • 카메라 전면부 설계: 카메라 전면부는 유리나 투명한 플라스틱 재질로 제작하고 카메라 렌즈 위치를 제외한 나머지 부분은 불투명 필름처리 해야함

 

2. 하우징 개발 업체선정

: 제품 하우징 개발을 위해 업체 선정을 진행하였습니다. 업체 선정은 크몽이라는 아웃소싱 플랫폼을 통해 진행하였고 3개업체 미팅 후 최종적으로 "OO" 이라는 업체를 선정하였습니다. 해당업체를 선택한 이유는 업체가 중국에 위치해있어서 현지 협력업체를 통해 샘플 가공을 저렴하게 진행할 수 있었고, 해외박람회 출품 경험을 다수 보유하고 있었습니다.

 

3. 디자인 레퍼런스 제공

: 하우징 제작을 시작할 당시 아직 임베디드보드 설계를 끝마치지 못한 상황이었습니다. 그러나 박람회 출품일정 때문에 임베디드보드 설계가 끝날때까지 기다릴 수 없었기 때문에 우선적으로 임베디드보드 제작을 위한 최소한의 보드 사이즈를 정한 뒤 이에 맞춰 하우징 디자인부터 진행하였습니다. 저희가 원한 디자인의 키워드는 다음과 같았습니다.

  • compact: 기존 산업용 카메라들처럼 거대하고 돔형이나 박스같이 투박한 모양보다는 최대한 작고 단순한 디자인을 원했습니다. 디자이너님께 원하는 느낌을 설명하기 위해 애플의 아이맥 제품을 레퍼런스로 설명드렸습니다.
  • fancy: AI 기술이 접목된 첨단 카메라라는 것을 보여주고 싶었습니다. 그래서 제품이 조금 무겁더라도 알류미늄과 같은 재질에 검정색 도장을 입힌 우주선에 쓰일 것 같은 재질을 원했습니다. 디자이너님께 원하는 느낌을 설명하기 위해 Intel realsense D455, stereolabs ZED X 제품을 레퍼런스로 설명드렸습니다.

업체측에 전달한 디자인 레퍼런스

 

4. 디자인 시안 확인 및 피드백 전달

2023년 6월 12일 디자이너님으로부터 디자인 시안 2개를 전달받았습니다. A안은 Intel realsense 제품의 라운드한 디자인을 오마주하였고 B안은 차량 블랙박스에서 인사이트를 얻어서 디자인해보았다고 하였습니다.

 

디자인 시안

디자인 시안 A,B

 

피드백

: 교수님과 회의를 통해 디자이너님이 보내주신 시안중 A안으로 결정하였습니다. 추가적으로 기능적인 측면에서 수정사항이 있어 디자이너님께 정리하여 전달드렸습니다.

  • 카메라 마운트: 카메라 마운트 홀은 1/4인치 홀 규격을 따르며 하우징 뒷면 정가운데 or 아랫면 정가운데 위치해야함

  • 케이블 배선1: 전원 및 이더넷 케이블은 제품과 일체형이 아닌 외부에서 탈부착이 가능해야 함.

  • 케이블 배선2: 전원 및 이더넷 케이블은 양쪽 사이드에 각각 위치한 방식이 아닌 한곳에 붙어있어야 함

 

5. 제품 설계

: 2023년 6월 26일 기준 임베디드보드 설계가 완료되어 보드 설계도를 기반으로 제품 설계를 진행하였습니다.

  • 하우징 재료 선정: 하우징은 초기 알류미늄 재질에 무광블랙 도장을 진행하려고 하였으나, 알류미늄 재질의 특성상 공유기로부터 임베디드보드로 전달되는 대부분의 wifi 신호가 차단된다는 업체측 의견에 따라 방염 ABS(플라스틱) 재질에 무광블랙 도장을 진행하기로 하였습니다. 또한 실제 축산농가에서는 가축이 출하된 뒤, 매우 강력한 고압수로 농장 전체를 청소하기 때문에 파손 위험이 있는 유리보다는 투명 PC 판을 사용하기로 하였습니다.
  • 방진 및 방수 설계: 탈부착이 가능하면서 IP55 등급의 방진 방수 기능을 구현하기 위해 아래와 같이 모듈형 암수 케이블을 알리익스프레스에서 찾아서 선정하였습니다.

모듈형 암수 방수케이블

  • 카메라 마운트: 카메라 마운트는 알리익스프레스에서 적당한 가격의 1/4인치 마운트 및 인서트 너트를 찾아서 업체측에 확인요청드렸고 설치 가능하다는 답변을 받았습니다.

6. 하우징 목업 제작

: 완성된 하우징 설계도를 토대로 업체측에서 목업제작을 진행하였습니다. 목업 제작은 약 2주정도 소요되었고 결과적으로 7월 22일경 택배로 제품 목업 20개를 받아볼 수 있었습니다.

 

 

이렇게 6단계에 걸쳐 제품 하우징 제작을 완료하였습니다.

 

Reference