☰
答题卡识别并打分:OpenCV图像处理完整项目实战解析
2026/10/21 14:11:56 网站建设 项目流程

第一次见有人把“答题卡识别并打分”做成一个完整的OpenCV项目,是在某高校的模拟项目X里。当时那位开发者给我看了他的运行结果:一张拍歪了的答题卡照片丢进去,程序自动把答题区域拉正、定位每一道题的填涂区域、比对学生答案和标准答案,最后在控制台里打印出分数,还把错题用红圈标出来。整个过程不到1秒。我当时就觉得,这个项目太适合作为计算机视觉的入门练手了——它把图像预处理、轮廓检测、透视变换、阈值分割这些基本功全部串了起来,而且最终效果非常直观,做出来特别有成就感。

这篇文章我就以这个“答题卡识别并打分”项目为主线,完整拆解一遍它的设计思路、核心原理、实操步骤,以及我在本地复现和调试过程中踩过的坑。内容主要围绕OpenCV(图像处理库)展开,使用的是Python环境。不管你是刚学完图像处理基础想找项目练手,还是正在准备相关课程设计、面试作品,这篇文章都能给你一条可以直接照着走的路线。

1. 先把需求拆明白:所谓“识别”到底在识别什么

动手写代码之前,我习惯先做一件事:把自然语言描述的需求翻译成图像处理的步骤。这个项目标题是“答题卡识别并打分”,听起来是个整体,但拆开之后其实是四个独立的小任务。

第一个任务是“定位答题卡位置”,也就是在一张包含背景的图片里找到答题卡的区域。第二个任务是“矫正视角”,因为拍摄或扫描时答题卡往往不是完全正对着镜头的,会有倾斜甚至透视畸变,需要把它变换成规整的矩形。第三个任务是“读取填涂结果”,对每道题的每个选项区域,判断学生到底涂了哪个(或哪几个)选项。第四个任务是“打分”,拿识别结果跟标准答案对比,算出总分。

这四个任务对应到OpenCV里,分别就是轮廓检测、透视变换、阈值分割与填充率计算、以及普通的字符串或数组比对。你看,这就把看似复杂的项目拆成了几个可以用现成函数组合去实现的模块。后面所有代码,本质上都是围绕这条主线展开的。

这里也顺便回答一个我经常被问到的问题:为什么这个项目选OpenCV而不是OCR(光学字符识别)或者深度学习?原因有三点。第一,答题卡上的内容是规则的圆圈或矩形填涂区,不是自由书写的文字,OCR在这里属于杀鸡用牛刀。第二,传统图像处理方案完全够用,而且每一步做什么、为什么这么参数都能讲清楚,非常适合学习。第三,深度学习方案需要准备训练数据、调模型,部署起来也更重,对于一个课程设计或练手项目来说性价比较低。

1.1 运行环境与版本选择

我复现这个项目时用的环境是Python 3.9(版本版本其实影响不大,3.7以上都行),OpenCV版本是4.5.5,配合NumPy 1.23。安装只需要一条命令即可完成,建议创建一个新的虚拟环境,避免跟其他项目互相影响依赖。如果你还没装OpenCV,最稳妥的方式是先安装国内镜像源,再装opencv-python,这样下载速度会快很多。

另外我要特别提醒一点:OpenCV有两个常用的发行包,一个叫opencv-python,另一个叫opencv-contrib-python。本项目用到的函数都集中在基础模块里,所以装opencv-python就够了,不需要额外装contrib版,后者打包了更多扩展模块,体积更大且容易跟基础版混淆。

1.2 输入图像应该长什么样

这个项目的输入可以有两种形态:一种是扫描仪导出的整洁图片,另一种是手机随手拍摄的照片。实际做的时候,后者更有挑战性,也更能体现图像处理的价值,所以我建议你直接以拍照图为目标。

拍照图通常有几个特点:背景比较复杂(桌子纹路、手指、其他纸张)、答题卡有倾斜、光照不均匀、填涂笔迹深浅不一。这些问题恰好都是图像预处理要解决的。我在模拟项目X里测试时,特意用手机在同一张答题卡的不同角度、不同光线下各拍了几张,作为测试集,这样能更全面地检验算法的鲁棒性。

