OpenCV-答题卡识别-四点透视变换

news2025/2/27 23:09:44

目录

  • 答题卡识别
    • 图片读取
    • 四点透视变换 划出区域
    • 处理选择题区域
    • 处理准考证号区域
    • 处理科目区域
    • 得分导出结果
  • 封装成品

答题卡识别

使用opencv技术,实现对答题卡的自动识别,并进行答题结果的统计

技术目的:

  1. 能够捕获答题卡中的每个填涂选项;
  2. 将获取的填涂选项与正确选项做对比计算其答题正确率;

技术流程:

  1. 识别答题区域,对于答题结果进行统计,并且做出打分;
  2. 识别准考证号,正确读取学生准考证号;
  3. 识别科目代号,正确读取科目代码;
    5
import cv2
import numpy as np
from imutils.perspective import four_point_transform
from matplotlib import pyplot as plt

import matplotlib
matplotlib.rc("font",family='SimHei')

%matplotlib inline
def judgeX(x,mode):
    if mode=="point":
        if x<600:
            return int(x/100)+1
        elif x>600 and x<1250:
            return int((x-650)/100)+6
        elif x>1250 and x<1900:
            return int((x-1250)/100)+11
        elif x>1900:
            return int((x-1900)/100)+16
    elif mode=="ID":
        return int((x)/50)+1
        # return int((x-110)/260)+1
    elif mode=="subject":
        if x<1500:
            return False

def judgeY(y,mode):
    if mode=="point":
        if y%560>180 and y%560<240:
            return 'A'
        elif y%560>260 and y%560<320:
            return 'B'
        elif y%560>340 and y%560<380:
            return 'C'
        elif y%560>420 and y%560<480:
            return 'D'
        else:
            return False
    
    elif mode=="ID":
        if y>135:
            return int((y-135)/30) # return int((y-950)/180)
        else:
            return False
    
    elif mode=="subject":
        print (y, mode)
        if int((y-140)/25)==0:
            return  "政治"
        elif int((y-140)/25)==1:
            return  "语文"
        elif int((y-140)/25)==2:
            return  "数学"
        elif int((y-140)/25)==3:
            return  "物理"
        elif int((y-140)/25)==4:
            return  "化学"
        elif int((y-140)/25)==5:
            return  "英语"
        elif int((y-140)/25)==6:
            return  "历史"
        elif int((y-140)/25)==7:
            return  "地理"
        elif int((y-140)/25)==8:
            return  "生物"
        else:
            return "科目代号填涂有误"
def judge(x,y,mode):
    if judgeY(y,mode)!=False and judgeX(x,mode)!=False:
        if mode=="point":
            return (int(y/560)*20+judgeX(x,mode),judgeY(y,mode))
        elif mode=="ID":
            return (judgeX(x, mode), judgeY(y, mode))
        elif mode=="subject":
            print (y, mode)
            return judgeY(y, mode)
    else:
        return 0

def judge_point(answers, mode):
    IDAnswer = []
    for answer in answers:
        if(judge(answer[0],answer[1],mode)!=0):
            IDAnswer.append(judge(answer[0],answer[1],mode))
        else:
            continue
    IDAnswer.sort()
    return IDAnswer


def judge_ID(IDs, mode):
    student_ID=[]
    for ID in IDs:
        if(judge(ID[0],ID[1], mode)!=False):
            student_ID.append(judge(ID[0], ID[1], mode))
        else:
            continue
    student_ID.sort()
    print ('student_ID:', student_ID)
    return student_ID

def judge_Subject(subject, mode):
    return judge(subject[0][0], subject[0][1], mode)

图片读取

因要做后续分割,所以要用到边缘检测,先灰度化再二值化

# 读取图片 RGB \ BGR
img = cv2.imread('./images/5.png')

# 转换为灰度图 COLOR_BGR2GRAY、COLOR_BGR2RGB
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

# 高斯滤波 中值滤波
blurred_gauss = cv2.GaussianBlur(gray, (3,3), 0)
# 增强亮度
def imgBrightness(img1, c, b): 
    rows, cols= img1.shape
    blank = np.zeros([rows, cols], img1.dtype)
    # cv2.addWeighted 实现两副相同大小的图像融合相加
    rst = cv2.addWeighted(img1, c, blank, 1-c, b)
    return rst

blurred_bright = imgBrightness(blurred_gauss, 1.5, 3)

自适应二值化函数: cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C, dst=None)

参数解释
src指原图像,原图像应该是灰度图;
maxValue指当像素值高于(有时是小于)阈值时应该被赋予的新的像素值;
adaptive_method自适应阈值算法,CV_ADAPTIVE_THRESH_MEAN_C 或 CV_ADAPTIVE_THRESH_GAUSSIAN_C
threshold_type取阈值类型, 必须是下者之一 (CV_THRESH_BINARY, CV_THRESH_BINARY_INV)
block_size计算阈值的象素邻域大小: 3, 5, 7, …
C常数,每个区域计算出的阈值的基础上在减去这个常数作为这个区域的最终阈值,可以为负数;
dst输出图像,可以忽略;
# 自适应二值化

blurred_threshold = cv2.adaptiveThreshold(blurred_bright, 
                                          255, 
                                          cv2.ADAPTIVE_THRESH_MEAN_C, 
                                          cv2.THRESH_BINARY, 51, 2)
# 显示原来的和缩放后的图像  Create a figure
fig = plt.figure(figsize=(16, 12))

