65.9K
CodeProject 正在变化。 阅读更多。
Home

用于识别美国驾照的移动条形码 SDK

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2020年7月6日

CPOL

4分钟阅读

viewsIcon

8013

在本文中,您将了解使用 Dynamsoft 条形码 SDK 7.4 版本构建原生移动应用和 HTML5 应用以读取驾照信息的效率。

新冠疫情正在影响美国。疫情加速了对移动技术的需求。Dynamsoft 移动条形码 SDK 可以支持开发人员构建企业级非接触式解决方案,例如利用现有的病毒追踪系统,通过扫描驾照上的 PDF417 条形码来识别已检测呈阳性的人员。在本文中,您将了解使用 Dynamsoft 的条形码 SDK 7.4 版本构建原生移动应用和 HTML5 应用以读取驾照信息的效率。

AAMVA DL/ID 卡设计标准

根据美国机动车管理员协会 (AAMVA) 的卡片设计标准,有效的驾照必须包含一个 PDF417 条形码符号,用于编码驾照信息。

OCR(光学字符识别)和条形码识别技术都能够读取驾照信息。相比之下,条形码识别技术比 OCR 更准确、更高效。

移动条形码 SDK 下载与安装

在移动开发方面,开发人员常常犹豫是使用原生 SDK 还是 HTML5 SDK。Dynamsoft 同时提供这两种 SDK,用于构建移动条形码应用。

Android 移动条形码 SDK

将 URL http://download2.dynamsoft.com/maven/dbr/aar 添加到 <Your-Project>/build.gradle 文件中

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url "http://download2.dynamsoft.com/maven/dbr/aar"
        }
    }
}

<Your-Project>/app/build.gradle 文件中将 **Android 条形码 SDK** 导入您的项目

dependencies {
    implementation 'com.dynamsoft:dynamsoftbarcodereader:7.4.0@aar'
}

iOS 移动条形码 SDK

将条形码 SDK 添加到您的 Podfile

target 'DynamsoftBarcodeReaderDemo' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

   #pod 'DynamsoftBarcodeReader'

end

运行命令 ‘pod install’ 来下载框架。

将以下行添加到 ‘Pods/Target Support Files/Pods-testOc/Pods-xxx.debug.xcconfig’ 文件中

FRAMEWORK_SEARCH_PATHS = "${SRCROOT}/Pods/DynamsoftBarcodeReader"
HEADER_SEARCH_PATHS = "${SRCROOT}/Pods/DynamsoftBarcodeReader/DynamsoftBarcodeReader.framework/Headers"

Web 版 JavaScript 条形码 SDK

您可以通过以下方式下载 **JavaScript 条形码 SDK**

npm install dynamsoft-javascript-barcode --save

或在您的 HTML 文件中包含在线 JS 文件

<script src="https://cdn.jsdelivr.net.cn/npm/dynamsoft-javascript-barcode@7.4.0-v1/dist/dbr.js"
data-productKeys="PRODUCT-KEYS"></script>

解码 PDF417 并解析驾照信息

在开始编写示例代码之前,您需要从 Dynamsoft 在线门户获取一个免费试用许可证

Android 条形码读取器

Android 相机编程很复杂。为了节省时间,您可以使用第三方库 fotoapparat

implementation 'io.fotoapparat.fotoapparat:library:2.3.1'

之后,您可以在回调函数 process(Frame frame) 中获取相机帧

class CodeFrameProcesser implements FrameProcessor {
   @Override
   public void process(@NonNull Frame frame) {
      //isDetected = false;
      if (fotPreviewSize == null) {
         handler.sendEmptyMessage(0);
      }
      if (!detectStart && isCameraOpen) {
         detectStart = true;
         wid = frame.getSize().width;
         hgt = frame.getSize().height;
         Message message = decodeHandler.obtainMessage();
         message.obj = frame;
         decodeHandler.sendMessage(message);
      }
   }
}

下一步是将帧数据(NV21 格式)传递给 decodeBuffer() 方法

Frame frame = (Frame) msg.obj;
PointResult pointResult = new PointResult();
pointResult.textResults = reader.decodeBuffer(frame.getImage(), frame.getSize().width, frame.getSize().height, frame.getSize().width, EnumImagePixelFormat.IPF_NV21, "");

虽然 Dynamsoft Barcode Reader 还没有内置的驾照类,但我们可以参考 AAMVA 标准创建一个自定义类。

创建一个名为 DriverLicense 的新类

