How to Build a Web App to Scan an ID Card via Camera

An identity document or ID card is any document that may be used to prove a person’s identity. There are various forms of identity documents: driver’s license, passport and formal identity card.

Barcodes and MRZ (machine-readable zones) are often printed on an ID card so that its info can be extracted using a machine.

Driver’s license example:

driver's license

Formal ID Card example:

id card

ID cards are often scanned through a camera or a flatbed scanner. In this article, we are going to build a web app to scan ID cards via cameras.

The following SDKs by Dynamsoft are used:

Online demo

New HTML File

Create a new HTML file with the following template:

<!DOCTYPE html>
<html>
<head>
  <title>ID Card Scanner</title>
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
  <style></style>
</head>
<body>
  <script type="text/javascript"></script>
</body>
</html>

Include Libraries

Include the libraries via CDN by adding the following in the head:

<script src="https://cdn.jsdelivr.net/npm/dynamsoft-core@3.0.33/dist/core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-license@3.0.40/dist/license.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-barcode-reader@10.0.21/dist/dbr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-label-recognizer@3.0.30/dist/dlr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-normalizer@2.0.20/dist/ddn.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-code-parser@2.0.20/dist/dcp.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-router@2.0.32/dist/cvr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@4.0.2/dist/dce.js"></script>

Layout Design

We are going to adopt the following design:

desktop design

In the left, there is a sidebar to select the camera and its resolution, and a button to start the camera or capture a frame. In the right, there is a viewer for scanned documents and the camera, and a few buttons to perform operations.

If using a mobile phone, use the following layout:

mobile design

When the page is opened, it will prompt the user to fill in the license to use the products. You can apply for licenses here.

license modal

When the ID card’s info is extracted, use a modal to display the results.

result modal

Code:

<!DOCTYPE html>
<html>
<head>
  <title>ID Card Scanner</title>
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
  <style>
    .scanner {
      display: flex;
    }

    .options {
      flex-basis: 30%;
      background: #F0EDE9;
      overflow: auto;
    }

    .viewer {
      flex-basis: 70%;
      overflow: hidden;
    }

    #documentViewer {
      display: flex;
      flex-wrap: wrap;
      overflow: auto;
      border: 1px solid gray;
      border-radius: 0.5%;
      width: 100%;
      height: 100%;
    }

    #documentViewer canvas{
      width: 30%;
      height: 50%;
      object-fit: contain;
      border: 1px solid gray;
      margin: 5px;
    }

    #documentViewer canvas:hover{
      background-color: azure;
    }

    #documentViewer canvas.selected{
      border: 1px solid orange;
      background-color: azure;
    }

    .dce-video-container, .dce-image-container {
      position: relative;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
    }

    .dce-image-container {
      display: none;
    }

    #confirmBtn {
      position: absolute;
      left: 0; 
      z-index: 999;
    }
    
    #cancelBtn {
      position: absolute;
      right: 0;
      z-index: 999;
    }

    .navbar {
      display: flex;
      align-items: center;
      height: 50px;
      background: black;
      width: 100%;
    }

    body {
      margin: 0;
    }

    .fullwidth {
      width: 100%;
    }

    .title {
      margin-left: 8px;
      text-decoration: none;
      color: white;
      font-family: sans-serif;
      font-size: larger;
      text-transform: uppercase;
    }

    .scanner {
      min-height: 320px;
      height: calc(100vh - 50px);
    }

    .section {
      padding: 8px;
    }

    .viewer .section {
      height: calc(100% - 16px);
    }

    .options select {
      width: 100%;
      height: 30px;
    }

    .options div {
      margin-bottom: 10px;
    }

    .d-primary-btn {
      display: inline-block;
      background-color: #fe8e14;
      color: #fff;
      text-align: center;
      cursor: pointer;
      transition: ease-in .2s all;
      font-family: "sans-serif"
    }

    .d-secondary-btn {
      display: inline-block;
      background-color: transparent;
      color: #fe8e14;
      text-align: center;
      cursor: pointer;
      font-family: "sans-serif"
    }

    @media(any-hover:hover){
      .d-primary-btn:hover {
        box-shadow: -4px 4px 0 0 #000;
        transform: translate(4px,-4px);
      }
      .d-secondary-btn:hover {
        color: #fea543;
      }
    }

    .d-primary-btn:active {
      color: #fea543;
    }

    .d-secondary-btn:active {
      color: #fea543;
    }

    .actions {
      display: flex;
      justify-content: flex-start;
      align-items: center;
      overflow: auto;
      height: 40px;
      white-space: nowrap;
    }

    .actions .d-primary-btn {
      padding: 5px 10px;
      margin-right: 5px;
    }

    #enhancerUIContainer {
      display: none;
    }

    .main-view {
      height: calc(100% - 40px);
    }

    .ml-10 {
      margin-left: 10px;
    }

    .modal {
      display: flex;
      align-items: flex-start;
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      min-width: 250px;
      min-height: 150px;
      border: 1px solid gray;
      border-radius: 5px;
      background: white;
      z-index: 9999;
      padding: 10px;
      visibility: hidden;
    }

    .input-modal.active {
      visibility: inherit;
    }

    .result-modal {
      overflow: auto;
      max-height: 50%;
      max-width: 80%;
    }

    .result-modal.active {
      visibility: inherit;
    }

    .result-modal li {
      margin-bottom: 10px;
    }

    .result-modal img {
      max-width:100px;
    }

    .input-modal input {
      width: calc(100% - 10px);
    }

    .progress-modal {
      align-items: center;
      text-align: center;
      justify-content: center;
      min-height: 50px;
      max-height: 100px;
    }

    .progress-modal.active {
      visibility: inherit;
    }

    table, th, td {
      border: 1px solid black;
      border-collapse: collapse;
    }

    @media screen and (max-device-width: 600px){
      .scanner {
        flex-wrap: wrap;
      }
      .options {
        flex-basis: 100%;
      }
      .viewer {
        flex-basis: 100%;
      }

      .scanner {
        height: auto;
      }

      .main-view {
        height: 300px;
      }

      #documentViewer canvas{
        width: calc(100% - 10px);
        height: 100%;
      }
    }
  </style>
