15- 答题卡识别及分数判定项目 (OpenCV系列) (项目十五)

news2025/7/20 10:44:17

项目要点

  • 图片读取 : img = cv2.imread('./images/test_01.png')
  • 灰度图:  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
  • 高斯模糊:  blurred = cv2.GaussianBlur(gray, (5, 5), 0)     # 去噪点
  • 边缘检测:  edged = cv2.Canny(blurred, 75, 200)
  • 检测轮廓:  cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]    # 两个返回值 contours, hierarchy
  • 描绘轮廓cv2.drawContours(contours_img, cnts, -1, (0, 0, 255), 2)
  • 轮廓面积排序:  cnts = sorted(cnts, key = cv2.contourArea, reverse = True)
  • 计算轮廓周长:  perimeter = cv2.arcLength(c, True)
  • 得到近似轮廓:  approx = cv2.approxPolyDP(c, 0.15 * perimeter, True)
  • 计算变换矩阵:  M = cv2.getPerspectiveTransform(rect, dst)  # dst 为目标值
  • 通过坐标透视变换转换: warped = cv2.warpPerspective(image,M,(max_width, max_height))  # 注意传参
  • ret, thresh1 = cv2.threshold(img,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)  二值化处理图片
  • 计算非零个数, 先做与运算:  mask = cv2.bitwise_and(thresh, thresh, mask = mask)
  • 计算非零个数,选中的选项, 非零个数比较多:  total = cv2.countNonZero(mask)
  • 画出正确选项cv2.drawContours(warped, [cnts[k]], -1, color, 3)
  • cv2.putText(warped, f'{score:.2f}',(210,36), cv2.FONT_HERSHEY_SIMPLEX,0.9, (0,0,255), 2)  # 在图片上体现分数
  • 显示图片cv2.imshow(name, img)


1 提取答题卡部分图像内容

1.1 读取图片

  • 定义显示函数
def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
import cv2
import numpy as np

# 读图片,预处理
img = cv2.imread('./images/test_01.png')
# 转变为黑白图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv_show('img', img)

1.2 边缘检测

  • 定义显示函数
# 高斯模糊去噪点
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 边缘检测
edged = cv2.Canny(blurred, 75, 200)
cv_show('img', edged)

        

1.3 检测轮廓

# 检测轮廓
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 画轮廓会修改原图
contours_img = img.copy()
cv2.drawContours(contours_img, cnts, -1, (0, 0, 255), 2)
cv_show('contours_img', contours_img)

1.4 找出答题卡轮廓

  • 本次其实原本也只检测到了一个轮廓.

# 确保拿到的轮廓是答题卡的轮廓
if cnts:
    # 对轮廓面积排序
    cnts = sorted(cnts, key = cv2.contourArea, reverse = True)
    # 遍历每一个轮廓
    for c in cnts:
        # 计算周长
        perimeter = cv2.arcLength(c, True)
        # 得到近似轮廓
        approx = cv2.approxPolyDP(c, 0.15 * perimeter, True)
        # 应该只剩下四个角的坐标
        print(len(c))
        print(len(approx))
        if len(approx) == 4:
            # 保存approx
            docCnt = approx 
            # 找到后直接退出
            break
print(docCnt)
contours_img = img.copy()
cv2.drawContours(contours_img, docCnt, -1, (0, 0, 255), 10)
cv_show('contours_img', contours_img)

     

 1.5 透视变换

# 找到进行透视变换的点
# 要求变换矩形的四个坐标
# 先对获取到的4个角点坐标排序
# 排序功能封装为一个函数
def order_points(pts):
    # 创建全为0 的矩阵, 来接收找到的坐标
    rect = np.zeros((4, 2), dtype = 'float32')
    s = pts.sum(axis = 1)
    # 左上角的坐标一定是X,Y相加最小的,右下为最大的
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]
    
    # 右上角的x,y 相减的差值一定是最小的
    # 左下角的x,y 相减,差值一定是最大的
    diff = np.diff(pts, axis = 1)
    rect[1] = pts[np.argmin(diff)]
    rect[3] = pts[np.argmax(diff)]
    return rect
