How to Build a Simple MRZ and VIN Scanner Application for Android

Dynamsoft’s pre-built Android MRZ (Machine Readable Zone) Scanner component, which wraps low-level Dynamsoft Capture Vision API, is open-source and available for download on GitHub. An Android VIN (Vehicle Identification Number) Scanner has not been officially released yet, but a relevant example project is available. This article will guide you through enhancing the existing Android MRZ Scanner component by adding VIN scanning capability.

Android MRZ/VIN Scanner Demo Video

Prerequisites

  • Free Trial License for Dynamsoft Capture Vision.
  • Android MRZ Scanner: This official repository includes two projects: an Android Module and an Android Application. The module is the MRZ Scanner component, while the application demonstrates how to use it.
  • Android VIN Scanner: An official sample project demonstrating how to scan VINs using Dynamsoft Capture Vision.

Make sure to request a license key and download both projects before getting started.

Importing the Android MRZ Scanner Module

  1. Open the mrz-scanner-mobile\android\samples\ScanMRZ project in Android Studio.
  2. Select File > New > Import Module, and add the mrz-scanner-mobile\android\src\DynamsoftMRZScannerBundle module. Note: the module name is dynamsoftmrzscannerbundle.

    Import Android Module

  3. Modify the dependency in the build.gradle file as follows:

     dependencies {
             //    implementation 'com.dynamsoft:mrzscannerbundle:2.0.1'
         implementation project(':dynamsoftmrzscannerbundle')
     }
    
  4. Build the project to ensure the module source code is ready for integration.

Integrating VIN Recognition Capability into the MRZ Scanner Module

MRZ and VIN recognition rely on model files that are encapsulated into AAR packages. To use them, navigate to the build.gradle file of the dynamsoftmrzscannerbundle module and add the following lines to the dependencies section:

dependencies {
    api "com.dynamsoft:dynamsoftcapturevisionbundle:2.6.1003"
    api 'com.dynamsoft:dynamsoftmrz:3.4.20'
    api "com.dynamsoft:dynamsoftvin:3.4.20"
}

The Dynamsoft Capture Vision engine will automatically load the appropriate model files when you specify a template name in the startCapturing() method. For MRZ recognition, use the template name ReadPassportAndId, and for VIN recognition, use ReadVINText. No major code changes are required. We will only add support for the VIN scanning scenario and create corresponding data structures.

Detection Type Enum

The source code was initially designed for MRZ scanning only. To support VIN scanning, create an EnumDetectionType.java file in the com.dynamsoft.mrzscannerbundle.ui package. This enum will distinguish between detection types:

package com.dynamsoft.mrzscannerbundle.ui;

public enum EnumDetectionType {
    MRZ,
    VIN,
}

VIN Data Structure

Based on MRZData.java and MRZScanResult.java, create VINData.java and VINScanResult.java in the same package. These classes handle VIN data extraction and scan results.

  • VINData.java

      package com.dynamsoft.mrzscannerbundle.ui;
        
      import androidx.annotation.NonNull;
      import androidx.annotation.Nullable;
        
      import java.util.HashMap;
        
      public class VINData {
        
          public String vinString;
          public String wmi;
          public String region;
          public String vds;
          public String checkDigit;
          public String modelYear;
          public String plantCode;
          public String serialNumber;
        
          public static VINData extractItem(@Nullable HashMap<String, String> item) {
              if  (item == null) return null;
        
              VINData data = new VINData();
              data.vinString = item.get("vinString") == null ? "" : item.get("vinString");
              data.wmi = item.get("WMI") == null ? "" : item.get("WMI");
              data.region = item.get("region") == null ? "" : item.get("region");
              data.vds = item.get("VDS") == null ? "" : item.get("VDS");
              data.checkDigit = item.get("checkDigit") == null ? "" : item.get("checkDigit");
              data.modelYear = item.get("modelYear") == null ? "" : item.get("modelYear");
              data.plantCode = item.get("plantCode") == null ? "" : item.get("plantCode");
              data.serialNumber = item.get("serialNumber") == null ? "" : item.get("serialNumber");
        
              return data;
          }
        
          @NonNull
          public String toString() {
              return "VIN String: " + vinString + "\n" +
                      "WMI: " + wmi + "\n" +
                      "Region: " + region + "\n" +
                      "VDS: " + vds + "\n" +
                      "Check Digit: " + checkDigit + "\n" +
                      "Model Year: " + modelYear + "\n" +
                      "Manufacturer plant: " + plantCode + "\n" +
                      "Serial Number: " + serialNumber;
          }
      }
        
    
  • VINScanResult.java

      package com.dynamsoft.mrzscannerbundle.ui;
        
      import static com.dynamsoft.mrzscannerbundle.ui.ScannerActivity.EXTRA_RESULT;
      import static com.dynamsoft.mrzscannerbundle.ui.ScannerActivity.EXTRA_STATUS_CODE;
        
      import android.content.Intent;
        
      import java.util.HashMap;
        
      public class VINScanResult extends CommonResult {
          private VINData vinData;
        
          public VINScanResult(int resultCode, Intent data, EnumDetectionType detectionType) {
              if (data != null) {
                  super.resultStatus = data.getIntExtra(EXTRA_STATUS_CODE, resultCode);
                  assembleMap((HashMap<String, String>) data.getSerializableExtra(EXTRA_RESULT));
                  super.detectionType = detectionType;
              }
          }
        
          @Override
          public void assembleMap(HashMap<String, String> entry) {
              vinData = VINData.extractItem(entry);
          }
        
          public VINData getData() {
              return vinData;
          }
      }
        
    

