Raspberry Pi Camera: Quét mã QR/Barcode tự động
Trung cấp1/8/2026- Tác giả: IoTlabs Maker

Raspberry Pi Camera: Quét mã QR/Barcode tự động

Dùng Raspberry Pi 4 + Camera Module v3 với thư viện pyzbar và OpenCV để quét tự động mã QR và các loại barcode phổ biến (EAN-13, Code128...) theo thời gian thực, log kết quả kèm thời gian quét.

Raspberry PiQR CodeBarcodepyzbarOpenCVCameraPython

Project này biến Raspberry Pi 4 và Camera Module v3 thành một đầu đọc mã QR/barcode thời gian thực, dùng thư viện pyzbar (bọc quanh ZBar) kết hợp OpenCV để giải mã.

Camera liên tục chụp khung hình, pyzbar quét toàn bộ khung tìm mọi vùng chứa mã (QR Code, EAN-13, Code128, v.v.), vẽ khung bao quanh mã tìm được và in nội dung giải mã ra console/log.

Một bộ lọc chống trùng (debounce) đảm bảo cùng một mã đứng yên trước camera không bị log lặp lại liên tục — chỉ log lại khi mã biến mất rồi xuất hiện lại, hoặc sau một khoảng thời gian tối thiểu.

Hướng dẫn chi tiết

Dùng Raspberry Pi 4 + Camera Module v3 với pyzbar và OpenCV để quét tự động mã QR và barcode phổ biến (EAN-13, Code128...) theo thời gian thực, chống trùng và log kết quả ra CSV.

1. Giới thiệu

Project này biến Raspberry Pi 4 và Camera Module v3 thành một đầu đọc mã QR/barcode thời gian thực, dùng thư viện pyzbar (bọc quanh thư viện giải mã ZBar) kết hợp OpenCV để lấy khung hình.

Camera liên tục chụp khung hình, pyzbar quét toàn bộ khung tìm mọi vùng chứa mã (QR Code, EAN-13, Code128, và nhiều định dạng khác mà ZBar hỗ trợ), vẽ khung bao quanh mã tìm được và in nội dung giải mã ra console/log.

Một bộ lọc chống trùng (debounce) đảm bảo cùng một mã đứng yên trước camera không bị log lặp lại liên tục — chỉ log lại khi mã biến mất rồi xuất hiện lại, hoặc sau một khoảng thời gian tối thiểu (mặc định 3 giây).

Đây là một pipeline nhẹ, không cần huấn luyện mô hình gì — pyzbar/ZBar là bộ giải mã tất định (deterministic), hoạt động tốt trong hầu hết điều kiện ánh sáng bình thường. Phù hợp làm trạm quét check-in, quét mã sản phẩm, hoặc mã QR truy cập trên Raspberry Pi.

2. Linh kiện cần có

Linh kiện

Số lượng

Giá tham khảo

Raspberry Pi 4 Model B (2/4/8GB RAM)

1

~1.500.000 – 2.200.000đ

Raspberry Pi Camera Module v3 (IMX708)

1

~700.000 – 900.000đ

Thẻ nhớ microSD 32GB (Class 10 trở lên, cài Raspberry Pi OS) — phụ kiện, không nằm trong sơ đồ điện

1

~150.000đ

Nguồn USB-C 5V/3A — phụ kiện, không nằm trong sơ đồ điện

1

~150.000đ

3. Sơ đồ kết nối

RPi Camera Module v3

Raspberry Pi 4

CSI (Ribbon connector)

CSI Camera Port

SDA (I2C control)

GPIO2 / SDA1 (Pin 3)

SCL (I2C control)

GPIO3 / SCL1 (Pin 5)

3V3 (Power)

3V3 (Pin 1/17)

GND

GND

Kết nối chính là cáp ribbon CSI 15-chân đi kèm camera — cắm thẳng vào cổng CSI trên Raspberry Pi 4 (nằm giữa cổng HDMI và jack audio).

Lưu ý chiều cáp: mặt có các đường tiếp xúc kim loại phải hướng về phía cổng HDMI; cắm ngược chiều là nguyên nhân phổ biến nhất khiến hệ thống báo "no cameras available". Mở khoá chốt nhựa trên đầu nối CSI trước khi lắp cáp, luồn cáp vào rồi ấn khoá lại chắc chắn.

