尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

车牌识别毕设避坑指南:OpenCV定位分割+轻量CNN识别全流程

车牌识别毕设避坑指南:OpenCV定位分割+轻量CNN识别全流程 简介面向计算机毕业设计学生与图像处理入门者这份资料围绕车牌定位、字符分割与字符识别完整流程深度融合数字图像处理与CNN深度学习技术既能支撑毕设课题设计也适合智能交通场景下的算法复现。资源包共56个文件以jpg图像数据集、hpp/cpp等C源码为主另含sln工程文件、PDF论文、xlsx题目清单与PPT模板总大小约346MB目录结构清晰便于按模块快速检索。项目覆盖图像预处理去噪、边缘检测、CNN模型定位、字符分割与识别等关键步骤附带的论文对研究背景、技术路线与实验结果作系统阐述CNN模型经大量样本训练可直接加载运行。压缩包内还额外提供300套计算机毕业设计题目和答辩PPT模板辅助完成开题与答辩准备。目前已吸引1496人学习下载适合需要快速获得完整可运行方案、同时兼顾论文与答辩材料的本科毕业生。1. 车牌识别为什么是毕业设计的“常青树”也是“翻车重灾区”在毕业设计的选题清单里车牌识别已经火了不止五年。每年都有学生交“基于数字图像处理和深度学习的车牌定位、字符分割与识别系统”但答辩现场翻车的比例高得吓人定位框框到车灯上字符分割把“京”切成两半识别结果把“O”和“0”混成一团。问题不是算法原理讲不通而是能跑通的Demo和能复现的系统之间隔着光照阈值、数据集划分、依赖版本、误差评估这些非常具体的工程细节。这篇文章按我实际做过一轮的方案来讲用数字图像处理完成定位和字符分割用轻量CNN完成字符识别最后把三个模块串成一条能在答辩现场复现的流水线并给出每条坑的避让方法。2. 先定技术路线为什么“传统定位分割 深度学习识别”才是最优解2.1 三阶段管线定位、分割、识别各管一段一个车牌识别系统拆开来看是三个彼此独立的子问题车牌在哪字符边界在哪字符是什么。很多人为了省事想把这三个问题全塞进一个端到端网络里就毕设而言这是自己给自己挖坑。目标检测模型能给出车牌的外接矩形但矩形内部还包含边框、螺丝、渐变喷漆和凹陷的字体如果直接把这个区域喂给识别网络字符之间的相对位置会被这些噪声干扰。端到端OCR模型则依赖大量带序列标注的车牌数据而大多数毕设手里只有几百张图片训练数据不够。更重要的是答辩老师问“字符分割你是怎么实现的”时如果回答“OCR库里封装了”这等于把题目里最有区分度的一环让了出去。所以常见做法是三条路线打组合OpenCV负责车牌候选区域提取和字符切分一个轻量CNN负责把切好的字符识别成汉字、字母和数字。这条路线的优势和题目表述天然对应“数字图像处理”承担定位和分割“深度学习”承担识别两者各自有可展开的工作量也能在论文里形成两个章节而不是一段空话。阶段传统数字图像处理方案深度学习方案本项目选择车牌定位HSV颜色过滤 形态学 轮廓筛选YOLO目标检测传统方案字符分割垂直投影 连通域分析语义分割网络传统方案 规则兜底字符识别模板匹配、特征工程CNN图像分类CNN为什么这样选后面三章的代码会给出答案。先说结论传统方案的每个阈值你都能对着图像解释比如为什么宽高比阈值取2.0到5.5一旦答辩老师问“为什么不用深度学习做定位”你还能回应“本项目用深度学习做字符识别定位采用数字图像处理来降低对训练数据量的依赖二者互补”。这句话比“我直接YOLO端到端”更能体现对题目两个关键词的理解。2.2 定位选型HSV颜色过滤加轮廓比直接上YOLO更值得定位环节最常被问的一句话是“现在YOLO这么成熟为什么还要用颜色和边缘”我的回答是YOLO至少在毕设阶段有一个很尴尬的问题——它要求你有一批标注好的车牌框数据还要处理漏检、误检、类别不平衡。用传统视觉做定位你不需要一台GPU训练半天也不需要标注几十个类别的背景目标一张蓝色掩码图就能把图像里非车牌的区域成片干掉。但传统视觉的定位也有它的适用边界它更适合蓝底白字的传统燃油车牌对新能源的渐变绿牌和警车白牌需要重新调HSV阈值。我会在第五章的避坑部分专门讲光照对阈值的冲击。这里先给出选型判断的条件如果你的拍摄场景是校园停车场、小区出入口这类相对固定的视角传统方案完全够用如果测试图里出现夜间远光灯、暴雨反光、极度倾斜那才需要考虑用深度学习检测器做一级候选。我的建议是在答辩PPT里把YOLO放在“改进方向”而不是“技术方案”里。这样既展示了你知道有更新方案又不会因为没调好而翻车。2.3 分割选型字符分割为什么不能直接丢给OCR定位框拿到后接下来是字符分割。我第一次做这个项目时觉得这步最简单结果花的时间最多。问题主要出在汉字上。蓝色车牌的第一个字符是各省简称比如京、沪、浙、苏。汉字的笔画结构和字母数字完全不同垂直投影时“京”字中间的横竖交叉会让投影值出现多个波谷一个字符被切成两段而“沪”字三点水左侧会单独形成一个连通域容易被误认为两个字符。如果分割做不好识别部分再强也没用因为送入模型的图像根本不是一个完整字符。反过来只要分割稳定识别用一个小网络就能达到95%以上的字符准确率。最稳妥的思路不是“一个算法吃遍所有字”而是用先验规则加固先对二值图做一次横向膨胀把汉字内部笔画粘连起来再用垂直投影找字符边界最后用“必须切出5到8个候选块按间距聚类成7个字符”这种规则做兜底。第三个选择是一开始就放弃投影法改用连通域分析。这个方法对“川”这类左右结构汉字效果好但容易被车牌中间的小圆点和螺丝干扰。我在实际操作中会把两种方法都保留先跑投影失败再切连通域用简单规则做优先级。3. 用OpenCV跑通定位与分割完整代码和三条调参经验3.1 预处理链路为什么先缩放到统一高度再转HSV写定位代码之前我习惯先把图像统一到一个尺寸范围。原因很简单不同相机分辨率差很多同样一个车牌在1080p下占的区域是720p下的两倍如果后续用固定像素阈值过滤面积那就没法写。通常做法是把图像高度缩放到720以内宽等比缩放。import cv2 import numpy as np def preprocess(src): # 统一处理高度避免远处车牌因为绝对像素太小而被误删 h, w src.shape[:2] if h 720: scale 720.0 / h src cv2.resize(src, None, fxscale, fyscale, interpolationcv2.INTER_AREA) # 转HSV用于颜色过滤灰度图留给后续边缘检测 hsv cv2.cvtColor(src, cv2.COLOR_BGR2HSV) gray cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) # 蓝色车牌在HSV中的Hue区间100到124 # S和V放宽考虑不同光照下饱和度变化 blue_mask cv2.inRange(hsv, (100, 80, 80), (124, 255, 255)) # 闭运算填掉车牌字符之间的孔洞开运算去掉背景零星蓝点 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) blue_mask cv2.morphologyEx(blue_mask, cv2.MORPH_CLOSE, kernel) blue_mask cv2.morphologyEx(blue_mask, cv2.MORPH_OPEN, kernel) return gray, blue_mask, src这里有三个调参经验。第一HSV的Hue下界我放在100而不是90因为90到100之间偏青蓝色很多车身漆色混在里面会带来大量假阳性。第二S和V的下界不能都设成0否则阴影里的蓝色路面都会被当成车牌80这个值是校园场景的折中如果测试图整体偏暗可以降到40到50。第三形态学核的大小取决于输入图像尺寸如果前面不做缩放5×5在4K图上几乎没有效果所以先统一高度再定核很重要。3.2 候选框筛选面积、宽高比、蓝像素占比三重过滤获得了蓝色掩码之后接下来用边缘加轮廓的方式找候选车牌框。注意边缘检测和颜色掩码是“与”的关系而不是先在全图上找轮廓再筛选。def locate_plate(gray, blue_mask, src): # Canny高阈值200避免背景纹理混入低阈值120保住车牌字符边缘 edges cv2.Canny(gray, 120, 200) # 只保留蓝色区域内的边缘这一步比先找轮廓再过滤高效得多 mask cv2.bitwise_and(edges, edges, maskblue_mask) # RETR_EXTERNAL只取最外层轮廓避免车身文字轮廓干扰 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: area cv2.contourArea(cnt) if area 1500: continue # 缩放后小于1500像素的块基本不可能是车牌 rect cv2.minAreaRect(cnt) # 得到带角度的外接矩形 w, h rect[1] if w h: w, h h, w aspect w / h # 标准车牌宽高比为440/140约3.14透视变形后2.0到5.5都能接受 if not (2.0 aspect 5.5): continue # 再验证框内蓝色像素占比至少30%才算可信车牌区 box np.int0(cv2.boxPoints(rect)) frame_mask np.zeros(gray.shape, dtypenp.uint8) cv2.drawContours(frame_mask, [box], -1, 255, thickness-1) overlap cv2.bitwise_and(blue_mask, frame_mask) ratio cv2.countNonZero(overlap) / max(1, cv2.countNonZero(frame_mask)) if ratio 0.3: continue # 得分 蓝色占比 * 面积多候选时取最大值 score ratio * area candidates.append((box, rect, score)) if not candidates: return None candidates.sort(keylambda x: x[2], reverseTrue) return candidates[0]把边缘与颜色掩码做“与”可以滤掉大量蓝色车身、蓝色指示牌的边缘比单纯在整图上找轮廓再判断快很多。需要注意这里用到的宽高比阈值是配合缩放后图像的经验值如果前面把高度缩放到其他尺寸面积阈值要跟着调整。cv2.drawContours填充的是候选框内部frame_mask覆盖的是一个倾斜矩形区域里面包含车牌和四角一点背景。蓝色占比大于0.3这个值是我在白天场景下的经验值阴天会偏低雨后偏高后面避坑章节会讲怎么动态处理。3.3 透视矫正与字符分割把旋转的车牌拉正再切字找到候选框后需要用minAreaRect得到的角度信息把旋转的车牌矫正成水平矩形。这一步不做分割会很痛苦因为投影方向一旦偏移字符之间会连成一片。def order_points(box): # box来自cv2.boxPoints四个点顺序是乱的 s box.sum(axis1) diff np.diff(box, axis1).ravel() tl box[np.argmin(s)] br box[np.argmax(s)] tr box[np.argmin(diff)] bl box[np.argmax(diff)] return np.array([tl, tr, br, bl], dtypefloat32) def rectify_plate(src, box): src_pts order_points(box) dst_pts np.array([[0, 0], [240, 0], [240, 80], [0, 80]], dtypefloat32) M cv2.getPerspectiveTransform(src_pts, dst_pts) warped cv2.warpPerspective(src, M, (240, 80)) return warped为什么目标尺寸选240×80标准车牌比例是440×140约3.14240×80保持这个比例又不会因为太大而让后续分割计算变慢。order_points里的求和方法是固定套路x加y最小是左上x加y最大是右下x减y最小是右上x减y最大是左下。字符分割的完整逻辑如下。def segment_chars(warped): gray cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) # 蓝底白字白字亮度高需要反色二值化OTSU自动选阈值 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 行投影去掉上下边框和铆钉 row_sums binary.sum(axis1) // 255 rows np.where(row_sums 0)[0] if len(rows) 0: return [] binary binary[max(0, rows[0] - 2):min(len(row_sums), rows[-1] 3), :] # 关键一步横向膨胀让“京”“沪”这类汉字的离散笔画连起来 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 1)) binary cv2.dilate(binary, kernel, iterations1) # 列投影找字符边界 col_sums binary.sum(axis0) // 255 chars [] start None for i, v in enumerate(col_sums): if v 0 and start is None: start i elif v 0 and start is not None: width i - start if width 8: chars.append((start, i)) start None if start is not None and len(col_sums) - start 8: chars.append((start, len(col_sums))) # 分割出的块可能多于7个按列间距合并 while len(chars) 7: gaps [chars[i 1][0] - chars[i][1] for i in range(len(chars) - 1)] idx int(np.argmin(gaps)) chars[idx] (chars[idx][0], chars[idx 1][1]) del chars[idx 1] char_imgs [] for left, right in chars: char_img warped[0:80, left:right] char_imgs.append(char_img) return char_imgs列投影自然能切出连续的字符块但汉字内部的缝隙会捣乱。所以我在投影前先做了dilate核是3×1也就是只横向膨胀不纵向膨胀。这样“京”字中间的小间隙被连起来而字符与字符之间的间隔还在。合并过宽的块的循环是兜底策略防止边缘或螺丝被当成一个字符。如果分割结果少于7个说明汉字膨胀后和旁边的字母粘连了。常见补救办法是把膨胀核宽度从3改成2或者直接放弃投影法改用cv2.connectedComponentsWithStats做连通域分析再按x坐标排序切分。两种方法各有优势建议在代码里都实现一遍用简单的规则决定先跑哪个。4. 用轻量CNN做字符识别数据集、模型、训练与推理4.1 字符数据集准备按车牌ID划分别按字符打乱把分割好的字符整理成训练集有一个非常容易被忽略的原则必须按“整张车牌”分组划分训练集和验证集。如果单纯把所有字符随机打乱那么同一张车牌的“京”和“A12345”可能一部分进训练集一部分进验证集。验证集相当于是被预演过的得到的高准确率并不能反映真实场景。答辩现场随便拍一张图模型表现就垮掉。正确做法是先把车牌ID提取出来比如字符文件命名成“京A12345_0.png”然后按“京A12345”这个前缀做分组让同一个前缀的所有字符都落在同一侧。import os import random def split_by_plate(char_root, train_file, val_file, seed42): random.seed(seed) plates {} # 遍历字符目录按文件名前缀分组 for label in os.listdir(char_root): label_dir os.path.join(char_root, label) if not os.path.isdir(label_dir): continue for fname in os.listdir(label_dir): plate_id fname.rsplit(_, 1)[0] plates.setdefault(plate_id, []).append( os.path.join(label_dir, fname)) plate_ids list(plates.keys()) random.shuffle(plate_ids) split int(len(plate_ids) * 0.8) train_ids set(plate_ids[:split]) # 写清单文件后续Dataset直接读 with open(train_file, w) as f: for pid, paths in plates.items(): if pid in train_ids: for p in paths: f.write(p \n) with open(val_file, w) as f: for pid, paths in plates.items(): if pid not in train_ids: for p in paths: f.write(p \n)这样训练集和验证集之间没有同源样本评估出来的准确率才敢写进论文。4.2 最小可靠CNN从Dataset定义到训练循环字符识别模型不需要很大。输入是32×32的灰度图输出对应字符类别数。我用一个三层卷积加两个全连接的结构参数量不到50万在CPU上训练几千张图也就几分钟到十几分钟。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image class CharDataset(Dataset): def __init__(self, list_file): self.paths [] self.labels [] with open(list_file, r) as f: for line in f: line line.strip() if not line: continue self.paths.append(line) self.labels.append(os.path.basename(os.path.dirname(line))) self.classes sorted(set(self.labels)) self.label2idx {c: i for i, c in enumerate(self.classes)} self.idx2label {i: c for c, i in self.label2idx.items()} self.transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(L) img self.transform(img) label self.label2idx[self.labels[idx]] return img, label这个Dataset的类别名来自字符目录名。如果目录名是汉字“京”它就能自动编号。Normalize([0.5], [0.5])表示把0到1的像素值变成-1到1这是常见的灰度图归一化方式。模型定义如下class CharNet(nn.Module): def __init__(self, num_classes): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 4 * 4, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x))32×32的输入经过三次池化变成4×4最后的特征图是128×4×42048维。加一层Dropout是为了防止在小数据集上过拟合这是毕设场景里最常见的问题。训练循环def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total, correct 0, 0 for x, y in loader: x, y x.to(device), y.to(device) optimizer.zero_grad() out model(x) loss criterion(out, y) loss.backward() optimizer.step() pred out.argmax(dim1) total y.size(0) correct (pred y).sum().item() return correct / total device torch.device(cuda if torch.cuda.is_available() else cpu) train_ds CharDataset(train.txt) train_loader DataLoader(train_ds, batch_size64, shuffleTrue, num_workers2) model CharNet(len(train_ds.classes)).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) for epoch in range(30): acc train_one_epoch(model, train_loader, optimizer, criterion, device) print(fepoch {epoch 1:02d}, train acc {acc:.4f})为什么用交叉熵而不用MSE字符分类是标准多分类问题交叉熵自带Softmax概率解释收敛比MSE稳定。为什么用Adam而不用SGD小数据集上Adam的默认学习率更稳省去调学习率调度的麻烦。30个epoch听起来不多但字符图像结构简单通常第10轮左右就开始过拟合所以更好的做法是每个epoch后在验证集上算一次准确率连续5轮不上升就提前停止。4.3 推理流水线定位、分割、识别串成一个函数训练完成后把第三章的定位分割和第四章的识别模型串起来。思路是输入图片路径输出车牌字符串和每个字符的置信度。def recognize_plate(image_path, model, idx2label, device, threshold0.8): src cv2.imread(image_path) if src is None: return None, 图片路径读不到先检查路径 gray, blue_mask, resized preprocess(src) result locate_plate(gray, blue_mask, resized) if result is None: return None, 没有定位到车牌 box, rect, score result warped rectify_plate(resized, box) char_imgs segment_chars(warped) if len(char_imgs) 7: return None, f分割出{len(char_imgs)}个字符少于7个 plate details [] for char_img in char_imgs: char_gray cv2.cvtColor(char_img, cv2.COLOR_BGR2GRAY) char_resized cv2.resize(char_gray, (32, 32), interpolationcv2.INTER_AREA) x torch.from_numpy(char_resized).float().unsqueeze(0).unsqueeze(0) x (x - 0.5) / 0.5 x x.to(device) with torch.no_grad(): logits model(x) probs torch.softmax(logits, dim1) conf, pred probs.max(dim1) char idx2label[pred.item()] if conf.item() threshold: char ? plate char details.append((char, conf.item())) return plate, details我在推理时加了一个阈值判断如果最大置信度低于0.8就把这个字符置为“?”而不是硬报一个结果。这个“拒识”机制很重要它让整个系统的输出带上了置信度边界答辩时可以明确说“不确定的字符我选择不识别”这比输出一个错误字符更可靠。模型推理前要不要再把图像转彩色不需要。分割阶段已经把颜色信息用过了识别阶段用灰度图可以减少过拟合也加快推理速度。5. 车牌识别项目避坑指南5个让“可靠运行”变成玩笑的细节5.1 定位框偏移一大截先查Canny阈值和轮廓层级现象测试图上框出的区域不是车牌而是前挡风玻璃或店面招牌定位框偶尔还会剧烈跳动。原因直接在整图上findContours树形嵌套的轮廓会被当成一个整体车灯、玻璃边缘、深色车身的轮廓全部混杂进来。另一个常见原因是Canny低阈值设得太低比如80以下背景纹理变成边缘高阈值设得太高车牌字符边缘又断掉。解决先做颜色掩码在颜色掩码和边缘的“与”结果上找轮廓再用RETR_EXTERNAL只取最外层轮廓。如果还是偏移把Canny低阈值从120往上调到140高阈值从200往下调到180这两个值在实际数据里往往只需要微调十来个单位。5.2 汉字全认成字母字符分割把字形切碎了现象训练时字符准确率很高但真实测试时车牌第一位汉字几乎全错“京”“沪”被识别成“O”“I”甚至空白。原因字符分割阶段把汉字切成上下两截或左右两块识别网络看到的是裂开的图像学到的特征是裂缝而不是字形。汉字笔画密集垂直投影天然不稳定。解决分割前先做横向膨胀核可以是3×1让汉字内部笔画粘连。另一个实用技巧是用位置先验纠错车牌的语义结构是“汉字字母五个字母数字”识别完一个车牌后检查第一个字符是否在省份简称集合里第二个字符是否在英文字母集合里如果不符合就回退到分割阶段重新处理或对候选分割结果做二次识别。5.3 白天定位正常黄昏就开始找不到车牌现象下午三点测试全部通过傍晚五点开始大面积漏检定位成功率从95%跌到40%。原因HSV颜色空间里蓝色车牌的Hue值相对稳定但饱和度和亮度会随光照急剧下降。如果inRange的S和V下界写死80黄昏时的车牌像素饱和度可能跌到40直接被过滤掉。解决不要用固定阈值直接在HSV空间做轻量自适应。做法是先计算整张图像的V通道均值再用这个均值动态调整V下界比如v_min max(40, avg_v * 0.3)。更稳的方案是先把图像做直方图均衡化让亮度分布更均匀再转HSV取色。但要记住均衡化会改变色彩关系所以最好只在定位阶段使用均衡化后的图不把均衡化结果用于分割和识别。5.4 “保证可靠运行”却在答辩电脑上打不开环境依赖没冻结现象在自己电脑上一切正常把项目拷到答辩教室电脑OpenCV报错、PyTorch导入失败、模型路径找不到。原因OpenCV和PyTorch都是重依赖版本不同API就有差异很多学生直接把整个Anaconda环境复制到U盘搬过去又因为路径硬编码而崩溃。解决项目根目录放一份requirements.txt写明Python版本和主要依赖版本例如python3.8、opencv-python4.6.0.66、torch1.13.1。图片路径、模型路径全部用相对os.path.join拼出来不要写死绝对路径。答辩前一周在一台干净电脑上从零创建虚拟环境跑通一次完整推理流程。模型尽量用CPU能跑的结构因为答辩机多半没有NVIDIA独立显卡。5.5 论文里的准确率被质疑混淆了字符级和整牌级指标现象论文写“字符识别准确率98%”答辩老师现场用一张完整车牌图测试系统要么漏检要么识别错一个字符于是被质疑结果造假。原因训练时按单字符分割评测测试时按整张车牌评测两个指标的统计口径完全不一样。整张车牌只要错一个字符就整个失败所以整牌准确率必然低于字符准确率通常低5到15个百分点。解决论文里必须同时给两个指标。字符级准确率定义为单个字符预测正确的比例整牌级准确率定义为一张车牌七个字符全部正确的比例另外再加一个拒识率。答辩时主动说“整牌准确率是91%”比被动地被发现更体面。统计整牌准确率时测试集必须按车牌ID分组这也就是4.1节强调的划分方式。6. 答辩前的可靠性验证错误可视化与PPT的最后一页6.1 一个脚本暴露所有弱点把定位框、分割块、置信度画在同一张图与其等到答辩现场翻车不如自己先给系统“找茬”。我习惯写一个可视化评估脚本把定位框、字符分割边界、每个字符的预测值和置信度都画到原图上。这样一眼就能看出问题出在哪个模块。def debug_visualize(image_path, model, idx2label, device, out_dir): src cv2.imread(image_path) gray, blue_mask, resized preprocess(src) result locate_plate(gray, blue_mask, resized) if result is None: cv2.putText(src, no plate found, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imwrite(os.path.join(out_dir, os.path.basename(image_path)), src) return box, rect, _ result warped rectify_plate(resized, box) char_imgs segment_chars(warped) cv2.drawContours(resized, [np.int0(box)], -1, (0, 255, 0), 2) # 画分割边界和识别结果 x_offset 0 for char_img in char_imgs: h, w char_img.shape[:2] cv2.rectangle(resized, (box[0][0] x_offset, box[0][1]), (box[0][0] x_offset w, box[0][1] h), (255, 0, 0), 1) x_offset w cv2.imwrite(os.path.join(out_dir, os.path.basename(image_path)), resized)这个脚本把定位和分割的中间结果固化到图片上跑完全部测试图后按“定位失败”“分割失败”“识别低置信度”三个维度分类放文件夹。这个过程本身就是答辩PPT里“系统可靠性分析”一节的素材。6.2 答辩PPT的“可靠性叙事”素材怎么讲答辩PPT的最后一页不要放架构图放错误分析表。例如在100张校园停车场测试图里定位失败6张分割失败4张识别失败2张每类失败配一张现场图。这说明可靠性不是玄学而是可以量化的工程指标。我自己的讲解逻辑是先展示整体流程再放成功的案例然后主动放失败案例最后给出失败原因和改进方向。比如“6张定位失败里4张是因为傍晚光照下蓝色饱和度过低下一步可以引入YOLO做候选区域兜底”。这句话比“准确率99%”更有说服力因为老师看到的是你在做研究而不是在报喜。我第一次做车牌识别项目也栽在字符分割上后来在分割前加一次横向膨胀整牌识别率直接涨了十来个点。如果你拿到这个项目建议先跑通第三章的定位分割再看第四章的识别不要急着改模型结构。希望帮到你。本文还有配套的精品资源点击获取
返回列表