Common Result Class

Create a shared CommonResult.java to handle common fields and methods for both MRZ and VIN scanning results. This class will be used as a base class for both MRZScanResult and VINScanResult.

package com.dynamsoft.mrzscannerbundle.ui;

import java.util.HashMap;

public class CommonResult {
    protected EnumDetectionType detectionType;
    @EnumResultStatus
    protected int resultStatus;
    protected String errorString;
    protected int errorCode;
    public void assembleMap(HashMap<String, String> entry) {}

    public EnumDetectionType getDetectionType() {
        return detectionType;
    }

    public @interface EnumResultStatus {
        int RS_FINISHED = 0;
        int RS_CANCELED = 1;
        int RS_EXCEPTION = 2;
    }

    @EnumResultStatus
    public int getResultStatus() {
        return resultStatus;
    }

    public String getErrorString() {
        return errorString;
    }

    public int getErrorCode() {
        return errorCode;
    }
}

Scanner Configuration

  1. Rename MrzScannerConfig.java to ScannerConfig.java and modify the class name accordingly. This class is responsible for configuring the scanner settings.
  2. Add support for detection type configuration:

     private EnumDetectionType detectionType = EnumDetectionType.MRZ;
    
     public EnumDetectionType getDetectionType() {
         return detectionType;
     }
    
     public void setDetectionType(EnumDetectionType detectionType) {
         this.detectionType = detectionType;
     }
    
    

Scanner Activity

  1. Rename MrzScannerActivity.java to ScannerActivity.java and modify the class name accordingly. This class is responsible for handling the camera preview and scanning process.
  2. Modify UI and logic based on the detection type:

     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_scanner);
    
         findViewById(R.id.btn_capture).setOnClickListener(v -> {
             enableCapture = true;
         });
    
         ...
    
         switch (configuration.getDetectionType()) {
             case MRZ:
             {
                 guideFrame.setVisibility(isGuideFrameVisible ? View.VISIBLE : View.GONE);
                 try {
                     mCamera.enableEnhancedFeatures(EnumEnhancerFeatures.EF_FRAME_FILTER);
                 } catch (CameraEnhancerException e) {
                     throw new RuntimeException(e);
                 }
             }
                 break;
             case VIN: {
                 guideFrame.setVisibility(View.GONE);
                 try {
                     mCamera.setScanRegion(new DSRect(0.1f, 0.4f, 0.9f, 0.6f, true));
                 } catch (CameraEnhancerException e) {
                     e.printStackTrace();
                 }
             }
                 break;
             default:
                 break;
         }
     }
    
  3. Configure the template for VIN scanning in the configCVR() method:

     private void configCVR() {
         ...
         switch (configuration.getDetectionType()) {
             case MRZ:
             {
                 if (configuration.getDocumentType() != null) {
                     switch (configuration.getDocumentType()) {
                         case DT_ALL:
                             mCurrentTemplate = "ReadPassportAndId";
                             break;
                         case DT_ID:
                             mCurrentTemplate = "ReadId";
                             break;
                         case DT_PASSPORT:
                             mCurrentTemplate = "ReadPassport";
                             break;
                     }
    
                 }
             }
                 break;
             case VIN:
                 mCurrentTemplate = "ReadVINText";
                 break;
         }
         ...
     }
    
  4. Parse the result accordingly:

     @Override
     public CommonResult parseResult(int i, @Nullable Intent intent) {
         switch (config.getDetectionType()) {
             case MRZ: {
                 return new MRZScanResult(i, intent, config.getDetectionType());
             }
    
             case VIN:
             {
                 return new VINScanResult(i, intent, config.getDetectionType());
             }
    
         }
    
         return null;
     }
    