Bốn dây SDA/SCL/3V3/GND chỉ áp dụng cho board có route thêm chân I2C điều khiển autofocus ra ngoài — nếu module của bạn chỉ có đầu nối ribbon CSI đơn thuần, bỏ qua phần này, chỉ cần cáp CSI là đủ.

4. Code ví dụ #1 — Kiểm tra camera hoạt động (Camera Self-Test)

Chạy script này đầu tiên sau khi lắp camera để xác nhận phần cứng nhận diện đúng trước khi chạy ứng dụng quét mã. Script in banner khởi động, thử mở camera qua picamera2, chụp thử vài khung hình trong 3 giây và báo FPS ước tính.

"""
camera_test.py
Kiem tra nhanh Camera Module v3 truoc khi chay bo quet QR/Barcode.
In banner khoi dong, mo camera qua picamera2, chup vai khung hinh, in
kich thuoc + FPS uoc tinh. Bao loi ro rang neu thieu picamera2 hoac
khong mo duoc phan cung, khong treo may.
"""

import sys
import time


def main() -> int:
    print("[BOOT] Raspberry Pi QR/Barcode Scanner - Camera Self-Test")
    print("[BOOT] Dang khoi tao picamera2...")

    try:
        from picamera2 import Picamera2
    except ImportError:
        print("[ERROR] Khong tim thay thu vien picamera2.")
        print("        Cai dat: sudo apt install -y python3-picamera2")
        return 1

    try:
        picam2 = Picamera2()
        config = picam2.create_preview_configuration(
            main={"size": (640, 480), "format": "RGB888"}
        )
        picam2.configure(config)
        picam2.start()
        time.sleep(1.0)
    except Exception as exc:  # noqa: BLE001
        print(f"[ERROR] Khong the khoi dong camera: {exc}")
        print("        Kiem tra: cap ribbon CSI cam dung chieu, dung chan,")
        print("        camera duoc bat trong raspi-config (Interface Options).")
        return 1

    frame_count = 0
    start = time.monotonic()
    test_duration_s = 3.0

    while time.monotonic() - start < test_duration_s:
        frame = picam2.capture_array()
        frame_count += 1
        if frame_count == 1:
            print(f"[OK] Khung hinh dau tien: shape={frame.shape}, dtype={frame.dtype}")

    elapsed = time.monotonic() - start
    fps = frame_count / elapsed if elapsed > 0 else 0.0
    print(f"[OK] Da chup {frame_count} khung hinh trong {elapsed:.2f}s (~{fps:.1f} FPS)")

    picam2.stop()
    print("[DONE] Camera hoat dong binh thuong.")
    return 0


if __name__ == "__main__":
    sys.exit(main())

5. Code ví dụ #2 — Ứng dụng chính: quét QR/Barcode thời gian thực (qr_barcode_scanner.py)

Vòng lặp chính: đọc khung hình từ camera, đưa cho pyzbar.decode() để tìm và giải mã mọi mã QR/barcode trong khung, áp dụng DedupeWindow để không log lặp cùng một mã đứng yên trước camera, ghi kết quả ra CSV qua ScanCsvLogger.

Script tuân thủ các quy tắc mô phỏng bắt buộc: banner khởi động, retry có giới hạn khi mở camera thất bại (không lặp vô hạn), bỏ qua khung hình lỗi thay vì crash, và in telemetry JSON định kỳ mỗi 5 giây.

"""
qr_barcode_scanner.py
Ung dung chinh: quet QR Code va cac loai barcode pho bien (EAN-13, Code128,
...) theo thoi gian thuc, dung Raspberry Pi 4 + Camera Module v3 + pyzbar +
OpenCV (chi dung OpenCV de ve khung bao, giai ma hoan toan boi pyzbar/ZBar).

Hanh vi mo phong quan sat duoc:
  - In banner ngay sau khi console san sang.
  - Retry co gioi han khi mo camera that bai (khong while vo han).
  - Khung hinh loi -> bo qua, khong crash.
  - Debounce chong log lap (scan_logger.DedupeWindow).
  - In telemetry JSON dinh ky.
"""

from __future__ import annotations

import json
import time
from typing import Optional

from scan_logger import DedupeWindow, ScanCsvLogger

FRAME_WIDTH = 640
FRAME_HEIGHT = 480
TELEMETRY_INTERVAL_S = 5.0
CAMERA_RETRY_INTERVAL_S = 3.0
CAMERA_MAX_RETRIES = 5
DEDUPE_WINDOW_S = 3.0
CSV_LOG_PATH = "qr_barcode_scans.csv"


