DEV Community

Xiao Ling
Xiao Ling

Posted on • Updated on • Originally published at dynamsoft.com

How to Build Python MRZ Scanner SDK and Publish It to PyPI

The latest version 2.2.10 of Dynamsoft Label Recognizer C++ SDK has optimized the MRZ recognition model, which is much smaller and more accurate than the previous version. It now supports recognizing MRZ from passport, Visa, ID card and travel documents. To facilitate developing desktop MRZ recognition software, we can bind the C++ OCR APIs to Python.
This article goes through the steps to build a Python MRZ Scanner module based on Dynamsoft Label Recognizer C++ APIs.

Implementing Python MRZ Scanner SDK in C/C++

Here is the structure of the Python MRZ extension project.

Dynamsoft Python MRZ project structure

The C/C++ MRZ SDK consists of a MRZ model, a JSON-formatted model configuration file, header files and shared libraries (*.dll and *.so) for Windows and Linux.

The mrzscanner.cpp is the entry point of the Python MRZ Scanner SDK. It defines two methods:

  • initLicense(): Initialize the global license key.
  • createInstance(): Create an instance of DynamsoftMrzReader class.
#include <Python.h>
#include <stdio.h>
#include "dynamsoft_mrz_reader.h"
#define INITERROR return NULL

struct module_state {
    PyObject *error;
};

#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m))

static PyObject *
error_out(PyObject *m)
{
    struct module_state *st = GETSTATE(m);
    PyErr_SetString(st->error, "something bad happened");
    return NULL;
}

static PyObject *createInstance(PyObject *obj, PyObject *args)
{
    if (PyType_Ready(&DynamsoftMrzReaderType) < 0)
         INITERROR;

    DynamsoftMrzReader* reader = PyObject_New(DynamsoftMrzReader, &DynamsoftMrzReaderType);
    reader->handler = DLR_CreateInstance();
    return (PyObject *)reader;
}

static PyObject *initLicense(PyObject *obj, PyObject *args)
{
    char *pszLicense;
    if (!PyArg_ParseTuple(args, "s", &pszLicense))
    {
        return NULL;
    }

    char errorMsgBuffer[512];
    // Click https://www.dynamsoft.com/customer/license/trialLicense/?product=dbr to get a trial license.
    int ret = DLR_InitLicense(pszLicense, errorMsgBuffer, 512);
    printf("DLR_InitLicense: %s\n", errorMsgBuffer);

    return Py_BuildValue("i", ret);
}

static PyMethodDef mrzscanner_methods[] = {
  {"initLicense", initLicense, METH_VARARGS, "Set license to activate the SDK"},
  {"createInstance", createInstance, METH_VARARGS, "Create Dynamsoft MRZ Reader object"},
  {NULL, NULL, 0, NULL}       
};

static struct PyModuleDef mrzscanner_module_def = {
  PyModuleDef_HEAD_INIT,
  "mrzscanner",
  "Internal \"mrzscanner\" module",
  -1,
  mrzscanner_methods
};

PyMODINIT_FUNC PyInit_mrzscanner(void)
{
    PyObject *module = PyModule_Create(&mrzscanner_module_def);
    if (module == NULL)
        INITERROR;


    if (PyType_Ready(&DynamsoftMrzReaderType) < 0)
       INITERROR;

    Py_INCREF(&DynamsoftMrzReaderType);
    PyModule_AddObject(module, "DynamsoftMrzReader", (PyObject *)&DynamsoftMrzReaderType);

    if (PyType_Ready(&MrzResultType) < 0)
       INITERROR;

    Py_INCREF(&MrzResultType);
    PyModule_AddObject(module, "MrzResult", (PyObject *)&MrzResultType);

    PyModule_AddStringConstant(module, "version", DLR_GetVersion());
    return module;
}
Enter fullscreen mode Exit fullscreen mode

The DynamsoftMrzReader class is defined in dynamsoft_mrz_reader.h. It defines three methods:

  • decodeFile(): Recognize MRZ from an image file.
  • decodeMat(): Recognize MRZ from OpenCV Mat.
  • loadModel(): Load the MRZ model by parsing a JSON-formatted configuration file.
