Xiao Ling
Posted on September 6, 2021
If you search for passport scanner software or MRZ reader software, you will find many of them are only available for mobile devices. For police officers, scanning passports by mobile devices is convenient when they are patrolling. However, for customs and immigration officers, they usually use desktop system and professional passport scanner or reader, which cost a lot, to check passengers' passport information. Dynamsoft's OCR SDK is available for both mobile and desktop scenarios. In this article, I will demonstrate an economic way that uses a cheap USB web camera (less than $20), Qt, and Dynamsoft MRZ SDK to build a desktop passport scanner application for Windows and Linux.
Prerequisites
- Dynamsoft Capture Vision Trial License
-
Qt 5.12.11
- Windows
-
Linux
sudo apt-get install qt5-default
The Skeleton of Qt C++ Project for Desktop Passport Scanner
Before getting started, let's get the codebase of the barcode scanning application that I implemented recently.
git clone https://github.com/yushulx/Qt-desktop-barcode-reader.git
The codebase has implemented the file loading and camera video streaming functions. What I need to do is to replace barcode recognition SDK with MRZ recognition SDK. In addition, the project needs to import extra character models (trained by Caffe) for OCR and a template file for providing MRZ recognition parameters.
Character model files
NumberUppercase.caffemodel
NumberUppercase.prototxt
NumberUppercase.txt
NumberUppercase_Assist_1lIJ.caffemodel
NumberUppercase_Assist_1lIJ.prototxt
NumberUppercase_Assist_1lIJ.txt
NumberUppercase_Assist_8B.caffemodel
NumberUppercase_Assist_8B.prototxt
NumberUppercase_Assist_8B.txt
NumberUppercase_Assist_8BHR.caffemodel
NumberUppercase_Assist_8BHR.prototxt
NumberUppercase_Assist_8BHR.txt
NumberUppercase_Assist_number.caffemodel
NumberUppercase_Assist_number.prototxt
NumberUppercase_Assist_number.txt
NumberUppercase_Assist_O0DQ.caffemodel
NumberUppercase_Assist_O0DQ.prototxt
NumberUppercase_Assist_O0DQ.txt
NumberUppercase_Assist_upcase.caffemodel
NumberUppercase_Assist_upcase.prototxt
NumberUppercase_Assist_upcase.txt
Template file
{
"CharacterModelArray" : [
{
"DirectoryPath": "CharacterModel",
"FilterFilePath": "",
"Name": "NumberUppercase"
}
],
"LabelRecognizerParameterArray" : [
{
"BinarizationModes" : [
{
"BlockSizeX" : 0,
"BlockSizeY" : 0,
"EnableFillBinaryVacancy" : 1,
"LibraryFileName" : "",
"LibraryParameters" : "",
"Mode" : "BM_LOCAL_BLOCK",
"ThreshValueCoefficient" : 15
}
],
"CharacterModelName" : "NumberUppercase",
"LetterHeightRange" : [ 5, 1000, 1 ],
"LineStringLengthRange" : [44, 44],
"MaxLineCharacterSpacing" : 130,
"LineStringRegExPattern" : "(P[OM<][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}[0-9<][0-9]){(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 ]
}
]
}
Desktop Passport Scanner for Windows and Linux
Since we have already got the codebase, it won't take too much time to get the application work.
Library Linking
We extract the library files from the downloaded archive and put them into the corresponding folders:
-
Windows
Copy
DynamsoftLabelRecognizerx64.lib
toplatform/windows/lib
.Copy
DynamicPdfx64.dll
,DynamsoftLabelRecognizerx64.dll
,DynamsoftLicenseClientx64.dll
andvcomp140.dll
toplatform/windows/bin
. -
Linux
Copy
libDynamicPdf.so
,libDynamsoftLabelRecognizer.so
, andlibDynamsoftLicenseClient.so
toplatform/linux
.
After that, we update the CMakeLists.txt
file to link libraries and copy model and template files:
if (CMAKE_HOST_WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Widgets Qt5::MultimediaWidgets "DynamsoftLabelRecognizerx64")
elseif(CMAKE_HOST_UNIX)
target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Widgets Qt5::MultimediaWidgets "DynamsoftLabelRecognizer")
endif()
# Copy DLLs
if(CMAKE_HOST_WIN32)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${PROJECT_SOURCE_DIR}/platform/windows/bin/"
$<TARGET_FILE_DIR:${PROJECT_NAME}>)
endif()
# Copy template
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${PROJECT_SOURCE_DIR}/template/"
$<TARGET_FILE_DIR:${PROJECT_NAME}>)
# Copy model files
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${PROJECT_SOURCE_DIR}/CharacterModel"
$<TARGET_FILE_DIR:${PROJECT_NAME}>/CharacterModel)
Steps to Modify the Code for MRZ Recognition
Next, we import DynamsoftLabelRecognizer.h
and DynamsoftCore.h
to mainwindow.h
:
#include "DynamsoftLabelRecognizer.h"
#include "DynamsoftCore.h"
In mainwindow.cpp
, we search for the line of invoking DBR_DecodeFile()
method and then replace the line with:
int errorCode = DBR_DecodeFile(reader, fileName.toStdString().c_str(), "");
Add the following code block to get passport information:
DLR_Result **results = handler->results;
for (int ri = 0; ri < handler->resultsCount; ++ri)
{
DLR_Result* result = handler->results[ri];
int lCount = result->lineResultsCount;
for (int li = 0; li < lCount; ++li)
{
DM_Point *points = result->lineResults[li]->location.points;
int x1 = points[0].x, y1 = points[0].y;
int x2 = points[1].x, y2 = points[1].y;
int x3 = points[2].x, y3 = points[2].y;
int x4 = points[3].x, y4 = points[3].y;
}
if (lCount < 2)
{
continue;
}
string line1 = result->lineResults[0]->text;
string line2 = result->lineResults[1]->text;
if (line1.length() != 44 || line2.length() != 44)
{
continue;
}
if (line1[0] != 'P')
continue;
else {
// Type
string tmp = "Type: ";
tmp.insert(tmp.length(), 1, line1[0]);
out += QString::fromStdString(tmp) + "\n";
// Issuing country
tmp = "Issuing country: "; line1.substr(2, 5);
tmp += line1.substr(2, 3);
out += QString::fromStdString(tmp) + "\n";
// Surname
int index = 5;
tmp = "Surname: ";
for (; index < 44; index++)
{
if (line1[index] != '<')
{
tmp.insert(tmp.length(), 1, line1[index]);
}
else
{
break;
}
}
out += QString::fromStdString(tmp) + "\n";
// Given names
tmp = "Given Names: ";
index += 2;
for (; index < 44; index++)
{
if (line1[index] != '<')
{
tmp.insert(tmp.length(), 1, line1[index]);
}
else
{
tmp.insert(tmp.length(), 1, ' ');
}
}
out += QString::fromStdString(tmp) + "\n";
// Passport number
tmp = "Passport number: ";
index = 0;
for (; index < 9; index++)
{
if (line2[index] != '<')
{
tmp.insert(tmp.length(), 1, line2[index]);
}
else
{
break;
}
}
out += QString::fromStdString(tmp) + "\n";
// Nationality
tmp = "Nationality: ";
tmp += line2.substr(10, 3);
out += QString::fromStdString(tmp) + "\n";
// Date of birth
tmp = line2.substr(13, 6);
tmp.insert(2, "/");
tmp.insert(5, "/");
tmp = "Date of birth (YYMMDD): " + tmp;
out += QString::fromStdString(tmp) + "\n";
// Sex
tmp = "Sex: ";
tmp.insert(tmp.length(), 1, line2[20]);
out += QString::fromStdString(tmp) + "\n";
// Expiration date of passport
tmp = line2.substr(21, 6);
tmp.insert(2, "/");
tmp.insert(5, "/");
tmp = "Expiration date of passport (YYMMDD): " + tmp;
out += QString::fromStdString(tmp) + "\n";
// Personal number
if (line2[28] != '<')
{
tmp = "Personal number: ";
for (index = 28; index < 42; index++)
{
if (line2[index] != '<')
{
tmp.insert(tmp.length(), 1, line2[index]);
}
else
{
break;
}
}
out += QString::fromStdString(tmp) + "\n";
}
}
}
DLR_FreeResults(&handler);
So far, the static image recognition is completed. In the following, we will implement real-time passport scanning by camera video stream.
To store MRZ recognition information and share it between threads, we create a new class MRZInfo
:
#ifndef MRZINFO_H
#define MRZINFO_H
#include <QString>
class MRZInfo
{
public:
MRZInfo() = default;
~MRZInfo(){};
bool isNull();
public:
QString text;
int x1, y1, x2, y2, x3, y3, x4, y4, xx1, yy1, xx2, yy2, xx3, yy3, xx4, yy4;
};
#endif // MRZINFO_H
Open work.h
to add a new slot function detectMRZ()
, which works in a worker thread for recognizing MRZ:
void Work::detectMRZ()
{
while (m_bIsRunning)
{
QImage image;
m_mutex.lock();
// wait for QList
if (queue.isEmpty())
{
m_listIsEmpty.wait(&m_mutex);
}
if (!queue.isEmpty())
{
image = queue.takeFirst();
}
m_mutex.unlock();
if (!image.isNull())
{
// Detect MRZ
}
}
}
To recognize MRZ and extract passport information, we firstly convert QImage
to ImageData
and then call DLR_RecognizeByBuffer()
method:
// Convert QImage to ImageData
ImageData data;
data.bytes = (unsigned char *)image.bits();
data.width = image.width();
data.height = image.height();
data.stride = image.bytesPerLine();
data.format = IPF_ARGB_8888;
data.bytesLength = image.byteCount();
QDateTime start = QDateTime::currentDateTime();
int errorCode = DLR_RecognizeByBuffer(recognizer, &data, "locr");
QDateTime end = QDateTime::currentDateTime();
DLR_ResultArray *handler = NULL;
DLR_GetAllResults(recognizer, &handler);
std::vector<MRZInfo> all;
QString out = "Elapsed time: " + QString::number(start.msecsTo(end)) + "ms\n\n";
DLR_Result **results = handler->results;
for (int ri = 0; ri < handler->resultsCount; ++ri)
{
DLR_Result* result = handler->results[ri];
int lCount = result->lineResultsCount;
if (lCount < 2)
{
continue;
}
DLR_LineResult *l1 = result->lineResults[0];
DLR_LineResult *l2 = result->lineResults[1];
string line1 = l1->text;
string line2 = l2->text;
if (line1.length() != 44 || line2.length() != 44)
{
continue;
}
if (line1[0] != 'P')
continue;
MRZInfo info;
DM_Point *points = l1->location.points;
int x1 = points[0].x, y1 = points[0].y;
int x2 = points[1].x, y2 = points[1].y;
int x3 = points[2].x, y3 = points[2].y;
int x4 = points[3].x, y4 = points[3].y;
DM_Point *points2 = l2->location.points;
int xx1 = points2[0].x, yy1 = points2[0].y;
int xx2 = points2[1].x, yy2 = points2[1].y;
int xx3 = points2[2].x, yy3 = points2[2].y;
int xx4 = points2[3].x, yy4 = points2[3].y;
// Type
string tmp = "Type: ";
tmp.insert(tmp.length(), 1, line1[0]);
out += QString::fromStdString(tmp) + "\n";
// Issuing country
tmp = "Issuing country: "; line1.substr(2, 5);
tmp += line1.substr(2, 3);
out += QString::fromStdString(tmp) + "\n";
// Surname
int index = 5;
tmp = "Surname: ";
for (; index < 44; index++)
{
if (line1[index] != '<')
{
tmp.insert(tmp.length(), 1, line1[index]);
}
else
{
break;
}
}
out += QString::fromStdString(tmp) + "\n";
// Given names
tmp = "Given Names: ";
index += 2;
for (; index < 44; index++)
{
if (line1[index] != '<')
{
tmp.insert(tmp.length(), 1, line1[index]);
}
else
{
tmp.insert(tmp.length(), 1, ' ');
}
}
out += QString::fromStdString(tmp) + "\n";
// Passport number
tmp = "Passport number: ";
index = 0;
for (; index < 9; index++)
{
if (line2[index] != '<')
{
tmp.insert(tmp.length(), 1, line2[index]);
}
else
{
break;
}
}
out += QString::fromStdString(tmp) + "\n";
// Nationality
tmp = "Nationality: ";
tmp += line2.substr(10, 3);
out += QString::fromStdString(tmp) + "\n";
// Date of birth
tmp = line2.substr(13, 6);
tmp.insert(2, "/");
tmp.insert(5, "/");
tmp = "Date of birth (YYMMDD): " + tmp;
out += QString::fromStdString(tmp) + "\n";
// Sex
tmp = "Sex: ";
tmp.insert(tmp.length(), 1, line2[20]);
out += QString::fromStdString(tmp) + "\n";
// Expiration date of passport
tmp = line2.substr(21, 6);
tmp.insert(2, "/");
tmp.insert(5, "/");
tmp = "Expiration date of passport (YYMMDD): " + tmp;
out += QString::fromStdString(tmp) + "\n";
// Personal number
if (line2[28] != '<')
{
tmp = "Personal number: ";
for (index = 28; index < 42; index++)
{
if (line2[index] != '<')
{
tmp.insert(tmp.length(), 1, line2[index]);
}
else
{
break;
}
}
out += QString::fromStdString(tmp) + "\n";
}
info.text = out;
info.x1 = x1;
info.y1 = y1;
info.x2 = x2;
info.y2 = y2;
info.x3 = x3;
info.y3 = y3;
info.x4 = x4;
info.y4 = y4;
info.xx1 = xx1;
info.yy1 = yy1;
info.xx2 = xx2;
info.yy2 = yy2;
info.xx3 = xx3;
info.yy3 = yy3;
info.xx4 = xx4;
info.yy4 = yy4;
all.push_back(info);
}
DLR_FreeResults(&handler);
surface->appendResult(all);
How to Build the Qt CMake Project
The CMake build commands are a bit different between Windows and Linux:
# Windows
mkdir build
cd build
cmake -G "MinGW Makefiles" ..
cmake --build .
MRZRecognizer.exe
# Linux
mkdir build
cd build
cmake ..
cmake --build .
./MRZRecognizer
Running Passport Scanner
When running the program, you need to enter a valid license key:
Then you can scan passport information from static images or webcam.
Source Code
https://github.com/yushulx/cmake-cpp-barcode-qrcode/tree/main/examples/qt_mrz
Posted on September 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.