Custom processor on 2-line license plate recognition

Please provide complete information as applicable to your setup.

• Hardware Platform (Jetson / GPU): GPU NVIDIA RTX 3050 6GB, Ubuntu 22.04 Docker
• DeepStream Version: 7.1
• JetPack Version (valid for Jetson only)
• TensorRT Version: v100300
• NVIDIA GPU Driver Version (valid for GPU only): 570.133.07
• Issue Type( questions, new requirements, bugs): Question

Hi everyone, I’m working on a license plate detection + recognition pipeline, run with gst-launch-1.0 command, using YOLO for detection model and LPRNet trained from TAO Toolkit for recognition. My problem is in my country, the license plate have both 1-line and 2-line type, depend on user customization. It make the LPR model works well on 1-line license plate, but in 2-line license plate, not so much (sometimes have output on screen, but all it wrong, or no output, only lp). How can I handle this situation?

This is a gst-launch-1.0 command I’m using:

gst-launch-1.0 -v filesrc location=/opt/nvidia/deepstream/deepstream-7.1/lab/recorded_vms_data/converted1.mp4 ! \
    qtdemux name=demux ! \
    queue ! \
    h264parse ! \
    nvv4l2decoder ! \
    queue ! \
    nvvideoconvert ! \
    "video/x-raw(memory:NVMM), format=NV12, width=1920, height=1080" ! \
    mux.sink_0 nvstreammux name=mux batch-size=1 width=1920 height=1080 live-source=0 ! \
    queue ! \
    nvinferserver config-file-path=config_infer_primary_person_vehicle_detector.txt ! \
    queue ! \
    nvinferserver config-file-path=config_infer_secondary_license_plate_detector.txt ! \
    queue ! \
    nvinferserver config-file-path=config_infer_tertiary_license_plate_recognizer.txt ! \
    queue ! \
    nvdsosd ! \
    nvvideoconvert ! \
    "video/x-raw, format=RGBA" ! \
    nveglglessink sync=false

config_infer_primary_person_vehicle_detector.txt:

infer_config {
  unique_id: 1
  gpu_ids: [0]
  max_batch_size: 1

  backend {
    inputs: [{
      name: "images"
      dims: [3, 640, 640]  # Fixed input size
    }]
    outputs: [{name: "output0"}]  # Match the output layer name
    triton {
      grpc {
         url: "localhost:8001"  # Địa chỉ Triton Server (gRPC)
         enable_cuda_buffer_sharing: false
      }
      model_name: "person_vehicle_detector"
      version: -1
    }
  }

  preprocess {
    network_format: IMAGE_FORMAT_RGB
    tensor_order: TENSOR_ORDER_LINEAR
    tensor_name: "images"
    maintain_aspect_ratio: 1
    frame_scaling_hw: FRAME_SCALING_HW_DEFAULT
    frame_scaling_filter: 2
    normalize {
      scale_factor: 0.003921569  # 1/255
      channel_offsets: [0.0, 0.0, 0.0]
    }
  }

  postprocess {
    labelfile_path: "/opt/nvidia/deepstream/deepstream-7.1/lab/triton_repo/person_vehicle_detector/1/labels.txt"
    detection {
      num_detected_classes: 5
      custom_parse_bbox_func: "NvDsInferParseYolo"
      nms {
        confidence_threshold: 0.3
        iou_threshold: 0.5
        topk: 300
      }
    }
  }

  custom_lib {
    path: "/opt/nvidia/deepstream/deepstream-7.1/lib/libnvdsinfer_custom_impl_Yolo.so"
  }

  extra {
    copy_input_to_host_buffers: 0
    output_buffer_pool_size: 16
  }
}

config_infer_secondary_license_plate_detector.txt:

infer_config {
  unique_id: 2
  gpu_ids: [0]
  max_batch_size: 1

  backend {
    inputs: [{
      name: "images"
      dims: [3, 640, 640]
    }]
    outputs: [{name: "output0"}]
    triton {
      grpc {
        url: "localhost:8001"
        enable_cuda_buffer_sharing: false
      }
      model_name: "license_plate_detector"
      version: -1
    }
  }

  preprocess {
    network_format: IMAGE_FORMAT_RGB
    tensor_order: TENSOR_ORDER_LINEAR
    tensor_name: "images"
    maintain_aspect_ratio: 0
    symmetric_padding: 0
    frame_scaling_hw: FRAME_SCALING_HW_DEFAULT
    frame_scaling_filter: 2
    normalize {
      scale_factor: 0.003921569
      channel_offsets: [0.0, 0.0, 0.0]
    }
  }

  postprocess {
    labelfile_path: "/opt/nvidia/deepstream/deepstream-7.1/lab/triton_repo/license_plate_detector/1/labels.txt"
    detection {
      num_detected_classes: 1
      custom_parse_bbox_func: "NvDsInferParseYolo"
      nms {
        confidence_threshold: 0.3
        iou_threshold: 0.5
        topk: 300
      }
    }
  }

  custom_lib {
    path: "/opt/nvidia/deepstream/deepstream-7.1/lib/libnvdsinfer_custom_impl_Yolo.so"
  }

  extra {
    copy_input_to_host_buffers: 0
    output_buffer_pool_size: 16
  }
}

input_control {
  process_mode: PROCESS_MODE_CLIP_OBJECTS
  operate_on_gie_id: 1
  operate_on_class_ids: [1, 2, 4]
}

config_infer_tertiary_license_plate_recognizer.txt:

infer_config {
  unique_id: 3
  gpu_ids: [0]
  max_batch_size: 1
  backend {
    inputs: [{
      name: "image_input"
      dims: [3, 48, 120]
    }]
    outputs: [{
      name: "tf_op_layer_ArgMax"
    }, {
      name: "tf_op_layer_Max"
    }]
    triton {
      model_name: "license_plate_recognizer"
      version: -1
      grpc {
        url: "localhost:8001"
        enable_cuda_buffer_sharing: false
      }
    }
  }
  preprocess {
    network_format: IMAGE_FORMAT_RGB
    tensor_order: TENSOR_ORDER_LINEAR
    tensor_name: "image_input"
    maintain_aspect_ratio: 0
    symmetric_padding: 0
    normalize {
      scale_factor: 0.00392156862745098
      channel_offsets: [0, 0, 0]
    }
  }
  postprocess {
    classification {
      threshold: 0.7
      custom_parse_classifier_func: "NvDsInferParseCustomNVPlate"
    }
  }
  custom_lib {
    path: "/opt/nvidia/deepstream/deepstream-7.1/lib/libnvdsinfer_custom_impl_lpr.so"
  }
  extra {
    copy_input_to_host_buffers: 1
    output_buffer_pool_size: 16
  }
}
input_control {
  process_mode: PROCESS_MODE_CLIP_OBJECTS
  operate_on_gie_id: 2
  operate_on_class_ids: [0]
}

With YOLO model, I’m using this github repo for custom parser: GitHub - marcoslucianops/DeepStream-Yolo: NVIDIA DeepStream SDK 7.1 / 7.0 / 6.4 / 6.3 / 6.2 / 6.1.1 / 6.1 / 6.0.1 / 6.0 / 5.1 implementation for YOLO models

With LPRNet model, I’m using the custom parser provided by NVIDIA and make some changes:

nvinfer_custom_lpr_parser.cpp