# 把透视变换功能封装为一个函数
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)
    max_width = 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)
    max_wdith = (int(widthA), int(widthB))
    max_height =max(int(heightA), int(heightB))
    
    # 构造变换后的空间坐标
    dst = np.array([
        [0, 0],
        [max_width - 1, 0],
        [max_width - 1, max_height -1],
        [0, max_height - 1]], dtype = 'float32')
    
    # 计算变换矩阵
    M = cv2.getPerspectiveTransform(rect, dst)  # dst 为目标值
    # 透视变换
    warped = cv2.warpPerspective(image, M, (max_width, max_height))  # 注意传参
    return warped

# 进行透视变换
warped = four_point_transform(gray, docCnt.reshape(4, 2))
cv_show('warped', warped)

2 处理答题卡

2.1 二值化图像  (两个返回值)

# 二值化   # +inv 黑白颠倒
# ret, thresh1 = cv2.threshold(img,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)
thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)

2.2 找每一个圆圈的轮廓

# 找每一个圆圈的轮廓
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
thresh_contours = thresh.copy()
cv2.drawContours(thresh_contours, cnts, -1, 255, 3)  # 255表示填充白色,填充为0时为后图
cv_show('thresh_contours', thresh_contours)

 

 2.3 找出特定轮廓  (选项)

# 遍历所有轮廓, 找到特点宽高和特定比例的轮廓, 及圆圈的轮廓
question_cnts = []
for c in cnts:
    # 找到轮廓的外接矩形
    (x, y, w, h) = cv2.boundingRect(c)
    # 计算高宽比
    ar = w / float(h)
    
    # 根据实际情况制定标准
    if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:
        question_cnts.append(c)
print(len(question_cnts))       # 25

3 分数判定

3.1 轮廓排序封装函数

# 设计做法看轮廓,找出选择的答案
# 轮廓排序封装函数
def sort_contours(cnts, method = 'left-to-right'):
    reverse = False
    # 排序的时候取x轴数据, i= 0, 取y轴数据 i=1
    i = 0
    if method == 'right-to-left' or method == 'bottom-to-top':
        reverse = True
    if method == 'top-to-bottom' or method == 'bottom-to-top':
        i = 1
    # 计算每个轮廓的外接矩形
    bounding_boxes = [cv2.boundingRect(c) for c in cnts]   
    # 计算轮廓的垂直边界最小矩形,矩形是与图像上下边界平行的
    (cnts, bounding_boxes) = zip(*sorted(zip(cnts, bounding_boxes),
                                  key = lambda a: a[1][i], reverse = reverse))
    # a 表示zip(cnt, bounding_boxes)
    # rint(bounding_boxes[1])
    return cnts, bounding_boxes

# 按照从上到下的排序 :question_cnts
question_cnts = sort_contours(question_cnts, method = 'top-to-bottom')[0]
print(len(question_cnts))   # 25

3.2 找出选择答案, 然后评分

# 正确答案
ANSWER_KEY = {0:1, 1:2, 2:1, 3:2, 4:1}   # 1,4,0,2,1
correct = 0
# enumerate,遍历了所有元素,并从零开始的计为每个元素生成索引, q为index
for (q, i) in enumerate(np.arange(0, 25, 5)):  
    first_num = True
    # 每次取出5个,再按x大小排序
    # 每5个一组进行排序, 0表示cnts 原轮廓, 默认排序'left-to-right'
    cnts = sort_contours(question_cnts[i: i+ 5])[0]  
    print('-------------------', len(cnts))   # 5个选项
    bubbled = None  # 冒泡
    # 遍历每一个结果
    for (j, c) in enumerate(cnts):
        #使用掩膜,即mask
        mask = np.zeros(thresh.shape, dtype = 'uint8')
        cv2.drawContours(mask, [c], -1, 255, -1)    # 255表示填充白色

        # 计算非零个数, 先做与运算
        mask = cv2.bitwise_and(thresh, thresh, mask = mask)
        # cv_show('mask', mask)
        # 计算非零个数,选中的选项, 非零个数比较多, 没选中的非零个数小一些
        total = cv2.countNonZero(mask)
        # print('******', total,j)
        
        if bubbled is None or total > bubbled[0]:
            bubbled = (total, j)    # 正确选项的 total 值较高
            
        color = (0, 0 ,255)
        k = ANSWER_KEY[q]    # enumerate 的序列值
        # print(len(cnts))
        # 判断是否正确
        if k == bubbled[1] and first_num == True:
            correct += 1
            first_num = False
            print('正确选项: ',k)
        cv2.drawContours(warped, [cnts[k]], -1, color, 3)  # 画出正确选项
        
