Wednesday, February 27, 2013

Hotfix for leaking filedescriptors

I just came across this neat gdb trick. Say you have a badly written program that's leaking file descriptors all over.. and it's in prod (the horror!) and it's imperative that you let it limp along till the patched binary comes around... GDB can close the file descriptors for you!

Here's how:

Leaky Program
#include <stdio.h>
#include <unistd.h>
#include <string.h>
void open_file(char *filename)
{
FILE *fd = fopen(filename, "r");
}
int main(void)
{
char filename[FILENAME_MAX];
strcpy(filename, "/etc/passwd");
int iteration = 0;
while (1) {
iteration++;
printf("Iteration %d\n", iteration);
open_file(filename);
sleep(60);
}
return 0;
}
view raw leaky.c hosted with ❤ by GitHub


Lsof tells it all. Take note of the two open descriptors to /etc/passwd
$ lsof -p $(pgrep leaky)
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
leaky 16095 laban cwd DIR 0,30 4096 10936796 /home/laban/devel/private/learning/c/leak_descriptors
leaky 16095 laban rtd DIR 252,1 4096 2 /
leaky 16095 laban txt REG 0,30 11267 10919599 /home/laban/devel/private/learning/c/leak_descriptors/leaky
leaky 16095 laban mem REG 252,1 1811128 19136595 /lib/x86_64-linux-gnu/libc-2.15.so
leaky 16095 laban mem REG 252,1 149280 19136932 /lib/x86_64-linux-gnu/ld-2.15.so
leaky 16095 laban 0u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 1u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 2u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 3r REG 252,1 2667 1575354 /etc/passwd
leaky 16095 laban 4r REG 252,1 2667 1575354 /etc/passwd
view raw lsof hosted with ❤ by GitHub


Playing around with variables in GDB. Can you guess why we chose the magic value 2147483646? For a hint, look at the last gist in this post
$ sudo gdb -p $(pgrep leaky)
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://bugs.launchpad.net/gdb-linaro/>.
Attaching to process 16095
Reading symbols from /home/laban/devel/private/learning/c/leak_descriptors/leaky...done.
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug/lib/x86_64-linux-gnu/libc-2.15.so...done.
done.
Loaded symbols for /lib/x86_64-linux-gnu/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2...Reading symbols from /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.15.so...done.
done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
0x00007f253e7cf820 in __nanosleep_nocancel () at ../sysdeps/unix/syscall-template.S:82
82 ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb)
(gdb) bt
#0 0x00007f253e7cf820 in __nanosleep_nocancel () at ../sysdeps/unix/syscall-template.S:82
#1 0x00007f253e7cf6dc in __sleep (seconds=0) at ../sysdeps/unix/sysv/linux/sleep.c:138
#2 0x0000000000400620 in main () at leaky.c:21
(gdb) up
#1 0x00007f253e7cf6dc in __sleep (seconds=0) at ../sysdeps/unix/sysv/linux/sleep.c:138
138 ../sysdeps/unix/sysv/linux/sleep.c: No such file or directory.
(gdb) up
#2 0x0000000000400620 in main () at leaky.c:21
21 sleep(60);
(gdb) set filename="/etc/group"
...
... Awhile later
...
(gdb) set iteration=4
...
... Awhile later
...
(gdb) set iteration=2147483646
(gdb) p iteration
$1 = 2147483646
(gdb) quit
A debugging session is active.
Inferior 1 [process 16095] will be detached.
Quit anyway? (y or n) y
Detaching from program: /home/laban/devel/private/learning/c/leak_descriptors/leaky, process 16095
view raw gistfile1.txt hosted with ❤ by GitHub


