Barcode Scanner with zxing-cpp

In this tutorial, we’ll walk through the creation of a Barcode Extraction AI using the zxing-cpp library. We’ll implement a BarcodeAnnotation class, an Extraction AI logic, and integrate the AI with the konfuzio-sdk library. This AI will be able to detect barcodes from Documents and generate bounding box Annotations for the detected barcodes.

The final result on the DVUI would look like this:

../../../_images/barcode_scanner_example.png

Requirements


Before we start, please ensure that the following requirements are available and properly installed on your system:

  1. Python 3.8 or a higher version. 🐍

  2. The konfuzio-sdk package. ✅

  3. The zxing-cpp library. 💻

1. Set up the BarcodeAnnotation class


The first step is to create a BarcodeAnnotation class that inherits from Annotation.

This is mainly due to the fact that the Annotation class is based on Spans and it’s bboxes are computed using these Spans.

However, in our case, we want to use custom bounding boxes that are computed using the zxing-cpp library. Therefore, we need to override the bboxes property of the Annotation class to return our custom bounding boxes.

This will later be used by the Server as well as the DVUI to annotate the barcodes in the Document.

The difference between Span based bboxes (dashed-line boxes) and custom bboxes (yellow box) is illustrated in the following image:

../../../_images/barcode_example.png
from typing import Dict, List
from konfuzio_sdk.data import Annotation


class BarcodeAnnotation(Annotation):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.custom_bboxes = kwargs.get("custom_bboxes", [])

    @property
    def bboxes(self) -> List[Dict]:
        """
        This method of the Annotation class must be overridden to return the custom_bboxes.
        :return: List of dictionaries, each in the format:
            {
                'x0': int, 'x1': int, 'y0': int, 'y1': int,
                'top': int, 'bottom': int, 'page_index': int,
                'offset_string': str, 'custom_offset_string': bool
            }
        """
        return self.custom_bboxes

2. Defining the BarcodeExtractionAI class methods


The second step is to create a custom Extraction AI class that leverages the CustomAnnotation class defined in Step 1 in order to extract barcodes from Documents. We will start by explaining the different methods that will be used in this class.

NB. If you want to directly have the full code of the BarcodeExtractionAI class, you can skip to Step 3.

2.1. Extract Bounding Boxes from Image


In this step, we’ll implement the method to extract bounding boxes and barcode text from an image using the zxing-cpp library.

def get_bboxes_from_image(self, image, page_index):
    """
    Extract the bboxes and texts of the barcodes from an image and format the bbox dictionaries in the right format
    :param image: PIL image (the image of the page, resize to original size or probide the original image for better results )
    :param page_index: int
    """
    # import the necessary function from the library
    from zxingcpp import read_barcodes

    # create empty list to store the bboxes
    bboxes_list = []
    # get the results from the library
    barcodes_lib_results = read_barcodes(image)
    # loop through the results and extract the bboxes
    for result in barcodes_lib_results:
        # unpack the result position ## output: '496x453 743x453 743x550 496x550'
        position = str(result.position).rstrip("\x00")
        # unpack the result text ## output: '123456'
        barcode_text_value = str(result.text).rstrip("\x00")
        # unpack the coordinates of the bottom-left and top-right corners of the detected barcode
        top_right = position.split()[1].split("x")
        bottom_left = position.split()[-1].split("x")
        # create the bbox dictionary
        bbox_dict = self.get_bbox_dict_from_coords(
            top_right, bottom_left, page_index, image, barcode_text_value
        )
        # append the bbox dictionary to the list
        bboxes_list.append(bbox_dict)
    return bboxes_list

2.2. Create Bbox Dictionary from zxing-cpp Output


In this step, we’ll implement the method to create the Bbox dictionary from the output of zxing-cpp.

