본문 바로가기
영상처리/스테레오 비전

스테레오 카메라 및 OpenCV(Python/C++)를 사용한 깊이 추정

by j2b2 2023. 4. 10.

https://learnopencv.com/depth-perception-using-stereo-camera-python-c/

 

Stereo Camera Depth Estimation With OpenCV (Python/C++)

Stereo Camera Depth Estimation with OpenCV- Disparity map for rectified stereo image pair, depth map from disparity map-Bonus code for obstacle avoidance system

learnopencv.com

위 글을 구글 번역기로 돌린 결과물


 

스테레오 카메라 및 OpenCV(Python/C++)를 사용한 깊이 추정

로봇이 어떻게 자율적으로 탐색하고, 다른 물체를 잡거나, 움직이는 동안 충돌을 피하는지 궁금한 적이 있습니까? 스테레오 비전 기반 깊이 추정을 사용하는 것은 이러한 응용 프로그램에 사용되는 일반적인 방법입니다. 이 게시물에서는 스테레오 매칭과 깊이 인식을 위한 고전적인 방법에 대해 논의합니다. 스테레오 카메라와 OpenCV를 사용하여 깊이 지각을 설명합니다. 실습 경험을 위해 Python 및 C++로 코드를 공유합니다.

이 게시물은 다음 문서로 구성된 공간 AI 소개 시리즈의 일부입니다.

  1. 에피폴라 기하학 및 스테레오 비전 소개
  2. OpenCV를 사용하여 저렴한 스테레오 카메라 만들기
  3. 스테레오 카메라 및 OpenCV를 사용한 깊이 추정
  4. 앞으로 더

이 게시물과 내용을 따르고 이해하려면 관련된 기본 사항에 대한 일정 수준의 이해가 필요합니다. 아직 익숙하지 않은 경우 계속 진행하기 전에 읽어야 할 전제 조건 게시물 목록은 다음과 같습니다.

  1. OpenCV를 사용한 카메라 보정
  2. 렌즈 왜곡 이해하기

물체를 잡고 움직이는 동안 충돌을 피하는 것과 같은 작업을 위해 로봇은 3D 공간에서 세계를 인식해야 합니다. 다음은 스테레오 비전 기반 깊이 추정을 사용하여 세상을 3D로 인식하는 장애물 회피 로봇의 시연입니다.

 

https://www.youtube.com/watch?v=F1BLRJMhlLU 

OAK-D를 사용한 스테레오 비전 장애물 회피 데모 비디오

 

 

 

장애물 회피 데모 비디오 이 시리즈의 처음 두 게시물에서 배운 내용 요약

공간 AI 소개 시리즈의 첫 번째 게시물에서 우리는 주어진 장면의 깊이(3D 구조)를 추정하기 위한 두 가지 필수 요구 사항인 점 대응과 카메라의 상대 위치에 대해 논의했습니다.

요약: 해당 포인트는 동일한 3D 포인트의 투영인 서로 다른 이미지의 픽셀입니다. 스테레오 이미지 쌍의 두 이미지에서 캡처된 모든 3D 포인트에 대한 포인트 대응을 찾는 것은 조밀한 깊이 맵을 찾고 3D 세계를 인식하는 데 사용할 수 있는 조밀한 대응을 제공합니다.

우리는 조밀한 대응을 찾는 것이 종종 어렵다는 것을 관찰했으며 점 대응을 위한 검색 공간을 줄이는 데 있어 에피폴라 기하학의 아름다움과 효율성을 실현했습니다. 우리는 또한 평행한 에피폴라 라인을 갖는 것이 어떻게 문제를 더 단순화하는지 논의했습니다. 해당 지점은 수평 변위와 관련되기 때문입니다. 이 변위를 시차라고 합니다.

 

 

공간 AI 소개 시리즈의 두 번째 게시물에서는 맞춤형 저가 스테레오 카메라를 만들었습니다. 또한 스테레오 정류 및 보정에 대해서도 논의했습니다. 에피폴라 라인을 수평으로 만들기 위해 스테레오 정류 및 보정이 수행됩니다. 이러한 계산을 통해 조밀한 대응 관계를 쉽게 찾을 수 있습니다.

이 게시물은 수정된 스테레오 이미지 쌍에 대한 조밀한 대응과 디스패리티 맵을 찾기 위한 블록 매칭 및 반 글로벌 블록 매칭 방법에 대해 설명합니다. 또한 디스패리티 맵에서 깊이 맵을 찾는 방법도 배웁니다. 마지막으로 깊이 추정을 사용하여 장애물 회피 시스템을 만드는 방법을 살펴보겠습니다.

나머지 내용은 다음과 같이 구성됩니다.

목차
1. 고밀도 스테레오 대응을 위한 블록 매칭.
    2. 블록 매칭 알고리즘의 매개변수를 조정하는 GUI.
    3. 각 매개변수의 효과를 이해합니다.
4. 디스패리티 맵에서 깊이 맵으로.
5. 장애물 회피 시스템.
6. OpenCV AI Kit with Depth(OAK-D)와의 비교.
7. 요약.

 

 


밀집 스테레오 대응을 위한 블록 매칭

수평 스테레오 카메라 설정이 있는 경우 수정된 스테레오 이미지 쌍의 해당 지점은 동일한 Y 좌표를 갖습니다. 그러면 해당 점을 찾는 방법은 무엇입니까?

스테레오 이미지 쌍의 동일한 행에 있는 픽셀 값을 단순히 비교하는 순진한 접근 방식을 생각할 수 있습니다. 그러나 이것은 강력한 방법이 아닙니다. 다른 이미지에 해당하는 여러 픽셀은 동일한 픽셀 강도를 가질 수 있습니다. 또한, 촬상 센서의 차이나 노출 값의 차이와 같은 실질적인 제약을 고려할 때 해당 픽셀의 픽셀 값이 동일하지 않을 수 있습니다.



