본문 바로가기

C# Windows Forms

C# OpenCvSharp를 사용해서 두 영상의 특징점 추출과 동일한 부분을 찾아보자!

반응형

C# OpenCvSharp를 사용해서 두 영상의 특징점 추출과 동일한 부분을 찾아보자!

 

오늘은 C# OpenCvSharp를 사용해서 두 영상의 특징점을 추출하고 동일한 부분을 파란색 테두리로 표시해 보도록 하겠습니다.

오늘의 폼은 버튼 하나만 추가해 주시면 됩니다.

1. 준비물(영상 2개)

위와 같이 동일한 부분이 존재하는 영상을 2개 준비해 줍니다.
저는 oCam을 다운 받아서 윈도우 기본 화면을 10초씩 녹화해서 준비했습니다.

2. 비디오 캡처 객체 생성하기

// 비교할 두 비디오 파일 경로
string videoPath1 = "D:/Tools/oCam/FisrtVideo.mp4";
string videoPath2 = "D:/Tools/oCam/SecondVideo.mp4";

// 두 개의 비디오 캡처 객체 생성
var cap1 = new VideoCapture(videoPath1);
var cap2 = new VideoCapture(videoPath2);

// 비디오 캡처 객체가 열리지 않으면 에러 메시지 출력
if (!cap1.IsOpened() || !cap2.IsOpened())
{
    MessageBox.Show("비디오를 열 수 없습니다.");
    return;
}

Mat frame1 = new Mat();  // 첫 번째 비디오의 프레임을 저장할 Mat 객체
Mat frame2 = new Mat();  // 두 번째 비디오의 프레임을 저장할 Mat 객체

 

  • videoPath1videoPath2: 비교할 두 개의 비디오 파일 경로를 지정합니다. 이 파일들은 화면의 좌측과 우측을 촬영한 비디오입니다.
  • VideoCapture: OpenCV에서 비디오 파일을 열고 프레임을 읽을 수 있게 해주는 객체입니다. 각각 cap1cap2는 두 비디오를 캡처하는 객체입니다.
  • IsOpened(): 비디오 캡처 객체가 정상적으로 열렸는지 확인합니다. 하나라도 열리지 않으면 "비디오를 열 수 없습니다"라는 메시지가 나타납니다.

 

 

3. 프레임을 읽고 ORB 특징점 추출