# Subplot for original image
a=fig.add_subplot(2,3,1)
imgplot = plt.imshow(img)
a.set_title('原始图片')

a=fig.add_subplot(2,3,2)
imgplot = plt.imshow(gray, cmap='gray')
a.set_title('灰度图')

a=fig.add_subplot(2,3,3)
imgplot = plt.imshow(blurred_gauss, cmap='gray')
a.set_title('高斯滤波')

a=fig.add_subplot(2,3,4)
imgplot = plt.imshow(blurred_bright, cmap='gray')
a.set_title('增强亮度')

a=fig.add_subplot(2,3,5)
imgplot = plt.imshow(blurred_threshold, cmap='gray')
a.set_title('自适应二值化')

plt.show()
# plt.savefig('5张图片.png', bbox_inches='tight')

image-20230512210555920

image-20230512210606092

canny边缘检测

根据轮廓大小,将要处理的几部分分割出来; 如果提取效果不好,可能是拍摄光线原因,导致图片亮度不好,增强一下亮度,二值化后的图片效果会好一点,这样canny边缘检测结果也会好一点

cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient ]]])

参数解释
image要检测的图像
threshold1阈值1(最小值)
threshold2阈值2(最大值),使用此参数进行明显的边缘检测
edges图像边缘信息
apertureSizesobel算子(卷积核)大小
L2gradient布尔值, True:使用更精确的L2范数进行计算(即两个方向的导数的平方和再开方),False:使用L1范数(直接将两个方向导数的绝对值相加)
# 用来给图片添加边框
blurred_border = cv2.copyMakeBorder(blurred_threshold, 5, 5, 5, 5, cv2.BORDER_CONSTANT, value=(255,255,255))

# canny边缘检测
edged = cv2.Canny(blurred_border, 0, 255)

# 查找检测物体的轮廓信息
cnts, hierarchy = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

docCnt = []
count = 0
# 确保至少有一个轮廓被找到
if len(cnts)>0:
    #将轮廓按照大小排序
    cnts = sorted(cnts, key=cv2.contourArea, reverse=True)

# 对排序后的轮廓进行循环处理
for c in cnts:
    
    # 获取近似的轮廓
    # 轮廓周长。也称为弧长
    peri = cv2.arcLength(c, True)
    # 一个重新采样的轮廓,所以它仍然会返回一组 (x, y) 点
    approx = cv2.approxPolyDP(c,0.02*peri,True)
    
    #如果近似轮廓有四个顶点,那么就认为找到了答题卡
    if len(approx) == 4:
        docCnt.append(approx)
        count += 1
        if count==3:
            break
    # break

查找检测物体的轮廓信息
cv2.findContours(image, mode, method, contours=None, hierarchy=None, offset=None)

参数解释
image源图像,寻找轮廓的图像,注意输入的图片必须为二值图片;若输入的图片为彩色图片,必须先进行灰度化和二值化;
mode轮廓的检索方式,有4种;
method轮廓的近似办法,有4种,一般用cv.CHAIN_APPROX_SIMPLE,就表示用尽可能少的像素点表示轮廓;
contours图像轮廓坐标,是一个链表,使用findContours检测到的轮廓数据,每个轮廓以点向量的形式存储,list中每个元素(轮廓信息)类型为ndarray,point类型的vector
hierarchy[Next, Previous, First Child, Parent], 可选层次结构信息
offset可选轮廓偏移参数,用制定偏移量offset=(dx, dy)给出绘制轮廓的偏移量

轮廓的检索模式

mode解释
cv2.RETR_EXTERNAL只检测外轮廓;
cv2.RETR_LIST提取所有轮廓,并放置在list中,检测的轮廓不建立等级关系;
cv2.RETR_CCOMP建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息,如果内孔内还有一个连通物体,这个物体的边界也在顶层;
cv2.RETR_TREE检测所有轮廓,建立完整的层次结构,建立网状轮廓结构;

轮廓的近似办法

method解释
cv2.CHAIN_APPROX_NONE获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1;
cv2.CHAIN_APPROX_SIMPLE压缩水平方向,垂直方向,对角线方向的元素,值保留该方向的重点坐标,如果一个矩形轮廓只需4个点来保存轮廓信息;
cv2.CHAIN_APPROX_TC89_L1使用Teh-Chini chain近似算法
cv2.CHAIN_APPROX_TC89_KCOS使用Teh-Chini chain近似算法

四点透视变换 划出区域

imutils 是在 opencv 基础上的一个封装,达到更为简结的调用 opencv 接口的目的,它可以轻松的实现图像的平移,旋转,缩放,骨架化等一系列的操作;

安装方法: pip install imutils

直接用imutils包中的 four_point_transform 将需要的区域提取出来,灰度图用来处理;因为四个点确定的近似轮廓不一定是矩形,对图像中的ROI执行4点透视变换,并获得ROI的自上而下的“鸟瞰图”;

# 四点变换, 划出选择题区域

paper  = four_point_transform(img,  np.array(docCnt[0]).reshape(4,2))
warped = four_point_transform(gray, np.array(docCnt[0]).reshape(4,2))

# 四点变换, 划出准考证区域

ID_Area        = four_point_transform(img,  np.array(docCnt[1]).reshape(4,2))
ID_Area_warped = four_point_transform(gray, np.array(docCnt[1]).reshape(4,2))

# 四点变换, 划出科目区域

Subject_Area        = four_point_transform(img,  np.array(docCnt[2]).reshape(4,2))
Subject_Area_warped = four_point_transform(gray, np.array(docCnt[2]).reshape(4,2))
# cv2.imwrite('选择题区域.jpg', paper)