def get_bbox_dict_from_coords(
    top_right, bottom_left, page_index, image, barcode_text_value
):
    """
    transform the coordinates of the bottom-left and top-right corners
    of the detected barcode from cv2 coordinates system to DVUI coordinates system
    """
    # get the coordinates of the top-right corner in cv2 coordinates system
    x1 = int(top_right[0])
    y1 = int(top_right[1])
    # get the coordinates of the bottom-left corner in cv2 coordinates system
    x0 = int(bottom_left[0])
    y0 = int(bottom_left[1])
    # top and bottom are resp. equal to y1 and y0 in the cv2 coordinates system
    # they don't need to be transformed to the DVUI coordinates system because they are distances and not coordinates
    top = y1
    bottom = y0
    # transform the coordinates from cv2 coordinates system to DVUI coordinates system
    # x0 and x1 don't need to be transformed because the x axis is unchanged in the DVUI coordinates system
    temp_y0 = image.height - y0
    temp_y1 = image.height - y1
    y0 = temp_y0
    y1 = temp_y1
    # create the bbox dictionary
    bbox_dict = {
        "x0": x0,
        "x1": x1,
        "y0": y0,
        "y1": y1,
        "top": top,
        "bottom": bottom,
        "page_index": page_index,
        "offset_string": barcode_text_value,
        "custom_offset_string": True,
    }
    return bbox_dict

With these two separate steps, you have a clear distinction between extracting bounding boxes from the image using zxing-cpp and creating the final bbox dictionary from the output. This makes the code more modular and easier to maintain.

2.3. Install Dependencies


In this step, we’ll implement the method to install the zxing-cpp library (since it might not be installed by default on the running environment).

def install_dependencies():
    # try installing the zxing-cpp library otherwise raise an error
    try:
        import subprocess

        package_name = "zxing-cpp"
        # Run the pip install command
        subprocess.check_call(["pip", "install", package_name])
        print(f"The package {package_name} is ready to be used.")
    except:
        raise Exception(
            "An error occured while installing the zxing-cpp library. Please install it manually."
        )

2.4. Check if the Extraction AI is ready


In this step, we’ll implement a function that will be used to check if the Extraction AI is ready. This is needed by the server to know when to start the extraction process.

def check_is_ready(self) -> bool:
    # check if the zxing-cpp library is already installed
    try:
        self.install_dependencies()
        import zxingcpp
        return True
    except:
        return False

3. Putting it All Together


Finally, we’ll create a BarcodeExtractionAI class that uses all the earlier defined functions to extract the barcode from a Document.

from typing import Dict, List
from konfuzio_sdk.trainer.information_extraction import AbstractExtractionAI
from konfuzio_sdk.data import Document, Category, Annotation, AnnotationSet
from konfuzio_sdk.tokenizer.regex import WhitespaceTokenizer
from copy import deepcopy


class BarcodeAnnotation(Annotation):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.custom_bboxes = kwargs.get("custom_bboxes", [])

    @property
    def bboxes(self) -> List[Dict]:
        """
        This method of the Annotation class must be overwriten to return the custom_bboxes
        :return: List of dictionaries each of the format {
            'x0': int, 'x1': int, 'y0': int, 'y1': int,
            'top': int, 'bottom': int, 'page_index': int,
            'offset_string': str, 'custom_offset_string': bool
        }
        """
        return self.custom_bboxes