</head>
<body>
  <nav class="navbar">
    <a class="title" href="#">ID Card Scanner</a>
  </nav>
  <div class="container">
    <div class="scanner">
      <div class="options">
        <div class="section">
          <div>
            <label>
              Camera:
              <select id="select-camera"></select>
            </label>
          </div>
          <div>
            <label>
              Resolution:
              <select id="select-resolution">
                <option value="640x480">640x480</option>
                <option value="1280x720">1280x720</option>
                <option value="1920x1080" selected>1920x1080</option>
                <option value="3840x2160">3840x2160</option>
              </select>
            </label>
          </div>
          <div>
            <input type="checkbox" id="autoCropAndCapture"/>
            <label for="autoCropAndCapture">
              Auto Crop and Capture
            </label>
          </div>
          <div>
            <a id="cameraBtn" onclick="performCameraAction();" class="d-primary-btn fullwidth" style="padding-top:5px;padding-bottom:5px;">Start Camera</a>
          </div>
        </div>
      </div>
      <div class="viewer">
        <div class="section" >
          <div id="viewerContainer" class="main-view">
            <div id="documentViewer"></div>
          </div>
          <div id="enhancerUIContainer" class="main-view">
            <div class="dce-video-container"></div>
            <div class="dce-image-container">
              <button id="confirmBtn">Confirm</button>
              <button id="cancelBtn">Cancel</button>
            </div>
          </div>
          <div class="actions">
            <span style="margin-right:10px;">For Selected:</span>
            <a class="d-primary-btn" onclick="deleteSelected();">Delete</a>
            <a class="d-primary-btn" onclick="editSelected();">Edit</a>
            <a class="d-primary-btn" onclick="readSelected();">Read</a>
            <a onclick="triggerFileInput();" class="d-secondary-btn" style="margin-top:5px;">Import Local Image</a>
            <input style="display:none;" type="file" id="file" onchange="loadImageFromFile();" accept=".jpg,.jpeg,.png,.bmp" />
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="modal input-modal">
    <div>
      <div>
        Please input your Dynamsoft Capture Vision's license (<a href="https://www.dynamsoft.com/customer/license/trialLicense" target="_blank">apply</a> or use the one-day trial):
      </div>
      <br/>
      <label>
        Dynamsoft Capture Vision:
      </label>
      <br/>
      <input type="text" id="dcvLicense"/>
      <br/>
      <button id="saveLicenseBtn">Save</button>
    </div>
  </div>
  <div class="modal result-modal">
    <div>
      <p>Results:</p>
      <ol id="results"></ol>
      <button id="closeBtn">Close</button>
    </div>
  </div>
  <div class="modal progress-modal"></div>
