Favicon

Template matching, vision alignment

Peponi7/8/202510m

C#
NugetPackageOpenCvSharp4MatchTemplateMinMaxLoc

1. Introduction

Template matching은 image processing의 한 기법으로, 템플릿 이미지와 일치하는 이미지 상의 영역을 찾는 것을 목표로 한다. 템플릿 이미지가 매칭 이미지 상의 각 픽셀을 이동하면서 유사도를 측정 후 가장 높은 (또는 낮은) 유사도를 가지는 위치를 객체의 위치로 판단한다. 템플릿 이미지만 있으면 쉽게 적용할 수 있기 때문에 비전 검사, SLAM 등에 자주 활용된다. 다만, 템플릿 이미지를 이용해 유사도를 측정하는 만큼 조명, 노이즈 등의 환경 변화에 취약한 편이다.

이 문서에서는 기본적인 template matching부터 시작하여 한 이미지에 여러 객체가 존재하는 경우, 회전된 객체를 matching 하는 방법 등을 알아본다.

실습에 사용할 이미지는 다음과 같다.

Image by Rudy and Peter Skitterians from Pixabay

2. Match single object

match result

이 절에서는 매칭 이미지 상의 객체 중 템플릿 이미지와 가장 잘 매칭되는 객체를 찾는 방법을 알아본다. 템플릿 이미지는 다음 이미지를 사용하며, 위의 결과 이미지와 같이 검출된 지점에 붉은 박스를 그려 표시한다.

silver bullet

private void Match(Mat image, Mat template)
{
    // 전처리 (여기서는 간단하게 thresholding만 수행)
    using var thresholdImage = image.CvtColor(ColorConversionCodes.BGR2GRAY).Threshold(170, 255, ThresholdTypes.BinaryInv);
    using var thresholdTemplate = template.CvtColor(ColorConversionCodes.BGR2GRAY).Threshold(170, 255, ThresholdTypes.BinaryInv);
 
    // Template match 수행
    using var result = thresholdImage.MatchTemplate(thresholdTemplate, TemplateMatchModes.CCoeffNormed);
 
    // maxValue, maxLocation에 가장 잘 매칭된 경우의 값과 지점이 저장됨
    result.MinMaxLoc(out var minValue, out var maxValue, out var minLocation, out var maxLocation);
 
    using var matched = image.Clone();
 
    matched.Rectangle(maxLocation, new OpenCvSharp.Point(maxLocation.X + template.Width, maxLocation.Y + template.Height), Scalar.Crimson, 2);
 
    Cv2.ImShow("Matched", matched);
}

Mat.MatchTemplate() 메서드에 사용되는 TemplateMatchModes에 대해서는 다음 내용을 참조한다.

ModeDescriptionResult
SqDiff매칭 이미지와 템플릿 이미지의 제곱 차이 합 (SSD) 을 계산Lower is better
SqDiffNormedSqDiff 방식에 정규화를 적용하여 이미지의 밝기, 대비에 덜 민감Lower is better
CCorr매칭 이미지와 템플릿 이미지의 상관 관계 (Cross-Correlation) 를 계산Higher is better
CCorrNormedCCorr에 정규화를 적용하여 이미지의 밝기, 대비에 강건함Higher is better
CCoeff매칭 이미지와 템플릿 이미지의 상관 계수 (Cross-Correlation Coefficient) 를 계산하며 밝기 변화에 매우 강건함Higher is better
CCoeffNormedCCoeff에 정규화를 적용하여 밝기, 대비 변화에 매우 강건함. 일반적으로 많이 사용Higher is better

3. Match multiple object

match result

앞서 가장 잘 매칭되는 하나의 객체를 찾아내는 경우에는 Mat.MinMaxLoc() 메서드를 통해 간단하게 계산값과 지점을 얻을 수 있었다. 그러나 하나의 이미지 안에는 여러 객체가 존재할 수 있는데, 이 경우에는 계산 결과를 순회하며 매칭 여부를 판단한다.

