[C++/MFC] 다양한 Threshold 기법 사용하기

Lachi_·2023년 12월 7일
0

mfc

목록 보기
12/16
post-thumbnail

8-bit Gray Scale의 .raw 파일을 불러와 흑과 백으로 이진화하는 Threshold를 진행한다.
이때 다양한 방법을 사용해 이진화를 진행하는 것이 이번 프로젝트의 목표이다.

요구사항: Method로 Normal, Auto, Adaptive 중에 선택 가능하게

Normal

이전에 진행했던 Threshold 기능이 그대로 구현되면 됨.

CString strValue;
		GetDlgItemText(IDC_EDIT_THRESHOLD, strValue);

		int nThreshold = _ttoi(strValue);
		if (nThreshold < 0 || nThreshold > 255)
		{
			AfxMessageBox(_T("0~255 사이의 값을 입력해주세요."));
			return;
		}

		if (!m_bImageOpened)
		{
			AfxMessageBox(_T("이미지를 열어주세요."));
			return;
		}

		for (int i = 0; i < IMAGE_SIZE; ++i)
		{
			m_result[i] = m_original[i] > nThreshold ? 255 : 0;
		}

Auto

Otsu 알고리즘 적용해서 출력하게 하면 될 듯. 자동으로 임계값을 찾아서 출력하게 해야함

Otsu's 알고리즘의 개요

Otsu's 알고리즘은 영상 이진화 방법 중에 하나이다. 영상의 이진화 방법은 여러가지 방법이 있는데, 그 중에서도 이 Otsu's 알고리즘은 Threshold값을 알고리즘을 통해 최적의 임계값을 구하여 이진화를 수행한다.

기존의 임계값을 통한 이진화는 0~255 사이의 픽셀값에 임의의 임계값을 설정해 임계값을 기준으로 흑백을 나누어 진행했다. 이러한 임계값은 사용자가 정해줘야하기 때문에 여러 시행착오가 필요하고, 최적의 임계값을 구하는데 시간이 걸리게 만드는 요인이 된다.

그래서 등장한 것이 Otsu's 알고리즘이다.

알고리즘의 설명

임계값을 t라고 가정한다면 t를 기준으로 픽셀값들을 두 집합으로 나누었을 때, 각 집합의 밝기값의 분포가 균등할수록 좋다는 점에 착안하여서 균등성이 제일 높도록 이진화하는 t에게 높은 점수를 준다. 이 균등성은 각 그룹의 분산으로 측정을 하고, 분산이 작을수록 균등성이 높다. 이 t를 0~255까지 움직이며 모든 경우의 점수를 계산하고 그 중 가장 높은 점수를 가진 t를 최종 임계값으로 취하는 알고리즘이다.

분산: 차이값의 제곱의 평균

이 알고리즘을 구하기 위해 필요로하는 변수가 가중치, 평균, 분산이고, 이 값들을 구하기 위해 사전에 영상에 대한 정규화된 히스토그램이 구해져있어야한다. 그래야 이 히스토그램을 통해서 가중치, 평균, 분산값을 구할 수가 있게 된다.

먼저, 정규화된 히스토그램안에서 임계값 0~t까지의 누적합, t+1부터 255까지 누적합을 구해서 두 개의 가중치를 구한다. 그리고 0~t까지와 t+1~255까지의 평균을 구하고 평균을 통해 분산을 구한다.

그래서 집합의 가중치과 분산을 곱해준 값과 집합의 가중치와 분산을 곱해준 값을 더해준다.

이렇게 0~255까지 t를 순회가 끝나면 그 중 가장 높은 값을 가지는 t를 최종 임계값으로 취해주면 된다.