</body>
</html>

Camera Accessing

Next, let’s use Dynamsoft Camera Enhancer to start the camera.

  1. Create an instance of CameraView and bind it to a container for the camera stream.

    let cameraView = await Dynamsoft.DCE.CameraView.createInstance(document.getElementById("enhancerUIContainer"));
    
  2. Create an instance of CameraEnhancer to control the camera.

    let cameraEnhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance(cameraView); //bind the view to the enhancer
    
  3. List the connected cameras.

    async function listCameras(){
      let cameraSelect = document.getElementById("select-camera");
      cameras = await cameraEnhancer.getAllCameras();
      for (let index = 0; index < cameras.length; index++) {
        const camera = cameras[index];
        cameraSelect.appendChild(new Option(camera.label,camera.deviceId));
      }
    }
    
  4. Open the selected camera with the desired resolution.

    async function startCamera(){
      toggleCamera(true);
      let selectedCamera = cameras[document.getElementById("select-camera").selectedIndex];
      let selectedResolution = document.getElementById("select-resolution").selectedOptions[0].value;
      let width = parseInt(selectedResolution.split("x")[0]);
      let height = parseInt(selectedResolution.split("x")[1]);
      await cameraEnhancer.selectCamera(selectedCamera);
      await cameraEnhancer.setResolution({width:width, height:height});
      await cameraEnhancer.open();
    }
    

    Hide the document viewer, display the camera container and toggle the button’s text when the camera is opened:

    function toggleCamera(show){
      let cameraButton = document.getElementById("cameraBtn");
      if (show) {
        document.getElementById("viewerContainer").style.display = "none";
        document.getElementById("enhancerUIContainer").style.display = "block";
        cameraButton.innerText = "Capture";
      }else{
        document.getElementById("viewerContainer").style.display = "block";
        document.getElementById("enhancerUIContainer").style.display = "none";
        cameraButton.innerText = "Start Camera";
      }
    }
    
  5. Capture a camera frame, append it as canvas to the document viewer and close the camera after the capture button is clicked.

    function captureFrame(){
      let image = cameraEnhancer.fetchImage();
      let viewer = document.getElementById("documentViewer");
      let canvas = image.toCanvas();
      viewer.appendChild(canvas);
      toggleCamera(false);
      cameraEnhancer.close();
      viewer.scroll(0,viewer.scrollHeight);
    }
    

Document Detection and Cropping

