You are on page 1of 63

Decoding Barcodes

Institute for Personal Robots in Education


CS 1 with Robots
(IPRE)
• Barcodes are designed to be machine readable
• They encode numbers and symbols using black and white
bars.
• The examples on this page are standard 1D barcodes
using the Code39 encoding scheme.
• Usually read by laser scanners, they can also be read
using a camera.

Aug 29 2007 2
• Code39 (Sometimes called 3 from 9) barcodes use 9 bars
to represent each symbol.
• The bars can be black or white.
• The bars are either narrow or wide.
• Wide bars must be 2.1 to 3 times larger than narrow bars.
• Each symbol pattern starts and ends with a black bar.
• A valid barcode starts and ends with the STAR (*) symbol,
which is used as a delimiter.

The STAR (*) symbol is made up of a


narrow black bar, a wide white bar, a
narrow black bar, a narrow white bar, a
wide black bar, a narrow white bar, a wide
black bar, a narrow white bar, and a
narrow black bar.

Aug 29 2007 3
• Code39 (Sometimes called 3 from 9) barcodes use 9 bars
to represent each symbol.
• The bars can be black or white.
• The bars are either narrow or wide.
• Wide bars must be 2.1 to 3 times larger than narrow bars.
• Each symbol pattern starts and ends with a black bar.
• A valid barcode starts and ends with the STAR (*) symbol,
which is used as a delimiter.

This could also be represented


as the string
“bWbwBwBwb”

Aug 29 2007 4
• How many bars is in a barcode that encodes 3 symbols?
• Although each symbol pattern starts and ends with a black
bar, patterns must be separated by a white bar (typically
narrow), so each symbol except the last is represented
with 10 bars in total. (The last symbol has 9 bars, and
does not need a separator after it.)
• Don't forget the Start and Stop symbol!

Aug 29 2007 5
• How many bars is in a barcode that encodes 3 symbols?
• Although each symbol pattern starts and ends with a black
bar, patterns must be separated by a white bar (typically
narrow), so each symbol except the last is represented
with 10 bars in total. (The last symbol has 9 bars, and
does not need a separator after it.)
• Don't forget the Start and Stop symbol!

• 3 symbols + start + stop = 5 symbols, at 9 bars each, plus


4 narrow white bars to separate the symbols is 9 * 5 + 4,
or 10*5 – 1 to make 49 bars total!

Aug 29 2007 6
All of the symbol patterns:

What symbol is on the


right?
Aug 29 2007 7
It's an “I”

What symbol is on the


right?
Aug 29 2007 8
But what does a barcode look like from the robot?

• The robot's camera has relatively low resolution (256x192


pixels).
• To decode a barcode successfully from an image, we
need multiple pixels for each bar . This means that we are
limited in the size of barcodes we can successfully use.
• Here is a picture of a two symbol (4 patterns total)
barcode taken with a (VERY) carefully aimed robot
camera:

Aug 29 2007 9
It's sort of messy!

• High contrast elements (black and white lines) generate


color artifacts due to the bayer filter layout in the camera.

Aug 29 2007 10
Step 1: Lets clean it up!

• Convert to black and white with a thresholding process!

Aug 29 2007 11
Step 1: Lets clean it up!

• Convert to black and white with a thresholding process!


• For each pixel, check to see if it's brighter than a threshold
(say, 127).
– If yes, set the color to white!
– If no, set the color to black!

Aug 29 2007 12
Threshold Code

Aug 29 2007 13
Threshold Code

def threshold(pic):
for i in getPixels(pic):
g = getGreen(i)
if( g < 127):
setRed(i,0)
setGreen(i,0)
setBlue(i,0)
else:
setRed(i,255)
setGreen(i,255)
setBlue(i,255)
return(pic)
Aug 29 2007 14
Threshold Code: How to improve it!

• Note that we are using the green value as a proxy for


the “brightness” (or luminance) of the pixel.
• To do this correctly, we should calculate the luminance
of the pixel with the following formula:

Y = 0.2126 * Red + 0.7152 * Green + 0.0722 * Blue

• Notice how the Green component makes up 70% of


the Luminance (Y) value?
• That is why it's almost OK to cheat and just use the
green channel!

Aug 29 2007 15
Now what?

• We have a thresholded image, now we have to scan


across it to look for bars.
• Lets start out with a simpler task, just scan across it and
save a list of the pixel values (white=255 or black=0) in a
list.
• But where do we scan?

