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:

1
brew install zbar

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 librarypyzbar 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")