본문 바로가기

Robotics

59일차 - c++ opencv ORB, XCode 라이브러리 설정, c++ npy 불러오기

반응형

CSV 형식으로 특징점 저장

.npy파일로 저장하면 파이썬에서 불러오기에는 용이하지만 c++에서 불러오기 위해서는cnpy라이브러리를 사용하여야 합니다. 오늘은cnpy라이브러리를 사용하기 위해 먼저 특징점을 일정한 길이로 만든 뒤,.npy로 저장하고, c++에서 불러오는 작업을 시작하겠습니다.

# 템플릿 특징점 추출
import cv2
import numpy as np
from google.colab import drive
drive.mount('/content/drive')

# 작업 경로 설정
workspace = '/content/drive/My Drive/돈과 유명세를 잡자/Zumi/recognize_test/{filename}' # {filename} 앞 경로를 리소스가 존재하는 경로로 변경하세요.
templatePath = workspace.format(filename='Hero {num}.png')

# 특징점 추출 알고리즘
orb = cv2.ORB_create()

# 템플릿 이미지 로드 및 특징점 추출
descriptors = []
for num in range(1, 105):
  template = cv2.imread(templatePath.format(num=num), cv2.IMREAD_GRAYSCALE)
  kp, des = orb.detectAndCompute(template, mask=None)
  descriptors.append(des.tolist())

# 패딩을 위한 변수 설정
minmax = lambda arr: (min(arr), max(arr))
min_len, max_len = minmax([len(row) for row in descriptors])
fixed_len = max_len - min_len

# 기존 자료형이 uint였으므로, 패딩된 값인 걸 명시하기 위해 -1로 패딩
for idx, row in enumerate(descriptors):
  padded = np.pad(row, (0, fixed_len), mode='constant', constant_values=-1)[:max_len]
  descriptors[idx] = np.asarray(padded, dtype=np.int16)

# 특징점 저장
np.save(workspace.format(filename='template.npy'), descriptors)
np.asarray(descriptors).shape, \
descriptors[0][0], descriptors[0][-1], \
np.max(descriptors), np.min(descriptors)
# 출력
((104, 398, 58),
 array([152, 221,  31, 254,  50, 206,  87, 184,  52,  10, 226,  96, 247,
        119,  32,  81, 214, 247,  93,  98, 105, 205,  89,  63, 225, 235,
        103,  16,  48, 242,  39,  59,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
         -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
         -1,  -1,  -1,  -1,  -1,  -1], dtype=int16),
 array([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        -1, -1, -1, -1, -1, -1, -1], dtype=int16),
 255,
 -1)

C++에서 불러오기

앞서 설명하였듯, c++에서 .npy 파일을 불러오기 위해서는 외부 라이브러리인 cnpy를 사용하여야 합니다. 아래 레포지토리를 로컬 경로에 저장합니다.

라이브러리 레포지토리: https://github.com/rogersce/cnpy

XCode에서 라이브러리를 불러오기 위해선 먼저 Inspector에서 프로젝트 폴더를 우클릭하고 Add Files to "프로젝트 폴더 이름"...을 누릅니다. 다음으로 추가할 폴더를 선택하고 Added folders 옵션을 Create groups로 설정합니다.

 

TARGETSBuild Settings에서 Other Linker Flags에 다음을 추가해줍니다.

-lcnpy -lz

opencv를 불러오려면 조금더 복잡합니다. brew를 통해 opencv를 설치한 경우, TARGETSBuild Settings에서 Header Search Paths/usr/local/include/opencv4를 추가하고, Library Search Paths/usr/local/lib를 추가합니다. 이후 아래 명령어를 통해 출력된 내용을 Other Linker Flags에 추가합니다.

$ cd /usr/local/Cellar/opencv
$ pkg-config --cflags --libs ./4.4.0_1/lib/pkgconfig/opencv4.pc 
-I/usr/local/Cellar/opencv/4.4.0_1/include/opencv4 -L/usr/local/Cellar/opencv/4.4.0_1/lib -lopencv_gapi -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_highgui -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_sfm -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_datasets -lopencv_text -lopencv_dnn -lopencv_plot -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_xfeatures2d -lopencv_shape -lopencv_ml -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core

pkg-config가 설치되어 있지 않다면 아래 명령어를 통해 설치해주세요.

brew install pkg-config

출처: https://gist.github.com/sigmadream/f1a7778eeaeab79f9888a3292976e438

