NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 학습
  • 매뉴얼

    학습


    C# C# .NET 매크로 프로그램 만들기. (이미지 서치 액션)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 오랜만에 이미지 관련 액션을 만들게 되었습니다. 아직 추가해야할 액션들이 많긴하지만, 기본이 되는 액션을 먼저 만드는게 좋을듯 합니다. 앞으로 여러가지 액션들을 추가하면서 테스트를 진행해야 하는데요. 대부분 이미지 인식과 문자 또는 숫자 인식을 중요하게 생각할겁니다. 이 부분들을 테스트하려면 이미지 서치와 이미지 매치 기능이 꼭 필요합니다. 물론, OCR 기능도 추가해야겠지만요.

     

    이미지 매치는 OpenCV의 Templete Matching을 사용합니다. 템플릿 매칭은 더 큰 이미지에서 템플릿 이미지의 위치를 검색하고 찾는 방법입니다. 이는 단순히 입력 이미지 위로 템플릿 이미지를 2D 컨볼루션에서와 같이 슬라이드하고 템플릿 이미지 아래의 입력 이미지 패치와 템플릿을 비교합니다. OpenCV에는 여러 비교 방법이 구현되어 있습니다. 각 픽셀은 해당 픽셀의 이웃이 템플릿과 얼마나 일치하는지 나타내는 회색조 이미지를 반환합니다. 그렇다보니 템플릿 매칭은 동일한 이미지의 컬러 값을 비교할 수 없습니다.

     

    위의 문제를 해결하기 위한 방법으로 이미지 서치를 사용합니다. 이미지 서치는 각각의 픽셀 범위를 비교하는 알고리즘을 사용합니다. 따라서, 템플릿 매칭에서 동일한 이미지의 컬러를 판단할 수 없을 때 이미지 서치를 사용하면 유용합니다. 개념적인 내용을 알아봤으므로 이미지 서치를 어떻게 구현해야 하는지를 알아보도록 하겠습니다. 우선, 이미지 서치 모델을 하나 추가하고, 기본이 되는 배이스를 상속 받습니다.

    public class ImageSearchModel : ImageModel

     

    이미지 모델은 이미지 조건 처리 액션들이 모두 상속 받아서 구현해야 하는데요. 이미지 모델은 Source 이미지와 Target 이미지를 가져오고, 이 둘의 이미지를 비교할 수 있는 상태까지 미리 처리해줍니다. 또한, Source 이미지를 캡쳐할 수 있는 속성도 가지고 있습니다. RPA 매크로에서 현재 화면에 내가 원하는 이미지가 나타났는지를 체크해야 하기 때문에 찾을 이미지인 Source 이미지를 캡쳐해서 미리 저장해두어야 합니다.

    [LocalizedCategory("SelectImage")]
    [LocalizedDisplayName("Capture")]
    [LocalizedDescription("Capture")]
    [Browsable(true)]
    [DefaultValue(null)]
    [Editor(typeof(TypeEditor.CaptureEditor), typeof(System.Drawing.Design.UITypeEditor))]
    public virtual string? Capture 
    { 
        get 
        {
            return Path.GetFileName(this.SelectImageFile);
        }
        set
        {
            SelectImageFile = value?.ToString();
        }
    }

     

    ImageModel에서 찾을 이미지(Source image)와 찾을 이미지가 있는 현재 윈도우 화면(Target image)을 제대로 가져왔는지 체크합니다.

    public override string? Execute(Ai.Interface.IPlayer player)
    {
        string? id = base.Execute(player);
    
        if (!string.IsNullOrEmpty(SelectImageFile))
        {
            _source = Ai.Api.ImageManager.ImageEditor.Load(SelectImageFile);
    
            if (player.Manager.Option.UseScaleAndLayout)
            {
                int sf = Ai.Common.Windows.GetScalingFactor();
                _source = Ai.Common.ImageEditor.Resize(_source, sf - player.Manager.Option.ScaleAndLayoutValue);
            }
        }
    
        return id;
    }

     

    이전 버전에서는 윈도우의 배율 및 레이아웃을 사용자가 선택해야 하는 번거로움이 있었습니다. 하지만, 새 버전에서는 작업 컴퓨터의 배율이 100프로고, 배포하는 다른 컴퓨터의 배율이 150프로더라도 문제없이 처리되도록 개선했습니다. 이 처리를 담당하는 함수가 ScalingFactor입니다.

     

    이미지 매치에도 있었던 정확도 속성을 이미지 서치에서도 동일하게 사용합니다. 이전 버전에서는 이미지 매치와 이미지 서치의 처리 방식이 달라서 정확도와 공차라는 서로 다른 명칭을 사용했습니다. 그리고, 값도 서로 다르게 적용되었는데요. 일관성을 위해 이미지 서치와 이미지 매치가 같은 로직으로 처리될 수 있도록 했습니다.

    [LocalizedCategory("Action")]
    [LocalizedDisplayName("AccuracyRate")]
    [LocalizedDescription("AccuracyRate")]
    [Browsable(true)]
    [DefaultValue(80)]
    public int AccuracyRate { get; set; } = 80;

     

    UsedSkipTransparent 속성은 찾을 이미지인 Source에서 투명하게 처리된 부분은 비교에서 제외시키는 옵션입니다. 일반적으로 찾고 싶은 이미지를 캡쳐할 사각형으로 캡쳐가 이루어집니다. 하지만, 찾을 물체는 사각형이 아닌 경우가 대부분입니다. 그렇다보니 찾고 싶은 물체의 뒤쪽 배경이 변화하는 환경에서는 이미지 서치의 비교 성공율이 극도로 낮아지는 현상이 발생합니다. 따라서, 찾을 물체만 남겨두고 배경을 투명으로 날리면 좀 더 쉽게 이미지 프로세스를 처리할 수 있습니다.

    ※ 이런 경우에는 대부분 이미지 매치를 사용합니다. 하지만, 이미지 매치를 사용할 수 없는 경우 유용한 기능입니다.

    [LocalizedCategory("Action")]
    [LocalizedDisplayName("UsedSkipTransparent")]
    [LocalizedDescription("UsedSkipTransparent")]
    [Browsable(true)]
    [DefaultValue(false)]
    public bool UsedSkipTransparent { get; set; }

     

    IsSearchDirectionReverse 속성은 이미지를 찾을 때 왼쪽 위에서 오른쪽 아래로 찾는 방식을 반대로 처리해줍니다. 이 부분은 테스트를 통해 확인할 수 있습니다.

    [LocalizedCategory("Action")]
    [LocalizedDisplayName("IsSearchDirectionReverse")]
    [LocalizedDescription("IsSearchDirectionReverse")]
    [Browsable(true)]
    [DefaultValue(false)]
    public bool IsSearchDirectionReverse { get; set; }

     

    이미지를 찾는 로직은 약간 복잡합니다. 일반적으로 픽셀 정보를 읽어와서 하나씩 비교하는 방식을 사용하는데요. 이 방식은 GDI+를 사용하기 때문에 속도가 매우 느립니다. 장점은 편리하고 간단하게 구현할 수 있습니다. 하지만, 매크로 프로그램은 실시간으로 이미지를 분석해야 하는데요. GDI+를 사용하면 속도 문제로 원하는 동작을 보장할 수 없게됩니다. 그래서, 메모리에서 픽셀 정보를 찾아서 비교해야 하는데요. 이렇게 구현하려면 기본적으로 메모리 구조와 픽셀 데이터에 대한 이해가 필요합니다.

     

    이미지 분석에 필요한 메소드를 하나 추가했습니다.

    Rectangle rect = Ai.Common.ConditionImage.ImageSearch(_source, target, AccuracyRate, UsedSkipTransparent, IsSearchDirectionReverse, PixelFormat);

     

    아래 전체 코드에서 핵심은 GetPixelArray 메소드입니다.

    internal static Rectangle ImageSearch(Image source, Image target, int tolerance = 80, bool skipTransparent = false, bool isSearchDirectionReverse = false, PixelFormat pixelFormat = PixelFormat.DontCare)
    {
        if (Screen.PrimaryScreen == null)
            return Rectangle.Empty;
    
        if (null == target || null == source)
            return Rectangle.Empty;
    
        try
        {
            if (target.Width < source.Width || target.Height < source.Height)
                return Rectangle.Empty;
    
            tolerance = 100 - tolerance;
            var targetArray = GetPixelArray((Bitmap)target, pixelFormat);
            var sourceArray = GetPixelArray((Bitmap)source, pixelFormat);
    
            if (targetArray.Length == 1 && sourceArray.Length == 1)
            {
                if (ContainSameElements(targetArray[0], 0, sourceArray[0], 0, 1, tolerance, skipTransparent))
                    return new Rectangle(new System.Drawing.Point(0, 0), source.Size);
    
                return Rectangle.Empty;
            }
    
            int takeArray = target.Height - source.Height;
            if (target.Height == source.Height && targetArray.Length > 0)
            {
                takeArray = 1;
    
                List<int> subArr = targetArray[0].ToList();
                subArr.Add(-1);
                targetArray[0] = subArr.ToArray();
            }
    
            foreach (var firstLineMatchPoint in FindMatch(targetArray.Take(takeArray), sourceArray[0], tolerance, skipTransparent, isSearchDirectionReverse))
            {
                if (IsSourcePresentAtLocation(targetArray, sourceArray, firstLineMatchPoint, 1, tolerance, skipTransparent))
                    return new Rectangle(firstLineMatchPoint, source.Size);
            }
    
            return Rectangle.Empty;
        }
        catch
        {
            throw;
        }
    }

     

    GetPixelArray 메소드는 아래와 같이 구현되어 있는데요. 픽셀이 모인 이미지의 Width, Height에 대해 2차원 배열을 만들고, 각각의 배열마다 픽셀 정보를 비트 값으로 저장해줍니다. 이 때 픽셀들을 어떻게 처리할지는 PixelFormat으로 결정합니다.

    private static int[][] GetPixelArray(Bitmap bitmap, PixelFormat pixelFormat)
    {
        var result = new int[bitmap.Height][];
        BitmapData? bd = null;
    
        try
        {
            PixelFormat pf = PixelFormat.Format32bppArgb;
    
            if (!(pixelFormat == PixelFormat.Undefined || pixelFormat == PixelFormat.DontCare))
                pf = pixelFormat;
    
            bd = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, pf);
    
            for (int y = 0; y < bitmap.Height; ++y)
            {
                result[y] = new int[bitmap.Width];
                Marshal.Copy(bd.Scan0 + y * bd.Stride, result[y], 0, result[y].Length);
            }
        }
        finally
        {
            if (bd != null)
                bitmap?.UnlockBits(bd);
        }
        return result;
    }

     

    메모리의 픽셀 정보에 접근하면 스레드로부터 안전하지 않습니다. 그래서, 해당 비트맵을 잠궈줘야 합니다.

    bd = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, pf);

     

    메모리에서 픽셀 정보를 마샬링을 통해 얻습니다. 메모리의 픽셀 영역을 스캔하면 4개의 비트가 나옵니다. 여기서 스트라이더를 통해 비트를 하나씩 이동합니다. 참고로, 픽셀의 비트 영역은 4단계고, 각각의 비트는 BGRA를 비트로 표현합니다. 비트는 0~255의 값을 가집니다.

    Marshal.Copy(bd.Scan0 + y * bd.Stride, result[y], 0, result[y].Length);

     

    비트맵을 계산하는 공식은 아래와 같습니다.

    int[] tc = new int[3];
    tc[0] = (byte)((target[i + targetStart] >> 16) & 0xFF);
    tc[1] = (byte)((target[i + targetStart] >> 8) & 0xFF);
    tc[2] = (byte)((target[i + targetStart]) & 0xFF);
    
    int upRGB = tc[0] + tolerance + tc[1] + tolerance + tc[2] + tolerance;
    int downRGB = tc[0] - tolerance + tc[1] - tolerance + tc[2] - tolerance;
    int scRGB = (byte)((source[i + sourceStart] >> 16) & 0xFF) +
        (byte)((source[i + sourceStart] >> 8) & 0xFF) +
        (byte)((source[i + sourceStart]) & 0xFF);

     

    찾을 이미지의 비트맵과 대상 이미지의 비트맵을 비교하면서 비트맵 데이타에 임시 저장합니다. Tolerance(공차)를 적용하면서 다음 RGB 값을 비교합니다. 이렇게 임시 저장된 값이 이미지 크기와 같아지면 참이되고, 다르면 거짓이 됩니다. 이 조건을 응용해서 메모리의 비트맵 정보로 이미지 서치를 구현할 수 있습니다.

     

    그림판에 오랜지색 점을 2개 찍어두었습니다.

    TPMTtHD.png

     

     

    매크로 에디터 프로그램을 실행하고, 이미지 서치를 추가하세요.

    VprBHUf.png

     

     

    매크로를 실행해보세요. 그전에 동작 확인을 위해 마우스 사용을 True로 변경해야 합니다. 그림판의 좌상단 오랜지색 점을 잘 찾네요. 두번째 테스트는 반대로 찾기 옵션을 True로 변경하고 실행했습니다. 이번에는 우하단의 오랜지색 점을 찾아서 클릭합니다.

     

     

    이미지 서치 관련해서 더 많은 기능들이 있는데요. 좀 더 테스트해보고, 세부적인 내용들도 추가하려고 했는데~ 감기가 심하게 걸려서 좀 쉬어야겠어요. 다음에 다시 한번 이미지 서치를 다룰 때 추가된 기능들에 대해서 다시한번 소개해야 할거 같습니다. 컨디션이 안좋은데 오전부터 일처리할게 있어서 나갔다와서 오후에 교육을 했더니 체력이 거의 남아있지 않네요.

     

    개발자에게 후원하기

    MGtdv7r.png

     

    추천, 구독, 홍보 꼭~ 부탁드립니다.

    여러분의 후원이 빠른 귀농을 가능하게 해줍니다~ 답답한 도시를 벗어나 귀농하고 싶은 개발자~

    감사합니다~

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.