블록 매칭 알고리즘의 작동

더 나은 접근 방식은 일부 인접 픽셀도 고려하는 것입니다. 이것이 블록 매칭 알고리즘에서 우리가 하는 일입니다. 스테레오 대응을 위해 스캔하는 행을 스캔라인이라고 합니다. 이것은 좋은 소리입니다! 그러나 최고의 일치를 어떻게 수량화합니까? 창이 얼마나 잘 일치하는지 수량화하는 데 사용할 수 있는 메트릭이 있습니까?

일치를 수량화하는 데 사용할 수 있는 SAD(Sum of Absolute Differences), SSD(Sum of Squared Differences) 및 NCC(Normalized Cross Correlation)와 같은 여러 메트릭이 있습니다. 예를 들어, SAD 점수가 가장 낮은 쌍이 전체 픽셀 단위 합계가 가장 작기 때문에 가장 잘 일치합니다. 그림 1은 스테레오 이미지 쌍의 스캔라인 및 참조 블록을 보여줍니다. 각 스캔라인의 SAD에 대한 플롯은 가장 오른쪽 이미지에 표시됩니다.

그림 1 - 왼쪽 이미지는 대상 창과 함께 스캔라인을 보여줍니다.

 

중앙 이미지는 스캔라인(파란색), 가장 잘 일치하는 블록(녹색) 및 두 번째로 가장 잘 일치하는 블록(빨간색)을 보여줍니다.
오른쪽 이미지는 왼쪽 이미지의 참조 블록과 오른쪽 이미지의 스캔라인을 따른 슬라이딩 창 사이의 SAD(Sum of Absolute Difference)에 대한 플롯입니다.

위의 방법은 하나의 해당 쌍만 제공합니다. 우리는 스캔라인의 모든 픽셀에 대한 대응을 찾고 싶습니다. 간단한 해결책은 스캔라인의 모든 픽셀에 대해 프로세스를 반복하는 것입니다. 그러나 이것은 노이즈 출력으로 이어집니다. 실제로 모든 스캔라인에 대해 일대일 대응을 얻는 것은 불가능합니다. 주어진 블록에 대해 여러 개의 일치 항목이 있는 이유는(반복 텍스처 또는 텍스처가 없는 영역의 경우) 이상적인 일치가 없을 수 있습니다(오클루전으로 인해).



동적 프로그래밍과 스캔라인

동적 프로그래밍은 스캔라인에 대해 일대일 대응을 적용하는 데 사용되는 표준 방법입니다. 목표는 스캔라인에 대한 일대일 대응을 찾는 것이므로 전체 비용을 최소화하여 위에서 언급한 실제적인 문제를 극복합니다.

그림 2 - 주어진 스캔라인에 대한 절대차(SAD) 값의 합계.


그림 2는 주어진 스캔라인에 대한 SAD 값을 보여줍니다. 왼쪽 그림은 고유한 이미지로 구성된 스테레오 이미지 쌍에 해당하고 오른쪽 그림은 단일 이미지를 사용하여 비교를 위해 두 번 전달할 때 동일한 스캔라인에 대한 SAD 값을 보여줍니다.



SAD 값 설명

더 밝은 값은 더 큰 불일치를 나타내고 더 어두운 값은 더 나은 일치를 나타냅니다. 대응을 찾으려면 경로에 포함된 값의 합이 최소가 되도록 왼쪽 아래 모서리에서 오른쪽 위 모서리까지의 경로를 찾아야 합니다. 그림 2의 오른쪽 이미지의 경우 이러한 경로가 대각선(왼쪽 하단 모서리에서 오른쪽 상단 모서리까지 가는 검은색 가는 선)을 따라 명확하게 보입니다. 왼쪽 그림의 경우 대각선에 더 밝은 값도 포함되어 있습니다(오클루전으로 인해 일치하는 블록이 없으므로 가장 작은 SAD 값도 상대적으로 밝음).

OpenCV는 블록 매칭 알고리즘인 StereoBM 클래스에 대한 구현을 제공합니다. StereoBM 클래스의 메서드는 한 쌍의 수정된 스테레오 이미지에 대한 디스패리티 맵을 계산합니다. 카메라 센서의 종류, 카메라 간의 거리 및 기타 여러 요인에 따라 다양한 스테레오 카메라 설정 조합이 가능합니다. 따라서 고정된 매개변수 세트는 스테레오 카메라 설정의 모든 가능한 조합에 대해 우수한 품질의 디스패리티 맵을 제공할 수 없습니다.

 

 

블록 일치 알고리즘의 매개변수를 조정하는 GUI

이 섹션에서는 출력 디스패리티 맵을 개선하기 위해 블록 일치 알고리즘의 매개변수를 조정하는 GUI를 만듭니다. OpenCV GUI 도구를 사용하여 매개변수를 변경하는 트랙바를 생성합니다. 코드는 C++ 및 Python에서 공유됩니다.


C++

#include <opencv2/opencv.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <stdio.h>
#include <iostream>
#include "opencv2/imgcodecs.hpp"

// initialize values for StereoSGBM parameters
int numDisparities = 8;
int blockSize = 5;
int preFilterType = 1;
int preFilterSize = 1;
int preFilterCap = 31;
int minDisparity = 0;
int textureThreshold = 10;
int uniquenessRatio = 15;
int speckleRange = 0;
int speckleWindowSize = 0;
int disp12MaxDiff = -1;
int dispType = CV_16S;

// Creating an object of StereoSGBM algorithm
cv::Ptr<cv::StereoBM> stereo = cv::StereoBM::create();

cv::Mat imgL;
cv::Mat imgR;
cv::Mat imgL_gray;
cv::Mat imgR_gray;

// Defining callback functions for the trackbars to update parameter values

static void on_trackbar1( int, void* )
{
  stereo->setNumDisparities(numDisparities*16);
  numDisparities = numDisparities*16;
}

