Article
· Nov 5 7m read

Recognizing Barcodes With Embedded Python

 

As we keep updating our software, we often realize that we require more and more modern solutions. So far, only one major piece of our software relies on reading barcodes in documents and images. Since Cache did not have a means of reading barcodes in the past, we have always achieved our goals by using a Visual Basic 6 application. However, it is no longer an ideal solution because it is currently complicated to maintain it. IRIS also lacks this capability, but it has recently got an option that makes up for it: embedded Python!

To be able to read barcodes, we must install a couple of packages. We will need the pyzbar and the cv2 module for Python, so we should install the pyzbar and opencv-python packages. If we were utilizing a typical Python installation, we would require the following:

py -m pip install opencv-python pyzbar

However, to get packages installed correctly for embedded Python, we should take a different approach for a few reasons. First, embedded Python will look for packages in the /mgr/python directory of the IRIS installation. We could make the installation process target that folder by using the --target option for pip. Yet, as I learned the hard way, it can cause another problem. I had Python 3.13 installed on my machine, but the embedded Python on my version of IRIS is Python 3.9. As a result, when I ran pip, it would install packages for the wrong version of Python! To make sure we are going to get the correct and compatible version of those packages, we will utilize the irispython command found in the bin directory of our IRIS installation. Note that my IRIS instance is named “NEWIRIS”, so you will have to modify the path according to your needs.

cd C:\InterSystems\NEWIRIS\bin
irispython -m pip install --target C:\InterSystems\NEWIRIS\mgr\python opencv-python pyzbar

The abovementioned line will install the required packages and their prerequisites. As a side note, one of those prerequisites is Numpy, a package with which I ran into compatibility errors when I used the wrong pip command for installation! As a result, the version meant for Python 3.13 was installed instead of the one compatible with Python 3.9.

We will also need to bridge the gap between ObjectScript and Python. One simple way to do that is to write Python methods that return JSON, and then use the JSON in ObjectScript. We will be required to utilize the JSON package to do that. However, since it is a default Python package, we will not have to take extra steps to install it.

If you wish to verify that the packages are installed correctly and will be functional in embedded Python, you can use an easy solution provided by the Python shell. To do that, we should open a terminal, invoke the Python shell, and attempt to import all the packages to check for errors.

Do ##class(%SYS.Python).Shell()
Import cv2
Import pyzbar
Import json

If all of those commands work without errors, we are ready to go!

Next, we will create a new class which I will name User.PyBarScan and write a class method that uses embedded Python. Our class method will take a file path as an argument and return information about the barcodes it finds as a JSON array. We will need to import the packages that we have just installed (or at least parts of them), and we have a couple of options for how to do it. One of them is to include an XData block in our class definition. If we choose this option, we must name the XData block %import, and its MimeType should be application/python. We can place our import statements there. We might define this as follows (note that we only need the decode function from pyzbar, so we have imported only that part, not the whole package):

XData %import [ MimeType = application/python ]{
    import cv2
    from pyzbar.pyzbar import decode
    import json
}

The other option is to include the import statements in the method itself making them usable only in that method.

ClassMethod Scan(path As %String) [ Language = python ]
{
        import cv2
        from pyzbar.pyzbar import decode
        import json
}

Either option will work equally well, but each has advantages and disadvantages. The XData block will make the imported packages available to every method in the class, meaning that you will not need to import them again for each method. It will also keep all of the import statements together in one place which you may consider beneficial from a code management perspective. On the other hand, if you have a class that has a lot of methods but only some of them use the imports, every instance of the class will still import those items every single time.

We can easily see which packages are needed in each method if we import statements into the methods. If we delete the last method in the class that uses a particular package, it will no longer get imported since that method will not be called. The downside to this approach is the need to remember to do our imports into every method. Additionally, if we ever want to replace an imported package, we should do it in multiple places throughout the class.

We can also blend both approaches if we wish. First, we could import packages that will be used by several methods in the XData block. Then, we could import a certain package in a specific method that will utilize it for a reason that no other ones will. It will also do the job.