Aug 29 2007 16
Now what?

• We have a thresholded image, now we have to scan


across it to look for bars.
• Lets start out with a simpler task, just scan across it and
save a list of the pixel values (white=255 or black=0) in a
list.
• But where do we scan?
• How about the middle?
• How do you find the middle of the image?

Aug 29 2007 17
Our Image:

X=255
X=0 Width = 256
Y=0

Height =
192

Y=191
Aug 29 2007 18
Our Image: Middle

X=255
X=0 Width = 256
Y=0

Middle =
Height = Height / 2
192

Y=191
Aug 29 2007 19
Code to save pixel values along a horizontal scanline:

def makeScanLine(bwPic):

return(values)
Aug 29 2007 20
Code to save pixel values along a horizontal scanline:

def makeScanLine(bwPic):
height = getHeight(bwPic)
mid = height // 2
width = getWidth(bwPic)
values = []
for x in range(0, width):
pix = getPixel(bwPic, x, mid)
val = getGreen(pix)
values.append(val)

return(values)
Aug 29 2007 21
Example Scanline Data:

[0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255,
255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255,
255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255,
255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255,
255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0,
0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255,
255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255,
255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0,
0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255]

Note the large runs of white at the beginning and end of the barcode!

Aug 29 2007 22
How to improve our data?

• Scanline data presents the raw pixel data, but it's not very
easy to understand.
• Lets scan for “runs” of pixels of the same color.
• Convert this:
[0,0, 255,255,255,255,255, 0,0, 255,255, 0,0,0,0, 255,255,
0,0,0,0, 255,255, 0,0,0,0]

Aug 29 2007 23
How to improve our data?

• Scanline data presents the raw pixel data, but it's not very
easy to understand.
• Lets scan for “runs” of pixels of the same color.
• Convert this:
[0,0, 255,255,255,255,255, 0,0, 255,255, 0,0,0,0, 255,255,
0,0,0,0, 255,255, 0,0,0,0]
to this:
[ (2,0), (5,255), (2,0), (2,255), (4,0), (2,255), (4,0), (2,255),
(2,0)]

Aug 29 2007 24
How to improve our data?

• Scanline data presents the raw pixel data, but it's not very
easy to understand.
• Lets scan for “runs” of pixels of the same color.
• Convert this:
[0,0, 255,255,255,255,255, 0,0, 255,255, 0,0,0,0, 255,255,
0,0,0,0, 255,255, 0,0,0,0]
to this:
[ (2,0), (5,255), (2,0), (2,255), (4,0), (2,255), (4,0), (2,255),
(2,0)]
Which could be read as:
“bWbwBwBwb”

Aug 29 2007 25
Code to spot runs of the same color

def parseScanline(scanLine):

return(barData)

Aug 29 2007 26
Code to spot runs of the same color

def parseScanline(scanLine):
barData = []
previous = scanLine[0]
length = 0

for element in scanLine:


if (element != previous): #a change has occured!
myTuple = (length, previous)
barData.append( myTuple ) #add run info to barData list
length = 1
previous = element
else:
#No change.
length = length + 1

Aug 29 2007 27
Don't forget to record the last run!

def parseScanline(scanLine):
barData = []
previous = scanLine[0]
length = 0

for element in scanLine:


if (element != previous): #a change has occured!
myTuple = (length, previous)
barData.append( myTuple ) #add run info to barData list
length = 1
previous = element
else:
#No change.
length = length + 1

#Rescue the last bit of data stored in the previous


# and length variables!
myTuple = (length, previous)
barData.append( myTuple )
return(barData)
Aug 29 2007 28
Some real data!

• Actual scanline data:


[ (2, 0), (26, 255), (3, 0), (8, 255), (3, 0), (3, 255), (8, 0),
(4, 255), (8, 0), (4, 255), (2, 0), (4, 255), (8, 0), (4, 255),
(4, 0), (8, 255), (2, 0), (4, 255), (4, 0), (4, 255), (8, 0),
(2, 255), (1, 0), (1, 255), (6, 0), (4, 255), (4, 0), (8, 255),
(8, 0), (4, 255), (2, 0), (4, 255), (4, 0), (4, 255), (2, 0),
(8, 255), (4, 0), (4, 255), (8, 0), (4, 255), (7, 0), (3, 255),
(4, 0), (39, 255) ]