static void on_trackbar2( int, void* )
{
  stereo->setBlockSize(blockSize*2+5);
  blockSize = blockSize*2+5;
}

static void on_trackbar3( int, void* )
{
  stereo->setPreFilterType(preFilterType);
}

static void on_trackbar4( int, void* )
{
  stereo->setPreFilterSize(preFilterSize*2+5);
  preFilterSize = preFilterSize*2+5;
}

static void on_trackbar5( int, void* )
{
  stereo->setPreFilterCap(preFilterCap);
}

static void on_trackbar6( int, void* )
{
  stereo->setTextureThreshold(textureThreshold);
}

static void on_trackbar7( int, void* )
{
  stereo->setUniquenessRatio(uniquenessRatio);
}

static void on_trackbar8( int, void* )
{
  stereo->setSpeckleRange(speckleRange);
}

static void on_trackbar9( int, void* )
{
  stereo->setSpeckleWindowSize(speckleWindowSize*2);
  speckleWindowSize = speckleWindowSize*2;
}

static void on_trackbar10( int, void* )
{
  stereo->setDisp12MaxDiff(disp12MaxDiff);
}

static void on_trackbar11( int, void* )
{
  stereo->setMinDisparity(minDisparity);
}


int main()
{
  // Initialize variables to store the maps for stereo rectification
  cv::Mat Left_Stereo_Map1, Left_Stereo_Map2;
  cv::Mat Right_Stereo_Map1, Right_Stereo_Map2;

  // Reading the mapping values for stereo image rectification
  cv::FileStorage cv_file2 = cv::FileStorage("data/stereo_rectify_maps.xml", cv::FileStorage::READ);
  cv_file2["Left_Stereo_Map_x"] >> Left_Stereo_Map1;
  cv_file2["Left_Stereo_Map_y"] >> Left_Stereo_Map2;
  cv_file2["Right_Stereo_Map_x"] >> Right_Stereo_Map1;
  cv_file2["Right_Stereo_Map_y"] >> Right_Stereo_Map2;
  cv_file2.release();

  // Check for left and right camera IDs
  // These values can change depending on the system
  int CamL_id{2}; // Camera ID for left camera
  int CamR_id{0}; // Camera ID for right camera

  cv::VideoCapture camL(CamL_id), camR(CamR_id);

  // Check if left camera is attached
  if (!camL.isOpened())
  {
    std::cout << "Could not open camera with index : " << CamL_id << std::endl;
    return -1;
  }

  // Check if right camera is attached
  if (!camL.isOpened())
  {
    std::cout << "Could not open camera with index : " << CamL_id << std::endl;
    return -1;
  }

  // Creating a named window to be linked to the trackbars
  cv::namedWindow("disparity",cv::WINDOW_NORMAL);
  cv::resizeWindow("disparity",600,600);

  // Creating trackbars to dynamically update the StereoBM parameters
  cv::createTrackbar("numDisparities", "disparity", &numDisparities, 18, on_trackbar1);
  cv::createTrackbar("blockSize", "disparity", &blockSize, 50, on_trackbar2);
  cv::createTrackbar("preFilterType", "disparity", &preFilterType, 1, on_trackbar3);
  cv::createTrackbar("preFilterSize", "disparity", &preFilterSize, 25, on_trackbar4);
  cv::createTrackbar("preFilterCap", "disparity", &preFilterCap, 62, on_trackbar5);
  cv::createTrackbar("textureThreshold", "disparity", &textureThreshold, 100, on_trackbar6);
  cv::createTrackbar("uniquenessRatio", "disparity", &uniquenessRatio, 100, on_trackbar7);
  cv::createTrackbar("speckleRange", "disparity", &speckleRange, 100, on_trackbar8);
  cv::createTrackbar("speckleWindowSize", "disparity", &speckleWindowSize, 25, on_trackbar9);
  cv::createTrackbar("disp12MaxDiff", "disparity", &disp12MaxDiff, 25, on_trackbar10);
  cv::createTrackbar("minDisparity", "disparity", &minDisparity, 25, on_trackbar11);

  cv::Mat disp, disparity;

  while (true)
  {
    // Capturing and storing left and right camera images
    camL >> imgL;
    camR >> imgR;

    // Converting images to grayscale
    cv::cvtColor(imgL, imgL_gray, cv::COLOR_BGR2GRAY);
    cv::cvtColor(imgR, imgR_gray, cv::COLOR_BGR2GRAY);

    // Initialize matrix for rectified stereo images
    cv::Mat Left_nice, Right_nice;

    // Applying stereo image rectification on the left image
    cv::remap(imgL_gray,
              Left_nice,
              Left_Stereo_Map1,
              Left_Stereo_Map2,
              cv::INTER_LANCZOS4,
              cv::BORDER_CONSTANT,
              0);

    // Applying stereo image rectification on the right image
    cv::remap(imgR_gray,
              Right_nice,
              Right_Stereo_Map1,
              Right_Stereo_Map2,
              cv::INTER_LANCZOS4,
              cv::BORDER_CONSTANT,
              0);

    // Calculating disparith using the StereoBM algorithm
    stereo->compute(Left_nice,Right_nice,disp);

    // NOTE: Code returns a 16bit signed single channel image,
// CV_16S containing a disparity map scaled by 16. Hence it 
    // is essential to convert it to CV_32F and scale it down 16 times.

    // Converting disparity values to CV_32F from CV_16S
    disp.convertTo(disparity,CV_32F, 1.0);

    // Scaling down the disparity values and normalizing them 
    disparity = (disparity/16.0f - (float)minDisparity)/((float)numDisparities);

    // Displaying the disparity map
    cv::imshow("disparity",disparity);

    // Close window using esc key
    if (cv::waitKey(1) == 27) break;
  }
  return 0;
}


Python

import numpy as np 
import cv2

# Check for left and right camera IDs
# These values can change depending on the system
CamL_id = 2 # Camera ID for left camera
CamR_id = 0 # Camera ID for right camera