We can detect ID documents in live, automatically capture a frame and crop the document image. This can be done using Dynamsoft Document Normalizer.

  1. Create a capture vision router instance to call Dynamsoft’s image processing SDKs.

    let router;
    init();
    async function init(){
      Dynamsoft.Core.CoreModule.loadWasm(["DDN","DBR","DLR"]);
      router = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();
    }
    
  2. Start an interval to detect documents in the camera stream.

    let processing;
    let interval;
    async function startLiveDetection(){
      //update the runtime settings for detecting documents
      await router.initSettings("{\"CaptureVisionTemplates\": [{\"Name\": \"Default\"},{\"Name\": \"DetectDocumentBoundaries_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-document-boundaries\"]},{\"Name\": \"DetectAndNormalizeDocument_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-and-normalize-document\"]},{\"Name\": \"NormalizeDocument_Binary\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-binary\"]},  {\"Name\": \"NormalizeDocument_Gray\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-gray\"]},  {\"Name\": \"NormalizeDocument_Color\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-color\"]}],\"TargetROIDefOptions\": [{\"Name\": \"roi-detect-document-boundaries\",\"TaskSettingNameArray\": [\"task-detect-document-boundaries\"]},{\"Name\": \"roi-detect-and-normalize-document\",\"TaskSettingNameArray\": [\"task-detect-and-normalize-document\"]},{\"Name\": \"roi-normalize-document-binary\",\"TaskSettingNameArray\": [\"task-normalize-document-binary\"]},  {\"Name\": \"roi-normalize-document-gray\",\"TaskSettingNameArray\": [\"task-normalize-document-gray\"]},  {\"Name\": \"roi-normalize-document-color\",\"TaskSettingNameArray\": [\"task-normalize-document-color\"]}],\"DocumentNormalizerTaskSettingOptions\": [{\"Name\": \"task-detect-and-normalize-document\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect-and-normalize\"}]},{\"Name\": \"task-detect-document-boundaries\",\"TerminateSetting\": {\"Section\": \"ST_DOCUMENT_DETECTION\"},\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect\"}]},{\"Name\": \"task-normalize-document-binary\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",   \"ColourMode\": \"ICM_BINARY\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]},  {\"Name\": \"task-normalize-document-gray\",   \"ColourMode\": \"ICM_GRAYSCALE\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]},  {\"Name\": \"task-normalize-document-color\",   \"ColourMode\": \"ICM_COLOUR\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}],\"ImageParameterOptions\": [{\"Name\": \"ip-detect-and-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}},{\"Name\": \"ip-detect\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0,\"ThresholdCompensation\" : 7}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7},\"ScaleDownThreshold\" : 512},{\"Name\": \"ip-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}}]}");
      stopLiveDetection();
      interval = setInterval(processFrame,50);
    }
    
    function stopLiveDetection(){
      clearInterval(interval);
      processing = false;
      previousDetectionResults = [];
      drawingLayer.clearDrawingItems();
    }
       
    async function processFrame(){
      if (processing == true) {
        return;
      }
      processing = true;
      let image = cameraEnhancer.fetchImage();
      let result = await router.capture(image,"DetectDocumentBoundaries_Default");
      for (let index = 0; index < result.items.length; index++) {
        const detectionResult = result.items[index];
        if (detectionResult.type ==  Dynamsoft.Core.EnumCapturedResultItemType.CRIT_DETECTED_QUAD) {
          drawOverlay(detectionResult);
          if (steady(detectionResult)) {
            stopLiveDetection();
            appendCroppedFrame(image,detectionResult);
            cameraEnhancer.close();
          }
        }
        break;
      }
      processing = false;
    }
    
  3. Draw the overlay for detected documents using Camera Enhancer. A drawing layer is created for this and its style is updated to use a blue stroke and fill.

    let drawingLayer;
    function init(){
      drawingLayer = cameraView.createDrawingLayer();
      let newStyleId = Dynamsoft.DCE.DrawingStyleManager.createDrawingStyle({
          fillStyle: "rgba(100, 75, 245, 0.3)",
          lineWidth: 5,
          paintMode: "strokeAndFill",
          strokeStyle: "rgba(73, 173, 245, 1)"
      });
      drawingLayer.setDefaultStyle(newStyleId);
    }
       
    function drawOverlay(detectionResult,layer){
      let layerToDraw = layer ?? drawingLayer;
      layerToDraw.clearDrawingItems();
      let quadItem = new Dynamsoft.DCE.QuadDrawingItem(
        {points:detectionResult.location.points}
      );
      layerToDraw.addDrawingItem(quadItem);
    }
    
  4. If the document is detected five times and the detected quadrilaterals’ IoUs (intersection over union) are over 90%, we can know that the document image is steady. Then, we can capture a frame and close the camera.

    function steady(detectionResult){
      if (previousDetectionResults.length < 5) {
        previousDetectionResults.push(detectionResult);
        return false;
      }else{
        let smallIoU = false;
        for (let i = 0; i < previousDetectionResults.length; i++) {
          if (smallIoU) {
            break;
          }
          const result1 = previousDetectionResults[i];
          for (let j = 0; j < previousDetectionResults.length; j++) {
            if (i == j) {
              continue;
            }
            const result2 = previousDetectionResults[j];
            let iou = intersectionOverUnion(result1.location.points,result2.location.points);
            if (iou < 0.9) {
              smallIoU = true;
              break;
            }
          }
        }
        if (smallIoU) {
          previousDetectionResults.splice(0,1);
          previousDetectionResults.push(detectionResult);
          return false;
        }else{
          return true;
        }
      }
    }
    

    Helper functions to calculate the IoU:

    function intersectionOverUnion(pts1 ,pts2) {
      let rect1 = getRectFromPoints(pts1);
      let rect2 = getRectFromPoints(pts2);
      return rectIntersectionOverUnion(rect1, rect2);
    }
    
    function rectIntersectionOverUnion(rect1, rect2) {
      let leftColumnMax = Math.max(rect1.left, rect2.left);
      let rightColumnMin = Math.min(rect1.right,rect2.right);
      let upRowMax = Math.max(rect1.top, rect2.top);
      let downRowMin = Math.min(rect1.bottom,rect2.bottom);
    
      if (leftColumnMax>=rightColumnMin || downRowMin<=upRowMax){
        return 0;
      }
    
      let s1 = rect1.width*rect1.height;
      let s2 = rect2.width*rect2.height;
      let sCross = (downRowMin-upRowMax)*(rightColumnMin-leftColumnMax);
      return sCross/(s1+s2-sCross);
    }
    
    function getRectFromPoints(points) {
      if (points[0]) {
        let left;
        let top;
        let right;
        let bottom;
           
        left = points[0].x;
        top = points[0].y;
        right = 0;
        bottom = 0;
    
        points.forEach(point => {
          left = Math.min(point.x,left);
          top = Math.min(point.y,top);
          right = Math.max(point.x,right);
          bottom = Math.max(point.y,bottom);
        });
    
        let r = {
          left: left,
          top: top,
          right: right,
          bottom: bottom,
          width: right - left,
          height: bottom - top
        };
           
        return r;
      }else{
        throw new Error("Invalid number of points");
      }
    }
    
  5. Run perspective transformation to get a normalized document image based on the detection result and append it to the document viewer.

    async function appendCroppedFrame(image,detectionResult){
      let normalized = await normalizedImage(image,detectionResult.location.points);
      let viewer = document.getElementById("documentViewer");
      if (normalized) {
        viewer.appendChild(normalized.toCanvas());
      }
      toggleCamera(false);
      viewer.scroll(0,viewer.scrollHeight);
    }
    
    async function normalizedImage(image,points){
      //update the runtime settings for normalizing the image
      await router.initSettings("{\"CaptureVisionTemplates\": [{\"Name\": \"Default\"},{\"Name\": \"DetectDocumentBoundaries_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-document-boundaries\"]},{\"Name\": \"DetectAndNormalizeDocument_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-and-normalize-document\"]},{\"Name\": \"NormalizeDocument_Binary\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-binary\"]},  {\"Name\": \"NormalizeDocument_Gray\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-gray\"]},  {\"Name\": \"NormalizeDocument_Color\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-color\"]}],\"TargetROIDefOptions\": [{\"Name\": \"roi-detect-document-boundaries\",\"TaskSettingNameArray\": [\"task-detect-document-boundaries\"]},{\"Name\": \"roi-detect-and-normalize-document\",\"TaskSettingNameArray\": [\"task-detect-and-normalize-document\"]},{\"Name\": \"roi-normalize-document-binary\",\"TaskSettingNameArray\": [\"task-normalize-document-binary\"]},  {\"Name\": \"roi-normalize-document-gray\",\"TaskSettingNameArray\": [\"task-normalize-document-gray\"]},  {\"Name\": \"roi-normalize-document-color\",\"TaskSettingNameArray\": [\"task-normalize-document-color\"]}],\"DocumentNormalizerTaskSettingOptions\": [{\"Name\": \"task-detect-and-normalize-document\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect-and-normalize\"}]},{\"Name\": \"task-detect-document-boundaries\",\"TerminateSetting\": {\"Section\": \"ST_DOCUMENT_DETECTION\"},\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect\"}]},{\"Name\": \"task-normalize-document-binary\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",   \"ColourMode\": \"ICM_BINARY\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]},  {\"Name\": \"task-normalize-document-gray\",   \"ColourMode\": \"ICM_GRAYSCALE\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]},  {\"Name\": \"task-normalize-document-color\",   \"ColourMode\": \"ICM_COLOUR\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}],\"ImageParameterOptions\": [{\"Name\": \"ip-detect-and-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}},{\"Name\": \"ip-detect\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0,\"ThresholdCompensation\" : 7}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7},\"ScaleDownThreshold\" : 512},{\"Name\": \"ip-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}}]}");
      let newSettings = await router.getSimplifiedSettings("NormalizeDocument_Color");
      newSettings.roiMeasuredInPercentage = 0;
      newSettings.roi.points = points;
      await router.updateSettings("NormalizeDocument_Color", newSettings);
      let normalizeResult = await router.capture(image, "NormalizeDocument_Color");
      if (normalizeResult.items[0]) {
        return normalizeResult.items[0];
      }else{
        return null;
      }
    }
    