int Otsu_Thres = 0;

    double inter_class_variance = 0;

    for (int O_thres = 0; O_thres < 256; O_thres++) {
        int alpha = 0, beta = 0;
        int sum1 = 0, sum2 = 0;
        double mu1 = 0, mu2 = 0;// , var1, var2;

        for (int i = 0; i < O_thres; i++)
            sum1 += calcBinary[i];

        sum2 = [dst.rows](dst.rows) * [dst.cols](dst.cols) - sum1;
        
        cout << Otsu_Thres << endl;
        cout << "sum1 : " << sum1 << endl;
        cout << "sum2 : " << sum2 << endl;

        alpha = (double)sum1 / (double)calcBinary.size();
        beta = (double)sum2 / (double)calcBinary.size();

        cout << "alpha : " << alpha << endl;
        cout << "beta : " << beta << endl;

        // Calc average

        for (int m = 0; m < O_thres; m++) {
            mu1 += (double)(m * calcBinary[m]) / (double)sum1;
        }

        for (int m = O_thres; m < 256; m++) {
            mu2 += (double)(m * calcBinary[m]) / (double)sum2;
        }

        //cout << "mu1 : " << mu1 << endl;
        //cout << "mu2 : " << mu2 << endl;

        /*// Calc variance

        for (int v = 0; v < O_thres; v++) {
            var1 += (double)(pow((v - mu1), 2) * calcBinary[v]) / (double)sum1;
        }

        for (int v = O_thres; v < 256; v++) {
            var2 += (double)(pow((v - mu2), 2) * calcBinary[v]) / (double)sum2;
        }*/

        double temp = alpha * beta * pow((mu1 - mu2), 2);

        if (inter_class_variance < temp) {
            inter_class_variance = temp;
            Otsu_Thres = O_thres;
        }

}

출처: [https://ottuging.tistory.com/2](https://ottuging.tistory.com/2) [오뚜깅:티스토리]

int histogram[256] = { 0, };  // 픽셀 값(0~255)
		for (int i = 0; i < IMAGE_SIZE; ++i)
		{
			histogram[m_original[i]]++;
		}

		// Otsu 알고리즘 -> 픽셀 값을 두 개의 클래스로 나눔. B: 임계값보다 작거나 같은 픽셀 값들의 집합, F: 임계값보다 큰 픽셀 값들의 집합
		int total = IMAGE_SIZE;  // 이미지의 전체 픽셀 개수
		float sum = 0;  // 가중치 합계를 저장할 변수
		for (int i = 0; i < 256; ++i)
			sum += i * histogram[i];  // 픽셀 값과 그 빈도수를 곱한 값을 sum에 더함

		float sumB = 0;  // 클래스 B의 가중치 합계
		int wB = 0;  // 클래스 B의 픽셀 수
		int wF = 0;  // 클래스 F의 픽셀 수

		float varMax = 0;  // 클래스 간 분산의 최대값
		int threshold = 0;

		for (int i = 0; i < 256; ++i)
		{
			wB += histogram[i];  // 해당 픽셀 값의 빈도수를 wB에 더함
			if (wB == 0)
				continue;

			wF = total - wB;  // 임계값보다 큰 픽셀을 구하기 위해 전체에서 wB만큼을 뺌
			if (wF == 0)
				break;

			sumB += (float)(i * histogram[i]);  // 픽셀 값과 그 빈도수를 곱한 값을 sumB에 더함

			float mB = sumB / wB;  // 클래스 B의 가중치 평균
			float mF = (sum - sumB) / wF;  // 클래스 F의 가중치 평균

			float varBetween = (float)wB * (float)wF * (mB - mF) * (mB - mF);  // 클래스 간 분산을 계산함
			// 분산: 차이값의 제곱의 평균

			if (varBetween > varMax)  // 클래스 간 분산이 varMax보다 크면
			{
				varMax = varBetween;  // varMax를 varBetween으로 업데이트
				threshold = i;  // 임계값을 현재의 픽셀 값으로 업데이트
			}
		}

		// 이진화
		for (int i = 0; i < IMAGE_SIZE; ++i)
		{
			m_result[i] = m_original[i] > threshold ? 255 : 0;
		}

Adaptive

적응적 임계값은 Window Size와 Offset이라는 두 변수를 기반으로 이진화를 진행한다.
Normal 임계값 적용 방법은 모든 픽셀에 대해 동일한 임계값을 적용하기에 영상이 확실히 구분된다면 뛰어난 성능을 보이지만 영상 속의 조명이 일정하지 않거나 다양한 색을 지닌 경우 하나의 임계값만으로 구분하기 어렵다.

따라서 영상을 일정 블록으로 분할하고 블록마다 다른 임계값을 적용하는 것이 적응적 임계값이다.

GetDlgItemText(IDC_EDIT_WINDOW_SIZE, strAdaptiveWindowSize);
		int adaptiveWindowSize = _ttoi(strAdaptiveWindowSize);


		GetDlgItemText(IDC_EDIT_OFFSET, strAdaptiveOffset);
		int adaptiveOffset = _ttoi(strAdaptiveOffset);

		if (!m_bImageOpened)
		{
			AfxMessageBox(_T("이미지를 열어주세요."));
			return;
		}

		int* adaptiveThreshold = new int[IMAGE_SIZE]; // 임계값을 저장할 동적 배열 선언과 동시에 메모리 할당
		int halfWindowSize = adaptiveWindowSize / 2;
		for (int y = 0; y < IMAGE_HEIGHT; ++y)
		{
			for (int x = 0; x < IMAGE_WIDTH; ++x)
			{
				int total = 0; // 주변 픽셀 값의 합을 저장할 변수
				int count = 0; // 주변 픽셀의 개수를 저장할 변수
				for (int dy = -halfWindowSize; dy <= halfWindowSize; ++dy)
				{
					for (int dx = -halfWindowSize; dx <= halfWindowSize; ++dx)
					{
						int nx = x + dx; // 창 내의 픽셀의 x좌표
						int ny = y + dy; // 창 내의 픽셀의 y좌표

						// (nx, ny)가 이미지 내에 있다면
						if (nx >= 0 && nx < IMAGE_WIDTH && ny >= 0 && ny < IMAGE_HEIGHT)
						{
							total += m_original[ny * IMAGE_WIDTH + nx];// 원본 이미지의 (nx, ny) 픽셀 값을 nTotal에 더함
							++count; // 주변 픽셀의 개수 증가
						}
					}
				}
				adaptiveThreshold[y * IMAGE_WIDTH + x] = total / count + adaptiveOffset; // 주변 픽셀 값의 평균에 오프셋을 더한 값을 임계값으로 설정		
			}
		}

		// 이진화
		for (int i = 0; i < IMAGE_SIZE; ++i)
		{
			m_result[i] = m_original[i] > adaptiveThreshold[i] ? 255 : 0;
		}

		delete[] adaptiveThreshold;
	}