# 计算得分
score = int((correct / 5)* 100)
print(f'score:{score:.2f} 分')

warped_copy = warped.copy()
cv2.putText(warped_copy, f'{score:.2f}', (210, 36), cv2.FONT_HERSHEY_SIMPLEX,
            0.9, (0, 0, 255), 2)
cv_show('result', warped_copy)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/368752.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ai作画studio环境布置

大家好&#xff0c;今天跟大家介绍如何用stable diffusion webui布置自己的Ai作画工作环境。这部分主要就是实操&#xff0c;没有太多理论知识介绍。跟着做就好了&#xff0c;当成是一次计算机实验上机课就好基础环境布置这次开发环境选择的是AutoDL的GPU云环境&#xff0c;链接…

ros下用kinectv2运行orbslam2

目录 前提 创建工作空间 orbslam2源码配置、测试&#xff1a; 配置usb_cam ROS功能包 配置kinect 前提 vim 、 cmake 、 git 、 gcc 、 g 这些一般都装了 主要是Pangolin 、 OpenCV 、 Eigen的安装 18.04建议Pangolin0.5 创建工作空间 我们在主目录下创建一个catkin_…

听车企做开发朋友说,面试Framework 必问~

近期听在车企工作的朋友说&#xff0c;今年去他们公司面试的人比往年增长了30%左右&#xff0c;但实际面试达到标准的人屈指可数&#xff0c;大多都是从 Android 开发方向转过来的。 车企招聘要求有哪些&#xff1f; 每个车企因为业务部门的不同&#xff0c;他们的要求也会有…

Tapdata Connector 实用指南:实时数仓场景之数据实时同步至 ClickHouse

【前言】作为中国的 “Fivetran/Airbyte”, Tapdata 是一个以低延迟数据移动为核心优势构建的现代数据平台&#xff0c;内置 60 数据连接器&#xff0c;拥有稳定的实时采集和传输能力、秒级响应的数据实时计算能力、稳定易用的数据实时服务能力&#xff0c;以及低代码可视化操作…

搭建直播平台服务器什么配置最合适?直播平台专用服务器(驰网i9-13900k服务器)

如今&#xff0c;视频直播越来越受欢迎&#xff0c;视频和直播平台也越来越多&#xff0c;直播平台和视频网站都需要更好的服务器来支持。那么&#xff0c;视频直播平台需要什么服务器呢&#xff1f;以一个简单的直播网站为例。如果每天在线人数约1000人&#xff0c;同时在线人…

Win10 22H2 19045.2670系统原版镜像

相比以前系统&#xff0c;修复了如下KB5022906 修复了诸多问题&#xff0c;IT之家将官方更新日志翻译如下&#xff1a;修复了在 Microsoft Excel 中无法打开超链接的问题修复了影响 Appx 状态存储库的问题。在用户删除用户配置文件后&#xff0c;相关清理工作未能妥善完成&…

【nohup引发磁盘读写高】nohup命令导致服务器磁盘读写占满该如何修复?

【写在前面】自己在跑一个项目的时候&#xff0c;猛然发现服务器挂了&#xff0c;直接访问不了&#xff0c;呈现出一种卡死现象&#xff0c;我当时都懵了&#xff0c;难道阿里在后端升级&#xff0c;也不会选择在工作日的时间升级吧&#xff0c;于是乎就咨询了一下客服。才有下…

【项目精选】jsp码头船只出行及配套货柜码放管理系统的设计与实现(视频+源码+论文)

点击下载源码 jsp码头船只出行及配套货柜码放管理系统主要用于实现高校在线考试&#xff0c;基本功能包括&#xff1a;用户登录、修改个人信息、查看码头信息&#xff1b;系统管理人员管理&#xff1b;船只信息管理&#xff1b;船只路线信息管理&#xff1b;货柜信息管理等。本…

