🔥 Programming/OpenCV

[OpenCV4] C# OpenCV4 Contours와 Moment를 이용한 MatchShape

스쳐가는비 2022. 12. 15. 21:16

Contours란?

Contours는 이미지 윤곽을 말한다.

즉, 같은 색이나 강도를 가지는 연속된 점들을 연결시켜놓은 선이다. 등고선 또는 등치선이라고 한다.

 

그럼 실제로 사용해보도록 하자.

인자로 받아온 Bitmap을 Matrix형태로 변환시켜준 이후, 더 나은 이미지 검출을 위해 이진이미지로 바꿔준다.

Threshold는 실제로 바꿔보며 가장 검출이 잘되는 값을 찾아넣어준다. 

 

그리고 검출된 Contours들을 실제로 사용하기위해 반환값으로 사용한 이미지와

검출된 contours, 계층으로 설정해준다.

 

사용한 이미지는 그림판으로 직접 만든 조잡한 아래의 이미지이다.

배경 이미지
찾고자하는 이미지

그럼 실제로 찾고 그려주는 부분의 코드를 확인해보자

 

Code ( Contours 만 이용 )

 Point[][] contours1;
            HierarchyIndex[] hierarchy1;

            Mat pattern;
            (pattern, contours1, hierarchy1) = FindContours(Properties.Resources.Pin_roi);

            Cv2.ImShow("pattern", pattern);

            Point[][] contours2;
            HierarchyIndex[] hierarchy2;

            Mat data;
            (data, contours2, hierarchy2) = FindContours(Properties.Resources.PinImage);

            var output = new Mat();
            Cv2.CvtColor(data, output, ColorConversionCodes.GRAY2BGR);

            Cv2.ImShow("data", data);

            // 지직 거리는 노이즈 날리기
            ////////////////////////////////////////////////////
            for (int i = 0; i < contours2.GetLength(0); i++)
            {
                var area = Cv2.ContourArea(contours2[i].ToArray());

                if (area < 100)
                {
                    var rect = Cv2.BoundingRect(contours2[i]);
                    rect.Inflate(width: 1, height: 1);
                    output.Rectangle(rect, Scalar.Black, thickness: -1);
                }
            }
            ///////////////////////////////////////////////////
           
            // 윤곽선의 좌표는 우하좌상으로 들어있음
            for (int i = 0; i < contours2.GetLength(0); i++)
            {
                int cnt = contours2[i].Count();

                // 찾은게 윤곽선 좌표 개수가 말도안되게 많을 경우 무시해주기
                if (cnt >= 1000)
                    continue;

                var area = Cv2.ContourArea(contours2[i].ToArray());

                // 찾은 윤곽선 면적이 말도안되게 작을 경우 무시해주기
                if (area < 500)
                    continue;

                var rect = Cv2.BoundingRect(contours2[i]);


                double ratio = Cv2.MatchShapes(data[rect], pattern, ShapeMatchModes.I3);
                if (ratio < 0.05)
                {
                    output.PutText(ratio.ToString("F3"), rect.TopLeft, HersheyFonts.HersheySimplex, fontScale: 0.8, Scalar.White);
                    output.Rectangle(rect, Scalar.Blue);
                }
            }

 

찾고자하는 배경이미지와 잘라낸 이미지의 contours를 찾아준다.

이후 이미지의 노이즈 제거를 위해 가로 세로 1크기의 검은색으로 채워진 사각형을 그려준다.

 

그리고 찾은 좌표들을 가지고 사각형을 그려준다.

 

아래 결과를 확인해보자.

결과

실상 전부다 0.000으로 나오니... 아주 잘찾은거지만(0에 가까울수록 유사하게 찾은 이미지) 

잘 찾고있는지 증명을 위해 이미지를 살짝 바꿔 테스트해보자.

 

이미지는 Threshold 타입을 Binary에서 BinaryInv로 변경하였다.

그러니 Contours가 제대로 찾아지지가 않아, 검색방법을 External에서 Ccomp로 변경하였다.

그리고 MatchShape한 결과 값을 기존의 0.05에서 0.5로 변경하였다.

 

아래이미지는 바꾼 후 결과이다.

찾은 값이 0에 가깝지 않은만큼 정확하지 않다고 나오지만, 이 방법으로 찾은게 정상적이라는게 증명되었다.