이 절에서는 템플릿 이미지로 구리 탄환을 선택하여 여러 객체를 검출하는 예를 알아본다.

copper bullet

private void MultipleMatch(Mat image, Mat template)
{
    // 전처리 (여기서는 간단하게 thresholding만 수행)
    using var thresholdImage = image.CvtColor(ColorConversionCodes.BGR2GRAY).Threshold(170, 255, ThresholdTypes.BinaryInv);
    using var thresholdTemplate = template.CvtColor(ColorConversionCodes.BGR2GRAY).Threshold(170, 255, ThresholdTypes.BinaryInv);
 
    // Template match 수행
    using var result = thresholdImage.MatchTemplate(thresholdTemplate, TemplateMatchModes.CCoeffNormed);
 
    using var matched = image.Clone();
 
    // 결과를 순회하며 thresholding 수행
    for (int i = 0; i < result.Cols; i++)
    {
        for (int j = 0; j < result.Rows; j++)
        {
            // 0.7 이상의 값을 가진 경우 매칭된 것으로 판정
            if (result.Get<float>(j, i) > 0.7)
            {
                matched.Rectangle(new OpenCvSharp.Point(i, j), new OpenCvSharp.Point(i + template.Width, j + template.Height), Scalar.Crimson, 2);
            }
        }
    }
 
    Cv2.ImShow("Multiple matched", matched);
}

4. Match rotated object

match rotated

앞서 템플릿 이미지를 사용하는 template matching의 특성상 환경 요인에 취약하다 기술하였는데, 이미지에 회전이 가해지는 경우 엉뚱한 지점을 검출하는 경향이 있다. 여기서는 올바른 객체 검출을 위해 템플릿 이미지를 돌려가며 매칭을 수행, 각 각도에서 매칭 여부를 판단하는 예를 알아본다.

템플릿 이미지는 다음과 같으며, 객체 검출 시점의 템플릿 이미지는 우측에 표시되어 있다.

silver bulletrotated bullet
private void MatchWithTemplateRotation(Mat image, Mat template)
{
    int rotateAngle = 0;
 
    // 전처리 (여기서는 간단하게 thresholding만 수행)
    using var thresholdImage = image.CvtColor(ColorConversionCodes.BGR2GRAY).Threshold(170, 255, ThresholdTypes.BinaryInv);
    using var thresholdTemplate = template.CvtColor(ColorConversionCodes.BGR2GRAY).Threshold(170, 255, ThresholdTypes.BinaryInv);
 
    while (true)
    {
        // 템플릿 이미지를 돌려가며 매칭 수행
        using var matchingTemplate = Rotate(thresholdTemplate, rotateAngle * Math.PI / 180);
 
        using var result = thresholdImage.MatchTemplate(matchingTemplate, TemplateMatchModes.CCoeffNormed);
 
        // maxValue를 이용해 얼마나 잘 매칭되었는지 확인
        result.MinMaxLoc(out var minValue, out var maxValue, out var minLocation, out var maxLocation);
 
        // 0.7 이상의 값을 가질 때 매칭된 것으로 판정
        if (maxValue > 0.7)
        {
            using var matched = image.Clone();
 
            matched.Rectangle(maxLocation, new OpenCvSharp.Point(maxLocation.X + matchingTemplate.Width, maxLocation.Y + matchingTemplate.Height), Scalar.Crimson, 2);
 
            Cv2.ImShow("Matched with rotation", matched);
            Cv2.ImShow("Template with rotation", matchingTemplate);
 
            break;
        }
        else
        {
            // 매칭이 잘 되지 않는 구간은 회전을 크게 돌려 탐색 속도를 빠르게 함
            rotateAngle += maxValue switch
            {
                >= 0.3 and < 0.5 => 3,
                >= 0.5 and < 0.6 => 2,
                >= 0.6 => 1,
                _ => 5
            };
 
            if (rotateAngle > 360)
                break;
        }
    }
}

