Real-Time Barcode and QR Code Scanning with Webcam, OpenCV, and Python

Dynamsoft is the only provider of an enterprise-class Python Barcode and QR Code SDK for Windows, Linux, macOS, and Raspberry Pi OS. The SDK allows developers to quickly build robust command-line, web, and desktop applications that can scan barcodes and QR codes from a wide range of sources. In this article, we will demonstrate how to use Dynamsoft Barcode Reader, OpenCV, and a webcam to create a cross-platform desktop barcode and QR code scanner in Python.

Download the Required SDKs

  • OpenCV

    Used for accessing the webcam and stitching images.

      pip install opencv-python
    
  • Dynamsoft Barcode Reader

    Used for decoding barcodes and QR codes from images.

      pip install dbr
    

License Activation

Obtain a desktop license key from Dynamsoft to activate the Dynamsoft Barcode Reader:

from dbr import *
BarcodeReader.init_license("LICENSE-KEY")

Building a Barcode and QR Code Scanner in Python

Python’s Global Interpreter Lock (GIL) can be a performance bottleneck for multi-threaded applications. For CPU-intensive tasks like barcode and QR code detection, it’s recommended to use Python’s multiprocessing library. You can refer to OpenCV’s video_threaded.py sample for guidance.

Steps to Build the Scanner:

  1. Import the necessary packages:

     import numpy as np
     import cv2 as cv
        
     from multiprocessing.pool import ThreadPool
     from collections import deque
        
     import dbr
     from dbr import *
    
  2. Activate the license and initialize the barcode reader:

     BarcodeReader.init_license("LICENSE-KEY")
     reader = BarcodeReader()
    
  3. Create a thread pool for processing frames:

     threadn = 1 # cv.getNumberOfCPUs()
     pool = ThreadPool(processes = threadn)
     barcodeTasks = deque()
    
  4. Define a function to process video frames:

     def process_frame(frame):
         results = None
         try:
             results = reader.decode_buffer(frame)
         except BarcodeReaderError as bre:
             print(bre)
            
         return results
    
     while True:
         ret, frame = cap.read()
         while len(barcodeTasks) > 0 and barcodeTasks[0].ready():
             results = barcodeTasks.popleft().get()
             if results != None:
                 for result in results:
                     points = result.localization_result.localization_points
                     cv.line(frame, points[0], points[1], (0,255,0), 2)
                     cv.line(frame, points[1], points[2], (0,255,0), 2)
                     cv.line(frame, points[2], points[3], (0,255,0), 2)
                     cv.line(frame, points[3], points[0], (0,255,0), 2)
                     cv.putText(frame, result.barcode_text, points[0], cv.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255))
    
         if len(barcodeTasks) < threadn:
             task = pool.apply_async(process_frame, (frame.copy(), ))
             barcodeTasks.append(task)
    
         cv.imshow('Barcode & QR Code Scanner', frame)
         ch = cv.waitKey(1)
         if ch == 27:
             break
    
  5. Run the barcode and QR code scanner:

    Python barcode and QR code reader

Dynamsoft Barcode Reader can detect multiple barcodes and QR codes from a single image. However, image quality affects detection accuracy. As shown in the above image, to capture all barcodes and QR codes, you may need to increase the camera’s depth of field, which can make the codes too small to read. To solve this, move the camera closer to get high-quality images for scanning, then use OpenCV’s stitching API to merge multiple images into a panorama.

Experiment: Image Stitching and Scanning Multiple QR Codes in a Batch

Stitching Multiple Barcode and QR Code Images into a Panorama

OpenCV includes a stitching.py sample that demonstrates how to use the stitching API.

