*

Georiga Tech Senior Design Project

The License Plate Recognition System
My senior deisgn project for ECE 4006 was a group project consisting of a Signal Processing problem of our choosing. My group members were Devin Porter, Chike Lindsay-Ajudua, Leo Dachevski, Muhammad Raza. I would also like to take the time the praise Leo and thank him for his hard work and "above and beyond" attitude in getting the project done while we were working together for hours in the CoC. Back to the topic at hand though, we began by deciding on our project by choosing from several options which we had compiled into a list with pros and cons for each idea. we decided on a system for recognizing license plates for several reasons:
  • It had not been done before by any previous ECE 4006 project group
  • It was challenging enough, but still possible to complete in the time provided
  • It just sounded like something very cool
After consulting a few professors who specialized in image processing, we decided on a method where the license plate would be first converted to binary (black and white) according the some threshold. Second, the picture would be processed into two signals, one horizontal and one vertical. Third, The signals would be stretched, clipped according to smart algorithms, and analyzed in order to find the location of the license plate in the image. Lately, the signals would be compared using cross correlation to find matches between license plates. using cross correlation. Cross correlation will be described later, since I would like to now go through the whole process in an example. Please also note that code is available to test our system as well, although it is very limited it only exists to prove the system is feasible, but could easily be modifiable and extended to more robust solutions.

Also, you can download the full source code for the system including testing images at the bottom of this page.


Getting the Pictures of the cars
Several compromised had to be made on retrieving the pictures of the cars. First, it was assumed that the license plates would be standard Georgia state license plates, or at least a plate that was dark text on a light colored background. Second, it was assumed that either a light was being shown on the back of the car or that a flash was used when taking the pictures. The light used illuminated the license plate's reflective surface making the job of finding the plate within the picture much easier, especially on cars with a white paint job. These compromises were designed to make the project possible in the given amount of time due to the complexity of the problem and the lack of any previous project similar enough to simply improve upon.

Examples of a plate without a light or flash, and a plate with light or flash are given below. Note the drastic difference in the pictures. Also, please note that both pictures were taken during the same time of day, while it was daylight outside. Although our system would work with less drastic differences in lightness between the plate and car, for the purpose of our project this advantage was utilized mainly to ensure that the a working example could be demonstrated above and beyond any other goals. Also included below is a picture of a binarized image. Please note that this image is never actually generated in order to do further analysis, it is just a visual. In reality, the image is turned into two vectors directly, using a threshold calculated by the original image. More of this will be discussed in the below though.

No Flash used

 
Flash Used

 
Binarized Image




Convert the Images to Binary
In order to better analyze the image, the images were binarized so that all pixels were cast to either 0 or 1 (black or white respectively). In order to binarize the image though, a calculation had to be done first to know the threshold where pixels should be cast to either black or white. From the sample pictures we had taken, a percentage of 95% black on the image seemed to fit well when trying to show the plate as all white. Below is the code which took that 95%, and calculated what level of color in the picture represented that 95% cutoff.

// get total number of pixels in the area we are analyzing
int total = (h_offset2 - h_offset) * (v_offset2 - v_offset);
// create vector, level it to all zero.
unsigned int tmp[256];
for( i=0; i <= 255; i++ )
        tmp[i] = 0;
// make sure offsets make sense
if( h_offset2 < h_offset )
        return 0;
if( v_offset2 < v_offset )
        return 0;
// count shades into tmp
for( i=v_offset; i < v_offset2 ; i++ ){
        for( j=h_offset; j < h_offset2 ; j++ ){
                t = (unsigned int)input[i][j];
                if(t>=0 && t<=255){
                        tmp[ (unsigned int)input[i][j] ] += 1;
                }
        }
}
// calculate the number of pixels to stop making black.
total = total * percent / 100;
// go through tmp, find best shade to make threshold at.
for( i=0; i <= 255; i++ ){
        m = total;
        total -= tmp[i];
        if( total <= 0 ){
                if(tmp[i] > m*1.5 && i>0)
                        return i-1;
                else return i;
        }
}



