Building a Flutter VIN Scanner for Windows Using Dynamsoft Capture Vision C++ SDK

The VIN (Vehicle Identification Number) is a unique 17-character code assigned to every motor vehicle. It serves as a fingerprint for the vehicle, providing essential information such as the manufacturer, model, engine type, year of production, and country of origin. In this tutorial, we will demonstrate how to perform VIN recognition on Windows using Flutter and the Dynamsoft Capture Vision C++ SDK.

Flutter Windows VIN Scanner Demo

Prerequisites

  • License Key: Obtain a valid license key for the Dynamsoft Capture Vision SDK.
  • Dynamsoft Capture Vision for Python: Download the tar.gz file and extract it. This source code package for Python includes all necessary header files and libraries for C++.
  • flutter_ocr_sdk: Clone the Flutter plugin project originally developed for MRZ recognition using the Dynamsoft Label Recognizer. Dynamsoft Label Recognizer is now a core part of Dynamsoft Capture Vision. We will modify the plugin to support VIN recognition.

Linking Dynamsoft Capture Vision C++ SDK to Flutter in CMakeLists.txt

  1. Copy the .h, .lib, and .dll files from the Dynamsoft Capture Vision for Python package into the appropriate folders under flutter_ocr_sdk/windows:

    • Place header files (.h) in the include folder.
    • Place library files (.lib) in the lib folder.
    • Place dynamic link libraries (.dll) in the bin folder.
  2. Modify the CMakeLists.txt file to link the C++ SDK with your Flutter project.

     target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin "DynamsoftCorex64" "DynamsoftLicensex64" "DynamsoftCaptureVisionRouterx64" "DynamsoftUtilityx64")
    
  3. Open the windows/include/dlr_manager.h file and update the included header files and namespaces to use the Dynamsoft Capture Vision SDK.

     #include "DynamsoftCaptureVisionRouter.h"
     #include "DynamsoftUtility.h"
        
     using namespace std;
     using namespace dynamsoft::license;
     using namespace dynamsoft::cvr;
     using namespace dynamsoft::dlr;
     using namespace dynamsoft::utility;
     using namespace dynamsoft::basic_structures;
    

Loading VIN Recognition Model

  1. Copy the VIN model files from dynamsoft_capture_vision_bundle-2.4.2100/CharacterModel/ to the flutter_ocr_sdk/lib/model/vin folder.

    VIN model files

  2. Use the VINScanner.json template from dynamsoft_capture_vision_bundle-2.4.2100/resource/Templates/ as a reference to create a new VIN.json file.

     {
       "LabelRecognizerTaskSettingOptions": [
         {
           "Name": "task_vin_text",
           "TextLineSpecificationNameArray": [
             "tls_vin_text"
           ],
           "SectionImageParameterArray": [
             {
               "Section": "ST_REGION_PREDETECTION",
               "ImageParameterName": "ip_recognize_text"
             },
             {
               "Section": "ST_TEXT_LINE_LOCALIZATION",
               "ImageParameterName": "ip_recognize_text"
             },
             {
               "Section": "ST_TEXT_LINE_RECOGNITION",
               "ImageParameterName": "ip_recognize_text"
             }
           ]
         }
       ],
       "TextLineSpecificationOptions": [
         {
           "Name": "tls_vin_text",
           "CharacterModelName": "VIN",
           "StringRegExPattern": "[0-9A-HJ-NPR-Z]{9}[1-9A-HJ-NPR-TV-Y][0-9A-HJ-NPR-Z]{2}[0-9]{5}",
           "CharHeightRange": [
             5,
             1000,
             1
           ],
           "StringLengthRange": [
             17,
             17
           ],
           "OutputResults": 1
         }
       ],
       "CaptureVisionTemplates": [
         {
           "Name": "ReadVINText",
           "ImageROIProcessingNameArray": [
             "roi_vin_text"
           ]
         }
       ],
       "ImageParameterOptions": [
         {
           "Name": "ip_recognize_text",
           "TextDetectionMode": {
             "Mode": "TTDM_LINE",
             "Direction": "HORIZONTAL",
             "CharHeightRange": [
               5,
               1000,
               1
             ],
             "Sensitivity": 7
           },
           "GrayscaleTransformationModes": [
             {
               "Mode": "GTM_ORIGINAL"
             },
             {
               "Mode": "GTM_INVERTED"
             }
           ]
         }
       ],
       "TargetROIDefOptions": [
         {
           "Name": "roi_vin_text",
           "TaskSettingNameArray": [
             "task_vin_text"
           ]
         }
       ],
       "CharacterModelOptions": [
         {
           "Name": "VIN",
           "DirectoryPath": "",
           "CharSet": {
             "ExcludeChars": [
               "O",
               "Q",
               "I"
             ]
           }
         }
       ]
     }
    

    Note: The parameters in this template are tailored for OCR only. To successfully load the model, the DirectoryPath must be dynamically assigned.

  3. Add the model directories to the assets section of the pubspec.yaml file to package the resources with the app.

     assets:
         - lib/model/mrz/
         - lib/model/vin/
    
  4. In lib/flutter_ocr_sdk_method_channel.dart, update the loadModel function to load the VIN template file and set the DirectoryPath dynamically.

     Future<int?> loadModel({ModelType modelType = ModelType.mrz}) async {
         var directory =
         'data/flutter_assets/packages/flutter_ocr_sdk/lib/model/mrz/';
         String modelPath = 'packages/flutter_ocr_sdk/lib/model/mrz/';
         var fileName = "MRZ";
         var templateName = 'MRZ.json';
    
         if (modelType == ModelType.vin) {
             fileName = "VIN";
             templateName = 'VIN.json';
             modelPath = 'packages/flutter_ocr_sdk/lib/model/vin/';
             directory =
               './data/flutter_assets/packages/flutter_ocr_sdk/lib/model/vin/';
         }
    
         ...
            
         var templatePath = join(modelPath, templateName);
         String template = await loadAssetString(templatePath);
    
         if (isDesktop) {
           String exePath = Platform.resolvedExecutable;
           String exeDir = dirname(exePath);
           String assetPath = join(exeDir, directory);
           var templateMap = json.decode(template);
           templateMap['CharacterModelOptions'][0]['DirectoryPath'] = assetPath;
        
           await methodChannel
               .invokeMethod('loadModel', {'template': json.encode(templateMap)});
         }
     }
    

    Explanation

    • Platform.resolvedExecutable: Fetches the path of the executable file.
    • dirname: Retrieves the directory containing the executable.
    • The absolute path of the model directory is dynamically constructed and assigned to the DirectoryPath field in the JSON template.