# cv2.imwrite('准考证区域.jpg', ID_Area)

# cv2.imwrite('科目区域.jpg', Subject_Area)

#输出一下四点透视变换分割的区域
fig = plt.figure(figsize=(16, 12))

# Subplot for original image

a=fig.add_subplot(2,3,1)
imgplot = plt.imshow(paper)
a.set_title('选择题区域')

a=fig.add_subplot(2,3,2)
imgplot = plt.imshow(ID_Area)
a.set_title('准考证区域')

a=fig.add_subplot(2,3,3)
imgplot = plt.imshow(Subject_Area)
a.set_title('科目区域')

image-20230512210642485

处理选择题区域

1、提取选项轮廓

# 处理选择题区域统计答题结果
thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

# fig = plt.figure(figsize=(5, 3))

# # Subplot for original image
# a=fig.add_subplot(1,1,1)
imgplot = plt.imshow(thresh, cmap='gray')
a.set_title('选择题区域-原始图片')

image-20230512210826359

thresh = cv2.resize(thresh, (2400, 2800), cv2.INTER_LANCZOS4)
paper  = cv2.resize(paper,  (2400, 2800), cv2.INTER_LANCZOS4)
warped = cv2.resize(warped, (2400, 2800), cv2.INTER_LANCZOS4)
# canny边缘检测
edged = cv2.Canny(thresh, 0, 255)

# 查找检测物体的轮廓信息
cnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
questionCnts = []
answers = []

# 对每一个轮廓进行循环处理
for c in cnts:
    # 计算轮廓的边界框,然后利用边界框数据计算宽高比
    (x,y,w,h) = cv2.boundingRect(c)
    ar = w/float(h)
    
    # 判断轮廓是否是答题框
    if w>=40 and h>=15 and ar>=1 and ar<=1.8:
        M = cv2.moments(c)
        cX = int(M["m10"]/M["m00"])
        cY = int(M["m01"]/M["m00"])
        questionCnts.append(c)
        answers.append((cX, cY))
        cv2.circle(paper, (cX,cY), 7, (255,255,255), -1)
# Create a figure

# fig = plt.figure(figsize=(8, 4))

# # Subplot for original image

# a=fig.add_subplot(1,1,1)

imgplot = plt.imshow(paper)
a.set_title('选择题区域-原始图片-边界框')

plt.show()

image-20230512210921188

ID_Answer = judge_point(answers, mode="point")
paper_drawcontours = cv2.drawContours(paper, questionCnts, -1, (255,0,0), 3)

ID_Answer[0]

image-20230512210949753

# Create a figure
# fig = plt.figure(figsize=(8, 4))

# # Subplot for original image
# a=fig.add_subplot(1,1,1)
imgplot = plt.imshow(paper)
a.set_title('选择题区域-原始图片-边界框')

plt.show()

image-20230512211121126

处理准考证号区域

1、提取填涂结果

因为准考证号区域有一部分是红色的,所以二值化之后结果没有答题区域那么理想

ID_Area_warped.shape

image-20230512211212808

# 处理选择题区域统计答题结果

ID_Area_warped_thresh = cv2.adaptiveThreshold(ID_Area_warped.copy(),255, cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,153,50)

# fig = plt.figure(figsize=(5, 3))

# # Subplot for original image
# a=fig.add_subplot(1,1,1)
imgplot = plt.imshow(ID_Area_warped_thresh, cmap='gray')
a.set_title('准考证号区域-原始图片')

image-20230512211230596

# 腐蚀处理 cv2.erode(), 
# (src: 输入图像对象矩阵,为二值化图像; 
#  kernel:进行腐蚀操作的核,可以通过函数getStructuringElement()获得;
#  anchor:锚点,默认为(-1,-1);
#  iterations:腐蚀操作的次数,默认为1;
#  borderType: 边界种类,有默认值;
#  borderValue:边界值,有默认值)
kernel=np.ones((3,3),dtype=np.uint8)
img_dilate=cv2.dilate(ID_Area_warped_thresh.copy(),kernel,iterations=3)

kernel=np.ones((5,5),dtype=np.uint8)
img_fushi=cv2.erode(img_dilate.copy(),kernel,iterations=3)

plt.imshow(img_fushi,cmap='gray')

image-20230512211250463

# ID_Area_warped_thresh = cv2.resize(thresh, (2400, 2800), cv2.INTER_LANCZOS4)
# ID_Area  = cv2.resize(ID_Area,  (2400, 2800), cv2.INTER_LANCZOS4)
# ID_Area_warped = cv2.resize(ID_Area_warped, (2400, 2800), cv2.INTER_LANCZOS4)

# canny边缘检测
edged = cv2.Canny(img_fushi, 0, 255)
plt.imshow(edged,cmap='gray')

image-20230512211618630

# 查找检测物体的轮廓信息
cnts, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
questionCnts = []
answers = []

# 对每一个轮廓进行循环处理
for c in cnts:
    # 计算轮廓的边界框,然后利用边界框数据计算宽高比
    (x,y,w,h) = cv2.boundingRect(c)
    ar = w/float(h)
    
    # 判断轮廓是否是答题框
    if w>=40 and h>=15 and ar>=1 and ar<=1.9:
        M = cv2.moments(c)
        cX = int(M["m10"]/M["m00"])
        cY = int(M["m01"]/M["m00"])
        questionCnts.append(c)
        answers.append((cX, cY))
        cv2.circle(ID_Area, (cX,cY), 7, (255,255,255), -1)