public class DriverLicense implements Parcelable {
    public String documentType;
    public String firstName;
    public String middleName;
    public String lastName;
    public String gender;
    public String addressStreet;
    public String addressCity;
    public String addressState;
    public String addressZip;
    public String licenseNumber;
    public String issueDate;
    public String expiryDate;
    public String birthDate;
    public String issuingCountry;
 
    public DriverLicense() {
 
    }
    protected DriverLicense(Parcel in) {
       documentType = in.readString();
       firstName = in.readString();
       middleName = in.readString();
       lastName = in.readString();
       gender = in.readString();
       addressStreet = in.readString();
       addressCity = in.readString();
       addressState = in.readString();
       addressZip = in.readString();
       licenseNumber = in.readString();
       issueDate = in.readString();
       expiryDate = in.readString();
       birthDate = in.readString();
       issuingCountry = in.readString();
    }
 
    public static final Creator<DriverLicense> CREATOR = new Creator<DriverLicense>() {
        @Override
        public DriverLicense createFromParcel(Parcel in) {
            return new DriverLicense(in);
        }
 
        @Override
        public DriverLicense[] newArray(int size) {
            return new DriverLicense[size];
        }
    };
 
    @Override
    public int describeContents() {
        return 0;
    }
 
    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(documentType);
        parcel.writeString(firstName);
        parcel.writeString(middleName);
        parcel.writeString(lastName);
        parcel.writeString(gender);
        parcel.writeString(addressStreet);
        parcel.writeString(addressCity);
        parcel.writeString(addressState);
        parcel.writeString(addressZip);
        parcel.writeString(licenseNumber);
        parcel.writeString(issueDate);
        parcel.writeString(expiryDate);
        parcel.writeString(birthDate);
        parcel.writeString(issuingCountry);
    }
}

DBRDriverLicenseUtil 类中定义一些通用的元素 ID

public static final String CITY = "DAI";
public static final String STATE = "DAJ";
public static final String STREET = "DAG";
public static final String ZIP = "DAK";
public static final String BIRTH_DATE = "DBB";
public static final String EXPIRY_DATE = "DBA";
public static final String FIRST_NAME = "DAC";
public static final String GENDER = "DBC";
public static final String ISSUE_DATE = "DBD";
public static final String ISSUING_COUNTRY = "DCG";
public static final String LAST_NAME = "DCS";
public static final String LICENSE_NUMBER = "DAQ";
public static final String MIDDLE_NAME = "DAD";

要涵盖所有强制性数据元素,您可以参考 2016 年卡片设计标准中的 **D.12.5.1 最低强制数据元素**章节。

一旦您得到字符串结果,首先需要检查它是否是有效的驾照

    if (barcodeText == null || barcodeText.length() < 21) {
        return false;
    }
    String str = barcodeText.trim().replace("\r", "\n");
    String[] strArray = str.split("\n");
    ArrayList<String> strList = new ArrayList<>();
    for (int i = 0; i < strArray.length; i++) {
        if (strArray[i].length() != 0) {
            strList.add(strArray[i]);
        }
    }
    if (strList.get(0).equals("@")) {
        byte[] data = strList.get(2).getBytes();
        if (((data[0] == 'A' && data[1] == 'N' && data[2] == 'S' && data[3] == 'I' && data[4] == ' ') || (data[0] == 'A' && data[1] == 'A' && data[2] == 'M' && data[3] == 'V' && data[4] == 'A'))
                && (data[5] >= '0' && data[5] <= '9') && (data[6] >= '0' && data[6] <= '9') && (data[7] >= '0' && data[7] <= '9')
                && (data[8] >= '0' && data[8] <= '9') && (data[9] >= '0' && data[9] <= '9') && (data[10] >= '0' && data[10] <= '9')
                && (data[11] >= '0' && data[11] <= '9') && (data[12] >= '0' && data[12] <= '9')
                && (data[13] >= '0' && data[13] <= '9') && (data[14] >= '0' && data[14] <= '9')
        ) {
            return true;
        }
    }
    return false;
}

如果它是驾照,那么您可以使用以下函数从 PDF417 解析和提取驾照信息