#ifndef __MRZ_READER_H__
#define __MRZ_READER_H__

#include <Python.h>
#include <structmember.h>
#include "DynamsoftLabelRecognizer.h"
#include "mrz_result.h"

#define DEBUG 0

typedef struct
{
    PyObject_HEAD
    void *handler;
} DynamsoftMrzReader;

static int DynamsoftMrzReader_clear(DynamsoftMrzReader *self)
{
    if(self->handler) {
        DLR_DestroyInstance(self->handler);
        self->handler = NULL;
    }
    return 0;
}

static void DynamsoftMrzReader_dealloc(DynamsoftMrzReader *self)
{
    DynamsoftMrzReader_clear(self);
    Py_TYPE(self)->tp_free((PyObject *)self);
}

static PyObject *DynamsoftMrzReader_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    DynamsoftMrzReader *self;

    self = (DynamsoftMrzReader *)type->tp_alloc(type, 0);
    if (self != NULL)
    {
        self->handler = DLR_CreateInstance();
    }

    return (PyObject *)self;
}

static PyMethodDef instance_methods[] = {
  {"decodeFile", decodeFile, METH_VARARGS, NULL},
  {"decodeMat", decodeMat, METH_VARARGS, NULL},
  {"loadModel", loadModel, METH_VARARGS, NULL},
  {NULL, NULL, 0, NULL}       
};

static PyTypeObject DynamsoftMrzReaderType = {
    PyVarObject_HEAD_INIT(NULL, 0) "mrzscanner.DynamsoftMrzReader", /* tp_name */
    sizeof(DynamsoftMrzReader),                              /* tp_basicsize */
    0,                                                           /* tp_itemsize */
    (destructor)DynamsoftMrzReader_dealloc,                  /* tp_dealloc */
    0,                                                           /* tp_print */
    0,                                                           /* tp_getattr */
    0,                                                           /* tp_setattr */
    0,                                                           /* tp_reserved */
    0,                                                           /* tp_repr */
    0,                                                           /* tp_as_number */
    0,                                                           /* tp_as_sequence */
    0,                                                           /* tp_as_mapping */
    0,                                                           /* tp_hash  */
    0,                                                           /* tp_call */
    0,                                                           /* tp_str */
    PyObject_GenericGetAttr,                                                           /* tp_getattro */
    PyObject_GenericSetAttr,                                                           /* tp_setattro */
    0,                                                           /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,                    /*tp_flags*/
    "DynamsoftMrzReader",                          /* tp_doc */
    0,                                                           /* tp_traverse */
    0,                                                           /* tp_clear */
    0,                                                           /* tp_richcompare */
    0,                                                           /* tp_weaklistoffset */
    0,                                                           /* tp_iter */
    0,                                                           /* tp_iternext */
    instance_methods,                                                 /* tp_methods */
    0,                                                 /* tp_members */
    0,                                                           /* tp_getset */
    0,                                                           /* tp_base */
    0,                                                           /* tp_dict */
    0,                                                           /* tp_descr_get */
    0,                                                           /* tp_descr_set */
    0,                                                           /* tp_dictoffset */
    0,                       /* tp_init */
    0,                                                           /* tp_alloc */
    DynamsoftMrzReader_new,                                  /* tp_new */
};

#endif
Enter fullscreen mode Exit fullscreen mode

The model configuration file looks like this:

{
   "CharacterModelArray" : [
    {
      "DirectoryPath": "model",
      "FilterFilePath": "",
      "Name": "MRZ"
    }
   ],
   "LabelRecognizerParameterArray" : [
      {
         "BinarizationModes" : [
            {
               "BlockSizeX" : 0,
               "BlockSizeY" : 0,
               "EnableFillBinaryVacancy" : 1,
               "LibraryFileName" : "",
               "LibraryParameters" : "",
               "Mode" : "BM_LOCAL_BLOCK",
               "ThreshValueCoefficient" : 15
            }
         ],
         "CharacterModelName" : "MRZ",
         "LetterHeightRange" : [ 5, 1000, 1 ],
         "LineStringLengthRange" : [30, 44],
         "MaxLineCharacterSpacing" : 130,
         "LineStringRegExPattern" : "([ACI][A-Z<][A-Z<]{3}[A-Z0-9<]{9}[0-9][A-Z0-9<]{15}){(30)}|([0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z<]{3}[A-Z0-9<]{11}[0-9]){(30)}|([A-Z<]{0,26}[A-Z]{1,3}[(<<)][A-Z]{1,3}[A-Z<]{0,26}<{0,26}){(30)}|([ACIV][A-Z<][A-Z<]{3}([A-Z<]{0,27}[A-Z]{1,3}[(<<)][A-Z]{1,3}[A-Z<]{0,27}){(31)}){(36)}|([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{8}){(36)}|([PV][A-Z<][A-Z<]{3}([A-Z<]{0,35}[A-Z]{1,3}[(<<)][A-Z]{1,3}[A-Z<]{0,35}<{0,35}){(39)}){(44)}|([A-Z0-9<]{9}[0-9][A-Z<]{3}[0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z0-9<]{14}[A-Z0-9<]{2}){(44)}",
         "MaxThreadCount" : 4,
         "Name" : "locr",
         "TextureDetectionModes" :[
            {
                "Mode" : "TDM_GENERAL_WIDTH_CONCENTRATION",
                "Sensitivity" : 8
            }
         ],
         "ReferenceRegionNameArray" : [ "DRRegion" ]
      }
   ],
   "LineSpecificationArray" : [
    {
        "Name":"L0",
        "LineNumber":"",
        "BinarizationModes" : [
            {
               "BlockSizeX" : 30,
               "BlockSizeY" : 30,
               "Mode" : "BM_LOCAL_BLOCK"
            }
         ]
    }
    ],
   "ReferenceRegionArray" : [
      {
         "Localization" : {
            "FirstPoint" : [ 0, 0 ],
            "SecondPoint" : [ 100, 0 ],
            "ThirdPoint" : [ 100, 100 ],
            "FourthPoint" : [ 0, 100 ],
            "MeasuredByPercentage" : 1,
            "SourceType" : "LST_MANUAL_SPECIFICATION"
         },
         "Name" : "DRRegion",
         "TextAreaNameArray" : [ "DTArea" ]
      }
   ],
   "TextAreaArray" : [
      {
         "LineSpecificationNameArray" : ["L0"],
         "Name" : "DTArea",
         "FirstPoint" : [ 0, 0 ],
         "SecondPoint" : [ 100, 0 ],
         "ThirdPoint" : [ 100, 100 ],
         "FourthPoint" : [ 0, 100 ]
      }
   ]
}
Enter fullscreen mode Exit fullscreen mode

You don't need to change the default parameters except for DirectoryPath, which could be the relative path to the execution directory or the absolute path to the directory where the model files are located. To load the model file successfully in Python, we must set the absolute path of the model file in MRZ.json. The model path is not settled until the MRZ scanner SDK is installed to the Python site-packages folder, which varies depending on different Python environments. Therefore, we check and dynamically modify the absolute path of the model file in MRZ.json when invoking the get_model_path() function implemented in __init__.py.

def get_model_path():
    config_file = os.path.join(os.path.dirname(__file__), 'MRZ.json')
    try:
        # open json file
        with open(config_file, 'r+') as f:
            data = json.load(f)
            if data['CharacterModelArray'][0]['DirectoryPath'] == 'model':
                data['CharacterModelArray'][0]['DirectoryPath'] = os.path.join(os.path.dirname(__file__), 'model')
                # print(data['CharacterModelArray'][0]['DirectoryPath'])

                # write json file
                f.seek(0) # rewind
                f.write(json.dumps(data))
    except Exception as e:
        print(e)
        pass

    return config_file
Enter fullscreen mode Exit fullscreen mode

The mrz_result.h file contains the structure of MRZ recognition result.

typedef struct 
{
    PyObject_HEAD
    PyObject *confidence;
    PyObject *text;
    PyObject *x1;
    PyObject *y1;
    PyObject *x2;
    PyObject *y2;
    PyObject *x3;
    PyObject *y3;
    PyObject *x4;
    PyObject *y4;
} MrzResult;
Enter fullscreen mode Exit fullscreen mode

