In the previous article, we loaded an image, converted it into grayscale, and introduced the core data structures used by the recognition engine.
In this part, we begin building the preprocessing stage of the LPR pipeline.
The goal of preprocessing is to enhance image regions that are likely to contain license plate characters.
We will implement:
- Integral image generation
- Local contrast analysis
- Edge map extraction
- Ternary edge image conversion
These operations are designed for high-speed processing and are suitable for real-time systems.
Why preprocessing matters in LPR
License plates typically contain:
- High-contrast characters
- Dense vertical and horizontal transitions
- Repetitive edge structures
Instead of applying expensive operations across the entire image, preprocessing helps emphasize candidate regions before plate detection begins.
The pipeline implemented in this article focuses on local intensity differences.
Integral Image
The first optimization step is building an integral image.
An integral image allows us to compute the sum of any rectangular region in constant time.
Without an integral image:
Region Sum = O(width × height)
With an integral image:
Region Sum = O(1)
This becomes extremely important when processing every pixel in large images.
Computing the integral image
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;
}
}
}
Fast local contrast edge map
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);
}
}
}
The algorithm performs:
- Local neighborhood extraction
- Center intensity averaging
- Surrounding intensity averaging
- Difference calculation
Result:
Strong intensity transitions
↓
High edge responses
This is particularly effective for license plate characters because they contain strong contrast boundaries.
Current preprocessing pipeline
Our LPR engine now performs:
Input Image
↓
Grayscale Conversion
↓
Integral Image
↓
Local Contrast Analysis
↓
Edge Map Generation
↓
Ternary Edge Image
At this point, the engine can already highlight regions containing dense character-like structures.
In the next article, we will begin locating candidate license plate regions using the generated edge information.
Top comments (0)