• Can you spot the narrow and wide bars?

Aug 29 2007 29
Some real data!

• Actual scanline data:


[ (2, 0), (26, 255), (3, 0), (8, 255), (3, 0), (3, 255), (8, 0),
(4, 255), (8, 0), (4, 255), (2, 0), (4, 255), (8, 0), (4, 255),
(4, 0), (8, 255), (2, 0), (4, 255), (4, 0), (4, 255), (8, 0),
(2, 255), (1, 0), (1, 255), (6, 0), (4, 255), (4, 0), (8, 255),
(8, 0), (4, 255), (2, 0), (4, 255), (4, 0), (4, 255), (2, 0),
(8, 255), (4, 0), (4, 255), (8, 0), (4, 255), (7, 0), (3, 255),
(4, 0), (39, 255) ]

• Narrow bars look to be around 3-4 pixels in size, and


wide bars appear to be around 6-8 pixels in size!

Aug 29 2007 30
Some real data! With real-world problems!

• Wait! What's that black bar doing at the front of our image
(2,0) before all that white space (26,255)?

• Actual scanline data:


[ (2, 0), (26, 255), (3, 0), (8, 255), (3, 0), (3, 255), (8, 0),
(4, 255), (8, 0), (4, 255), (2, 0), (4, 255), (8, 0), (4, 255),
(4, 0), (8, 255), (2, 0), (4, 255), (4, 0), (4, 255), (8, 0),
(2, 255), (1, 0), (1, 255), (6, 0), (4, 255), (4, 0), (8, 255),
(8, 0), (4, 255), (2, 0), (4, 255), (4, 0), (4, 255), (2, 0),
(8, 255), (4, 0), (4, 255), (8, 0), (4, 255), (7, 0), (3, 255),
(4, 0), (39, 255) ]

Aug 29 2007 31
Some real data!

• Wait! What's that black bar doing at the front of our image
(2,0) before all that white space (26,255)?

• Actual scanline data:


[ (2, 0), (26, 255), (3, 0), (8, 255), (3, 0), (3, 255), (8, 0),
(4, 255), (8, 0), (4, 255), (2, 0), (4, 255), (8, 0), (4, 255),
(4, 0), (8, 255), (2, 0), (4, 255), (4, 0), (4, 255), (8, 0),
(2, 255), (1, 0), (1, 255), (6, 0), (4, 255), (4, 0), (8, 255),
(8, 0), (4, 255), (2, 0), (4, 255), (4, 0), (4, 255), (2, 0),
(8, 255), (4, 0), (4, 255), (8, 0), (4, 255), (7, 0), (3, 255),
(4, 0), (39, 255) ]

Zoom in:
Aug 29 2007 32
Some real data!

• The robot's camera has a bug! It produces two columns of


black pixels on the left of every image!

• But no problems! We'll just make sure that our barcode


parsing code can handle random bars before the barcode
officially starts!

Zoom in:
Aug 29 2007 33
Another problem!

• Wait! What are those single pixel black and white bars
doing in the middle of our image?
• Actual scanline data:
[ (2, 0), (26, 255), (3, 0), (8, 255), (3, 0), (3, 255), (8, 0),
(4, 255), (8, 0), (4, 255), (2, 0), (4, 255), (8, 0), (4, 255),
(4, 0), (8, 255), (2, 0), (4, 255), (4, 0), (4, 255), (8, 0),
(2, 255), (1, 0), (1, 255), (6, 0), (4, 255), (4, 0), (8, 255),
(8, 0), (4, 255), (2, 0), (4, 255), (4, 0), (4, 255), (2, 0),
(8, 255), (4, 0), (4, 255), (8, 0), (4, 255), (7, 0), (3, 255),
(4, 0), (39, 255) ]

Aug 29 2007 34
Another problem!

• Wait! What are those single pixel black and white bars
doing in the middle of our image?
• Actual scanline data:
[ (2, 0), (26, 255), (3, 0), (8, 255), (3, 0), (3, 255), (8, 0),
(4, 255), (8, 0), (4, 255), (2, 0), (4, 255), (8, 0), (4, 255),
(4, 0), (8, 255), (2, 0), (4, 255), (4, 0), (4, 255), (8, 0),
(2, 255), (1, 0), (1, 255), (6, 0), (4, 255), (4, 0), (8, 255),
(8, 0), (4, 255), (2, 0), (4, 255), (4, 0), (4, 255), (2, 0),
(8, 255), (4, 0), (4, 255), (8, 0), (4, 255), (7, 0), (3, 255),
(4, 0), (39, 255) ]