Vectorizing the image
Once the threshold is known, we can know which pixels should be white and which should be black. Using this, we can then convert the image into two vectors. The first vector simply counts the number of white pixels in each column (horizontal vector) and the second vector counts the number of white pixels in each row (vertical vector). Below is the code used to vectorize the image. Note that this looks at the original image, and decides if each pixel should be black or white on the fly. It does not actually save a binarized version of the image first. The binarized picture above is generated just for visual and testing purposes.

int i,j,t,s,pix = 0;
// clear past vectorization.
clear_vector(hh, h2-h1);
clear_vector(vv, v2-v1);
// start binarizing and vectorizing.
for( i=v1; i < v2 ; i++ ){
        for( j=h1; j < h2 ; j++ ){
                if(h1!=0 && (j<h1+5 || j>h2-5 || i<v1+10 || i>v2-10))
                        pix = 999;
                else
                        pix = (int)input[i][j];
                // test threshold, add to signatues appropriatly.
                if(pix > thresh){
                        hh[s] += (short)1;
                        vv[t] += (short)1;
                        output[i][j] = 0xFF;
                }
                else{
                        output[i][j] = 0x00;
                }
                s++;
        }
        s=0;
        t++;
}
return 1;

Sample vectors (taken from the same car pictures shown above) are displayed below. These were generated for testing purposes, and show how for each white pixel in a given row or column, the vector height raises by one pixel as well. Essentially, it just shows how the white pixels in the rows and columns of the binarized image are added up to show a function.

This is the Horizontal Vector (sum of columns)

 
This is the Vertical Vector (sum of rows)


There are problems with these vectors though. Notice how some other spikes come up nearly as far as the license plate spikes? Also, notice how some dips in the license plate spikes come far enough to cause confusion, even farther below the top of spikes that are not within the license plate. To account for this, the vectors are then filtered several ways. First, the signal is put through an averager to smooth the signal and remove very dramatic changes in the vector. Second, the vectors are put through a multiplier to help bring out the taller spikes for better analysis. Finally, the vectors are put through a custom "despiker" filter which removes all spikes in the signal that are not as wide as the smallest expected width of the license plate spike.

void averager(unsigned short* sig, int len, int num){
        int j,i,start,stop,avg,tmp = 0;
        int m[9] = {0,0,0,0,0,0,0,0,0};
        // checking
        if(num%2 == 0)
                num += 1;
        if(num > 9)
                num = 9;
        if(num < 0)
                return;
        // get the avg of the signal
        for(i=0; i<len; i++){
                avg += (int)sig[i];
        }
        avg /= len;
        m[0] = avg;
        m[1] = avg;
        m[2] = avg;
        m[3] = avg;
        // start binarizing and vectorizing.
        for( j=0; j < len ; j++ ){
                m[0] = m[1];
                m[1] = m[2];
                m[2] = m[3];
                m[3] = m[4];
                m[4] = (int)sig[j];
                if(j+1 < len) m[5] = (int)sig[j+1];
                else m[5] = avg;
                if(j+2 < len) m[6] = (int)sig[j+2];
                else m[6] = avg;
                if(j+3 < len) m[7] = (int)sig[j+3];
                else m[6] = avg;
                if(j+4 < len) m[8] = (int)sig[j+4];
                else m[6] = avg;
                // test threshold, add to signatues appropriatly.
                start = 4 - (num - 1)/2;
                stop = 4 + (num - 1)/2;
                for(i=start; i<=stop; i++){
                        tmp += (int)m[i];
                }
                sig[j] = (unsigned short)(tmp/num);
                tmp = 0;;
        }
}

