简介:面向计算机相关专业学生与初级开发者,这是一套基于机器学习和OCR技术的车牌识别系统Java实现,包含完整源码、字符样本、项目文档与辅助资源。项目经导师指导认可,答辩评审分达到95分,代码注释详尽,新手也能较快理解核心逻辑,可直接部署用于毕业设计、课程设计、期末大作业或项目立项演示,也便于二次扩展。包内共有248个文件,以145张图片(界面截图与车牌样本)、56个HTML帮助文档、21个Java核心类为主,另含字符集压缩包、配置文件、日志及依赖jar包,整体约44.34MB。其中字符集资源与车牌识别实现类覆盖了从样本训练、图像预处理到字符识别的完整流程,HTML文档对类与方法作了梳理,读者可据此搭建环境并按模块理解代码。系统经严格调试,界面简洁、运行稳定,当前已有364人学习,尤其适合人工智能、自动化等专业学生作为结业项目或实战练手参考。
1. 车牌识别为什么不能只靠 OCR 开场
把“车牌识别”直接丢给通用 OCR,结果往往是一堆乱码。原因很简单:车牌是一种强约束目标——蓝底白字或黄底黑字、字符间距固定、宽高比约 3.14:1、材质反光、还经常倾斜。通用 OCR 擅长的是“版面里大量连续文本”,而车牌识别的核心难点不在“认字”,而在“先把那块铁皮从复杂背景里抠出来”。所以 Java 做车牌识别,正路是:图像预处理 → 车牌定位 → 字符分割 → 单个字符识别。这个过程中,机器学习负责“字符分类”这种模式匹配问题,OCR 引擎负责“把图像转成文本”的最后一跳,两者不是替代关系,而是流水线里不同工位。
这篇文章用 Java + OpenCV 把整条流水线拆开讲。你不需要有计算机视觉基础,但最好熟悉 Java 集合、类和接口的基本用法。读完你能跑通一个最小实现,也明白每个环节的参数为什么这样设——这对你后面看任何一份车牌识别源码都管用。
2. 用 OpenCV 做车牌定位:颜色阈值里藏着第一个坑
2.1 为什么要先定位而不是直接识别
通用 OCR 面对的是一整页文字,引擎能找到文字块;但车牌在自然场景里只占画面很小一块,周围是车身、道路、绿化带,直接整图 OCR 的话,引擎根本不知道哪行文字是车牌,而且车牌字符高度不够、背景杂乱,识别率会低到不可用。
定位的本质是把问题降维:把“在整图中找七个字符”变成“在整图中找一块矩形区域”,再把“在一堆内容里读文字”变成“在裁剪好的方块里读文字”。两步走的好处在于,每一步的失败都可以独立排查——定位不准不怪识别,识别不准不怪定位。
2.2 HSV 阈值和形态学操作的参数选择
车牌定位最常见的做法是利用车牌底色。中国蓝牌的主色调是蓝色,在 RGB 空间里蓝色受光照影响极大——同一个“蓝”,晴天和阴天拍出来 RGB 值差很多。所以第一步是把图像从 BGR 转到 HSV 空间,H(色相)对光照相对不敏感。
OpenCV 的 inRange 函数用法如下。注意 OpenCV 里 H 范围是 0 到 180,不是 0 到 360,很多人第一次写会踩这个坑。蓝色车牌的 H 值一般在 100 到 124 之间,S 饱和度在 43 以上(低于这个值会偏灰白),V 亮度在 46 以上(太低就是黑色)。这三个下界比你想的必要,因为阴影里的车牌 V 值会很低;而上界也别给太大,否则车身深蓝色的区域也会混进来。
形态学操作是定位的第二步。面蓝色 mask 会有很多孔洞和断裂,比如铆钉把字符区域分割开、光照不均匀导致蓝色区域断成几块。用闭运算(先膨胀再腐蚀)能把断开的区域连起来。结构元素用矩形,尺寸不是固定的:图像宽度在 800 像素左右时,17×3 的核比较合适。这个 17 是横向长度,来自车牌宽度约是字符高度的 3 倍这个先验;纵向取 3 是避免把上下两个相邻车牌糊在一起。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 蓝色 H 下界 | 100 | 蓝色色相起点,暗蓝会略低 |
| 蓝色 H 上界 | 124 | 超过这个值会混入紫色 |
| S 下界 | 43 | 低于 43 的颜色接近灰白 |
| V 下界 | 46 | 阴影下亮度会下降,别设太高 |
| 闭运算核 | 17×3 | 横向连接字符断裂,纵向避免粘连 |
2.3 候选区域筛选:宽高比不是死的
形态学处理后得到的候选区域有好几个,需要用几何特征筛掉假的。标准车牌宽高比是 440:140,约 3.14:1。但拍摄角度会导致透视变形,所以实际宽高比在 2.0 到 4.5 之间都应该保留。面积也要设个下限,比如整幅图 800×600 时,车牌区域面积一般大于 5000 像素,小于这个值的基本都是噪点。
这时候还要考虑一个真实场景问题:如果图像里同时出现蓝色车身和蓝色车牌,面积更大的车身区域会被优先选中。解决的办法是加一个“矩形度”判断——计算轮廓的面积和最小外接矩形的面积之比,车牌是规整矩形,这个比值接近 1;车身区域往往形状不规整,比值会偏低。
2.4 定位的最小可运行代码
import org.opencv.core.*; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; import java.util.ArrayList; import java.util.List; public class PlateLocator { public static Rect locatePlate(Mat src) { // 1. BGR 转 HSV:色相对光照不敏感 Mat hsv = new Mat(); Imgproc.cvtColor(src, hsv, Imgproc.COLOR_BGR2HSV); // 2. 提取蓝色区域,下界/上界按 2.2 节表格设置 Mat mask = new Mat(); Core.inRange(hsv, new Scalar(100, 43, 46), new Scalar(124, 255, 255), mask); // 3. 闭运算:膨胀一次再腐蚀一次,连接断裂的蓝色区域 Mat kernel = Imgproc.getStructuringElement( Imgproc.MORPH_RECT, new Size(17, 3)); Mat closed = new Mat(); Imgproc.morphologyEx(mask, closed, Imgproc.MORPH_CLOSE, kernel); // 4. 提取轮廓,只取外轮廓 List<MatOfPoint> contours = new ArrayList<>(); Mat hierarchy = new Mat(); Imgproc.findContours(closed, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE); // 5. 按宽高比+面积筛选 Rect best = null; double bestArea = 0; for (MatOfPoint contour : contours) { Rect rect = Imgproc.boundingRect(contour); double ratio = rect.width / (double) rect.height; double area = rect.area(); if (ratio > 2.0 && ratio < 4.5 && area > 5000 && area > bestArea) { bestArea = area; best = rect; } } return best; // 可能为 null,调用方要做空判断 } public static void main(String[] args) { System.loadLibrary(Core.NATIVE_LIBRARY_NAME); Mat src = Imgcodecs.imread("car.jpg"); Rect plate = locatePlate(src); if (plate != null) { Mat plateImg = new Mat(src, plate); Imgcodecs.imwrite("plate.jpg", plateImg); } } }这段代码里的 findContours 参数值得解释一下:RETR_EXTERNAL 表示只提取最外层轮廓,因为车牌内字符在 mask 里是白色区域里的黑色洞,如果取 RETR_LIST 会把字符轮廓也带进来,增加无意义的候选;CHAIN_APPROX_SIMPLE 压缩轮廓点,只保留端点,减少内存开销。
筛选条件里把面积下限定在 5000,这里假设输入图像宽度在 800 像素左右。如果你用的是高清图,面积阈值要按图像尺寸等比放大,否则小噪点会全部混进候选。代码里没有做“矩形度”过滤,这个可以自己加:用 Imgproc.minAreaRect 得到旋转矩形,比较它和 boundingRect 的面积差。
3. 字符分割:投影法、连通域与粘连字符的处理
3.1 分割前必做的两件小事
定位拿到的是彩色车牌图,分割字符前必须先做两件事:灰度化和二值化。灰度化好理解;二值化这一步,很多人直接用固定阈值 127,然后发现有的图字符全黑、有的图字符全白——因为光照不恒定。正确做法是用 Otsu 自适应阈值,它会根据图像灰度分布自动算出一个分割阈值,在字符和背景对比度不是极端低的情况下都适用。
二值化的 polarity 也有讲究。车牌的字符是白色,背景是蓝色,转灰度后字符比背景亮,二值化时应该让白色字符变成 255。但如果直接 THRESH_BINARY,铆钉和边框也会变成白色,后面投影时会干扰分割。我一般先用 THRESH_BINARY_INV 让字符变白色、背景变黑色,这样投影统计的是“字符像素”,更直观。
去边框也是分割前必须做的。车牌上下边框会在水平投影上形成一条连续白色带,如果不先去掉,投影曲线的波谷会被抬高,影响字符边界的判断。做法是统计每一行的白色像素数,从上下两端找到第一个像素数明显下降的行,直接裁剪掉。
3.2 垂直投影的切分逻辑与阈值
垂直投影的原理:把二值图按列统计白色像素个数,得到一个一维数组。字符区域对应的列会有较高的像素值,字符间的缝隙对应的列接近 0。从左到右扫描这个数组,遇到“从 0 变正”记一个开始点,遇到“从正变 0”记一个结束点,这一对坐标就是一个字符的左右边界。
听起来简单,但实际有三个坑:
- 噪声导致投影曲线出现细小毛刺。解决办法是设一个最小宽度阈值,比如小于 8 像素的区间直接丢弃。
- 第二个坑:汉字字符内部有断笔。比如“京”字的左右结构在投影上中间可能有一个低谷,如果阈值设得太敏感,会把一个汉字切成两半。这种情况不靠投影解决,要么结合连通域分析,要么用字符宽度的先验——车牌字符宽约 45 像素(归一化后),如果某个切分区间宽度远小于这个值,就检查它左右邻居,决定是否合并。
- 第三个坑:字符 1 和数字 1 宽度极小,容易被宽度阈值滤掉。处理的方法是保留所有不小于 4 像素的区间,最后根据“第一位数是汉字、第二位是字母、后五位是字母数字混合”的规则来纠正分割结果。
3.3 处理汉字粘连与边框残留
汉字是车牌分割里最头疼的。像“川”这种左右结构,垂直投影会在中间出现明显的低谷;像“鲁”这种上下结构,水平投影也有类似问题。连通域分析对这类问题有效——它不是按列切,而是找白色像素的连通块,每个连通块独立成一个字符。缺点是计算量比投影大,并且字符笔画如果受到噪声干扰,可能一个字符裂成好几个连通域。
更实用的方案:先投影粗切,再用每个字符块的宽度一致性来修正。标准车牌的七个字符里,第一个汉字宽一点,第二个字母窄一点,后面五个字符等宽。如果某个切分块的宽度超过平均宽度的 1.5 倍,说明它包含了两个字符,需要在该块内部再做一次更细的投影切分。
3.4 字符分割的 Java 实现
public static List<Mat> segmentCharacters(Mat plateImg) { // 1. 灰度 + 反转二值化(Otsu 自动阈值) Mat gray = new Mat(); Imgproc.cvtColor(plateImg, gray, Imgproc.COLOR_BGR2GRAY); Mat binary = new Mat(); Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU); // 2. 去掉上下边框:统计每行白点数,砍掉首尾白色行 Mat cropped = removeTopBottomBorder(binary); // 3. 垂直投影:统计每列白点数 int cols = cropped.cols(); int[] colCount = new int[cols]; for (int x = 0; x < cols; x++) { for (int y = 0; y < cropped.rows(); y++) { if (cropped.get(y, x)[0] > 0) { colCount[x]++; } } } // 4. 扫描波谷,切出字符区间 List<int[]> ranges = new ArrayList<>(); boolean inChar = false; int start = 0; for (int x = 0; x < cols; x++) { if (colCount[x] > 0 && !inChar) { inChar = true; start = x; } else if (colCount[x] == 0 && inChar) { inChar = false; if (x - start >= 4) { // 最小字符宽度 ranges.add(new int[]{start, x}); } } } // 如果扫描到结尾还在字符内,补一个区间 if (inChar) { ranges.add(new int[]{start, cols - 1}); } // 5. 按区间截取字符子图 List<Mat> chars = new ArrayList<>(); for (int[] range : ranges) { int w = range[1] - range[0]; Rect rect = new Rect(range[0], 0, w, cropped.rows()); chars.add(new Mat(cropped, rect)); } return chars; }这段代码里的 minWidth = 4 是个经验值,对应 800 像素宽图像上的字符 1 的宽度。如果图像经过缩放,这个值也要按比例调整。比较稳妥的做法是先统计所有区间的宽度,取中位数,然后把小于中位数一半的区间丢弃,而不是用固定值。
分割结果的质量直接决定识别上限。如果分割偏左或偏右,后面识别阶段无论用什么模型都救不回来。调试的时候建议把每个切出来的字符子图保存成单独文件,肉眼检查一遍,而不是直接跳到识别看结果——这样你能区分是“切歪了”还是“认错了”。
4. 分类器训练与 Tesseract 集成:识别阶段的双保险
4.1 两条技术路线的适用边界
字符识别阶段有两条路线:一是用 Tesseract 这类现成 OCR 引擎,二是自己训练一个机器学习分类器。两条路线各有不可替代的位置,但很多教程把它们当成二选一,这是误区。
Tesseract 是开源 OCR 引擎,用 Java 调用它最直接的方式是 Tess4J 这个封装库。但它对车牌这种短文本的识别率不算高——因为车牌字符字体特殊,而且有汉字、字母、数字混排,Tesseract 的语言模型是为自然语言设计的,它会对字符序列做语言概率校正,而车牌序列(比如“京A·88888”)根本不符合自然语言规律。
自训分类器的思路完全不同:先把分割好的单个字符归一化成固定尺寸,提取特征,用 SVM 或 KNN 训练一个多分类器,让它输出每个字符的类别。这条路线的优势是完全针对车牌数据定制,识别率可以做到 98% 以上;缺点是坑比较深——如果你把“江苏”的“苏”字样本量搞不足,模型就会把“苏”认成别的。所以业界常见做法是把两条线叠起来:分类器做第一轮的字符识别,Tesseract 做置信度较低字符的补充验证。
4.2 Tesseract 的 Java 配置:白名单与 psm 模式
如果只用 Tesseract,最关键的配置不是语言包,而是两个参数:
- 白名单 tessedit_char_whitelist。车牌字符不包含字母 I 和 O(避免跟 1 和 0 混淆),所以白名单应该设置成“0123456789ABCDEFGHJKLMNPQRSTUVWXYZ”加上“京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领”这些省份简称。
- psm 设置。psm 7 表示把图片当作单行文本处理,psm 8 表示当作单个单词,这两种都比默认的 psm 3(自动版面分析)更适合车牌。
import net.sourceforge.tess4j.Tesseract; public class PlateOcr { public static String recognize(Mat plateImg) { Tesseract tesseract = new Tesseract(); tesseract.setDatapath("/usr/share/tesseract-ocr/4.00/tessdata"); tesseract.setLanguage("chi_sim+eng"); // 车牌专用白名单:去掉 I、O,防止和 1、0 混淆 tesseract.setTessVariable("tessedit_char_whitelist", "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领"); // 7 = 单行文本,8 = 单个单词,都适合车牌 tesseract.setPageSegMode(7); try { return tesseract.doOCR(plateImg); } catch (Exception e) { return ""; } } }这段代码里 setDatapath 指向 tessdata 目录,具体路径取决于你的 Tesseract 安装方式。Windows 上通常在 Program Files 的 Tesseract-OCR 目录下,Linux 上通常在 /usr/share/tesseract-ocr/ 下。如果跑起来报错找不到语言包,先确认 chi_sim.traineddata 文件存在于该目录中,下载后直接丢进去即可。
实际使用中,Tesseract 对分割干净的字符图识别率还行,但如果你把整个车牌图直接丢给它,它经常会把“A”和“4”、“D”和“0”搞混。我的习惯是:Tesseract 只作为后验校验器,也就是说,分类器给每个字符一个置信度分数,如果某个字符置信度低于阈值,再单独把它交给 Tesseract 做二次确认。
4.3 自训 SVM 分类器:特征选择与样本准备
字符识别本质上是一个图像分类问题,70 个类别左右(31 个汉字 + 24 个字母 + 10 个数字)。机器学习里这类问题最简单的解法是 SVM。SVM 在小样本、高维特征下表现好,在车牌字符这种“每个类别样本可能只有几十张”的场景下,比深度学习更容易训练、更容易收敛。
先讲特征。直接拿像素值当特征维度太高,一般用 HOG(方向梯度直方图)。HOG 的核心思想是统计图像局部区域的梯度方向分布,对光照变化不敏感,特别适合字符这种边缘明显的目标。OpenCV 的 HOGDescriptor 在 Java 里可以直接用,不需要自己实现。
import org.opencv.ml.SVM; import org.opencv.ml.TrainData; import org.opencv.core.*; import org.opencv.imgproc.Imgproc; public class CharClassifier { private SVM svm; public void train(List<Mat> samples, Mat labels) { // 把样本图统一缩放到 20x20 Mat trainData = new Mat(samples.size(), 20 * 20, CvType.CV_32FC1); for (int i = 0; i < samples.size(); i++) { Mat resized = new Mat(); Imgproc.resize(samples.get(i), resized, new Size(20, 20)); Mat gray = new Mat(); Imgproc.cvtColor(resized, gray, Imgproc.COLOR_BGR2GRAY); // 逐像素拉成一行特征向量 for (int r = 0; r < 20; r++) { for (int c = 0; c < 20; c++) { trainData.put(i, r * 20 + c, gray.get(r, c)[0] / 255.0); } } } trainData.convertTo(trainData, CvType.CV_32FC1); // 创建 SVM:C_SVC + 线性核,适合高维稀疏特征 svm = SVM.create(); svm.setType(SVM.C_SVC); svm.setKernel(SVM.LINEAR); svm.setC(1.0); svm.train(TrainData.create(trainData, ml.ROW_SAMPLE, labels)); svm.save("plate_svm.xml"); } public int predict(Mat charImg) { Mat resized = new Mat(); Imgproc.resize(charImg, resized, new Size(20, 20)); Mat gray = new Mat(); Imgproc.cvtColor(resized, gray, Imgproc.COLOR_BGR2GRAY); Mat row = new Mat(1, 400, CvType.CV_32FC1); for (int r = 0; r < 20; r++) { for (int c = 0; c < 20; c++) { row.put(0, r * 20 + c, gray.get(r, c)[0] / 255.0); } } return (int) svm.predict(row); } }上面的代码用的是原始像素特征,维度 400,SVM 线性核就能跑。C 值是惩罚系数:C 越大,模型越倾向把所有样本正确分类,但也可能过拟合。一般从 0.1 到 10 之间调,我用 1.0 起步。
这个代码刻意没引入 HOG,因为 HOG 计算稍复杂;对于 20×20 的小图,像素特征的效果和 HOG 差距不大。如果你追求更高的精度,可以把特征换成 HOGDescriptor 的 compute 输出,特征维度会升到几百维,但线性核 SVM 处理这些维度完全没问题。
4.4 训练样本从哪里来
训练样本的收集是这类项目里最耗时的一环。常见做法是:先从网上拿一些公开车牌数据集做初版,然后把自己实际拍摄的图片通过定位+分割流程切出字符,人工标注好类别,加入训练集,再重新训练模型。这个过程需要迭代两三轮,每轮加几百个新样本。
样本增强对车牌识别格外有效。同一个“京”字,不同车牌上的字体粗细、倾斜角度、噪声程度都不一样。最简单的增强操作包括:水平平移 1~2 像素、旋转 ±5 度、缩放 0.9~1.1 倍、加高斯噪声。增强的目的不是制造假数据,而是让模型对真实拍摄的细微差异更鲁棒。
| 类别 | 样本量(每个字符) | 增强方式 | 说明 |
|---|---|---|---|
| 汉字 | 500+ | 平移/旋转/缩放 | 字体笔画复杂,样本不足容易漏识别 |
| 字母 | 800+ | 平移/旋转/噪声 | 注意排除车牌不用的 I、O |
| 数字 | 800+ | 平移/旋转/噪声 | 0、1 与其他字符易混淆,多加正样本 |
训练完成后,svm.save 会把模型保存为 XML 文件。这个文件在部署时直接放到项目 resources 目录里,预测时 SVM.load 加载,不需要重新训练。注意 OpenCV Java 里 SVM.load 的路径要写对,而且 load 之后要确认 svm.isTrained() 为 true,否则 predict 会抛异常。
4.5 把整条流水线拼起来
public static String recognizePlate(String imgPath) { Mat src = Imgcodecs.imread(imgPath); // 1. 定位 Rect plateRect = PlateLocator.locatePlate(src); if (plateRect == null) return ""; Mat plateImg = new Mat(src, plateRect); // 2. 分割 List<Mat> chars = PlateSegmenter.segmentCharacters(plateImg); if (chars.size() < 7) return ""; // 3. 识别 CharClassifier classifier = new CharClassifier(); classifier.load("plate_svm.xml"); StringBuilder sb = new StringBuilder(); for (Mat c : chars) { int label = classifier.predict(c); sb.append(mapLabelToChar(label)); } return sb.toString(); }这段流程代码是最简版,实际项目里每个环节都要加防御性判断:定位区域如果与图像边界重叠,裁剪时要 clamp;分割出的字符数量如果不是 7 个,要考虑是否重新调整定位框;预测结果如果包含非法字符(比如 I、O),要做后处理替换。正则表达式对结果做一次清洗也行,车牌格式是“1 个汉字 + 1 个字母 + 5 个字母数字”,不匹配就丢给异常处理分支。
5. 文档与源码里的隐藏信息:六张表先看哪一张
一份完整的车牌识别源码包,通常包含定位、分割、识别、工具类、模型文件、训练脚本和文档。拿到源码后别急着编译,先找六个关键点,能帮你省出至少半天排查时间。
第一是项目依赖清单。Java 车牌识别项目基本绕不开 opencv、tess4j、javacv 这几个库,依赖冲突是报错的头号来源。先确认 pom.xml 或 build.gradle 里的 OpenCV 版本和你本机安装的版本一致,否则运行时报一堆 UnsatisfiedLinkError。常见报错“no opencv_java412 in java.library.path”就是版本不匹配导致的。
第二是模型文件的位置和格式。源码包里通常会附带一个 xml 或 dat 后缀的 SVM 模型文件,不要放在 resources 根目录就不管了——里面对应的特征是什么?如果模型是 20×20 像素特征训练的,而你的预处理代码把字符缩放到 32×32,预测精度会直接崩掉。看模型前先确认训练代码里的预处理参数。
第三是 Tesseract 语言包路径。Tess4J 的 setDatapath 指向一个绝对路径,Windows 和 Linux 上这个路径不一样,源码里写死的路径在别的机器上一定会错。文档里如果写了“请修改为本地路径”,说明作者在这上面摔过跟头。
第四是图像尺寸假设。前面说的面积阈值、宽度阈值,全部依赖输入图像的原始尺寸。如果源码里检测的是 1280×720 的图,而你拿 1920×1080 的图直接跑,定位面积阈值要等比放大 2 倍左右。文档里如果提供了“建议输入图片尺寸”,一定按它来。
第五是车牌颜色假定。前文整套方案只针对蓝底白字。如果源码里还带了黄牌、绿牌的处理逻辑,说明它支持新能源和大型车;如果只有蓝色阈值,那它天然不支持别的车牌类型。这是一个取舍,不要指望一个阈值通吃所有颜色。
第六是识别结果的后处理规则。车牌字符有强规则:第一位必须是汉字(省份简称),第二位必须是字母,后五位必须是字母和数字,且不能出现 I、O。源码里有没有实现这个规则校验,直接决定它的出错率。没有的话,自己加一个正则:^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{5}$。最后那个 {5} 表示五个字母数字混排,整个表达式能挡住绝大多数低级识别错误。
调试时先跑通定位和分割两环,把中间结果落盘成图片检查,确认无误后再做识别。这一步别图省事——你一眼能看出的“字符切歪了”,模型永远认不出来。
本文还有配套的精品资源,点击获取