2. 图像预处理:灰度化、去噪、边缘检测

预处理是整个项目的地基,地基没打好,后面透视变换和填涂识别都会出问题。这里我按实际处理顺序,把几个关键环节的原理和参数选择一并说清楚。

2.1 灰度化与高斯模糊

OpenCV读进来的图片默认是BGR三通道的彩色图。彩色图当然也能直接处理,但轮廓检测和阈值分割通常只需要亮度信息,用灰度图可以减少计算量,也避免颜色噪声干扰。所以第一步是转灰度图。

转完之后紧接着做一次高斯模糊。这一步很容易被忽略,但它在后续边缘检测中非常重要。拍照产生的传感器噪点、纸面的纹理颗粒,如果不去掉,用Canny做边缘检测时会算出大量细小而杂乱的边缘,干扰真正的答题卡轮廓。高斯模糊的核大小我习惯用5x5,也就是传入(5, 5)。核太小去噪效果弱,核太大又会让答题卡边缘变得模糊,定位就不准了。5x5算是实用性和效果的平衡点。

2.2 Canny边缘检测与最大轮廓筛选

做完模糊,就可以用Canny算子找边缘了。Canny需要两个阈值:一个是高阈值,一个是低阈值。高阈值用于确定哪些是强边缘,低阈值用于连接边缘的连续性。我实测下来,这对参数用100和200(即threshold1=100, threshold2=200)在大多数拍照条件下效果都不错。如果光线比较暗或者对比度低,可以适当调低到80和160,具体可以事后根据调试情况微调。

拿到边缘图之后,调用findContours查找轮廓。这里注意,新版OpenCV的findContours返回两个值——轮廓列表和层级关系,并且强烈建议传cv2.RETR_EXTERNAL配合cv2.CHAIN_APPROX_SIMPLE。RETR_EXTERNAL表示只检测最外层轮廓,这样不会把答题卡里面的选项圆圈、题号等内部轮廓都带出来,方便我们直接定位答题卡这个最大的外框。

筛选最大轮廓的逻辑也很简单:在轮廓列表里遍历,用contourArea算面积,取面积最大的那一个,它就是答题卡的边界。这个过程有一个隐含前提——答题卡是画面中面积最大的物体。在模拟项目X的测试图片里这个假设是成立的,但如果你的拍摄场景里有一张比答题卡更大的白纸、或者桌面反射严重,这个前提就会被破坏。解决办法后面在常见问题章节会说。

2.3 为什么透视变换在答题卡识别里是不可或缺的

这一步是这个项目最具“视觉感”的部分,也是理解图像处理价值的关键一环。

手机拍照时,镜头平面和答题卡平面通常不平行,导致答题卡在画面里呈现一个不规则的四边形。如果直接在这种图上定位选项,每个圆圈都是斜的、椭圆变形的,位置偏移会很大。透视变换的作用,就是把这个不规则的四边形映射回一个规整的矩形,效果等同于把答题卡“正面摆正”了。

OpenCV里做这件事需要两个输入:源图像的四个顶点坐标和目标的四个顶点坐标。源顶点就是我们刚才找出的最大轮廓的四个角点,可以用approxPolyDP把轮廓近似成一个四边形,再按某种顺序(左上、右上、右下、左下)依次取出。目标顶点就是我们要映射到的矩形,比如一张宽度为700、高度为950的空白画布的四角坐标。然后用getPerspectiveTransform计算变换矩阵,再用warpPerspective应用变换即可。

这里有一个细节容易错:轮廓的四个角点顺序不一定是我们期望的左上、右上、右下、左下。直接拿原始顺序去算变换矩阵,出来的结果可能是旋转倒置的。我通常在拿到四角点后,先算它们的坐标和(x+y)以及坐标差(x-y),再按几何位置排序。具体来说:左上角的x+y最小,右下角的x+y最大,右上角的x-y最小,左下角的x-y最大。这样排序后再作为源点传入,透视结果就不会乱。

3. 填涂区域定位与选项识别:从像素到答案

完成了透视变换,我们得到一张正视角的答题卡。接下来要回答的问题是:每一道题的A、B、C、D选项分别在图像的哪个位置?学生涂了哪一个?