CamL= cv2.VideoCapture(CamL_id)
CamR= cv2.VideoCapture(CamR_id)

# Reading the mapping values for stereo image rectification
cv_file = cv2.FileStorage("data/stereo_rectify_maps.xml", cv2.FILE_STORAGE_READ)
Left_Stereo_Map_x = cv_file.getNode("Left_Stereo_Map_x").mat()
Left_Stereo_Map_y = cv_file.getNode("Left_Stereo_Map_y").mat()
Right_Stereo_Map_x = cv_file.getNode("Right_Stereo_Map_x").mat()
Right_Stereo_Map_y = cv_file.getNode("Right_Stereo_Map_y").mat()
cv_file.release()

def nothing(x):
    pass

cv2.namedWindow('disp',cv2.WINDOW_NORMAL)
cv2.resizeWindow('disp',600,600)

cv2.createTrackbar('numDisparities','disp',1,17,nothing)
cv2.createTrackbar('blockSize','disp',5,50,nothing)
cv2.createTrackbar('preFilterType','disp',1,1,nothing)
cv2.createTrackbar('preFilterSize','disp',2,25,nothing)
cv2.createTrackbar('preFilterCap','disp',5,62,nothing)
cv2.createTrackbar('textureThreshold','disp',10,100,nothing)
cv2.createTrackbar('uniquenessRatio','disp',15,100,nothing)
cv2.createTrackbar('speckleRange','disp',0,100,nothing)
cv2.createTrackbar('speckleWindowSize','disp',3,25,nothing)
cv2.createTrackbar('disp12MaxDiff','disp',5,25,nothing)
cv2.createTrackbar('minDisparity','disp',5,25,nothing)

# Creating an object of StereoBM algorithm
stereo = cv2.StereoBM_create()

while True:

# Capturing and storing left and right camera images
retL, imgL= CamL.read()
retR, imgR= CamR.read()

# Proceed only if the frames have been captured
if retL and retR:
imgR_gray = cv2.cvtColor(imgR,cv2.COLOR_BGR2GRAY)
imgL_gray = cv2.cvtColor(imgL,cv2.COLOR_BGR2GRAY)

# Applying stereo image rectification on the left image
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)

# Updating the parameters based on the trackbar positions
numDisparities = cv2.getTrackbarPos('numDisparities','disp')*16
blockSize = cv2.getTrackbarPos('blockSize','disp')*2 + 5
preFilterType = cv2.getTrackbarPos('preFilterType','disp')
preFilterSize = cv2.getTrackbarPos('preFilterSize','disp')*2 + 5
preFilterCap = cv2.getTrackbarPos('preFilterCap','disp')
textureThreshold = cv2.getTrackbarPos('textureThreshold','disp')
uniquenessRatio = cv2.getTrackbarPos('uniquenessRatio','disp')
speckleRange = cv2.getTrackbarPos('speckleRange','disp')
speckleWindowSize = cv2.getTrackbarPos('speckleWindowSize','disp')*2
disp12MaxDiff = cv2.getTrackbarPos('disp12MaxDiff','disp')
minDisparity = cv2.getTrackbarPos('minDisparity','disp')

# Setting the updated parameters before computing disparity map
stereo.setNumDisparities(numDisparities)
stereo.setBlockSize(blockSize)
stereo.setPreFilterType(preFilterType)
stereo.setPreFilterSize(preFilterSize)
stereo.setPreFilterCap(preFilterCap)
stereo.setTextureThreshold(textureThreshold)
stereo.setUniquenessRatio(uniquenessRatio)
stereo.setSpeckleRange(speckleRange)
stereo.setSpeckleWindowSize(speckleWindowSize)
stereo.setDisp12MaxDiff(disp12MaxDiff)
stereo.setMinDisparity(minDisparity)

# Calculating disparity using the StereoBM algorithm
disparity = stereo.compute(Left_nice,Right_nice)
# NOTE: Code returns a 16bit signed single channel image,
# CV_16S containing a disparity map scaled by 16. Hence it 
# is essential to convert it to CV_32F and scale it down 16 times.

# Converting to float32 
disparity = disparity.astype(np.float32)

# Scaling down the disparity values and normalizing them 
disparity = (disparity/16.0 - minDisparity)/numDisparities

# Displaying the disparity map
cv2.imshow("disp",disparity)

# Close window using esc key
if cv2.waitKey(1) == 27:
break

else:
CamL= cv2.VideoCapture(CamL_id)
CamR= cv2.VideoCapture(CamR_id)


위의 코드를 사용하여 만든 GUI는 블록 매칭 알고리즘의 다양한 매개변수를 변경하는 데 도움이 됩니다. OpenCV의 FileStorage 클래스를 사용하여 매개변수를 저장할 수도 있습니다. FileStorage 클래스 사용법에 대한 자세한 내용은 이전 게시물을 참조하십시오.

 

 

 

각 매개변수의 효과 이해

  • 디스패리티 수(numDisparities): 검색할 디스패리티 값의 범위를 설정합니다. 전체 범위는 최소 디스패리티 값에서 최소 디스패리티 값 + 디스패리티 수까지입니다. 다음 이미지 쌍은 두 개의 서로 다른 디스패리티 범위에 대해 계산된 디스패리티 맵을 보여줍니다. 디스패리티의 수를 증가시키면 디스패리티 맵의 정확도가 증가한다는 것을 분명히 볼 수 있습니다.

 

그림 3 - 두 개의 서로 다른 시차 범위에 대해 계산된 시차 맵을 보여주는 GIF 이미지.

 

  • 블록 크기(blockSize): 수정된 스테레오 이미지 쌍에서 해당 픽셀을 찾기 위해 블록 일치에 사용되는 슬라이딩 창의 크기입니다. 값이 클수록 창 크기가 커집니다. 다음 GIF는 이 매개변수를 늘리면 더 부드러운 디스패리티 맵이 생성됨을 나타냅니다.

 