Zoom in:
Aug 29 2007 35
Another problem!

• We need to remove those single pixel errors!

Zoom in:
Aug 29 2007 36
Code to remove single pixel errors:

• Remove single pixel errors!


• Example data:
[ (4, 255), (8, 0), (2, 255), (1, 0), (1, 255), (6, 0), (4, 255) ]

• We want:
[ (4, 255), (8, 0), (2, 255), (6, 0), (4, 255) ]

Aug 29 2007 37
Code to remove single pixel errors:

def removeSingles(barData):

return(newBarData)

Aug 29 2007 38
Code to remove single pixel errors:

def removeSingles(barData):
newBarData =[]
for item in barData:
length = item[0]
if (length != 1):
newBarData.append(item)
return(newBarData)

Aug 29 2007 39
Good data, but how to find Wide and Narrow bars?

[(2, 0), (26, 255), (3, 0), (8, 255), (3, 0), (3, 255), (8, 0), (4, 255), (8, 0), (4, 255),
(2, 0), (4, 255), (8, 0), (4, 255), (4, 0), (8, 255), (2, 0), (4, 255), (4, 0), (4, 255),
(8, 0), (2, 255), (6, 0), (4, 255), (4, 0), (8, 255), (8, 0), (4, 255), (2, 0), (4, 255),
(4, 0), (4, 255), (2, 0), (8, 255), (4, 0), (4, 255), (8, 0), (4, 255), (7, 0), (3, 255),
(4, 0), (255, 39)]

How do we pick the threshold that


separates wide from narrow bars?
Look at just the widths:

[2, 26, 3, 8, 3, 3, 8, 4, 8, 4, 2, 4, 8, 4, 4, 8,
2, 4, 4, 4, 8, 2, 6, 4, 4, 8, 8, 4, 2, 4, 4, 4, 2,
8, 4, 4, 8, 4, 7, 3, 4, 39]

Aug 29 2007 40
Good data, but how to find Wide and Narrow bars?

SORT the widths:


[2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 7, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 26, 39]

Eyeball it! What would make a good


threshold?

Aug 29 2007 41
Good data, but how to find Wide and Narrow bars?

SORT the widths:


[2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 7, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 26, 39]

Eyeball it! A 5 or 6 would make a good


threshold! But how does the computer
figure that out?

Aug 29 2007 42
Good data, but how to find Wide and Narrow bars?

SORT the widths:


[2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 7, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 26, 39]

What is the median width?


[2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 7, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 26, 39]
Aug 29 2007 43
Good data, but how to find Wide and Narrow bars?

What is the median width?


[2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 7, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 26, 39]

How about ¾ of the way up the list?


[2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 7, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 26, 39]
Aug 29 2007 44
Good data, but how to find Wide and Narrow bars?

Pick a threshold halfway between the


median (4 = narrow bar size) and the ¾
point ( 8 = wide bar size):
(8 – 4) / 2 = 2
( 2 bigger than median value is 6!)
4+2=6
Anything 6 pixels or larger is a wide bar!

Aug 29 2007 45
Code to find the width threshold:

def calculateWidthThreshold(barData):

return( threshold )

Aug 29 2007 46
Code to find the width threshold:

def calculateWidthThreshold(barData):
#Load just the widths!
barWidths = []
for x in barData:
barWidths.append(x[0])
barWidths.sort()
#Find the size of a narrow bar!
medianIdx = len(barWidths) // 2
narrowSize= barWidths[medianIdx]
#Go to the 3/4 point, find the size of a wide bar!
wideIdx = medianIdx + (medianIdx // 2)
wideSize = barWidths[wideIdx]
#Calculate the threshold
dist = (wideSize – narrowSize) // 2
threshold = narrowSize + dist
return( threshold )

Aug 29 2007 47
Decoding the bars!

• Now that we know the width threshold, we can convert our


barData into a string representing the barcode! (made up
of the letters {b,B,w,W})
• For example:
barData = [ (4, 255), (8, 0), (7, 255), (3, 0) ]
should produce a string like this:
“wBWb”
(narrow white, wide Black, white White, narrow black)

Aug 29 2007 48
Decoding the bars!

def decodeBars(barData,widthThreshold):

return(barString)
Aug 29 2007 49
Decoding the bars!

def decodeBars(barData,widthThreshold):
barString = ""
for bar in barData:
if(bar[1] == 255): #It's a white bar!
if(bar[0] >= widthThreshold):
#It's a wide white bar!
barString = barString + "W"
else:
#It's a narrow white bar
barString = barString + "w"
else: #It's a black bar!
if(bar[0] >= widthThreshold):
#It's a wide black bar!
barString = barString + "B"
else:
#it's a narrow black bar!
barString = barString + "b"

return(barString)
Aug 29 2007 50
Parsing the barcode string

• Actual barString:
bWbWbwBwBwbwBwbWbwbwBwBwbWBwbw
bwbWbwBwBwbW
• Now we just have to parse this string to find our
barcode!
• All (valid) barcodes start with the pattern:
“bWbwBwBwb” or the “*” symbol
• Lets go looking for it!

Aug 29 2007 51
Parsing the barcode string

• There it is!

bWbWbwBwBwbwBwbWbwbwBwBwbWBw
bwbwbWbwBwBwbW

Aug 29 2007 52
Parsing the barcode string

• Now, a white bar will separate the start symbol


from the first data symbol!

bWbWbwBwBwbwBwbWbwbwBwBwbWBw
bwbwbWbwBwBwbW

Aug 29 2007 53
Parsing the barcode string

• The second symbol is 9 bars long, and is also


followed by a white bar!

BwbWbwBwBwbwBwbWbwbwBwBwbWBw
bwbwbWbwBwBwbW
• So if we look up “BwbWbwbwB” we can figure
out what our first symbol is!

Aug 29 2007 54
All of the symbol patterns:

How do we get all those


symbols into our code to do
the lookup?
Aug 29 2007 55
All of the symbol patterns:

#Use a dictionary! Luckily I've already typed


code39dict = { the codes in for you, look on
'BwbWbwbwB': "1", the website for the
code39dict.py file!
'bwBWbwbwB': "2",
'BwBWbwbwb': "3",
'bwbWBwbwB': "4",
'BwbWBwbwb': "5",
...
'BwbwbWbwB': "A",
'bwBwbWbwB': "B",
'BwBwbWbwb': "C",
...
'bWbwBwBwb': "*", #Start/Stop character
}
Aug 29 2007 56
Parsing the barcode string

code39dict = {
'BwbWbwbwB': "1",
}

answer = code39dict[“BwbWbwbwB”]
print answer
“1”
Our first symbol is a 1!

Aug 29 2007 57
Parsing the barcode string

• Each symbol is 9 bars long, and separated by a


white bar!

...wBwbWbwbwBwBwbWBwbwbwbWbwBwB
wbW
• Our second symbol is a “5”

Aug 29 2007 58
Parsing the barcode string

• The second symbol is 9 bars long, and is also


followed by a white bar!
...BwbWBwbwbwbWbwBwBwbW
• And our last symbol should look familiar,
because it is the “*” or Start/Stop symbol, and
is the same as our first symbol!
• Note that the large white area after the barcode
is represented by a single W.
• All together, our barcode reads: “*15*”
• We don't report the *'s, so our number is 15

Aug 29 2007 59
Code to find the start symbol!

def findCode39(barString):

Aug 29 2007 60
Code to find the start symbol!

def findCode39(barString):

#Search for a start code!


startLoc = barString.find("bWbwBwBwb")
if(startLoc == -1): #No start character found
return(None)
#Beginning of first data symbol...
#each code is 9 bars long,
#plus one bar to separate them!
startLoc = startLoc + 10
#Initialize a variable to store our code
codeData = “”
Aug 29 2007 61
Code to read each symbol!

while( startLoc < len(barString)):


code = barString[startLoc:startLoc+9]
letter = code39dict.get(code,-1)
if (letter == -1): #Invalid code!
return(None)
else: #Valid code!
if(letter == '*'): #Found end of barcode
return(codeData) #Return the data!
else: #Add letter to our codeData
codeData = codeData + letter
#We advance by 10 to the next code symbol
startLoc = startLoc + 10
#did not find a stop code! Abort!
return(None)
Aug 29 2007 62
Barcodes – Not that hard after all!

Aug 29 2007 63

You might also like