Radio Button 을 만들 때 같은 그룹에 속하는 Radio Button 중 가장 먼저 만들어진 라디오 버튼의 Group 속성을 True 로 설정해야 합니다. 이렇게 하게 되면 그 이후에 만들어진 라디오 버튼이 모두 자동으로 같은 그룹에 속하게 됩니다.
ID
자료형
변수 이름
컨트롤 변수
IDC_EDIT_NAME
CString
m_strName
체크 해제
IDC_EDIT_INFO
CString
m_strInfo
체크 해제
IDC_EDIT_INTEREST
CString
m_interest
체크 해제
IDC_RADIO_FEMALE
int
m_Sex
체크 해제
같은 그룹에 속하는 Radio Button 을 위한 변수를 만들 때는 정수 변수 하나면 됩니다.
Radio Button 은 같은 그룹에서 하나만 선택이 가능하기 때문에 선택되면 변수에는 선택된 버튼의 인덱스 번호가 저장됩니다.
Radio Button 가운데 Group 속성이 True 인 컨트롤에 대해서만 변수를 만들면 되는것입니다.
이제 TestDlg.cpp 파일로 넘어와서 DoDataExchange()함수에 Radio Button 과 Edit Control 변수를 추가해줍니다.
위의 IDC_EDIT_NAME 과 IDC_EDIT_INFO 는 전 게시물에서 추가하였고, 이번에 만든 두 컨트롤에 대해 새로운 함수를 추가하였습니다.
void CTestDlg::OnBnClickedButtonEnlist()
{
// TODO: 여기에 컨트롤 알림 처리기 코드를 추가합니다.
LPCTSTR gender[2] = { _T("여성"),_T("남성") };// TCHAR 문자열에 대한 포인터 상수 자료형
UpdateData();
if (m_strName.IsEmpty())
return;
m_strInfo.Append(m_strName);
m_strInfo.AppendChar(_T(':')); // 성별과 관심 분야 정보를 추가할 때 구분하기 위해서 : 문자를 추가함
m_strInfo.Append(gender[m_Sex]); // m_Sex에 저장된 값을 이용하여 성별 정보 문자열을 m_strInfo 에 추가한다.
m_strInfo.AppendChar(_T(':')); // 성별과 관심 분야 정보를 추가할 때 구분하기 위해서 : 문자를 추가함
m_strInfo.Append(m_interest); // 관심 분야 정보를 m_interest 에서 가져와서 추가함
m_strInfo.Append(_T("\r\n"));
m_Sex = 0; // 가입 버튼을 완료한후 선택 Radio Button 을 초기화함
m_strName.Empty();
m_interest.Empty();
UpdateData(false);
}
OnBnClickedButtonEnlist() 함수에도 마찬가지로 주석이 달린 부분을 추가해 줍니다.
안녕하세요. 본 게시물에서는 대화상자 기반 MFC 프로젝트를 생성하고 위의 사진과 같이 간단한 예제를 만들 것입니다.
본 설명은 Visual Studio 2015 기준으로 진행하고 있습니다.
우선 MFC 프로젝트를 위해서는 Visual Studio 에 MFC 템플릿을 설치해야 합니다.
비주얼 스튜디오 설치 당시에 C++ 체크를 진행하고 설치를 해야 이용할 수 있습니다.
Visual C++ -> MFC -> MFC 응용 프로그램을 체크합니다.
저는 대화 상자 기반으로 예제를 진행하기 때문에 위와 같이 체크를 한 후 다음을 눌러줍니다.
SDL 검사는 체크를 꼭 해제 해 줍니다. C언어나 C++ 을 진행해본 사람이라면 프로젝트 생성시 한번 씩 눌렀던 경험이 있을겁니다. 자세히는 모르지만 SDL 검사가 2010 버전부터 생겨서 체크를 진행하고 프로젝트를 실행하면 scanf 대신 scanf_s 를 써주거나 pragma를 이용하여 오류검사에서 배제시키는 귀찮은 일이 많이 생기기 때문에 해제한다고 알고 있습니다.
사용자 인터페이스 기능 창에서는 정보 상자 박스체크를 해제 합니다.
본 기능은 게시물에서 사용하지 않는 번거로운 코드가 생기므로 추후에 필요할 때 추가할 수 있으니 체크 해제를 하고 진행하도록 합니다.
고급 기능 역시 마찬가지로 모두 체크를 해제하고 다음을 눌러줍니다.
저도 여기에서 사용해본 기능은 MFC에서 소켓을 이용한 프로그램 공부시에 Windows 소켓 기능 빼고 진행해본 적이 없습니다..
이제 마침버튼을 누르면 컴퓨터 마다 상이할 수 있으나 로딩이 조금 걸립니다.
위의 사진과 같이 파일 구문 분석을 진행하면서 아래 하단 바에 로딩을 하고 있습니다.
이제 프로젝트명을 각자 다르게 적었기 때문에 파일 이름이 조금 다를 수 있지만 일단 프로젝트명을 Test라고 가정하고
Test.cpp , TestDlg.cpp , Test.h 파일이 있습니다. 여기서 Test.cpp 에 들어갑니다.
BOOL CTestApp::InitInstance()
{
CWinApp::InitInstance();
// 대화 상자에 셸 트리 뷰 또는
// 셸 목록 뷰 컨트롤이 포함되어 있는 경우 셸 관리자를 만듭니다.
CShellManager *pShellManager = new CShellManager;
// MFC 컨트롤의 테마를 사용하기 위해 "Windows 원형" 비주얼 관리자 활성화
CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows));
// 표준 초기화
// 이들 기능을 사용하지 않고 최종 실행 파일의 크기를 줄이려면
// 아래에서 필요 없는 특정 초기화
// 루틴을 제거해야 합니다.
// 해당 설정이 저장된 레지스트리 키를 변경하십시오.
// TODO: 이 문자열을 회사 또는 조직의 이름과 같은
// 적절한 내용으로 수정해야 합니다.
SetRegistryKey(_T("로컬 응용 프로그램 마법사에서 생성된 응용 프로그램"));
CTestDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: 여기에 [확인]을 클릭하여 대화 상자가 없어질 때 처리할
// 코드를 배치합니다.
}
else if (nResponse == IDCANCEL)
{
// TODO: 여기에 [취소]를 클릭하여 대화 상자가 없어질 때 처리할
// 코드를 배치합니다.
}
else if (nResponse == -1)
{
TRACE(traceAppMsg, 0, "경고: 대화 상자를 만들지 못했으므로 응용 프로그램이 예기치 않게 종료됩니다.\n");
TRACE(traceAppMsg, 0, "경고: 대화 상자에서 MFC 컨트롤을 사용하는 경우 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS를 수행할 수 없습니다.\n");
}
// 위에서 만든 셸 관리자를 삭제합니다.
if (pShellManager != NULL)
{
delete pShellManager;
}
#ifndef _AFXDLL
ControlBarCleanUp();
#endif
// 대화 상자가 닫혔으므로 응용 프로그램의 메시지 펌프를 시작하지 않고 응용 프로그램을 끝낼 수 있도록 FALSE를
// 반환합니다.
return FALSE;
}
보이는 코드와 같이 InitInstance 함수에 들어가서 실질적으로 필요한 코드만 남겨놓아야 합니다.
TestDlg.cpp 파일로 들어가서 DoDataExchange()함수를 위와같이 수정해줍니다.
pDX : CDataExchange 객체에 대한 포인터로 데이터 교환 방향과 대화 상자에 대한 포인터 등이 저장되어 있습니다.
위의 DDX_Text 함수는 IDC_EDIT_NAME과 m_strName 에 대한 데이터 교환을 위한 함수입니다.
이제 데이터 교환을 위한 준비는 모두 끝났으니,
버튼을 클릭했을 때 작성한 이름이 아래 컨트롤에 생성이되는 이벤트를 추가하여야 합니다.
따라서 아까 만들어놨던 이벤트 함수인 OnBnClickedButtonEnlist() 함수로 이동하여 코드를 수정해 줍니다.
void CTestDlg::OnBnClickedButtonEnlist()
{
// TODO: 여기에 컨트롤 알림 처리기 코드를 추가합니다.
UpdateData(); // 컨트롤에 입력된 데이터를 변수로 얻어온다.
if (m_strName.IsEmpty()) // 이름을 입력하지 않은 채 버튼을 클릭한 상황을 걸러내기 위함이다.
return;
m_strInfo.Append(m_strName); // m_strName 에 저장된 이름을 Append()를 이용하여 m_strInfo 에 추가한다.
m_strInfo.Append(_T("\r\n")); // 개행 문자를 추가하여 다음 이용자의 이름을 다음 행에 출력하기 위함이다.
m_strName.Empty(); // 이름이 추가되었으므로 원래 작성했던 이름을 삭제해야한다.
UpdateData(false); // false를 통해 m_strName 의 데이터를 컨트롤에 전달한다.
}
UpdateData() or UpdateData(true) 의 경우는 컨트롤 -> 변수로 값을 받아오며
UpdateData(false) 의 경우는 변수 -> 컨트롤 로 값을 전달합니다.
이제 모든 작업이 끝났으니 빌드를 하면 됩니다.
이름을 Edit Control에 작성하고 가입버튼을 누르면 아래의 Edit Control에 정상적으로 기입이 됩니다.
이번시간에는 MFC 대화상자 기반 프로젝트 기본 생성 방법과 Edit Control , Button 의 사용법에 대해 정리해 보았습니다.
주의할 코드부분은 관심영역을 자르고 나서 원본 사진의 해상도와 맞게 resize를 시켜주었습니다.
OpenCV에서는 resize 진행시 보간법(interpolation methods)을 지정해줄 수 있습니다. 대표적으로 INTER_CUBIC ,INTER_LINEAR 등이 있습니다만, INTER_CUBIC을 사용하면 보다 더 선명한 이미지를 얻을수 있다고 합니다. 그러나 저는 INTER_LINEAR 방식으로 진행하였습니다.
히스토그램 생성
관심영역이 추출된 이미지를 가지고 본격적으로 인식에 가장 필요한 히스토그램을 생성하는 파트입니다.
히스토그램이란?
히스토그램(Histogram)은 표로 되어 있는 도수 분포를 정보 그림으로 나타낸 것입니다.
그러나 이미지 히스토그램은 이야기가 조금 다릅니다.
이미지 히스토그램은 가로축(x축) 에는 이미지의 픽셀 값을 나타내는 좌표값이고, 세로축(y축)으로는 픽셀의 수를 나타내는 좌표값입니다. 히스토그램으로 이미지의 대비(Contrast)나 빛의 강도 등을 나타낼 수도 있기 때문에, 이미지에서 어떤 특징점이 어느정도 분포하고 있는지 알 수 있는 그래프라고 볼 수 있습니다.
저는 본 프로젝트를 진행하면서, 그레이스케일 변환한 숫자에 대해 히스토그램을이용하여 숫자 0~9 까지의 히스토그램을 모두 그려보았으나 뚜렷한 특징을 찾을 수가 없었습니다. 따라서 아래의 그림과 같이 진행하였습니다.
가로, 세로 축 좌표를 기준으로 숫자의 픽셀(검정색)의 개수를 카운팅하여, 빈 검정색 이미지에 line함수로 픽셀의 개수만큼 그려주었습니다. 축의 기준을 나눠서 히스토그램을 생성한 이유는 기존 히스토그램 사용시에, 밝은 픽셀과 어두운 픽셀의 분포를 표시하기 때문에 입력되는 이미지의 숫자의 크기에 따라 오인식 하는 확률이 크기 때문에 위와 같이 진행했었습니다.
x축 좌표에 대한 히스토그램 을 보면 가로축은 x가 0부터 image의 rows까지 , 세로축은 픽셀의 개수를 나타내었고
y축 좌표에 대한 히스토그램 역시 보면 가로축은 y가 0부터 image의 cols까지, 세로축은 픽셀의 개수를 나타내었습니다.
/************************** ROI 좌표 찾기 *****************************/
roi_image = binary_image(Rect(Point(x_min, y_min), Point(x_max, y_max)));
resize(roi_image, roi_image, Size(200, 200), 0, 0, INTER_LINEAR);
Mat x_hist_image = Mat::zeros(roi_image.rows, roi_image.cols, CV_8U);
Mat y_hist_image = Mat::zeros(roi_image.rows, roi_image.cols, CV_8U);
Mat total_hist_image = Mat::zeros(roi_image.cols, roi_image.rows * 2, CV_8U);
/************************** histogram ****************************/
for (int y = 0; y < roi_image.rows; y++) //y축 히스토그램
{
pixel_count = 0;
for (int x = 0; x < roi_image.cols; x++)
{
if (roi_image.at<uchar>(x, y) == 0)
{
pixel_count++;
}
}
y_count[y] = pixel_count;
}
for (int x = 0; x < roi_image.cols; x++) //x축 히스토그램
{
pixel_count = 0;
for (int y = 0; y < roi_image.rows; y++)
{
if (roi_image.at<uchar>(x, y) == 0)
{
pixel_count++;
}
}
x_count[x] = pixel_count;
}
for (int x = 0; x < roi_image.rows; x++) //x축 히스토그램 그리기
line(x_hist_image, Point(x, roi_image.rows), Point(x, roi_image.rows - x_count[x]), Scalar(255, 255, 255), 0);
for (int y = 0; y < roi_image.cols; y++) //y축 히스토그램 그리기
line(y_hist_image, Point(y, roi_image.cols), Point(y, roi_image.cols - y_count[y]), Scalar(255, 255, 255), 0);
/* 통합 히스토그램 그리기 */
for (int x= 0; x < roi_image.rows; x++)
line(total_hist_image , Point(x + 200, roi_image.rows), Point(x+200, roi_image.rows - x_count[x]), Scalar(255, 255, 255), 0);
for (int y = 0; y < roi_image.cols; y++)
line(total_hist_image, Point(y, roi_image.cols), Point(y, roi_image.cols - y_count[y]), Scalar(255, 255, 255), 0);
/************************** histogram ****************************/
imshow("x_hist", x_hist_image);
imshow("y_hist", y_hist_image);
imshow("original", original_image);
imshow("bin", binary_image);
imshow("roi_image", roi_image);
imshow("total", total_hist_image);
waitKey(0);
}
코드 중 가로 세로축 히스토그램을 그리는 코드의 일부입니다.
위의 코드 역시 이중 for문을 이용하여 이미지의 행 ,열을 훑어 연산하면서 픽셀의 카운트를 진행하였습니다.
테스트할 이미지의 데이터와 비교할 표준 숫자 데이터 0~9까지의 히스토그램을 모두 생성하여 저장하였습니다.
그림과 같이 각 숫자마다 모두 다른 히스토그램을 확인할 수 있습니다.
이제 표준 데이터의 히스토그램을 모두 저장하였으니,
테스트할 숫자 이미지 역시 지금까지 진행했던 방식으로 히스토그램을 생성합니다.
테스트할 숫자 데이터는 인터넷에서 자동차 번호판의 숫자를 찾아서 아래 그림과 같이 크기와 위치가 다르게 저장하여 진행하였습니다.
/* compare */
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
for (int y = 0; y < total_hist_image[i].cols; y++) //y축 히스토그램
{
for (int x = 0; x < total_hist_image[i].rows; x++)
{
if (total_hist_image[i].at<uchar>(x, y) != test_total_hist_image[j].at<uchar>(x, y))
test_check_sum[i]++;
}
}
if (check_sum[i] <= min)
min = test_check_sum[i];
}
if (test_check_sum[i] == min)
min = i;
detect_num[i] = min;
}
/* compare */
표준 히스토그램 데이터와의 픽셀차이를 계산하는 코드중 일부입니다.
본 코드가 관심영역 추출이 올바르게 되는지, 글자의 크기에 영향을 미치지 않는지 테스트를 진행해보았습니다.
테스트할 숫자 이미지가 입력이 되면 표준 숫자 데이터 0~9 를 각각 비교하면서 픽셀차이가 가장 적은 숫자를
인식하는 숫자로 판정하였습니다. 위의 사진과 같이 똑같은 글꼴이지만 위치와 숫자의 크기를 달리 하여도 표준 히스토그램의 특징과 뚜렷하게 다른점이 거의 없습니다.
테스트 숫자의 픽셀이 관심영역을 추출하면서 resize되어
글꼴이 깨져도 숫자의 본 모습은 크게 바뀌지 않아서 그런것 같습니다.
한계점..
ㄴ
본 프로젝트에서 진행한 숫자인식 방법은 가로 세로축을 기준으로 픽셀의 개수를 측정하였기 때문에 숫자가 조금만 회전이 되어도 오인식 하는 경우가 많았습니다..
회전이나 글꼴 차이에 해당하는 경우의 수는 고려하지 않고 반영하였기 때문에 본프로젝트는 추후에 더욱 좋은 구상을 통해 보완해야할 것 같습니다. 추후에 더욱 좋은 구상을 통해 보완하게 된다면 비교적 무거운 딥러닝을 사용하지 않고도 자동차 번호판을 인식할 수 있는 프로그램을 만들 수 있기 때문에 제한된 하드웨어 성능내에서 효율적인 시스템을 만들수 있을 것 같습니다.
본 프로젝트는 자동차 번호판 숫자 인식을 위한 프로젝트의 일부로 진행하였습니다.
막상 딥러닝을 이용할때는 몰랐는데 딥러닝의 필요성을 알게되는 프로젝트 중 하나였습니다. 영상처리에 대한 공부를 진행하면서 숫자 인식을 위해 진행했던 부분들이 흥미로운점이 많았던 것 같습니다.
OpenCV를 이용하여 비주얼 스튜디오 2015 에서 지정된 이미지를 축소 하고 회전시킬 수 있습니다.
먼저 코드를 실행시킨 모습입니다.
원본 사진의 크기와 축소시킨 사진의 크기를 비교했습니다.
원본 사진의 해상도는 1280 X 1920이며 1/4 크기, 320 X 480로 축소시켰습니다.
Mat image; // 빈 영상 생성
image = imread("woman.JPG", IMREAD_COLOR); //이미지 읽기
if (image.empty()) //이미지를 제대로 읽을 수 없으면 에러 메시지 출력
{
printf("Loading Error!");
return 0;
}
/*콘솔창에 띄워주기*/
printf("Original image is %d x %d\n", image.rows, image.cols);
namedWindow("Original image"); // 원본 이미지를 전시할 창 만들기
imshow("Original image", image); // 원본 이미지 보여주기
/* 축소 이미지띄우기 */
Mat resized_image; //축소된 이미지 저장
resize(image, resized_image, Size(), 0.25, 0.25, INTER_AREA); //원본 이미지 크기 조정하기. 가로 세로 모두 1/4로 축소.
printf("Resized image is %d x %d\n", resized_image.rows, resized_image.cols); // 축소된 이미지 사이즈 출력
namedWindow("Resized image");
imshow("Resized image", resized_image); // 축소된 이미지 보여주기
이미지를 imread 함수로 불러온 후 resize 함수를 이용하여 0.25(1/4)로 축소하도록 선언했습니다.
상세 기능은 주석으로 달았습니다.
이번에 쓴 기능은 원본사진의 크기만큼 빈 이미지파일을 생성시킨후에 축소된 사진을 복사하여 넣었습니다.
/* 축소 이미지->원본사이즈에 대입 */
Mat cover_image(image.rows, image.cols, CV_8UC3); // 원본사이즈만큼의 빈 이미지 생성
resized_image.copyTo(cover_image(Rect(100, 100, resized_image.cols, resized_image.rows))); //축소한 이미지를 원본사이즈크기에 결합
imshow("covered_image", cover_image);
cover_image라 이름을 짓고, copyTo를 이용하여 복사하여 넣습니다.
마지막으로 사진을 원하는 각도만큼 회전시킬 수 있습니다.
/*회전 이미지 띄우기 */
Mat rotation; // 회전할 이미지
Point2f pt(resized_image.cols / 2., resized_image.rows / 2.); //Point2f 로 회전시킬 이미지의 중심 설정 (정중앙)
Mat r = getRotationMatrix2D(pt, 45, 0.7); // 사진 회전시키기 (사진 중앙값, 회전값, 사진 비율)
warpAffine(resized_image, rotation, r, Size(resized_image.cols, resized_image.rows)); //설정해준 값을 회전시켜줌
imshow("rotate_image", rotation);
Point2f로 이미지를 회전시킬 때 중심을 설정해줍니다.
축소한 사진의 가로,세로에 2로 나누면 정중앙을 기준으로 회전합니다.
getRotationMatrix2D()함수를 이용해 각도, 사진의 크기 비율을 조절하여 회전시킬 준비를합니다.
warpAffine()함수는 설정해준 값을 기준으로 회전 시켜줍니다.
아래는 전체 소스입니다.
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
int main()
{
Mat image; // 빈 영상 생성
image = imread("woman.JPG", IMREAD_COLOR); //이미지 읽기
if (image.empty()) //이미지를 제대로 읽을 수 없으면 에러 메시지 출력
{
printf("Loading Error!");
return 0;
}
/*콘솔창에 띄워주기*/
printf("Original image is %d x %d\n", image.rows, image.cols);
namedWindow("Original image"); // 원본 이미지를 전시할 창 만들기
imshow("Original image", image); // 원본 이미지 보여주기
/* 축소 이미지띄우기 */
Mat resized_image; //축소된 이미지 저장
resize(image, resized_image, Size(), 0.25, 0.25, INTER_AREA); //원본 이미지 크기 조정하기. 가로 세로 모두 1/4로 축소.
printf("Resized image is %d x %d\n", resized_image.rows, resized_image.cols); // 축소된 이미지 사이즈 출력
namedWindow("Resized image");
imshow("Resized image", resized_image); // 축소된 이미지 보여주기
/* 축소 이미지->원본사이즈에 대입 */
Mat cover_image(image.rows, image.cols, CV_8UC3); // 원본사이즈만큼의 빈 이미지 생성
resized_image.copyTo(cover_image(Rect(100, 100, resized_image.cols, resized_image.rows))); //축소한 이미지를 원본사이즈크기에 결합
imshow("covered_image", cover_image);
/*회전 이미지 띄우기 */
Mat rotation; // 회전할 이미지
Point2f pt(resized_image.cols / 2., resized_image.rows / 2.); //Point2f 로 회전시킬 이미지의 중심 설정 (정중앙)
Mat r = getRotationMatrix2D(pt, 45, 0.7); // 사진 회전시키기 (사진 중앙값, 회전값, 사진 비율)
warpAffine(resized_image, rotation, r, Size(resized_image.cols, resized_image.rows)); //설정해준 값을 회전시켜줌
imshow("rotate_image", rotation);
waitKey(0);
return 0;
}