Mat frame1 = new Mat();  // 첫 번째 비디오의 프레임을 저장할 Mat 객체
            Mat frame2 = new Mat();  // 두 번째 비디오의 프레임을 저장할 Mat 객체

            var orb = ORB.Create();  // ORB (Oriented FAST and Rotated BRIEF) 특징점 검출기 생성

            while (true)
            {
                cap1.Read(frame1);
                cap2.Read(frame2);

                if (frame1.Empty() || frame2.Empty())
                    break;

                if (frame1.Size() != frame2.Size())
                {
                    Cv2.Resize(frame2, frame2, frame1.Size());
                }

 

 

  • Mat frame1 / frame2: 각 비디오에서 읽어온 프레임을 저장할 Mat 객체입니다. Mat은 OpenCV에서 이미지를 표현하는 기본 데이터 구조입니다.
  • ORB.Create(): ORB (Oriented FAST and Rotated BRIEF) 알고리즘을 사용해 특징점을 추출하는 객체를 생성합니다. ORB는 이미지에서 중요한 특징점을 검출하고 이들을 비교하여 매칭하는 데 사용됩니다.
  • while(true): 무한 루프를 통해 비디오의 각 프레임을 처리합니다.
    • cap1.Read(frame1)과 cap2.Read(frame2)를 사용하여 각 비디오에서 한 프레임을 읽어옵니다.
    • 프레임이 비어있으면 루프를 종료합니다.
    • 두 비디오의 크기가 다르면 두 번째 비디오의 크기를 첫 번째 비디오와 동일하게 조정합니다 Cv2.Resize()

 

 

4. 전체 프레임 수와 현재 프레임 번호 출력

double totalFrames = cap1.Get(VideoCaptureProperties.FrameCount);
double currentFrame = cap1.Get(VideoCaptureProperties.PosFrames);
Console.WriteLine($"전체 프레임 수: {totalFrames}, 현재 프레임 번호: {currentFrame}");

 

  • 현재 비디오의 전체 프레임 수와 현재 프레임 번호를 출력하여 비디오 진행 상황을 확인할 수 있습니다. 

5. 특징점 추출 및 매칭

Mat descriptors1 = new Mat();
                Mat descriptors2 = new Mat();
                Mat gray1 = new Mat();
                Mat gray2 = new Mat();

                Cv2.CvtColor(frame1, gray1, ColorConversionCodes.BGR2GRAY);
                Cv2.CvtColor(frame2, gray2, ColorConversionCodes.BGR2GRAY);

                orb.DetectAndCompute(gray1, null, out KeyPoint[] keypoints1, descriptors1);
                orb.DetectAndCompute(gray2, null, out KeyPoint[] keypoints2, descriptors2);

                Console.WriteLine($"Frame 1 Keypoints: {keypoints1.Length}, Frame 2 Keypoints: {keypoints2.Length}");

 

 

  • Mat descriptors1 / descriptors2: 각 비디오 프레임에서 추출한 특징점을 설명하는 디스크립터를 저장하는 객체입니다.
  • Cv2.CvtColor(frame1, gray1, ColorConversionCodes.BGR2GRAY): BGR 컬러 이미지를 그레이스케일 이미지로 변환합니다. 특징점 추출은 그레이스케일 이미지에서 더 효과적으로 수행됩니다.
  • orb.DetectAndCompute(): ORB 알고리즘을 사용하여 각 프레임에서 특징점을 검출하고, 그 특징점의 디스크립터를 계산합니다. 특징점은 keypoints1, keypoints2 배열에 저장됩니다.
  • 콘솔 출력: 각 프레임에서 검출된 특징점의 개수를 출력하여 얼마나 많은 특징점이 추출되었는지 확인합니다.

 

 

6. BFMatcher를 이용한 특징점 매칭

var matcher = new BFMatcher(NormTypes.L2, crossCheck: false);
                var matches = matcher.KnnMatch(descriptors1, descriptors2, 2);

                double ratio = 0.75;
                List<DMatch> goodMatches = new List<DMatch>();

                foreach (var matchPair in matches)
                {
                    if (matchPair.Count() >= 2)
                    {
                        var first = matchPair[0];
                        var second = matchPair[1];

                        if (first.Distance < second.Distance * ratio)
                        {
                            goodMatches.Add(first);
                        }
                    }
                }

 

 

 

  • BFMatcher: BFMatcher (Brute Force Matcher)는 두 디스크립터 집합 간의 유사성을 계산하여 매칭하는 방법입니다. NormTypes.L2는 유클리디안 거리를 기반으로 매칭을 수행합니다.
  • knnMatch(): 각 특징점의 디스크립터를 다른 비디오에서 두 개씩 매칭하여 가장 가까운 두 개의 매칭을 찾습니다.
  • ratio: Lowe의 비율 테스트를 적용하여 좋은 매칭을 선택합니다. 첫 번째 매칭이 두 번째 매칭보다 일정 비율만큼 가까우면 유효한 매칭으로 판단합니다.
  • 매칭된 특징점 중 좋은 매칭을 goodMatches 리스트에 저장합니다.

 

 

7. 매칭된 특징점 좌표 추출 및 원근 변환 행렬 계산

Point2f[] srcPts = new Point2f[goodMatches.Count];
                Point2f[] dstPts = new Point2f[goodMatches.Count];

                for (int i = 0; i < goodMatches.Count; i++)
                {
                    srcPts[i] = keypoints1[goodMatches[i].QueryIdx].Pt;
                    dstPts[i] = keypoints2[goodMatches[i].TrainIdx].Pt;
                }

                Mat srcMat = new Mat(srcPts.Length, 1, MatType.CV_32FC2);
                Mat dstMat = new Mat(dstPts.Length, 1, MatType.CV_32FC2);

                for (int i = 0; i < srcPts.Length; i++)
                {
                    srcMat.Set(i, 0, srcPts[i]);
                    dstMat.Set(i, 0, dstPts[i]);
                }

                Mat mtrx = Cv2.FindHomography(srcMat, dstMat, HomographyMethods.Ransac, 3.0);

 

 

 

  • srcPts / dstPts: 각각 첫 번째 비디오와 두 번째 비디오의 매칭된 특징점들의 좌표를 저장합니다.
  • Mat srcMat / dstMat: Point2f[] 배열의 좌표들을 Mat 객체로 변환하여 원근 변환 행렬을 계산할 준비를 합니다.
  • Cv2.FindHomography(): 두 개의 영상에서 매칭된 점들을 이용해 원근 변환 행렬을 계산합니다. Ransac 알고리즘을 사용해 이상치(Outliers)를 제거합니다.

 

 

8. 변환된 영역 표시 및 매칭 결과 출력

int h = frame1.Height;
                int w = frame1.Width;
                Point2f[] pts = new Point2f[]
                {
                    new Point2f(0, 0),
                    new Point2f(0, h - 1),
                    new Point2f(w - 1, h - 1),
                    new Point2f(w - 1, 0)
                };

                if (mtrx.Empty() || mtrx.Rows != 3 || mtrx.Cols != 3)
                {
                    Console.WriteLine("Invalid homography matrix: " + mtrx);
                }
                Point2f[] dst = Cv2.PerspectiveTransform(pts, mtrx);

                Cv2.Polylines(frame2, new OpenCvSharp.Point[][] { Array.ConvertAll(dst, p => new OpenCvSharp.Point((int)p.X, (int)p.Y)) }, isClosed: true, color: new Scalar(255, 0, 0), thickness: 3, lineType: LineTypes.AntiAlias);

                Mat output = new Mat();
                Cv2.DrawMatches(frame1, keypoints1, frame2, keypoints2, goodMatches, output,
                                new Scalar(0, 255, 0), new Scalar(255, 0, 0),
                                null, DrawMatchesFlags.NotDrawSinglePoints);

                Cv2.ImShow("Matches", output);

 

 

 

  • 원근 변환 영역 표시: 원본 영상에서 네 개의 모서리 좌표를 계산하고, 이를 원근 변환 행렬을 사용해 변환한 후, 두 번째 영상에 그립니다.
  • Cv2.Polylines: 원근 변환된 좌표를 사용해 두 번째 영상에 변환된 영역을 빨간색 선으로 그립니다.
  • Cv2.DrawMatches: 매칭된 특징점을 두 비디오 프레임에 그려 시각화합니다. 이때 매칭된 점들을 goodMatches를 기반으로 그립니다.
  • Cv2.ImShow: 결과를 화면에 출력합니다.

 

 

9. 종료 조건: 'q' 키로 종료

if (Cv2.WaitKey(1) == 'q')
                    break;
            }

            Cv2.DestroyAllWindows();
        }

 

 

 

  • Cv2.WaitKey(1): 1ms 동안 키보드 입력을 기다립니다. 'q' 키를 누르면 루프를 종료하여 프로그램을 종료합니다.
  • Cv2.DestroyAllWindows(): 모든 OpenCV 창을 닫습니다.