그림 4 - 블록 일치에 사용되는 슬라이딩 창의 크기가 증가하면 더 부드러운 디스패리티 맵이 생성됨을 보여주는 GIF 이미지.

 

 

 

기타 매개변수

  • 사전 필터 유형(preFilterType): 블록 매칭 알고리즘에 전달하기 전에 이미지에 적용할 사전 필터링 유형을 결정하는 매개변수입니다. 이 단계는 텍스처 정보를 향상시키고 블록 매칭 알고리즘의 결과를 향상시킵니다. 필터 유형은 CV_STEREO_BM_XSOBEL 또는 CV_STEREO_BM_NORMALIZED_RESPONSE일 수 있습니다.
  • 사전 필터 크기(preFilterSize): 사전 필터링 단계에서 사용되는 필터의 창 크기입니다.
  • 사전 필터 캡(preFilterCap): 필터링된 출력을 특정 값으로 제한합니다.
  • 최소 디스패리티(minDisparity): 검색할 디스패리티의 최소값. 대부분의 시나리오에서는 0으로 설정됩니다. 스테레오 카메라 설정에 따라 음수 값으로 설정할 수도 있습니다.
  • 텍스처 임계값(textureThreshold): 안정적인 일치를 위한 텍스처 정보가 충분하지 않은 영역을 필터링합니다.
  • Uniqueness Ratio(uniquenessRatio): 또 다른 사후 필터링 단계입니다. 최상의 일치 디스패리티가 검색 범위의 다른 모든 디스패리티보다 충분히 좋지 않으면 픽셀이 필터링됩니다. 다음 GIF는 고유성 비율을 높이면 필터링되는 픽셀 수가 증가함을 보여줍니다.

 

그림 4 - 요점을 나타내는 GIF 이미지: 고유성 비율을 높이면 필터링되는 픽셀 수가 증가합니다.

 

 


기타 매개변수, 계속

  • 스페클 범위(speckleRange) 및 스페클 창 크기(speckleWindowSize): 스페클은 개체의 경계 근처에서 생성되며, 일치하는 창이 한쪽의 전경을 포착하고 다른 쪽의 배경을 포착합니다. 이러한 아티팩트를 제거하기 위해 두 개의 매개변수가 있는 스페클 필터를 적용합니다. 반점 범위는 불일치 값이 동일한 얼룩의 일부로 간주되어야 하는 정도를 정의합니다. 반점 창 크기는 그 아래에서 디스패리티 블롭이 "반점"으로 무시되는 픽셀 수입니다.
  • disp12MaxDiff: 픽셀은 왼쪽 이미지에서 오른쪽 이미지로, 오른쪽 이미지에서 왼쪽 이미지로 양방향으로 일치합니다. disp12MaxDiff는 원래 왼쪽 픽셀과 다시 일치하는 픽셀 간의 최대 허용 차이를 정의합니다.

 

주목해야 할 중요한 점은 블록 일치 클래스의 메서드가 16으로 스케일된 디스패리티 값을 포함하는 16비트 부호 있는 단일 채널 이미지를 반환한다는 것입니다. 따라서 이러한 고정 소수점 표현에서 실제 디스패리티 값을 얻으려면 다음을 나누어야 합니다. 16의 차이 값.

OpenCV는 또한 Hirschmüller의 원래 SGM[2] 알고리즘의 구현인 StereoSGBM을 제공합니다. SGBM은 Semi-Global Block Matching의 약자입니다. 또한 Brichfield 등이 제안한 하위 픽셀 추정을 구현합니다. [3]

단순 블록 매칭의 경우 최소 비용을 사용하면 가끔 잘못된 매칭이 나올 수 있습니다. 잘못된 일치는 올바른 일치보다 비용이 낮을 수 있습니다. 따라서 SGBM은 8-연결된 이웃에서 격차의 변화에 ​​페널티를 부여하여 부드러움을 증가시키기 위해 추가적인 제약을 적용합니다. 제약 조건의 경우 여러 방향의 1차원 최소 비용 경로가 집계됩니다.

블록 매칭 기반 알고리즘은 고전적인 컴퓨터 비전 알고리즘입니다. 심층 신경망의 출현 이후 스테레오 이미지 쌍 간의 조밀한 대응을 찾기 위해 여러 딥 러닝 아키텍처가 제안되었습니다. 자세한 내용은 스테레오 매칭에 대한 게시물을 참조하세요.

 

 

 

디스패리티 맵에서 깊이 맵으로

지금까지 얻은 회색조 이미지는 깊이 맵이 아니라 디스패리티 맵일 뿐입니다. 블록 매칭 방법을 사용하여 수정된 스테레오 이미지 쌍에 대한 조밀한 대응을 계산했습니다. 이러한 조밀한 대응(해당 픽셀 간의 이동)을 사용하여 각 픽셀의 시차를 계산했습니다. 에피폴라 기하학에 대한 이전 게시물은 불일치가 깊이와 어떻게 관련되는지에 대한 좋은 직관을 제공합니다.

이 게시물에서는 깊이가 불일치와 어떻게 관련되어 있는지에 대한 이해를 높이고 불일치에서 깊이를 계산하는 표현식을 도출합니다. 다음 그림은 식의 파생을 시각화하는 데 도움이 됩니다.