Vue-cli脚手架在做些什么(源码角度分析)

什么是Vue脚手架&#xff1f;在学习初期&#xff0c;我们的项目往往需要借助webpack、vite等打包工具配置Vue的开发环境&#xff0c;但是在真实开发中我们不可能每个项目从头来完成所有的webpack配置&#xff0c;这样显得开发的效率会大大的降低&#xff1b;所有的真实开发中&a…

手写SpringBoot的starter

自定义SpringBoot的starter 引言 starter命名格式&#xff1a; 官方的 starter 的命名格式为 spring-boot-starter-{xxxx} 比如spring-boot-starter-activemq 第三方我们自己的命名格式为 {xxxx}-spring-boot-starter。比如mybatis-spring-boot-starter。 如果我们忽略这种约定…

【Git】与“三年经验”就差个分支操作的距离

前言 Java之父于胜军说过&#xff0c;曾经一位“三年开发经验”的程序员粉丝朋友&#xff0c;刚入职因为不会解决分支问题而被开除&#xff0c;这是不是在警示我们什么呢&#xff1f; 针对一些Git的不常用操作&#xff0c;我们通过例子来演示一遍 1.版本回退 1.1已提交但未p…

【Nginx】|入门连续剧——安装

作者&#xff1a;狮子也疯狂 专栏&#xff1a;《Nginx从入门到超神》 坚持做好每一步&#xff0c;幸运之神自然会降临在你的身上 目录一. &#x1f981; 前言Ⅰ. &#x1f407; 为啥我们要使用Nginx&#xff1f;二. &#x1f981; 搭建流程Ⅰ. &#x1f407; 环境准备Ⅱ. &…

LeetCode 热题 C++ 141. 环形链表 142. 环形链表 II(详解!)

力扣141 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&am…

图片和ppm文件互转

一、代码结构 二、代码实现 Denoise.java&#xff1a; package com.xj.ppm.toimg;import java.awt.FlowLayout; import java.awt.image.BufferedImage; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JLabel;/*** Image noise reduction pro…

I.MX6ULL内核开发10:设备树

目录 一、设备树简介 二、设备树源码 三、获取设备树信息 1、增加设备节点 2、内核编译设备树 3、替换设备树文件 4、查看设备树节点 5、在驱动中获取节点的属性 6、编译驱动模块 7、加载模块 一、设备树简介 设备树的作用是描述一个硬件平台的硬件资源。这个“设备树…

Redis主从和哨兵搭建

今天主要分享Redis主从架构和哨兵的搭建。 主从集群搭建 总共三个节点&#xff0c;一个主节点和两个从节点。都安装在一台机器上模拟主从集群&#xff0c;信息如下&#xff1a; IPPORT角色192.168.246.1407001slave192.168.246.1407002master192.168.246.1407003slave 我们只…

FreeRTOS入门(04):中断、内存、追踪与调试

文章目录目的中断内存堆&#xff08;heap&#xff09;栈&#xff08;stack&#xff09;断言调试总结目的 有了前面的几篇文章 FreeRTOS 基本上已经可以在项目中使用上了&#xff1a; 《FreeRTOS入门&#xff08;01&#xff09;&#xff1a;基础说明与使用演示》 《FreeRTOS入门…

【java 8】强大的 Stream API

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…

因子分析计算权重

因子分析两类权重计算方法总结 案例背景 疫情爆发以来&#xff0c;越来越多的人为了避免线下与人接触&#xff0c;选择了线上购买生活必需品。网购虽然方便快捷&#xff0c;但是随着订单压力的增加&#xff0c;物流问题也随之出现&#xff0c;近期有很多卖家收到物流投诉的问题…

Ubuntu下Python的安装及管理

Ubuntu下Python的安装及管理 1.概述 Ubuntu下python的安装及配置。 2.安装 安装python2.7&#xff1a; python --version #或python2.7 --version检查检查python是否存在&#xff0c;有则无需继续安装python2.7 sudo apt-get update sudo apt-get install python #或者su…