추가 조건

각 Method 마다 입력해야하는 값들이 있고, 굳이 입력할 필요가 없는 값들이 있다. 입력할 필요가 없는 Edit Control 항목은 막아버려 입력하지 못하도록 하자.



EnableWindow()는 수정 가능하게 할지 말지를 결정하는 요소이다.
따라서 수정 가능하게 하려면 TRUE를 주면 된다.

switch (nMethod)
	{
	case 0: // Normal
		pWnd = GetDlgItem(IDC_EDIT_WINDOW_SIZE);
		if (pWnd) pWnd->EnableWindow(FALSE);
		pWnd = GetDlgItem(IDC_EDIT_OFFSET);
		if (pWnd) pWnd->EnableWindow(FALSE);
		pWnd = GetDlgItem(IDC_EDIT_THRESHOLD);
		if (pWnd) pWnd->EnableWindow(TRUE);
		break;
	case 1: // Auto
		pWnd = GetDlgItem(IDC_EDIT_WINDOW_SIZE);
		if (pWnd) pWnd->EnableWindow(FALSE);
		pWnd = GetDlgItem(IDC_EDIT_OFFSET);
		if (pWnd) pWnd->EnableWindow(FALSE);
		pWnd = GetDlgItem(IDC_EDIT_THRESHOLD);
		if (pWnd) pWnd->EnableWindow(FALSE);
		break;
	case 2: // Adaptive
		pWnd = GetDlgItem(IDC_EDIT_WINDOW_SIZE);
		if (pWnd) pWnd->EnableWindow(TRUE);
		pWnd = GetDlgItem(IDC_EDIT_OFFSET);
		if (pWnd) pWnd->EnableWindow(TRUE);
		pWnd = GetDlgItem(IDC_EDIT_THRESHOLD);
		if (pWnd) pWnd->EnableWindow(FALSE);
		break;
	}
profile
개인 저장용. 오류 매우 많음.

0개의 댓글