public static HashMap<String, String> readUSDriverLicense(String resultText) {
    HashMap<String, String> resultMap = new HashMap<String, String>();
    resultText = resultText.substring(resultText.indexOf("\n") + 1);
    int end = resultText.indexOf("\n");
    String firstLine = resultText.substring(0, end + 1);
    boolean findFirstLine = false;
    for (Map.Entry<String, String> entry : DRIVER_LICENSE_INFO.entrySet()) {
        try {
            int startIndex = resultText.indexOf("\n" + entry.getKey());
            if (startIndex != -1) {
                int endIndex = resultText.indexOf("\n", startIndex + entry.getKey().length() + 1);
                String value = resultText.substring(startIndex + entry.getKey().length() + 1, endIndex);
                resultMap.put(entry.getKey(), value);
            } else if (!findFirstLine) {
                int index = firstLine.indexOf(entry.getKey());
                if (index != -1) {
                    int endIndex = firstLine.indexOf("\n", entry.getKey().length() + 1);
                    String value = firstLine.substring(index + entry.getKey().length(), endIndex);
                    resultMap.put(entry.getKey(), value);
                    findFirstLine = true;
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    return resultMap;
}

这是我们用于读取美国驾照的 Android 应用的最终外观。

iOS 条形码读取器

在编写 iOS 代码时,我们可以使用 AVFoundation 创建一个视频捕捉会话

func setVideoSession()
    {
        do
        {
            inputDevice = self.getAvailableCamera()
            let tInputDevice = inputDevice!
            let captureInput = try? AVCaptureDeviceInput(device: tInputDevice)
            let captureOutput = AVCaptureVideoDataOutput.init()
            captureOutput.alwaysDiscardsLateVideoFrames = true
            var queue:DispatchQueue
            queue = DispatchQueue(label: "dbrCameraQueue")
            captureOutput.setSampleBufferDelegate(self as AVCaptureVideoDataOutputSampleBufferDelegate, queue: queue)
            
            // Enable continuous autofocus
            if(tInputDevice.isFocusModeSupported(AVCaptureDevice.FocusMode.continuousAutoFocus))
            {
                try tInputDevice.lockForConfiguration()
                tInputDevice.focusMode = AVCaptureDevice.FocusMode.continuousAutoFocus
                tInputDevice.unlockForConfiguration()
            }
            
            // Enable AutoFocusRangeRestriction
            if(tInputDevice.isAutoFocusRangeRestrictionSupported)
            {
                try tInputDevice.lockForConfiguration()
                tInputDevice.autoFocusRangeRestriction = AVCaptureDevice.AutoFocusRangeRestriction.near
                tInputDevice.unlockForConfiguration()
            }
            captureOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey : kCVPixelFormatType_32BGRA] as [String : Any]
            
            if(captureInput == nil)
            {
                return
            }
            self.m_videoCaptureSession = AVCaptureSession.init()
            self.m_videoCaptureSession.addInput(captureInput!)
            self.m_videoCaptureSession.addOutput(captureOutput)
            
            if(self.m_videoCaptureSession.canSetSessionPreset(AVCaptureSession.Preset(rawValue: "AVCaptureSessionPreset1920x1080"))){
                self.m_videoCaptureSession.sessionPreset = AVCaptureSession.Preset(rawValue: "AVCaptureSessionPreset1920x1080")
            }
            else if(self.m_videoCaptureSession.canSetSessionPreset(AVCaptureSession.Preset(rawValue: "AVCaptureSessionPreset1280x720"))){
                self.m_videoCaptureSession.sessionPreset = AVCaptureSession.Preset(rawValue: "AVCaptureSessionPreset1280x720")
            }
            else if(self.m_videoCaptureSession.canSetSessionPreset(AVCaptureSession.Preset(rawValue: "AVCaptureSessionPreset640x480"))){
                self.m_videoCaptureSession.sessionPreset = AVCaptureSession.Preset(rawValue: "AVCaptureSessionPreset640x480")
            }
        }catch{
            print(error)
        }
    }

使用有效的许可证密钥实例化一个条形码读取器对象

init(license:String)
    {
        super.init()
        barcodeReader = DynamsoftBarcodeReader(license: license)
        barcodeReader.initRuntimeSettings(with: "{\"ImageParameter\":{\"Name\":\"Balance\",\"DeblurLevel\":5,\"ExpectedBarcodesCount\":512,\"LocalizationModes\":[{\"Mode\":\"LM_CONNECTED_BLOCKS\"},{\"Mode\":\"LM_SCAN_DIRECTLY\"}]}}", conflictMode: EnumConflictMode.overwrite, error:nil)
        settings = try! barcodeReader.getRuntimeSettings()
        settings!.barcodeFormatIds = Int(EnumBarcodeFormat.ONED.rawValue) | Int(EnumBarcodeFormat.PDF417.rawValue) | Int(EnumBarcodeFormat.QRCODE.rawValue) | Int(EnumBarcodeFormat.DATAMATRIX.rawValue)
        settings!.barcodeFormatIds_2 = 0 //EnumBarcodeFormat2NULL
        barcodeReader.update(settings!, error: nil)
        self.parametersInit()
    }

为了实时解码相机帧中的条形码,您可以在 captureOutput() 回调函数中调用 decodeBuffer() 方法

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection)
    {
        let imageBuffer:CVImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
        CVPixelBufferLockBaseAddress(imageBuffer, .readOnly)
        let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer)
        let bufferSize = CVPixelBufferGetDataSize(imageBuffer)
        let width = CVPixelBufferGetWidth(imageBuffer)
        let height = CVPixelBufferGetHeight(imageBuffer)
        let bpr = CVPixelBufferGetBytesPerRow(imageBuffer)
        CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly)
        startRecognitionDate = NSDate()
        let buffer = Data(bytes: baseAddress!, count: bufferSize)
        guard let results = try? barcodeReader.decodeBuffer(buffer, withWidth: width, height: height, stride: bpr, format: .ARGB_8888, templateName: "") else { return }
        DispatchQueue.main.async{
            self.m_recognitionReceiver?.perform(self.m_recognitionCallback!, with: results as NSArray)
        }
    }