class BarcodeExtractionAI(AbstractExtractionAI):
    """
    A Wrapper to extract Barcodes from Documents using zxing-cpp library.
    """

    # you must set this to True if your AI requires pages images
    requires_images = True

    def __init__(self, category: Category, *args, **kwargs):
        super().__init__(category)
        self.tokenizer = WhitespaceTokenizer()

    def fit(self):
        # no training is needed since the zxing-cpp library can be used directly for extraction
        pass

    def extract(self, document: Document) -> Document:
        # check if the model is ready otherwise raise an error
        self.check_is_ready()
        result_document = super().extract(document)
        result_document._text = "this should be a long text or at least twice the number of barcodes in the document"
        barcode_label = self.project.get_label_by_name("Barcode")
        barcode_label_set = self.project.get_label_set_by_name("Barcodes Set")
        barcode_annotation_set = AnnotationSet(
            document=result_document, label_set=barcode_label_set
        )
        # loop through the pages of the document and extract the barcodes
        for page_index, page in enumerate(document.pages()):
            page_width = page.width
            page_height = page.height
            # get the page in image format
            image = page.get_image(update=True)
            # convert the image to RGB
            image = image.convert("RGB")
            # resize the image to the page size
            # IMPORTANT: since the image is already resized we won't need any scaling factors to transform the coordinates
            image = image.resize((int(page_width), int(page_height)))
            # get the bboxes and texts of the barcodes using zxing-cpp
            page_bboxes_list = self.get_bboxes_from_image(image, page_index)
            # loop through the bboxes and create the annotations using enumerate
            for bbox_index, bbox_dict in enumerate(page_bboxes_list):
                _ = BarcodeAnnotation(
                    document=result_document,
                    annotation_set=barcode_annotation_set,
                    spans=[],
                    start_offset=bbox_index + 1,
                    end_offset=bbox_index + 2,
                    label=barcode_label,
                    label_set=barcode_label_set,
                    confidence=1.0,
                    bboxes=None,
                    custom_bboxes=[bbox_dict],
                )

        return result_document

    def get_bboxes_from_image(self, image, page_index):
        from zxingcpp import read_barcodes
        bboxes_list = []
        barcodes_lib_results = read_barcodes(image)
        for result in barcodes_lib_results:
            position = str(result.position).rstrip("\x00")
            barcode_text_value = str(result.text).rstrip("\x00")
            top_right = position.split()[1].split("x")
            bottom_left = position.split()[-1].split("x")
            bbox_dict = self.get_bbox_dict_from_coords(
                top_right, bottom_left, page_index, image, barcode_text_value
            )
            bboxes_list.append(bbox_dict)
        return bboxes_list

    def get_bbox_dict_from_coords(
        self, top_right, bottom_left, page_index, image, barcode_text_value
    ):
        x1 = int(top_right[0])
        y1 = int(top_right[1])
        x0 = int(bottom_left[0])
        y0 = int(bottom_left[1])
        top = y1
        bottom = y0
        temp_y0 = image.height - y0
        temp_y1 = image.height - y1
        y0 = temp_y0
        y1 = temp_y1
        bbox_dict = {
            "x0": x0,
            "x1": x1,
            "y0": y0,
            "y1": y1,
            "top": top,
            "bottom": bottom,
            "page_index": page_index,
            "offset_string": barcode_text_value,
            "custom_offset_string": True,
        }
        return bbox_dict

    def install_dependencies(self):
        try:
            import subprocess
            package_name = "zxing-cpp"
            subprocess.check_call(
                ["pip", "install", package_name])
            print(f"The package {package_name} is ready to be used.")
        except:
            raise Exception(
                "An error occured while installing the zxing-cpp library. Please install it manually."
            )

    def check_is_ready(self) -> bool:
        try:
            self.install_dependencies()
            import zxingcpp
            return True
        except:
            return False

4. Saving the BarcodeExtractionAI


Now, let’s create the main script to save our Extraction AI that will be used to process Documents on Konfuzio:

We start by defining our project_id (don’t forget to change the project_id to your own project_id, it should be an int) then we save the Extraction AI as a pickle file that we will upload to the Server later.

project_id = "my_project_id"
from konfuzio_sdk.data import Project

project = Project(id_=project_id, update=True, strict_data_validation=False)
barcode_extraction_ai = BarcodeExtractionAI(category=project.categories[0])
pickle_model_path = barcode_extraction_ai.save()

This is an example of how the output of this Barcode Scanner will look like on the DVUI:

../../../_images/barcode_scanner_example.png