Barcode Reading

Next, after the document image is captrued, we can process it to extract its info.

Let’s use Dynamsoft Barcode Reader to read the PDF417 on driver’s licenses. It is straightforward with the following code:

let barcodeReadingResult = await router.capture(canvas, "ReadBarcodes_Balance");

MRZ Recognizing

Let’s continue to use Dynamsoft Label Recognizer to recognize the MRZ on ID cards.

  1. Update the runtime settings to recognize MRZ.

    await router.initSettings("{\"CaptureVisionTemplates\": [{\"Name\": \"mrz\",\"ImageROIProcessingNameArray\": [\"roi-mrz-passport\"]}],\"TargetROIDefOptions\": [{\"Name\": \"roi-mrz-passport\",\"TaskSettingNameArray\": [\"task-mrz-passport\"]}],\"TextLineSpecificationOptions\": [{\"Name\": \"tls-mrz-text\",\"CharacterModelName\": \"MRZ\",\"StringRegExPattern\": \"([ACI][A-Z<][A-Z<]{3}[A-Z0-9<]{9}[0-9][A-Z0-9<]{15}){(30)}|([0-9]{2}[(01-12)][(01-31)][0-9][MF<][0-9]{2}[(01-12)][(01-31)][0-9][A-Z<]{3}[A-Z0-9<]{11}[0-9]){(30)}|([A-Z<]{30}){(30)}|([ACIV][A-Z<][A-Z<]{3}[A-Z<]{31}){(36)}|([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<]{8}){(36)}|([PV][A-Z<][A-Z<]{3}[A-Z<]{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}[A-Z0-9<]{2}){(44)}\",\"StringLengthRange\": [30,44],\"CharHeightRange\": [5,1000,1],\"BinarizationModes\": [{\"BlockSizeX\": 30,\"BlockSizeY\": 30,\"Mode\": \"BM_LOCAL_BLOCK\",\"MorphOperation\": \"Close\"}]},{\"Name\": \"tls-mrz-passport\",\"StringRegExPattern\": \"(P[A-Z<][A-Z<]{3}[A-Z<]{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)}\",\"StringLengthRange\": [44,44],\"BaseTextLineSpecificationName\": \"tls-mrz-text\"}],\"LabelRecognizerTaskSettingOptions\": [{\"Name\": \"mrz-text-task\",\"TextLineSpecificationNameArray\": [\"tls-mrz-text\"],\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-mrz-text\"},{\"Section\": \"ST_TEXT_LINE_LOCALIZATION\",\"ImageParameterName\": \"ip-mrz-text\"},{\"Section\": \"ST_TEXT_LINE_RECOGNITION\",\"ImageParameterName\": \"ip-mrz-text\"}]},{\"Name\": \"task-mrz-passport\",\"TextLineSpecificationNameArray\": [\"tls-mrz-text\"],\"BaseLabelRecognizerTaskSettingName\": \"mrz-text-task\"}],\"CharacterModelOptions\": [{\"Name\": \"MRZ\"}],\"ImageParameterOptions\": [{\"Name\": \"ip-mrz-text\",\"TextureDetectionModes\": [{\"Mode\": \"TDM_GENERAL_WIDTH_CONCENTRATION\",\"Sensitivity\": 8}],\"TextDetectionMode\": {\"Mode\": \"TTDM_LINE\",\"CharHeightRange\": [20,1000,1],\"Sensitivity\": 7}}]}");
    
  2. Call Dynamsoft Label Recognizer to recognize the MRZ.

    let OCRResult = await router.capture(canvas, "mrz");
    