Implementing VIN Recognition in C++

When implementing VIN recognition in C++ for a Flutter application, it’s essential to ensure that CPU-intensive tasks are executed on separate threads to prevent blocking the main thread. Dynamsoft Capture Vision SDK simplifies this by providing asynchronous APIs, minimizing the need for manual threading.

Here are the steps to implement VIN recognition in dlr_manager.h:

  1. Create a MyCapturedResultReceiver class to receive the VIN recognition results and send them back to Dart.

     class MyCapturedResultReceiver : public CCapturedResultReceiver
     {
     public:
         vector<CRecognizedTextLinesResult *> results;
         vector<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>> pendingResults = {};
         EncodableList out;
        
     public:
         void OnRecognizedTextLinesReceived(CRecognizedTextLinesResult *pResult) override
         {
             WrapResults(pResult);
         }
        
         void sendResult()
         {
             std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result = std::move(pendingResults.front());
             pendingResults.erase(pendingResults.begin());
             result->Success(out);
             out.clear();
         }
        
         void WrapResults(CRecognizedTextLinesResult *pResults)
         {
        
             if (!pResults)
             {
                 return;
             }
        
             int count = pResults->GetItemsCount();
        
             for (int i = 0; i < count; i++)
             {
                 EncodableList area;
        
                 const CTextLineResultItem *result = pResults->GetItem(i);
                 CPoint *points = result->GetLocation().points;
        
                 int x1 = points[0][0];
                 int y1 = points[0][1];
                 int x2 = points[1][0];
                 int y2 = points[1][1];
                 int x3 = points[2][0];
                 int y3 = points[2][1];
                 int x4 = points[3][0];
                 int y4 = points[3][1];
        
                 EncodableMap map;
                 map[EncodableValue("confidence")] = EncodableValue(result->GetConfidence());
                 map[EncodableValue("text")] = EncodableValue(result->GetText());
                 map[EncodableValue("x1")] = EncodableValue(x1);
                 map[EncodableValue("y1")] = EncodableValue(y1);
                 map[EncodableValue("x2")] = EncodableValue(x2);
                 map[EncodableValue("y2")] = EncodableValue(y2);
                 map[EncodableValue("x3")] = EncodableValue(x3);
                 map[EncodableValue("y3")] = EncodableValue(y3);
                 map[EncodableValue("x4")] = EncodableValue(x4);
                 map[EncodableValue("y4")] = EncodableValue(y4);
                 area.push_back(map);
        
                 out.push_back(area);
             }
         }
     };
    

    Explanation

    • OnRecognizedTextLinesReceived: Callback invoked when recognition results are ready.
    • WrapResults: Converts the results into a Dart-compatible format.
    • sendResult: Sends results back to Dart using the Flutter method channel.
  2. Create a MyImageSourceStateListener class to observe the state of the recognition task:

     class MyImageSourceStateListener : public CImageSourceStateListener
     {
     private:
         CCaptureVisionRouter *m_router;
         MyCapturedResultReceiver *m_receiver;
        
     public:
         MyImageSourceStateListener(CCaptureVisionRouter *router, MyCapturedResultReceiver *receiver)
         {
             m_router = router;
             m_receiver = receiver;
         }
        
         void OnImageSourceStateReceived(ImageSourceState state)
         {
             if (state == ISS_EXHAUSTED)
             {
                 m_router->StopCapturing();
                 m_receiver->sendResult();
             }
         }
     };
    

    Explanation

    • ISS_EXHAUSTED: Indicates the task is completed, and results can be sent back to Dart.
  3. Set the license key, initialize MyCapturedResultReceiver, CImageSourceStateListener, CFileFetcher and CCaptureVisionRouter in the int Init(const char *license) function:

     MyCapturedResultReceiver *capturedReceiver;
     CImageSourceStateListener *listener;
     CFileFetcher *fileFetcher;
     CCaptureVisionRouter *cvr;
    
     int Init(const char *license)
     {
         char errorMsgBuffer[512];
         int ret = CLicenseManager::InitLicense(license, errorMsgBuffer, 512);
         printf("InitLicense: %s\n", errorMsgBuffer);
    
         cvr = new CCaptureVisionRouter;
    
         fileFetcher = new CFileFetcher();
         ret = cvr->SetInput(fileFetcher);
         if (ret)
         {
             printf("SetInput error: %d\n", ret);
         }
    
         capturedReceiver = new MyCapturedResultReceiver;
         ret = cvr->AddResultReceiver(capturedReceiver);
         if (ret)
         {
             printf("AddResultReceiver error: %d\n", ret);
         }
    
         listener = new MyImageSourceStateListener(cvr, capturedReceiver);
         ret = cvr->AddImageSourceStateListener(listener);
         if (ret)
         {
             printf("AddImageSourceStateListener error: %d\n", ret);
         }
    
         return ret;
     }
    

    Explanation

    • CCaptureVisionRouter: Manages the recognition process.
    • CFileFetcher: Loads the image file or buffer.
  4. Add functions to handle VIN recognition from an image file or a camera feed.

     void start()
     {
         char errorMsg[512] = {0};
         int errorCode = cvr->StartCapturing("", false, errorMsg, 512);
         if (errorCode != 0)
         {
             printf("StartCapturing: %s\n", errorMsg);
         }
     }
    
     void RecognizeFile(std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> &pendingResult, const char *filename)
     {
         printf("RecognizeFile: %s\n", filename);
         capturedReceiver->pendingResults.push_back(std::move(pendingResult));
         fileFetcher->SetFile(filename);
         start();
     }
    
     void RecognizeBuffer(std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> &pendingResult, const unsigned char *buffer, int width, int height, int stride, int format)
     {
         capturedReceiver->pendingResults.push_back(std::move(pendingResult));
         CImageData *imageData = new CImageData(stride * height, buffer, width, height, stride, getPixelFormat(format));
         fileFetcher->SetFile(imageData);
         delete imageData;
    
         start();
     }
    

    Explanation

    • StartCapturing: Starts a recognition task.

