Microsoft MVP성태의 닷넷 이야기
Math: 2. "Zhang Suen 알고리즘(세선화, Thinning/Skeletonization)"의 C# 버전 [링크 복사], [링크+제목 복사],
조회: 27344
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

"Zhang Suen 알고리즘(세선화, Thinning/Skeletonization)"의 C# 버전

세선화(Thinning, Skeletonization)라... ^^

[AS3] 세선화, Thinning
; http://broneri.tistory.com/entry/AS3-%EC%84%B8%EC%84%A0%ED%99%94-Thinning

재미있을 것 같아서, C#으로 포팅해 보았습니다. 로직은 위의 글에 나온 것을 그대로 적용했고 단지 이미지 파일을 직접 읽어들여서 이를 true/false 배열로 변경하는 것과 그 반대의 작업을 할 수 있는 코드를 추가했습니다.

============ 이미지 파일을 읽어서 true/false 배열로 반환 ============
Image img = Bitmap.FromFile("triangle.png");
Bitmap bmp = new Bitmap(img);

bool[,] imgBinaries = ConvertToBool(bmp);

bool[,] ConvertToBool(Bitmap bmp)
{
    bool[,] imgData = new bool[bmp.Width, bmp.Height];

    for (int x = 0; x < bmp.Width; x++)
    {
        for (int y = 0; y < bmp.Height; y++)
        {
            Color c = bmp.GetPixel(x, y);
            imgData[x, y] = (uint)c.ToArgb() != 0xff000000; // 단일 색과 비교해서 true로 설정
        }
    }

    return imgData;
}

============ true/false 배열을 이미지로 변환 ============
Bitmap ToBitmaps(bool[,] imgBinaries)
{
    int width = imgBinaries.GetLength(0);
    int height = imgBinaries.GetLength(1);

    Bitmap bmp = new Bitmap(width, height);

    for (int x = 0; x < bmp.Width; x++)
    {
        for (int y = 0; y < bmp.Height; y++)
        {
            if (imgBinaries[x, y] == true)
            {
                bmp.SetPixel(x, y, Color.Black);
            }
        }
    }

    return bmp;
}

물론, 세선화하기 위해서는 단일 색으로 가정하는 것이 코딩이 편하기 때문에 제 경우는 위에서 본 것처럼 하얀색(0xff000000)으로 지정해 두었습니다.

