Microsoft MVP성태의 닷넷 이야기
Math: 2. "Zhang Suen 알고리즘(세선화, Thinning/Skeletonization)"의 C# 버전 [링크 복사], [링크+제목 복사],
조회: 27347
글쓴 사람
정성태 (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분
글쎄요. 그런 부분은 스스로 해결해야 할 문제 같습니다. ^^
정성태

1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13392정성태7/16/20233617오류 유형: 872. Oracle - ORA-01031: insufficient privileges
13391정성태7/14/20233633닷넷: 2132. C# - sealed 클래스의 메서드를 callback 호출했을 때 인라인 처리가 될까요?
13390정성태7/12/20233546스크립트: 53. 파이썬 - localhost 호출 시의 hang 현상
13389정성태7/5/20233615개발 환경 구성: 684. IIS Express로 호스팅하는 웹을 WSL 환경에서 접근하는 방법
13388정성태7/3/20233804오류 유형: 871. 윈도우 탐색기에서 열리지 않는 zip 파일 - The Compressed (zipped) Folder '[...].zip' is invalid. [1]파일 다운로드1
13387정성태6/28/20233869오류 유형: 870. _mysql - Commands out of sync; you can't run this command now
13386정성태6/27/20233989Linux: 61. docker - 원격 제어를 위한 TCP 바인딩 추가
13385정성태6/27/20234189Linux: 60. Linux - 외부에서의 접속을 허용하기 위한 TCP 포트 여는 방법
13384정성태6/26/20233903.NET Framework: 2131. C# - Source Generator로 해결하는 enum 박싱 문제파일 다운로드1
13383정성태6/26/20233634개발 환경 구성: 683. GPU 런타임을 사용하는 Colab 노트북 설정
13382정성태6/25/20233682.NET Framework: 2130. C# - Win32 API를 이용한 윈도우 계정 정보 (예: 마지막 로그온 시간)파일 다운로드1
13381정성태6/25/20234098오류 유형: 869. Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
13380정성태6/24/20233504스크립트: 52. 파이썬 3.x에서의 동적 함수 추가
13379정성태6/23/20233540스크립트: 51. 파이썬 2.x에서의 동적 함수 추가
13378정성태6/22/20233422오류 유형: 868. docker - build 시 "CANCELED ..." 뜨는 문제
13377정성태6/22/20237299오류 유형: 867. 파이썬 mysqlclient 2.2.x 설치 시 "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" 오류
13376정성태6/21/20233685.NET Framework: 2129. C# - Polly를 이용한 클라이언트 측의 요청 재시도파일 다운로드1
13375정성태6/20/20233363스크립트: 50. Transformers (신경망 언어모델 라이브러리) 강좌 - 2장 코드 실행 결과
13374정성태6/20/20233455오류 유형: 866. 파이썬 - <class 'AttributeError'> module 'flask.json' has no attribute 'JSONEncoder'
13373정성태6/19/20234745오류 유형: 865. 파이썬 - pymssql 설치 관련 오류 정리
13372정성태6/15/20233317개발 환경 구성: 682. SQL Server TLS 통신을 위해 사용되는 키 길이 확인 방법
13371정성태6/15/20233415개발 환경 구성: 681. openssl - 인증서 버전(V1 / V3)
13370정성태6/14/20233563개발 환경 구성: 680. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 - TLS 1.2 지원
13369정성태6/13/20233372개발 환경 구성: 679. PyCharm(을 비롯해 JetBrains에 속한 여타) IDE에서 내부 Window들의 탭이 없어진 경우
13368정성태6/13/20233532개발 환경 구성: 678. openssl로 생성한 인증서를 SQL Server의 암호화 인증서로 설정하는 방법
13367정성태6/10/20233690오류 유형: 864. openssl로 만든 pfx 인증서를 Windows Server 2016 이하에서 등록 시 "The password you entered is incorrect" 오류 발생
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...