What does lsof think?
$ lsof -p $(pgrep leaky)
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
leaky 16095 laban cwd DIR 0,30 4096 10936796 /home/laban/devel/private/learning/c/leak_descriptors
leaky 16095 laban rtd DIR 252,1 4096 2 /
leaky 16095 laban txt REG 0,30 11267 10919599 /home/laban/devel/private/learning/c/leak_descriptors/leaky
leaky 16095 laban mem REG 252,1 1811128 19136595 /lib/x86_64-linux-gnu/libc-2.15.so
leaky 16095 laban mem REG 252,1 149280 19136932 /lib/x86_64-linux-gnu/ld-2.15.so
leaky 16095 laban 0u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 1u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 2u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 3r REG 252,1 2667 1575354 /etc/passwd
leaky 16095 laban 4r REG 252,1 2667 1575354 /etc/passwd
leaky 16095 laban 5r REG 252,1 1210 1575723 /etc/group
leaky 16095 laban 6r REG 252,1 1210 1575723 /etc/group
leaky 16095 laban 7r REG 252,1 1210 1575723 /etc/group
leaky 16095 laban 8r REG 252,1 1210 1575723 /etc/group
view raw lsof-output2.sh hosted with ❤ by GitHub


Bulk close of fds
Leaks all over the place
$ lsof -p $(pgrep leaky)
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
leaky 16095 laban cwd DIR 0,30 4096 10936796 /home/laban/devel/private/learning/c/leak_descriptors
leaky 16095 laban rtd DIR 252,1 4096 2 /
leaky 16095 laban txt REG 0,30 11267 10919599 /home/laban/devel/private/learning/c/leak_descriptors/leaky
leaky 16095 laban mem REG 252,1 1811128 19136595 /lib/x86_64-linux-gnu/libc-2.15.so
leaky 16095 laban mem REG 252,1 149280 19136932 /lib/x86_64-linux-gnu/ld-2.15.so
leaky 16095 laban 0u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 1u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 2u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 3r REG 252,1 2667 1575354 /etc/passwd
leaky 16095 laban 4r REG 252,1 2667 1575354 /etc/passwd
leaky 16095 laban 5r REG 252,1 1210 1575723 /etc/group
leaky 16095 laban 6r REG 252,1 1210 1575723 /etc/group
leaky 16095 laban 7r REG 252,1 1210 1575723 /etc/group
leaky 16095 laban 8r REG 252,1 1210 1575723 /etc/group
GDB closing fds
$ sudo gdb -p $(pgrep leaky)
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://bugs.launchpad.net/gdb-linaro/>.
Attaching to process 16095
Reading symbols from /home/laban/devel/private/learning/c/leak_descriptors/leaky...done.
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug/lib/x86_64-linux-gnu/libc-2.15.so...done.
done.
Loaded symbols for /lib/x86_64-linux-gnu/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2...Reading symbols from /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.15.so...done.
done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
0x00007f253e7cf820 in __nanosleep_nocancel () at ../sysdeps/unix/syscall-template.S:82
82 ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) set $max=8
(gdb) set $current=3
(gdb) while ($current < $max)
> p close($current++)
>end
$5 = -1
$6 = 0
$7 = 0
$8 = 0
$9 = 0
(gdb) ^Z
[2]+ Stopped sudo gdb -p $(pgrep leaky)
What lsof thinks after GDB's done
$ lsof -p $(pgrep leaky)
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
leaky 16095 laban cwd DIR 0,30 4096 10936796 /home/laban/devel/private/learning/c/leak_descriptors
leaky 16095 laban rtd DIR 252,1 4096 2 /
leaky 16095 laban txt REG 0,30 11267 10919599 /home/laban/devel/private/learning/c/leak_descriptors/leaky
leaky 16095 laban mem REG 252,1 1811128 19136595 /lib/x86_64-linux-gnu/libc-2.15.so
leaky 16095 laban mem REG 252,1 149280 19136932 /lib/x86_64-linux-gnu/ld-2.15.so
leaky 16095 laban 0u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 1u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 2u CHR 136,19 0t0 22 /dev/pts/19
leaky 16095 laban 8r REG 252,1 1210 1575723 /etc/group
view raw gistfile1.txt hosted with ❤ by GitHub


