Decoding Barcodes
Institute for Personal Robots in Education
(IPRE)
CS 1 with Robots
•
•
•
•
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
Aug 29 2007
49 bars total!
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
Height =
192
Middle =
Height / 2
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 average 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?
[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!
modeIdx = len(barWidths) // 2
narrowSize= barWidths[modeIdx]
#Go to the 3/4 point, find the size of a wide bar!
wideIdx = modeIdx + (modeIdx // 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:
Luckily I've already typed
#Use a dictionary!
the codes in for you, look on
code39dict = {
the website for the
'BwbWbwbwB': "1",
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
© Copyright 2025 Paperzz