그림 5 &ndash; 깊이 Z와 디스패리티(x &ndash; x') 사이의 관계를 보여주는 그림.


위 그림에서 시차(x – x')와 깊이 Z 사이의 관계를 다음과 같이 도출할 수 있습니다.

(1) 


여기서 B는 스테레오 카메라 설정의 기준선이고 f는 초점 거리입니다.

이제 실제 설정의 경우 두 카메라의 초점 거리 f가 동일하지 않으며 B를 수동으로 측정하면 오류가 발생할 수도 있습니다. 이것은 우리를 중요한 질문으로 이끕니다. f와 B의 값은 무엇이어야 하며 어떻게 유도합니까?

우리는 그것을 실험적으로 도출합니다. 위의 방정식은 다음과 같이 쓸 수도 있습니다.

(2) 

Where
(3) 


대상 픽셀의 디스패리티 값을 표시하는 GUI를 생성하여 카메라로부터의 거리(Z)를 실질적으로 측정할 수 있습니다. 다중 디스패리티 판독값과 Z(깊이)를 기반으로 최소 제곱 문제를 공식화하여 M을 해결할 수 있습니다. 다음 그래프는 다양한 관찰에서 얻은 데이터 포인트를 강조하는 스테레오 카메라 설정에 대한 깊이와 불일치 사이의 관계를 보여줍니다.

그림 6 - 스테레오 카메라 설정에 대한 깊이와 디스패리티 값 사이의 관계를 보여주는 그래프.


괜찮아! 최소제곱 문제는 재미있을 것 같습니다. 그러나 정확히 무엇을 의미합니까? 그리고 우리는 그것을 어떻게 코딩합니까?

먼저 이전 방정식을 다음과 같이 재정렬합니다.

(4) 



대체하여
(5) 


우리는 다음 방정식을 얻습니다.
(6) 


비교적 쉽죠? M(하나의 방정식, 하나의 변수)을 계산하려면 (Z,a)를 한 번만 읽어야 합니다. 다중 판독값이 필요한 이유와 최소 제곱법을 사용하는 이유는 무엇입니까? 글쎄, 이것은 실용적인 세계에서 완벽한 것은 없기 때문입니다!



실제 시나리오에 대해 이야기할 때 항상 오류의 가능성이 있습니다. 이 경우 사람의 실수(거리 측정 시)가 있거나 알고리즘에 의해 계산된 디스패리티 값이 약간 부정확할 수 있습니다. 또한, 디스패리티 값의 양자화는 약간의 오류를 유발합니다. 따라서 단일 읽기에 의존하는 것은 좋은 생각이 아닙니다.

따라서 (Z,a)를 여러 번 읽습니다. 따라서 방정식의 수가 변수의 수를 초과합니다. 최소제곱법은 모든 판독값과 가장 잘 일치하는 M 값을 찾는 데 도움이 됩니다. 그래픽으로 보면, 그 선으로부터 모든 데이터 포인트의 거리 제곱의 합이 최소가 되도록 선을 찾는 과정입니다.

OpenCV의 solve() 메서드를 사용하여 최소제곱근사 문제에 대한 솔루션을 찾을 수 있습니다. 샘플 코드는 아래에서 공유됩니다.


Python

# solving for M in the following equation
# ||    depth = M * (1/disparity)   ||
# for N data points coeff is Nx2 matrix with values 
# 1/disparity, 1
# and depth is Nx1 matrix with depth values
ret, sol = cv2.solve(coeff,z,flags=cv2.DECOMP_QR)


C++

//solving for M in the following equation
//||    depth = M * (1/disparity)   ||
//for N data points coeff is Nx2 matrix with values 
//1/disparity, 1
//and depth is Nx1 matrix with depth values

cv::Mat Z_mat(z_vec.size(), 1, CV_32F, z_vec.data());
cv::Mat coeff(z_vec.size(), 2, CV_32F, coeff_vec.data());

cv::Mat sol(2, 1, CV_32F);
float M;

// Solving for M using least square fitting with QR decomposition method 
cv::solve(coeff, Z_mat, sol, cv::DECOMP_QR);

M = sol.at<float>(0,0);




M을 풀면 디스패리티 맵을 계산한 후 코드에 다음 방정식을 추가하여 디스패리티 맵을 깊이 맵으로 간단히 변환할 수 있습니다.
(7) 


다음 비디오는 이 게시물과 공유된 코드를 사용하여 불일치 값과 해당 깊이 값의 다중 판독값을 캡처하는 프로세스를 보여줍니다. 보다 정확한 솔루션을 위해 가능한 한 많은 판독값을 취하는 것이 좋습니다.

 

https://www.youtube.com/watch?v=-MWC4tI7K1g 

불일치 값 및 해당 깊이 값의 다중 판독값을 캡처하는 프로세스의 데모 비디오.

 

대박! 이제 우리는 블록 매칭 알고리즘을 사용하여 디스패리티 맵을 계산하는 방법, 스테레오 카메라 설정을 위한 좋은 디스패리티 맵을 제공하기 위해 블록 매칭 알고리즘의 매개변수를 조정하는 방법을 알고 있습니다. 또한 디스패리티 맵에서 깊이 맵을 얻는 방법도 알고 있습니다.

따라서 우리는 컴퓨터가 깊이를 인식하게 할 수 있습니다! 이것을 사용하여 매력적인 애플리케이션을 만들어 봅시다! 스테레오 카메라 설정으로 장애물 회피 시스템을 만들 것입니다. 이 게시물의 시작 부분에 공유된 비디오에서 시연된 것과 유사합니다.

 

 


장애물 회피 시스템

장애물 회피의 기본 개념은 센서에서 물체까지의 거리가 최소 임계 거리보다 가까운지 판단하는 것입니다. 우리의 경우 센서는 스테레오 카메라입니다. 거리 측정 및 장애물 회피에 널리 사용되는 또 다른 유형의 장치는 초음파 센서입니다.

스테레오 카메라와 달리 초음파 센서는 신호 전파에 대해 작동합니다. 알려진 주파수의 신호가 방출됩니다. 물체가 경로를 방해할 때까지 이동합니다. 물체에서 반사되어 센서가 반사된 신호를 받습니다. 신호의 속도를 알면 신호의 전송과 수신 사이에 경과된 시간을 사용하여 센서에서 물체까지의 거리를 계산할 수 있습니다. 박쥐는 비슷한 방법을 사용하여 장애물을 탐색하고 피합니다.

초음파 센서에 비해 스테레오 카메라의 주요 이점은 스테레오 카메라가 더 넓은 시야를 제공한다는 것입니다.



장애물 회피 시스템 구축 단계

그렇다면 스테레오 카메라를 사용하여 장애물 회피 시스템을 만드는 방법은 무엇입니까? 이러한 시스템을 구축하는 단계는 다음과 같습니다.

스테레오 카메라에서 깊이 맵을 가져옵니다.
임계값(최소 깊이 값)에 따라 깊이 값이 임계값보다 작은 깊이 맵의 영역을 결정합니다. inRange() 메서드를 사용하여 이러한 영역을 분할하는 마스크를 만듭니다.
윤곽 감지를 적용하고 가장 큰 윤곽을 찾습니다.
가장 큰 윤곽을 사용하여 새 마스크를 만듭니다.
4단계에서 만든 마스크를 사용하여 meanStdDev() 메서드를 사용하여 평균 깊이 값을 찾습니다.
경고 조치를 수행하십시오. 이 예에서는 장애물의 거리를 빨간색으로 표시합니다.

다음은 장애물 회피 시스템에 대한 코드입니다.


Python

depth_thresh = 100.0 # Threshold for SAFE distance (in cm)

# Mask to segment regions with depth less than threshold
mask = cv2.inRange(depth_map,10,depth_thresh)

# Check if a significantly large obstacle is present and filter out smaller noisy regions
if np.sum(mask)/255.0 > 0.01*mask.shape[0]*mask.shape[1]:

# Contour detection 
contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(contours, key=cv2.contourArea, reverse=True)

# Check if detected contour is significantly large (to avoid multiple tiny regions)
if cv2.contourArea(cnts[0]) > 0.01*mask.shape[0]*mask.shape[1]:

x,y,w,h = cv2.boundingRect(cnts[0])

# finding average depth of region represented by the largest contour 
mask2 = np.zeros_like(mask)
cv2.drawContours(mask2, cnts, 0, (255), -1)

# Calculating the average depth of the object closer than the safe distance
depth_mean, _ = cv2.meanStdDev(depth_map, mask=mask2)

# Display warning text
cv2.putText(output_canvas, "WARNING !", (x+5,y-40), 1, 2, (0,0,255), 2, 2)
cv2.putText(output_canvas, "Object at", (x+5,y), 1, 2, (100,10,25), 2, 2)
cv2.putText(output_canvas, "%.2f cm"%depth_mean, (x+5,y+40), 1, 2, (100,10,25), 2, 2)

else:
cv2.putText(output_canvas, "SAFE!", (100,100),1,3,(0,255,0),2,3)

cv2.imshow('output_canvas',output_canvas)


C++

float depth_thresh = 100.0; // Threshold for SAFE distance (in cm)
cv::Mat mask, mean, stddev, mask2;

// Mask to segment regions with depth less than safe distance
cv::inRange(depth_map, 10, depth_thresh, mask);
double s = (cv::sum(mask)[0])/255.0;
double img_area = double(mask.rows * mask.cols);

std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;

// Check if a significantly large obstacle is present and filter out smaller noisy regions
if (s > 0.01*img_area)
{
  // finding conoturs in the generated mask
  cv::findContours(mask, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
  
  // sorting contours from largest to smallest
  std::sort(contours.begin(), contours.end(), compareContourAreas);

  // extracting the largest contour
  std::vector<cv::Point> cnt = contours[0];

  // Check if detected contour is significantly large (to avoid multiple tiny regions)
  double cnt_area = fabs( cv::contourArea(cv::Mat(cnt)));
  if (cnt_area > 0.01*img_area)
  {
    cv::Rect box;

    // Finding the bounding rectangle for the largest contour
    box = cv::boundingRect(cnt);

    // finding average depth of region represented by the largest contour
    mask2 = mask*0;
    cv::drawContours(mask2, contours, 0, (255), -1);

    // Calculating the average depth of the object closer than the safe distance
    cv::meanStdDev(depth_map, mean, stddev, mask2);

    // Printing the warning text with object distance
    char text[10];
    std::sprintf(text, "%.2f cm",mean.at<double>(0,0));

    cv::putText(output_canvas, "WARNING!", cv::Point2f(box.x + 5, box.y-40), 1, 2, cv::Scalar(0,0,255), 2, 2);
    cv::putText(output_canvas, "Object at", cv::Point2f(box.x + 5, box.y), 1, 2, cv::Scalar(0,0,255), 2, 2);
    cv::putText(output_canvas, text, cv::Point2f(box.x + 5, box.y+40), 1, 2, cv::Scalar(0,0,255), 2, 2);

  }
}
else
{
  // Printing SAFE if no obstacle is closer than the safe distance
  cv::putText(output_canvas, "SAFE!", cv::Point2f(200,200),1,2,cv::Scalar(0,255,0),2,2);
}

// Displaying the output of the obstacle avoidance system
cv::imshow("output_canvas",output_canvas);

 


다음 비디오는 이 게시물에서 논의된 장애물 회피 시스템의 출력을 보여줍니다.

 

https://www.youtube.com/watch?v=yY6Gb-1fO-U 

임계 거리가 100cm로 설정된 장애물 회피 시스템의 데모 비디오.

 

 

 

깊이가 있는 OpenCV AI 키트(OAK-D)와의 비교.

이전 게시물 중 하나에서 맞춤형 저비용 스테레오 카메라 설정을 만들고 이를 보정하여 애너글리프 3D 비디오를 캡처했습니다. 이 게시물에서는 깊이 추정을 위해 보정된 스테레오 카메라 설정을 사용했습니다.

이 경험을 바탕으로 맞춤형 스테레오 카메라를 구축하는 것은 시간이 많이 걸리고 복잡한 과정이라는 것이 분명합니다. 스테레오 정류 및 카메라 보정부터 블록 매칭 매개변수의 미세 조정, 깊이 맵과 디스패리티 값 간의 매핑 찾기에 이르기까지 스테레오 비전의 주요 기본 개념을 다룹니다. 따라서 스테레오 비전과 고전적인 컴퓨터 비전에 관심이 있는 모든 컴퓨터 비전 애호가에게 훌륭한 운동입니다.

그러나 3D 재구성 또는 자율 탐색과 같은 다운스트림 작업에 깊이 맵을 사용하는 데 더 관심이 있을 수 있습니다. 이러한 경우 맞춤형 스테레오 카메라를 구축하는 것이 최선의 선택이 아닐 수 있습니다. 또한 블록 일치가 계산 집약적인 프로세스라는 것도 관찰했습니다. 따라서 Raspberry Pi와 같은 모든 에지 장치에서 실행하면 상당한 계산 능력이 소모될 수 있습니다.

따라서 OpenCV AI Kit with Depth(OAK-D)는 모든 컴퓨터 비전 애호가에게 축복이 될 것입니다. OAK-D에는 RGB 카메라와 함께 스테레오 카메라가 있습니다. 또한 자체 처리 장치(딥 러닝 추론용 Intel Myriad X)가 있습니다. 가장 좋은 점은 더 이상 호스트 시스템에서 깊이 추정 알고리즘을 실행할 필요가 없다는 것입니다. OAK-D는 RGB 카메라, 스테레오 카메라 및 해당 깊이 맵에서 캡처한 프레임을 반환합니다. 기본적으로 몇 줄의 코드만으로 호스트 시스템의 계산 능력을 사용하지 않고도 깊이 맵을 얻을 수 있습니다.



작동 중인 OAK-D 보기

OAK-D의 위력에 이미 놀라셨겠지만 깊이 지도는 빙산의 일각에 불과합니다. 우리는 객체 감지와 같은 작업에 대한 딥 러닝 모델을 실행하고 싶을 때 OAK-D의 진정한 잠재력을 깨닫습니다. 예! 당신은 그것을 올바르게 추측했다! OAK-D는 딥 러닝 모델 예측도 실행할 수 있습니다.

앞서 언급했듯이 OAK-D는 RGB 카메라와 함께 스테레오 카메라, 자체 처리 장치(딥 러닝 추론을 위한 Intel Myriad X), 물체 감지. 다음 비디오를 보고 OAK-D가 작동하는 모습과 놀라운 기능에 대해 알아보세요.

 

https://www.youtube.com/watch?v=3bffM_L2KE8 

OpenCV AI 키트의 데모 비디오


OAK-D에 관심이 있으세요? 구매하시겠습니까? 여기를 클릭하여 매장을 방문해주세요.

 

 


요약

이 기사는 공간 AI 소개 시리즈의 세 번째입니다. 이 게시물에서는 스테레오 카메라와 OpenCV를 사용하여 스테레오 매칭 및 깊이 추정을 위한 고전적인 방법에 대해 논의했습니다.

우리는 문제 진술로 시작했습니다. 로봇이 자동으로 탐색하거나 다른 물체를 잡거나 이동하는 동안 충돌을 피하기 위해 스테레오 비전 기반 깊이 추정을 사용합니다. 우리는 또한 이 시리즈의 처음 두 게시물에서 배운 내용을 요약했습니다.

고밀도 스테레오 통신을 위한 블록 매칭의 이론에 대해 자세히 논의했습니다. 우리는 고정된 매개변수 세트가 스테레오 카메라 설정의 모든 가능한 조합에 대해 좋은 품질의 디스패리티 맵을 제공하지 않는다는 것을 배웠습니다. 이러한 단점을 극복하기 위해 우리는 블록 매칭 알고리즘의 다양한 매개변수를 변경하는 데 도움이 되는 GUI를 만들었습니다. 또 다른 중요한 사실은 최소 비용을 사용할 때 단순한 블록 매칭이 잘못된 매칭을 제공할 수 있다는 것입니다. 그 이유는 잘못된 일치가 올바른 일치보다 비용이 낮을 수 있기 때문입니다.

다음으로 깊이가 격차와 어떤 관련이 있는지 논의했습니다. 컴퓨터가 깊이를 인지하게 하여 시차로부터 깊이를 계산하는 표현식을 도출했습니다.

그런 다음 장애물 회피의 기본 개념에 대해 논의했습니다. 즉, 센서에서 물체까지의 거리가 최소 임계 거리보다 가까운지 확인하는 것입니다. 우리는 실용적이고 문제를 해결하는 장애물 회피 시스템을 위한 코드를 만들었습니다.

마지막으로 맞춤형 저비용 스테레오 카메라 설정을 비교하고 깊이가 있는 OpenCV AI 키트(OAK-D)를 사용하여 애너글리프 3D를 캡처하도록 보정했습니다.

이 기사의 개념과 내용을 이해하려면 사전 지식이 필요하다는 친절한 알림입니다. 이 게시물의 시작 부분에 제안된 기사를 살펴보는 것이 좋습니다. 댓글 섹션을 사용하여 이 게시물의 경험과 학습 결과를 알려주십시오.



참고문헌

[1] C. 루프 및 Z. Zhang. 스테레오 비전에 대한 정류 호모그래피 계산. IEEE Conf. 컴퓨터 비전 및 패턴 인식, 1999.

[2] Hirschmüller, Heiko(2005). "세미 글로벌 매칭 및 상호 정보를 통한 정확하고 효율적인 스테레오 처리". 컴퓨터 비전 및 패턴 인식에 관한 IEEE 회의. 807~814쪽.

[삼] . Birchfield 및 C. Tomasi, "픽셀 대 픽셀 스테레오에 의한 깊이 불연속성", International Journal of Computer Vision, vol. 35, 아니. 3,pp. 269–293, 1999.

[4] 유사한 스테레오 카메라 프로젝트. GitHub 링크 https://github.com/LearnTechWithUs/Stereo-Vision