Shell output from leaky
$ ./leaky
Iteration 1
Iteration 2
Iteration 5
Iteration 6
Iteration 2147483647
Iteration -2147483648
Iteration -2147483647
view raw gistfile1.txt hosted with ❤ by GitHub


Thursday, February 7, 2013

Gem of a fortune - I. M. Banks.

"Good afternoon, madam.  How may I help you?"

"Good afternoon.  I'd like a FrintArms HandCannon, please."

"A--?  Oh, now, that's an awfully big gun for such a lovely lady.  I
mean, not everybody thinks ladies should carry guns at all, though I
say they have a right to.  But I think... I might... Let's have a look
down here.  I might have just the thing for you.  Yes, here we are!
Look at that, isn't it neat?  Now that is a FrintArms product as well,
but it's what's called a laser -- a light-pistol some people call
them.  Very small, as you see; fits easily into a pocket or bag; won't
spoil the line of a jacket; and you won't feel you're lugging half a
tonne of iron around with you.  We do a range of matching accessories,
including -- if I may say so -- a rather saucy garter holster.  Wish I
got to do the fitting for that!  Ha -- just my little joke.  And
there's *even*... here we are -- this special presentation pack: gun,
charged battery, charging unit, beautiful glider-hide shoulder holster
with adjustable fitting and contrast stitching, and a discount on your
next battery.  Full instructions, of course, and a voucher for free
lessons at your local gun club or range.  Or there's the *special*
presentation pack; it has all the other one's got but with *two*
charged batteries and a night-sight, too.  Here, feel that -- don't
worry, it's a dummy battery -- isn't it neat?  Feel how light it is?
Smooth, see?  No bits to stick out and catch on your clothes, *and*
beautifully balanced.  And of course the beauty of a laser is, there's
no recoil.  Because it's shooting light, you see?  Beautiful gun,
beautiful gun; my wife has one.  Really.  That's not a line, she
really has.  Now, I can do you that one -- with a battery and a free
charge -- for ninety-five; or the presentation pack on a special
offer for one-nineteen; or this, the special presentation pack, for
one-forty-nine."

"I'll take the special."

"Sound choice, madam, *sound* choice.  Now, do--?"

"And a HandCannon, with the eighty-mill silencer, five GP clips, three
six-five AP/wire-flechettes clips, two bipropellant HE clips, and a
Special Projectile Pack if you have one -- the one with the embedding
rounds, not the signalers.  I assume the night-sight on this toy is
compatible?"

"Aah... yes,  And how does madam wish to pay?"

She slapped her credit card on the counter.  "Eventually."

        -- Iain M. Banks, "Against a Dark Background"

Wednesday, February 6, 2013

Automatic number plate detection

ALPR stands for automatic license plate recognition. The process typically consists of:
  • Identifying potential license plate candidates from an image.
  • Ranking the potential plate candidates and selecting the most likely one.
  •  Identifying alphanumeric characters and converting them to text.

The hardest part of this process is reliably acquiring the plate candidate image with variables such as:
  • Lighting conditions.
  • Camera angle.
  • Image noise
  • Variations in the type of plates.
  • Distance to plate; and consequently, the size of the plate in the image

In this article, I'll be going through a gist that works for a few images. I'll cover the literature review in a followup article.

Please note that this implementation has been developed and tested with very few samples; overfitting will probably reduce it's effectiveness.

