본문 바로가기
대한상공회의소 스마트팩토리 교육/C# 프로그래밍

[시각화 프로그래밍] Gray Scale Image Processing Using C# «수업-3» : 이미지 영상처리 알고리즘 (히스토그램 처리)

by 나는영하 2022. 4. 8.

※ 주의사항 

본 블로그는 수업 내용을 바탕으로 제가 이해한 부분을 정리한 블로그입니다.
본 내용을 참고로만 보시고, 틀린 부분이 있다면 지적 부탁드립니다!

감사합니다😁

 

안녕하세요!!

오늘은 아래와 같은 내용을 확인해보겠습니다.

 

이미지 영상처리 알고리즘

히스토그램 처리

흑백처리(평균값)

스트래칭

엔드 - 인 탐색

평활화

[WinForm] statusStrip

[WinForm] ContextMenuStrip

[WinForm] 마우스 우클릭 이벤트


▣ 히스토그램 처리 시연 동영상 (C++)

 

▣ 히스토그램 처리 시연 동영상 (C#)


 

지난 시간에는 Gray Scale 이미지 파일을 열고 화소 점 처리(반전, 밝게, 어둡게, 감마 보정 등등...)와 기하학 처리(축소, 확대, 회전 등등)후 Display하는 과정을 수행하였습니다. 이번에는 영상처리의 기법중 히스토그램 처리 기법을 알아보고 프로그램의 퀄리티 향상과 사용자의 사용편의를 위해 추가한 기능들에 대해 알아보겠습니다.

 

                               화소 점 처리와 기하학 처리는 지난 내용 참고      ↓                        

2022.04.06 - [C# 프로그래밍] - [시각화 프로그래밍] Gray Scale Image Processing Using C# «수업-2» : 이미지 영상처리 알고리즘 (화소 점 처리 / 기하학 처리)

 

[시각화 프로그래밍] Gray Scale Image Processing Using C# «수업-2» : 이미지 영상처리 알고리즘 (화소 점

※ 주의사항 ※ 본 블로그는 수업 내용을 바탕으로 제가 이해한 부분을 정리한 블로그입니다. 본 내용을 참고로만 보시고, 틀린 부분이 있다면 지적 부탁드립니다! 감사합니다😁 안녕하세요!!

920416.tistory.com

1. 이미지 영상처리 알고리즘 - ③ : (히스토그램 처리)

※ 히스토그램 : 도수분포표의 각 계급의 양 끝 값을 가로축에 표시하고 그 계급의 도수를 세로축에 표시하여 직사각형 모양으로 나타낸 그래프를 히스토그램 이라고 한다.

→ 즉, 이미지의 색상(Gray Scale에선 명암 분포)의 분포를 확인하고 전체적으로 균일하게 만들어 주는 효과

전체 통계를 분석한 후에 화소점 처리 

1-1. 흑백처리(평균값)

- 히스토그램 처리에서의 흑백처리와 화소 점 처리에서의 흑백처리....   무엇이 다를까???

화소 점 처리에서의 흑백처리는 중간값(127)을 기준으로 흑과 백으로 나누었고, 히스토그램에서의 흑백처리는 평균값을 기준으로 흑과 백으로 나누었다. 따라서, 평균값으로 적용한 흑백처리 기법은 이미지의 전체 화소값을 분석하고 평균을 내어서 화소 점 처리를 하였기 때문에 히스토그램 처리에 속하게 된다.

 

- 코드 자체는 어렵지 않으므로 자세한 설명은 생략. 

- inImage의 전체 픽셀의 화소값을 더하고(sum), 평균을 내어서(avr), 평균을 기준으로 흑과 백으로 출력

	    int avr = 0; int sum = 0;
            for (int i = 0; i < inH; i++)
            {
                for (int k = 0; k < inW; k++)
                {
                    sum += inImage[i, k];
                }
            }
            avr = sum / (inW * inH);

            outH = inH; outW = inW;
            outImage = new byte[outH, outW];

            for (int i = 0; i < outH; i++)
            {
                for (int k = 0; k < outW; k++)
                {
                    if (inImage[i, k] < avr) outImage[i, k] = 0;
                    else outImage[i, k] = 255;
                }
            }

1-2. 스트래칭

사진에 따라 스트래칭의 효과가 미비한 경우도 있다..

- 스트래칭 기법은 명암 대비를 향상시키는 연산으로, 낮은 명암 대비를 보이는 영상의 화질을 향상시키는 방법

- 즉, 히스토그램이 모든 범위의 화소 값을 포함하도록 히스토그램의 분포를 넓힌다.

- 위의 그림과 같이 원본 이미지의 값의 LOW값과 HIGH값이 MIN(0)과 MAX(255)하고 차이난다면 각각 끝까지 당겨서(스트래칭) 명암의 분포가 골고루 되도록 해준다. 

- 하지만, 애초에 원본 이미지의 명암이 골고루 분포 되어있다면 스트래칭의 효과가 미비하다. (위의 인물 그림 처럼...)

 스트래칭 수행 공식

✔ outImage = (inImage - Low) / (High - Low) * 255

- Low : 히스토그램의 최저 명도 값

- High : 히스토그램의 최고 명도 값

 

- inImage의 화소 값을 비교해서 가장 작은 값 Low와 가장 큰 값 High를 구해 공식에 대입하면 된다.

	    // out = (in - low) / (high - low) * 255 --> psudo code
            // 그룹내의 값을 초기값으로 해줘야 버그를 잡아줄 수 있다.
            int low = inImage[0, 0], high = inImage[0, 0];
            for (int i = 0; i < inH; i++)
            {
                for (int k = 0; k < inW; k++)
                {
                    if (inImage[i, k] < low) low = inImage[i, k];
                    if (inImage[i, k] > high) high = inImage[i, k];
                }
            }
            for (int i = 0; i < inH; i++)
            {
                for (int k = 0; k < inW; k++)
                {
                    // int와 int의 연산의 결과는 int형이 되기 때문에 double로 형변환
                    double outValue = ((inImage[i, k] - (double)low) / (high - (double)low) * 255.0);
                    if (outValue < 0.0) outValue = 0.0;
                    else if (outValue > 255.0) outValue = 255.0;

                    outImage[i, k] = (byte)outValue;
                }
            }

1-3. 앤드-인 탐색(End-In Search)

- 일정한 양의 화소를 흰색이나 검정색으로 인위적으로 지정해서 히스토그램의 분포를 좀 더 균일하게 만듦

- 본 알고리즘은 사진의 화소 값을 인위적으로 잘라내는 것이기 때문에 썩 좋은 알고리즘이라고 볼 순 없다.

※ 앤드-인 탐색 공식

 

- 스트래칭 코드와 비교했을때 딱 한줄만 추가되었다.

- low += 50; high -= 50 → 즉, 인위적으로 high값과 low값을 변형시켜서 명암의 분포를 좀 더 균일하게 만듦

	    // out = (in - low) / (high - low) * 255 --> psudo code
            // 그룹내의 값을 초기값으로 해줘야 버그를 잡아줄 수 있다.
            int low = inImage[0, 0], high = inImage[0, 0];
            for (int i = 0; i < inH; i++)
            {
                for (int k = 0; k < inW; k++)
                {
                    if (inImage[i, k] < low) low = inImage[i, k];
                    if (inImage[i, k] > high) high = inImage[i, k];
                }
            }
            low += 50; high -= 50; // 스트래칭 알고리즘 대비 추가된 Line
            for (int i = 0; i < inH; i++)
            {
                for (int k = 0; k < inW; k++)
                {
                    // int와 int의 연산의 결과는 int형이 되기 때문에 double로 형변환
                    double outValue = ((inImage[i, k] - (double)low) / (high - (double)low) * 255.0);
                    if (outValue < 0.0) outValue = 0.0;
                    else if (outValue > 255.0) outValue = 255.0;

                    outImage[i, k] = (byte)outValue;
                }
            }
            displayImage();

1-4. 평활화

- 어둡게 촬영된 영상의 히스토그램을 조절하여 명암 분포가 빈약한 영상을 균일하게 만들어 준다.

- 즉, 명암이 뭉친 부분(많이 모여있는 부분)을 펴주는 효과

 

(1) 평활화 기법 1단계 : 히스토그램 생성

- 이미지의 픽셀값(화소값)을 전체를 분석해서 히스토그램을 작성한다.

1단계 : 히스토그램 생성 코드

 

(2) 평활화 기법 2단계 : 누적 히스토그램 생성(누적 합 계산)

 

2단계 : 누적 히스토그램 생성 코드

 

(3) 평활화 기법 3단계 : 정규화 된 누적 히스토그램 생성

※ 누적 합 정규화 공식

3단계 : 정규화 된 누적 히스토그램 생성 코드

 

(4) 평활화 전체 코드

	    outH = inH; outW = inW;
            outImage = new byte[outH, outW];
            // ** 영상처리 알고리즘 **
            // 1단계 : 히스토그램 생성
            int[] hist = new int[256];
            for (int i = 0; i < 256; i++)
                hist[i] = 0; // 0으로 초기화
            for (int i = 0; i < inH; i++)
                for (int k = 0; k < inW; k++)
                    hist[inImage[i, k]]++;
            // 2단계 : 누적 히스토그램 작성
            int[] sumHist = new int[256];
            int sValue = 0;
            for (int i = 0; i < 256; i++)
            {
                sValue += hist[i];
                sumHist[i] = sValue;
            }
            // 3단계 : 정규화된 누적 히스토그램 작성
            // n = (sumHist / (행 * 열)) * 255.0 
            double[] normalHist = new double[256];
            for (int i = 0; i < 256; i++)
            {
                normalHist[i] = (sumHist[i] / (double)(inH * inW)) * 255.0;
            }
            for (int i = 0; i < inH; i++)
            {
                for (int k = 0; k < inW; k++)
                {
                    outImage[i, k] = (byte)normalHist[inImage[i, k]];
                }
            }

2. 이미지 영상처리 프로그램 기능 추가

2-1. 이미지(파일명, 해상도) 상태창(StatusStrip) 추가

하단에 이미지 파일명과 원본 해상도, 출력 해상도가 나타난다.

Windows Form의 StatusStrip을 사용해서 MainForm 하단에 이미지의 정보를 나타내는 창을 만들어 보았다.

해당 정보는 이미지를 Open했을때와 이미지를 가공 후 Display했을때만 나와야 하기 때문에 displayInImage()와 displayImage() 함수에 넣었다.

 

도구 상자의 Menu & Toolbars에서 StatusStrip을 드래그하면 하단에 위와 같은 창이 한줄 생성되고 좌측의 아래 화살표를 누르면 여러 기능을 가진 StatusStrip 세부 기능들이 나온다. 나는 단순히 Text만 표출할거기 때문에 StatusLabel을 사용했지만 DropDownButton을 사용하면 우리가 흔히 사용하는 윈도우창 하단의 작업표시줄 처럼 만들수도 있고, ProgressBar를 사용하면 작업이 진행되는 정도를 확인할 수도 있을 것이다. 

 

어쨋든 나는 총 3개의 StatusLabel을 생성했다.

각각 사용처는 [toolStripStatusLabel1 = 파일명] / [toolStripStatusLabel2 = 원본 해상도] / [toolStripStatusLabel3 = 출력 해상도] 를 나타내고 있다. 

 

// displayInImage() 함수의 하단에 추가
// Image를 Open하게되면 원본 해상도만 필요하기 때문에 Label3는 공란으로 둔다.
toolStripStatusLabel1.Text = Path.GetFileName(fileName);
toolStripStatusLabel2.Text = "원본해상도 : " + inH.ToString() + 'x' + inW.ToString();
toolStripStatusLabel3.Text = "";

// displayImage() 함수의 하단에 추가
// displayImage() 함수는 이미지가 가공되고 나서만 사용되기 때문에 
// 출력 해상도만을 Label3에 표시한다.
toolStripStatusLabel3.Text = "출력해상도 : " + outH.ToString() + 'x' + outW.ToString();

 

2-2. 마우스 우클릭으로 파일 열기(ContextMenuStrip) : 마우스 우클릭 이벤트 발생

이거 하나할려고 2시간을 넘게... 고생했다..

MainForm에서 마우스를 우클릭해서 파일을 여는 창을 띄울 수 있도록 만들어 보았다.

제일먼저 Windows Form에서 ContextMenuStrip을 드래그를 하여 메뉴창을 만들었다.

 

그리고 여기서 이벤트라는 것을 만들어야 하는데 마치 마이크로컨트롤러의 인터럽트(?) 같은 느낌이 들었다. 엄연히 개념적으로는 다르지만.. 

 

(1) 마우스 포인터가 컨트롤 위에 있을 때 마우스 단추를 눌렀다 놓으면 이벤트가 발생하도록 MouseUp 이벤트를 생성한다.

 	public MainForm()
        {
            InitializeComponent();            
            this.MouseUp += mouseRightClick;
        }

 

(2) 마우스의 이벤트(MouseEventArgs) 속성은 Button, Click, Location, X, Y 등이 있는데, 나는 누른 마우스 단추를 나타내는 값을 가져오기 위해 Button을 사용하였다. 특히, 마우스 오른쪽 버튼을 누르면 동작하도록 하였다.

	private void mouseRightClick(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Right)
            {
                // -------------- ContextMenuStrip 창이 열리도록 설정 예정------------
            }
        }

 

(3) 마우스 오른쪽 버튼을 누르면 ContextMenuStrip이 생성되고, 메뉴창에는 "열기"와 "닫기" 버튼이 생성된다. 메뉴창의 위치는 마우스의 커서 포지션 값에 해당되며, 열기 또는 닫기 버튼을 클릭하면 ToolStripItemClickedEventHandler 가 실행된다.

	    if (e.Button == MouseButtons.Right)
            {
                ContextMenuStrip contextMenuStrip1 = new ContextMenuStrip();
                contextMenuStrip1.Items.Add(열기ToolStripMenuItem1);
                contextMenuStrip1.Items.Add(닫기ToolStripMenuItem);
                contextMenuStrip1.Show(MousePosition.X, MousePosition.Y);
                contextMenuStrip1.ItemClicked += new ToolStripItemClickedEventHandler(menu_Clicked);
            }

(4) ContextMenuStrip에서 Item(열기 or 닫기)을 클릭하면 openImage() 함수가 수행되거나 MainForm 창이 닫히도록 하였다.

	private void menu_Clicked(object sender, ToolStripItemClickedEventArgs e)
        {
            switch (e.ClickedItem.Text)
            {
                case "열기": contextMenuStrip1.Visible = true; openImage(); break;
                case "닫기": Close(); break;
            }
        }

 

※ 아직 이벤트, 매소드, 클래스 등 용어가 많이 생소하고 개념이 잡혀있지 않아서 다소 어려운 내용이였습니다. 그리고 열기를 누르면 ContextMenuStrip1창이 사라져야하는데 (contextMenuStrip.Visible = false; 를 사용하면 사라질줄 알았는데 아니였습니다..ㅠ) 사라지지 않아서 일단 위와 같이 두었습니다. 저렇게 두면 아래의 사진처럼 왼쪽 위 구석에만 조그맣게 생겨서 임시방편으로 두었습니다.

왼쪽 위에 생기는 MenuStrip창... 사용에는 문제가 없다.

댓글