이하 나머지 코드는 원 글에서 공개된 ActionScript를 거의 그대로 포팅한 것인데, 첨부 파일에도 "WindowsFormsApplication1" 프로젝트로 포함해 두었지만 간단하니 아래에 그대로 실었습니다.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        Image img = Bitmap.FromFile("triangle.png");
        picBefore.Image = img;

        Bitmap bmp = new Bitmap(img);

        bool[,] imgBinaries = ConvertToBool(bmp);

        this.tableLayoutPanel1.RowStyles[0].SizeType = SizeType.Absolute;
        this.tableLayoutPanel1.RowStyles[0].Height = bmp.Height + 10;

        OutputData(txtBefore, imgBinaries);

        thin_image(imgBinaries, bmp.Width, bmp.Height);

        bmp = ToBitmaps(imgBinaries);
        picAfter.Image = bmp;

        OutputData(txtAfter, imgBinaries);
    }

    void thin_image(bool[,] img, int width, int height)
    {
        while (true)
        {
            if (thin_loop1(img, width, height) == 0)
            {
                break;
            }
        }
        while (true)
        {
            if (thin_loop2(img, width, height) == 0)
            {
                break;
            }
        }
    }

    private int thin_loop1(bool[,] img, int width, int height)
    {
        bool[,] del = new bool[width, height];

        int thin_flag = 0;
        int y = 1;
        int x = 1;
        for (y = 1; y < height - 1; y++)
        {
            for (x = 1; x < width - 1; x++)
            {
                if (thin_pixel_loop1(img, x, y) == true)
                {
                    del[y, x] = true;
                }
            }
        }

        for (y = 1; y < height - 1; y++)
        {
            for (x = 1; x < width - 1; x++)
            {
                if (del[y, x] == true)
                {
                    img[y, x] = false;
                    thin_flag = 1;
                }
            }
        }
        return thin_flag;
    }

    private bool thin_pixel_loop1(bool[,] img, int x, int y)
    {
        if (thin_pixel_common(img, x, y) == 0)
            return false;

        if ((img[y - 1, x] == false || img[y, x - 1] == false || img[y, x + 1] == false) &&
                (img[y, x - 1] == false || img[y - 1, x] == false || img[y, x + 1] == false))
        {
            return true;
        }
        return false;
    }

    private int thin_pixel_common(bool[,] img, int x, int y)
    {
                // pixel removal condition
        // 1. pixel is black            
        if ( img[y, x] == false )
            return 0;
             
        bool [] near = new bool[] {
        img[y-1, x-1], img[y-1, x], img[y-1, x+1], img[y, x+1], 
        img[y+1, x+1], img[y+1, x], img[y+1, x-1], img[y, x-1], img[y-1, x-1]
        };
  
        int count = 0;
        for( int i = 0; i <= 7; i++ ) {
            if( near[i] == true ) count ++;
        }
             
            // 2. near pixels 
        // check black pixels are >=2 and <=6
        if ( count < 2 || count > 6 )
            return 0;
                                    
        int connect = 0;
        for( int i = 0; i <= 7; i++ ) {
            if( near[i] == true && near[i+1] == false ) connect ++;
        }
             
            // 3. connectivity is 1 
        if ( connect != 1 )
            return 0;
                 
        return 1;
    }

    private int thin_loop2(bool[,] img, int width, int height)
    {
        bool[,] del = new bool[width, height];
             
        int thin_flag = 0; 
        int y= 1;
        int x= 1;
        for ( y = 1; y < height-1; y++ ) {
            for ( x = 1; x < width-1; x++ ) {
                if ( thin_pixel_loop2( img, x,y ) == 1 ) {
                    del [y, x] = true;
                }
            }
        }
             
        for ( y = 1; y < height-1; y++ ) {
            for ( x = 1; x < width-1; x++ ) {
                if ( del [y, x] == true ) {
                    img[y, x] = false;
                    thin_flag = 1;
                }
            }
        }
        return thin_flag;
    }

    private int thin_pixel_loop2(bool[,] img, int x, int y)
    {
        if (thin_pixel_common(img, x, y) == 0)
            return 0;
        if ((img[y - 1, x] == false || img[y, x + 1] == false || img[y + 1, x] == false) &&
                (img[y, x - 1] == false || img[y + 1, x] == false || img[y, x + 1] == false))
        {
            return 1;
        }
        return 0;
    }

    private void OutputData(TextBox txtOutput, bool[,] imgBinaries)
    {
        StringBuilder sb = new StringBuilder();

        sb.AppendLine("{");
        for (int x = 0; x < imgBinaries.GetLength(0); x++)
        {
            sb.Append("\t{ ");
            for (int y = 0; y < imgBinaries.GetLength(1); y++)
            {
                if (imgBinaries[y, x] == true)
                {
                    sb.Append("1,");
                }
                else
                {
                    sb.Append("0,");
                }
            }

            sb.AppendLine(" },");
        }
        sb.AppendLine("}");

        txtOutput.Text = sb.ToString();
    }

    bool[,] ConvertToBool(Bitmap bmp)
    {
        bool[,] imgData = new bool[bmp.Width, bmp.Height];

        for (int x = 0; x < bmp.Width; x++)
        {
            for (int y = 0; y < bmp.Height; y++)
            {
                Color c = bmp.GetPixel(x, y);
                imgData[x, y] = (uint)c.ToArgb() != 0xff000000;
            }
        }

        return imgData;
    }

    Bitmap ToBitmaps(bool[,] imgBinaries)
    {
        int width = imgBinaries.GetLength(0);
        int height = imgBinaries.GetLength(1);

        Bitmap bmp = new Bitmap(width, height);

        for (int x = 0; x < bmp.Width; x++)
        {
            for (int y = 0; y < bmp.Height; y++)
            {
                if (imgBinaries[x, y] == true)
                {
                    bmp.SetPixel(x, y, Color.Black);
                }
            }
        }

        return bmp;
    }

}

그런데 버그가 있더군요. 원글에서 공개된 예제는 정상적으로 처리되지만, 제가 임의로 그린 아래의 98*98 이미지는,

triangle.png
(참고로, 저는 색상을 반전시켜서 하얀색을 1로 처리한 후, 검정색으로 출력했습니다.)

다음과 같이 밑변이 날아가는 오류가 있습니다. (게다가 위의 소스 코드는 입력 이미지가 정사각형 이미지만 정상적으로 처리한다는 단점도 있습니다.)

cs_thinning_1.png

