본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 16. 12:04

C++로 번역 엔진 구축하기 — 파트 2: 그레이스케일 이미지 전처리 및 국부 대비 에지 검출

요약

본 기사는 C++를 사용하여 LPR(번호판 인식) 파이프라인의 전처리 단계를 구축하는 방법을 다룹니다. 주요 목표는 번호판 문자가 포함될 가능성이 높은 영역을 강조하기 위해 적분 이미지 생성, 국부 대비 분석, 에지 맵 추출 등의 기술을 구현하는 것입니다. 특히, 적분 이미지를 활용하여 직사각형 영역 합 계산 시간을 O(width × height)에서 상수 시간 O(1)으로 최적화함으로써 고속 실시간 처리에 적합한 시스템을 설계합니다.

핵심 포인트

  • LPR 전처리는 번호판의 고대비 및 에지 구조를 강조하여 탐지 후보 영역을 좁히는 데 필수적이다.
  • 적분 이미지(Integral Image)를 사용하면 임의 직사각형 영역의 합을 상수 시간 O(1)에 계산할 수 있어 성능 최적화에 매우 유리하다.
  • 국부 대비 분석은 주변 환경과의 강도 차이에 초점을 맞춰 번호판 문자의 경계와 특징을 효과적으로 추출한다.
  • 이 파이프라인은 고속 처리를 목표로 설계되었으며, 실시간 시스템 구현에 적합한 구조를 갖추고 있다.

이전 기사에서는 이미지를 로드하고, 이를 그레이스케일 (Grayscale)로 변환하며, 인식 엔진에서 사용되는 핵심 데이터 구조를 소개했습니다. 이번 파트에서는 LPR (License Plate Recognition, 번호판 인식) 파이프라인의 전처리 단계를 구축하기 시작합니다. 전처리의 목표는 번호판 문자가 포함될 가능성이 높은 이미지 영역을 강화하는 것입니다. 우리는 다음을 구현할 것입니다:

  • 적분 이미지 (Integral image) 생성
  • 국부 대비 (Local contrast) 분석
  • 에지 맵 (Edge map) 추출
  • 삼진 에지 이미지 (Ternary edge image) 변환

이러한 작업들은 고속 처리를 위해 설계되었으며 실시간 시스템에 적합합니다.

LPR에서 전처리가 중요한 이유
번호판은 일반적으로 다음과 같은 특징을 포함합니다:

  • 고대비 (High-contrast) 문자
  • 조밀한 수직 및 수평 전이
  • 반복적인 에지 구조

이미지 전체에 비용이 많이 드는 연산을 적용하는 대신, 전처리는 번호판 탐지가 시작되기 전에 후보 영역을 강조하는 데 도움을 줍니다. 이 기사에서 구현된 파이프라인은 국부적인 강도 차이 (Local intensity differences)에 초점을 맞춥니다.

적분 이미지 (Integral Image)
첫 번째 최적화 단계는 적분 이미지를 구축하는 것입니다. 적분 이미지를 사용하면 임의의 직사각형 영역의 합을 상수 시간 (Constant time) 내에 계산할 수 있습니다.

적분 이미지가 없는 경우: 영역 합 = O(width × height)
적분 이미지가 있는 경우: 영역 합 = O(1)

이는 대형 이미지의 모든 픽셀을 처리할 때 매우 중요해집니다.

적분 이미지 계산
void CImageProc :: ComputeIntegralImage ( unsigned char * pbGray , int nWidth , int nHeight , int * pnSum ) {
int nW = nWidth + 1 ;
int nH = nHeight + 1 ;
int partialsum ;
memset ( pnSum , 0 , nH * nW * sizeof ( int ));
for ( int y = 1 ; y < nH ; y ++ ) {
pnSum [ y * nW ] = 0 ;
partialsum = 0 ;
for ( int x = 1 ; x < nW ; x ++ ) {
partialsum += ( int ) pbGray [ ( y - 1 ) * nWidth + ( x - 1 )];
pnSum [ y * nW + x ] = pnSum [( y - 1 ) * nW + x ] + partialsum ;
}
}
}