def open_camera():
    from picamera2 import Picamera2

    for attempt in range(1, CAMERA_MAX_RETRIES + 1):
        try:
            picam2 = Picamera2()
            config = picam2.create_preview_configuration(
                main={"size": (FRAME_WIDTH, FRAME_HEIGHT), "format": "RGB888"}
            )
            picam2.configure(config)
            picam2.start()
            time.sleep(0.5)
            print(f"[BOOT] Camera san sang sau {attempt} lan thu.")
            return picam2
        except Exception as exc:  # noqa: BLE001
            print(f"[WARN] Lan thu {attempt}/{CAMERA_MAX_RETRIES} mo camera that bai: {exc}")
            if attempt < CAMERA_MAX_RETRIES:
                time.sleep(CAMERA_RETRY_INTERVAL_S)

    print("[ERROR] Khong mo duoc camera sau nhieu lan thu, dung chuong trinh.")
    return None


def scan_frame(frame):
    """Quet 1 khung hinh, tra ve list cac ket qua pyzbar. [] neu khong co ma."""
    from pyzbar.pyzbar import decode

    if frame is None or frame.size == 0:
        return []
    return decode(frame)


def build_telemetry(total_scanned: int, last_type: Optional[str]) -> str:
    payload = {
        "ts": time.strftime("%Y-%m-%dT%H:%M:%S"),
        "total_scanned": total_scanned,
        "last_code_type": last_type,
    }
    return json.dumps(payload, ensure_ascii=False)