To do MRZ recognition in Python:

  1. Set the license key.

    mrzscanner.initLicense("your license key")
    
  2. Create a MRZ scanner object.

    scanner = mrzscanner.createInstance()
    
  3. Load the MRZ recognition model.

    scanner.loadModel(mrzscanner.get_model_path())
    
  4. Call decodeFile() or decodeMat() to recognize MRZ.

    results = scanner.decodeFile()
    # or
    results = scanner.decodeMat()
    
  5. Output the text results.

    for result in results:
        print(result.text)
    

Configuring Setup.py File for Building and Packaging Python C Extension

The following code shows how to build the Python C extension with shared libraries for Windows and Linux:

dbr_lib_dir = ''
dbr_include = ''
dbr_lib_name = 'DynamsoftLabelRecognizer'

if sys.platform == "linux" or sys.platform == "linux2":
    # Linux
    dbr_lib_dir = 'lib/linux'
elif sys.platform == "win32":
    # Windows
    dbr_lib_name = 'DynamsoftLabelRecognizerx64'
    dbr_lib_dir = 'lib/win'

if sys.platform == "linux" or sys.platform == "linux2":
    ext_args = dict(
        library_dirs=[dbr_lib_dir],
        extra_compile_args=['-std=c++11'],
        extra_link_args=["-Wl,-rpath=$ORIGIN"],
        libraries=[dbr_lib_name],
        include_dirs=['include']
    )


long_description = io.open("README.md", encoding="utf-8").read()

if sys.platform == "linux" or sys.platform == "linux2" or sys.platform == "darwin":
    module_mrzscanner = Extension(
        'mrzscanner', ['src/mrzscanner.cpp'], **ext_args)
else:
    module_mrzscanner = Extension('mrzscanner',
                                  sources=['src/mrzscanner.cpp'],
                                  include_dirs=['include'], library_dirs=[dbr_lib_dir], libraries=[dbr_lib_name])
Enter fullscreen mode Exit fullscreen mode

After building the Python extension, one more critical step is to copy model files and all dependent shared libraries to the output directory before packaging the Python MRZ scanner module.

def copyfiles(src, dst):
    if os.path.isdir(src):
        filelist = os.listdir(src)
        for file in filelist:
            libpath = os.path.join(src, file)
            shutil.copy2(libpath, dst)
    else:
        shutil.copy2(src, dst)

class CustomBuildExt(build_ext.build_ext):
    def run(self):
        build_ext.build_ext.run(self)
        dst = os.path.join(self.build_lib, "mrzscanner")
        copyfiles(dbr_lib_dir, dst)
        filelist = os.listdir(self.build_lib)
        for file in filelist:
            filePath = os.path.join(self.build_lib, file)
            if not os.path.isdir(file):
                copyfiles(filePath, dst)
                # delete file for wheel package
                os.remove(filePath)

        model_dest = os.path.join(dst, 'model')
        if (not os.path.exists(model_dest)):
            os.mkdir(model_dest)

        copyfiles(os.path.join(os.path.join(
            Path(__file__).parent, 'model')), model_dest)
        shutil.copy2('MRZ.json', dst)

setup(name='mrz-scanner-sdk',
      ...
      cmdclass={
          'build_ext': CustomBuildExt},
      )
Enter fullscreen mode Exit fullscreen mode

To build and install the package locally:

python setup.py build install
Enter fullscreen mode Exit fullscreen mode

To build the source distribution:

python setup.py sdist
Enter fullscreen mode Exit fullscreen mode

To build the wheel distribution:

pip wheel . --verbose
# Or
python setup.py bdist_wheel
Enter fullscreen mode Exit fullscreen mode

Testing Python MRZ Scanner SDK

  1. Install mrz and opencv-python.

    pip install mrz opencv-python
    