음... 호기심이 생기더군요. 그래도 "Zhang Suen 알고리즘"이라고 불릴 정도면 이런 식의 오류가 있을 것 같지는 않은데 어쩌면 ActionScript로 포팅된 소스가 잘못 옮겨진 것이 아닐까 하는 생각이 들었습니다.

그래서, 인터넷 검색 결과 C 버전의 "Zhang Suen 알고리즘" 코드를 발견할 수 있었습니다.

Zhang-Suen thinning
; http://pages.cpsc.ucalgary.ca/~parker/thin.c

오호~~~ 위의 코드로 테스트 해보니 정상적으로 삼각형 이미지에 대해서 thinning이 되었습니다. (위의 소스 코드를 Visual C++ 2010 프로젝트로 그대로 옮긴 것을 첨부 파일의 Thinning 프로젝트에 추가했으니 참고하십시오.)

C#으로는 다음과 같이 옮겼고,

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    ...[OnLoad 이벤트 생략]...

    int[,] y;

    void thin_b(int[,] image, int width, int height)
    {
        int i, j, n, m, k, cont, br, ar, p1, p2;
        br = 0;
        int[] a = new int[8];

        cont = 1;
        while (cont != 0)
        {
            cont = 0;

            /*  Sub-iteration 1: */
            for (i = 0; i < width; i++)
                for (j = 0; j < height; j++)
                {       /* Scan the entire image */
                    if (image[i, j] == 0)
                    {
                        y[i, j] = 0;
                        continue;
                    }

                    ar = t1a(image, i, j, a, ref br, width, height);    /* Function A */

                    p1 = a[0] * a[2] * a[4];
                    p2 = a[2] * a[4] * a[6];
                    if ((ar == 1) && ((br >= 2) && (br <= 6)) &&
                        (p1 == 0) && (p2 == 0))
                    {
                        y[i, j] = 1;
                        cont = 1;
                    }
                    else y[i, j] = 0;
                }
            subtr(y, image, width, height);

            /* Sub iteration 2: */
            for (i = 0; i < width; i++)
                for (j = 0; j < height; j++)
                {       /* Scan the entire image */
                    if (image[i, j] == 0)
                    {
                        y[i, j] = 0;
                        continue;
                    }
                    ar = t1a(image, i, j, a, ref br, width, height);    /* Function A */

                    p1 = a[0] * a[2] * a[6];
                    p2 = a[0] * a[4] * a[6];
                    if ((ar == 1) && ((br >= 2) && (br <= 6)) &&
                        (p1 == 0) && (p2 == 0))
                    {
                        y[i, j] = 1;
                        cont = 1;
                    }
                    else y[i, j] = 0;
                }

            subtr(y, image, width, height);
        }
    }

    int t1a(int[,] image, int i, int j, int[] a, ref int b, int nn, int mm)
    {
        /*  Return the number of 01 patterns in the sequence of pixels
        P2 p3 p4 p5 p6 p7 p8 p9.                    */

        int n,m;

        for (n=0; n<8; n++) a[n] = 0;
        if (i-1 >= 0) {
            a[0] = image[i-1,j];
            if (j+1 < mm) a[1] = image[i-1,j+1];
            if (j-1 >= 0) a[7] = image[i-1,j-1];
        }
        if (i+1 < nn) {
            a[4] = image[i+1,j];
            if (j+1 < mm) a[3] = image[i+1,j+1];
            if (j-1 >= 0) a[5] = image[i+1,j-1];
        }
        if (j+1 < mm) a[2] = image[i,j+1];
        if (j-1 >= 0) a[6] = image[i,j-1];

        m= 0;   b = 0;
        for (n=0; n<7; n++) {
            if ((a[n]==0) && (a[n+1]==1)) m++;
            b = b + a[n];
        }
        if ((a[7] == 0) && (a[0] == 1)) m++;
        b = b + a[7];
        return m;
    }

    void subtr(int[,] a, int[,] b, int n, int m)
    {
        int i, j;

        for (i = 0; i < n; i++)
            for (j = 0; j < m; j++)
            {
                b[i, j] -= a[i, j];
            }
    }
}

결과는 아래와 같이 밑변이 사라지지 않았습니다. (게다가, 정사각형 이미지가 아니어도 정상적으로 처리가 잘 되더군요. ^^)

cs_thinning_2.png

첨부된 파일은 위의 코드를 포함한 예제 프로젝트입니다.