len(cnts)
#9
answers

image-20230512211702855

student_ID=judge_ID(answers, mode="ID")

image-20230512211719959

ID_Area_drawcontours = cv2.drawContours(ID_Area, questionCnts, -1, (255,0,0), 3)
# Create a figure
fig = plt.figure(figsize=(8, 4))

# Subplot for original image
a=fig.add_subplot(1,1,1)
imgplot = plt.imshow(ID_Area)
a.set_title('准考证号区域-原始图片-边界框')

plt.show()

image-20230512211737996

处理科目区域

Subject_Area_warped.shape

image-20230512211754606

Subject_Area_warped_thresh = cv2.adaptiveThreshold(    # src:需要进行二值化的一张灰度图像
    Subject_Area_warped.copy(), 
    # maxValue:满足条件的像素点需要设置的灰度值。(将要设置的灰度值)
    255,
    # adaptiveMethod:自适应阈值算法。可选ADAPTIVE_THRESH_MEAN_C 或 ADAPTIVE_THRESH_GAUSSIAN_C
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    # thresholdType:opencv提供的二值化方法,只能THRESH_BINARY或者THRESH_BINARY_INV
    cv2.THRESH_BINARY,
    # blockSize:要分成的区域大小,上面的N值,一般取奇数
    53,
    # C:常数,每个区域计算出的阈值的基础上在减去这个常数作为这个区域的最终阈值,可以为负数
    30)

# fig = plt.figure(figsize=(5, 3))

# # Subplot for original image
# a=fig.add_subplot(1,1,1)
imgplot = plt.imshow(Subject_Area_warped_thresh, cmap='gray')
a.set_title('科目区域-原始图片')

image-20230512211820536

kernel=np.ones((3,3),dtype=np.uint8)
img_dilate=cv2.dilate(Subject_Area_warped_thresh.copy(),kernel,iterations=8)

kernel=np.ones((5,5),dtype=np.uint8)
img_fushi=cv2.erode(img_dilate.copy(),kernel,iterations=3)

plt.imshow(img_fushi,cmap='gray')

image-20230512211836612

# canny边缘检测
edged = cv2.Canny(img_fushi, 0, 255)
plt.imshow(edged,cmap='gray')

image-20230512211851678

# 查找检测物体的轮廓信息
cnts, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
questionCnts = []
answers = []

# 对每一个轮廓进行循环处理
for c in cnts:
    # 计算轮廓的边界框,然后利用边界框数据计算宽高比
    (x,y,w,h) = cv2.boundingRect(c)
    ar = w/float(h)
    
    # 判断轮廓是否是答题框
    if w>=30 and h>=10 and ar>=1 and ar<=2.5:
        M = cv2.moments(c)
        cX = int(M["m10"]/M["m00"])
        cY = int(M["m01"]/M["m00"])
        questionCnts.append(c)
        answers.append((cX, cY))
        cv2.circle(Subject_Area, (cX,cY), 7, (255,255,255), -1)
len(cnts)
#1
answers

image-20230512211940936

questionCnts

image-20230512211957949

Subject_Area_drawcontours = cv2.drawContours(Subject_Area, questionCnts, -1, (255,0,0), 3)
fig = plt.figure(figsize=(8, 4))

# Subplot for original image
a=fig.add_subplot(1,1,1)
imgplot = plt.imshow(Subject_Area)
a.set_title('科目区域-原始图片-边界框')

plt.show()

image-20230512212013966

student_IDlist = judge_Subject(answers, mode="subject")

image-20230512212029230

得分导出结果

import pandas as pd
df = pd.read_excel("./答题卡识别-图片/answer.xlsx")
index_list = df[["题号"]].values.tolist()
true_answer_list = df[["答案"]].values.tolist()
index = []
true_answer = []
score = 0

# 去括号
for i in range(len(index_list)):
    index.append(index_list[i][0])
for i in range(len(true_answer_list)):
    true_answer.append(true_answer_list[i][0])

answer_index=[]
answer_option=[]
for answer in ID_Answer:
    answer_index.append(answer[0])
    answer_option.append(answer[1])
for i in range(len(index)):
    if answer_option[i]==true_answer[i]:
        score+=1
    if i+1==len(answer_option):
        break
info = {"试卷类型":["A"], "准考证号":[student_ID], "科目代号":[student_IDlist]}
df1 = pd.DataFrame(info)
df2 = pd.DataFrame(np.array(true_answer).transpose(), index=index, columns=["正确答案"])
df2["学生答案"] = ''

for i in range(len(answer_option)):
    df2["学生答案"] [i+1] = answer_option[i]
df2

image-20230512212101250

df2["总得分"] = ''
df2["总得分"][1] = score
with pd.ExcelWriter("test.xlsx") as writer:
    df1.to_excel(writer, index=False, sheet_name="type") 
    df2.to_excel(writer, sheet_name="score")
    writer._save()
    print("导出excel成功!")

image-20230512212148313

image-20230512212202551

封装成品

# 答题卡图像识别

import cv2
import numpy as np
import pandas as pd
from imutils.perspective import four_point_transform
# from matplotlib import pyplot as plt

# import matplotlib
# matplotlib.rc("font",family='AR PL UKai CN')
# 增强亮度
def imgBrightness(img1, c, b): 
    rows, cols= img1.shape
    blank = np.zeros([rows, cols], img1.dtype)
    # cv2.addWeighted 实现两副相同大小的图像融合相加
    rst = cv2.addWeighted(img1, c, blank, 1-c, b)
    return rst