3.1 行与列的遍历策略

常规答题卡有固定布局:每行是同一题的四个(或更多)选项,行与行之间间距相等,列之间也基本对齐。定位这些选项最直接的方式,是在透视矫正后的图像上按行列坐标去切割子区域。

我用的方法是:先把图像二值化,再根据投影分布来自动估算每个选项圆圈的坐标。所谓投影,就是把图像沿水平或垂直方向求和,得到每个像素列(或行)上非零像素的数量。在答题卡上,由于选项外圈有黑色圆形边框,这些边框会在投影图上形成一个个明显的波峰。找到这些波峰的间隔,就能推断出每个选项间的行距和列距。

当然,这属于“自适应定位”的范畴,写起来稍微复杂。作为一个更简洁的方案,你完全可以在透视矫正后手动指定几个关键参数——选项区域左上角坐标、每行高度、每列宽度、行列数——然后用两层循环把它们遍历出来。这种做法虽然少了些自动化,但逻辑极其透明,也更容易调试和理解。我建议初学阶段先用这种参数化的方式,把识别主流程跑通,再考虑用投影法做自适应。

3.2 填涂检测的原理:阈值分割与填充率

拿到单个选项的小图像后,怎么判断它是否被填涂?最常见的特征就是“内部填充区域的面积占比”。

具体做法是:把该选项区域转成灰度,再用一个适当阈值做二值化,让填涂的黑色笔迹变成0(黑),空白背景变成255(白)。接着统计这幅小图中黑色像素占总像素的比例。如果占比超过某个阈值,比如15%,就认为这个选项被填涂了。反之就认定空白。

这里有两个参数需要调:二值化的阈值和判定填充率。二值化阈值我建议直接用OTSU(大津法),也就是在threshold函数里传cv2.THRESH_BINARY_INV参数。OTSU会自动根据像素分布算出最优分割阈值,省去手动调数。填充率阈值根据铅笔和圆珠笔的差异会有不同:铅笔涂得比较均匀,8%都能判断;圆珠笔笔迹浅、有笔痕空隙,我一般取12%到15%。这个阈值太严会漏判,太松会把橡皮擦不干净的痕迹误判成填涂,需要在自己采集的图片上多试几次才能确定。

3.3 评分逻辑与结果可视化

识别出每道题学生选了什么之后,就进入打分模块了。这里我建议把标准答案存储成一个Python列表或字典,例如["A", "B", "C", "D", ...],然后拿识别结果逐题比对。

打分本身没有技术难度,但有几个细节值得注意。一是多选情况:如果识别结果中某题同时有多个选项被判定为填涂,这道题应该被视为多选错误,而不是取其中某个选项。二是不选情况:如果某题没有任何选项被判定填涂,要单独提示“未作答”,不能简单归为错误。这些边界情况处理好了,评分逻辑才算完整。

结果可视化方面,我喜欢在透视矫正后的答题卡原图上,用绿色的圆圈标出正确答案对应的选项区域,用红色的圆圈标出学生填涂错误或者漏选的区域,最后在图的右下角把得分打上去。这样一眼就能看出学生错在哪,也方便后续人工复核。

4. 核心代码实现:以一个模拟答题卡项目为例

前面讲了原理,这一节我以“模拟项目X”为例,给出一个真正能跑通整个流程的代码骨架。这个项目的答题卡是我们自己设计的——只有20道单选选择题,每道题有A、B、C、D四个选项,填涂区域是直径约15像素的圆形。代码分三个部分:预处理与透视矫正、选项定位与填涂识别、评分与可视化。每部分我都加上必要的注释,并说明参数的含义。

4.1 预处理与透视矫正代码

