简介:这是一套基于Python与OpenCV、深度学习的车牌识别毕业设计源码及文档,面向计算机视觉方向的本科生与研究生,可用于毕业设计、课程设计或期末大作业。系统完整覆盖图像预处理、车牌区域定位、字符分割与卷积神经网络识别等核心流程,代码逐模块含注释,各环节划分清晰,便于新手对照原理理解并二次开发。压缩包共18个文件,包含5个Python脚本、5张PNG与3张JPG示例图片、2个DAT模型数据、1份PPTX答辩演示、1个Markdown说明文档及配置文件,整包仅4.73MB,小巧易部署。目前已有86人学习下载,除可运行的完整源码外,还提供识别流程演示图、训练好的模型数据与答辩PPT,能帮助读者从环境搭建到算法实现快速落地,也可作为同类图像识别任务的参考模板。
1. 车牌识别毕设从哪下手:不是模型选型,而是先把流程拆成三段
停车场夜间识别翻车,这个场景大多数做过车牌识别的人都不陌生:白天准确率还挺好看,到了晚上蓝牌被路灯染成灰蓝色,字符边缘糊成一团。这套基于Python和OpenCV的深度学习车牌识别系统,把毕设课题拆成「OpenCV做定位与校正、深度学习做字符识别」两段,代码量在一个学期能完成的范围,论文也有清晰的技术主线。它解决的核心问题是「从一张停车场出入口原图到输出车牌字符串」的完整链路,而不是一个孤立的模型。适合正在选毕设题、或想快速搭一个可演示车牌识别系统的开发者。接下来按流程怎么拆、数据怎么造、代码怎么写、坑在哪四个角度展开。
2. 为什么是OpenCV+深度学习:车牌识别的分工边界与技术选型
车牌的视觉特征和通用物体差别很大,它是一块高对比度、等宽字符、颜色固定的印刷体金属板。这个先验决定了技术选型不能照搬通用OCR或者通用目标检测的思路,而应该把任务按「哪儿有车牌、车牌正不正、牌上是什么字」拆成三段,每一段用最便宜可靠的手段去做。
2.1 传统CV负责什么:定位与校正为什么不用深度模型
车牌定位是一个强先验任务:蓝底白字或黄底黑字、宽高比接近3.14、字符横向等距排列、位置固定在车头车尾。这些约束让OpenCV的颜色分割加轮廓筛选变得非常划算,在一张1080p图上单帧定位耗时20到40毫秒,CPU就能跑。如果用目标检测网络,先要标注几千张带车牌框的图片,训练完还要处理NMS阈值、置信度阈值、anchor尺寸这些新调参点,对毕设来说投入产出比不高。
深度检测的真正优势在极端场景:车牌倾斜超过45度、被保险杠遮挡、雨雾天气、多目标同帧。如果演示场景就是普通停车场出入口,光线基本可控,那么传统定位的稳定性足够用。更重要的是答辩时能说清楚每个步骤的选型理由:「利用车牌的颜色与宽高比先验,在计算资源受限时达到实时帧率」,这句话比「我调了一个YOLO」扎实得多。定位模块的输出统一成旋转矩形的四个顶点,以后想换深度检测器,只需要替换这一个模块,后续校正和识别流程完全不用动。
| 对比维度 | 传统CV定位 | 深度目标检测 |
|---|---|---|
| 标注成本 | 不需要标注 | 需要数千张车牌框标注 |
| 单帧耗时 | 20-40ms(CPU) | 30-100ms(GPU)或更高 |
| 极端场景鲁棒性 | 差 | 好 |
| 调参复杂度 | 低,阈值直观 | 高,涉及anchor和NMS |
| 毕设论文篇幅 | 需解释每个算子 | 需解释网络结构 |
2.2 深度学习负责什么:字符识别才是深度模型的主场
车牌字符是一个封闭小集合:31个省份简称汉字、24个字母(不含I和O)、10个数字,加起来65类左右。这种细粒度图像分类任务正是小型CNN擅长的,不需要上CRNN或者端到端车牌识别网络。常用做法是「定位→校正→分割→分类」,每个环节的错误都能单独定位:分割出错就查投影参数,分类出错就查训练集。如果直接上端到端模型,输入一张车牌图输出字符串,表面省事,实际上需要上万张整牌标注数据,而且一旦识别错,你分不清是检测器的问题还是识别器的问题,论文里也没法分章节展开。
为什么不直接用通用OCR引擎?车牌用的是定制字体,字符间距固定,通用OCR的文本检测框对七位等宽字符经常误切;而且通用引擎输出的是「一行文字」,你还要自己按坐标切字符,白白多一层转换。数据量上也可以算一笔账:分段识别只需要65类、每类200张字符图就能起步,总共一万多张;端到端方案的数据需求是它的五到十倍。对毕设的时间预算来说,这个差距是决定性的。
2.3 环境与依赖:版本怎么配才能少踩坑
开发环境建议用虚拟环境独立装,避免和机器上其他项目的包互相污染。常用依赖就五个:Python、OpenCV、PyTorch、NumPy、Pillow,全部装CPU版也能完成这个项目。最容易翻车的点是opencv-python和opencv-contrib-python混装,两个包同时存在会导致cv2模块部分函数行为异常,表现是无缘无故报错或者某个函数找不到。处理办法很简单:全部卸载,只装opencv-python。车牌识别用不到contrib里的SIFT、SURF那一类功能。
| 包名 | 用途 | 版本建议 |
|---|---|---|
| opencv-python | 图像读写、颜色分割、形态学、轮廓 | 4.5以上即可 |
| PyTorch | 字符分类网络训练与推理 | 1.10以上或2.x |
| numpy | 数组运算与投影统计 | 1.21以上 |
| Pillow | 数据增强与字符图渲染 | 8.3以上 |
CPU能不能训练?能。这个字符分类网络的参数量在一两百万级别,65类、32×32输入,普通笔记本CPU跑30个epoch大约一到两小时,完全不需要GPU。如果你赶时间,把输入缩到28×28,训练时间能再压缩三分之一。
3. 数据先于代码:字符级数据集怎么组织、怎么合成、怎么标
很多人在这个项目上进度失控,不是因为网络训练不动,而是因为数据集没想清楚就开写代码。车牌识别项目里,代码是确定性工作,数据才是决定上限的部分。这一章把数据从零到可用讲透。
3.1 最小可用数据集的构成
先明确一点:这个项目不需要收集整张车牌图并标注字符位置,只需要字符级的小图。目录结构建议按类别分文件夹,每一类就是一个字符。最小可用标准是每类200张训练图、50张验证图,65类合计约1.6万张,这个量级CPU训练毫无压力。省份简称只有31类,样本量天然少于数字和字母,这是后面识别率失衡的根源,必须在建目录时就有意识地去平衡。
真实数据从哪来?最常见的做法是拿手机或摄像头在停车场出入口录一段视频,用第四章的定位代码批量把车牌裁出来,再自动分割成单字符,人工筛掉分割错误的图。这个流程跑一轮就能拿到几百张真实字符图。合成数据用PIL渲染:在纯色或渐变背景上按车牌标准字形绘制字符,加一点高斯模糊和透视畸变。合成时有三个纪律:字体要接近车牌标准字形,别用艺术字体;不要做随机裁剪,字符一定要占满整图;每类生成数量保持均衡,尤其汉字类别不能偷懒。
不用现成公开整牌数据集的原因有两个:一是整牌数据集的标注格式五花八门,转成字符级要写一堆转换脚本;二是部分来源的图片包含真实车辆信息,毕设源码要提交归档,数据版权和隐私问题容易惹争议。自己造字符集数据完全可控,答辩时还能把数据生成流程写进论文。
3.2 数据增强的四个参数
车牌字符增强和通用分类增强不一样,字符是等宽、居中、占满整图的,增强的重点是几何扰动而不是内容裁剪。直接照搬通用分类的RandomResizedCrop会出问题:字符图的边角一旦被裁掉,笔画信息就丢了,识别率反而下降。
| 增强操作 | 建议参数 | 说明 |
|---|---|---|
| 随机旋转 | ±10度 | 超过15度字符形变严重,反而不利于分割来的样本 |
| 亮度扰动 | 原值±30 | 模拟白天和黑夜的光照差 |
| 高斯噪声 | 标准差5 | 模拟摄像头传感器噪声 |
| 透视畸变 | 幅度0.02 | 模拟侧视角度,幅度再大会让笔画扭曲 |
增强顺序也有讲究:先做几何(旋转、透视),再做光度(亮度、噪声)。透视和旋转不要同时开到最大,两个形变叠加后字符会失去可辨识性。对汉字类别,可以额外多做一些旋转增强,因为汉字笔画密,对角度更敏感。
3.3 目录扫描与标签映射
训练时需要把字符类别映射成数字索引。常见做法是扫描目录生成类别清单,再按8:2划分训练和验证列表。下面这段代码直接可用。
import os import random data_dir = "datasets/char" classes = sorted(os.listdir(os.path.join(data_dir, "train"))) # classes 形如 ['0','1','A','B','京','粤',...],每个名字是一个字符类别 with open("classes.txt", "w", encoding="utf-8") as f: for c in classes: f.write(c + "\n") train_lines, val_lines = [], [] for idx, cls in enumerate(classes): img_names = os.listdir(os.path.join(data_dir, "train", cls)) random.shuffle(img_names) split = int(len(img_names) * 0.8) for name in img_names[:split]: train_lines.append(f"{cls}/{name} {idx}\n") for name in img_names[split:]: val_lines.append(f"{cls}/{name} {idx}\n") with open("train.txt", "w", encoding="utf-8") as f: f.writelines(train_lines) with open("val.txt", "w", encoding="utf-8") as f: f.writelines(val_lines) print(f"classes: {len(classes)} train: {len(train_lines)} val: {len(val_lines)}")这段逻辑的核心是类别索引和classes.txt行号一一对应,训练时用索引查表得到字符。用文件列表而不是每次os.walk遍历目录,是因为PyTorch的Dataset拿内存里的列表比每次重新扫磁盘稳定,尤其在训练过程中反复读取时能省去大量IO。split=0.8是训练比例,数据量小的时候可以调到0.85,但验证集会变薄,最好保持0.8。另外强调一点:目录名和classes.txt里不要用中文路径,Windows下Pillow读取中文路径偶尔会报错,这个坑一次就能耗掉半天。
4. 核心流程实现:从车牌定位到字符识别的全链路代码
这一章按实际运行顺序给出四个模块的代码:定位、校正、分割、识别。每个模块都是独立函数,可以单独调试。整体流程是定位 -> 校正 -> 分割 -> 识别,前两步归OpenCV管,最后一步归深度模型管,中间的分割是桥。
4.1 定位:HSV颜色分割与轮廓筛选的最小实现
定位的思路是先按颜色把车牌区域从背景里分离出来,再用形态学把字符间的缝隙连通,最后按面积和宽高比筛选候选框。
import cv2 import numpy as np def detect_plate(frame): # 缩小尺寸:定位阶段不需要高分辨率,缩图能降噪并加速 img = cv2.resize(frame, None, fx=0.8, fy=0.8) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌:H 100~124,S 90+,V 80+ blue_mask = cv2.inRange(hsv, (100, 90, 80), (124, 255, 255)) # 黄色车牌(部分货车/教练车):H 20~35 yellow_mask = cv2.inRange(hsv, (20, 90, 80), (35, 255, 255)) mask = cv2.bitwise_or(blue_mask, yellow_mask) # 闭运算连接车牌字符区域的断口 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cands = [] for c in contours: area = cv2.contourArea(c) x, y, w, h = cv2.boundingRect(c) if w < 60 or h < 15: # 过滤太小噪声 continue ratio = w / h if 2.5 < ratio < 5.0: # 车牌标准宽高比约3.14 cands.append((x, y, w, h, area)) if not cands: return None cands.sort(key=lambda t: t[4], reverse=True) x, y, w, h = cands[0] return img[y:y+h, x:x+w]HSV的H通道对光照相对鲁棒,但夜间色相仍在漂移,所以S和V都要给下限。形态学核(17, 5)是经验值:宽度方向要大,因为车牌字符横向排列,闭运算能把字符间隙连成一个块;高度方向太大容易把车灯和车身色块一起并入。宽高比放宽到2.5到5.0,是因为透视压缩会改变外接矩形的比例。按面积降序取最大,适配单车牌场景;出入口双车道画面里有两块车牌时,改成取前两个并按x坐标排序去重。HSV阈值与其网上抄,不如自己取:打开一张样例图,用取色器在车牌蓝色区域采五到十个点,统计H/S/V范围再套进去,这样出来的阈值最贴合你的摄像头。
4.2 校正:minAreaRect与仿射变换
定位裁出来的框如果是倾斜的,直接切出来送识别会带进大量背景,字符分割也会受影响。校正的标准做法是找轮廓的最小外接旋转矩形,再旋转回水平。
def correct_plate(img_gray): # 输入是 4.1 步裁出的候选区域(可能是倾斜的) _, binary = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None c = max(contours, key=cv2.contourArea) rect = cv2.minAreaRect(c) # ((cx,cy),(w,h),angle) 旋转矩形 angle = rect[2] (h, w) = img_gray.shape[:2] # 宽小于高时说明矩形竖着,角度要补90度 if rect[1][0] < rect[1][1]: angle -= 90 # OpenCV的角度定义是顺时针为正,旋转矩阵里要取反 M = cv2.getRotationMatrix2D((w / 2, h / 2), -angle, 1.0) rotated = cv2.warpAffine(img_gray, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) return rotatedminAreaRect返回的角度范围是-90度到0度,对竖矩形和横矩形的定义不同,最常见的翻车点是宽小于高时没补90度,以及旋转方向取反。BORDER_REPLICATE复制边缘像素填充旋转后的空白区,避免黑边干扰后续投影分割。旋转后图幅会变大,所以我一般会再做一次轮廓提取,把校正后的车牌区域重新裁干净,再进分割模块。校正用灰度图做就够,颜色在校正环节没有信息贡献;INTER_CUBIC适合缩小,如果校正后还要放大,换INTER_LINEAR更稳。
4.3 分割:二值化、水平投影与垂直投影
分割的输入是校正后的灰度车牌图,输出是七个单字符小图。原理是投影法:先把字符变成白色区域,然后统计每一行的白色像素数,找出字符行的上下边界;再在字符行内逐列统计,用列投影的谷值切分字符。
def segment_chars(plate_bgr): gray = cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2GRAY) # 中值滤波去掉铆钉和边框噪声 gray = cv2.medianBlur(gray, 3) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 水平投影:找字符行的上下边界 h_proj = np.sum(binary // 255, axis=1) rows = np.where(h_proj > 0)[0] if len(rows) == 0: return [] top, bottom = rows.min(), rows.max() # 垂直投影:逐列统计,用谷值切分 v_proj = np.sum(binary[top:bottom, :] // 255, axis=0) cols = np.where(v_proj > 0)[0] segments = [] start = cols[0] for i in range(1, len(cols)): if cols[i] - cols[i-1] > 1: # 列不连续,说明是字符间隙 if cols[i-1] - start >= 5: # 过滤宽度小于5的噪声列 segments.append((start, cols[i-1])) start = cols[i] if cols[-1] - start >= 5: segments.append((start, cols[-1])) chars = [] for sx, ex in segments: char_img = binary[top:bottom, sx:ex] chars.append(char_img) return charsTHRESH_BINARY_INV让字符为白、背景为黑,投影统计的是白色像素。中值滤波核取3,只压单像素噪声,取5会把细笔画抹平。字符间隙的判断阈值是「列号差大于1」,分辨率低时字符边缘会粘连,这个阈值可以放宽到2,但会把「川」这类左右分离的汉字切开,所以不要轻易动。宽度小于5的片段直接丢弃,用来滤掉字符中间的隔离点。分割完检查一下段数,不是七段就走回退逻辑:按七等分宽度均匀切分,作为保底方案。汉字被切开的问题在第5章细讲。
4.4 识别:训练字符CNN并推理
分割完的每个字符是大小不一的二值图,统一缩放成32×32再喂给CNN。网络结构用三层卷积加一层全连接,参数约40万,普通CPU跑得飞快。
import torch import torch.nn as nn class CharNet(nn.Module): def __init__(self, num_classes=65): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), # 32x32 -> 16x16 nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), # 16x16 -> 8x8 nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), # 8x8 -> 4x4 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 4 * 4, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes), ) def forward(self, x): return self.classifier(self.features(x))为什么不用ResNet这类深网络:车牌字符是等宽印刷体,纹理简单,加深网络收益不明显,反而训练时间翻倍。BatchNorm放在卷积和ReLU之间是常规排布,注意推理时必须model.eval(),否则BN的统计量会用当前batch的,结果飘忽不定。输入是单通道图,因为字符分割出来本身是二值图,颜色没信息。
训练部分用Adam优化器,lr=1e-3、batch=64、epochs=30是字符分类的保守起点。数据集小、类别均衡,Adam比SGD收敛快,也省去学习率调度的心智负担。
from torch.utils.data import Dataset, DataLoader from PIL import Image import torchvision.transforms as T class CharDataset(Dataset): def __init__(self, list_path, data_dir): self.samples = [] with open(list_path, encoding="utf-8") as f: for line in f: path, label = line.strip().split() self.samples.append((os.path.join(data_dir, path), int(label))) def __len__(self): return len(self.samples) def __getitem__(self, i): path, label = self.samples[i] img = Image.open(path).convert("L").resize((32, 32)) img = T.ToTensor()(img) # [0,1] img = (img - 0.5) / 0.5 # 归一化到 [-1,1] return img, label train_ds = CharDataset("train.txt", "datasets/char") train_dl = DataLoader(train_ds, batch_size=64, shuffle=True, num_workers=0) model = CharNet(num_classes=len(open("classes.txt", encoding="utf-8").readlines())) opt = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.CrossEntropyLoss() for epoch in range(30): model.train() total_loss = 0.0 for imgs, labels in train_dl: opt.zero_grad() out = model(imgs) loss = loss_fn(out, labels) loss.backward() opt.step() total_loss += loss.item() print(f"epoch {epoch+1:02d} loss {total_loss / len(train_dl):.4f}") torch.save(model.state_dict(), "char_net.pt")归一化到[-1,1]这一步最容易漏:训练时用了(img - 0.5) / 0.5,推理时忘了做同样的归一化,识别率会掉一大截。num_workers=0在Windows下最省心,设成大于0偶尔会卡在多进程初始化。loss在30轮内降到0.1以下基本可以停。训练完在验证集上快速看准确率:
model.load_state_dict(torch.load("char_net.pt")) model.eval() correct = total = 0 val_ds = CharDataset("val.txt", "datasets/char") val_dl = DataLoader(val_ds, batch_size=128, shuffle=False) with torch.no_grad(): for imgs, labels in val_dl: out = model(imgs) pred = out.argmax(dim=1) correct += (pred == labels).sum().item() total += labels.size(0) print(f"val acc: {correct / total:.4f}")推理阶段把分割出来的单字符缩放后走同一套归一化,取top1的类别索引,到classes.txt里查字符,按顺序拼接就是车牌字符串。缩放字符图用INTER_AREA,对笔画保持比INTER_LINEAR更稳。
5. 车牌识别的常见问题与排查:五个高频翻车点
这章内容是真实项目里最常见的五个坑,每一条都按「现象→原因→解决」写,照着排查能省下大量调试时间。
5.1 夜间蓝牌变成灰蓝色,HSV阈值直接漏检
现象:白天识别正常,到了晚上同一台相机拍到的车牌检测不出来,定位mask输出几乎全黑。原因是夜间路灯是暖黄光,蓝色区域在低照度下饱和度掉到60以下,亮度掉到60以下,HSV的下限(100, 90, 80)把目标直接滤掉了。解决方法是把S和V下限降到40,同时加一条灰度边缘检测的并联通道:原图转灰度后做Sobel边缘检测,再加形态学闭运算,两条mask取或。边缘不受色相影响,夜间至少能保证车牌框不丢。注意降低S/V下限后会引入一些蓝色车身噪声,交给后面的面积和宽高比筛选去过滤即可。
5.2 「京」「川」这类左右结构汉字被投影切成两半
现象:分割出来的字符数超过七个,输出字符串长度不对。原因是垂直投影把汉字内部的自然空隙当成了字符间隙。解决方法是切分后按宽度做合并:统计所有片段的宽度,如果一个片段的宽度明显小于平均水平(比如低于平均值的0.6倍),并且它和相邻片段的水平距离也小于阈值,就把两个片段合并。更省事的做法是切完后检查段数,不等于7就回退到按整牌宽度七等分均匀切。这两种方案用哪个取决于你的数据分布:如果倾斜校正做得好,均匀切分的稳定性其实很高,因为车牌字符位置本身是等距的。
5.3 汉字识别率比数字低一大截,错的全是省份简称
现象:整体准确率显示98%,把所有错误样本翻出来一看,几乎全是汉字错,数字和字母全对。原因有两个:省份简称只有31类,每类样本量天然比10个数字少;汉字笔画密,压到32×32之后横竖撇捺挤在一起,结构信息损失大。解决方法是给汉字类别单独做增强,把每类样本扩到和数字类一个量级;训练时在损失函数里给样本少的类别加权,用CrossEntropyLoss的weight参数即可。如果追求更好的效果,可以训练两个网络:一个专门认汉字,输入放大到48×48;一个认字母数字,输入保持32×32。推理时先用字符的宽高比判断进哪个网络,汉字普遍比字母数字结构宽。
5.4 minAreaRect角度转错方向,校正后文字倒过来
现象:定位框裁出来了,但校正后字符全部倒置,或者角度越转越歪。原因是OpenCV的minAreaRect角度定义是-90到0度,短边和长边的判定直接影响是否需要补90度。解决方法是先判断矩形的宽和高:如果rect[1][0] < rect[1][1],说明矩形竖着,要把角度减90再取反。拿到角度后getRotationMatrix2D里用-angle,因为旋转矩阵定义的是逆时针为正。一个更鲁棒的做法是拿到旋转矩形后不依赖角度,直接用cv2.getPerspectiveTransform把四个顶点映射到标准矩形,透视变换对倾斜的校正比单纯旋转更准,尤其当车牌带俯仰角时。
5.5 验证集95%,演示现场连续翻车
现象:字符级准确率验证集95%,拿到停车场录一段二十辆车的视频,错了三四辆。原因是验证集是合成数据加少量真实抠图,而设备实际画面的分辨率、角度、曝光和训练数据分布不一致。解决方法是演示前先到目标场景录几分钟视频,抽出两三百帧跑一遍,把误检样本人工标注后增量训练。这个「现场采集-误检回流」的循环跑两轮,准确率基本就能稳定住。这条不是模型问题,而是数据分布问题的典型,也是实际交付和课程作业最本质的区别。
6. 答辩与演示的加分项:指标口径、模型导出与场景化验证
毕设演示不只是让系统跑起来,更关键的是能拿出让人信服的评估数字。建议报两个指标:字符级准确率和整牌准确率。字符级准确率是预测对的字符数除以总字符数;整牌准确率是七位字符全部一致才算对。前者体现模型细节能力,后者体现系统整体可用性。两个口径都要给,因为只给字符级98%会被追问「整牌正确率多少」,只给整牌90%又看不出瓶颈在哪。演示时还要报一个FPS,用总帧数除以总处理耗时,OpenCV直接算即可。
模型导出这块,训练完的PyTorch权重最好转成TorchScript,演示机器上不用装训练环境,推理速度也更快。
model.eval() example = torch.randn(1, 1, 32, 32) traced = torch.jit.trace(model, example) traced.save("char_net.jit")加载时用torch.jit.load,和原模型调用方式一致。如果想把OpenCV贯穿全链路,也可以导出ONNX再走OpenCV DNN模块加载,答辩时统一性更好,但多一个转换步骤,TorchScript最省事。
最后一个习惯是关于场景化验证的:把演示环境固定下来,机位高度、拍摄距离、光照方向都定好,让系统工作在它的设计边界内。这不是投机取巧,而是所有视觉系统交付前都要做的事——你要知道系统在哪些条件下可靠,哪些条件下会失效,并能在答辩时诚实地说出来。我自己的习惯是先把最小闭环跑通,再回头调参数,这个项目最让进度失控的从来不是模型精度,而是数据标注和分割环节的反复返工。先跑通再调优,希望帮到你。
本文还有配套的精品资源,点击获取