Updating Dart Code for MRZ and VIN Recognition

With the Flutter plugin complete, you can now update the example app to support both MRZ and VIN recognition.

  1. Create a toggle button to switch between MRZ and VIN recognition in example/lib/home_page.dart:

     bool isMrzSelected = true;
        
     FlutterOcrSdk detector = FlutterOcrSdk();
     ModelType model = ModelType.mrz;
    
     Future<void> switchModel(ModelType newModel) async {
       model = newModel;
       await detector.loadModel(modelType: newModel);
     }
    
     return Scaffold(
       body: Column(
         children: [
           title,
           description,
           Center(
             child: ToggleButtons(
               borderRadius: BorderRadius.circular(10),
               isSelected: [isMrzSelected, !isMrzSelected],
               selectedColor: Colors.white,
               fillColor: Colors.orange,
               color: Colors.grey,
               children: const [
                 Padding(
                   padding: EdgeInsets.symmetric(horizontal: 20),
                   child: Text('MRZ'),
                 ),
                 Padding(
                   padding: EdgeInsets.symmetric(horizontal: 20),
                   child: Text('VIN'),
                 ),
               ],
               onPressed: (index) {
                 setState(() {
                   isMrzSelected = (index == 0);
                   if (isMrzSelected) {
                     switchModel(ModelType.mrz);
                   } else {
                     switchModel(ModelType.vin);
                   }
                 });
               },
             ),
           ),
           ...
         ],
       ),
     );
    
  2. Build and run the Flutter Windows app:

     flutter run -d windows
    
  3. Press the Image Scan button to recognize VIN from an image file.

    Flutter Windows VIN reader

  4. Press the Camera Scan button to recognize VIN from a live camera feed.

    Flutter Windows VIN scanner

Source Code

https://github.com/yushulx/flutter_ocr_sdk/tree/DCV/example