[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 6/21/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2011-09-05 06시28분
아 이런~ 재미난 "image to ascii" or "ascii to image" 연예인 사진들좀 하면 재미 날 것 같다.
문우영
2015-12-06 08시05분
[동국대학생] 정말 좋은자료 보고 많은것을 배워갑니다! Thinning 알고리즘에대한 궁금증이 시원하게 해결되었습니다!
[guest]
2016-06-23 12시23분
[대학생] 이진화 시킨 글자이미지를 세선화 처리 할려고 했는데, 색깔 반전도 검정색이 읽히도록 다시 반전해줬는데도 이미지를 넣어서 디버깅 시키면 깨져서 나오네요. 어떻게 해야 할까요
[guest]
2016-06-23 12시47분
글쎄요. 그런 부분은 스스로 해결해야 할 문제 같습니다. ^^
정성태

... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13241정성태2/3/20234003디버깅 기술: 188. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.IO.FileNotFoundException
13240정성태2/1/20234166디버깅 기술: 187. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.Web.HttpException
13239정성태2/1/20233848디버깅 기술: 186. C# - CacheDependency의 숨겨진 예외 - System.Web.HttpException
13238정성태1/31/20235972.NET Framework: 2092. IIS 웹 사이트를 TLS 1.2 또는 TLS 1.3 프로토콜로만 운영하는 방법
13237정성태1/30/20235648.NET Framework: 2091. C# - 웹 사이트가 어떤 버전의 TLS/SSL을 지원하는지 확인하는 방법
13236정성태1/29/20235171개발 환경 구성: 663. openssl을 이용해 인트라넷 IIS 사이트의 SSL 인증서 생성
13235정성태1/29/20234774개발 환경 구성: 662. openssl - 윈도우 환경의 명령행에서 SAN 적용하는 방법
13234정성태1/28/20235884개발 환경 구성: 661. dnSpy를 이용해 소스 코드가 없는 .NET 어셈블리의 코드를 변경하는 방법 [1]
13233정성태1/28/20237272오류 유형: 840. C# - WebClient로 https 호출 시 "The request was aborted: Could not create SSL/TLS secure channel" 예외 발생
13232정성태1/27/20234947스크립트: 43. uwsgi의 --processes와 --threads 옵션
13231정성태1/27/20233958오류 유형: 839. python - TypeError: '...' object is not callable
13230정성태1/26/20234329개발 환경 구성: 660. WSL 2 내부로부터 호스트 측의 네트워크로 UDP 데이터가 1개의 패킷으로만 제한되는 문제
13229정성태1/25/20235338.NET Framework: 2090. C# - UDP Datagram의 최대 크기
13228정성태1/24/20235458.NET Framework: 2089. C# - WMI 논리 디스크가 속한 물리 디스크의 정보를 얻는 방법 [2]파일 다운로드1
13227정성태1/23/20235131개발 환경 구성: 659. Windows - IP MTU 값을 바꿀 수 있을까요? [1]
13226정성태1/23/20234810.NET Framework: 2088. .NET 5부터 지원하는 GetRawSocketOption 사용 시 주의할 점
13225정성태1/21/20234012개발 환경 구성: 658. Windows에서 실행 중인 소켓 서버를 다른 PC 또는 WSL에서 접속할 수 없는 경우
13224정성태1/21/20234444Windows: 221. Windows - Private/Public/Domain이 아닌 네트워크 어댑터 단위로 방화벽을 on/off하는 방법
13223정성태1/20/20234609오류 유형: 838. RDP 연결 오류 - The two computers couldn't connect in the amount of time allotted
13222정성태1/20/20234303개발 환경 구성: 657. WSL - DockerDesktop.vhdx 파일 위치를 옮기는 방법
13221정성태1/19/20234470Linux: 57. C# - 리눅스 프로세스 메모리 정보파일 다운로드1
13220정성태1/19/20234574오류 유형: 837. NETSDK1045 The current .NET SDK does not support targeting .NET ...
13219정성태1/18/20234190Windows: 220. 네트워크의 인터넷 접속 가능 여부에 대한 판단 기준
13218정성태1/17/20234087VS.NET IDE: 178. Visual Studio 17.5 (Preview 2) - 포트 터널링을 이용한 웹 응용 프로그램의 외부 접근 허용
13217정성태1/13/20234704디버깅 기술: 185. windbg - 64비트 운영체제에서 작업 관리자로 뜬 32비트 프로세스의 덤프를 sos로 디버깅하는 방법
13216정성태1/12/20234958디버깅 기술: 184. windbg - 32비트 프로세스의 메모리 덤프인 경우 !peb 명령어로 나타나지 않는 환경 변수
... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...