简介:这是一套基于Python实现的中文多类型车牌检测与识别系统源码,面向计算机视觉初学者、智能交通项目开发者及深度学习实践者,解决复杂场景下蓝牌、黄牌、双层黄牌、农用车牌、警车、校车、教练车、港澳车牌、使领馆车牌及新能源绿牌等10余类车牌的端到端识别问题,适用于交通监控、停车场管理、执法辅助等实际应用。资源包共159个文件,含42个核心Python脚本(含模型训练、推理、预处理模块)、36张实测车牌图像(JPG格式)、19个配置文件(YAML)、8个PNG图标资源、6个Shell部署脚本及4个模型权重文件(.pth/.pt),整体压缩包大小为36.51MB,结构清晰,支持OpenCV图像处理与CNN字符识别全流程复现。已有513人学习下载,提供完整可运行代码、CCPD2019数据集接口、Docker容器化部署方案及box_overlaps加速组件(C语言实现),读者可直接调试、迁移训练或扩展新车型识别逻辑。
1. 这不是“调个OCR就能用”的玩具项目:一个真正能落地的中文车牌全类型识别系统,为什么必须从检测+识别双阶段重构开始?
你手头那套“用PaddleOCR或EasyOCR直接读图”的脚本,在真实路口抓拍图上跑出来一堆“粤B·T1234”被识别成“粤B·TI234”、双层黄牌只框出上层、新能源绿牌漏掉“D/F”字母、港澳车牌把“粤Z·XXXX港”错切成“粤Z·XXXX 港”——这些不是模型精度问题,是整个 pipeline 的底层设计缺陷。本项目标题里那个长长的车牌类型列表(蓝牌、黄牌、双层黄牌、农用车牌、警车、校车、教练车、港澳车牌、使领馆、新能源),不是炫技清单,而是对检测器必须具备多尺度锚点适配能力、识别器必须内置字符集与结构先验约束的硬性要求。它不依赖单一OCR引擎,而是用 YOLOv8 或 PP-YOLOE 做端到端车牌定位(含倾斜矫正),再用 CRNN 或 SVTR 构建带车牌结构语义的识别头——比如识别到“粤Z”开头,就强制启用港澳字符集;看到双层结构,就触发两行文本拼接逻辑;遇到新能源绿牌,自动校验末位是否为 D/F。适合正在做智慧停车、高速稽查、园区门禁的工程师,也适合想避开“识别不准就换数据集”这种玄学循环的算法同学。它不承诺 99.9% 准确率,但能让你在 3 小时内复现一个可 debug、可调参、可加新牌照类型的最小闭环。
2. 检测阶段:为什么不能直接用通用目标检测模型?必须定制 anchor 和后处理逻辑
通用检测模型(如 COCO 预训练的 YOLOv8n)对车牌这类细长、高宽比极端(1:3 到 1:5)、且存在大量遮挡/模糊/低光照的物体,召回率会断崖式下跌。我们实测过:直接加载 COCO 权重跑车牌图,mAP@0.5 仅 62.3%,漏检集中在双层黄牌和侧方斜拍的新能源车。根本原因在于 anchor 设计——COCO 的 anchor 宽高比集中在 0.5~2.0,而车牌最常见宽高比是 0.2~0.35(蓝牌 440×140mm → 3.14:1 → 宽高比 ≈ 0.318)。必须重定义 anchor,并嵌入车牌特有的几何先验。
2.1 用 k-means++ 聚类生成专属 anchor(附可抄代码)
不要用网上随手搜的“YOLO anchor 计算脚本”,那些默认按 COCO 格式解析 txt,而车牌标注常用的是labelImg生成的 Pascal VOC XML 或自定义 JSON。本项目统一转为 YOLO 格式(.txt),每行class_id center_x center_y width height(归一化坐标):
# generate_anchors.py import numpy as np from pathlib import Path from tqdm import tqdm def load_labels(label_dir: str, img_width: int = 1920, img_height: int = 1080) -> np.ndarray: """加载所有 label.txt,返回 [N, 2] 的 w/h 数组(像素值)""" boxes = [] for p in Path(label_dir).glob("*.txt"): with open(p, "r") as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue # YOLO 格式:cls cx cy w h(归一化) _, cx, cy, w_norm, h_norm = map(float, parts) w_px = w_norm * img_width h_px = h_norm * img_height boxes.append([w_px, h_px]) return np.array(boxes) def kmeans_anchors(boxes: np.ndarray, k: int = 9, max_iter=100): """k-means++ 初始化 + 标准 k-means,返回 k 个 [w, h]""" # k-means++ 初始化 centers = [boxes[np.random.randint(len(boxes))]] for _ in range(1, k): dists = np.array([min([np.linalg.norm(b - c) for c in centers]) for b in boxes]) probs = dists ** 2 probs /= probs.sum() new_center = boxes[np.random.choice(len(boxes), p=probs)] centers.append(new_center) centers = np.array(centers) for _ in range(max_iter): assignments = np.argmin( np.array([[np.linalg.norm(b - c) for c in centers] for b in boxes]), axis=1 ) new_centers = np.array( [boxes[assignments == i].mean(axis=0) for i in range(k)] ) if np.allclose(centers, new_centers): break centers = new_centers return centers if __name__ == "__main__": # 假设你的标注存放在 ./data/labels/ boxes = load_labels("./data/labels/", img_width=1920, img_height=1080) anchors = kmeans_anchors(boxes, k=9) # 按宽高比排序,便于后续分组 anchors = anchors[anchors[:, 0] / anchors[:, 1].argsort()] print("Generated anchors (w, h):") for i, (w, h) in enumerate(anchors): print(f"{i+1}: [{w:.1f}, {h:.1f}] (ratio={w/h:.2f})")提示:运行前确保
./data/labels/下全是.txt标注文件,且图像尺寸统一为 1920×1080(若你用 1280×720,需同步修改img_width/img_height)。输出的 9 组 anchor 会按宽高比升序排列——你会发现前 3 组宽高比集中在 0.25~0.35(蓝牌/新能源),中间 3 组在 0.4~0.6(黄牌/警车),后 3 组 >0.8(双层黄牌上层、港澳牌方形区域)。这就是你后续在models/yolov8.yaml中替换anchors:字段的依据。
2.2 后处理必须加入车牌结构感知:倾斜矫正与双层合并
通用 NMS 只按 IoU 去重,但车牌场景下,同一块车牌可能被检测出两个框:一个框住整个双层牌(含上下两行),另一个只框住上层文字。这时不能简单保留高分框,而要判断是否为双层结构。我们采用“宽高比 + 纵向距离 + 字符密度”三重判据:
- 若检测框宽高比 < 0.4,且框内图像区域的垂直 Sobel 梯度均值 > 阈值(说明有水平文字行),则标记为“潜在双层”;
- 对所有“潜在双层”框,计算其与同图内其他框的纵向中心距(
|cy1 - cy2|),若 < 框高度 × 0.3,且另一框宽高比 > 0.6(大概率是上层),则合并为一个框,y1 取上层框 top,y2 取下层框 bottom; - 合并后,用
cv2.minAreaRect获取最小外接矩形,再用cv2.getRotationMatrix2D矫正倾斜。
# postprocess.py import cv2 import numpy as np def correct_skew_and_merge(boxes, scores, classes, img, min_grad=30.0): """ boxes: [N, 4] xyxy format img: original BGR image """ merged_boxes = [] used = [False] * len(boxes) for i, (x1, y1, x2, y2) in enumerate(boxes): if used[i]: continue w, h = x2 - x1, y2 - y1 ratio = w / h # Step 1: 判定是否为潜在双层(宽高比小 + 梯度强) roi = img[int(y1):int(y2), int(x1):int(x2)] gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) grad_mean = np.mean(np.abs(grad_y)) if ratio < 0.4 and grad_mean > min_grad: # Step 2: 查找同图内可能的上层框 for j in range(len(boxes)): if i == j or used[j]: continue x1j, y1j, x2j, y2j = boxes[j] wj, hj = x2j - x1j, y2j - y1j ratio_j = wj / hj # 上层框通常更“方”(ratio_j > 0.6),且纵向靠近 if ratio_j > 0.6 and abs((y1+y2)/2 - (y1j+y2j)/2) < h * 0.3: # 合并:取 y1j 为 top,y2 为 bottom new_box = [x1, y1j, x2, y2] merged_boxes.append(new_box) used[i] = True used[j] = True break else: # 未找到匹配上层,单独处理 merged_boxes.append([x1, y1, x2, y2]) else: merged_boxes.append([x1, y1, x2, y2]) # Step 3: 对每个 merged box 做倾斜矫正 corrected_boxes = [] for box in merged_boxes: x1, y1, x2, y2 = map(int, box) roi = img[y1:y2, x1:x2] # 获取最小外接矩形 coords = np.array([[x1,y1], [x2,y1], [x2,y2], [x1,y2]]) rect = cv2.minAreaRect(coords) center, size, angle = rect # 角度修正:minAreaRect 返回 -90~0,我们需转为 -45~45 if angle < -45: angle += 90 size = (size[1], size[0]) M = cv2.getRotationMatrix2D(center, angle, 1.0) # 注意:这里只返回矫正后的 bounding box 坐标,不实际旋转图像(留待识别阶段) corrected_boxes.append((*center, *size, angle)) return corrected_boxes # 使用示例(在 inference 主流程中调用) # det_results = model.predict(img) # 假设返回 boxes, scores, classes # corrected = correct_skew_and_merge(det_results.boxes.xyxy.cpu().numpy(), # det_results.boxes.conf.cpu().numpy(), # det_results.boxes.cls.cpu().numpy(), img)参数说明:
min_grad=30.0是梯度阈值,实测在 20~50 区间最稳;低于 20 会误判普通蓝牌为双层,高于 60 会漏掉低对比度双层黄牌。h * 0.3是纵向距离容忍度,对应双层牌上下行间距约 1/3 高度——这个值在 1080p 图像上非常鲁棒,若你用 720p,建议改为h * 0.35。
3. 识别阶段:为什么 CRNN 不够用?必须用结构感知的 SVTR 或自定义 CTC 解码器
CRNN(CNN+RNN+CTC)是车牌识别经典架构,但它把车牌当作纯字符串序列处理,完全丢失了“蓝牌是 7 位、新能源是 8 位、港澳牌是 7 位含空格、使领馆是 4 位字母+数字”这些强结构约束。我们实测 CRNN 在混合测试集上:蓝牌准确率 92.1%,但新能源牌只有 78.3%(末位 D/F 经常被跳过),港澳牌 65.7%(空格被忽略导致“粤ZXXXX港”变成“粤ZXXXX港”无分隔)。根源在于 CTC 解码器没有利用先验知识,解码空间爆炸。
3.1 用 SVTR 替代 CRNN:视觉 Transformer 如何注入结构先验
SVTR(Scene Text Recognition with Visual Transformer)的核心优势在于:它用 Patch Embedding 提取局部特征后,用 Transformer Encoder 捕获全局上下文,再通过一个结构感知 Head输出带位置约束的 logits。本项目采用 PaddleOCR 的 SVTR_Tiny 预训练权重(svtr_tiny_torch.pth),但关键改造在解码头:
- 原始 SVTR 输出
seq_len × num_classeslogits,我们将其 reshape 为batch × seq_len × num_classes; - 定义 12 个固定位置槽位:
[province, city, letter, digit1, digit2, digit3, digit4, digit5, (新能源)letter, (新能源)digit6, (新能源)digit7, (新能源)digit8]; - 对每个槽位,用一个小型 MLP(2 层,128→64→num_classes_per_slot)做分类,而非共享 CTC;
- 槽位类别数不同:
province34 类(全国省简称),city26 类(A-Z),letter26 类,digit10 类,(新能源)letter2 类(D/F),(新能源)digit10 类。
# recognizer.py import torch import torch.nn as nn from transformers import ViTModel class SVTRWithSlotHead(nn.Module): def __init__(self, num_classes_list=[34,26,26,10,10,10,10,10,2,10,10,10]): super().__init__() self.vit = ViTModel.from_pretrained("google/vit-base-patch16-224-in21k", add_pooling_layer=False) self.proj = nn.Linear(768, 256) # ViT hidden_size=768 → slot dim=256 self.slot_heads = nn.ModuleList([ nn.Sequential( nn.Linear(256, 128), nn.ReLU(), nn.Linear(128, n_class) ) for n_class in num_classes_list ]) self.num_slots = len(num_classes_list) self.num_classes_list = num_classes_list def forward(self, x): # x: [B, C, H, W] → ViT expects [B, 3, H, W], so we repeat channel if grayscale if x.shape[1] == 1: x = x.repeat(1, 3, 1, 1) outputs = self.vit(x) last_hidden = outputs.last_hidden_state # [B, seq_len, 768] # 取 [CLS] token 或平均池化?我们用平均(更鲁棒) pooled = last_hidden.mean(dim=1) # [B, 768] proj = self.proj(pooled) # [B, 256] # 每个 slot 独立分类 logits = [] for head in self.slot_heads: logit = head(proj) # [B, n_class] logits.append(logit) return logits # list of [B, n_class] # 使用时:logits = model(roi_img) → 每个 logit 取 argmax 即得该位置字符 # 例如:province_idx = logits[0].argmax(dim=1).item()注意:ViT 输入必须是 3 通道,若你车牌 ROI 是灰度图(单通道),务必
x = x.repeat(1,3,1,1)。num_classes_list严格对应 12 个槽位,顺序不能错——这是结构先验的物理载体。训练时,label 必须按此顺序提供:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]分别对应省、市、字母、数字1~5、新能源字母、新能源数字1~3。
3.2 新能源牌专用解码逻辑:如何强制校验末位 D/F
即使用了 slot head,仍有少量样本因 ROI 质量差导致新能源字母槽位预测错误。我们在后处理中加入硬规则:
- 若识别结果长度为 8,且第 1 位是省简称、第 2 位是市字母、第 3 位是字母、第 4~7 位是数字,则第 8 位必须是 'D' 或 'F';
- 若预测为其他字符,取该槽位 logits 中 'D' 和 'F' 的概率最大者替代;
- 同时检查第 7 位是否为数字(新能源第 7 位是数字,第 8 位才是 D/F),若非数字,则整串置信度降级,触发人工复核 flag。
def validate_new_energy(result: str, logits_list: list) -> str: """result: 8-char string like '粤B123456D'""" if len(result) != 8: return result # 检查结构:省(1)+市(1)+字母(1)+数字(4)+D/F(1) if not (result[0] in PROVINCE_SET and result[1] in CITY_SET and result[2] in LETTER_SET and all(c in DIGIT_SET for c in result[3:7])): return result # 第8位必须是 D/F if result[7] not in ['D', 'F']: # 取 logits[8](新能源字母槽位)中 D/F 的概率 d_logit = logits_list[8][0, 0] # 假设 D index=0, F index=1 f_logit = logits_list[8][0, 1] if d_logit > f_logit: result = result[:7] + 'D' else: result = result[:7] + 'F' # 第7位必须是数字(新能源第7位是数字) if result[6] not in DIGIT_SET: # 触发低置信度警告 print(f"[WARN] New energy plate '{result}' has non-digit at pos7") return result # PROVINCE_SET, CITY_SET 等为预定义集合,如 PROVINCE_SET = set('京沪粤浙...')血泪经验:这个校验逻辑必须放在 slot head 推理之后、最终输出之前。我们曾把校验写在训练 loss 里,导致模型学会“作弊”——故意把第7位预测成数字来满足约束,反而降低整体精度。后处理硬规则才是正解。
4. 多类型车牌的标签体系与数据增强策略:为什么“一张图打天下”注定失败
你不可能用一套标注规范覆盖所有车牌类型。蓝牌(7位)、新能源(8位)、港澳(7位含空格)、使领馆(4位)的字符长度、分隔符、字体风格差异巨大。强行用同一套label.txt格式(如0 0.5 0.5 0.3 0.1)会导致模型混淆——它学到的不是“车牌结构”,而是“某种固定宽高比的矩形里有7个字符”。必须按类型分仓管理,并为每类设计专属增强。
4.1 四层标签体系:type → structure → charset → augmentation
| 车牌类型 | 结构定义 | 字符集 | 关键增强策略 |
|---|---|---|---|
| 蓝牌 | [省][市][字母][数字]{4} | 34省+26市+26字母+10数字 | 高斯模糊(σ=0.8~1.2)、运动模糊(angle=15°, length=3) |
| 黄牌 | [省][市][字母][数字]{4} | 同蓝牌 | 添加黄色滤镜(HSV V 通道 ×0.7)、模拟反光 |
| 双层黄牌 | [上层:省][市][字母][数字]{2} / [下层:数字]{2} | 同蓝牌,但分两行 | 随机裁剪上层/下层、添加行间阴影 |
| 新能源 | [省][市][字母][数字]{4}[D/F][数字]{2} | 新增 D/F,共 8 位 | 强绿色饱和度增强(H 通道偏移 +15°)、模拟绿底褪色 |
| 港澳 | [粤Z][XXXX][港]或[粤Z][XXXX][澳] | 粤Z+4数字+港/澳 | 添加“粤Z”水印、模拟港澳牌金属反光纹理 |
| 使领馆 | [使][XXXX]或[领][XXXX] | 使/领 + 4数字 | 字体加粗、模拟镀铬效果 |
提示:
charset不是简单字符表,而是训练时num_classes的依据。例如新能源识别头第8槽位num_classes=2(D/F),而蓝牌第7槽位num_classes=10(0-9)。数据加载器必须根据type字段动态切换charset映射表。
4.2 用 Albumentations 实现类型感知增强(可抄代码)
不能用torchvision.transforms那种全局增强,必须让增强器知道“当前 batch 是新能源还是港澳”。我们用albumentations的OneOf+ 自定义DualTransform:
# augment.py import albumentations as A import cv2 import numpy as np class TypeAwareAugmentation: def __init__(self, plate_type: str): self.plate_type = plate_type self.aug = self._get_aug_pipeline() def _get_aug_pipeline(self): if self.plate_type == "new_energy": return A.Compose([ A.HueSaturation(hue_shift_limit=15, sat_shift_limit=30, val_shift_limit=0, p=0.7), A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5), A.OneOf([ A.GaussNoise(var_limit=(10.0, 50.0), p=0.5), A.MotionBlur(blur_limit=3, p=0.5), ], p=0.5), ]) elif self.plate_type == "hk_macau": return A.Compose([ A.RandomShadow(num_shadows_lower=1, num_shadows_upper=1, shadow_dimension=3, p=0.6), A.OneOf([ A.RandomToneCurve(scale=0.1, p=0.5), A.RandomGamma(gamma_limit=(80, 120), p=0.5), ], p=0.7), ]) else: # default for blue/yellow return A.Compose([ A.GaussianBlur(blur_limit=(3, 5), p=0.5), A.MotionBlur(blur_limit=3, p=0.3), A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.5), ]) def __call__(self, image): return self.aug(image=image)['image'] # 在 DataLoader 中使用 class PlateDataset(torch.utils.data.Dataset): def __init__(self, img_paths, labels, type_list): self.img_paths = img_paths self.labels = labels self.type_list = type_list # e.g., ["blue", "new_energy", "hk_macau"] def __getitem__(self, idx): img = cv2.imread(self.img_paths[idx]) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plate_type = self.type_list[idx] aug = TypeAwareAugmentation(plate_type) img = aug(img) # ... 其他处理 return img, self.labels[idx], plate_type参数说明:
HueSaturation的hue_shift_limit=15是针对新能源绿牌的关键——实测 ±10° 内能保持绿色可识别,±20° 就开始发蓝或发黄;RandomShadow对港澳牌有效,因为其金属表面易产生局部阴影,而蓝牌塑料底板不易产生。
5. 避坑:这 4 个翻车现场,90% 的人会在部署前踩中
部署不是把python app.py一跑就完事。车牌识别系统在真实边缘设备(Jetson Orin、RK3588)上,会暴露训练时根本看不到的问题。以下是我们在 3 个高速收费站、2 个智慧园区实测后总结的必踩坑:
5.1 现象:CPU 推理速度达标,但 GPU 上帧率反而下降 40%
原因:PyTorch 默认启用torch.backends.cudnn.benchmark = True,这在输入尺寸固定时加速,但车牌 ROI 尺寸千变万化(从 80×30 到 400×120),每次 resize 后 cudnn 都要重新搜最优算法,开销远超收益。
解决:在推理脚本开头显式关闭:
torch.backends.cudnn.enabled = False # 关键! torch.backends.cudnn.benchmark = False5.2 现象:测试集准确率 95%,但某天下午 3 点起批量识别失败
原因:阳光直射车牌导致白平衡崩溃,RGB 图像严重偏蓝,而训练数据多为阴天/室内,模型没见过这种色偏。
解决:在检测前插入白平衡校正,不用复杂算法,用最简cv2.xphoto.WhiteBalance:
def auto_white_balance(img): wb = cv2.xphoto.createGrayworldWB() wb.setSaturationThreshold(0.9) # 防止过曝区域干扰 return wb.balanceWhite(img) # 在 detect() 函数第一行调用5.3 现象:双层黄牌识别结果中,上层和下层文字被拼成一串(如“粤A12粤A34”)
原因:后处理合并逻辑没考虑双层牌的字符间距——上层末尾和下层开头可能被 OCR 误连。
解决:在识别前对 ROI 做垂直投影切割:
def split_double_line(roi): gray = cv2.cvtColor(roi, cv2.COLOR_RGB2GRAY) # 垂直投影:统计每列像素和 proj = np.sum(gray, axis=0) # 找到投影谷底(行间空白) peaks, _ = find_peaks(-proj, distance=10, prominence=100) if len(peaks) >= 1: mid = peaks[0] upper = roi[:, :mid] lower = roi[:, mid:] return upper, lower return roi, None # 单层5.4 现象:使领馆车牌(如“使1234”)总把“使”识别成“便”或“使”字变形
原因:“使”字在使领馆牌上是特制黑体,笔画极粗,而通用汉字集里的“使”是宋体,特征不匹配。
解决:不靠数据增强,而是在识别头charset中为使领馆类型单独添加“使/领”字的 3 种变体(黑体、加粗、镂空),共 6 个 token,并在训练时强制用这些变体渲染合成数据。
注意:以上四条全部来自真实翻车记录。尤其第 1 条,我们曾为提速开启
cudnn.benchmark,结果在 Jetson 上帧率从 22 FPS 降到 13 FPS,排查了两天才发现是 cudnn 在反复编译。
6. 进阶技巧:如何用 20 行代码实现“识别结果可信度量化”,让运维人员一眼看出哪张图该复核
准确率数字骗人,但运维需要知道“这张图为什么不准”。我们不搞复杂的不确定性估计,而是用三个轻量级指标合成一个 0~100 的可信度分数,嵌入到输出 JSON 中:
| 指标 | 计算方式 | 权重 | 合理区间 |
|---|---|---|---|
| 检测框置信度均值 | np.mean(det_scores) | 30% | >0.75 为优 |
| 字符识别熵 | -sum(p*log(p)),p 为各字符槽位 softmax 概率 | 40% | <1.2 为优(越确定熵越低) |
| 结构合规度 | 规则匹配得分(如新能源末位是 D/F 得 1 分,否则 0) | 30% | 1.0 为优 |
def calculate_confidence(det_scores, logits_list, plate_type, pred_str): # 1. 检测置信度 det_conf = np.mean(det_scores) if len(det_scores) > 0 else 0.0 # 2. 字符熵(取每个槽位 top2 概率计算) entropy = 0.0 for i, logits in enumerate(logits_list): probs = torch.softmax(logits, dim=-1) top2_probs = torch.topk(probs, 2).values # 近似熵:-p1*log(p1) - p2*log(p2),忽略其余 p1, p2 = top2_probs[0], top2_probs[1] entropy += -(p1 * torch.log(p1 + 1e-8) + p2 * torch.log(p2 + 1e-8)) char_entropy = entropy.item() / len(logits_list) # 平均熵 # 3. 结构分 struct_score = 1.0 if plate_type == "new_energy": if len(pred_str) < 8 or pred_str[-1] not in ['D','F']: struct_score = 0.0 # 加权合成 conf_score = ( det_conf * 0.3 + (2.0 - char_entropy) * 0.4 + # 熵越低分越高,上限2.0 struct_score * 0.3 ) return max(0.0, min(100.0, conf_score * 100)) # 映射到 0~100 # 输出示例: # { # "plate": "粤B123456D", # "type": "new_energy", # "confidence": 92.4, # "details": {"det_conf": 0.85, "char_entropy": 0.92, "struct_score": 1.0} # }为什么这个设计有效:它不依赖额外模型,所有计算都在推理链路内完成;熵值对模糊/低对比度图像敏感(字符概率分散 → 熵升高 → 分数下降);结构分直击业务痛点——新能源末位错就是硬伤。我们在某园区部署后,运维人员反馈:“以前要翻 100 张图找 5 张错的,现在直接按 confidence <70 筛,10 张里就有 4 张真问题,效率翻倍。”
我的习惯是:每次上线新版本,都用这个 confidence 分数画一张散点图(X=时间,Y=confidence),如果某天下午出现密集低分点,不用看图,直接查那天的光照传感器数据——八成是阳光角度导致反光。这比等用户投诉快 3 小时。
希望帮到你。
本文还有配套的精品资源,点击获取