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:
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 删除。