onnx的输入输出如图:
在这里插入图片描述
其中车牌识别的字典顺序是:

CHARS = ['京', '沪', '津', '渝', '冀', '晋', '蒙', '辽', '吉', '黑',
         '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤',
         '桂', '琼', '川', '贵', '云', '藏', '陕', '甘', '青', '宁',
         '新',
         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K',
         'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
         'W', 'X', 'Y', 'Z', 'I', 'O', '-'
         ]

共68个字符。


🚗 背景:LPRNet 模型结构

  • 输入[batch, 3, 24, 94]
    • 一张归一化后的车牌图像(高 24,宽 94)
  • 输出[batch, 68, 18]
    • 68:字符类别数(包括 67 个有效字符 + 1 个 blank 空白符)
    • 18:时间步(time steps),即模型对输入图像在水平方向上划分的 18 个“帧”

💡 关键理解:LPRNet 将车牌识别建模为 序列到序列 的任务 ——
输入是一张图,输出是一个长度为 18 的概率序列,每个位置预测一个字符(含 blank)。


🔍 第一步:postprocess() —— 从 logits 到类别索引序列

✅ 目标

将模型输出的 logits(未归一化的分数) 转换为 每个 time step 最可能的字符索引,得到一个长度为 18 的整数序列。

📌 代码解析

// output 布局: [C, T] = [68, 18]
// 内存排布: 先存第0个字符在所有18个time step的值,再存第1个字符...
// 即: [c0_t0, c0_t1, ..., c0_t17, c1_t0, c1_t1, ..., c67_t17]
核心操作:
  1. 遍历每个 time step t(共 18 次)
  2. 在该 time step 上,遍历所有 68 个类别 c
  3. 找到概率最大的类别索引 max_class_id
  4. max_class_id 加入 preds 向量

🔑 内存访问关键
由于 TensorRT 输出是 [C, T] 布局(channel-first),
所以第 c 个类别在第 t 个 time step 的值位于:

output[c * NUM_TIME_STEPS + t]  // 正确!

🧾 输出示例

假设某次推理得到:

preds = [27, 27, 67, 67, 67, 67, 67, 67, 67, 67, 64, 64, 32, 32, 32, 32, 51, 39]
  • 这是一个 原始的、含重复和 blank 的序列

🔠 第二步:ctc_decode() —— CTC 解码:去重 + 去 blank

✅ 目标

preds 中的 冗余信息(连续重复、blank 符号)去除,还原出真实的车牌字符串。

📌 CTC 解码规则(Greedy Decoding)

  1. 折叠连续重复的相同字符(如 A A AA
  2. 移除所有 blank 符号(通常设为最后一个类别,即 67

💡 为什么需要 CTC?
因为 CNN+RNN/Transformer 在对齐时会产生:

  • 多个 time step 对应同一个字符(导致重复)
  • 不确定区域预测为 blank

📌 代码解析

const int blank_id = NUM_CLASSES - 1; // 67
int prev_class_id = -1;

for (int class_id : preds) {
    if (class_id == prev_class_id) {
        continue; // 跳过连续重复
    }
    if (class_id != blank_id) {
        plate += charset[class_id]; // 只保留非 blank 字符
    }
    prev_class_id = class_id;
}

🧾 解码过程演示

原始 preds 处理步骤 结果
27, 27 折叠重复 27
67, 67, ..., 67 全是 blank → 移除 (空)
64, 64 折叠重复 64
32, 32, 32, 32 折叠重复 32
51, 39 无重复、非 blank 51, 39

最终得到有效索引序列:[27, 64, 32, 51, 39]

再通过 charset 映射为字符,例如:

charset = '京', '沪', '津', '渝', '冀', '晋', '蒙', '辽', '吉', '黑',
         '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤',
         '桂', '琼', '川', '贵', '云', '藏', '陕', '甘', '青', '宁',
         '新',
         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K',
         'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
         'W', 'X', 'Y', 'Z', 'I', 'O', '-'
// 假设:
// 27 → '粤'
// 64 → 'B'
// 32 → '8'
// 51 → '6'
// 39 → '9'
// 最终车牌:"粤B869"

LPRNet 后处理流程详解

LPRNet 是一种端到端的车牌识别模型,其输出是一个形状为 [batch, 68, 18] 的张量,其中:

  • 68 表示字符类别数(67 个有效字符 + 1 个 blank 空白符)
  • 18 表示时间步(即模型对车牌图像在水平方向上的 18 个采样点)

后处理分为两个关键步骤:

1️⃣ ArgMax 解码:获取最可能的字符索引序列

对每个时间步(共 18 个),在 68 个类别中选取概率最高的类别索引,得到一个长度为 18 的整数序列。
由于 TensorRT 输出采用 [C, T] 内存布局,需按 output[c * 18 + t] 方式正确访问。

2️⃣ CTC Greedy 解码:去除冗余信息
  • 折叠连续重复的相同字符(如 A A AA
  • 移除所有 blank 符号(通常为最后一个类别 ID)

最终得到干净的字符索引序列,并通过字符集映射为可读的车牌字符串。

示例
原始输出:[27, 27, 67, 67, ..., 64, 64, 32, 32, 51, 39]
CTC 解码后:[27, 64, 32, 51, 39]"粤B869"

该方法简单高效,适用于大多数固定长度车牌场景。


std::string LPRNet::postprocess(float* output) {
    // output 缓冲区是 [C, T] 布局 (trtexec 证实为 1x68x18)
    // NUM_CLASSES = 68
    // NUM_TIME_STEPS = 18
    
    std::vector<int> preds; // 存储每个 time step 的 argmax 索引
    preds.reserve(NUM_TIME_STEPS);

    // 迭代 18 个 time steps
    for (int t = 0; t < NUM_TIME_STEPS; ++t) { //68行18列,找到每一列最大的索引存到 max_class_id,将这18个值都放到preds中
        
        // --- 查找 ArgMax ---
        // 在当前 time step (t),遍历所有 68 个类别 (c),找到概率最高的那个
        float max_prob = -1e6; // 初始设为极小值
        int max_class_id = -1;

        for (int c = 0; c < NUM_CLASSES; ++c) {
            
            // 关键修复: 以 [C, T] 的方式访问内存
            // output[c * 18 + t]
            float prob = output[c * NUM_TIME_STEPS + t]; 
            
            if (prob > max_prob) {
                max_prob = prob;
                max_class_id = c;
            }
        }
        preds.push_back(max_class_id);
    }

    // CTC 解码 (ctc_decode 函数本身是正确的,无需修改)
    return ctc_decode(preds);
}
std::string LPRNet::ctc_decode(const std::vector<int>& preds) {
    std::cout<<"====车牌识别结果索引====="<<std::endl;
    //27 27 67 67 67 67 67 67 67 67 64 64 32 32 32 32 51 39 
    for(int i = 0;i<preds.size();i++){
        std::cout<<preds[i]<<" ";
    }
    std::cout<<std::endl;
    std::string plate = "";
    int prev_class_id = -1; 
    const int blank_id = NUM_CLASSES - 1; // 68-1   假设 'blank' 是最后一个字符

    for (int class_id : preds) {
        if (class_id == prev_class_id) {
            continue; // 折叠重复项
        }
        if (class_id != blank_id) { // 忽略 'blank'
            plate += charset[class_id];
            std::cout<<class_id<<" ";
        }
        prev_class_id = class_id;
    }
    //27 64 32 51 39
    return plate;
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