본문 바로가기

Image Processing

[영상처리/C++/OpenCV] CannyEdge detection & RealSense

이번 시간에는 CannyEdge 감지 기법과 RealSense 카메라를 사용해 보았습니다.

 

CannyEdge detection

말 그대로 가장자리, 경계선을 찾는 알고리즘 입니다. 알고리즘은 4단계로 구성되어 있습니다. 

  • 이미지 노이즈 제거
  • Sobel filtering
  • NMS
  • Double thrsing --> edge tracking by htsteresis

 

일반적으로 이미지에 생기는 가우시안 노이즈를 제거하기위해서 가우시안 필터를 활용합니다. NMS에서 gradinet를 활용할 것이기 때문에 노이즈가 gradient를 계산하는데 있어서 매우 큰 방해 요소가 됩니다.

 

노이즈에 의해 크게 진동하는 gradient

sobel filter는 가로, 세로 상의 픽셀 값의 변화를 통해 영역의 경계면을 검출하는 필터입니다. 1차 미분 sobel mask를 활용하여 최고점을 edge로 인식할 수 있습니다.

Sobel mask

 

NMS(Non - maximum suppression)을 적용합니다. Local maxima를 선택하는 단계가 됩니다. 가우시안 필터에 의해서 흐려진 edge에서 잘못된 검출이 발생할 수 있습니다. edge가 두꺼워 질 수도 있기 때문에 local maxiam를 남기고 나머지를 모두 제거해 줍니다. edge를 더 얇게 만드는 효과를 보일 수 있습니다.

아래는 local에서의 gradient 값입니다. 가장 큰 값만을 취하면서 얇은 edge를 획득합니다.

 

Local gradient value

 

마지막으로, double thresholding기법을 적용해 줍니다. Low level과 high level 2개를 사용합니다. 처리과정은 먼저, low level보다 낮은 edge 값들은 버립니다. 그리고 high level 보다 높은 경우는 모두 남깁니다. Low level, high level의 사이 값인 경우에는 해당 edge가 high level를 넘는 edge와 연결 되어 있다면 edge에 연결 시켜 줍니다.

 

B는 남을수 없고, A는 남을 수 있음

코드 실습

Canny() 함수를 사용하여 실습을 진행하고, 2개의 threshold를 하이퍼 파라미터로 사용 할 수 있도록 설정하였습니다.

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/calib3d.hpp>
#include <iostream>
#include <iomanip>

using namespace std;
using namespace cv;

int main(int argc, char** argv)
{
    VideoCapture cap(1);
    if (!cap.isOpened()) {
        cout << "Cam Error" << endl;
        exit(-1);
    }
    
    int w = cap.get(CAP_PROP_FRAME_WIDTH);
    int h = cap.get(CAP_PROP_FRAME_HEIGHT);


    // 출력 하기
    VideoWriter writer;
    
    int codec = VideoWriter::fourcc('D', 'I', 'V', 'X');
    writer.open("result.mp4", codec, 20.0, Size(w, h), true);
    
    Mat frame;

    int value1 = 50;
    int value2 = 100;
    

    while(cap.read(frame)){
        
        if (frame.empty()) {
            cout << "Frame Over" << endl;
            exit(-1);
        }

        cvtColor(frame, frame, COLOR_BGR2GRAY);

        createTrackbar("weak", "video", &value1, 255); // 트랙바 이름, 트랙바 설치한 윈도우 , value값 , value 최대값 
        createTrackbar("strong", "video", &value2, 255);
        
        Canny(frame, frame, (double)value1, (double)value2, 3, false); // input, ouput, threshold 2개 , sobel mask size , l2 gradient or l1 gradient
        
        imshow("video", frame);

       
        cvtColor(frame, frame, COLOR_GRAY2BGR);
        writer.write(frame);

        int key = waitKey(1);
        if ((char)key == 'q') {
            writer.release();
            break;
        }
    }
    
    cap.release();

     
    return 0;   
}

 