#! /usr/bin/env python
import cv2
import numpy as np
import pymeanshift as pms
from blobs.BlobResult import CBlobResult
from blobs.Blob import CBlob # Note: This must be imported in order to destroy blobs and use other methods
#############################################################################
# so, here is the main part of the program
if __name__ == '__main__':
import sys
import os
blob_overlay = True
file_name = "plates/license1.png"
if len(sys.argv) != 1:
file_name = sys.argv[1]
base_name = os.path.basename(file_name)
fname_prefix = ".".join(base_name.split(".")[:-1])
print fname_prefix
# Image load & conversion to cvmat
license_plate = cv2.imread(file_name, cv2.CV_LOAD_IMAGE_COLOR)
# Segment
segmented, labels, regions = pms.segment(license_plate, 3, 3, 50)
print "Segmentation results"
print "%s: %s" % ("labels", labels)
print "%s: %s" % ("regions", regions)
cv2.imwrite('%s_segmented.png' % fname_prefix, segmented)
license_plate = cv2.imread('%s_segmented.png' % fname_prefix, cv2.CV_LOAD_IMAGE_COLOR)
license_plate_size = (license_plate.shape[1], license_plate.shape[0])
license_plate_cvmat = cv2.cv.fromarray(license_plate)
license_plate_ipl = cv2.cv.CreateImage(license_plate_size, cv2.cv.IPL_DEPTH_8U, 3)
cv2.cv.SetData(
license_plate_ipl,
license_plate.tostring(),
license_plate.dtype.itemsize * 3 * license_plate.shape[1])
license_plate_white_ipl = cv2.cv.CreateImage(license_plate_size, cv2.cv.IPL_DEPTH_8U, 1)
cv2.cv.Set(license_plate_white_ipl, 255)
# Grayscale conversion
inverted_license_plate_grayscale_ipl = cv2.cv.CreateImage(
license_plate_size,
cv2.cv.IPL_DEPTH_8U, 1)
license_plate_grayscale_ipl = cv2.cv.CreateImage(
license_plate_size,
cv2.cv.IPL_DEPTH_8U, 1)
cv2.cv.CvtColor(
license_plate_cvmat,
license_plate_grayscale_ipl,
cv2.COLOR_RGB2GRAY);
license_plate_grayscale_np = np.asarray(license_plate_grayscale_ipl[:,:])
# We can also use cv.saveimage
# cv2.cv.SaveImage('license1_grayscale.png', license_plate_grayscale_ipl)
cv2.imwrite('%s_grayscale.png' % fname_prefix, license_plate_grayscale_np)
# Thresholding or binarization of images
(threshold_value, thresh_image) = cv2.threshold(
license_plate_grayscale_np,
128,
255,
cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print "Thresholding complete. Partition value is %d" % threshold_value
cv2.imwrite('%s_threshold.png' % fname_prefix, thresh_image)
# Create a mask that will cover the entire image
mask = cv2.cv.CreateImage (license_plate_size, 8, 1)
cv2.cv.Set(mask, 1)
#if not blob_overlay:
# # Convert black-and-white version back into three-color representation
# cv2.cv.CvtColor(my_grayscale, frame_cvmat, cv2.COLOR_GRAY2RGB);
# Blob detection
thresh_image_ipl = cv2.cv.CreateImage(license_plate_size, cv2.cv.IPL_DEPTH_8U, 1)
cv2.cv.SetData(
thresh_image_ipl,
thresh_image.tostring(),
thresh_image.dtype.itemsize * 1 * thresh_image.shape[1])
cv2.cv.Not(thresh_image_ipl, inverted_license_plate_grayscale_ipl)
# Min blob size and Max blob size
min_blob_size = 100 # Blob must be 30 px by 30 px
max_blob_size = 10000
threshold = 100
# Plate area as % of image area:
max_plate_to_image_ratio = 0.3
min_plate_to_image_ratio = 0.01
image_area = license_plate_size[0] * license_plate_size[1]
# Mask - Blob extracted where mask is set to 1
# Third parameter is threshold value to apply prior to blob detection
# Boolean indicating whether we find moments
myblobs = CBlobResult(thresh_image_ipl, mask, threshold, True)
myblobs.filter_blobs(min_blob_size, max_blob_size)
blob_count = myblobs.GetNumBlobs()
print "Found %d blob[s] betweeen size %d and %d using threshold %d" % (
blob_count, min_blob_size, max_blob_size, threshold)
for i in range(blob_count):
my_enumerated_blob = myblobs.GetBlob(i)
# print "%d: Area = %d" % (i, my_enumerated_blob.Area())
my_enumerated_blob.FillBlob(
license_plate_grayscale_ipl,
#license_plate_ipl,
#cv2.cv.Scalar(255, 0, 0),
cv2.cv.CV_RGB(255, 0, 0),
0, 0)
my_enumerated_blob.FillBlob(
license_plate_white_ipl,
#license_plate_ipl,
#cv2.cv.Scalar(255, 0, 0),
cv2.cv.CV_RGB(255, 255, 255),
0, 0)
# we can now save the image
#annotated_image = np.asarray(license_plate_ipl[:,:])
blob_image = np.asarray(license_plate_grayscale_ipl[:,:])
cv2.imwrite("%s_blobs.png" % fname_prefix, blob_image)
blob_white_image = np.asarray(license_plate_white_ipl[:,:])
cv2.imwrite("%s_white_blobs.png" % fname_prefix, blob_white_image)
# Looking for a rectangle - Plates are rectangular
# Thresholding image, the find contours then approxPolyDP
(threshold_value, blob_threshold_image) = cv2.threshold(
blob_white_image,
128,
255,
cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print "Thresholding complete. Partition value is %d" % threshold_value
cv2.imwrite('%s_blob_threshold.png' % fname_prefix, blob_threshold_image)
# Blur to reduce noise?
#blurred_plate = cv2.GaussianBlur(blob_threshold_image, (5,5), 0)
#blob_threshold_image = blurred_plate
# Erode then dilate to reduce noise
blob_threshold_image_invert = cv2.bitwise_not(blob_threshold_image)
cv2.imwrite("%s_pre_dilated_and_eroded.png" % fname_prefix, blob_threshold_image_invert)
eroded_white_blobs = cv2.erode(blob_threshold_image_invert, None, iterations=4);
cv2.imwrite("%s_eroded_image.png" % fname_prefix, eroded_white_blobs)
dilated_white_blobs = cv2.dilate(eroded_white_blobs, None, iterations=4);
cv2.imwrite("%s_dilated.png" % fname_prefix, dilated_white_blobs)
blob_threshold_image = cv2.bitwise_not(blob_threshold_image_invert)
cv2.imwrite("%s_dilated_and_eroded.png" % fname_prefix, blob_threshold_image)
blob_threshold_image_invert = cv2.bitwise_not(blob_threshold_image)
contours, hierarchy = cv2.findContours(
blob_threshold_image,
cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
#print "Contours: ", contours
# We now have contours. Approximate the polygon shapes
largest_rectangle_idx = 0
largest_rectangle_area = 0
rectangles = []
colours = ( (255,0,0), (0,255,0), (0,0,255), (255,255,0), (0,255,255))
for idx, contour in enumerate(contours):
print "Contour: %d" % idx
contour_area = cv2.contourArea(contour)
if float(contour_area / image_area) < min_plate_to_image_ratio:
print "Contour %d under threshold. Countour Area: %f" % (idx, contour_area)
continue
elif float(contour_area / image_area) > max_plate_to_image_ratio:
print "Contour %d over threshold. Countour Area: %f" % (idx, contour_area)
continue
approx = cv2.approxPolyDP(
contour,
0.02 * cv2.arcLength(contour, True),
True)
print "\n -"
print "%d. Countour Area: %f, Arclength: %f, Polygon %d colour:%s" % (idx,
contour_area,
cv2.arcLength(contour, True),
len(approx),
colours[idx%len(colours)])
minarea_rectangle = cv2.minAreaRect(contour)
minarea_box = cv2.cv.BoxPoints(minarea_rectangle)
print "> ", minarea_rectangle
print ">> ", minarea_box
centre, width_and_height, theta = minarea_rectangle
aspect_ratio = float(max(width_and_height) / min(width_and_height))
print " aspect ratio: %f for %s " % (aspect_ratio, width_and_height)
minarea_box = np.int0(minarea_box)
cv2.drawContours(license_plate, [minarea_box], 0, (255,0,255), 2)
cv2.drawContours(
license_plate,
[contours[idx]],
0,
colours[idx%len(colours)])
# Aspect ratio removal
if aspect_ratio < 3 or aspect_ratio > 5:
print " Aspect ratio bounds fails"
continue
# Rectangles have polygon shape 4
if len(approx) == 4:
# Select the largest rect
rectangles.append(contour)
if contour_area > largest_rectangle_area :
largest_rectangle_area = contour_area
largest_rectangle_idx = idx
print "Probable plate hit is %d" % largest_rectangle_idx
cv2.drawContours(
license_plate,
[contours[largest_rectangle_idx]],
0,
colours[0],
idx + 1)
cv2.imwrite("%s_contours_colored.png" % fname_prefix, license_plate)
# Create a mask for the detected plate
#hull = cv2.convexHull(contours[largest_rectangle_idx])
# This bounding rectangle does not consider rotation
license_plate = cv2.imread(file_name, cv2.CV_LOAD_IMAGE_COLOR)
bounding_rectangle = cv2.boundingRect(contours[largest_rectangle_idx])
b_rect_x, b_rect_y, b_rect_w, b_rect_h = bounding_rectangle
plate_rectangle = (b_rect_x, b_rect_y, b_rect_w, b_rect_h)
print "Plate rectangle is: ", plate_rectangle
cv2.rectangle(license_plate, (b_rect_x, b_rect_y), (b_rect_x + b_rect_w, b_rect_y + b_rect_h), (0, 255, 0), 2)
cv2.imwrite("%s_bounding_box.png" % fname_prefix, license_plate)
license_plate = cv2.imread(file_name, cv2.CV_LOAD_IMAGE_COLOR)
minarea_rectangle = cv2.minAreaRect(contours[largest_rectangle_idx])
minarea_box = cv2.cv.BoxPoints(minarea_rectangle)
minarea_box = np.int0(minarea_box)
cv2.drawContours(license_plate, [minarea_box], 0, (0,0,255), 2)
cv2.imwrite("%s_bounding_box_minarea.png" % fname_prefix, license_plate)
view raw alpr.py hosted with ❤ by GitHub

  1. Load an image using opencv and python (Line 33)
  2. Run mean shift segmentation (I haven't evaluated the effectiveness of this) and save the image.
  3. Use the segmented image as the camera image.
  4. Convert the camera image (defaults as a numpy array) into OpenCV Mat and an IPL image (CV2 can work with numpy arrays. Plain old cv requires an IPL or Mat object).
  5. Convert the camera image into grayscale.
  6. Conduct thresholding on the image (Line 76). This means that pixels that are above the threshold become white and below black. This process is also called binarization.
  7. Create a mask that will be used for blob extraction that covers the entire image; setting all the pixels to 1 means that the mask will be applied everywhere.
  8. Invert the binarized image. License plates are mostly white with a few characters in black. Inversion means that the plate becomes mostly black with a few white character strokes (Line 94).
  9. Blob detection parameters. We don't want very small or very large blobs.
  10. Do the actual blob detection (Line 115).
  11. Iterate over the grayscale image adding an RGB color << this is a bug
  12. Mark potential blobs as black in a white image (Line 131).
  13. Another threshold step << This is probably unnecessary.
  14. Dilation and erosion to clean up the image << probably unnecessary.
  15. Find contours over the black images.
  16. For each contour, try and estimate it's overall shape. triangle? rectangle? n-gon (Line 196)? after eliminating very small and very large contours.
  17. Draw a bounding box.
  18. Calculate an estimate of the aspect ratio. Plates have a W/H ratio of 1 to 5
  19. Off the cuff heuristic. Largest rectangle that has an acceptable aspect ratio and is a rectangle is probably a plate. This is not always true. You might much a window or a large rectangular object...
  20. For the selected plate candidate, draw a bounding rect
Here are the results:
Segmented image
Thresholded image
Grayscale conversion

Blobs on a white image
Effects of erosion on an image
Effects of dilation on an image

Plate candidates - from contours
Min area box over plate candidate
Bounding box over plate candidate
And here are more images from a few more tests in picassa: