首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >A method for detecting the positioning points on an answer sheet

A method for detecting the positioning points on an answer sheet

原创
作者头像
Swing Dunn
发布2025-10-15 17:31:08
发布2025-10-15 17:31:08
860
举报
文章被收录于专栏:答题卡识别答题卡识别

In the general design of an answer sheet, there is always a rectangular black block at each corner of the top, bottom, left and right, which is used for positioning the answer sheet and limiting the layout area of the answer sheet. Therefore, the purpose of detecting the positioning points is to identify rectangles of the target size in the target area for subsequent correction of the answer sheet.

Based on the number [n] of identifiable positioning blocks, there are the following correction methods:

if n == 1:Suppose there is only translation transformation between the template and the user image

if n ==2:Suppose the transformation between the template and the user image only involves scaling and panning

if n == 3:Suppose the transformation between the template and the user image only includes scaling, panning and rotation

Although three are sufficient for actual correction, given that this is just one case, we will try to detect four positioning blocks as much as possible; otherwise, there will be disputes over the completeness of the test paper.

This article still locates the target rectangle through contour detection. However, based on the characteristics of the answer sheet printing, especially when the printing quality is poor, some reasonable threshold Settings will be adopted.

When recognition is not possible, the results will all be displayed as binary in original image, which will reflect the issue of printing quality.The successfully identified positioning points will be marked with red rectangles on the original image.

The final experimental results are as follows blow:

the result of adaptive threshold binarization:

the result of binarization with a fixed 178 threshold:

Source code:

代码语言:txt
复制

import cv2
import os

global_extendsize = 40

# 读取目标图像和模板图像
def show_image(img):
    cv2.namedWindow("1",cv2.WINDOW_FREERATIO)
    cv2.imshow("1",img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

def detect_archor_block(gray, temp_archor):
    #模板定位点的位置和大小
    height, width = gray.shape[:2]
    ar_lt_pt = (temp_archor[0],temp_archor[1])
    ar_width = temp_archor[2]
    ar_height = temp_archor[3]
    ar_rb_pt = (ar_lt_pt[0] + ar_width, ar_lt_pt[1] + ar_height)

    # cv2.rectangle(gray,ar_lt_pt ,ar_rb_pt, (0,0,255),2)
    #向各个方向外扩75个像素
    expand_range_lt = (ar_lt_pt[0] - global_extendsize if ar_lt_pt[0] - global_extendsize > 0 else 0, ar_lt_pt[1] - global_extendsize if ar_lt_pt[1] - global_extendsize > 0 else 0)
    expand_range_rb = (ar_rb_pt[0] + global_extendsize if ar_rb_pt[0] + global_extendsize < width else width  , ar_rb_pt[1] + global_extendsize if ar_rb_pt[1] + global_extendsize < height else height)
    # cv2.rectangle(gray,expand_range_lt ,expand_range_rb, (0,0,255),2)
    
    aim_range_image = gray[expand_range_lt[1]:expand_range_rb[1], expand_range_lt[0]: expand_range_rb[0]]

    display_aim_img = cv2.cvtColor(aim_range_image, cv2.COLOR_GRAY2BGR)
    #show_image(aim_range_image) 

    #预处理
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    aim_range_image = clahe.apply(aim_range_image)
    aim_range_image = cv2.GaussianBlur(aim_range_image,(3,3),0)
    #考虑到印刷的情况  和答题卡周边一般为不会被污染的区域  所以这里采用一个较高的阈值进行二值化 而不要采用自适应的阈值
    _,binary = cv2.threshold(aim_range_image, 178,255, cv2.THRESH_BINARY_INV)
    #_,binary = cv2.threshold(aim_range_image, 56,255, cv2.THRESH_BINARY_INV )
    # 形态学闭操作
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 9))
    closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)

    #show_image(closed)
    binary = closed.copy()
    #轮廓搜索
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    rects = []
    for contour in contours:
        rect = cv2.boundingRect(contour)
        if rect[2] > ar_width - 10 and rect[3] > ar_height - 10 and rect[2] < ar_width + 16 and rect[3] < ar_height + 16:        
            # 使用 approxPolyDP 近似轮廓
            epsilon = 0.02 * cv2.arcLength(contour, True)
            approx = cv2.approxPolyDP(contour, epsilon, True)
            if len(approx) >= 2:
                rects.append(rect)
                #cv2.rectangle(display_aim_img,(rect[0],rect[1]) ,(rect[0] + rect[2],rect[1] + rect[3]), (0,0,255),2)
    #找出最接近目标矩形
    aim_rect = [0,0,0,0]
    if len(rects) >0 :
        min_data = 999999
        for current_rect in rects:
            if abs(current_rect[2] + current_rect[3] - ar_width - ar_height) < min_data:
                min_data = abs(current_rect[2] + current_rect[3] - ar_width - ar_height)
                # 更新 aim_rect
                aim_rect[0] = current_rect[0]
                aim_rect[1] = current_rect[1]
                aim_rect[2] = current_rect[2]
                aim_rect[3] = current_rect[3]
    find : bool =  False
    if(sum(aim_rect) > 0):
        find = True
        result_rect = (expand_range_lt[0] +  aim_rect[0], expand_range_lt[1] +  aim_rect[1],aim_rect[2], aim_rect[3])
    else:
        find = False
        result_rect = (expand_range_lt[0],  expand_range_lt[1], expand_range_rb[0],  expand_range_rb[1])

    #cv2.rectangle(display_aim_img,(aim_rect[0],aim_rect[1]) ,(aim_rect[0] + aim_rect[2],aim_rect[1] + aim_rect[3]), (0,255,0),2) 
    #show_image(display_aim_img)
    return find, result_rect, binary