def main() -> int:
    print("[BOOT] Raspberry Pi QR/Barcode Scanner khoi dong...")
    print(f"[BOOT] Do phan giai khung hinh: {FRAME_WIDTH}x{FRAME_HEIGHT}")

    try:
        import cv2  # noqa: F401 - dung de ve khung bao khi debug truc quan
    except ImportError:
        print("[ERROR] Thieu thu vien opencv-python. Cai dat: pip install opencv-python")
        return 1

    try:
        from pyzbar.pyzbar import decode  # noqa: F401
    except ImportError:
        print("[ERROR] Thieu thu vien pyzbar. Cai dat: pip install pyzbar")
        print("        Yeu cau them thu vien he thong: sudo apt install -y libzbar0")
        return 1

    picam2 = open_camera()
    if picam2 is None:
        return 1

    dedupe = DedupeWindow(window_s=DEDUPE_WINDOW_S)
    logger = ScanCsvLogger(CSV_LOG_PATH)

    total_scanned = 0
    last_type: Optional[str] = None
    last_telemetry_at = time.monotonic()

    try:
        while True:
            try:
                frame = picam2.capture_array()
            except Exception as exc:  # noqa: BLE001
                print(f"[WARN] Loi doc khung hinh, bo qua: {exc}")
                frame = None

            results = scan_frame(frame) if frame is not None else []

            for result in results:
                code_type = result.type
                try:
                    data = result.data.decode("utf-8", errors="replace")
                except Exception:  # noqa: BLE001
                    data = repr(result.data)

                if dedupe.should_log(code_type, data):
                    x, y, w, h = result.rect
                    logger.log(code_type, data, (x, y, w, h))
                    total_scanned += 1
                    last_type = code_type
                    print(f"[SCAN] type={code_type} data={data!r} box=({x},{y},{w},{h})")

            now = time.monotonic()
            if now - last_telemetry_at >= TELEMETRY_INTERVAL_S:
                print(build_telemetry(total_scanned, last_type))
                last_telemetry_at = now

            time.sleep(0.05)
    except KeyboardInterrupt:
        print("[STOP] Nhan Ctrl+C, dang dung...")
    finally:
        logger.close()
        picam2.stop()
        print(f"[DONE] Tong so ma da quet: {total_scanned}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

6. Code ví dụ #3 — Module hỗ trợ: chống trùng và ghi log (scan_logger.py)

Module này tách riêng hai tiện ích mà qr_barcode_scanner.py import: DedupeWindow (chỉ cho phép log lại cùng một nội dung mã sau khi qua một khoảng thời gian tối thiểu, tránh spam khi mã đứng yên nhiều khung hình liên tiếp) và ScanCsvLogger (ghi mỗi lần quét thành công ra file CSV kèm loại mã, nội dung, và toạ độ khung bao, tự tạo file + header nếu chưa có).

"""
scan_logger.py
Tien ich dung chung cho qr_barcode_scanner.py:
  - DedupeWindow: chan log lap lai cung mot noi dung ma trong khoang thoi
    gian toi thieu (debounce), tranh spam khi ma dung yen truoc camera.
  - ScanCsvLogger: ghi moi lan quet thanh cong ra file CSV (timestamp,
    loai ma, noi dung, vi tri khung bao).
"""

from __future__ import annotations

import csv
import os
import time
from typing import Dict, Tuple


class DedupeWindow:
    """Chi cho phep log lai cung 1 (type, data) sau khi qua window_s giay."""

    def __init__(self, window_s: float = 3.0) -> None:
        self.window_s = window_s
        self._last_seen: Dict[Tuple[str, str], float] = {}

    def should_log(self, code_type: str, data: str) -> bool:
        key = (code_type, data)
        now = time.monotonic()
        last = self._last_seen.get(key)
        if last is None or (now - last) >= self.window_s:
            self._last_seen[key] = now
            return True
        return False


class ScanCsvLogger:
    def __init__(self, csv_path: str) -> None:
        self.csv_path = csv_path
        is_new_file = not os.path.exists(csv_path)
        self._file = open(csv_path, "a", newline="", encoding="utf-8")
        self._writer = csv.writer(self._file)
        if is_new_file:
            self._writer.writerow(["timestamp_iso", "code_type", "data", "box_x", "box_y", "box_w", "box_h"])
            self._file.flush()

    def log(self, code_type: str, data: str, box) -> None:
        x, y, w, h = box
        timestamp_iso = time.strftime("%Y-%m-%dT%H:%M:%S")
        self._writer.writerow([timestamp_iso, code_type, data, x, y, w, h])
        self._file.flush()

    def close(self) -> None:
        self._file.close()

7. Lỗi thường gặp

Lỗi

Nguyên nhân

Cách sửa

"no cameras available" / camera không nhận

Cáp ribbon CSI cắm ngược chiều hoặc chưa cài chặt khoá chốt

Rút cáp, kiểm tra mặt tiếp xúc kim loại hướng về phía cổng HDMI, cắm lại và khoá chốt chắc chắn

ImportError: No module named pyzbar

Chưa cài thư viện pyzbar hoặc thiếu thư viện hệ thống libzbar

Cài bằng pip install pyzbarsudo apt install -y libzbar0

ImportError: No module named picamera2

Chưa cài thư viện picamera2

Cài bằng sudo apt install -y python3-picamera2

Mã không được nhận diện dù nằm trong khung hình

Mã quá nhỏ, mờ, phản chiếu ánh sáng gắt, hoặc góc chụp quá nghiêng

Đưa mã gần camera hơn hoặc tăng độ phân giải khung hình, tránh ánh sáng phản chiếu trực tiếp lên bề mặt mã, giữ mã vuông góc với ống kính

Một mã bị log lặp lại liên tục dù không di chuyển

Thời gian debounce (DEDUPE_WINDOW_S) quá ngắn so với tốc độ khung hình

Tăng giá trị DEDUPE_WINDOW_S trong qr_barcode_scanner.py (mặc định 3 giây)

FPS thấp khi có nhiều mã trong khung hình cùng lúc

pyzbar phải giải mã tuần tự từng mã, tốn CPU hơn khi số lượng mã tăng

Giảm độ phân giải khung hình, giới hạn số mã kỳ vọng trong một khung, hoặc tăng khoảng nghỉ giữa các vòng lặp

8. Tổng kết

Với Raspberry Pi 4, Camera Module v3, pyzbar và một vòng lặp Python gọn nhẹ, bạn đã có một trạm quét QR/barcode thời gian thực, tự động log kết quả ra CSV kèm timestamp.

Vì pyzbar/ZBar là bộ giải mã cố định (không phải machine learning), kết quả ổn định và có thể dự đoán được trong điều kiện ánh sáng và khoảng cách hợp lý — đây là điểm mạnh so với các phương pháp thị giác máy tính khác trong bài viết cùng series.

Bạn có thể mở rộng project này bằng cách gửi dữ liệu quét được lên MQTT/webhook, tích hợp với hệ thống chấm công, hoặc thêm màn hình nhỏ để hiển thị phản hồi tức thì cho người dùng.