def judgeX(x,mode):
    if mode=="point":
        if x<600:
            return int(x/100)+1
        elif x>600 and x<1250:
            return int((x-650)/100)+6
        elif x>1250 and x<1900:
            return int((x-1250)/100)+11
        elif x>1900:
            return int((x-1900)/100)+16
    elif mode=="ID":
        return int((x)/50)+1
        # return int((x-110)/260)+1
    elif mode=="subject":
        if x<1500:
            return False

def judgeY(y,mode):
    if mode=="point":
        if y%560>180 and y%560<240:
            return 'A'
        elif y%560>260 and y%560<320:
            return 'B'
        elif y%560>340 and y%560<380:
            return 'C'
        elif y%560>420 and y%560<480:
            return 'D'
        else:
            return False
    
    elif mode=="ID":
        if y>135:
            return int((y-135)/30) # return int((y-950)/180)
        else:
            return False
    
    elif mode=="subject":
        print (y, mode)
        if int((y-140)/25)==0:
            return  "政治"
        elif int((y-140)/25)==1:
            return  "语文"
        elif int((y-140)/25)==2:
            return  "数学"
        elif int((y-140)/25)==3:
            return  "物理"
        elif int((y-140)/25)==4:
            return  "化学"
        elif int((y-140)/25)==5:
            return  "英语"
        elif int((y-140)/25)==6:
            return  "历史"
        elif int((y-140)/25)==7:
            return  "地理"
        elif int((y-140)/25)==8:
            return  "生物"
        else:
            return "科目代号填涂有误"
        
def judge(x,y,mode):
    if judgeY(y,mode)!=False and judgeX(x,mode)!=False:
        if mode=="point":
            return (int(y/560)*20+judgeX(x,mode),judgeY(y,mode))
        elif mode=="ID":
            return (judgeX(x, mode), judgeY(y, mode))
    elif mode=="subject":
        print (y, mode)
        return judgeY(y, mode)
    else:
        return 0

def judge_point(answers, mode):
    IDAnswer = []
    for answer in answers:
        if(judge(answer[0],answer[1],mode)!=0):
            IDAnswer.append(judge(answer[0],answer[1],mode))
        else:
            continue
    IDAnswer.sort()
    return IDAnswer


def judge_ID(IDs, mode):
    student_ID=[]
    for ID in IDs:
        if(judge(ID[0],ID[1], mode)!=False):
            student_ID.append(judge(ID[0], ID[1], mode))
        else:
            continue
    student_ID.sort()
    print ('student_ID:', student_ID)
    return student_ID

def judge_Subject(subject, mode):
    return judge(subject[0][0], subject[0][1], mode)