/*
 * Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

 #include <string>
 #include <string.h>
 #include <stdio.h>
 #include <iostream>
 #include <vector>
 #include <assert.h>
 #include <locale>
 #include <codecvt>
 #include "nvdsinfer.h"
 #include <fstream>
 #include <cstdint>
 
 using namespace std;
 using std::string;
 using std::vector;
 
 static bool dict_ready = false;
 std::vector<string> dict_table;
 
 extern "C"
 {
 
     bool NvDsInferParseCustomNVPlate(std::vector<NvDsInferLayerInfo> const &outputLayersInfo,
                                      NvDsInferNetworkInfo const &networkInfo, float classifierThreshold,
                                      std::vector<NvDsInferAttribute> &attrList, std::string &attrString)
     {
         int64_t *outputStrBuffer = NULL;
         float *outputConfBuffer = NULL;
         NvDsInferAttribute LPR_attr;
 
         int seq_len = 0;
 
         // Get list
         vector<int64_t> str_idxes;
         int64_t prev = 100;
 
         // For confidence
         double bank_softmax_max[16] = {0.0};
         unsigned int valid_bank_count = 0;
         bool do_softmax = false;
         ifstream fdict;
 
         setlocale(LC_CTYPE, "");
 
         if (!dict_ready)
         {
             fdict.open("/opt/nvidia/deepstream/deepstream-7.1/lab/custom_parser_lpr/dict.txt");
             if (!fdict.is_open())
             {
                 cout << "open dictionary file failed." << endl;
                 return false;
             }
             while (!fdict.eof())
             {
                 string strLineAnsi;
                 if (getline(fdict, strLineAnsi))
                 {
                     dict_table.push_back(strLineAnsi);
                 }
             }
             dict_ready = true;
             fdict.close();
         }
 
         int layer_size = outputLayersInfo.size();
 
         LPR_attr.attributeConfidence = 1.0;
 
         seq_len = networkInfo.width / 4;
 
         for (int li = 0; li < layer_size; li++)
         {
             if (!outputLayersInfo[li].isInput)
             {
                 if (outputLayersInfo[li].dataType == 0) // FLOAT (FP32)
                 {
                     if (!outputConfBuffer)
                         outputConfBuffer = static_cast<float *>(outputLayersInfo[li].buffer);
                 }
                 else if (outputLayersInfo[li].dataType == 4) // Changed to 4 for INT64
                 {
                     if (!outputStrBuffer)
                         outputStrBuffer = static_cast<int64_t *>(outputLayersInfo[li].buffer);
                 }
             }
         }
 
         // Safety check for NULL buffers
         if (outputStrBuffer == NULL || outputConfBuffer == NULL)
         {
             std::cerr << "Error: output buffers not found" << std::endl;
             return false;
         }
 
         for (int seq_id = 0; seq_id < seq_len; seq_id++)
         {
             do_softmax = false;
 
             int64_t curr_data = outputStrBuffer[seq_id];
             if (curr_data < 0 || curr_data > static_cast<int64_t>(dict_table.size()))
             {
                 continue;
             }
             if (seq_id == 0)
             {
                 prev = curr_data;
                 str_idxes.push_back(curr_data);
                 if (curr_data != static_cast<int64_t>(dict_table.size()))
                     do_softmax = true;
             }
             else
             {
                 if (curr_data != prev)
                 {
                     str_idxes.push_back(curr_data);
                     if (static_cast<size_t>(curr_data) != dict_table.size())
                         do_softmax = true;
                 }
                 prev = curr_data;
             }
 
             // Do softmax
             if (do_softmax)
             {
                 do_softmax = false;
                 bank_softmax_max[valid_bank_count] = outputConfBuffer[seq_id];
                 valid_bank_count++;
             }
         }
 
         attrString = "";
         for (unsigned int id = 0; id < str_idxes.size(); id++)
         {
             if (static_cast<size_t>(str_idxes[id]) < dict_table.size())
             {
                 attrString += dict_table[str_idxes[id]];
             }
         }
 
         // Ignore the short string, it may be wrong plate string
         if (valid_bank_count >= 3)
         {
             LPR_attr.attributeIndex = 0;
             LPR_attr.attributeValue = 1;
             LPR_attr.attributeLabel = strdup(attrString.c_str());
             for (unsigned int count = 0; count < valid_bank_count; count++)
             {
                 LPR_attr.attributeConfidence *= bank_softmax_max[count];
             }
             attrList.push_back(LPR_attr);
         }
 
         return true;
     }
 
 } // end of extern "C"

Makefile:

################################################################################
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
################################################################################
CC:= g++

CFLAGS:= -Wall -Werror -std=c++11 -shared -fPIC -Wno-error=deprecated-declarations

CFLAGS+= -I/opt/nvidia/deepstream/deepstream/sources/includes

LIBS:= -lnvinfer
LFLAGS:= -Wl,--start-group $(LIBS) -Wl,--end-group

SRCFILES:= nvinfer_custom_lpr_parser.cpp
TARGET_LIB:= libnvdsinfer_custom_impl_lpr.so

all: $(TARGET_LIB)

$(TARGET_LIB) : $(SRCFILES)
	$(CC) -o $@ $^ $(CFLAGS) $(LFLAGS)

clean:
	rm -rf $(TARGET_LIB)

The compiled .so file, I copied to path /opt/nvidia/deepstream/deepstream-7.1/lib

Some example images about the LPR output:


Hi,

You probably need to retrain the plate recognition model using a dataset that includes 2-line plates. You can start the retraining process using the weights from the 1-line model, which should reduce the number of additional images required.

2 Likes

Thank you. Let me try that, when I have result, I will post it later.

Hi @miguel.taylor, I have re-trained the model with both 1-line and 2-line license plate, but the accuracy after evaluate process is very low (~66%), if the dataset only 1-line license plate, the accuracy can reach 92%. Current dataset images look like this:

Example label of the first image (2-line license plate): 49C078.07
Example label of the third image (1-line license plate): 79A-085.99

You can refer to https://forums.developer.nvidia.com/t/how-to-train-lprnet-for-license-plates-with-two-lines/235827 to learn how to train that. Thanks

1 Like

Hi @yuweiw,

As the post you mentioned, do I need to divide the dataset into this description?

  • Create 2 dataset folders with the same train and val images, but the label files are different with same image. In example, I have this image:

  • In 1st folder, the label file content is: 47C
  • In 2nd folder, the label file content is: 218.41

For questions regarding the details of model training process, please file the topic to the TAO forum. Thanks

1 Like

Yes, exactly that, you will have 2 datasets and 2 separate models, one for the first line and one for the second line. It works, I have it in production.

1 Like

Thank you, I will try that and post the result later.

Hi @Levi_Pereira, I’m trained done 2 models for first and second line, got 95% accuracy for first line and 85% accuracy for second line. My problem is in my country, we can use both single line and double line license plate on the front or the rear of the car. I’m also used a custom parser written in C++ for the both types of license plate, but somehow, the result is very bad on double line license plate, the single line is pretty good. This is an example image:

This is custom parser I’m used:

#include <iostream>
#include <vector>
#include <string.h>
#include <fstream>
#include <algorithm>
#include "nvdsinfer.h"

// Static vector to hold the character dictionary
static std::vector<std::string> dict_table;
static bool dict_ready = false;

// Function to load the dictionary from dict.txt
static void load_dict() {
    if (dict_ready) {
        return;
    }
    std::ifstream fdict("/opt/nvidia/deepstream/deepstream-7.1/lab/custom_parser_lpr/dict.txt");
    if (!fdict.is_open()) {
        std::cerr << "ERROR: Failed to open dictionary file 'dict.txt'" << std::endl;
        return;
    }
    while (!fdict.eof()) {
        std::string strLineAnsi;
        if (getline(fdict, strLineAnsi) && !strLineAnsi.empty()) {
            dict_table.push_back(strLineAnsi);
        }
    }
    fdict.close();
    if (dict_table.empty()) {
        std::cerr << "ERROR: Dictionary is empty. Check 'dict.txt'." << std::endl;
    } else {
        dict_ready = true;
        std::cout << "INFO: Dictionary loaded successfully. Size: " << dict_table.size() << " characters." << std::endl;
    }
}

// Common parsing function for Triton LPRNet output
static bool NvDsInferParseTritonLPR(
    const std::vector<NvDsInferLayerInfo> &outputLayersInfo,
    std::string &attrString) {

    if (!dict_ready) {
        load_dict(); // Attempt to load dictionary if not already loaded
        if (!dict_ready) return false;
    }

    // Find the output layer for ArgMax. Triton provides named output layers.
    const NvDsInferLayerInfo *argmax_layer = nullptr;
    for (const auto &layer : outputLayersInfo) {
        if (std::string(layer.layerName) == "tf_op_layer_ArgMax") {
            argmax_layer = &layer;
            break;
        }
    }

    if (!argmax_layer) {
        std::cerr << "ERROR: Could not find output layer 'tf_op_layer_ArgMax'" << std::endl;
        return false;
    }

    // The output of ArgMax is a sequence of character indices
    const long *indices = (const long *)argmax_layer->buffer;
    const int seq_len = argmax_layer->inferDims.d[0];

    std::string res = "";
    long last_char_idx = -1;

    // CTC Greedy Decoding
    for (int i = 0; i < seq_len; i++) {
        long current_idx = indices[i];
        
        // Add character if it's not a repeat and not the blank character
        if (current_idx != last_char_idx) {
            // Assumes blank character is the last one in the dictionary
            if (current_idx >= 0 && current_idx < (long)dict_table.size() - 1) {
                res.append(dict_table[current_idx]);
            }
            last_char_idx = current_idx;
        }
    }
    
    attrString = res;
    return true;
}


// C-style export for DeepStream to find these functions
extern "C" {

// The actual parsing function for classifiers
bool NvDsInferClassiferParseCustomUpper(
    std::vector<NvDsInferLayerInfo> const &outputLayersInfo,
    NvDsInferNetworkInfo const &networkInfo,
    float classifierThreshold,
    std::vector<NvDsInferAttribute> &attrList,
    std::string &attrString) {

    std::string decoded_text;
    if (NvDsInferParseTritonLPR(outputLayersInfo, decoded_text)) {
        attrString = "UPPER:" + decoded_text;
        
        NvDsInferAttribute attr;
        attr.attributeIndex = 0;
        attr.attributeValue = 1;
        attr.attributeConfidence = 1.0;
        attr.attributeLabel = strdup(attrString.c_str());
        attrList.push_back(attr);
        
        return true;
    }
    return false;
}

// Parser for the LOWER line model
bool NvDsInferClassiferParseCustomLower(
    std::vector<NvDsInferLayerInfo> const &outputLayersInfo,
    NvDsInferNetworkInfo const &networkInfo,
    float classifierThreshold,
    std::vector<NvDsInferAttribute> &attrList,
    std::string &attrString) {
    
    std::string decoded_text;
    if (NvDsInferParseTritonLPR(outputLayersInfo, decoded_text)) {
        attrString = "LOWER:" + decoded_text;

        NvDsInferAttribute attr;
        attr.attributeIndex = 0;
        attr.attributeValue = 1;
        attr.attributeConfidence = 1.0;
        attr.attributeLabel = strdup(attrString.c_str());
        attrList.push_back(attr);

        return true;
    }
    return false;
}

} // extern "C"

My pipeline:

GST_DEBUG=3 gst-launch-1.0 -v filesrc location=/opt/nvidia/deepstream/deepstream-7.1/lab/recorded_vms_data/traffic_h264.mp4 ! \
  qtdemux ! h264parse ! nvv4l2decoder ! \
  m.sink_0 nvstreammux name=m batch-size=1 width=1920 height=1080 ! \
  queue ! \
  nvinferserver config-file-path=config_infer_primary_person_vehicle_detector.txt ! \
  queue ! \
  nvinferserver config-file-path=config_infer_secondary_license_plate_detector.txt ! \
  queue ! \
  nvinferserver config-file-path=config_triton_lpr_upper.txt ! \
  queue ! \
  nvinferserver config-file-path=config_triton_lpr_lower.txt ! \
  queue ! \
  nvmultistreamtiler rows=1 columns=1 width=1920 height=1080 ! \
  nvdsosd ! \
  nvvideoconvert ! \
  "video/x-raw, format=RGBA" ! \
  nveglglessink sync=false

If you got 95% and 85% then the issue may be on deepstream/triton-server.

You can try:
I created 2 parse function due dict ( i have different dicts).

My double line plate have the same dict (from A to Z, from 0 to 9), but the upper line only have 3 characters (2 digits and 1 alphabet character), lower line only have 4-5 digits.

In my opinion, if the model had high accuracy and you manually test the image inferences through manual inference with the same accuracy, you should be able to achieve the same in DeepStream. Maybe slightly lower after converting to TensorRT (but it shouldn’t be something notable, maybe it drops 0.05%).

If you ensure that the model is performing well, then you must have some problem in the model conversion or configuration of the sgie_config files.

I read this post and saw that the post owner use tee to split into 2 branches:

https://forums.developer.nvidia.com/t/is-the-lprnet-trainable-for-license-plates-with-two-separate-lines-instead-of-single-one-as-of-us/169648/13?u=hungtv

PGIE --> SGIE_1 (LPDnet)|--> SGIE_2 (LPRnet_1)
                        |--> SGIE_3 (LPRnet_2)

I tried that but I don’t know how to merge the SGIE_2 and SGIE_3 together, is that an element I can use to merge 2 branches? Or I need to write some C++ code to work on that?

No tee component is required. You get sgie_3 from sgie_1 using the same method as sgie_2 from sgie_1 (single line plate). The only difference is that instead of one OCR text output, you’ll have two separate OCR text outputs from two differente unique_component_id . Simply concatenate the strings from both: sgie_2(ocr_text) + sgie_3(ocr_text).

  • unique_component_idint, Unique component id that attaches NvDsClassifierMeta metadata.

while l_obj is not None:
   try:
       obj_meta = pyds.NvDsObjectMeta.cast(l_obj.data)
       
       ocr_texts = {}  # Store OCR results by component_id
       
       l_class = obj_meta.classifier_meta_list
       while l_class is not None:
           try:
               class_meta = pyds.NvDsClassifierMeta.cast(l_class.data)
               component_id = class_meta.unique_component_id
               
               print(f"Processing component_id: {component_id}")
               
               # Check if this is an OCR component (component_id 3 or 4)
               if component_id in [3, 4]:
                   l_label = class_meta.label_info_list
                   while l_label is not None:
                       try:
                           label_info = pyds.NvDsLabelInfo.cast(l_label.data)
                           ocr_text = label_info.result_label
                           
                           # Store OCR text by component_id
                           if component_id not in ocr_texts:
                               ocr_texts[component_id] = []
                           ocr_texts[component_id].append(ocr_text)
                           
                           print(f"  OCR Text: {ocr_text}")
                           
                           l_label = l_label.next
                       except StopIteration:
                           break
               
               l_class = l_class.next
           except StopIteration:
               break
       
       # Concatenate OCR results from both components - ORDER MATTERS!
       # component_id = 3: First OCR region (left/top part)
       # component_id = 4: Second OCR region (right/bottom part)
       if 3 in ocr_texts and 4 in ocr_texts:
           combined_text = "".join(ocr_texts[3]) + "".join(ocr_texts[4])
           print(f"Combined OCR (3+4): {combined_text}")
       elif 3 in ocr_texts:
           print(f"OCR from component 3 only: {''.join(ocr_texts[3])}")
       elif 4 in ocr_texts:
           print(f"OCR from component 4 only: {''.join(ocr_texts[4])}")
   except StopIteration:
       break

My approach maybe different than yours. My team have developed a custom platform, but they only support with GStreamer pipeline. So maybe I will create a custom plugin to process the detected license plate.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.