10. 코드 전체

public partial class Form1 : Form
{

    public Form1()
    {
        InitializeComponent();  // 폼 초기화
    }

    // 'Play' 버튼 클릭 시 호출되는 이벤트 핸들러
    private void Btn_Play_Click(object sender, EventArgs e)
    {
        FindSamePointVideo();  // 비디오에서 동일한 지점을 찾는 함수 호출
    }

    // 두 개의 비디오에서 동일한 지점을 찾아 매칭하는 함수
    public void FindSamePointVideo()
    {
        // 비교할 두 비디오 파일 경로
        string videoPath1 = "D:/Tools/oCam/FirstVideo.mp4";
        string videoPath2 = "D:/Tools/oCam/SecondVideo.mp4";

        // 두 개의 비디오 캡처 객체 생성
        var cap1 = new VideoCapture(videoPath1);
        var cap2 = new VideoCapture(videoPath2);

        // 비디오 캡처 객체가 열리지 않으면 에러 메시지 출력
        if (!cap1.IsOpened() || !cap2.IsOpened())
        {
            MessageBox.Show("비디오를 열 수 없습니다.");
            return;
        }

        Mat frame1 = new Mat();  // 첫 번째 비디오의 프레임을 저장할 Mat 객체
        Mat frame2 = new Mat();  // 두 번째 비디오의 프레임을 저장할 Mat 객체

        var orb = ORB.Create();  // ORB (Oriented FAST and Rotated BRIEF) 특징점 검출기 생성

        // 무한 루프를 통해 프레임을 하나씩 처리
        while (true)
        {
            // 각 비디오에서 프레임을 읽어온다.
            cap1.Read(frame1);
            cap2.Read(frame2);

            // 두 비디오 중 하나라도 더 이상 프레임이 없으면 종료
            if (frame1.Empty() || frame2.Empty())
                break;

            // 두 프레임의 크기가 다르면 두 번째 비디오 프레임의 크기를 첫 번째 비디오와 맞춘다.
            if (frame1.Size() != frame2.Size())
            {
                Cv2.Resize(frame2, frame2, frame1.Size());
            }

            // 전체 프레임 수 및 현재 프레임 번호 출력
            double totalFrames = cap1.Get(VideoCaptureProperties.FrameCount);
            double currentFrame = cap1.Get(VideoCaptureProperties.PosFrames);
            Console.WriteLine($"전체 프레임 수: {totalFrames}, 현재 프레임 번호: {currentFrame}");

            // 디스크립터를 저장할 객체
            Mat descriptors1 = new Mat();
            Mat descriptors2 = new Mat();

            // 그레이스케일로 변환할 Mat 객체
            Mat gray1 = new Mat();
            Mat gray2 = new Mat();

            // 두 프레임을 그레이스케일로 변환
            Cv2.CvtColor(frame1, gray1, ColorConversionCodes.BGR2GRAY);
            Cv2.CvtColor(frame2, gray2, ColorConversionCodes.BGR2GRAY);

            // ORB를 사용하여 특징점 및 디스크립터 추출
            orb.DetectAndCompute(gray1, null, out KeyPoint[] keypoints1, descriptors1);
            orb.DetectAndCompute(gray2, null, out KeyPoint[] keypoints2, descriptors2);

            // 두 프레임의 특징점 개수 출력
            Console.WriteLine($"Frame 1 Keypoints: {keypoints1.Length}, Frame 2 Keypoints: {keypoints2.Length}");

            // BFMatcher 객체를 사용하여 특징점 매칭 (비교기준: L2 노름, 크로스 체크 하지 않음)
            var matcher = new BFMatcher(NormTypes.L2, crossCheck: false);
            var matches = matcher.KnnMatch(descriptors1, descriptors2, 2);  // 2개 매칭을 반환

            // 매칭 필터링 비율 설정
            double ratio = 0.75;
            List<DMatch> goodMatches = new List<DMatch>();

            // 좋은 매칭 점을 필터링
            foreach (var matchPair in matches)
            {
                if (matchPair.Count() >= 2) // 두 개의 매칭이 존재하는 경우
                {
                    var first = matchPair[0];
                    var second = matchPair[1];

                    // 첫 번째 매칭이 두 번째 매칭보다 훨씬 더 가까우면 좋은 매칭으로 판단
                    if (first.Distance < second.Distance * ratio)
                    {
                        goodMatches.Add(first);
                    }
                }
            }

            // 좋은 매칭 점들의 좌표를 가져온다.
            Point2f[] srcPts = new Point2f[goodMatches.Count];
            Point2f[] dstPts = new Point2f[goodMatches.Count];

            // 매칭된 특징점의 좌표를 배열에 저장
            for (int i = 0; i < goodMatches.Count; i++)
            {
                srcPts[i] = keypoints1[goodMatches[i].QueryIdx].Pt;
                dstPts[i] = keypoints2[goodMatches[i].TrainIdx].Pt;
            }

            // Mat 객체에 좌표 데이터를 설정
            Mat srcMat = new Mat(srcPts.Length, 1, MatType.CV_32FC2);
            Mat dstMat = new Mat(dstPts.Length, 1, MatType.CV_32FC2);

            // 좌표 데이터를 Mat 객체에 복사
            for (int i = 0; i < srcPts.Length; i++)
            {
                srcMat.Set(i, 0, srcPts[i]);
                dstMat.Set(i, 0, dstPts[i]);
            }

            // RANSAC 알고리즘을 사용하여 원근 변환 행렬 계산
            Mat mtrx = Cv2.FindHomography(srcMat, dstMat, HomographyMethods.Ransac, 3.0);

            // 원본 영상 크기에서 변환된 좌표들을 생성
            int h = frame1.Height;
            int w = frame1.Width;
            Point2f[] pts = new Point2f[]
            {
                new Point2f(0, 0),
                new Point2f(0, h - 1),
                new Point2f(w - 1, h - 1),
                new Point2f(w - 1, 0)
            };

            // 변환 행렬이 유효한지 확인
            if (mtrx.Empty() || mtrx.Rows != 3 || mtrx.Cols != 3)
            {
                Console.WriteLine("Invalid homography matrix: " + mtrx);
            }

            // 변환 좌표 계산
            Point2f[] dst = Cv2.PerspectiveTransform(pts, mtrx);

            // 변환된 좌표들을 두 번째 프레임에 그리기
            Cv2.Polylines(frame2, new OpenCvSharp.Point[][] { Array.ConvertAll(dst, p => new OpenCvSharp.Point((int)p.X, (int)p.Y)) }, isClosed: true, color: new Scalar(255, 0, 0), thickness: 3, lineType: LineTypes.AntiAlias);

            // 두 프레임의 매칭 결과를 화면에 그리기
            Mat output = new Mat();
            Cv2.DrawMatches(frame1, keypoints1, frame2, keypoints2, goodMatches, output,
                            new Scalar(0, 255, 0), new Scalar(255, 0, 0),
                            null, DrawMatchesFlags.NotDrawSinglePoints);

            // 매칭 결과를 창에 표시
            Cv2.ImShow("Matches", output);

            // 'q' 키를 눌러 종료
            if (Cv2.WaitKey(1) == 'q')
                break;
        }

        // 모든 창 닫기
        Cv2.DestroyAllWindows();
    }
}

 

 

11. 결과물

위 사진과 같이 서로 동일한 특징점을 포인트와 라인으로 표시하고 파란색 테두리로 동일한 부분을 표시해 줍니다.

12. 끝으로....

이 과정은 두 영상의 동일한 부분을 체크해 겹침으로써
파노라마 영상을 만들고 싶어서 찾다가 공부하게 되고 정리한 것입니다.
함수나 여러 용어들에 대해 이해하고 분석하는 것에 어려움이 있었지만
이것저것 수정해 보고 찾아보면서 stitching 하는 것에 성공했다.
다음에 영상 stitching에 대해서 정리해 보겠습니다.

반응형