import cv2 import numpy as np def order_points(pts): # pts: 四个角点的坐标,顺序可能是乱的 # 按顺序返回 [左上, 右上, 右下, 左下] rect = np.zeros((4, 2), dtype="float32") s = pts.sum(axis=1) diff = np.diff(pts, axis=1) rect[0] = pts[np.argmin(s)] # 左上: x+y 最小 rect[2] = pts[np.argmax(s)] # 右下: x+y 最大 rect[1] = pts[np.argmin(diff)] # 右上: x-y 最小 rect[3] = pts[np.argmax(diff)] # 左下: x-y 最大 return rect def perspective_transform(image, pts): rect = order_points(pts) (tl, tr, br, bl) = rect # 计算目标矩形的宽高,取最大值保证不变形 widthA = np.linalg.norm(br - bl) widthB = np.linalg.norm(tr - tl) maxWidth = max(int(widthA), int(widthB)) heightA = np.linalg.norm(tr - br) heightB = np.linalg.norm(tl - bl) maxHeight = max(int(heightA), int(heightB)) dst = np.array([ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype="float32") M = cv2.getPerspectiveTransform(rect, dst) warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) return warped # 主流程第一步 image = cv2.imread("answer_sheet.jpg") # 读入原始照片 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 灰度化 gray = cv2.GaussianBlur(gray, (5, 5), 0) # 高斯去噪 edged = cv2.Canny(gray, 100, 200) # 边缘检测 contours, _ = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 取面积最大的轮廓作为答题卡 screenCnt = max(contours, key=cv2.contourArea) peri = cv2.arcLength(screenCnt, True) approx = cv2.approxPolyDP(screenCnt, 0.02 * peri, True) # 如果近似到4个点,说明找到了答题卡四边形 if len(approx) == 4: warped = perspective_transform(image, approx)

这段代码里,approxPolyDP的第二个参数是最大距离阈值,我取轮廓周长的2%。这个比例对大多数答题卡轮廓都适用,如果你拍的答题卡特别大或边缘特别弯曲,可以改成1%或3%试试。

4.2 选项填涂识别代码

透视矫正之后,假设我们设计的模板每道题有4个选项,且每个选项的小图在横向、纵向上排列规则。这里我直接用行列循环遍历选项区域,并设置好行列数目,以及每个选项区域在矫正后图像中的大致坐标范围。

def detect_filled(image, threshold_ratio=0.15): # 输入: 已经透视矫正的单选项小图 (可以是彩色或灰度) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # OTSU二值化: 填涂笔迹 -> 0, 背景 -> 255 _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) total_pixels = thresh.size # 统计非零像素个数, 也就是被涂黑的像素数 filled_pixels = cv2.countNonZero(thresh) ratio = filled_pixels / total_pixels return ratio > threshold_ratio, ratio # 遍历参数以示例给出, 实际数值需要根据矫正后的图片调整 question_num = 20 choices = 4 # startX, startY 为第一题第一选项的区域左上角 # w, h 为每个选项小图的宽高 # offsetY, offsetX 为行间距、列间距 answers = [] for q in range(question_num): row_filled = [] for c in range(choices): x = startX + c * offsetX y = startY + q * offsetY cell = warped[y:y+h, x:x+w] filled, ratio = detect_filled(cell) row_filled.append((filled, ratio)) # 取填涂比例最高的那个选项作为本题答案 best_index = max(range(choices), key=lambda i: row_filled[i][1]) answers.append(chr(ord("A") + best_index))

这里有个逻辑需要解释一下:每道题内部,我并不是只按照填充率是否超过阈值来决定答案,而是先计算每个选项的填充比例,然后取比例最高的那一个作为最终答案。这样做的好处是,即便某道题有两个选项都超过了15%的阈值(比如橡皮擦没擦干净),我们依然能按最高比例选出一个主答案,然后再用额外的多选判断逻辑来检测异常情况。这个综合判断策略在实测中比单一阈值判断要稳定很多。

4.3 评分与可视化代码

识别完成后,把学生答案与标准答案做逐题比对,统计分数,并把结果画在矫正后的答题卡上。

standard_answers = ["A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C", "D"] score = 0 for i, std in enumerate(standard_answers): if answers[i] == std: score += 5 color = (0, 255, 0) label = f"Q{i+1}: {answers[i]} OK" else: color = (0, 0, 255) label = f"Q{i+1}: {answers[i]} vs {std}" # 在答题卡对应位置画圆圈或标注文字 cv2.putText(warped, label, (10, 20 + i * 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) total_score = score cv2.putText(warped, f"Score: {total_score}", (10, 40 + question_num * 20), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2) cv2.imshow("Result", warped) cv2.waitKey(0)