웹캠으로 부터 들어오는 frame 정보에 적용해 보았습니다. Low, high 임계값은 코드 실행 중에도 변경할 수 있습니다.

 

 

 

RealSense

RealSense는 IR센서도 제공하면서 streo도 제공하는 depth 카메라입니다. C++, OpenCV를 활용할 때 자주 사용합니다.

 

Realsesne d435

 

Realsense를 활용하면 disparity map을 획득할 수 있습니다. 저번 영상처리 때 실습과정을 통해 추정해 본적이 있습니다.

https://mynameisoh.tistory.com/32

 

[영상처리/C언어] Disparity Estimation (DE)

Disparity Estimation Disparity Estimation은 말 그대로 disparity를 추정하는 기법입니다. Disparity(시차)는 두 이미지내에서 x축의 차이라고 볼 수 있습니다. 즉, 이 disparity는 지난 시간의 ME에서의 motion..

mynameisoh.tistory.com

 

 

Disparity 맵은 기준 영상에서의 한 점에 대한 동일한 점을 목표 영상에서 찾을 때 사용하게 됩니다. 기준 영상과 목표영상에서의 한 점에 대한 x축의 위치의 차이를 시차(disparity)라고 합니다. Realsense는 동일한 y축을 가지는 2개의 카메라가 있으므로 시차를 구할 수 있습니다. 그리고 얻게 된 시차와 카메라 변수를 통해서 깊이를 추정할 수도 있습니다.

 

 

Realsense를 통해서 얻은 disparity map은 다음과 같습니다. 1번과 2번의 차이를 통해 disparity를 추정합니다.

 

코드실습

Realsense를 opencv에서 활용하려면 아래 코드 처럼 활용하시면 됩니다. Realsense와 관련환 라이브러리를 홈페이지에서 다운받고 링킹을 하셔야합니다.

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/highgui.hpp>
#include <librealsense2/rs.hpp> // Include RealSense Cross Platform API
#include <iostream>
#include <opencv2/calib3d.hpp>
#include <iomanip>

using namespace std;
using namespace cv;