# 边缘检测,获取轮廓信息
def get_contours(img_contours):
    '''
        输入:一张二值化的图片,图片
        输出:查找到的轮廓信息,列表list
    '''
    # canny边缘检测
    edged_ = cv2.Canny(img_contours, 0, 255)

    # 查找检测物体的轮廓信息
    cnts_, hierarchy = cv2.findContours(edged_, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # 确保至少有一个轮廓被找到
    if len(cnts_)>0:
        # 将轮廓按照大小排序
        cnts_ = sorted(cnts_, key=cv2.contourArea, reverse=True)
        return cnts_
    
    print ('获取轮廓信息出错')
    return 'error'


# 获取图像腐蚀、膨胀处理
def get_erode(img_warped, blockSize, c, iterations_dilate, iterations_erode):
    '''
        输入:
        输出:
    '''
    thresh_ = cv2.adaptiveThreshold(img_warped,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY,blockSize,c)

    #膨胀处理 cv2.dilate
    kernel = np.ones((3,3), dtype=np.uint8)
    pengzang_ = cv2.dilate(thresh_.copy(), kernel, iterations=iterations_dilate)

    kernel = np.ones((5,5), np.uint8)
    fushi_ = cv2.erode(pengzang_.copy(), kernel, iterations = iterations_erode)
    return fushi_


# 获取三个主要检测区域
def get_area(img, gray, blurred_border):
    '''
        输入:
        输出:
    '''
    # 查找检测物体的轮廓信息
    cnts = get_contours(blurred_border)
    docCnt = []
    count = 0

    # 对排序后的轮廓进行循环处理
    for c in cnts:

        # 获取近似的轮廓
        # 轮廓周长。也称为弧长
        peri = cv2.arcLength(c, True)
        # 一个重新采s样的轮廓,所以它仍然会返回一组 (x, y) 点
        approx = cv2.approxPolyDP(c,0.02*peri,True)

        #如果近似轮廓有四个顶点,那么就认为找到了答题卡
        if len(approx) == 4:
            docCnt.append(approx)
            count += 1
            if count==3:
                break
    
    # 四点变换, 划出选择题区域
    paper  = four_point_transform(img,  np.array(docCnt[0]).reshape(4,2))
    warped = four_point_transform(gray, np.array(docCnt[0]).reshape(4,2))

    # 四点变换, 划出准考证区域
    ID_Area        = four_point_transform(img,  np.array(docCnt[1]).reshape(4,2))
    ID_Area_warped = four_point_transform(gray, np.array(docCnt[1]).reshape(4,2))

    # 四点变换, 划出科目区域
    Subject_Area        = four_point_transform(img,  np.array(docCnt[2]).reshape(4,2))
    Subject_Area_warped = four_point_transform(gray, np.array(docCnt[2]).reshape(4,2))

    # 图像存储
    cv2.imwrite('选择题区域.png', paper)
    cv2.imwrite('准考证区域.png', ID_Area)
    cv2.imwrite('科目区域.png', Subject_Area)
    
    return paper, warped, ID_Area, ID_Area_warped, Subject_Area, Subject_Area_warped

# 处理选择题区域
def get_area_1(paper, warped):
    '''
    '''
    # 处理选择题区域统计答题结果
    thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

    # 图像放大
    thresh = cv2.resize(thresh, (2400, 2800), cv2.INTER_LANCZOS4)
    paper  = cv2.resize(paper,  (2400, 2800), cv2.INTER_LANCZOS4)
    warped = cv2.resize(warped, (2400, 2800), cv2.INTER_LANCZOS4)
    
    # 查找检测物体的轮廓信息
    cnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    questionCnts = []
    answers = []

    # 对每一个轮廓进行循环处理
    for c in cnts:
        # 计算轮廓的边界框,然后利用边界框数据计算宽高比
        (x,y,w,h) = cv2.boundingRect(c)
        ar = w/float(h)

        # 判断轮廓是否是答题框
        if w>=40 and h>=15 and ar>=1 and ar<=1.8:
            M = cv2.moments(c)
            cX = int(M["m10"]/M["m00"])
            cY = int(M["m01"]/M["m00"])
            questionCnts.append(c)
            answers.append((cX, cY))
            cv2.circle(paper, (cX,cY), 7, (255,255,255), -1)
    print ('检测到的选择题目信息长度:', len(answers))
    
    # 根据像素坐标获取选择题答案
    ID_Answer = judge_point(answers, mode="point")
    # 将像素坐标位置画到原图上
    paper_drawcontours = cv2.drawContours(paper, questionCnts, -1, (255,0,0), 3)
    cv2.imwrite('选择题区域识别结果.jpg', paper)
    
    return ID_Answer

# 处理准考证号区域
def get_area_2(ID_Area, ID_Area_warped, blockSize, c, iterations_dilate, iterations_erode):
    fushi = get_erode(ID_Area_warped, blockSize, c, iterations_dilate, iterations_erode)
    cnts, hierarchy = cv2.findContours(fushi.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

    ID_Cnts=[]
    IDs=[]
    # 对每一个轮廓进行循环处理
    for c in cnts:
        # 计算轮廓的边界框,然后利用边界框数据计算宽高比
        (x,y,w,h) = cv2.boundingRect(c)
        ar = w/float(h)
        # print (w, h, ar, y, x)

        # 判断轮廓是否是答题框
        if w>=15 and w<300 and h>=5 and ar>=1 and ar<=3.0 and y>100:
            M = cv2.moments(c)
            cX = int(M["m10"]/M["m00"])
            cY = int(M["m01"]/M["m00"])
            if cY<265:
                ID_Cnts.append(c)
                IDs.append((cX, cY))
                cv2.circle(ID_Area, (cX,cY), 7, (255,255,255), -1)
    
    
    # 从像素坐标转换成学生的准考证号
    student_IDlist = judge_ID(IDs, mode="ID")
    # 判断准考证号是否有误
    ID_index = 1
    student_ID=""
    for tuple_ in student_IDlist:
        if tuple_[0] == ID_index:
            student_ID += str(tuple_[1])
            ID_index += 1
        else:
            student_ID = "准考证号填涂有误"
            break
    # 像素坐标信息画到原图上
    ID_Area_drawContours = cv2.drawContours(ID_Area, ID_Cnts, -1, (255,0,0), 3)
    
    cv2.imwrite('准考证区域识别结果.jpg', ID_Area)
    return student_ID

# 科目代码区域
def get_area_3(Subject_Area, Subject_Area_warped, blockSize, c, iterations_dilate, iterations_erode):
    '''
        输入:
        输出:
    '''
    Subject_thresh_fushi = get_erode(Subject_Area_warped, blockSize, c, iterations_dilate, iterations_erode)
    cnts, hierarchy = cv2.findContours(Subject_thresh_fushi.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

    ID_Cnts=[]
    IDs=[]
    # 对每一个轮廓进行循环处理
    for c in cnts:
        # 计算轮廓的边界框,然后利用边界框数据计算宽高比
        (x,y,w,h) = cv2.boundingRect(c)
        ar = w/float(h)

        # 判断轮廓是否是答题框
        if w>=15 and w<150 and h>=5 and ar>=1 and ar<=3.0 and x>50:
            M = cv2.moments(c)
            cX = int(M["m10"]/M["m00"])
            cY = int(M["m01"]/M["m00"])
            if cY<2650:
                ID_Cnts.append(c)
                IDs.append((cX, cY))
                cv2.circle(Subject_Area, (cX,cY), 7, (255,255,255), -1)
                
    student_IDlist = judge_Subject(IDs, mode="subject")
    cv2.imwrite('科目代码区域识别结果.jpg', Subject_Area)
    return student_IDlist

# 存储 表格
def save_csv(answer_path, save_path, ID_Answer, student_ID, student_IDlist):
    df = pd.read_excel(answer_path)
    index_list = df[["题号"]].values.tolist()
    true_answer_list = df[["答案"]].values.tolist()
    index = []
    true_answer = []
    score = 0

    # 去括号
    for i in range(len(index_list)):
        index.append(index_list[i][0])
    for i in range(len(true_answer_list)):
        true_answer.append(true_answer_list[i][0])

    answer_index=[]
    answer_option=[]
    for answer in ID_Answer:
        answer_index.append(answer[0])
        answer_option.append(answer[1])
    for i in range(len(index)):
        if answer_option[i]==true_answer[i]:
            score+=1
        if i+1==len(answer_option):
            break

    # 写入表格
    info = {"试卷类型":["A"], "准考证号":[student_ID], "科目代号":[student_IDlist]}
    df1 = pd.DataFrame(info)
    df2 = pd.DataFrame(np.array(true_answer).transpose(), index=index, columns=["正确答案"])
    df2["学生答案"] = ''

    for i in range(len(answer_option)):
        df2["学生答案"] [i+1] = answer_option[i]

    df2["总得分"] = ''
    df2["总得分"][1] = score
    with pd.ExcelWriter(save_path) as writer:
        df1.to_excel(writer, index=False, sheet_name="type") 
        df2.to_excel(writer, sheet_name="score")
        writer.save()
        print("导出excel成功!")
    
    return 


# 答题卡图片处理主要步骤
def img_1(image_path, answer_path, save_path):
    # 读取图片 RGB \ BGR
    img = cv2.imread(image_path)

    # 转换为灰度图 COLOR_BGR2GRAY、COLOR_BGR2RGB
    gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

    # 高斯滤波 中值滤波
    blurred_gauss = cv2.GaussianBlur(gray, (3,3), 0)

    # 增强亮度
    blurred_bright = imgBrightness(blurred_gauss, 1.5, 3)

    # 自适应二值化
    blurred_threshold = cv2.adaptiveThreshold(blurred_bright, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 51, 2)

    # 用来给图片添加边框
    blurred_border = cv2.copyMakeBorder(blurred_threshold, 5, 5, 5, 5, cv2.BORDER_CONSTANT, value=(255,255,255))
    
    # 获取主要区域
    paper, warped, ID_Area, ID_Area_warped, Subject_Area, Subject_Area_warped = get_area(img, gray, blurred_border)
    
    # 处理选择题区域
    ID_Answer = get_area_1(paper, warped)
    
    # 处理准考证号区域
    student_ID = get_area_2(ID_Area, ID_Area_warped, 53, 30, 3, 3)
    
    # 科目代码区域
    student_IDlist = get_area_3(Subject_Area, Subject_Area_warped, 53, 30, 4, 3)
    
    # 存储表格
    save_csv(answer_path, save_path, ID_Answer, student_ID, student_IDlist)
    
    return 
    
if __name__ == '__main__':
    image_path = 'images/5.png'
    answer_path = "answer.xlsx"
    save_path = "test.xlsx"
    
    img_1(image_path, answer_path, save_path)
    print('预测结束')

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

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

相关文章

【新星计划-2023】TCP三次握手和四次挥手讲解

关于TCP三次握手和四次挥手&#xff0c;各位想必在读大学的时候或者是在面试的时候一定遇到过&#xff0c;三次握手和四次挥手本身是不是太难的&#xff0c;但它容易忘&#x1f61e;&#xff0c;今天我就在这里给大家讲解一下三次握手与四次挥手。 一、三次挥手 TCP三次握手建…

我,大厂P9,找不到工作

作者| 老W 编辑| Emma 来源| 技术领导力(ID&#xff1a;jishulingdaoli) K哥写在前面的话&#xff1a;这是一位读者投稿&#xff0c;读者老W讲述了自己从大厂P9失业后、再就业的故事&#xff0c;并总结了自己的心路历程&#xff0c;很真实的记录与思考&#xff0c;值得大家借…

SensorX2car:在道路场景下的完成传感器到车体坐标系标定

文章&#xff1a;SensorX2car: Sensors-to-car calibration for autonomous driving in road scenarios 作者&#xff1a;Guohang Yan, Zhaotong Luo, Zhuochun Liu and Yikang Li 编辑&#xff1a;点云PCL 代码&#xff1a;https://github.com/OpenCalib/SensorX2car 作者单位…

IDEA+maven+Springboot工程创建超详细过程示例

IDEAmavenSpringboot工程创建超详细过程示例 1、IDEA、Maven下载安装及IDEA配置Maven教程2、IDEAmavenSpringboot的web工程创建示例2.1 SpringBoot简介2.2 maven形式创建sprintboot工程2.3 导入相关依赖2.4 创建SpringBoot启动类2.5 创建 Controller2.6 启动项目2.7 配置端口信…

springboot第22集:security,Lombok,token,redis

Spring Security是一个基于Spring框架的权限管理框架&#xff0c;用于帮助应用程序实现身份验证和授权功能。它可以为Web应用程序、REST API和方法级安全性提供支持&#xff0c;并支持各种认证方式。 Spring Security最初是Acegi Security的前身&#xff0c;但由于其配置繁琐而…

【第六期】Apache DolphinScheduler 每周 FAQ 集锦

点击蓝字 关注我们 摘要 为了让 Apache DolphinScheduler 的广大用户和爱好者对于此项目的疑问得到及时快速的解答&#xff0c;社区特发起此次【每周 FAQ】栏目&#xff0c;希望可以解决大家的实际问题。 关于本栏目的要点&#xff1a; 本栏目每周将通过腾讯文档&#xff08;每…

用排列组合来编码通信(六)——魔术《5张牌的预言》的魔术拓展之《My Fitch Four Glory》...

早点关注我&#xff0c;精彩不错过&#xff01; 在上一篇中&#xff0c;我们介绍了《5张牌的预言》这个魔术的一个精彩的扩展表演《Eigens Value》&#xff0c;把这个魔术和数学性质的结合做到了极致&#xff0c;相关内容请戳&#xff1a; 用排列组合来编码通信&#xff08;五&…

第二十二章 Unity 光照贴图

光照贴图过程将预先计算场景中静态物体表面的亮度&#xff0c;并将结果存储在称为“光照贴图”的纹理中供以后使用。光照贴图可以包含直接光照和间接光照&#xff0c;以及阴影效果。但是&#xff0c;烘焙到光照贴图中的数据无法在运行时更改&#xff0c;这就是为什么移动静态物…

Angular开发之——Angular介绍(01)

一 概述 Angular是什么AngularJS和Angular关系Angular特性Angular的发展历史Angular学习建议 二 Angular是什么 Angular(读音[ˈŋɡjələr])是一套用于构建用户界面的javaScript框架。由Google开发和维护&#xff0c;主要被用来开发单页面应用程序类似于Vue.js(MVVM数据驱动…

视觉检测技术在图书生产缺陷控制中的应用

在过去我们一直向大家展示的是视觉检测应用在重工业制造之上&#xff0c;让很多人误以为这种新兴的检测技术更加倾向于重工业&#xff0c;或者说因为成本因素&#xff0c;这项技术对目前的轻工业来说更加的不友好&#xff0c;其实并不是这样的。 轻工业我们之所以很少的提机器…

docker入门和docker应用场景,镜像制作,服务编排,docker私服

一、简介 docker解决了什么问题docker和虚拟机的区别在CentOS7里安装docker 1. docker简介 我们写的代码会接触到好几个环境&#xff1a;开发环境、测试环境以及生产环境等等。多种环境去部署同一份代码&#xff0c;由于环境原因往往会出现软件跨环境迁移的问题&#xff08;也就…

黏包和半包

黏包和半包 黏包&#xff1a; Slf4j public class HelloWorldServer {public static void main(String[] args) {NioEventLoopGroup boss new NioEventLoopGroup();NioEventLoopGroup worker new NioEventLoopGroup();try {ServerBootstrap serverBootstrap new ServerBoo…

open3d教程(二):可视化三维模型,并转换成点云(Python版本)

1、三维模型获取 可以自己用建模软件建立一个模型从free3d免费下载 2、关键函数 open3d.visualization.draw_geometries 参数&#xff1a; geometry_list(List[open3d.geometry.Geometry])&#xff1a;要可视化的几何体列表.window_name(str, optional, defaultOpen3D)&…

Linux环境下编程遇到“fatal error:stdio.h:没有那个文件或目录”错误解决办法

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天总结一下linux环境下如何解决一个常见的问题&#xff0c;也就是“fatal error:stdio.h:没有那个文件或目录”错误。 不少初学者在linux环境下用gcc编译C语言时&#xff0c;经常会遇到这个问题。 比如当…

工具推荐二

&#xff08;点击上方公众号&#xff0c;可快速关注&#xff09; GUI框架 我是个后端程序员&#xff0c;使用GUI框架的机会很少&#xff0c;主要用来编写个人或工作中的一些小工具上&#xff0c;经验有限&#xff0c;仅供参考。 Tk框架这个框架tcl语言自带&#xff0c;在python…

海报图片生成服务在狐友的落地实践

本文字数&#xff1a;22817字 预计阅读时间&#xff1a;58分钟 项目背景 狐友作为搜狐的一款社交产品&#xff0c;在流量传播上有着旺盛的需求点。而在流量传播所需的众多载体之中&#xff0c;海报图片以其简单的分享形式、可定制的视觉体验、自带二维码识别导流等特点&#xf…

入门与 Follow GPT 的路径分析:LLM 道阻且长,行则将至

动手点关注 干货不迷路 本文只用于技术交流&#xff0c;仅代表作者个人观点。 作为 CEO&#xff0c;Sam 将 OpenAI 的内部氛围组织的很好&#xff0c;有位 OpenAI 的前员工告诉拾象团队&#xff0c;当 2018 年 GPT-2 的论文被驳回时&#xff0c;Sam 在团队周会上将拒信的内容朗…

ONES X 高测股份|用数字化,重构新材料企业的研发管理体系

近日&#xff0c;ONES 签约高硬材料切割的领军企业——高测股份&#xff0c;助力高测股份建立有效、规范的研发测试管理体系&#xff0c;实现项目管理、测试管理、知识库管理、工时管理、组织效能管理等端到端的研发管理&#xff0c;提升测试效率和产品交付质量&#xff0c;并进…

用Python+OpenCV+Yolov5+PyTorch开发的车牌识别软件(包含训练数据)

目录 演示视频 软件使用说明 软件设计思路 演示视频 这是一个可以实时识别车牌的软件&#xff0c;支持图片和视频识别&#xff0c;以下是软件的演示视频。 车牌识别软件 点击查看代码购买地址 软件使用说明 1. 下载源码后&#xff0c;首先安装依赖库。项目所用到的依赖库已…

bash shell 基础命令

章节目录&#xff1a; 一、浏览文件系统1.1 Linux 文件系统1.2 遍历目录 二、列出文件和目录三、处理文件3.1 创建文件3.2 复制文件3.3 命令行补全3.4 链接文件3.5 文件重命名3.6 删除文件 四、管理目录4.1 创建目录4.2 删除目录 五、查看文件内容5.1 查看文件类型5.2 查看整个…