这段代码把每道题的判题结果显示在图像上。画文字时要注意一下Y坐标的递增,别让文字溢出图片范围。另外总分的字体大小我取0.8,比题号标注大一号,方便肉眼快速读取。

4.4 参数调优对照表

我把这个项目里最关键的几个参数和它们对结果的影响整理成了下面这张表,你在跑通流程之后可以对照着调。

参数常见取值对结果的影响我的调参经验
高斯模糊核大小(5, 5)越大越平滑,边缘越模糊超过(7,7)后答题卡边缘会定位不准
Canny低阈值100越小保留越多边缘拍照暗的时候降到80
Canny高阈值200越大保留越少边缘对比度强的时候可升到220
轮廓近似比例0.02 * 周长越小四边形拟合越严格边缘有阴影时要放宽到0.03
OTSU二值化自动自适应不需要手动调
填充率阈值0.15越小越易判已涂铅笔可降到0.08,圆珠笔提到0.18
透视目标宽度700~1000越大看得越清但耗时增加项目里设700已经够用

调参的原则是:一次只动一个参数,观察它的影响,不要同时调好几个变量,否则出了问题根本不知道是哪个参数引起的。我把这个经验放在这里,是因为我早期调试时经常犯这个错误,改了好几个参数后整个识别结果完全变了,也不知道回退到哪个值,折腾了很长时间才明白应该控制变量法。

5. 常见问题与排查技巧实录

不管代码写得多顺,拿到真实图片上跑一轮,总会遇到各种预料之外的问题。这一章我把自己在模拟项目X中实际踩过的坑和解决方法整理出来,按问题类型分类,供你遇到同样情况时快速定位。

5.1 问题速查表

下面这个表格是我最常被问到的几个问题,我直接给到现象、原因和解决方案。

问题现象根本原因解决方案
最大轮廓找错,定位到桌面或背景物体答题卡不是画面中面积最大的区域增加面积阈值过滤,或先预处理突出答题卡(如形态学闭运算)
透视后答题卡上下颠倒角点排序错误用前面说的坐标和/坐标差的排序方法校正
某道题识别结果不稳定,时对时错选项区域切割坐标偏差把切割小图保存下来,逐个 visualize 检查
学生涂浅了但被判为空白填充率阈值设置过高根据笔迹类型降低阈值到0.08~0.12
橡皮擦残留痕迹被判为填涂填充率阈值设置过低提高阈值并做形态学开运算去掉小噪点
图片太亮导致二值化后大面积全黑光照过曝先做直方图均衡化或亮度归一化,再二值化
面积大的轮廓不只一个,但都不干净轮廓断裂先膨胀(dilate)再查找轮廓,让边缘连续起来

排查这类问题有一个通用方法:把每一步的中间结果都用cv2.imshow输出来看。看边缘检测图、透视矫正图、选项切割图、二值化图,很快就能定位是哪一步出了问题。依赖脑补和print是排查不了图像问题的。

5.2 三个我踩得最深的坑

第一个坑跟光照有关。我在某一次测试时,答题卡刚好放在台灯正下方,画面中央特别亮,边缘相对偏暗。这种情况下,OTSU二值化虽然自适应,但它是全局估计,一个阈值对所有区域有效。结果中央区域的填涂笔迹被识别得不错,边缘区域的浅色铅笔笔迹却全部消失了,因为有光照梯度。解决方案是在二值化前先做一次直方图均衡化,或者对图像做分块阈值处理,让局部亮度归一化。我后来用cv2.createCLAHE做对比度受限自适应直方图均衡化,效果显著改善。

第二个坑是选项间距不是整数倍的。我们的模板设计时行距、列距是等分的,但透视矫正本身有微小误差,导致切割第5题以后,切割框开始偏移到选项边框上,把边框也算进了填涂区域。这个问题后来靠扩大切割框的容错范围,以及把切割框稍微缩小、只取中心区域来判断填涂解决了。核心思路是:切割区域宁可小于实际选项框,也尽量不要包含外圈黑色边框,否则灰度统计会被边框污染。