그럼 기존 이미지에서 moment를 이용하여 질량의 중심도 구해서 원을 그려줘보자.

 

추가하여 변경된 코드이다.

 

Code ( 이미지를 변경하여 Test해봄 )

        /*##################################################################################################################################################
             * 밑에꺼 함수원형임
             *                (중심을 찾을 Contours 배열, BinaryImage인지 아닌지);
             * Moments Moments(IEnumerable<Point> array, bool binaryImage = false);
             * 
             * <<Moment 종류>>
             * 공간 모멘트
             * | m00,m01,m10| 
             * | m11,m20,m02| 
             * | m30,m21,m12|
             * | m03        |
             * 
             * 중심 모멘트
             * | mu20,mu11,mu02|
             * | mu30,mu21,mu12|
             * | mu03          |
             * 
             * 평준화 중심 모멘트
             * | nu20,nu11,nu02|
             * | nu30,nu21,nu12|
             * | nu03          |
             * 
             * https://en.wikipedia.org/wiki/Image_moment
             * ##################################################################################################################################################*/
            // 윤곽선의 좌표는 우하좌상으로 들어있음
            for (int i = 0; i < contours2.GetLength(0); i++)
            {
                int cnt = contours2[i].Count();

                // 찾은게 윤곽선 좌표 개수가 말도안되게 많을 경우 무시해주기
                if (cnt >= 1000)
                    continue;

                var area = Cv2.ContourArea(contours2[i].ToArray());

                // 찾은 윤곽선 면적이 말도안되게 작을 경우 무시해주기
                if (area < 500)
                    continue;

                var rect = Cv2.BoundingRect(contours2[i]);

                // Moment 찾을 Contours
                var moments = Cv2.Moments(contours2[i]);

                // Contours의 무게중심을 찾을 좌표 
                Point Center = new Point();

                /* 질량중심 Center X,Y 구하는 공식 
                 * Center X = (m10 / m00)
                 * Center Y = (m01 / m00)
                 */
                Center.X = (int)(moments.M10 / moments.M00);
                Center.Y = (int)(moments.M01 / moments.M00);

                Cv2.Circle(output, Center, 3, Scalar.Red, -1, LineTypes.AntiAlias);

                double ratio = Cv2.MatchShapes(data[rect], pattern, ShapeMatchModes.I3);
                if (ratio < 0.05)
                {
                    output.PutText(ratio.ToString("F3"), rect.TopLeft, HersheyFonts.HersheySimplex, fontScale: 0.8, Scalar.White);
                    output.Rectangle(rect, Scalar.Blue);
                }
            }

 

저장된 Contours들을 Cv2.Moment를 이용하여 속성등을 구하고

그려진 Contours의 중심을 구하여 빨간색의 채워진 원을 해당 위치에 그려준다.

 

아래는 결과 이미지이다.

정상적으로 그려진것을 확인할 수 있다.

 

나는 이런식으로 취득한 이미지를 가지고 양쪽 끝에서 MatchShape로 찾은 모양만 가지고 

양쪽 끝단 기준 틀어진 정도, 즉 사분면 기준으로 일직선인지를 구하여 사용하려고한다.

 

그럼 왼쪽에서 찾은 이미지의 질량 중심 X,Y와 오른쪽 끝에서 찾은 이미지의 질량중심 X,Y의 차이만큼

Sin, Cos을 이용하여 Theta의 값을 구하는식으로 사용이 가능하다.

 

아래는 양쪽 끝단을 찾은 결과이미지이고, 그아래 코드를 참조하자.

 

 

