테스트장소: 카페
1.두 이미지간 ORB 매칭하면서 나오는 출력 영상에서 두 이미지가 동시에 나오기에, 주요한 실시간 이미지만을 ROI로 축출함.
2. 해당 ROI 실시간 이미지에서 Contour함수와 approxPolyDP함수 이용해 4개의 꼭지점 좌표 얻음.
(왜 매칭 된 좌표들을 이용하지 않았나? 앞서 두 이미지가 하나의 이미지로 동시에 나와서 사이즈크기가 컸다. 이에 매칭 좌표들 또한 +Offset이 되었다. 그렇기에 ROI 실시간 이미지에서 위 두 함수를 이용하는게 더 간단하게 구할 수 있다 생각했다)
3. 해당 4개의 꼭지점 인덱스가 실시간 영상 객체(핸드폰)의 기울기 부호에 따라 바뀌어지는 걸 swap 함수 통해 통일시킴.
4.서울 관광 영상을 실시간 영상에서 4개의 꼭짓점 에 넣어주기 위해 투시변환(getPerspectiveTransform, warpPerspective), 마스킹(bit_wise 함수)사용해 영상합성.
이슈: 실행 시 로딩시간이 좀 길다. 사용하는 cpu는 amd며 그래픽카드는 nvdia가 아니다.
ocl::setUseOpenCL(false); 또한 입력함에도 이미지를 불러오는 로딩시간이 길었다.
해당 문제는 동영상을 불러오는 로딩시간이 길다. 아무래도 동영상 용량이 커서 로딩이 길게 걸리는 듯하다.
코드가 길어진 원인은 이미지 매칭 시 하나의 window창에 2개의 이미지가 있어서 우측의 이미지를 처리할 때 어떻게 해야 단순하게 처리할지 아이디어가 생각나지 않았다. 이에 우회해서 해결을 하여 코드가 길어지게 됐다.
#include <iostream>
#include "opencv2/opencv.hpp"
#include "opencv2/core/ocl.hpp"
#include <algorithm>
/*
sequence :
homography\ 계산해 실시간 캠과
*/
using namespace std;
using namespace cv;
bool find_homography(const Mat& REF_IMG, const Mat& CAM_ORI, Mat& src);
const Mat get_ROI(const Mat& src);
const vector<vector<Point>> getContours(const Mat& src, const Mat& dst);
vector<Point> getVertex(vector<vector<Point>> contours_points, const Mat& src);
void mergeAr(vector<Point>Vertex, VideoCapture& Video, const Mat& CAM_ORI);
int main(void)
{
ocl::setUseOpenCL(false);
Mat REF_IMG = imread("korea.jpg", IMREAD_GRAYSCALE); // 야경 이미지.
if (REF_IMG.empty()) {
cerr << "Image load failed!" << endl;
return -1;
}
VideoCapture CAM(0);
if (!CAM.isOpened()) {
cerr << "Camera open failed!" << endl;
return -1;
}
VideoCapture Video("korea.mp4");
if (!Video.isOpened()) {
cerr << "Video open failed!" << endl;
return -1;
}
// TODO:
while (1) {
Mat CAM_ORI; // 실시간 영상
CAM >> CAM_ORI;
//REAL_FRAME과 src 사진 매칭.
if (CAM_ORI.empty() || REF_IMG.empty()) {
if (REF_IMG.empty()) {
cerr << "Image can't loaded";
return -1;
}
cerr << "Frame can't loaded";
return -1;
}
Mat MATCHING_IMG;
if (find_homography(REF_IMG, CAM_ORI, MATCHING_IMG) == true) {//실시간영상과 야경 이미지 매칭
Mat ROI_IMG = get_ROI(MATCHING_IMG); // 매칭이미지가 실시간영상과 야경이미지가 함께있음에따라
//ROI로 실시간 영상 부분만 축출
vector<vector<Point>> contours_points = getContours(ROI_IMG, CAM_ORI); // ROI이미지에서 contour 추출
vector<Point>Vertex = getVertex(contours_points, CAM_ORI); // contour 좌표들을 input으로 하여 approxpoly함수를 사용, 이에 따라 꼭짓점 구함.
//실시간 캠과 서울 영상 합성하기
mergeAr(Vertex, Video, CAM_ORI);
}
int key = cv::waitKey(1);
if (key == 27 || key == ' ') return 0;
imshow("Result", CAM_ORI);
}
}
bool find_homography(const Mat & REF_IMG, const Mat & CAM_ORI, Mat & src) {
Mat CAM_GRAY;
cvtColor(CAM_ORI, CAM_GRAY, COLOR_BGR2GRAY);
TickMeter tm;
tm.start();
Ptr<ORB> detector = ORB::create();
vector<KeyPoint> keypoints1, keypoints2;
Mat desc1, desc2;
detector->detectAndCompute(REF_IMG, Mat(), keypoints1, desc1);
detector->detectAndCompute(CAM_GRAY, Mat(), keypoints2, desc2); // 특징자, 기술자 계산
Ptr<DescriptorMatcher> matcher = BFMatcher::create(NORM_HAMMING); // ORB 사용 시 NORM_HAMMING 인자를 줌.
// 상위 80개 선택해서 좋은 매칭 선택
vector<DMatch> matches;
matcher->match(desc1, desc2, matches);
std::sort(matches.begin(), matches.end());
vector<DMatch> good_matches(matches.begin(), matches.begin() + 80);
// 호모그래피 계산 코드
//start
vector<Point2f> pts1, pts2;
for (size_t i = 0; i < good_matches.size(); i++) { //good_matches.size() 80개로 가정하면. 80개를 pts1, pts2에 축적해야함.
pts1.push_back(keypoints1[good_matches[i].queryIdx].pt); // queryIdx: 1번 영상 특징점 번호
pts2.push_back(keypoints2[good_matches[i].trainIdx].pt); // 2번점들 전체 특징점 정보가 keypoints2에 들어갔으며, 잘 된 매칭의 trainIdx 좌표에 point갖고와 저장.
}
Mat inliners;
Mat H = findHomography(pts1, pts2, RANSAC, 3.0, inliners); // end
//pts1의 80개, pts2의 80개 이용해 이 둘에서 outlier 있다고 가정하여 RANSAC 기법사용해 좋은 매칭결과 반환
int inlier_cnt = countNonZero(inliners);
if (inlier_cnt <= 30) return false; // Inlier 갯수 30개 미만은 검출이 안된 걸로 판단. flase 예외처리
tm.stop();
//cout << "time: " << tm.getTimeMilli() << endl;
drawMatches(REF_IMG, keypoints1, CAM_GRAY, keypoints2, good_matches, src,
Scalar(0, 0, 255), Scalar(0, 0, 255), vector<char>(),
DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
//Homography 결과를 display
vector<Point2f> corners1, corners2;
corners1.push_back(Point2f(0, 0)); // 0 x 0
corners1.push_back(Point2f(REF_IMG.cols - 1.f, 0)); // 가로 x 0
corners1.push_back(Point2f(REF_IMG.cols - 1.f, REF_IMG.rows - 1.f)); // 가로 x 세로
corners1.push_back(Point2f(0, REF_IMG.rows - 1.f)); // 0 x 세로크기
perspectiveTransform(corners1, corners2, H);
cout << "perspective" << endl;
vector<Point> corners_dst;
int x = 0;
for (Point2f pt : corners2) {
corners_dst.push_back(Point(cvRound(pt.x + REF_IMG.cols), cvRound(pt.y))); //실수형 -> 정수형 변환.
}
polylines(src, corners_dst, true, Scalar(0, 255, 0), 2, LINE_AA); //corners2 를 display
return true;
}
const Mat get_ROI(const Mat & src) {
//ROI
Mat ROI_dst = src(Rect(1279, 0, src.cols - 1279, src.rows)); // 매칭 시 하나의 화면에 이미지 2개가 있기에 실시간 캠 영역부분을 roi 로 축출했습니다.
cout << ROI_dst.rows << " " << ROI_dst.cols << endl;
Mat ROI_dst2 = ROI_dst(Rect(0, 0, 640, 480));
Mat Masking;
ROI_dst.copyTo(Masking);
return ROI_dst;
}
const vector<vector<Point>> getContours(const Mat & src, const Mat & dst) {
//contours 함수 input이미지는 1채널이기에 src(BGR)이미지를 hsv로 변환 후 h(색상 성분)를 contours함수 inputImg로 입력.
Mat hsv;
cvtColor(src, hsv, COLOR_BGR2HSV);
vector<Mat>planes;
split(hsv, planes);
//imshow("H", planes[0]);
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(planes[0], contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
//idx가 0 이상이면 루프 돌음
for (int idx = 0; idx >= 0; idx = hierarchy[idx][0]) { // [0] 은 next 정보
drawContours(dst, contours, idx, Scalar(255, 255, 255), -1, LINE_8, hierarchy); // 사이즈 -1 통해 외곽선 내부 흰색으로 채움.
}
//imshow("ROI_dst", dst);
return contours;
}
vector<Point> getVertex(vector<vector<Point>> contours_points, const Mat & src) {
Mat dst;
src.copyTo(dst);
for (vector<Point>& pts : contours_points) { // 외곽선 점들이 내재된 contours의 외곽선들을 하나씩 스캐닝하여 .
if (contourArea(pts) < 400) // 각 외곽선 면적 400 이상만 검출
continue; // 작은 크기의 외곽선 객체인 흰색객체가 나타날수 있기에 이런 오검을 방지하기 위해 조건문 세움.
vector<Point> approx;
approxPolyDP(pts, approx, arcLength(pts, true) * 0.02, true); // 각각의 외곽선을 근사화시킴. 3번쨰 인자 입실론 : 주어진 외곽선 전체 길이 * 0.02 형태로 입실론 값 지정함.
// 각 도형의 꼭지점 좌표만으로 구성된 approx 벡터 포인터 타입 변수에 값들이 채워짐
if (!isContourConvex(approx))
continue;
int vtc = (int)approx.size(); // 각 도형의 좌표들을 카운팅해 vtc 에 저장
if (vtc == 4) { // 4개이면 사각형
cout << "꼭짓점: " << endl;
/* 핸드폰 기울기에 따라 인덱스 번호가 달라지는걸 통일 시킴.
0, 1번째 인덱스 간
x차이가 y차이보다 크면
1 0
2 3
작으면
0 3
1 2 로 되며 작은 경우의 인덱스 순서를 따르고자 한다.
*/
if (abs(approx[0].y - approx[1].y) < abs(approx[0].x - approx[1].x)) { // 인덱스 통일
swap(approx[0], approx[1]);
swap(approx[1], approx[2]);
swap(approx[2], approx[3]);
}
for (int x = 0; x < 4; ++x) {
string str = format("idx : %d, y: %d, x: %d ", x, approx[x].y, approx[x].x);
cout << approx[x] << " ";
putText(dst, str, approx[x], FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 255), 1, -1);
}
imshow("dst", dst);
cout << endl;
return approx;
}
}
return vector<Point>(0);
}
void mergeAr(vector<Point>Vertex, VideoCapture& Video, const Mat& CAM_ORI) {
if (Vertex.size() == 4) { // 꼭짓점 4개 인 경우만(사각형)
Mat Composite_Result; // warpPerspective 함수에서 output 이미지로 사용할 예정.
Mat Video_Frame;
Video >> Video_Frame;
Mat Mini_Frame;
Video_Frame.copyTo(Mini_Frame);
/*영상이 큼에도 resize 할 필요가없다. warpPerspective 함수 통해서 CAM_ORI 사이즈와 동일하게 출력하게 해줌,*/
//Video_Frame 영상 크기 resize .
//resize(Video_Frame, Mini_Frame, Size(abs(Vertex[2].x - Vertex[1].x)+15,abs( Vertex[1].y - Vertex[0].y) +1), 0, 0, 1); // Resize 기준은 Vertex 넓이 기준으로 조금 넓게. 빈틈이 없게 cover 하기 위함
Point2f srcPts[4];
Point2f dstPts[4];
srcPts[0] = Point2f(0, 0);
srcPts[1] = Point2f(0, Mini_Frame.rows);
srcPts[2] = Point2f(Mini_Frame.cols, Mini_Frame.rows);
srcPts[3] = Point2f(Mini_Frame.cols, 0);
dstPts[0] = Point2f(Vertex[0]);
dstPts[1] = Point2f(Vertex[1]);
dstPts[2] = Point2f(Vertex[2]);
dstPts[3] = Point2f(Vertex[3]);
Mat per_mat = getPerspectiveTransform(srcPts, dstPts); //per_mat : srcPts 좌표를 dstPts 변환하는 변환행렬 리턴.
warpPerspective(Mini_Frame, Composite_Result, per_mat, CAM_ORI.size()); //Composite_Result 이미지는 CAM_ORI 사이즈만큼 permat행렬 부분에 입력영상 Mini_Frame 을 넣어 warping함
//imshow("Composite_Result", Composite_Result);
Mat mask;
inRange(CAM_ORI, Scalar(255, 255, 255), Scalar(255, 255, 255), mask);
//imshow("thresholded image", mask);
cvtColor(mask, mask, COLOR_GRAY2BGR); // 컬러영상에 합치기 위함.
imshow("mask", mask);
//imshow("Composite_Result", Composite_Result);
bitwise_not(mask, mask);//white -> black
imshow("mask22", mask);
bitwise_and(CAM_ORI, mask, CAM_ORI); // CAM_ORI의 경우 Contour 내부 흰색으로 채웠음. 비트연산이기에 mask 행렬에 의해 0인 부분은 모두 0 (검정)으로 됨
bitwise_or(CAM_ORI, Composite_Result, CAM_ORI); // warping했던 Composite_Result (영상) 과 CAM_ORI(마스크 부분 검정값 0) 합침.
}
}
'프로그래머스 > OPENCV' 카테고리의 다른 글
| opencv 완벽 삭제 (0) | 2023.03.07 |
|---|---|
| 2주차 과제)subpixel 값으로 차선 엣지 좌표 검출하기 (0) | 2022.12.11 |
| 참고 (0) | 2022.12.09 |
| OpenCV 외곽선 함수와 응용 (1) | 2022.12.09 |
| 레이블링과 외곽선 검출 (0) | 2022.12.08 |