小白啃骨头系列是通过案例的分享,描述7是怎么入门某类技术的。下面就请进入正题吧~
相信大家对于有一些名词耳熟能详,可以谈天说地,但是真正想要用起来,却觉得门槛太高,望而却步。如今AI大火,凡是沾点边就感觉高级炸了。什么图像识别,语音识别,自动驾驶等等,今天7,就不怕死的去尝试了这么难啃的骨头,究竟结论怎样呢?我们先看样本骨头。
本次案例的目的是用python3的OpenCV库实现答题卡识别,这里可能要补充一点知识,毕竟对于真小白来说,我们需要名词解释环节。
OpenCV:分为OPEN + CV, open好理解open source即开源,CV指的是计算机视觉(computer version)。OpenCV最早由英特尔公司于1999年启动,后来以BSD许可证授权发行,可以在商业和研究领域中免费使用。简而言之,你做图像识别,或者计算机视觉或者模式识别,那很有可能和它打交道了。OpenCV最早支持C语言,因为就是C写的,而先前很火的OpenCV的例子是用MATLAB开发的,如今Java,Python等都有相关的库可以使用了。
关于各个平台python和OpenCV怎么安装,大家自行Google解决啦。
附上本地示例的原图:
解决一个问题,第一步是要有思路。但我相信很多新手对于要识别图像,识别答题卡是没有思路的,没错,要是你有思路那就不是小白了。7对于这艰难的第一步那就是去找文档和开源项目看,总结他们的相同点,整理核心算法思路,梳理整个识别的步骤。然后在debug自己的代码,一边被虐一边进步。
思路有了,下面就要开始敲一敲了。7会给出每一步对应的代码片段帮助大家理解过程。
进行图像灰度,使图像黑白(二值化)
为什么要进行二值化,我认为有两点,一是硬性要求,很多算法要求输入是一个二值化后的图像,俗称黑白图像;二是为了更好地区分主题和背景。比如你需要识别pdf里的文字,如果用彩色图像,那你就要先区分颜色,在处理文字,而实际上对于整个识别目的来说,只需要识别字,并不在意字本身什么颜色,也就是忽略颜色对识别的干扰。那为什么二值化之前要灰度,灰度图像像素介于0-255之间,在二值化的时候我们只需要设定某一个值,高于这个值就是黑,低于这个值则是白。这整个过程过程分为两小步:
对原图像进行灰度,然后应用高斯模糊使图像模糊一点,去除随机点
通俗的来说就是去噪,为了去除噪点对图像的影响,也就是平时大家P照片时美颜。
# 灰度gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)# 高斯模糊,3 * 3的核,此处核越大就越模糊,即美颜效果越好。blur = cv.GaussianBlur(gray, (3, 3), 0)
2. 使用自适应阈值设置每个像素为黑色或白色
这一步就是图像二值化,把像素分为0和255两个度,非黑即白。
# 图像二值化binary = cv.adaptiveThreshold(gray, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY_INV, 5, 4)
使用霍夫变换找到图中的直线, 找到直线的交点,形成四边形
对于答题卡来说,因为每个答题卡设计会影响我们的算法,这一步是核心算法,需要定位答题卡的答题范围。在7的示例中,我们采用的答题卡有一个很重要的特点,那就是答题卡区域整个答题卡最大的区域,也就是说,我们通过找出答题卡中的轮廓,找到最大的就是答题的区域了。
# 找到二值化图像所有的轮廓conts = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)[0]# 对轮廓进行反向排序,第一个就是最大的轮廓即示例中答题区域conts = sorted(conts, key=cv.contourArea, reverse=True)
找到四边形轮廓,并对四边形应用透视变换
为什么要进行透视变换?这里的透视变换是四点透视,透视变化是一种投影。大家可以思考一个例子,如果我们因为角度问题是不是在扫描矩形图片的时候并不是一个矩形,可能是个梯形,这与实际不符,算法需要规避这种角度问题,所以有了变化操作,而四点透视就是为了把一个梯形拉成一个矩形(可能比较抽象,如果不能理解,大家可以尝试用手机从非物体正上方的位置拍一张照感受下)
# 多边形拟合,我们找到的轮廓都是有像素点构成的,计算机只能处理像素,# 在计算机眼里并没有直线曲线之分。peri = 0.01 * cv.arcLength(cont, True)approx = cv.approxPolyDP(cont, peri, True# 判断是否为四边形if len(approx) == 4: # 四点透视变换 ox = four_point_transform(image, approx.reshape(4, -1))
使用霍夫变换找到图中的答案轮廓
有了轮廓的概念,其实答案框也是一个一个的轮廓,通过答案框长宽比例来过滤掉非答题框的轮廓。
for c in cnts: # 对每一个轮廓进行长宽比过滤,找到所有答案框的位置 x, y, w, h = cv.boundingRect(c) ar = w / float(h) if 80 >= w >= 46 and h >= 30 and 1.2 <= ar <= 1.9: cv.rectangle(question, (x, y), (x + w, y + h), (0, 0, 255), 2) question_choices.append(c)
区分答题框是否填充
这里我们采用在答题框轮廓中像素点进行反向排序,像素最多的即为填涂的选项(这里只适用于单选题)
for (i, q) in enumerate(np.arange(0, len(question_choices), 12)): if i <= 10: line_cnts = question_choices[q:q + 12] line_cnts = contours.sort_contours(line_cnts, method="left-to-right")[0] # 取出i行,第k题 for (k, m) in enumerate(np.arange(0, len(line_cnts), 4)): ticket_num = i + k * 17 + 1 bubble_rows = [] m_cnts = line_cnts[m:m + 4] # print("{}: {}".format(i, len(m_cnts))) # 取出k题 j选项 for j, c in enumerate(m_cnts): mask = np.zeros(tx.shape, dtype=tx.dtype) cv.drawContours(mask, [c], -1, 255, -1) mask = cv.bitwise_and(binary, binary, mask=mask) total = cv.countNonZero(mask) bubble_rows.append((total, j)) bubble_rows = sorted(bubble_rows, key=lambda x: x[0], reverse=True) choice_num = bubble_rows[0][1] student_answer.append((ticket_num, answer_format.get(choice_num)))
上面的算法写的比较初学者,大神们看看就行了
最后我们打印出题号和答案
# 比较简单就不赘述了 for item in student_answer: print("题号: {} 答案:{}".format(item[0], item[1])) return {"答题卡答案": [{'题号': x[0], '答案': x[1]} for x in student_answer]}
以上就是小白啃骨头的过程,完整代码会重新整理后放到GitHub上。由于本文涉及的内容比较专业,如果有说错的还请各路大神不吝赐教,早日指正!
推荐阅读
(点击标题可跳转阅读)
最详细、最完整的相机标定讲解
深度学习+机器视觉=下一代检测
视觉检测系统最经典的结构你了解吗?
机器视觉技术的十大应用领域
工业相机和普通相机究竟有什么不同?
基于机器视觉和深度学习的智能缺陷检测
波士顿等移动机器人的视觉算法解析
2020年37个人工智能技术发展趋势
机器视觉的光源选型及打光方案分析
光学三维测量技术及应用
国内80%搞机器视觉的工程师,走的路子是错的!
视觉+机器人,如何实现连接器的自动装配?
机器视觉技术发展的五大趋势
搞懂机器视觉基本内容,这份PPT就够了
机器视觉:PC式视觉系统与嵌入式视觉系统区别
基于HALCON的机器视觉开发,C++或C#如何选择?
3D视觉技术在机器人抓取作业中的应用实例
基于机器视觉的粗糙度检测方案
机器视觉常用图像软件对比及分析
工业相机编程流程及SDK接口使用汇总
End
声明:部分内容来源于网络,仅供读者学习、交流之目的。文章版权归原作者所有。如有不妥,请联系删除。