Steps to Implement Panorama Stitching:

  1. Initialize the stitcher object:

     modes = (cv.Stitcher_PANORAMA, cv.Stitcher_SCANS)
     stitcher = cv.Stitcher.create(modes[1])
     stitcher.setPanoConfidenceThresh(0.5)
    
  2. Define a function to stitch images:

     panoramaPool = ThreadPool(processes = threadn)
     panoramaTask = deque()
    
     def stitch_frame(self, frame):
     	try:
     		results = self.reader.decode_buffer(frame)
     		if results != None:
     			for result in results:
     				points = result.localization_result.localization_points
     				cv.line(frame, points[0], points[1], (0,255,0), 2)
     				cv.line(frame, points[1], points[2], (0,255,0), 2)
     				cv.line(frame, points[2], points[3], (0,255,0), 2)
     				cv.line(frame, points[3], points[0], (0,255,0), 2)
     				cv.putText(frame, result.barcode_text, points[0], cv.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255))
        
     			self.panorama.append((frame, len(results)))
     			print('Stitching .............')
     			try:
     				all_images = [frame for frame, count in self.panorama]
     				status, image = self.stitcher.stitch(all_images)
        
     				if status != cv.Stitcher_OK:
     					print("Can't stitch images, error code = %d" % status)
     					return self.panorama[0][0]
     				else:
     					# Stop stitching if the output image is out of control
     					if image.shape[0] >= frame.shape[0] * 1.5:
     						self.isPanoramaDone = True
     						self.save_frame(all_images[0])
     						print('Stitching is done.............')
     						return None
        
     					# Drop the stitched image if its quality is not good enough
     					total = 0
     					for frame, count in self.panorama:
     						total += count
        
     					count_stitch = self.count_barcodes(image)
     					if count_stitch > total or count_stitch < self.panorama[0][1]:
     						return self.panorama[0][0]
        
     					# Wait for the next stitching and return the current stitched image
     					self.panorama = [(image, count_stitch)]
     					return image
     			except Exception as e:
     				print(e)
     				return None
        				
     	except BarcodeReaderError as e:
     		print(e)
     		return None
        
     	return None
    
     while len(panoramaTask) > 0 and panoramaTask[0].ready():
         image = panoramaTask.popleft().get()
         if image is not None:
             cv.imshow('panorama', image)
                
     if len(panoramaTask) < threadn:
         task = panoramaPool.apply_async(self.stitch_frame, (frame_cp, ))
         panoramaTask.append(task)
    
  3. Run the code to see the panorama stitching result.

    Python barcode and QR code reader with panorama stitching

Barcode and QR Code Based Image Concatenation for Automated Inventory Management

Barcodes and QR codes are widely used in modern warehouse inventory management. A robot equipped with a camera can scan barcodes and QR codes to check product status and inventory. In the event of a missing product, a panorama image with drawn bounding boxes can indicate the missing item.

For warehouse robotics, precise image stitching may not be necessary if the camera moves at a constant speed, producing stable images. Instead, focus on barcode and QR code detection, concatenating images based on overlapping detection results.

Code Logic for Concatenating Images:

def stitch_frame(self, frame):
        try:
            results = self.reader.decode_buffer(frame)
            if results != None:
                # Draw results on the copy of the frame. Keep original frame clean.
                frame_cp = frame.copy()
                for result in results:
                    points = result.localization_result.localization_points
                    cv.line(frame_cp, points[0], points[1], (0,255,0), 2)
                    cv.line(frame_cp, points[1], points[2], (0,255,0), 2)
                    cv.line(frame_cp, points[2], points[3], (0,255,0), 2)
                    cv.line(frame_cp, points[3], points[0], (0,255,0), 2)
                    cv.putText(frame_cp, result.barcode_text, points[0], cv.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255))

                # Save frame and barcode info if panorama is empty
                if len(self.panorama) == 0:
                    self.panorama.append((frame, results, frame_cp))
                else:
                    # Compare results. If there is an intersection, transform and stitch. Otherwise, discard.
                    preFrame = self.panorama[0][0]
                    preResults = self.panorama[0][1]
                    preFrameCp = self.panorama[0][2]

                    while len(results) > 0:
                        result = results.pop()
                        for preResult in preResults:
                            if preResult.barcode_text == result.barcode_text and preResult.barcode_format == result.barcode_format:
                                prePoints = preResult.localization_result.localization_points
                                points = result.localization_result.localization_points

                                # # Crop image based on min area rect
                                preFrame = preFrame[0: preFrame.shape[0], 0: max(prePoints[0][0], prePoints[1][0], prePoints[2][0], prePoints[3][0]) + 10]
                                frame = frame[0: frame.shape[0], max(points[0][0], points[1][0], points[2][0], points[3][0]): frame.shape[1] + 10]

                                preFrameCp = preFrameCp[0: preFrameCp.shape[0], 0: max(prePoints[0][0], prePoints[1][0], prePoints[2][0], prePoints[3][0]) + 10]
                                frame_cp = frame_cp[0: frame_cp.shape[0], max(points[0][0], points[1][0], points[2][0], points[3][0]): frame_cp.shape[1] + 10]

                                # # Stitch images
                                frame = concat_images([preFrame, frame])
                                frame_cp = concat_images([preFrameCp, frame_cp])

                                # Re-detect barcodes from the new image
                                results = self.reader.decode_buffer(frame)

                                # Save results
                                self.panorama = [(frame, results, frame_cp)]
                                return frame, frame_cp

                return self.panorama[0][0], self.panorama[0][2]
                    
        except BarcodeReaderError as e:
            print(e)
            return None, None

        return None, None

Result:

concatenate images based on barcode and QR code

Source Code

https://github.com/yushulx/python-barcode-qrcode-sdk/tree/main/examples/official/9.x/webcam