For my purposes today, I will include the import statements in my method simply because that will keep everything together in one place and help me make a neater code snippet to show you.

Be aware that in this method you will see some things that work similarly to the way the %DyanmicObject and %DynamicArray classes in ObjectScript would work with JSON. However, they are not quite the same. The curly brackets represent an object known as a Dictionary in Python. It will look like our good friend %DynamicObject in many ways. Likewise, the square brackets will describe a list in Python that will resemble a %DynamicArray. Yet, if we try to return either of those objects directly, we will get a %SYS.Python object handle. Also, the data therein will utilize values and keys surrounded by single quotes instead of double ones, and null values will be handled a bit differently compared to JSON as well. Since it is not valid JSON, we can not construct our dynamic object and arrays from it in ObjectScript. This is why we should use the json.dumps method from the JSON package. It ensures that the null values are handled correctly, and the JSON is in the right format. Below you can find a full text of the method we will employ:

ClassMethod Scan(path As %String) [ Language = python ]
{
        import cv2
        from pyzbar.pyzbar import decode
        import json
        image = cv2.imread(path)
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        barcodes = decode(gray)
        code_data = []
        for barcode in barcodes:
            barcode_data = barcode.data.decode("utf-8")
            barcode_type = barcode.type
            print("Barcode Data: ", barcode_data)
            print("Barcode Type: ", barcode_type)
            (x,y,w,h) = barcode.rect
            thiscode = {}
            thiscode.update({"data":barcode_data})
            thiscode.update({"type":barcode_type})
            thiscode.update({"x":x})
            thiscode.update({"y":y})
            thiscode.update({"w":w})
            thiscode.update({"h":h})
            code_data.append(thiscode)
        return json.dumps(code_data)
}

You can see our imports at the top (we have already discussed it). Our next step is to use cv2 to read the file specified by the path we have passed to the method. Then we must convert the image to a grayscale image making barcode recognition work slightly better. We will later employ the decode method we imported from pyzbar.pyzbar. After that, we will set the code_data variable to an empty list. Finally, we will start a for loop. If we had a list of objects in ObjectScript, our for loop may look as the following:

For i=1:1:$LISTLENGTH(barcodes){
    set barcode = $LISTGET(barcodes,i)
    //do the things here
}

In Python, “for barcode in barcodes” means to iterate through the list using the variable name “barcode” for the current element of the list. We should find the barcode type and data. In my usage case, we operate Code218 barcodes. However, the same method works just as easily with UPCs or even QR codes. First, we write them out. Next, we get the bounding rectangle of the barcode. It includes the x and y coordinates of the top right corner, the width, and the height. There are a few other pieces of information available, but we will not need them today.

At this point, we start creating an empty dictionary object called thiscode. Then we use the dictionary’s update method to set some values there. The update method will create a new key/value pair if the key does not exist, or update it if it does exist. It will ensure that we do not have any duplicate keys in our final JSON. Our next step is to append this dictionary to the list, so we return the JSON using the json.dumps method to fix those potential formatting errors we mentioned above. At this point, we should have a familiar JSON object that is comfortable enough for our ObjectScript users to manipulate! For instance, in the terminal, I should be able to do the following now:

USER>set barcodes = ##class(User.PyBarScan).Scan("C:\path\to\file\barcodes.jpg")
Barcode Data:  SHIPPER
Barcode Type:  CODE128
Barcode Data:  28083901
Barcode Type:  CODE128
USER>w barcodes
[{"data": "SHIPPER", "type": "CODE128", "x": 8, "y": 157, "w": 280, "h": 75}, {"data": "12345601", "type": "CODE128", "x": 4, "y": 0, "w": 258, "h": 75}]
USER>set mybc = mybcarray.%Get(1)
USER>w mybc.%Get("type"),!,mybc.%Get("data")
CODE128
12345601

This is just one of many great options embedded Python can offer us once we find and install the right libraries!

Discussion (0)2
Log in or sign up to continue