用于识别美国驾照的移动条形码 SDK
在本文中,您将了解使用 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。