void despiker(unsigned short* sig, int len, int thresh, int diff){
        int j = 0;
        int i = 0;
        int k = 0;
        // first run, remove taller spikes
        for( j=0; j < len ; j++ ){
                if(sig[j] > thresh && j < len-1)
                        i++;
                else if(i < diff){
                        for(k=j-i; k<j; k++){
                                sig[k] = 0;
                        }
                        i=0;
                }
                else i=0;
        }
        // second run, remove sides of spikes left by first run
        thresh /= 4;
        i=0;
        for( j=0; j < len ; j++ ){
                if(sig[j] > thresh && j < len-1)
                        i++;
                else if(i < diff){
                        for(k=j-i; k<j; k++){
                                sig[k] = 0;
                        }
                        i=0;
                }
                else i=0;
        }
}

void multiplier(unsigned short* sig, int len, int m){
        int j = 0;
        for( j=0; j < len ; j++ ){
                sig[j] *= m;
        }
}


The results of these filters can be seen in the images below. The images clearly show how some of the original large spikes were rounded off, how the image was made taller, and how spikes to the sides of the plate spikes were cut off. This leaves us the ability to look for the sides of the license plate spike to know the boundaries of the license plate in the image.

This is the filtered Horizontal Vector

 
This is the filtered Vertical Vector



Turning the Plate into a Signature
Once the filtered vectors are generated, the sides of the license plate spike is found. These two sides from both images represent the borders of the license plate within the original image. Using these borders, the license plate by itself is turned into vertical and horizontal vectors just like the original image was before. This time however, the vectors will become the actual signature of the plate itself, and a different threshold is found since within the plate there is a different expected ratio of black and white pixels. Please note that we did not use the already calculated vector of the plate because it originally took into account all the background information as well when generating the signal. To get the cleanest signature possible, we did a new vectorization of only the plate to ensure that no background noise around the car or plate would compromise the signature.

Once the license plate is vectorized, it is put through the same averager as the vector of the orignal image. However, the plate vectors are not put through the despiker function or the multiplier as these would only degrade the signature in unpredictable ways. Instead, the plate vectors are put through a normalizer function which sets the average amplitude to a set position. This must be done in order to compare the signatures later on, since it is the shape of the vector signal and not the height of the signal that we are concerned about. Shown below are the edges of the plate found by the functions, and the vectors of the plate itself after being averaged and normalized.

The plate showing the calculated edges

 
Horizontal signature of the plate

 
Vertical signature of the plate




Comparing License Plate Signatures
Lets begin with a real world example; a parking garage deck. As you drive up to the gate, a camera behind your care snaps a picture of the back of your car. The system them uses the methods above to calculate the signature of your license plate, and then checks to see if that signature exists in a known database of other license plate signatures. The database of signatures would be of cars that are allowed to park in that parking deck, so if your plate's signature is found in the database the parking deck's mechanical arm would lift up and allow you to drive into the deck. How does the system check to see if your signature exists in the database? The problem is more complicated that it sounds, since slight variations in light, position of the car, and a variety of things I can't even think of could potentially effect the signal so that two signatures from the same plate taken on two different days would probably not match exactly.

To overcome this problem, we used a cross correlation function to calculate if the signals matched as a percentage. Not only that, but the cross correlation function has the added bonus of checking the signal across different offsets, so that if a license plate signature was slightly off to one side when compared to the same plate's signature in the database, the function would be able to account for this. The greatest percentage of matching between the different offsets is used as the true matching percentage of the signatures. The cross correlation function is shown below, and returns the matching percentage.