template.npyCreate groups로 불러온 후, 파란색 프로젝트를 누른 뒤, TARGETSBuild Phases에서 Copy Files 부분에 template.npy를 추가해줍니다. Copy FilesDestinationProducts Directory, Subpath는 비워주시고, Copy only while installing 체크 해제된 상태입니다.

c++ opencv에서 flann의 knnMatch를 이용하기 위해선 디스크립터가 cv::Mat 형태여야 합니다. 이를 위해 cnpy로 불러온 1차원 데이터를 cv::Mat vector로 변환하도록 하겠습니다.

#include <iostream>
#include <opencv2/opencv.hpp>
#include "cnpy/cnpy.h"

using namespace std;

int main(int argc, const char * argv[]) {
    // npy 로드, 1차원 array로 불러와집니다.
    cnpy::NpyArray arr = cnpy::npy_load("template.npy");
    // 탐색 및 순회를 위한 포인터
    int16_t* loaded = arr.data<int16_t>();

    // 불러온 npy로부터 데이터 파싱하기
    int16_t val;
    int rows, cols;
    auto dstDes = vector<cv::Mat>(arr.shape[0]);
    for (int i=0; i<arr.shape[0]; i++) {
        // get rows
        for (rows=0; rows<arr.shape[1]; rows++) {
            // (x, y) value
            val = *(loaded + rows * arr.shape[2]);
            // if it's padded value
            if (val == -1) {
                break;+
            }
        }
        // get cols
        for (cols=0; cols<arr.shape[2]; cols++) {
            // (x, y) value
            val = *(loaded + cols);
            // if it's padded value
            if (val == -1) {
                break;
            }
        }
        // append cv::Mat
        dstDes[i] = cv::Mat(rows, cols, CV_8UC1, loaded);
        loaded += arr.shape[1] * arr.shape[2];
    }
    return 0;
}

비디오 캡처

c++ opencv에서 비디오 프레임을 가져와 이전처럼 ORB를 이용하여 특징점을 추출해보도록 하겠습니다. XCode에서 카메라 권한을 얻기 위해서는 다음과 같은 작업이 필요합니다. 먼저 New File을 누르고 Resource > Property List를 선택합니다. 이름은 Info.plist로 지정합니다.

Info.plist에 진입하면 Root가 보입니다. 우클릭해서 Property List TypeInfo.plist로 변경합니다. 이후에는 key 항목이 권한 목록으로 보입니다. Privacy - Camera Usage Description을 선택하고, Value는 권한 요구를 위한 팝업창에 표시되는 문구로, 아무 문구나 상관 없습니다.

이제 코드를 작성하고 실행하면 다음과 같은 창이 뜹니다. 첫 실행 때에는 권한 확인 창을 띄우고, 권한이 없어 프로그램이 종료됩니다. 확인을 누르면 다음 실행 시부터 권한이 적용됩니다. 주의할 점은 코드가 바뀌어 빌드를 새로 하게 될 시, 권한 또한 새로 부여해야 한다는 점입니다.

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;

int main(int argc, const char * argv[]) {
    // 특징점 추출 알고리즘
    const auto& orb = cv::ORB::create();

    // 비디오 캡쳐 초기화
    cv::Mat frame;
    cv::VideoCapture cap(0);
    if (!cap.isOpened()) {
        cerr << "에러 - 카메라를 열 수 없습니다.\n";
        return -1;
    }

    // 특징점 매칭을 위한 변수 선언
    cv::Mat srcDes, out;
    vector<cv::KeyPoint> srcKp;

    // 비디오 캡쳐 시작
    while (true) {
        // 카메라로부터 캡쳐한 영상을 frame에 저장합니다.
        cap.read(frame);
        if (frame.empty()) {
            cerr << "빈 영상이 캡쳐되었습니다.\n";
            break;
        }

        // 특징점 매칭을 위한 변수 초기화
        srcKp.clear();
        // 영상에서 특징점을 추출합니다.
        orb->detectAndCompute(frame, cv::noArray(), srcKp, srcDes);

        // 프레임에 특징점 그리기
        cv::drawKeypoints(frame, srcKp, out, cv::Scalar(255,0,0));
        // 프레임 표시
        cv::imshow("cap", out);
        // ESC 키를 입력하면 루프가 종료됩니다.
        if (cv::waitKey(25) >= 0)
            break;
    }

    return 0;
}

반응형