DEV Community

Edward Obar Cabigting
Edward Obar Cabigting

Posted on

Building a License Plate Recognition Engine in C++ — Part 2: Grayscale Image Preprocessing and Local Contrast Edge Detection

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)
Enter fullscreen mode Exit fullscreen mode

With an integral image:

Region Sum = O(1)
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The algorithm performs:

  1. Local neighborhood extraction
  2. Center intensity averaging
  3. Surrounding intensity averaging
  4. Difference calculation

Result:

Strong intensity transitions
        ↓
High edge responses
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)