第三个坑是多选异常。本来我们的设计只支持单选,但有张测试卡上学生把某道题的B和D都涂了。按我的代码逻辑,取填充率最高的那一个作为答案,结果那道题被判为选了B,恰好B不是标准答案,就给评成了错题。但从实际阅卷角度看,这种多选应该单独提示,而不是简单判错。我后来在评分前增加了多检逻辑:如果该题有两个选项的填充率都超过了填涂阈值,就在结果图上标注“多选题”,不参与正常评分。这个边界情况处理起来不复杂,但能体现项目设计的完备性。

6. 从demo到可用的系统:扩展方向与优化建议

跑通一个基础版本的答题卡识别并打分之后,很多人会问:然后呢?这个问题问得好,因为课程设计和毕设的区别,恰恰体现在“然后”的部分——基础demo能实现单张图片的处理逻辑,而一个更完整的系统需要处理真实世界的复杂性和多人使用场景。下面我整理了三个我实际做过或见过的扩展方向,你可以根据自己的时间选一个深入。

6.1 从单题卡到多题型卡

基础的代码只支持单选题,而且格式是硬编码的。真实答题卡通常包含判断题、多选题、填空题甚至主观题区域。多选题的识别逻辑跟单选题差异很大,它需要检测所有超过阈值的选项,而不是只取最大值。判断题则只需检测“对”或“错”两个区域。填空题需要结合OCR才能完成全流程。你可以先从多选题入手,把判题逻辑从“取最高比”改成“收集所有超阈值的选项”,代码改动量不大,但对系统能力的提升很明显。

还有一个更实际的改动是把模板参数从硬编码改成外部配置,比如用JSON或YAML文件描述每道题的题型、选项数、坐标范围、分值。这样换一套答题卡就不用改代码,只改配置文件即可。这个改动虽然简单,但非常提升项目的工程化程度。

6.2 批量识别与GUI界面

课程设计答辩时,如果能现场演示批量处理一整个文件夹的答题卡图片,效果会比单张图展示好得多。批量识别的思路很简单:用os.listdir遍历文件夹下所有图片,逐张调用前面封装好的处理函数,结果汇总到一张Excel表格里,用pandas写导出CSV都很方便。这里要注意的是图片命名规范,比如用学生学号加姓名命名,方便结果和成绩对应起来。

GUI方面,可以用OpenCV自带的窗口加鼠标事件实现一个简单的交互界面,也可以直接用Tkinter做文件选择、识别按钮和结果展示。我个人的建议是:如果只是答辩展示,用Tkinter做一个最简界面就够了,不要过度投入前端,重点还是识别算法本身。

6.3 识别率评估与答题卡自适应定位

最后说一个容易被忽视的维度:怎么科学地评估你的系统做得好不好。我当时做了一个简单的测试集,包含50张不同光照、不同角度的答题卡照片,手写标注每张题卡上的真实填涂情况,然后写了一个脚本,统计每道题的识别正确率。

评估结果能告诉你算法在哪类场景下最脆弱。比如我测出来,系统在均匀光照下识别率接近100(100张里只错1到2道),但在有阴影遮挡的情况下跌到94左右,这样就能有针对性地改进预处理环节。这种“用数据说话的改进思路”,在写项目报告或者答辩时非常有说服力,也能帮助你快速判断自己是否真的掌握了这个项目。

最后说点实际体会

做完这个答题卡识别并打分项目之后,我最大的感受是:OpenCV这套工具表面上像是调函数,但真正让结果变好的,是对图像规律的敏感度。同样是二值化,不同光照环境下出来的效果完全不同;同样是找轮廓,边缘断裂和背景干扰的处理方式也完全不同。你花在调试中间步骤上的时间,往往比写主线逻辑的时间还多,但恰恰是这部分经验,才是做计算机视觉项目最宝贵的收获。

如果你是第一次接触这类项目,我建议不要急着跑通全流程就结束。拿着你自己的手机,拍几张不同角度、不同光线下的答题卡,把每道题都识别一遍,看看哪些步骤是脆弱的,然后去优化它们。这个过程会把书上学到的图像处理知识真正内化成你的直觉。等你把这些问题都解决一遍,再回头看我开头说的“1秒出分”,你就会明白,那背后一点都不简单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询