ImageProcessing 기술을 이용하여 적혈구 숫자를 세보자
2018, Jun 28
적혈구 사진을 준비하고 image에 나오는 적혈구 숫자를 세어보는 놀이를 해보자.
적혈구 사진
작업 순서는 다음과 같다.
- 이진화를 통해서 적혈구 만 남긴다.
- 이진화 이미지를 탐색한다. 2-1. 적혈구에 도달했을 때 region growing 으로 적혈구를 탐색한다. 2-2. 최소 좌표값과 최대 좌표값을 체크한다.(크기를 측정하기 위해서) 2-3. 적혈구 사이즈 인 것만 카운팅한다. 2-4. 적혈구 가운데에 하얀색 점을 찍는다.
먼저 이미 만들어놓은 이진화 함수와 이진이미지 침식 함수를 이용하여 적혈구만을 남겨보자. 헤더 파일에 선언부는 생략하겠다.
- 이진화를 통해서 적혈구만 남긴다.
ImageProcessingDoc.cpp
void CImageProcessingDoc::OnPracticeCountredbloodcell()
{
// TODO: 여기에 명령 처리기 코드를 추가합니다.
// 245로 이진화를 한다.
ImageProc::Binarization(m_Images[cur_index].image_gray,
m_Images[cur_index].width, m_Images[cur_index].height,245);
// window 크기 3으로 침식처리한다.
ImageProc::BinaryErosion(m_Images[cur_index].image_gray,
m_Images[cur_index].width, m_Images[cur_index].height, 3);
CImageProcessingView* pView = (CImageProcessingView*)((CMainFrame*)(AfxGetApp()->m_pMainWnd))->GetActiveView();
pView->SetDrawImage(m_Images[cur_index].image_color, m_Images[cur_index].image_gray,
m_Images[cur_index].width, m_Images[cur_index].height, 1);
pView->OnInitialUpdate();
}
실행하면 다음과 같은 이미지가 만들어진다.
이진화 이미지를 탐색해서 적혈구의 숫자를 세보자.
ImageProc.cpp
void ImageProc::CountRedBloodCell(unsigned char* image_input,
const int width, const int height)
{
// 좌표값을 저장할 수 있는 구조체를 정의한다.
struct POINT
{
int x;
int y;
POINT(void)
{
x = -1;
y = -1;
}
POINT(int _x, int _y)
{
x = _x;
y = _y;
}
};
// 적혈구 숫자 카운트하는 변수
int cnt = 0;
// 적혈구를 찾을 수 없을 때까지 반복한다.
while (1)
{
// 0 인 픽셀에 도달하면
// 해당 좌표를 시작지점으로
// region growing 을 시작한다.
// seed 선언
POINT seed = POINT();
for (int j = 0; j < height; j++)
{
for (int i = 0; i < width; i++)
{
// 픽셀 값이 0 이면
// seed에 좌표 저장
if (image_input[j*width + i] == 0)
{
seed = POINT(i, j);
break;
}
}
// seed 값을 찾으면 seed 찾는 반복문 종료
// 못찾으면 끝까지 탐색
if (seed.x != -1 && seed.y != -1) break;
}
// 끝까지 탐색해도 못찾으면
// 이진 이미지 탐색 종료
if (seed.x == -1 && seed.y == -1) break;
// seed 를 바탕으로 region growing 을 시작함
// region growing 이란 그래프 탐색에서 너비우선 탐색을 의미함.
// 탐색하면서 좌표의 최소값과 최대값을 저장한다.
// 이미 탐색한 적혈구는 gray 색으로 체크한다.
vector<POINT> queue;
queue.push_back(seed);
// 좌표의 최대값과 최소값을 저장할 변수
POINT minPoint = POINT(width-1, height-1);
POINT maxPoint = POINT(0, 0);
// queue 가 완전히 비워질때까지 탐색한다.
while (!queue.empty())
{
POINT cur_seed = POINT(queue[0].x, queue[0].y);
// 주위 1pixel 을 보며 queue에 넣고 체크한다.
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
if((cur_seed.y + y)>= height || (cur_seed.y + y) <0
|| (cur_seed.x + x) >= width || (cur_seed.x + x) < 0) continue;
if (image_input[width*(cur_seed.y + y)
+ (cur_seed.x + x)] == 0)
{
// 방문체크
image_input[width*(cur_seed.y + y)
+ (cur_seed.x + x)] = 127;
// 최소, 최대값 체크
minPoint.x = __min(minPoint.x, cur_seed.x + x);
minPoint.y = __min(minPoint.y, cur_seed.y + y);
maxPoint.x = __max(maxPoint.x, cur_seed.x + x);
maxPoint.y = __max(maxPoint.y, cur_seed.y + y);
// queue push back
queue.push_back(POINT(cur_seed.x + x, cur_seed.y + y));
}
}
}
// queue의 가장 앞부분을 pop 한다.
queue.erase(queue.begin());
}
// 최대 최소값을 통해 적혈구 여부를 파악하고 카운팅한다.
// 적혈구면 가운데 하얀점을 찍는다.
float cell_width = maxPoint.x - minPoint.x;
float cell_height = maxPoint.y - minPoint.y;
if ( cell_height == 0
|| cell_width / cell_height > 2
|| cell_width / cell_height < 0.5 ) continue;
// 적혈구 숫자 카운팅
cnt++;
// 가운데 점 찍기
POINT center = POINT((maxPoint.x + minPoint.x) / 2,
(maxPoint.y + minPoint.y) / 2);
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
image_input[(center.y+y)*width + center.x+x] = 255;
}
}
}
printf("red blood cell cnt : %d\n", cnt);
}
이벤트 처리기를 수정해보자
ImageProcessingDoc.cpp
void CImageProcessingDoc::OnPracticeCountredbloodcell()
{
// TODO: 여기에 명령 처리기 코드를 추가합니다.
// 침식할 경우 적혈구 사이의 거리가 너무 가까워 침식 적용 하지 않았다.
// 이진화 처리에 threshold 값을 약간 조절 했다.
ImageProc::Binarization(m_Images[cur_index].image_gray,
m_Images[cur_index].width, m_Images[cur_index].height,185);
/*ImageProc::BinaryErosion(m_Images[cur_index].image_gray,
m_Images[cur_index].width, m_Images[cur_index].height, 3);*/
ImageProc::CountRedBloodCell(m_Images[cur_index].image_gray,
m_Images[cur_index].width, m_Images[cur_index].height);
CImageProcessingView* pView = (CImageProcessingView*)((CMainFrame*)(AfxGetApp()->m_pMainWnd))->GetActiveView();
pView->SetDrawImage(m_Images[cur_index].image_color, m_Images[cur_index].image_gray,
m_Images[cur_index].width, m_Images[cur_index].height, 1);
pView->OnInitialUpdate();
}
두 적혈구의 거리가 너무 가깝거나 붙어있는 경우 두 적혈구 사이에 하얀점이 찍히는 문제가 있었다. 아래는 위 코드를 실행한 결과이다.
갯수는 red blood cell cnt : 252 라고 나왔다.