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

Xiao Ling
8 min readAug 8, 2022

--

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.

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;
}

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

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 ]
}
]
}

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

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;

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])

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},
)

To build and install the package locally:

python setup.py build install

To build the source distribution:

python setup.py sdist

To build the wheel distribution:

pip wheel . --verbose
# Or
python setup.py bdist_wheel

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.

2. Get a 30-day FREE trial license for activating the SDK.

3. 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()

4. Run the command-line MRZ scanning application.

python app.py

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 $
runs-on: $
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: $

- 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: $
skip_existing: true

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

Source Code

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

Originally published at https://www.dynamsoft.com on August 8, 2022.

--

--

Xiao Ling
Xiao Ling

Written by Xiao Ling

Manager of Dynamsoft Open Source Projects | Tech Lover

No responses yet