Whole Code ( C# ) - 이것저것 Test하느라 정리가 안되어있음. 참고 및 도움용

using OpenCvSharp;
using OpenCvSharp.Extensions;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Point = OpenCvSharp.Point;

namespace ShapesMatching
{
    public partial class Form1 : Form
    {
        // 무조건 24bppRgb만 윤곽선을 찾을수가 있씀
        private const PixelFormat AVAILABLE_PIXEL_FORMAT = PixelFormat.Format24bppRgb;

        public Form1()
        {
            InitializeComponent();
        }
        public static Bitmap ConvertTo24bpp(Image img)
        {
            var bmp = new Bitmap(img.Width, img.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
            using (var gr = Graphics.FromImage(bmp))
                gr.DrawImage(img, new Rectangle(0, 0, img.Width, img.Height));
            return bmp;
        }

        private (Mat, Point[][], HierarchyIndex[]) FindContours(Bitmap bmp)
        {
            //Cv2.CvtColor(mat, mat, ColorConversionCodes.BGR2GRAY);
            Mat mat = new Mat();

            //if (bmp.PixelFormat == AVAILABLE_PIXEL_FORMAT)
            //{
            //    mat = BitmapConverter.ToMat(bmp);
            //    //mat = DetectImageContour(mat);
            //}
            //else
            //{
            //    bmp = ConvertTo24bpp(bmp);
            //
            //    mat = BitmapConverter.ToMat(bmp);
            //    //mat = DetectImageContour(mat);

            //}

            mat = BitmapConverter.ToMat(bmp);
            Cv2.CvtColor(mat, mat, ColorConversionCodes.BGR2GRAY);

            /*##################################################################################################################################################
             * 밑에꺼 함수원형임
             *                                (윤곽 찾을 이미지, 찾은 윤곽선 좌표      , 윤곽검출 계층정보             , 검색방법           , 근사화 방법)
             * public static void FindContours(InputArray image, out Point[][] contours, out HierarchyIndex[] hierarchy, RetrievalModes mode, ContourApproximationModes method);
             * 
             * RetrievalMode 검색방법 종류
             * | External : 외부 윤곽만 검출, 계층정보 x  | 
             * | List     : 모든 윤곽 검출, 계층정보 x    | 
             * | Ccomp    : 모든 윤곽 검출, 계층정보 2단계|
             * | Tree     : 모든 윤곽 검출, 모든 계층정보 |
             * 
             * ContourApproximationMode 근사화 방법 종류              |
             * | ApproxNone     : 모든 윤곽점 반환                   |
             * | ApproxSimple   : 윤곽점들 단순화, 끝점만 반환       |
             * | ApproxTC89L1   : 프리먼 체인 코드에서의 윤곽선 적용 |
             * | ApproxTC89KCOS : 프리먼 체인 코드에서의 윤곽선 적용 | 
             * ##################################################################################################################################################*/

            Cv2.Threshold(mat, mat, thresh: 87, maxval: 255, ThresholdTypes.Binary);
            Cv2.FindContours(mat, out Point[][] contours, out HierarchyIndex[] hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxNone);

            return (mat, contours, hierarchy);
        }

        private void btn_Run_Click(object sender, EventArgs e)
        {
            Point[][] contours1;
            HierarchyIndex[] hierarchy1;

            Mat pattern;
            (pattern, contours1, hierarchy1) = FindContours(Properties.Resources.Pin_roi);

            Cv2.ImShow("pattern", pattern);

            Point[][] contours2;
            HierarchyIndex[] hierarchy2;

            Mat data;
            (data, contours2, hierarchy2) = FindContours(Properties.Resources.PinImage);

            var output = new Mat();
            Cv2.CvtColor(data, output, ColorConversionCodes.GRAY2BGR);

            Cv2.ImShow("data", data);

            // 지직 거리는 노이즈 날리기
            ////////////////////////////////////////////////////
            for (int i = 0; i < contours2.GetLength(0); i++)
            {
                var area = Cv2.ContourArea(contours2[i].ToArray());

                if (area < 100)
                {
                    var rect = Cv2.BoundingRect(contours2[i]);
                    rect.Inflate(width: 1, height: 1);
                    output.Rectangle(rect, Scalar.Black, thickness: -1);
                }
            }
            ///////////////////////////////////////////////////
            int min, max;
            min = max = 0;
            double ave = 0;
            List<(double lave, int idx)> ans = new List<(double lave, int idx)>();

            // 윤곽선의 좌표는 우하좌상으로 들어있음
            for (int i = 0; i < contours2.GetLength(0); i++)
            {
                int cnt = contours2[i].Count();

                // 찾은게 윤곽선 좌표 개수가 말도안되게 많을 경우 무시해주기
                if (cnt >= 1000)
                    continue;

                var area = Cv2.ContourArea(contours2[i].ToArray());

                // 찾은 윤곽선 면적이 말도안되게 작을 경우 무시해주기
                if (area < 500)
                    continue;

                // 왼쪽 끝 핀 위치 구하기
                for (int j = 0; j < contours2[i].Count(); j++)
                {
                    min += contours2[i][j].X;
                }
                ave = min / cnt;

                ans.Add((ave, i));
                min = 0; ave = 0;
            }

            ans.Sort();
            //var sort = ans.OrderBy(x => x.idx).ThenBy(x => x.lave).ToList();


            /*##################################################################################################################################################
             * 밑에꺼 함수원형임
             *                (중심을 찾을 Contours 배열, BinaryImage인지 아닌지);
             * Moments Moments(IEnumerable<Point> array, bool binaryImage = false);
             * 
             * <<Moment 종류>>
             * 공간 모멘트
             * | m00,m01,m10| 
             * | m11,m20,m02| 
             * | m30,m21,m12|
             * | m03        |
             * 
             * 중심 모멘트
             * | mu20,mu11,mu02|
             * | mu30,mu21,mu12|
             * | mu03          |
             * 
             * 평준화 중심 모멘트
             * | nu20,nu11,nu02|
             * | nu30,nu21,nu12|
             * | nu03          |
             * 
             * https://en.wikipedia.org/wiki/Image_moment
             * ##################################################################################################################################################*/

            {
                var rect = Cv2.BoundingRect(contours2[ans[0].idx]);

                // Moment 찾을 Contours
                var moments = Cv2.Moments(contours2[ans[0].idx]);
                
                // Contours의 무게중심을 찾을 좌표 
                Point Center = new Point();

                /* 질량중심 Center X,Y 구하는 공식 
                 * Center X = (m10 / m00)
                 * Center Y = (m01 / m00)
                 */
                Center.X = (int)(moments.M10 / moments.M00);
                Center.Y = (int)(moments.M01 / moments.M00);

                Cv2.Circle(output, Center, 3, Scalar.Red, -1, LineTypes.AntiAlias);

                double ratio = Cv2.MatchShapes(data[rect], pattern, ShapeMatchModes.I3);
                if(ratio < 0.5)
                {
                    output.PutText(ratio.ToString("F3"), rect.TopLeft, HersheyFonts.HersheySimplex, fontScale: 0.8, Scalar.White);
                    output.Rectangle(rect, Scalar.Blue);
                }
            }

            ans.Reverse();

            // 오른쪽 Test
            {
                var rect = Cv2.BoundingRect(contours2[ans[0].idx]);

                var moments = Cv2.Moments(contours2[ans[0].idx]);
                Point Center = new Point();
                Center.X = (int)(moments.M10 / moments.M00);
                Center.Y = (int)(moments.M01 / moments.M00);
                Cv2.Circle(output, Center, 3, Scalar.Red, -1, LineTypes.AntiAlias);
                double ratio = Cv2.MatchShapes(data[rect], pattern, ShapeMatchModes.I3);
                if (ratio < 0.5)
                {
                    output.PutText(ratio.ToString("F3"), rect.TopLeft, HersheyFonts.HersheySimplex, fontScale: 0.8, Scalar.White);
                    output.Rectangle(rect, Scalar.GreenYellow);
                }
            }

            Cv2.ImShow("output", output);
        }


        private Mat DetectImageContour(Mat mat)
        {
            Bitmap targetBitmap = BitmapConverter.ToBitmap(mat);

            int targetBitmapWidth = targetBitmap.Width;
            int targetBitmapHeight = targetBitmap.Height;

            Rectangle targetRectangle = new Rectangle(0, 0, targetBitmapWidth, targetBitmapHeight);

            BitmapData targetBitmapData = targetBitmap.LockBits(targetRectangle, ImageLockMode.ReadWrite, targetBitmap.PixelFormat);

            IntPtr targetBitmapHandle = targetBitmapData.Scan0;

            int totalPixelCount = targetBitmapWidth * targetBitmapHeight;
            int totalByteCount = totalPixelCount * 3;

            int totalByteCountPerLine = targetBitmapData.Stride;
            int actualByteCountPerLine = targetBitmapWidth * 3;

            int alignmentByteCount = totalByteCountPerLine - actualByteCountPerLine;

            totalByteCount += targetBitmapHeight * alignmentByteCount;

            byte[] sourceRGBArray = new byte[totalByteCount];

            Marshal.Copy(targetBitmapHandle, sourceRGBArray, 0, totalByteCount);

            byte[,,] targetRGBArray = new byte[targetBitmapWidth, targetBitmapHeight, 3];
            float[,] targetBrightnessArray = new float[targetBitmapWidth, targetBitmapHeight];

            int sourceIndex = 0;

            for (int y = 0; y < targetBitmapHeight; y++)
            {
                for (int x = 0; x < targetBitmapWidth; x++)
                {
                    targetRGBArray[x, y, 0] = sourceRGBArray[sourceIndex + 2]; // Red
                    targetRGBArray[x, y, 1] = sourceRGBArray[sourceIndex + 1]; // Green
                    targetRGBArray[x, y, 2] = sourceRGBArray[sourceIndex + 0]; // Blue

                    targetBrightnessArray[x, y] = Color.FromArgb
                    (
                        sourceRGBArray[sourceIndex + 2],
                        sourceRGBArray[sourceIndex + 1],
                        sourceRGBArray[sourceIndex + 0]
                    ).GetBrightness();

                    sourceIndex += 3;
                }

                sourceIndex += alignmentByteCount;
            }

            float lowerLimit = 0.04f;
            float upperLimit = 0.04f;

            float maximumValue = 0;

            for (int y = 1; y < targetBitmapHeight - 1; y++)
            {
                for (int x = 1; x < targetBitmapWidth - 1; x++)
                {
                    maximumValue = Math.Abs(targetBrightnessArray[x - 1, y - 1] - targetBrightnessArray[x + 1, y + 1]);

                    if (maximumValue < Math.Abs(targetBrightnessArray[x - 1, y + 1] - targetBrightnessArray[x + 1, y - 1]))
                    {
                        maximumValue = Math.Abs(targetBrightnessArray[x - 1, y + 1] - targetBrightnessArray[x + 1, y - 1]);
                    }

                    if (maximumValue < Math.Abs(targetBrightnessArray[x, y + 1] - targetBrightnessArray[x, y - 1]))
                    {
                        maximumValue = Math.Abs(targetBrightnessArray[x, y + 1] - targetBrightnessArray[x, y - 1]);
                    }

                    if (maximumValue < Math.Abs(targetBrightnessArray[x - 1, y] - targetBrightnessArray[x + 1, y]))
                    {
                        maximumValue = Math.Abs(targetBrightnessArray[x - 1, y] - targetBrightnessArray[x + 1, y]);
                    }

                    // 색반전
                    ///////////////////////////////////////////////////
                    if (maximumValue < lowerLimit)
                    {
                        targetRGBArray[x, y, 0] = (byte)255;
                        targetRGBArray[x, y, 1] = (byte)255;
                        targetRGBArray[x, y, 2] = (byte)255;
                    }
                    else if (maximumValue > upperLimit)
                    {
                        targetRGBArray[x, y, 0] = (byte)0;
                        targetRGBArray[x, y, 1] = (byte)0;
                        targetRGBArray[x, y, 2] = (byte)0;
                    }
                    ///////////////////////////////////////////////////
                }
            }

            for (int y = 1; y < targetBitmapHeight - 1; y++)
            {
                for (int x = 1; x < targetBitmapWidth - 1; x++)
                {
                    byte brightness = (byte)((0.299 * targetRGBArray[x, y, 0]) + (0.587 * targetRGBArray[x, y, 1]) + (0.114 * targetRGBArray[x, y, 2]));
            
                    targetRGBArray[x, y, 0] = targetRGBArray[x, y, 1] = targetRGBArray[x, y, 2] = brightness;
                }
            }
        

            sourceIndex = 0;

            for (int y = 0; y < targetBitmapHeight; y++)
            {
                for (int x = 0; x < targetBitmapWidth; x++)
                {
                    sourceRGBArray[sourceIndex + 2] = targetRGBArray[x, y, 0]; // Red
                    sourceRGBArray[sourceIndex + 1] = targetRGBArray[x, y, 1]; // Green
                    sourceRGBArray[sourceIndex + 0] = targetRGBArray[x, y, 2]; // Blue

                    sourceIndex += 3;
                }

                sourceIndex += alignmentByteCount;
            }

            Marshal.Copy(sourceRGBArray, 0, targetBitmapHandle, totalByteCount);

            targetBitmap.UnlockBits(targetBitmapData);

            return BitmapConverter.ToMat(targetBitmap);
        }
    }
}