def main():
    folder_path = './imgs4'
    #模板定位点信息
    temp_block_info =[(73, 126, 47, 30), (1526, 124, 47, 30),(76, 2166, 46, 31),(1530, 2160, 49, 32)]
    paper_num = 0
    error_paper_num = 0
    
    # 递归遍历文件夹
    for root, dirs, files in os.walk(folder_path):
        for filename in files:
            if filename.lower().endswith('.jpg'):
                file_path = os.path.join(root, filename)
                ori_img = cv2.imread(file_path, cv2.IMREAD_ANYCOLOR)  # 目标图像
                if len(ori_img.shape) == 3 and ori_img.shape[2] == 3:
                    gray_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2GRAY)
                elif len(ori_img.shape) == 2:
                    gray_img = ori_img.copy()
                else:
                    print('error: bad image')
                    return                

                #show_image(img)
                paper_num += 1
                lt_archor = temp_block_info[0]
                rt_archor = temp_block_info[1]
                lb_archor = temp_block_info[2]
                rb_archor = temp_block_info[3]

                # cv2.rectangle(ori_img,(lt_archor[0], lt_archor[1]) ,(lt_archor[0] +lt_archor[2] ,lt_archor[1] +lt_archor[3]), (0,0,255),2)
                # show_image(ori_img)
                #左上
                print(f"检测第{paper_num}试卷")
                exist_nofound = False

                find, re_lt_rect ,aim_binary =  detect_archor_block(gray_img, lt_archor)   
                if(find):
                    cv2.rectangle(ori_img,(re_lt_rect[0], re_lt_rect[1]) ,(re_lt_rect[0] +re_lt_rect[2] ,re_lt_rect[1] +re_lt_rect[3]), (0,0,255),2)
                else:
                    display_binary= cv2.cvtColor(aim_binary, cv2.COLOR_GRAY2BGR)
                    ori_img[re_lt_rect[1]:re_lt_rect[3], re_lt_rect[0]:re_lt_rect[2]] = display_binary
                    exist_nofound = True
                    print(f"第{paper_num}试卷 左上定位点缺失") 
                    
            
                #右上
                find, re_rt_rect ,aim_binary =  detect_archor_block(gray_img, rt_archor)
                if(find):
                    cv2.rectangle(ori_img,(re_rt_rect[0], re_rt_rect[1]) ,(re_rt_rect[0] +re_rt_rect[2] ,re_rt_rect[1] +re_rt_rect[3]), (0,0,255),2)
                else:
                    display_binary= cv2.cvtColor(aim_binary, cv2.COLOR_GRAY2BGR)
                    ori_img[re_rt_rect[1]:re_rt_rect[3], re_rt_rect[0]:re_rt_rect[2]] = display_binary
                    exist_nofound = True
                    print(f"第{paper_num}试卷 右上定位点缺失") 

                #左下
                find, re_lb_rect ,aim_binary =  detect_archor_block(gray_img, lb_archor)
                if(find):
                    cv2.rectangle(ori_img,(re_lb_rect[0], re_lb_rect[1]) ,(re_lb_rect[0] +re_lb_rect[2] ,re_lb_rect[1] +re_lb_rect[3]), (0,0,255),2)
                else:
                    display_binary= cv2.cvtColor(aim_binary, cv2.COLOR_GRAY2BGR)
                    ori_img[re_lb_rect[1]:re_lb_rect[3], re_lb_rect[0]:re_lb_rect[2]] = display_binary
                    exist_nofound = True
                    print(f"第{paper_num}试卷 左下定位点缺失") 

                #右下
                find, re_rb_rect ,aim_binary =  detect_archor_block(gray_img, rb_archor)
                if(find):
                    cv2.rectangle(ori_img,(re_rb_rect[0], re_rb_rect[1]) ,(re_rb_rect[0] +re_rb_rect[2] ,re_rb_rect[1] +re_rb_rect[3]), (0,0,255),2)
                else:
                    display_binary= cv2.cvtColor(aim_binary, cv2.COLOR_GRAY2BGR)
                    ori_img[re_rb_rect[1]:re_rb_rect[3], re_rb_rect[0]:re_rb_rect[2]] = display_binary
                    exist_nofound = True
                    print(f"第{paper_num}试卷 右下定位点缺失") 

                if exist_nofound:
                    error_paper_num += 1
                    show_image(ori_img)

                print(f"已检测{paper_num}张试卷, {error_paper_num}张试卷存在定位点检测缺失, 识别率{1 - error_paper_num / paper_num}")
                       
                print()

if __name__ == '__main__':
    main()

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档