double correlation(unsigned short *x, unsigned short *y, int n){
        int i,j, delay;
        double sx,sy,sx2,sy2,sxy,dx2,dy2,dxy,r,max=0;
        unsigned short val;
        for (delay=-_MAX_DELAY; delay<_MAX_DELAY; delay++){
                // reset values
                sx,sy,sx2,sy2,sxy = 0;
                // Calculate the mean of the two series x[], y[]
                for(i=0; i<n; i++){
                        j = i - delay;
                        if(j<0 || j>n)
                                val = 0;
                        else
                                val = y[j];
                        sx += (double)(x[i]);
                        sy += (double)(val);
                        sx2 += (double)(x[i] * x[i]);
                        sy2 += (double)(val * val);
                        sxy += (double)(x[i] * val);
                }
                // calculate the matching percentage between the two signals
                dx2 = sx2 - ( (sx*sx) / ((double)n) );
                dy2 = sy2 - ( (sy*sy) / ((double)n) );
                dxy = sxy - ( (sx*sy) / ((double)n) );
                r = dxy / sqrt( dx2 * dy2 );
                // if this round matches more, use this match percentage
                if(fabs(max) < fabs(r))
                        max = fabs(r);
        }
        // return the best matching percentage
        return max;
}



Conclusions and Final Thoughts
For two different pictures of the same car, the matching percentage between the two functions was typically above 95% during our testing, while matching percentages between two different cars typically varied between 20% and 80%. It should also be noted that the vertical signature would typically match much more between different plates that the horizontal signature, and therefore the horizontal signature's match percentage was more trustworthy and was made more influential when calculating if the two plates actually matched or not.

One of the questions we regularly got when describing our project to others was "why didn't you read the characters on the plate and save those instead of the signature?" The answer is that character recognition is an extremely difficult problem, and though we wanted the system to work that way in the beginning, it was decided that we might not be able to get a working system up and running in the time allotted for the project. Therefore, the simpler but nearly as effective method described above was used with very promising results.

What could be done better? Right now, the code has several percentages and thresholds hard coded into the program which were manually changed during testing for different photographs in order to accurately find the edges of the license plate in those images. For instance, for a white car or when glare is coming off the back window of the car there is a much greater amount of white in the picture which can result in much greater differences in the binarization of the image. We did not get a chance to write code to automatically calculate the thresholds and averages for our system, but doing so would make the system much more robust.

Secondly, the plate signatures were normalized vertically (set with the same average amplitude), but they were not normalized horizontally. This means that if pictures are taken at different distances, the signals would be different lengths and would probably not match anymore. For this reason, the pictures need to be very close to the same distance away, or a stretching/shrinking function needs to be put into the system to normalize each signature to a set length to account for such an issue. Other than that, I cannot think of anything off the top of my head but I'm sure other drawbacks probably exist which I have not thought of.


Test it out yourself!
Compile the code on a Linux system by simply running "make", but you will need to have the stdio.h, stdlib.h, memory.h, setjmp.h, and jpeglib.h libraries available on the system. For Ubuntu Linux, this means having the libjpeg62-dev, glibc-headers, and build-essential packages installed (in addition to the gcc package obviously). I cannot say what packages are needed for other distros though, but a google search for your distro and the library files mentioned above would probably come up with them.

Download the code here.

Also note that there is no "make install" or "configure", this is not an installation, just a demonstration. Not familiar with "make"? Just extract the files into a directory on Linux, open a command line in that directory, and type the command "make", and the program will compile (simple eh?). Once compiled, you run the file created named "test" by typing "./test", and then the program will give you a menu system with options to choose from. The "database" is actually just an array in memory and can only hold 10 signatures, but it gives a good demonstration at least. There are also a lot of debug messages outputted so you can see exactly what is going on.

There are some images included in the zip file (including the image shown above) for use in testing. Also note that it will save output images in the same directory, which show the different stages of the image and vectors. These are used for debug purposes so you can see where things might be going wrong if it's not working for you. Lastly, please note that the system expects a picture that is EXACTLY 600x800 pixels, so if you are testing with other images, be sure to resize them to that size. That's it!



reece
home
history
baby
photos
calendar
addresses
wall
projects
4006
word
flickr
monitor
chat
lolmail
work
cocard
ibm
resume
dev
sudoku
security
portsentry
portknock
badbot
setuid
web
greasemonkey
visitors
links
downloads
misc
art
vote
influence
waffles