TIP

여기서 사용된 Rotate() 메서드에 대한 자세한 내용은 Affine transformation을 참조한다.

5. Vision alignment

로보틱스 같은 분야에서는 종종 template matching을 통해 alignment를 수행하여 동작 정확성을 달성하기도 한다. 픽 앤 플레이스 동작과 같은 경우, 특히 플레이스 동작의 경우에는 종종 정확한 위치에 내려놓는 동작이 필요한데, 이 때 위치를 결정하기 위해 template matching을 활용하기도 한다. Real world에서 로봇이 들고 있는 물체는 항상 같은 포지션에 있다는 보장이 없기 때문인데, 비전 촬영을 통해 dx, dy, dr 값을 계산한 후 이동 경로를 보정하여 정확한 위치에 내려놓을 수 있도록 한다.

여기서는 임의의 위치, 기울기를 가진 총알을 찾은 후 각도를 정렬하여 화면 중앙으로 가져오는 예시를 소개한다.

alignment result

// 임의의 영역에 비스듬하게 찍혀있는 총알을 찾아 각도 정렬, 화면 중앙으로 가져오는 예
private void ExecuteAlignment(Mat image, Mat template)
{
    int rotateAngle = 0;
 
    // 전처리 (여기서는 간단하게 thresholding만 수행)
    using var thresholdImage = image.CvtColor(ColorConversionCodes.BGR2GRAY).Threshold(170, 255, ThresholdTypes.BinaryInv);
    using var thresholdTemplate = template.CvtColor(ColorConversionCodes.BGR2GRAY).Threshold(170, 255, ThresholdTypes.BinaryInv);
 
    while (true)
    {
        // Matching 이미지를 돌려가며 매칭 수행
        using var matchingImage = Rotate(thresholdImage, rotateAngle * Math.PI / 180);
 
        using var result = matchingImage.MatchTemplate(thresholdTemplate, TemplateMatchModes.CCoeffNormed);
 
        // maxValue를 이용해 얼마나 잘 매칭되었는지 확인
        result.MinMaxLoc(out var minValue, out var maxValue, out var minLocation, out var maxLocation);
 
        // 0.7 이상의 값을 가질 때 매칭된 것으로 판정
        if (maxValue > 0.7)
        {
            // 찾은 총알 표시
            using var matched = Rotate(image, rotateAngle * Math.PI / 180);
            matched.Rectangle(maxLocation, new OpenCvSharp.Point(maxLocation.X + thresholdTemplate.Width, maxLocation.Y + thresholdTemplate.Height), Scalar.Crimson, 2);
 
            // 단위행렬 생성 (1행 : [1, 0, 0],  2행 : [0, 1, 0])
            using Mat translation = Mat.Eye(2, 3, MatType.CV_32F);
 
            // 찾은 총알을 화면 중앙으로 가져오기 위해 center 계산
            float centerX = -1 * maxLocation.X + matched.Width / 2 - thresholdTemplate.Width / 2;
            float centerY = -1 * maxLocation.Y + matched.Height / 2 - thresholdTemplate.Height / 2;
            translation.Set(0, 2, centerX);
            translation.Set(1, 2, centerY);
 
            using var translated = new Mat();
 
            // 화면 중앙으로 찾은 총알 가져오기
            Cv2.WarpAffine(matched, translated, translation, new OpenCvSharp.Size(matched.Width, matched.Height));
 
            Cv2.ImShow("Matched with alignment", translated);
 
            break;
        }
        else
        {
            // 매칭이 잘 되지 않는 구간은 회전을 크게 돌려 탐색 속도를 빠르게 함
            rotateAngle += maxValue switch
            {
                >= 0.3 and < 0.5 => 3,
                >= 0.5 and < 0.6 => 2,
                >= 0.6 => 1,
                _ => 5
            };
 
            if (rotateAngle > 360)
                break;
        }
    }
}

6. 참조 자료