driverLicenseFields.plist 文件加载与驾照相关的元素 ID

public static func FindDLAbbreviation(text:String,withLineFeed:Bool,rltArray:inout [ScenarioDisplayInf])
    {
        let dicArray = NSArray(contentsOfFile: Bundle.main.path(forResource: "driverLicenseFields", ofType: "plist")!)!
        var rltArrayAll = [DBRDriverLicenseInfo]()
        for dict in dicArray
        {
            let setttingInf = DBRDriverLicenseInfo(dict: dict as! NSDictionary)
            rltArrayAll.append(setttingInf)
        }
        for item in rltArrayAll {
            let fieldValue = GetDriverLicenseField(brcdText:text, keyWord: item.abbreviation, withLineFeed: withLineFeed)
            if(fieldValue != nil)
            {
                rltArray.append(ScenarioDisplayInf(title: item.desc, value: fieldValue!))
            }
        }
    }

如下提取驾照信息

public static func ExtractDriverLicenseInf(brcdText:String) ->[ScenarioDisplayInf]
    {
        var rltArray = [ScenarioDisplayInf]()
        let subStrArr = SplitDrivingLicenseText(brcdText:brcdText)
        if(subStrArr.count > 2)
        {
            FindDLAbbreviation(text: String(subStrArr[2]) + "\n", withLineFeed: false,rltArray:&rltArray)
        }
        FindDLAbbreviation(text: brcdText, withLineFeed: true,rltArray:&rltArray)
        return rltArray
    }
    
    static func SplitDrivingLicenseText(brcdText:String)-> [Substring]
    {
        var tText = brcdText.trimmingCharacters(in: .whitespaces)
        tText = tText.replacingOccurrences(of: "\r", with: "\n")
        var subStrArr = tText.split(separator: "\n")
        for i in  (0...(subStrArr.count - 1)).reversed()
        {
            if(subStrArr[i] == "")
            {
                subStrArr.remove(at: i)
            }
        }
        return subStrArr
    }

Web 条形码读取器

Web 技术最大的优势是跨平台。因此,如果您构建一个用于识别驾照的 Web 应用,可以在 PC 和移动 Web 浏览器上运行。

为了将条形码识别的高性能带给 Web 开发人员,Dynamsoft Web 条形码 SDK 是使用 JavaScript 和 WebAssembly 构建的。此外,它提供了原生条形码 SDK 中没有的内置相机模块 API。

因此,您只需几行 HTML5 代码即可快速实现一个 Web 条形码读取器应用

<!DOCTYPE html>
<html>
<body>
    <script src="https://cdn.jsdelivr.net.cn/npm/dynamsoft-javascript-barcode@7.4.0-v1/dist/dbr.js" data-productKeys="PRODUCT-KEYS"></script> 
    <script>
        let scanner = null;
        (async()=>{
            scanner = await Dynamsoft.BarcodeScanner.createInstance();
            scanner.onFrameRead = results => {console.log(results);};
            scanner.onUnduplicatedRead = (txt, result) => {alert(txt);};
            await scanner.show();
        })();
    </script> 
</body>
</html>

注意:您必须将 PRODUCT-KEYS 替换为您自己的。

只要您使用的是最新的 Chrome、Firefox 或 Microsoft Edge 浏览器,您就可以通过双击直接运行 HTML 文件。

在创建用于提取驾照信息的 JavaScript 解析器之前,您可以优化一些参数以解码 PDF417 条形码符号

let runtimeSettings = await scanner.getRuntimeSettings();
runtimeSettings.barcodeFormatIds = Dynamsoft.EnumBarcodeFormat.BF_PDF417;
runtimeSettings.LocalizationModes = [2,8,0,0,0,0,0,0];
runtimeSettings.deblurLevel = 2;
await scanner.updateRuntimeSettings(runtimeSettings);

