Effortlessly Recognize US Driver's Licenses in Android Apps
According to the American Association of Motor Vehicle Administrators (AAMVA) specification, US driver’s licenses store personal information using the PDF417 barcode symbology. Google’s Mobile Vision APIs can recognize PDF417 barcodes and extract driver’s license data, while the combination of Dynamsoft Barcode Reader and Dynamsoft Code Parser achieves the same goal. This article will guide you through integrating driver’s license recognition into Android apps using both solutions.
This article is Part 7 in a 7-Part Series.
- Part 1 - High-Speed Barcode and QR Code Detection on Android Using Camera2 API
- Part 2 - Optimizing Android Barcode Scanning with NDK and JNI C++
- Part 3 - Choosing the Best Tool for High-Density QR Code Scanning on Android: Google ML Kit vs. Dynamsoft Barcode SDK
- Part 4 - The Quickest Way to Create an Android QR Code Scanner
- Part 5 - Real-time QR Code Recognition on Android with YOLO and Dynamsoft Barcode Reader
- Part 6 - Accelerating Android QR Code Detection with TensorFlow Lite
- Part 7 - Effortlessly Recognize US Driver's Licenses in Android Apps
Recognizing Driver’s Licenses on Android
Prerequisites
Sample Driver’s License Image
Below is a sample image of a driver’s license for testing purposes:
Driver’s License Recognition with Google Mobile Vision
Google’s Mobile Vision APIs can detect various mainstream barcode symbologies, including PDF417, which is commonly used on US driver’s licenses. The DriverLicense class provides predefined fields for extracting driver’s license information.
Quick Verification of Google’s API
To quickly test Google’s Mobile Vision API for driver’s license recognition, follow these steps:
- Clone the Android Vision sample code from GitHub.
-
Create a new file named ResultActivity.java to display the driver’s license information:
public class ResultActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tv = new TextView(this); tv.setVerticalScrollBarEnabled(true); tv.setText(""); tv.setMovementMethod(new ScrollingMovementMethod()); Intent intent = getIntent(); if (intent != null) { Barcode.DriverLicense driverLicense = (Barcode.DriverLicense) intent.getParcelableExtra("DriverLicense"); if (driverLicense != null) { String documentType = driverLicense.documentType; tv.append("Document Type:\n" + documentType + "\n\n"); String firstName = driverLicense.firstName; tv.append("First Name:\n" + firstName + "\n\n"); String middleName = driverLicense.middleName; tv.append("Middle Name:\n" + middleName + "\n\n"); String lastName = driverLicense.lastName; tv.append("Last Name:\n" + lastName + "\n\n"); String gender = driverLicense.gender; tv.append("Gender: \n" + gender + "\n\n"); String addressStreet = driverLicense.addressStreet; tv.append("Street:\n" + addressStreet + "\n\n"); String addressCity = driverLicense.addressCity; tv.append("City:\n" + addressCity + "\n\n"); String addressState = driverLicense.addressState; tv.append("State:\n" + addressState + "\n\n"); String addressZip = driverLicense.addressZip; tv.append("Zip:\n" + addressZip + "\n\n"); String licenseNumber = driverLicense.licenseNumber; tv.append("License Number:\n" + licenseNumber + "\n\n"); String issueDate = driverLicense.issueDate; tv.append("Issue Date:\n" + issueDate + "\n\n"); String expiryDate = driverLicense.expiryDate; tv.append("Expiry Date:\n" + expiryDate + "\n\n"); String birthDate = driverLicense.birthDate; tv.append("Birth Date:\n" + birthDate + "\n\n"); String issuingCountry = driverLicense.issuingCountry; tv.append("Issue Country:\n" + issuingCountry + "\n\n"); } } setContentView(tv); } @Override public void onBackPressed() { super.onBackPressed(); } }
-
In
BarcodeCaptureActivity.java
, locate theonBarcodeDetected(Barcode barcode)
function and add the following code to startResultActivity
when aPDF417
barcode is detected:if (barcode.format == Barcode.PDF417) { Barcode.DriverLicense driverLicense = barcode.driverLicense; if (driverLicense != null) { Intent intent = new Intent(BarcodeCaptureActivity.this, ResultActivity.class); intent.putExtra("DriverLicense", driverLicense); startActivity(intent); } }
-
Add the
ResultActivity
to yourAndroidManifest.xml
:<activity android:name=".ResultActivity" />
-
Compile the project and run the app on your device.
Reading Driver’s License with Dynamsoft Barcode Reader and Dynamsoft Code Parser
In this section, you’ll learn how to implement driver’s license recognition step by step using the Dynamsoft Barcode Reader and Dynamsoft Code Parser.
Android Project Configuration
-
In your project’s
build.gradle
file, include the Dynamsoft Maven repository:allprojects { repositories { google() mavenCentral() maven { url "https://download2.dynamsoft.com/maven/aar" } } }
-
In the module’s
build.gradle
file, add the following dependencies:implementation "com.dynamsoft:dynamsoftbarcodereaderbundle:10.2.1100" implementation "com.dynamsoft:dynamsoftcodeparser:2.2.11" implementation "com.dynamsoft:dynamsoftcodeparserdedicator:1.2.20"
Explanation
- The
dynamsoftbarcodereaderbundle
: Provides camera control and barcode decoding functionalities. - The
dynamsoftcodeparser
anddynamsoftcodeparserdedicator
: Used for parsing PDF417 barcodes and extracting driver’s license information.
- The
Activating Dynamsoft Barcode Reader
In the MainActivity.java
file, initialize the license key as follows:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
LicenseManager.initLicense("LICENSE-KEY", this, (isSuccessful, error) -> {
if (!isSuccessful) {
error.printStackTrace();
runOnUiThread(() -> ((TextView) findViewById(R.id.tv_license_error)).setText("License initialization failed: "+error.getMessage()));
}
});
}
...
}
Creating a Fragment for Barcode Scanning
-
Create a new layout file named
fragment_scanner.xml
in thelayout
folder:<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".fragments.ScannerFragment"> <com.dynamsoft.dce.CameraView android:id="@+id/camera_view" android:layout_width="match_parent" android:layout_height="match_parent"/> <TextView android:id="@+id/tv_parsed" android:layout_margin="20dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/white" app:layout_constraintBottom_toBottomOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
The layout includes a
CameraView
for displaying the camera preview and aTextView
showing parsed messages. -
Place a
drivers-license.json
template file in theassets
folder. This template defines how to decode the PDF417 barcode and extract driver’s license information. Below is an example of the template:{ "BarcodeFormatSpecificationOptions" : [ { "Name" : "bfs_pdf_417", "BarcodeBytesLengthRangeArray" : [ { "MaxValue" : 2147483647, "MinValue" : 0 } ], "BarcodeFormatIds" : [ "BF_DEFAULT", "BF_POSTALCODE", "BF_PHARMACODE", "BF_NONSTANDARD_BARCODE", "BF_DOTCODE" ], "BarcodeHeightRangeArray" : null, "BarcodeTextLengthRangeArray" : [ { "MaxValue" : 2147483647, "MinValue" : 0 } ], "MinResultConfidence" : 30, "MirrorMode" : "MM_NORMAL", "PartitionModes" : [ "PM_WHOLE_BARCODE", "PM_ALIGNMENT_PARTITION" ] } ], "BarcodeReaderTaskSettingOptions" : [ { "Name" : "pdf_417_task", "BarcodeColourModes" : [ { "LightReflection" : 1, "Mode" : "BICM_DARK_ON_LIGHT" } ], "BarcodeFormatSpecificationNameArray" : [ "bfs_pdf_417" ], "DeblurModes" : null, "LocalizationModes" : [ { "Mode" : "LM_CONNECTED_BLOCKS" }, { "Mode" : "LM_LINES" }, { "Mode" : "LM_STATISTICS" } ], "MaxThreadsInOneTask" : 1, "SectionImageParameterArray" : [ { "ContinueWhenPartialResultsGenerated" : 1, "ImageParameterName" : "ip_localize_barcode", "Section" : "ST_REGION_PREDETECTION" }, { "ContinueWhenPartialResultsGenerated" : 1, "ImageParameterName" : "ip_localize_barcode", "Section" : "ST_BARCODE_LOCALIZATION" }, { "ContinueWhenPartialResultsGenerated" : 1, "ImageParameterName" : "ip_decode_barcode", "Section" : "ST_BARCODE_DECODING" } ] } ], "CaptureVisionTemplates" : [ { "Name" : "ReadPDF417", "ImageROIProcessingNameArray" : [ "roi_pdf_417" ], "ImageSource" : "", "MaxParallelTasks" : 4, "MinImageCaptureInterval" : 0, "OutputOriginalImage" : 0, "SemanticProcessingNameArray": [ "sp_pdf_417" ], "Timeout" : 10000 } ], "GlobalParameter" : { "MaxTotalImageDimension" : 0 }, "ImageParameterOptions" : [ { "Name" : "ip_localize_barcode", "BinarizationModes" : [ { "BinarizationThreshold" : -1, "BlockSizeX" : 71, "BlockSizeY" : 71, "EnableFillBinaryVacancy" : 0, "GrayscaleEnhancementModesIndex" : -1, "Mode" : "BM_LOCAL_BLOCK", "ThresholdCompensation" : 10 } ], "GrayscaleEnhancementModes" : [ { "Mode" : "GEM_GENERAL" } ] }, { "Name" : "ip_decode_barcode", "ScaleDownThreshold" : 99999 }, { "Name": "ip_recognize_text", "TextDetectionMode": { "Mode": "TTDM_LINE", "Direction": "HORIZONTAL", "CharHeightRange": [ 20, 1000, 1 ], "Sensitivity": 7 } } ], "TargetROIDefOptions" : [ { "Name" : "roi_pdf_417", "TaskSettingNameArray" : [ "pdf_417_task" ] } ], "CharacterModelOptions": [ { "Name" : "NumberLetter" } ], "SemanticProcessingOptions": [ { "Name": "sp_pdf_417", "ReferenceObjectFilter": { "ReferenceTargetROIDefNameArray": [ "roi_pdf_417" ] }, "TaskSettingNameArray": [ "dcp_pdf_417" ] } ], "CodeParserTaskSettingOptions": [ { "Name": "dcp_pdf_417", "CodeSpecifications": ["AAMVA_DL_ID","AAMVA_DL_ID_WITH_MAG_STRIPE","SOUTH_AFRICA_DL"] } ] }
-
Inflate the layout and set up the barcode scanning in the
ScannerFragment.java
file:package com.dynamsoft.dcv.driverslicensescanner.fragments; import android.app.AlertDialog; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.dynamsoft.core.basic_structures.CompletionListener; import com.dynamsoft.cvr.CaptureVisionRouter; import com.dynamsoft.cvr.CaptureVisionRouterException; import com.dynamsoft.cvr.CapturedResultReceiver; import com.dynamsoft.dbr.DecodedBarcodesResult; import com.dynamsoft.dce.CameraEnhancer; import com.dynamsoft.dce.CameraEnhancerException; import com.dynamsoft.dcp.ParsedResult; import com.dynamsoft.dcv.driverslicensescanner.FileUtil; import com.dynamsoft.dcv.driverslicensescanner.MainViewModel; import com.dynamsoft.dcv.driverslicensescanner.ParseUtil; import com.dynamsoft.dcv.driverslicensescanner.R; import com.dynamsoft.dcv.driverslicensescanner.databinding.FragmentScannerBinding; import java.util.Locale; public class ScannerFragment extends Fragment { private static final String TEMPLATE_ASSETS_FILE_NAME = "drivers-license.json"; private static final String TEMPLATE_READ_PDF417 = "ReadPDF417"; private FragmentScannerBinding binding; private CameraEnhancer mCamera; private CaptureVisionRouter mRouter; private MainViewModel viewModel; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentScannerBinding.inflate(inflater, container, false); viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); viewModel.reset(); mCamera = new CameraEnhancer(binding.cameraView, getViewLifecycleOwner()); if (mRouter == null) { initCaptureVisionRouter(); } try { mRouter.setInput(mCamera); } catch (CaptureVisionRouterException e) { e.printStackTrace(); } return binding.getRoot(); } @Override public void onResume() { super.onResume(); try { mCamera.open(); } catch (CameraEnhancerException e) { e.printStackTrace(); } mRouter.startCapturing(TEMPLATE_READ_PDF417, new CompletionListener() { @Override public void onSuccess() { } @Override public void onFailure(int errorCode, String errorString) { requireActivity().runOnUiThread(() -> showDialog("Error", String.format(Locale.getDefault(), "ErrorCode: %d %nErrorMessage: %s", errorCode, errorString))); } }); } @Override public void onPause() { super.onPause(); try { mCamera.close(); } catch (CameraEnhancerException e) { e.printStackTrace(); } mRouter.stopCapturing(); } @Override public void onDestroyView() { super.onDestroyView(); binding = null; } private void initCaptureVisionRouter() { mRouter = new CaptureVisionRouter(requireContext()); try { String template = FileUtil.readAssetFileToString(requireContext(), TEMPLATE_ASSETS_FILE_NAME); mRouter.initSettings(template); } catch (CaptureVisionRouterException e) { e.printStackTrace(); } mRouter.addResultReceiver(new CapturedResultReceiver() { @Override public void onDecodedBarcodesReceived(DecodedBarcodesResult result) { if (result.getItems().length > 0) { viewModel.parsedText = result.getItems()[0].getText(); } } @Override public void onParsedResultsReceived(ParsedResult result) { if (result.getItems().length > 0) { String[] displayStrings = ParseUtil.parsedItemToDisplayStrings(result.getItems()[0]); if (displayStrings == null || displayStrings.length <= 1/*Only have Document Type content*/) { showParsedText(); return; } viewModel.results = displayStrings; requireActivity().runOnUiThread(() -> NavHostFragment.findNavController(ScannerFragment.this) .navigate(R.id.action_ScannerFragment_to_ResultFragment)); mRouter.stopCapturing(); } else { showParsedText(); } } }); } private void showParsedText() { if (viewModel.parsedText != null && !viewModel.parsedText.isEmpty()) { requireActivity().runOnUiThread(() -> { if (binding != null) { binding.tvParsed.setText("Failed to parse the result. The drivers' information does not exist in the barcode! "); } }); } } private void showDialog(String title, String message) { new AlertDialog.Builder(requireContext()) .setCancelable(true) .setPositiveButton("OK", null) .setTitle(title) .setMessage(message) .show(); } }
Explanation
CameraEnhancer
: Manages the camera preview.CaptureVisionRouter
: Handles the capture process, providing callbacks for decoded barcodes and parsed results.initSettings
: Loads the template file from the assets.ParsedResult
: Stores the parsed result.viewModel
: Shares data between fragments.
Creating a Fragment for Displaying Driver’s License Information
-
Create a layout file named
fragment_result.xml
containing aListView
to display the driver’s license information:<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".fragments.ResultFragment"> <ListView android:id="@+id/lv_result" android:padding="10dp" android:layout_width="match_parent" android:layout_height="match_parent"/> </androidx.constraintlayout.widget.ConstraintLayout>
-
Inflate the layout and load data from
viewModel
in theResultFragment.java
file:package com.dynamsoft.dcv.driverslicensescanner.fragments; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.dynamsoft.dcv.driverslicensescanner.MainViewModel; import com.dynamsoft.dcv.driverslicensescanner.databinding.FragmentResultBinding; public class ResultFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentResultBinding binding = FragmentResultBinding.inflate(inflater, container, false); MainViewModel viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, viewModel.results); binding.lvResult.setAdapter(adapter); return binding.getRoot(); } }