int main(int argc, char* argv[]) 
{

    // Declare depth colorizer for pretty visualization of depth data
    rs2::colorizer color_map;

    // Declare RealSense pipeline, encapsulating the actual device and sensors
    rs2::pipeline pipe;
    rs2::pipeline_profile pipeProfile;
    rs2::config cfg; // 선택적 옵션 , 내가 원하는 filter로 구성하여 스트리밍을 할 수 있도록 구성할 수 있음

    cfg.enable_stream(RS2_STREAM_INFRARED, 1, 640, 480, RS2_FORMAT_Y8, 30);  // ir left image를 위한 stream 생성 
    cfg.enable_stream(RS2_STREAM_INFRARED, 2, 640, 480, RS2_FORMAT_Y8, 30);  // ir right image를 위한 stream 생성
    cfg.enable_stream(RS2_STREAM_COLOR, 640, 480, RS2_FORMAT_BGR8, 30);
    cfg.enable_stream(RS2_STREAM_DEPTH, 640, 480, RS2_FORMAT_Z16, 30);
    // Instruct pipeline to start streaming with the requested configuration  
    pipeProfile = pipe.start(cfg);

    rs2_intrinsics intrinsics1 = pipeProfile.get_stream(RS2_STREAM_INFRARED,1).as<rs2::video_stream_profile>().get_intrinsics();
    rs2_intrinsics intrinsics2 = pipeProfile.get_stream(RS2_STREAM_INFRARED,2).as<rs2::video_stream_profile>().get_intrinsics();

    
    cout << "    " << left << setw(31) << "Width" << ": " << intrinsics1.width << endl <<
        "    " << left << setw(31) << "Height" << ": " << intrinsics1.height << endl <<
        "    " << left << setw(31) << "PPX" << ": " << setprecision(15) << intrinsics1.ppx << endl <<
        "    " << left << setw(31) << "PPY" << ": " << setprecision(15) << intrinsics1.ppy << endl <<
        "    " << left << setw(31) << "Fx" << ": " << setprecision(15) << intrinsics1.fx << endl <<
        "    " << left << setw(31) << "Fy" << ": " << setprecision(15) << intrinsics1.fy << endl <<
        "    " << left << setw(31) << "Distortion" << ": " << rs2_distortion_to_string(intrinsics1.model) << endl;
    
    cout << endl;

    cout << "    " << left << setw(31) << "Width" << ": " << intrinsics2.width << endl <<
         "    " << left << setw(31) << "Height" << ": " << intrinsics2.height << endl <<
         "    " << left << setw(31) << "PPX" << ": " << setprecision(15) << intrinsics2.ppx << endl <<
         "    " << left << setw(31) << "PPY" << ": " << setprecision(15) << intrinsics2.ppy << endl <<
         "    " << left << setw(31) << "Fx" << ": " << setprecision(15) << intrinsics2.fx << endl <<
         "    " << left << setw(31) << "Fy" << ": " << setprecision(15) << intrinsics2.fy << endl <<
         "    " << left << setw(31) << "Distortion" << ": " << rs2_distortion_to_string(intrinsics2.model) << endl;

    cout <<  endl;
	
    const auto window_name = "Left Image";
    namedWindow(window_name, WINDOW_AUTOSIZE);
    
    // stereoBM class parameter
    int numDisparities = 112;
    int SADWindowSIze = 7;
    
    Ptr<StereoBM> BM = StereoBM::create(numDisparities, SADWindowSIze); // ptr 클래스 메모리를 안전하게 사용
    
    // StereoBM* BM = StereoBM::create(numDisparities, SADWindowSIze);      이렇게 설정하면 안됨 이유는 모르겠음
    

        // Searchrange , BlockSize
        // SearchRange 16으로 나눌 수 있는 수

    

    while (waitKey(1) < 0 && getWindowProperty(window_name, WND_PROP_AUTOSIZE) >= 0)  // window가 닫히면  -1이 반환
    {
        rs2::frameset data = pipe.wait_for_frames(); // Wait for next set of frames from the camera
        rs2::frame ir_left = data.get_infrared_frame(1);
        rs2::frame ir_right = data.get_infrared_frame(2);
        rs2::frame RGB = data.get_color_frame();
        rs2::frame depth = data.get_depth_frame();
        
        // Query frame size (width and height)

        Mat Lir(Size(640, 480), CV_8UC1, (void*)ir_left.get_data(), Mat::AUTO_STEP);
        Mat Rir(Size(640, 480), CV_8UC1, (void*)ir_right.get_data(), Mat::AUTO_STEP);
        Mat disp(Size(640,480),CV_8U);
        Mat dep(Size(640, 480), CV_16U,(void*)depth.get_data(), Mat::AUTO_STEP); //    unsinged char 8U   unsigned short 16U 
        Mat rgb(Size(640, 480),CV_8UC3,(void*)RGB.get_data(), Mat::AUTO_STEP);
        
        
        BM->compute(Lir, Rir, disp); // disparity Mat 생성
        // 행렬 연산 과정 중 형변환이 발생해서 타입변환이 필요

        disp.convertTo(disp, CV_8U); //clipping
        
        /*for (int y = 0; y < disp.rows; y++) {
            for (int x = 0; x < disp.cols; x++) {
                depth.at<uchar>(y, x) = 1.93 * 50 / (float)depth.at<uchar>(y, x);
            }
        }
        depth.convertTo(depth, CV_8UC1);
        cout << depth << endl;*/


        // Display the image in GUI
        namedWindow("Left Image", WINDOW_AUTOSIZE);
        imshow("Left Image", Lir);
        imshow("Right Image", Rir);
        imshow("Dp Image", disp);
        imshow("RGB Image",rgb);
        imshow("Depth Image", dep*100);
        
    }

    return 0;
}