Modifying the Application Project to Support MRZ and VIN Scanning

  1. Add a raido group in activity_main.xml to choose the scanning mode:

     <RadioGroup
         android:id="@+id/radio_group_mode"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
         android:layout_centerHorizontal="true"
         android:layout_marginTop="40dp">
    
         <RadioButton
             android:id="@+id/radio_mrz"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="MRZ"
             android:checked="true"
             android:buttonTint="@color/orange"
             android:textColor="@android:color/black" />
    
         <RadioButton
             android:id="@+id/radio_vin"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="VIN"
             android:buttonTint="@color/orange"
             android:textColor="@android:color/black" />
     </RadioGroup>
    
  2. In MainActivity.java, pass the selected detection type to ScannerActivity:

     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
    
         findViewById(R.id.btn_scan).setOnClickListener(v -> {
             Intent intent = new Intent(MainActivity.this, ScannerActivity.class);
             if (findViewById(R.id.radio_mrz).isChecked()) {
                 intent.putExtra("detectionType", EnumDetectionType.MRZ.name());
             } else {
                 intent.putExtra("detectionType", EnumDetectionType.VIN.name());
             }
             startActivity(intent);
         });
     }
    
  3. Display scan results based on the detection type:

     launcher = registerForActivityResult(new ScannerActivity.ResultContract(), commonResult -> {
             if (commonResult == null) return;
    
             if (commonResult.getResultStatus() == CommonResult.EnumResultStatus.RS_FINISHED) {
                 switch (commonResult.getDetectionType()) {
                     case MRZ: {
                         MRZScanResult result = (MRZScanResult) commonResult;
                         if (result.getData() != null) {
                             MRZData data = result.getData();
                             content.removeAllViews();
                             content.addView(childView("Name:", data.getFirstName() + " " + data.getLastName()));
                             content.addView(childView("Sex:", data.getSex() == null ? ""
                                     : data.getSex().substring(0, 1).toUpperCase() + data.getSex().substring(1)));
                             content.addView(childView("Age:", data.getAge() + ""));
                             content.addView(childView("Document Type:", data.getDocumentType()));
                             content.addView(childView("Document Number:", data.getDocumentNumber()));
                             content.addView(childView("Issuing State:", data.getIssuingState()));
                             content.addView(childView("Nationality:", data.getNationality()));
                             content.addView(childView("Date of Birth(YYYY-MM-DD):", data.getDateOfBirth()));
                             content.addView(childView("Date of Expiry(YYYY-MM-DD):", data.getDateOfExpire()));
                         }
                     }
                     break;
                     case VIN: {
                         VINScanResult result = (VINScanResult) commonResult;
                         if (result.getData() != null) {
                             VINData data = result.getData();
                             content.removeAllViews();
                             content.addView(childView("VIN:", data.vinString));
                             content.addView(childView("WMI:", data.wmi));
                             content.addView(childView("Region:", data.region));
                             content.addView(childView("VDS:", data.vds));
                             content.addView(childView("Check Digit:", data.checkDigit));
                             content.addView(childView("Model Year:", data.modelYear));
                             content.addView(childView("Plant Code:", data.plantCode));
                             content.addView(childView("Serial Number:", data.serialNumber));
                         }
                     }
                     break;
                 }
    
    
             } 
             ...
         });
    

Running the Android MRZ/VIN Scanner Application

Android MRZ Scanner

MRZ Scanner UI

Android MRZ Scanner module

MRZ Result

Android MRZ Scanner

Android VIN Scanner

VIN Scanner UI

Android VIN Scanner module

VIN Result

Android VIN Scanner

Source Code

https://github.com/yushulx/android-camera-barcode-mrz-document-scanner/tree/main/examples/MrzVin