Parsing

After getting the barcode and MRZ results, we can use Dynamsoft Code Parser to parse the results.

  1. Load specifications.

    await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD1_ID");
    await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD2_FRENCH_ID")
    await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD2_ID")
    await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD2_VISA")
    await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD3_PASSPORT")  
    await Dynamsoft.DCP.CodeParserModule.loadSpec("MRTD_TD3_VISA")
    await Dynamsoft.DCP.CodeParserModule.loadSpec("AAMVA_DL_ID");
    
  2. Create an instance of code parser.

    let parser = await Dynamsoft.DCP.CodeParser.createInstance();
    
  3. Parse the barcode results.

    let readingResults = [];
    let imageIndices = [];
    async function parseBarcodeResult(result){
      for (let index = 0; index < result.items.length; index++) {
        const item = result.items[index];
        if (item.type == Dynamsoft.Core.EnumCapturedResultItemType.CRIT_BARCODE) {
          if (item.formatString.indexOf("PDF417") == -1) {
            continue;
          }
          try {
            let parsedResultItem = await parser.parse(item.bytes);
            readingResults.push(parsedResultItem);
          } catch (error) {
            console.log(error);
          }
        }
      }
    }
    
  4. Parse the OCR results.

    async function parseOCRResult(result){
      let str = "";
      if (result.items.length < 2) {
        return;
      }
      for (let index = 0; index < result.items.length; index++) {
        const item = result.items[index];
        if (item.type == Dynamsoft.Core.EnumCapturedResultItemType.CRIT_TEXT_LINE) {
          if (item.text.length == 30 || item.text.length == 44) {
            str = str + item.text;
          }
        }
      }
      if (str) {
        let parsedResultItem = await parser.parse(str);
        readingResults.push(parsedResultItem);
      }
    }
    
  5. Display the results as tables in a modal.

    let MRZFields = ["documentNumber","passportNumber","issuingState","name","sex","nationality","dateOfExpiry","dateOfBirth"];
    let DLFields = ["licenseNumber","lastName","firstName","city","expirationDate","birthDate","sex","issuedDate"];
    function showResultModal(){
      document.getElementsByClassName("result-modal")[0].classList.add("active");
      let ol = document.createElement("ol");
      ol.id = "results";
      for (let index = 0; index < readingResults.length; index++) {
        const result = readingResults[index];
        let fieldKeyAndValues = [];
        let table = templateTable();
        let body = table.getElementsByTagName("tbody")[0];
        if (result.codeType == "AAMVA_DL_ID") {
          fieldKeyAndValues.push({"field":"type","value":"Driver's License"});
          for (let j = 0; j < DLFields.length; j++) {
            const field = DLFields[j];
            const value = result.getFieldValue(field);
            fieldKeyAndValues.push({"field":field,"value":value});
          }
        }else if (result.codeType.indexOf("MRTD") != -1) {
          fieldKeyAndValues.push({"field":"type","value":"ID Card"});
          for (let j = 0; j < MRZFields.length; j++) {
            const field = MRZFields[j];
            const value = result.getFieldValue(field);
            if (value) {
              fieldKeyAndValues.push({"field":field,"value":value});
            }
          }
        }
        for (let j = 0; j < fieldKeyAndValues.length; j++) {
          let row = document.createElement("tr");
          const item = fieldKeyAndValues[j];
          let fieldCell = document.createElement("td");
          let valueCell = document.createElement("td");
          fieldCell.innerText = item.field;
          valueCell.innerText = item.value;
          row.appendChild(fieldCell);
          row.appendChild(valueCell);
          body.appendChild(row);
        }
        //append image
        let row = document.createElement("tr");
        let fieldCell = document.createElement("td");
        let valueCell = document.createElement("td");
        let canvas = getSelectedCanvas();
        let img = document.createElement("img");
        if (canvas) {
          img.src = canvas.toDataURL();
        }
        fieldCell.innerText = "image";
        valueCell.appendChild(img);
        row.appendChild(fieldCell);
        row.appendChild(valueCell);
        body.appendChild(row);
    
        let li = document.createElement("li");
        li.append(table);
        ol.appendChild(li);
      }
      document.getElementById("results").outerHTML = ol.outerHTML;
    }
    
    function templateTable(){
      let table = document.createElement("table");
      let head = document.createElement("thead");
      let headRow = document.createElement("tr");
      let th1 = document.createElement("th");
      th1.innerText = "Field";
      let th2 = document.createElement("th");
      th2.innerText = "Value";
      headRow.appendChild(th1);
      headRow.appendChild(th2);
      head.appendChild(headRow);
      let body = document.createElement("tbody");
      table.appendChild(head);
      table.appendChild(body);
      return table;
    }
    

Source Code

You can find the source code of the demo in the following repo: https://github.com/tony-xlh/Web-ID-Card-Scanner-via-Camera