Retail checkout is unforgiving: a scanner that misreads a UPC, chokes on a wrinkled GS1-128 logistics label, or stalls on a loyalty QR code slows every lane in the store. Choosing the right barcode reader means matching the symbologies, platform, and decode reliability that a point-of-sale (POS) terminal actually needs. This tutorial walks through those decision criteria and then proves them out with a desktop Python scanner built with PySide6 and the Dynamsoft Barcode Reader SDK.
What you'll build: A desktop POS-style scanner app in Python and PySide6 that reads every retail symbology — EAN-13, UPC-A, EAN-8, Code 128, GS1-128, and QR — from a live camera or an image file, drawing live overlays and building a deduplicated receipt with the Dynamsoft Barcode Reader Bundle.
Demo Video: Scanning a Self-Checkout Frame
Open a saved checkout frame or point a webcam at the shelf — the app boxes every barcode and lists each decoded item in a receipt panel:
How to Choose a Barcode Reader for Retail Checkout
Before writing code, evaluate a reader against four checkout-specific criteria.
Symbology Coverage
Retail product codes are almost always EAN-13, UPC-A, EAN-8, or UPC-E. Beyond the till, you also meet GS1 DataBar (produce, coupons, pharmacy), Code 128 / GS1-128 (cases, weight/batch data), and increasingly QR codes for mobile loyalty and digital coupons. A reader that only handles QR — or only handles 1D codes — will fail somewhere in the store.
Platform Fit
POS terminals and self-checkout kiosks run on desktop operating systems, so the decoding engine has to be a native desktop/server SDK rather than a mobile-only component. Dynamsoft ships the same engine across Python, C++, .NET, Java, and JavaScript, so a PySide6 prototype on desktop maps cleanly to a production C++ or .NET till.
Multi-Code Decoding
Self-checkout cameras and "scan-as-you-bag" stations capture several items at once. The reader must return every barcode in a frame — with its location — not just the first one it finds.
Decode Reliability
Real labels are wrinkled, glare-lit, rotated, and partially occluded. Production-grade engines apply localization, deblurring, and grayscale enhancement automatically — features a hand-rolled or open-source decoder rarely matches.
The app below satisfies all four with the Dynamsoft Barcode Reader Bundle.
Prerequisites
- Python 3.8+
-
Dynamsoft Barcode Reader Bundle
11.2.5000(pip install dynamsoft-barcode-reader-bundle) - PySide6, opencv-python, and numpy for the desktop UI and camera
-
python-barcode,qrcode, andPillowto synthesize the sample image set - A Dynamsoft license key
Get a 30-day free trial license at dynamsoft.com/customer/license/trialLicense
Install everything:
pip install dynamsoft-barcode-reader-bundle==11.2.5000 PySide6 opencv-python numpy python-barcode qrcode Pillow
Step 1: Generate a Realistic Retail Image Set
To test without a physical scanner, the project synthesizes product "packages" — a colored card with a name band and a barcode panel — and scatters them on a conveyor-belt background, the way an overhead self-checkout camera sees them. The catalog mixes every symbology a checkout meets:
# (filename, product label, symbology, value, card color)
CATALOG = [
("cereal_box", "Morning Oat Cereal 500g", "ean13", "4006381333931", (255, 214, 102)),
("soda_can", "Cola Classic 330ml", "upca", "036000291452", (214, 69, 65)),
("gum_pack", "Mint Gum", "ean8", "96385074", (120, 200, 160)),
("milk_carton", "Whole Milk 1L", "ean13", "5012345678900", (235, 238, 245)),
("shipping_label", "Backroom Carton", "code128", "SKU-7741-CASE24", (206, 178, 140)),
("weighed_produce", "Bananas (GS1-128)", "gs1_128", "0109501101530003", (245, 224, 120)),
]
Run it to produce images/products/*.png and images/checkout-belt.png:
python generate_retail_images.py
Step 2: Configure the SDK for Retail Symbologies
The decoding engine lives in a small RetailBarcodeEngine class. It initializes the license once, then restricts decoding to retail formats so the reader skips irrelevant work and avoids false reads. Setting expected_barcodes_count = 0 tells it to return every code in a frame:
RETAIL_FORMATS = (
EnumBarcodeFormat.BF_ONED
| EnumBarcodeFormat.BF_GS1_DATABAR
| EnumBarcodeFormat.BF_QR_CODE
)
class RetailBarcodeEngine:
"""Wraps a CaptureVisionRouter configured for retail symbologies."""
def __init__(self, license_key):
err_code, err_str = LicenseManager.init_license(license_key)
if err_code != EnumErrorCode.EC_OK and err_code != EnumErrorCode.EC_LICENSE_WARNING:
raise RuntimeError(f"License initialization failed: {err_str}")
self.cvr = CaptureVisionRouter()
err_code, err_str, settings = self.cvr.get_simplified_settings(
EnumPresetTemplate.PT_READ_BARCODES)
settings.barcode_settings.barcode_format_ids = RETAIL_FORMATS
settings.barcode_settings.expected_barcodes_count = 0
self.cvr.update_settings(EnumPresetTemplate.PT_READ_BARCODES, settings)
Step 3: Decode a Frame and Capture Barcode Locations
A POS UI needs both the text and the position of every code so it can draw overlays. decode_bgr accepts an OpenCV frame, wraps it in an ImageData, and returns the format, text, and corner points for each barcode:
def decode_bgr(self, frame):
"""Decode a BGR numpy frame; return a list of {format, text, points}."""
frame = np.ascontiguousarray(frame)
image_data = ImageData(
frame.tobytes(), frame.shape[1], frame.shape[0],
frame.strides[0], EnumImagePixelFormat.IPF_RGB_888,
)
result = self.cvr.capture(image_data, EnumPresetTemplate.PT_READ_BARCODES)
barcode_result = result.get_decoded_barcodes_result()
items = []
if barcode_result is not None:
for item in barcode_result.get_items():
location = item.get_location()
points = [(p.x, p.y) for p in location.points]
items.append({
"format": item.get_format_string(),
"text": item.get_text(),
"points": points,
})
return items
The same decode_bgr powers both the image-file mode and the live camera, so there is a single decode path to maintain.
Step 4: Draw Live Overlays Over Each Barcode
With the corner points in hand, drawing a bounding box and a symbology label is a few lines of OpenCV:
def draw_overlays(frame, items):
"""Draw green quadrilaterals and labels over each decoded barcode."""
annotated = frame.copy()
for item in items:
pts = np.array(item["points"], dtype=np.int32)
cv2.polylines(annotated, [pts], True, (0, 200, 0), 3)
x, y = pts[0]
cv2.putText(annotated, item["format"], (int(x), int(y) - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 200, 0), 2)
return annotated
Step 5: Stream the Camera on a Background Thread
Decoding every frame on the UI thread would freeze the window, so a QThread grabs frames, decodes them, and emits the annotated image plus the results back to the GUI:
class CameraWorker(QThread):
"""Grabs camera frames, decodes each one, and emits annotated results."""
frame_ready = Signal(QImage, list)
def __init__(self, engine, cam_index=0):
super().__init__()
self.engine = engine
self.cam_index = cam_index
self._running = False
def run(self):
cap = cv2.VideoCapture(self.cam_index)
if not cap.isOpened():
return
self._running = True
while self._running:
ok, frame = cap.read()
if not ok:
break
items = self.engine.decode_bgr(frame)
annotated = draw_overlays(frame, items)
self.frame_ready.emit(bgr_to_qimage(annotated), items)
cap.release()
def stop(self):
self._running = False
self.wait()
Step 6: Build the POS Receipt and Wire Up the UI
The main window shows the live preview on the left and an accumulating, deduplicated "receipt" table on the right. Both Open Image and Start Camera feed the same add_to_receipt method:
def add_to_receipt(self, item):
key = (item["format"], item["text"])
if key in self.seen:
return
self.seen.add(key)
row = self.table.rowCount()
self.table.insertRow(row)
fmt_item = QTableWidgetItem(item["format"])
fmt_item.setForeground(QColor("#1d8a4a"))
self.table.setItem(row, 0, fmt_item)
self.table.setItem(row, 1, QTableWidgetItem(item["text"]))
self.count_label.setText(f"{len(self.seen)} unique items")
Run the app:
python retail_scanner_gui.py
Click Open Image and choose images/checkout-belt.png to decode all seven barcodes in one frame, or click Start Camera to scan real products live. Each unique code lands in the receipt exactly once.
Source Code
https://github.com/yushulx/python-barcode-qrcode-sdk/tree/main/examples/official/retail-checkout


Top comments (0)