ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于Python与OpenCV的答题卡自动批改系统:从图像处理到工程实践

基于Python与OpenCV的答题卡自动批改系统:从图像处理到工程实践 简介本资源是一套基于Python实现的答题卡智能阅卷系统完整工程面向计算机、电子信息、数学等专业本科生及毕设/课程设计学习者解决人工批改效率低、易出错的痛点覆盖答题卡定位检测、试题区域切分、学生考号OCR识别、选择题填涂区域判别与自动评分全流程。压缩包共35个文件含8个核心Python脚本如detection_exam_num.py、detection_choice_question.py、app.py、2个Jupyter Notebook含检测与识别实验记录、11张测试图像jpg/png格式用于效果验证、1个Dockerfile支持容器化部署以及README.md、requirements.txt等工程配置文件整体大小6.04MB。已有231人学习下载提供可直接运行的端到端源码、清晰模块划分检测→切分→识别→判分、典型场景测试图例及基础环境配置说明适合算法入门实践、视觉项目拆解学习与毕设快速原型开发。1. 项目缘起从手动批改到自动化判卷的痛点与机遇作为一名在技术一线摸爬滚打了十多年的开发者我处理过不少和教育、考试相关的项目。每次看到老师们抱着一摞摞答题卡用红笔和读卡尺一张张地批改或者用老旧的OMR光学标记识别设备吭哧吭哧地扫描我就觉得这里面有巨大的效率提升空间。传统的OMR设备昂贵、笨重对答题卡的纸张、印刷精度要求极高而且一旦学生涂卡不规范识别率就直线下降。更重要的是它只能处理选择题对于需要切分试题、识别考号这类更灵活的需求就显得力不从心了。于是基于Python和计算机视觉技术自己动手打造一套轻量、灵活、高精度的答题卡处理系统就成了一个非常实际且有价值的项目。这套系统不依赖专用硬件一台普通的扫描仪或手机摄像头加上我们写的代码就能完成从图像预处理、答题卡定位、试题区域分割、考号识别到选择题自动批改的全流程。这不仅仅是“用代码替代人力”更是将批改流程数字化、标准化为后续的数据分析比如错题统计、知识点掌握度分析打下了坚实的基础。今天要分享的正是这样一个项目的核心思路、技术实现细节以及我踩过的那些坑希望能给正在开发类似系统或者对计算机视觉应用感兴趣的朋友一些实实在在的参考。2. 系统架构总览从一张图片到一批分数的流水线在深入代码细节之前我们先从宏观上理解整个系统的处理流水线。这就像一条工厂生产线原料是拍摄或扫描得到的答题卡图片最终产品是每个学生的考号和选择题得分。这条生产线主要分为四个核心工站第一工站图像预处理与答题卡定位。这是所有后续操作的基础。原始图片可能歪斜、有阴影、亮度不均甚至包含桌面的背景。我们的任务是把它“摆正”、“弄干净”并精准地框出答题卡区域。这里的关键技术是边缘检测和透视变换。第二工站试题区域网格化切分。定位到答题卡后我们需要根据答题卡的设计比如有多少道题每道题有几个选项将答题区域划分成一个个标准的小格子Cell。每个格子对应一个待涂的选项。这一步的核心是依据答题卡模板的几何特征进行行和列的等分计算。第三工站学生考号识别。考号通常也是以涂卡方式填写的比如用2B铅笔涂黑0-9的数字。我们需要先定位考号区域然后对每一位数字的涂写状态进行识别。这可以看作是一个简化版的OCR光学字符识别但因为我们只关心“涂了”还是“没涂”并且数字区域是固定的所以算法上更侧重于二值化和轮廓分析。第四工站选择题答案判定与批改。这是价值实现的环节。我们需要判断每个试题对应的选项格子是否被涂黑。将识别出的学生答案与标准答案进行比对计算得分。这里不仅要处理涂写深浅、涂出边界等常见问题还要设计一个合理的判定阈值。整个系统的技术栈以Python为核心主要依赖OpenCV进行图像处理NumPy进行数组运算可能还会用到PILPillow进行一些图像IO操作。下面我们就沿着这条流水线一个工站一个工站地拆解。3. 工站一图像预处理与答题卡定位的实战精要拿到一张答题卡图片第一步不是直接去数格子而是把它“准备好”。这个准备过程直接决定了后续所有步骤的精度上限。3.1 灰度化与高斯模糊为边缘检测铺路彩色图像包含RGB三个通道信息冗余且计算量大。我们首先将其转换为灰度图这能简化后续处理。之后通常会施加一个高斯模糊。很多新手会问模糊不是会让图像变“糊”吗为什么还要做这里的目的是降噪。答题卡图像在拍摄时可能产生微小的噪点纸张本身的纹理也可能被误判为边缘。高斯模糊可以平滑这些高频噪声避免它们在后续的边缘检测中“喧宾夺主”干扰真正代表答题卡边框的强边缘。import cv2 import numpy as np def preprocess_image(image_path): # 读取图像 image cv2.imread(image_path) # 转换为灰度图 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 应用高斯模糊核大小(5,5)是一个常用起点可根据图像分辨率调整 blurred cv2.GaussianBlur(gray, (5, 5), 0) return blurred3.2 边缘检测与轮廓查找抓住答题卡的“骨架”接下来是Canny边缘检测它是找到物体边界的利器。Canny算法会输出一个二值图像其中白色像素代表检测到的边缘。之后我们使用cv2.findContours来寻找这些边缘构成的轮廓。在答题卡场景下我们期望找到的最大的、近似四边形的轮廓就是答题卡的外边框。def find_card_contour(blurred_image): # Canny边缘检测阈值需要根据图像对比度微调 edged cv2.Canny(blurred_image, 50, 150) # 寻找轮廓 contours, _ cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 假设答题卡是最大的轮廓 if contours: # 按面积排序取最大的 contours sorted(contours, keycv2.contourArea, reverseTrue) card_contour contours[0] # 计算轮廓周长并用多边形近似 peri cv2.arcLength(card_contour, True) approx cv2.approxPolyDP(card_contour, 0.02 * peri, True) # 如果近似后有4个顶点我们认为找到了矩形答题卡 if len(approx) 4: return approx return None注意0.02 * peri是一个经验参数表示近似的精度。值越小近似多边形越贴近原始轮廓。对于非常规整的矩形这个值可以调小如0.01对于边框稍有破损或拍摄变形的可能需要调大如0.03以避免顶点数不为4。3.3 透视变换将歪斜的视图“掰正”通过上一步我们得到了答题卡四个顶点的坐标。但这四个点可能对应的是一个倾斜拍摄的平行四边形。我们需要通过透视变换将其映射到一个标准的矩形上。OpenCV的cv2.getPerspectiveTransform和cv2.warpPerspective函数可以完美完成这个任务。关键是确定目标矩形的四个点。通常我们会将原始四个点按顺序左上、右上、右下、左下排序然后映射到一个固定宽度和高度的矩形。def four_point_transform(image, pts): # 对四个点进行排序左上、右上、右下、左下 rect order_points(pts) # order_points是一个自定义的排序函数 (tl, tr, br, bl) rect # 计算新矩形的宽度取上边和下边的最大宽度 widthA np.sqrt(((br[0] - bl[0]) ** 2) ((br[1] - bl[1]) ** 2)) widthB np.sqrt(((tr[0] - tl[0]) ** 2) ((tr[1] - tl[1]) ** 2)) maxWidth max(int(widthA), int(widthB)) # 计算新矩形的高度取左边和右边的最大高度 heightA np.sqrt(((tr[0] - br[0]) ** 2) ((tr[1] - br[1]) ** 2)) heightB np.sqrt(((tl[0] - bl[0]) ** 2) ((tl[1] - bl[1]) ** 2)) maxHeight max(int(heightA), int(heightB)) # 定义目标点 dst np.array([ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtypefloat32) # 计算变换矩阵并应用透视变换 M cv2.getPerspectiveTransform(rect, dst) warped cv2.warpPerspective(image, M, (maxWidth, maxHeight)) return warped经过这一步我们得到了一张“俯视图”状态的、正对着的答题卡图像为后续的精确网格划分扫清了障碍。这是我踩过的第一个大坑忽略透视变换直接在原始倾斜图像上划分网格会导致切分位置严重偏移识别结果完全不可用。4. 工站二试题区域网格化切分的算法逻辑拿到校正后的答题卡接下来就要像棋盘一样把它划分成行和列。这一步的前提是答题卡的设计是规则的所有选择题的选项框大小相同、间距相等。4.1 确定网格参数行数、列数与单元格尺寸首先我们需要知道答题卡上有多少道题行每道题有多少个选项列。这通常来自答题卡模板的设计规范。例如一份试卷有50道单选题每题4个选项A/B/C/D那么网格就是50行 x 4列。其次我们需要测量出每个单元格即一个选项框的宽度和高度以及单元格之间的水平间距和垂直间距。这可以通过在预处理后的图像上手动测量或通过程序检测几个代表性区域来计算平均值获得。更稳健的方法是在答题卡模板上预先设计定位标记。例如在答题区域的四个角或边缘印刷一些特殊的黑色方块类似二维码的定位图案。程序可以先检测到这些标记然后根据标记的相对位置动态计算出网格的起始点、单元格尺寸和间距。这种方式容错性更高即使印刷有微小偏差或扫描时有拉伸也能准确定位。4.2 动态计算网格坐标假设我们已经知道了网格的起始坐标(startX, startY)单元格宽度cellW单元格高度cellH水平间距padX垂直间距padY。那么第i行、第j列选项框的左上角坐标(x, y)可以计算为x startX j * (cellW padX) y startY i * (cellH padY)其右下角坐标就是(x cellW, y cellH)。在代码中我们通常会使用嵌套循环来生成所有单元格的坐标列表def get_question_cells(warped_image, rows, cols, startX, startY, cellW, cellH, padX, padY): cells [] for i in range(rows): row_cells [] for j in range(cols): # 计算每个单元格的坐标 x1 startX j * (cellW padX) y1 startY i * (cellH padY) x2 x1 cellW y2 y1 cellH # 从校正后的图像中裁剪出该单元格 cell warped_image[y1:y2, x1:x2] row_cells.append({ coords: (x1, y1, x2, y2), image: cell, question_idx: i, option_idx: j }) cells.append(row_cells) return cells4.3 处理非均匀网格与偏移校正现实中的答题卡可能更复杂。比如考号区域可能单独位于顶部与试题区域的行高不同。或者答题卡在印刷或扫描时存在微小的整体偏移。对于前者我们需要将考号区域和试题区域视为两个独立的网格分别处理。对于后者一个实用的技巧是在批改前先处理一张“空白答题卡”或“模板卡”。在这张模板卡上运行同样的网格划分算法并记录下每个单元格在“理想状态”下的坐标或图像特征。在实际批改时可以将待批改卡的网格区域与模板卡的对应区域进行特征匹配比如比较边缘或角点计算出一个偏移量Δx, Δy然后对所有网格坐标进行补偿。这能有效对抗系统性偏移。5. 工站三学生考号识别的特殊性与策略考号识别本质上是多个并排的“是否涂黑”判断但它比普通选择题更需谨慎因为考号是学生的唯一标识识别错误后果严重。5.1 考号区域定位与分割考号区域通常有明确的标题如“准考证号”和一个个的数字框。定位策略有两种基于先验知识如果答题卡模板固定可以直接在图像上根据固定坐标裁剪出考号区域。基于文本检测使用轻量级的OCR引擎如Tesseract或基于深度学习的CTPN、EAST模型先定位“准考证号”这几个字然后根据相对位置偏移找到数字框区域。这种方法更通用但复杂度也更高。定位后需要将整个考号区域按数字位数进行横向等分得到每个数字对应的子区域。每个子区域内部又需要根据0-9这10个数字的印刷位置进一步划分出10个小格子对应数字0-9。也就是说一位考号对应一个10选1的问题。5.2 单数字涂写状态判定对于一位数字我们需要判断0-9这10个格子中哪个被涂黑了。这里不能简单地对每个小格子做二值化后统计非零像素因为学生可能涂得比较浅或者有涂改痕迹。我采用的策略是对包含0-9这10个选项的整个数字子区域进行自适应阈值二值化使涂写部分与背景分离。使用cv2.findContours查找所有轮廓。对每个轮廓计算其外接矩形和面积。设定一个面积阈值过滤掉过小的噪声点。剩下的轮廓中面积最大且位置在预期数字格范围内的那个就认为是学生涂写的数字。def recognize_digit(digit_region_image): # digit_region_image 是包含0-9十个格子的图像块 # 1. 二值化 gray cv2.cvtColor(digit_region_image, cv2.COLOR_BGR2GRAY) # 使用自适应阈值对光照不均更鲁棒 thresh cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2) # 2. 查找轮廓 contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) digit None max_area 0 # 3. 遍历轮廓找到最大的有效轮廓 for cnt in contours: area cv2.contourArea(cnt) if area 50: # 面积过滤阈值需根据图像分辨率调整 continue # 可以进一步检查轮廓位置是否落在某个数字格内需要预定义数字格坐标 # 这里简化为只找最大面积 if area max_area: max_area area # 可以根据最大轮廓的中心点坐标映射到0-9的数字需要预定义映射关系 # 假设我们已经有一个函数 get_digit_from_contour(cnt, predefined_grid) digit get_digit_from_contour(cnt, predefined_digit_grid) return digit # 返回识别出的数字如 55.3 校验与容错机制考号识别必须加入校验。常见的有长度校验识别出的数字位数是否符合预期如10位。校验位如果考号本身包含校验位如最后一位是前9位的和模10可以在识别后进行验证。重复识别对同一张答题卡用不同的图像预处理参数如调整二值化阈值识别两次对比结果。如果关键位不一致则标记为需要人工复核。我踩过的坑曾经有一次一个学生把数字“7”涂得特别粗墨水洇染导致轮廓检测时它和相邻的“1”的格子连成了一片被识别成一个超大轮廓程序错误地将其中心点映射到了数字“8”上。解决方案是在轮廓分析后增加一个“轮廓分离度”检查。如果某个轮廓的面积异常大且其外接矩形覆盖了多个数字格则尝试使用形态学操作如腐蚀将其断开或直接判定为该位识别失败交由人工处理。6. 工站四选择题答案判定与批改的核心算法这是最后一步也是直接产出价值的一步。目标很简单对于第i题第j个选项的格子判断学生是否涂选了它。6.1 涂写区域提取与特征计算我们已经有每个选项格子的图像cell。直接二值化后统计白色或黑色像素的比例是最朴素的方法但容易受光照和涂写深浅影响。更稳健的方法是计算涂写区域占比和图像非零像素的统计特征。def is_bubble_filled(cell_image, methodadaptive_thresh): 判断一个选项格子是否被涂写。 返回 True已涂 或 False未涂 gray cv2.cvtColor(cell_image, cv2.COLOR_BGR2GRAY) if method adaptive_thresh: # 方法1自适应阈值 面积占比 thresh cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2) filled_pixels cv2.countNonZero(thresh) total_pixels cell_image.shape[0] * cell_image.shape[1] fill_ratio filled_pixels / total_pixels # 设定一个阈值例如0.220%的区域被涂黑则认为已选 return fill_ratio 0.2 elif method otsu_std: # 方法2Otsu阈值 像素强度标准差 # Otsu自动计算最佳全局阈值 _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 计算二值图像中非零像素的数量涂黑部分 filled_pixels cv2.countNonZero(thresh) total_pixels cell_image.shape[0] * cell_image.shape[1] fill_ratio filled_pixels / total_pixels # 额外计算原灰度图的标准差涂写的格子灰度值变化更大 std_dev np.std(gray) # 结合两个特征做判断 return (fill_ratio 0.15) and (std_dev 25) # 阈值需根据实测调整 # 可以尝试更多方法...6.2 判定阈值的选择与动态调整上面的0.2、0.15、25这些阈值是魔法数字吗是的但我们可以让它不那么“魔幻”。一个关键技巧是利用答题卡上的“定位点”或“未涂写参考区域”进行动态校准。例如答题卡上某些固定位置如角落有永远不会被涂写的黑色标记块。我们可以提取这些标记块的图像计算它们的fill_ratio得到一个“完全涂黑”的基准值F_full。同时在答题区域外找一个干净的空白区域计算其fill_ratio得到“完全空白”的基准值F_empty。那么判定阈值可以设为(F_full F_empty) / 2 * k其中k是一个略小于1的系数如0.7。这样阈值就能适应不同扫描仪、不同纸张、不同光照条件。6.3 处理多选题与涂写不规范情况我们的系统目前按单选题设计即一道题只涂一个选项。但现实中学生可能涂多个误操作或多选题也可能涂得特别轻、涂出框、或用橡皮擦得不干净。多选题检测对于一道题如果检测到超过一个选项的is_bubble_filled返回True则这道题可以标记为“多选”按错误处理或特别记录。涂写过浅/过深动态阈值法对此有一定鲁棒性。还可以引入更复杂的特征如涂写区域的连通性、形状圆形度等。如果涂写区域被识别为多个分散的小轮廓可能是涂改不干净可以尝试合并轮廓或判定为无效。涂出框外我们的格子是预先划定的涂出框的部分在裁剪时已被切除不影响框内区域的判断。这是网格化切分的一个优势。6.4 批改逻辑与结果输出遍历所有题目和选项生成一个二维数组answer_sheet其中answer_sheet[i][j]表示第i题第j个选项是否被选中。将其与标准答案数组answer_key例如[0, 2, 1, ...]表示第0题选A(0)第1题选C(2)第2题选B(1)...进行比较计算得分。def grade_answers(answer_sheet, answer_key, score_per_question2): answer_sheet: 二维列表True/False表示是否选中 answer_key: 列表每个元素是对应题目的正确选项索引0-based total_questions len(answer_key) correct_count 0 details [] for i in range(total_questions): # 找到学生在这一题中选中的选项索引 student_choices [j for j, filled in enumerate(answer_sheet[i]) if filled] if len(student_choices) 1 and student_choices[0] answer_key[i]: correct_count 1 details.append((i, True, student_choices[0])) # 题号正确所选答案 else: # 多选、未选或选错 details.append((i, False, student_choices)) # 题号错误所选答案列表 total_score correct_count * score_per_question return total_score, correct_count, details最后将考号、每道题的答题情况、总分、以及可能需要人工复核的题目索引如多选、识别置信度过低输出为结构化的数据如JSON、CSV或直接录入数据库。7. 工程化实践从脚本到系统的关键考量把上述流程写成脚本能跑通和做成一个稳定可用的系统中间还有不少距离。以下是我在项目迭代中总结的几个工程化要点。7.1 参数配置化与模板管理硬编码图像尺寸、网格参数、阈值是非常糟糕的做法。一个成熟的系统应该将这些参数外部化。我推荐使用YAML或JSON配置文件来管理答题卡模板配置模板名称、图像尺寸、定位标记坐标、试题区域起始点、行数、列数、单元格尺寸、间距、考号区域坐标、数字格布局等。处理参数配置Canny阈值、二值化方法及参数、涂写判定阈值、动态校准的k系数等。批改规则配置每题分值、是否允许多选、考号校验规则等。这样更换不同的答题卡模板时只需更换配置文件而无需修改代码。7.2 性能优化与批量处理如果一次要处理成百上千张答题卡性能就很重要。图像解码与内存使用cv2.imread时对于大图可以考虑指定cv2.IMREAD_REDUCED_COLOR_2等参数先缩小尺寸处理如果精度允许。处理完一张后及时释放内存。循环优化Python的循环较慢。在网格切分、像素统计等操作中尽量使用NumPy的向量化操作替代显式循环。例如计算所有单元格的涂写比例可以先将整个答题卡图像转换为合适的格式然后利用数组切片和np.mean,np.std等函数批量计算。并行处理使用Python的concurrent.futures模块或多进程库如multiprocessing可以轻松实现多张答题卡的并行批改充分利用多核CPU。7.3 日志、监控与异常处理系统运行时必须有详细的日志记录每张答题卡的处理状态开始、定位成功/失败、识别出的考号、批改得分、异常信息。这对于排查问题和后期统计至关重要。异常处理要健壮。比如定位答题卡轮廓失败怎么办尝试调整边缘检测参数或返回错误提示考号识别位数不对怎么办标记为异常卡留待人工处理某道题识别出的涂写区域占比处于阈值临界点怎么办标记为低置信度供人工复核一个建议是设计一个处理状态枚举贯穿整个流水线class ProcessingStatus: SUCCESS success CARD_LOC_FAILED card_localization_failed ID_RECOGNITION_FAILED id_recognition_failed LOW_CONFIDENCE low_confidence_requires_review # ... 其他状态7.4 提供一个简单的用户界面可选对于非技术背景的使用者如教师一个简单的GUI或Web界面能极大提升易用性。可以使用tkinter、PyQt做桌面应用或用Flask/Django做Web服务。核心功能包括上传单张或批量答题卡图片。选择或上传对应的答题卡模板配置文件。上传标准答案文件。启动批改并显示进度。查看批改结果列表显示考号、分数并能点击查看每张答题卡的识别详情和原图。对标记为“需复核”的卡片提供人工修正界面如修改考号、修正某题答案。8. 避坑指南与实战心得最后分享一些在开发和实际部署中积累的血泪教训这些在官方文档里可找不到。坑一光照不均与阴影。这是影响识别率的最大元凶。用手机拍摄时尤其明显。解决方案除了前面提到的自适应阈值还可以在预处理阶段加入光照归一化。比如使用cv2.createCLAHE对比度受限的自适应直方图均衡化来增强局部对比度或者尝试cv2.detailEnhance滤波器。对于严重的阴影可以尝试基于Retinex理论的算法进行校正但计算量会增大。坑二纸张褶皱与破损。答题卡被折叠过会有明显的折痕线条在边缘检测时可能被误判为答题卡边框。应对策略是在轮廓查找后对找到的四边形进行几何合理性校验。比如检查四条边的长度是否大致相当对于矩形答题卡相邻边的夹角是否接近90度。如果偏差太大则舍弃这个轮廓尝试寻找次大的轮廓或判定为定位失败。坑三铅笔痕迹太浅或橡皮擦痕。2B铅笔涂写如果力度轻灰度值可能和纸张背景很接近。单纯全局阈值可能失效。此时局部二值化方法如自适应阈值比全局阈值如Otsu更有效。也可以尝试在判定时不仅看像素占比还看涂写区域的连通组件特性。一个真正的涂写通常是一个较大的、紧凑的连通区域而擦痕或噪点则是多个细小的、分散的区域。坑四扫描仪带来的“摩尔纹”。如果答题卡上有密集的印刷网格或点阵用扫描仪扫描时可能产生干涉条纹摩尔纹严重干扰图像。建议在扫描时选择较高的DPI如300DPI以上并在软件中开启“去网纹”功能。在代码层面可以尝试在灰度化后使用一个轻微的高斯模糊或中值滤波来平滑这种周期性噪声。心得一一定要准备高质量的测试集。不要只用几张完美的图片测试。收集各种“烂卡”歪斜的、过曝的、欠曝的、有折痕的、涂写出格的、用钢笔涂的反光、用红笔涂的与红色印刷标记混淆等等。用这些“烂卡”去调参和优化算法系统的鲁棒性才能上去。心得二识别率达不到100%是正常的。目标是尽可能提高自动化率比如达到98%以上然后将剩下的2%标记出来交给人工复核。设计系统时一定要留出人工复核接口。在输出结果中清晰标明哪些卡、哪些题目是低置信度或识别失败的并提供便捷的修正工具。这比追求100%的全自动但偶尔出现灾难性错误要靠谱得多。心得三从项目开始就考虑部署。用PyInstaller或cx_Freeze将脚本打包成可执行文件方便在没有Python环境的电脑上运行。如果做成Web服务考虑使用Docker容器化部署解决环境依赖问题。配置文件、模型文件如果用了机器学习等资源文件要有清晰的组织结构。这个基于Python的答题卡自动批改项目从技术上看是计算机视觉中一些经典方法边缘检测、轮廓分析、透视变换、图像分割的巧妙组合。它不涉及特别高深的深度学习模型但对问题的拆解、对细节的处理、对异常情况的考量恰恰是工程实践中最有价值的部分。希望这份超详细的拆解能帮助你少走弯路更快地构建出属于自己的、稳定可靠的自动化批改工具。本文还有配套的精品资源点击获取
返回列表