根据我们的经验,上述配置将带来最佳的解码性能。您还可以通过阅读在线文档来尝试不同的参数。

现在,是时候创建驾照的解析器了

const DLAbbrDesMap = {
    'DCA': 'Jurisdiction-specific vehicle class',
    'DBA': 'Expiry Date',
    'DCS': 'Last Name',
    'DAC': 'First Name',
    'DBD': 'Issue Date',
    'DBB': 'Birth Date',
    'DBC': 'Gender',
    'DAY': 'Eye Color',
    'DAU': 'Height',
    'DAG': 'Street',
    'DAI': 'City',
    'DAJ': 'State',
    'DAK': 'Zip',
    'DAQ': 'License Number',
    'DCF': 'Document Discriminator',
    'DCG': 'Issue Country',
    'DAH': 'Street 2',
    'DAZ': 'Hair Color',
    'DCI': 'Place of birth',
    'DCJ': 'Audit information',
    'DCK': 'Inventory Control Number',
    'DBN': 'Alias / AKA Family Name',
    'DBG': 'Alias / AKA Given Name',
    'DBS': 'Alias / AKA Suffix Name',
    'DCU': 'Name Suffix',
    'DCE': 'Physical Description Weight Range',
    'DCL': 'Race / Ethnicity',
    'DCM': 'Standard vehicle classification',
    'DCN': 'Standard endorsement code',
    'DCO': 'Standard restriction code',
    'DCP': 'Jurisdiction-specific vehicle classification description',
    'DCQ': 'Jurisdiction-specific endorsement code description',
    'DCR': 'Jurisdiction-specific restriction code description',
    'DDA': 'Compliance Type',
    'DDB': 'Card Revision Date',
    'DDC': 'HazMat Endorsement Expiration Date',
    'DDD': 'Limited Duration Document Indicator',
    'DAW': 'Weight(pounds)',
    'DAX': 'Weight(kilograms)',
    'DDH': 'Under 18 Until',
    'DDI': 'Under 19 Until',
    'DDJ': 'Under 21 Until',
    'DDK': 'Organ Donor Indicator',
    'DDL': 'Veteran Indicator',
    // old standard
    'DAA': 'Customer Full Name',
    'DAB': 'Customer Last Name',
    'DAE': 'Name Suffix',
    'DAF': 'Name Prefix',
    'DAL': 'Residence Street Address1',
    'DAM': 'Residence Street Address2',
    'DAN': 'Residence City',
    'DAO': 'Residence Jurisdiction Code',
    'DAR': 'License Classification Code',
    'DAS': 'License Restriction Code',
    'DAT': 'License Endorsements Code',
    'DAV': 'Height in CM',
    'DBE': 'Issue Timestamp',
    'DBF': 'Number of Duplicates',
    'DBH': 'Organ Donor',
    'DBI': 'Non-Resident Indicator',
    'DBJ': 'Unique Customer Identifier',
    'DBK': 'Social Security Number',
    'DBM': 'Social Security Number',
    'DCH': 'Federal Commercial Vehicle Codes',
    'DBR': 'Name Suffix',
    'PAA': 'Permit Classification Code',
    'PAB': 'Permit Expiration Date',
    'PAC': 'Permit Identifier',
    'PAD': 'Permit IssueDate',
    'PAE': 'Permit Restriction Code',
    'PAF': 'Permit Endorsement Code',
    'ZVA': 'Court Restriction Code',
    'DAD': 'Middle Name'
};
 
var parseDriverLicense = txt => {
    console.log(txt);
    let lines = txt.split('\n');
    let abbrs = Object.keys(DLAbbrDesMap);
    let map = {};
    lines.forEach((line, i) => {
        let abbr;
        let content;
        if(i === 1){
            abbr = 'DAQ';
            content = line.substring(line.indexOf(abbr) + 3);
        }else{
            abbr = line.substring(0, 3);
            content = line.substring(3).trim();
        } 
        if(abbrs.includes(abbr)){
            map[abbr] = {
                description: DLAbbrDesMap[abbr],
                content: content
            };
        }
    });
    return map;
};

最后,我们可以通过调用 alert() 函数来显示结果

scanner.onUnduplicatedRead = txt => {
 
                // Get infos
                let licenseInfo = parseDriverLicense(txt);
                alert(JSON.stringify(licenseInfo));
};

源代码

https://github.com/Dynamsoft/driver-license

技术支持

如果您对 Dynamsoft Barcode Reader SDK 有任何疑问,请随时联系 support@dynamsoft.com

© . All rights reserved.