Barcodes and QR codes are everywhere — product packaging, event tickets, warehouse shelves, restaurant menus. Reading them programmatically takes about 20 lines of Python if you pick the right libraries. OpenCV handles the image capture and drawing. pyzbar handles the actual decoding. Together they cover every common barcode symbology.
Install Dependencies#
You need three packages:
1
| pip install opencv-python pyzbar numpy
|
On Ubuntu/Debian, pyzbar depends on the system ZBar library:
1
| sudo apt install libzbar0
|
On macOS:
Scan Barcodes from an Image#
Start with the simplest case — read an image, find every barcode or QR code in it, and print the results.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| import cv2
from pyzbar.pyzbar import decode, ZBarSymbol
image = cv2.imread("sample.png")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
results = decode(gray)
for barcode in results:
data = barcode.data.decode("utf-8")
barcode_type = barcode.type
print(f"[{barcode_type}] {data}")
# Draw bounding box
points = barcode.polygon
if len(points) == 4:
pts = [(p.x, p.y) for p in points]
for i in range(4):
cv2.line(image, pts[i], pts[(i + 1) % 4], (0, 255, 0), 2)
else:
rect = barcode.rect
cv2.rectangle(
image,
(rect.left, rect.top),
(rect.left + rect.width, rect.top + rect.height),
(0, 255, 0),
2,
)
# Label the barcode
x, y = barcode.rect.left, barcode.rect.top - 10
cv2.putText(image, f"{barcode_type}: {data}", (x, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
cv2.imwrite("output.png", image)
print(f"Found {len(results)} code(s)")
|
The decode() function returns a list of Decoded objects. Each one has the raw bytes (.data), the symbology type (.type — things like QRCODE, EAN13, CODE128), and polygon coordinates for drawing. Converting the image to grayscale first improves detection reliability, especially with low-contrast images.
Filter by Barcode Type#
If you only care about QR codes, pass the symbols parameter to avoid false positives from other symbologies:
1
2
3
4
| from pyzbar.pyzbar import decode, ZBarSymbol
qr_only = decode(gray, symbols=[ZBarSymbol.QRCODE])
ean_only = decode(gray, symbols=[ZBarSymbol.EAN13, ZBarSymbol.EAN8])
|
This speeds up decoding too, since ZBar skips symbologies you don’t need.
Live Camera Scanner#
The real-world use case is scanning from a webcam in real time. This loop grabs frames, decodes any visible codes, draws boxes, and deduplicates results so you don’t log the same barcode fifty times per second.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| import cv2
from pyzbar.pyzbar import decode
cap = cv2.VideoCapture(0)
seen = set()
while True:
ret, frame = cap.read()
if not ret:
break
results = decode(frame)
for barcode in results:
data = barcode.data.decode("utf-8")
barcode_type = barcode.type
# Log new codes only
if data not in seen:
seen.add(data)
print(f"[NEW] [{barcode_type}] {data}")
# Draw bounding polygon
points = barcode.polygon
pts = [(p.x, p.y) for p in points]
n = len(pts)
for i in range(n):
cv2.line(frame, pts[i], pts[(i + 1) % n], (0, 255, 0), 3)
# Label
x, y = barcode.rect.left, barcode.rect.top - 10
cv2.putText(frame, data[:40], (x, max(y, 15)),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
cv2.imshow("Barcode Scanner", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
|
Press q to quit. The seen set prevents duplicate logging. In production you’d probably timestamp each scan and write to a database or CSV instead.
Save Scan Results to CSV#
For inventory or audit workflows, dump results to a file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| import csv
import cv2
from pyzbar.pyzbar import decode
from datetime import datetime
def scan_and_save(image_path, output_csv="scan_results.csv"):
image = cv2.imread(image_path)
results = decode(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY))
with open(output_csv, "a", newline="") as f:
writer = csv.writer(f)
for barcode in results:
row = [
datetime.now().isoformat(),
image_path,
barcode.type,
barcode.data.decode("utf-8"),
]
writer.writerow(row)
print(f"Saved: {row}")
return len(results)
count = scan_and_save("warehouse_shelf.png")
print(f"Logged {count} codes")
|
OpenCV Built-in QR Detector#
OpenCV 4.x ships with cv2.QRCodeDetector, which works without any extra libraries. It only handles QR codes (not barcodes), but it’s useful when you can’t install system-level ZBar.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| import cv2
image = cv2.imread("qr_sample.png")
detector = cv2.QRCodeDetector()
# Detect and decode a single QR code
data, points, straight_qrcode = detector.detectAndDecode(image)
if points is not None and data:
print(f"QR data: {data}")
points = points[0].astype(int)
for i in range(4):
cv2.line(image, tuple(points[i]), tuple(points[(i + 1) % 4]),
(255, 0, 0), 3)
# For multiple QR codes in one image (OpenCV 4.5.3+)
multi_detector = cv2.QRCodeDetectorAruco()
retval, decoded_info, points, straight_qrcode = multi_detector.detectAndDecodeMulti(image)
if retval:
for i, text in enumerate(decoded_info):
if text:
print(f"QR #{i + 1}: {text}")
cv2.imwrite("qr_detected.png", image)
|
The tradeoff: cv2.QRCodeDetector is QR-only and slightly less tolerant of skewed or damaged codes than ZBar. But it’s zero extra dependencies beyond OpenCV itself.
Handling Multiple Codes in One Image#
Dense images — like a shelf full of products — can contain dozens of codes. pyzbar.decode() already returns all of them. The key is handling overlapping bounding boxes and making the output readable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| import cv2
from pyzbar.pyzbar import decode
image = cv2.imread("multi_barcode.png")
results = decode(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY))
colors = [
(0, 255, 0), (255, 0, 0), (0, 0, 255),
(255, 255, 0), (0, 255, 255), (255, 0, 255),
]
for idx, barcode in enumerate(results):
color = colors[idx % len(colors)]
data = barcode.data.decode("utf-8")
rect = barcode.rect
cv2.rectangle(
image,
(rect.left, rect.top),
(rect.left + rect.width, rect.top + rect.height),
color, 2,
)
label_y = rect.top - 10 if rect.top > 20 else rect.top + rect.height + 20
cv2.putText(image, f"#{idx + 1} {data[:30]}", (rect.left, label_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
print(f"Detected {len(results)} codes in image")
cv2.imwrite("multi_output.png", image)
|
Each code gets a different color so you can visually match labels to boxes when codes are close together.
Common Errors and Fixes#
ImportError: Unable to find zbar shared library — pyzbar can’t find the ZBar C library. Install it at the system level: sudo apt install libzbar0 on Debian/Ubuntu, brew install zbar on macOS. On Windows, pyzbar bundles the DLL automatically, so this usually means a broken install — try pip install pyzbar --force-reinstall.
decode() returns an empty list — The image might be too blurry, too small, or have poor contrast. Try preprocessing: convert to grayscale, apply adaptive thresholding, or resize the image up. A quick fix:
1
2
3
4
5
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.resize(gray, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
results = decode(thresh)
|
cv2.QRCodeDetector().detectAndDecode() returns empty string — The built-in detector is pickier than ZBar. Make sure the QR code fills a reasonable portion of the frame. If the code is rotated more than about 45 degrees, the detector can miss it. Fall back to pyzbar for difficult angles.
Camera feed is slow or laggy — Decoding every frame is expensive. Skip frames or only decode every 3rd-5th frame:
1
2
3
4
5
6
7
8
9
10
11
| frame_count = 0
while True:
ret, frame = cap.read()
frame_count += 1
if frame_count % 3 != 0:
cv2.imshow("Scanner", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
continue
# decode only every 3rd frame
results = decode(frame)
|
UnicodeDecodeError when reading barcode data — Some barcodes contain binary data, not UTF-8 text. Use .data directly as bytes or decode with errors="replace":
1
| data = barcode.data.decode("utf-8", errors="replace")
|