- `mrz` is used to extract and check MRZ information from recognized text.
- `opencv-python` is used to display the image.
Enter fullscreen mode Exit fullscreen mode
  1. Get a 30-day FREE trial license for activating the SDK.
  2. Create an app.py file, which recognizes MRZ text from an image file.

    import argparse
    import mrzscanner
    import cv2
    import sys
    import numpy as np
    
    from mrz.checker.td1 import TD1CodeChecker
    from mrz.checker.td2 import TD2CodeChecker
    from mrz.checker.td3 import TD3CodeChecker
    from mrz.checker.mrva import MRVACodeChecker
    from mrz.checker.mrvb import MRVBCodeChecker
    
    def check(lines):
        try:
            td1_check = TD1CodeChecker(lines)
            if bool(td1_check):
                return "TD1", td1_check.fields()
        except Exception as err:
            pass
    
        try:
            td2_check = TD2CodeChecker(lines)
            if bool(td2_check):
                return "TD2", td2_check.fields()
        except Exception as err:
            pass
    
        try:
            td3_check = TD3CodeChecker(lines)
            if bool(td3_check):
                return "TD3", td3_check.fields()
        except Exception as err:
            pass
    
        try:
            mrva_check = MRVACodeChecker(lines)
            if bool(mrva_check):
                return "MRVA", mrva_check.fields()
        except Exception as err:
            pass
    
        try:
            mrvb_check = MRVBCodeChecker(lines)
            if bool(mrvb_check):
                return "MRVB", mrvb_check.fields()
        except Exception as err:
            pass
    
        return 'No valid MRZ information found'
    
    def scanmrz():
        """
        Command-line script for recognize MRZ info from a given image
        """
        parser = argparse.ArgumentParser(description='Scan MRZ info from a given image')
        parser.add_argument('filename')
        args = parser.parse_args()
        try:
            filename = args.filename
            ui = args.ui
    
            # Get the license key from https://www.dynamsoft.com/customer/license/trialLicense/?product=dlr
            mrzscanner.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==")
    
            scanner = mrzscanner.createInstance()
            scanner.loadModel(mrzscanner.get_model_path())
            results = scanner.decodeFile(filename)
            for result in results:
                print(result.text)
                s += result.text + '\n'
    
            print(check(s[:-1]))
    
        except Exception as err:
            print(err)
            sys.exit(1)
    
    if __name__ == "__main__":
        scanmrz()
    
  3. Run the command-line MRZ scanning application.

    python app.py
    

    Python MRZ scanner

GitHub Workflow Configuration

We create a new GitHub action workflow as follows:

name: Build and upload to PyPI

on: [push, pull_request]

jobs:
  build_wheels:
    name: Build wheels on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']

    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}

      - name: Run test.py in develop mode
        run: |
          python setup.py develop
          python -m pip install opencv-python mrz
          python --version
          python test.py

      - name: Build wheels for Linux
        if: matrix.os == 'ubuntu-latest'
        run: |
          pip install -U wheel setuptools auditwheel patchelf
          python setup.py bdist_wheel
          auditwheel repair dist/mrz_scanner_sdk*.whl --plat manylinux2014_$(uname -m)

      - name: Build wheels for Windows
        if: matrix.os == 'windows-latest'
        run: |
          pip install -U wheel setuptools
          python setup.py bdist_wheel -d wheelhouse

      - uses: actions/upload-artifact@v2
        with:
          path: wheelhouse/*.whl

  build_sdist:
    name: Build source distribution
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Build sdist
        run: python setup.py sdist -d dist

      - uses: actions/upload-artifact@v2
        with:
          path: dist/*.tar.gz

  upload_pypi:
    needs: [build_wheels, build_sdist]
    runs-on: ubuntu-latest

    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
    steps:
      - uses: actions/download-artifact@v2
        with:
          name: artifact
          path: dist

      - uses: pypa/gh-action-pypi-publish@v1.4.2
        with:
          user: __token__
          password: ${{ secrets.pypi_password }}
          skip_existing: true
Enter fullscreen mode Exit fullscreen mode

The workflow is configured to build the source and wheel distributions, as well as upload them to PyPI.

Install mrz-scanner-sdk from PyPI

https://pypi.org/project/mrz-scanner-sdk/

pip install mrz-scanner-sdk
Enter fullscreen mode Exit fullscreen mode

Source Code

https://github.com/yushulx/python-mrz-scanner-sdk

Latest comments (0)