빠른 국부 대비 에지 맵
void CImageProc :: ComputeLocalContrastEdgeMap ( unsigned char * pbGray , int * pnSum , int * pnEdge , int nWidth , int nHeight ) {
if ( ! pbGray || ! pnSum || !

pnEdge || nWidth <= 0 || nHeight <= 0 ) return ; const int rw = 3 ; const int rh = 3 ; const int nW = nWidth + 1 ; const int centerCount = 5 ; const int windowCount = ( rw * 2 + 1 ) * ( rh * 2 + 1 ); const int surroundCount = windowCount - centerCount ; if ( nWidth <= rw * 2 || nHeight <= rh * 2 ) return ; for ( int y = rh ; y < nHeight - rh ; y ++ ) { const int row = y * nWidth ; const int prevRow = ( y - 1 ) * nWidth ; const int nextRow = ( y + 1 ) * nWidth ; const int top = y - rh ; const int bottom = y + rh + 1 ; for ( int x = ( rw + 1 ); x < nWidth - rw ; x ++ ) { const int left = x - rw ; const int right = x + rw + 1 ; int centerSum = pbGray [ prevRow + x ] + pbGray [ row + x - 1 ] + pbGray [ row + x ] + pbGray [ row + x + 1 ] + pbGray [ nextRow + x ]; int windowSum = pnSum [ bottom * nW + right ] - pnSum [ top * nW + right ] - pnSum [ bottom * nW + left ] + pnSum [ top * nW + left ]; int surroundSum = windowSum - centerSum ; double centerAvg = ( double ) centerSum / centerCount ; double surroundAvg = ( double ) surroundSum / surroundCount ; pnEdge [ row + x ] = ( int )( surroundAvg - centerAvg ); } } ComputeBorderLocalContrast ( pbGray , pnSum , pnEdge , nWidth , nHeight , 0 , nWidth - 1 , 0 , rh - 1 , rw , rh ); ComputeBorderLocalContrast ( pbGray , pnSum , pnEdge , nWidth , nHeight , 0 , nWidth - 1 , nHeight - rh , nHeight - 1 , rw , rh ); ComputeBorderLocalContrast ( pbGray , pnSum , pnEdge , nWidth , nHeight , 0 , rw , rh , nHeight - rh , rw , rh ); ComputeBorderLocalContrast ( pbGray , pnSum , pnEdge , nWidth , nHeight , nWidth - rw , nWidth - 1 , rh , nHeight - rh , rw , rh ); } void CImageProc :: ComputeBorderLocalContrast ( unsigned char * pbGray , int * pnSum , int * lpOut , int nWidth , int nHeight , int x0 , int x1 , int y0 , int y1 , int rw , int rh ) { if ( ! pbGray || ! pnSum || !

lpOut || nWidth <= 0 || nHeight <= 0 ) return ; int nW = nWidth + 1 ; if ( x0 < 0 ) x0 = 0 ; if ( y0 < 0 ) y0 = 0 ; if ( x1 >= nWidth ) x1 = nWidth - 1 ; if ( y1 >= nHeight ) y1 = nHeight - 1 ; for ( int y = y0 ; y <= y1 ; y ++ ) { int row = y * nWidth ; for ( int x = x0 ; x <= x1 ; x ++ ) { int left = x - rw ; int right = x + rw ; int top = y - rh ; int bottom = y + rh ; if ( left < 0 ) left = 0 ; if ( top < 0 ) top = 0 ; if ( right >= nWidth ) right = nWidth - 1 ; if ( bottom >= nHeight ) bottom = nHeight - 1 ; int surroundCount = ( right - left + 1 ) * ( bottom - top + 1 ); int surroundSum = pnSum [( bottom + 1 ) * nW + ( right + 1 )] - pnSum [ top * nW + ( right + 1 )] - pnSum [( bottom + 1 ) * nW + left ] + pnSum [ top * nW + left ]; int centerSum = 0 ; int centerCount = 0 ; // 중심 픽셀 (Center pixel) centerSum += pbGray [ row + x ]; surroundSum -= pbGray [ row + x ]; centerCount ++ ; surroundCount -- ; // 왼쪽 (Left) if ( x - 1 >= 0 ) { centerSum += pbGray [ row + ( x - 1 )]; surroundSum -= pbGray [ row + ( x - 1 )]; centerCount ++ ; surroundCount -- ; } // 오른쪽 (Right) if ( x + 1 < nWidth ) { centerSum += pbGray [ row + ( x + 1 )]; surroundSum -= pbGray [ row + ( x + 1 )]; centerCount ++ ; surroundCount -- ; } // 위쪽 (Top) if ( y - 1 >= 0 ) { centerSum += pbGray [( y - 1 ) * nWidth + x ]; surroundSum -= pbGray [( y - 1 ) * nWidth + x ]; centerCount ++ ; surroundCount -- ; } // 아래쪽 (Bottom) if ( y + 1 < nHeight ) { centerSum += pbGray [( y + 1 ) * nWidth + x ]; surroundSum -= pbGray [( y + 1 ) * nWidth + x ]; centerCount ++ ; surroundCount -- ; } if ( centerCount == 0 || surroundCount <= 0 ) { lpOut [ row + x ] = 0 ; continue ; } double centerAvg = ( double ) centerSum / centerCount ; double surroundAvg = ( double ) surroundSum / surroundCount ; lpOut [ row + x ] = ( int )( surroundAvg - centerAvg ); } } } 이 알고리즘은 다음을 수행합니다:
국부 이웃 추출 (Local neighborhood extraction)
중심 강도 평균화 (Center intensity averaging)
주변 강도 평균화 (Surrounding intensity averaging)
차이 계산 (Difference calculation)
결과: 강한 강도 변화 (Strong intensity transitions) ↓ 높은 에지 (High edge)

반응 (responses): 이는 강한 대비 경계 (strong contrast boundaries)를 포함하고 있는 번호판 글자들에 특히 효과적입니다.

현재의 전처리 파이프라인 (Current preprocessing pipeline) 우리의 LPR 엔진은 현재 다음 과정을 수행합니다:

입력 이미지 (Input Image)

그레이스케일 변환 (Grayscale Conversion)

적분 이미지 (Integral Image)

국부 대비 분석 (Local Contrast Analysis)

에지 맵 생성 (Edge Map Generation)

삼진 에지 이미지 (Ternary Edge Image)

이 시점에서 엔진은 이미 밀집된 글자 형태의 구조를 포함하는 영역을 강조할 수 있습니다. 다음 기사에서는 생성된 에지 정보를 사용하여 후보 번호판 영역을 찾는 과정을 시작하겠습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0