python利用opencv如何實(shí)現(xiàn)答題卡自動(dòng)判卷
1、設(shè)定答題卡模板
該圖像為答題卡的答題區(qū)域,黑色邊框是為了能夠在各種環(huán)境中輕易的檢測,左部分和上部分的黑色矩形,是為能夠定位到答題選項(xiàng)的坐標(biāo)而設(shè)置,同時(shí)題目數(shù)量為20×3共60道選擇題,在進(jìn)行批改試卷之前,需要手動(dòng)輸入該次考試的正確答案作為模板來對(duì)識(shí)別的內(nèi)容進(jìn)行比較判分。
2、讀取答題卡圖像并對(duì)圖像進(jìn)行灰度化處理
# 最大值法求圖像灰度值 def graying(image): h, w = image.shape[0], image.shape[1] gray = np.zeros((h, w), np.uint8) for i in range(h): for j in range(w): gray[i, j] = max(image[i,j][0], image[i,j][1], image[i,j][2]) return gray
3、高斯模糊圖像去噪點(diǎn)
gray = cv2.GaussianBlur(gray, (3, 3), 0)
4、使用大津法二值分割圖像
經(jīng)過上一步操作后,答題卡的前背景分明,已經(jīng)能夠輕易將標(biāo)識(shí)矩陣和答題區(qū)域涂改信息和背景分離開來,接下來需要將圖像的標(biāo)識(shí)矩陣和答題區(qū)域的涂改信息提取出來,需要進(jìn)一步規(guī)劃數(shù)字圖像信息,二值化圖像能使圖像的數(shù)據(jù)量大大減少,既保留原有的數(shù)字信息,又能將無用的數(shù)據(jù)舍去,將原本數(shù)值范圍為0-255的圖像信息分割成像素值為0或255的二值圖像,在這里0值代表背景,無用且不需要處理的信息,255表示目標(biāo)信息(標(biāo)識(shí)矩陣和答題區(qū)域的涂改信息),其算法原理非常簡單,圖像中像素值大于閾值時(shí)為255,小于閾值時(shí)為0。在二值化時(shí),需要確定一個(gè)閾值,而這個(gè)閾值人為來定義是無法隨著環(huán)境變換隨時(shí)處于最優(yōu)狀態(tài),在這里我們使用1979年由學(xué)者大津提出的對(duì)圖像分割的高效算法來處理。大津法算法原理:
有假設(shè)如下:
- u: 圖像像素值平均值
- g: 圖像類間方差
- w0: 圖像背景像素點(diǎn)數(shù)占圖像的比例
- u0: 圖像背景像素點(diǎn)的平均值
- w1: 圖像前景像素點(diǎn)數(shù)占圖像的比例
- u1: 圖像前景像素點(diǎn)的平均值
算法公式為:
將第一個(gè)公式代入第二個(gè)得:
5、使用開運(yùn)算去噪點(diǎn)
此時(shí)已經(jīng)得到較為完美的預(yù)處理圖,但是不難發(fā)現(xiàn),我們答題卡有一小塊干擾像素。在實(shí)際情況中,這種干擾信息是有可能出現(xiàn)的,且大小與清晰度并沒有固定范圍,因此,在判卷之前,需要盡可能的將這種干擾信息去除,前面的各種圖像預(yù)處理方法僅僅是將圖像的前景和背景分離提取出定位標(biāo)識(shí)信息和涂改信息,面對(duì)這樣的情況,選擇使用機(jī)器視覺中
常用的開運(yùn)算方法處理圖像可以得到較好的效果。
開運(yùn)算:先對(duì)圖像進(jìn)行腐蝕操作,再進(jìn)行膨脹操作,就是開運(yùn)算操作,能夠消除細(xì)小的物體,將兩個(gè)物體的細(xì)小的連接處去除從而分離兩個(gè)物體,且擁有平滑邊界的效果,被廣泛應(yīng)用于去除圖像噪聲。
腐蝕算法原理:
步驟1:定義一個(gè)卷積核B,卷積核可以是隨意的大小與形狀,但通常是帶參考點(diǎn)的正方形或圓形,作為腐蝕的模板。
步驟2:將卷積核與原圖像進(jìn)行卷積操作,計(jì)算原圖像包裹卷積核B的區(qū)域的像素最小值,這個(gè)區(qū)域則作為腐蝕操作后的結(jié)果。
例如有原圖像A,卷積核B
則經(jīng)過腐蝕算法操作之后可得實(shí)驗(yàn)結(jié)果為如下:
實(shí)驗(yàn)發(fā)現(xiàn),右上角的小塊圖像噪聲被腐蝕掉了,同時(shí),下方的像素塊被腐蝕了一圈且兩塊被分割開來,為了盡量減少圖像的信息被過度腐蝕掉,接下來一步需要使用膨脹算法,將圖像像素膨脹回來,盡可能去掉圖像噪聲的同時(shí),也減少圖像信息的過度減少。
膨脹算法原理:
步驟1:定義一個(gè)卷積核B,開運(yùn)算時(shí)直接使用腐蝕操作時(shí)使用的卷積核B。
步驟2:將卷積核B與原圖像進(jìn)行卷積操作,計(jì)算原圖像包裹卷積核B的區(qū)域的像素最大值,這個(gè)區(qū)域則作為膨脹操作后的結(jié)果。使用腐蝕操作后的結(jié)果來進(jìn)行膨脹操作,實(shí)驗(yàn)效果如下:
觀察開運(yùn)算操作的前后對(duì)比,可以得知,右上角的噪聲完全去除的同時(shí),與原圖像信息相比較,僅僅有五個(gè)像素點(diǎn)被去除,是完全可以接受的。
開運(yùn)算操作實(shí)際實(shí)驗(yàn)效果如下:
步驟進(jìn)行到這,發(fā)現(xiàn)右下角那塊較大的圖像噪聲,仍舊無法消除,主要原因是該噪聲較大,形狀大小與顏色深度與目標(biāo)信息相似,在準(zhǔn)確保留目標(biāo)信息的情況下難以將其分割開來,因此接下來選擇通過定位圖像信息來排除該圖像噪聲,對(duì)其不進(jìn)行操作。
代碼網(wǎng)上找的,忘記從哪抄的了,百度一下都有。。。
# 圖像腐蝕 def etch(img, size): h=img.shape[0] w=img.shape[1] img1=np.zeros((h,w),np.uint8) for i in range (1,h-1): for j in range (1,w-1): min=img[i,j] for k in range (i-size,i+size): for l in range (j-size,j+size): if k<0|k>=h-1|l<0|l>=w-1:continue if img[k,l]<min:min=img[k,l] img1[i,j]=min return img1 # 圖像膨脹 def expand(img, size): h=img.shape[0] w=img.shape[1] img1=np.zeros((h,w),np.uint8) for i in range (1,h-1): for j in range (1,w-1): max=img[i,j] for k in range (i-size,i+size): for l in range (j-size,j+size): if k<0|k>=h-1|l<0|l>=w-1:continue if img[k,l]>max: max=img[k,l] img1[i,j]=max return img1 # 開運(yùn)算 def opening(image, size): etch_img = etch(image, size) expand_img = expand(etch_img, size) return expand_img
6、使用canny邊緣檢測算法
canny邊緣檢測算法是一種運(yùn)用非常廣泛的算法,由john F.Canny在1986年提出的,一種多階段的算法:
步驟1:對(duì)圖像進(jìn)行去噪
步驟2:計(jì)算圖像的強(qiáng)度梯度
步驟3:在邊緣上進(jìn)行非極大值抑制
步驟4:對(duì)檢測得到的邊緣使用雙閾值排查
步驟5:分析邊緣之間的連接
通過這一系列的操作后便可得到圖像內(nèi)容里的邊緣信息,我們前面已經(jīng)對(duì)圖像進(jìn)行了深度的去噪操作,已經(jīng)將大部分噪音完全清除,接下來的操作應(yīng)該是區(qū)分定位區(qū)域和判卷區(qū)域的坐標(biāo),來對(duì)其進(jìn)行判斷處理,這一步只是為了觀察圖像的邊緣信息,屬于測試步驟,在實(shí)際的運(yùn)用中,并不會(huì)使用該步驟來處理圖像。
實(shí)驗(yàn)結(jié)果如下:
7、篩選答題區(qū)域輪廓,透視變換矯正目標(biāo)區(qū)域
通過輪廓檢測可以計(jì)算多邊形的外界,在這里我們需要檢測出答題卡涂改區(qū)域的黑色邊框位置,定位得到邊框的四個(gè)頂點(diǎn)坐標(biāo),再對(duì)目標(biāo)進(jìn)行透視矯正操作。到這一步驟,已經(jīng)得到矯正后的答題區(qū)域,接下來需要對(duì)圖像的答題區(qū)域進(jìn)行定位判斷。
具體實(shí)驗(yàn)效果如下:
# 透視變換 from imutils.perspective import four_point_transform def wPs(image, points): warped = four_point_transform(image, points) return warped
最后判斷黑色集中的地方就可以判斷選項(xiàng)了
使用攝像頭實(shí)時(shí)判卷部分
調(diào)用攝像頭部分的處理方式是對(duì)圖像處理里使用的方法的一個(gè)總和。最終該系統(tǒng)的實(shí)時(shí)判卷,準(zhǔn)確率達(dá)到百分之百,并且是在環(huán)境較差的的情況下進(jìn)行判卷。
全部代碼如下
import cv2 import numpy as np # 選取區(qū)域去除邊緣 dist = 5 # 畫圖線粗度 line_w = 2 # 畫筆顏色 red = (0, 0, 255) green = (0, 255, 0) blue = (255, 0, 0) # 高斯模糊算法 #防止顏色值超出顏色取值范圍(0-255) # 開運(yùn)算,先腐蝕,后膨脹 # 圖像腐蝕 def etch(img, size): h=img.shape[0] w=img.shape[1] img1=np.zeros((h,w),np.uint8) for i in range (1,h-1): for j in range (1,w-1): min=img[i,j] for k in range (i-size,i+size): for l in range (j-size,j+size): if k<0|k>=h-1|l<0|l>=w-1:continue if img[k,l]<min:min=img[k,l] img1[i,j]=min return img1 # 圖像膨脹 def expand(img, size): h=img.shape[0] w=img.shape[1] img1=np.zeros((h,w),np.uint8) for i in range (1,h-1): for j in range (1,w-1): max=img[i,j] for k in range (i-size,i+size): for l in range (j-size,j+size): if k<0|k>=h-1|l<0|l>=w-1:continue if img[k,l]>max: max=img[k,l] img1[i,j]=max return img1 # 開運(yùn)算 def opening(image, size): etch_img = etch(image, size) expand_img = expand(etch_img, size) return expand_img # 最大值法求圖像灰度值 def graying(image): h, w = image.shape[0], image.shape[1] gray = np.zeros((h, w), np.uint8) for i in range(h): for j in range(w): gray[i, j] = max(image[i,j][0], image[i,j][1], image[i,j][2]) return gray # OTSU # 二值化 def otsu(img): h=img.shape[0] w=img.shape[1] m=h*w otsuimg=np.zeros((h,w),np.uint8) threshold_max=threshold=0 histogram=np.zeros(256,np.int32) probability=np.zeros(256,np.float32) for i in range(h): for j in range(w): s=img[i,j] histogram[s]+=1 for k in range(256): probability[k]=histogram[k]/m for i in range(255): w0 = w1 = 0 fgs = bgs = 0 for j in range (256): if j<=i: w0+=probability[j] fgs+=j*probability[j] else: w1+=probability[j] bgs+=j*probability[j] u0=fgs/w0 u1=bgs/w1 g=w0*w1*(u0-u1)**2 if g>=threshold_max: threshold_max=g threshold=i for i in range (h): for j in range (w): if img[i,j]<threshold: otsuimg[i,j]=255 else: otsuimg[i,j]=0 return otsuimg # 透視變換 from imutils.perspective import four_point_transform def wPs(image, points): warped = four_point_transform(image, points) return warped # 輪廓檢測函數(shù) def find_contour(image): contours = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] return contours # 冒泡排序 def bubble_sort(lists, type): ''' :param lists: 排序列表 :param type: 排序類型 :return: 排序結(jié)果 ''' count = len(lists) for i in range(0, count): xi, yi = cv2.boundingRect(lists[i])[0], cv2.boundingRect(lists[i])[1] for j in range(i + 1, count): xj, yj = cv2.boundingRect(lists[j])[0], cv2.boundingRect(lists[j])[1] if type == "title": if yi > yj: lists[i], lists[j] = lists[j], lists[i] elif type == "answer": if xi > xj: lists[i], lists[j] = lists[j], lists[i] else: return print("排序出錯(cuò)") return lists #統(tǒng)計(jì)結(jié)果 def count(roi): ''' :param roi: 答題選項(xiàng)區(qū)域 :return: 選擇結(jié)果 ''' grade = 0 long = roi.shape[1] / 8 contour = find_contour(roi) if len(contour) == 0: return 0 elif len(contour) >= 2: return -2 for c in contour: perimeter = cv2.arcLength(c, True) if perimeter > 5: x = cv2.boundingRect(c)[0] if x < long: grade = 1 elif x < long*3: grade = 2 elif x < long*5: grade = 3 else: grade = 4 return grade # 輪廓檢測處理 def contours(img, dst): ''' :param img: 查看效果圖像 :param dst: 輪廓檢測對(duì)象 :return: 效果圖像,輪廓檢測效果圖像,檢測結(jié)果 ''' img_dst = img.copy() edged = cv2.Canny(dst, 10, 100) img_cnts = find_contour(edged) # 如果未檢測到輪廓?jiǎng)t退出 c_len = len(img_cnts) if c_len == 0: print("error:No find contours!") return img, dst # 畫出所有輪廓 ## 得到答題區(qū)域 pt = None for c in img_cnts: cv2.drawContours(img, [c], -1, red, line_w) perimeter = cv2.arcLength(c, True) if perimeter < 40: continue approx = cv2.approxPolyDP(c, 0.02*perimeter, True) if len(approx) == 4: pt = approx hull = cv2.convexHull(c) cv2.polylines(img, [hull], True, green, line_w) pt = pt.reshape(4,2) # 透視變換 img_dst = wPs(img_dst, pt) dst = wPs(dst, pt) img_dst = img_dst[dist:img_dst.shape[0]-dist,dist:img_dst.shape[1]-dist] dst = dst[dist:dst.shape[0]-dist,dist:dst.shape[1]-dist] # 處理答題卡答題區(qū)域部分 contours_roi = find_contour(dst) title, answer = [], [] for c in contours_roi: x, y, w, h = cv2.boundingRect(c) if x >= dist and y <= dist: answer.append(c) if x < dist and y > dist: title.append(c) # 冒泡排序 title = bubble_sort(title, "title") answer = bubble_sort(answer, "answer") # 判卷 result = np.zeros(60, dtype=np.int8) for title_number in range(60): miny = cv2.boundingRect(title[title_number%20])[1] x, y, w, h = cv2.boundingRect(answer[int(title_number/20+1)*4-1]) x1= cv2.boundingRect(answer[int(title_number/20+1)*4-4])[0] maxx, maxy = x+w, miny+h cv2.rectangle(img_dst, (x1, miny), (maxx, maxy), blue, line_w) roi = dst[miny:maxy, x1:maxx] grade = count(roi) result[title_number] = grade print("title"+str(title_number+1)+":",grade) return img, img_dst, result def new_contours(img_dst, aim_otsu): ''' :param img_dst: 查看效果圖像 :param aim_otsu: 答題卡區(qū)域 :return: 效果圖像, 識(shí)別結(jié)果 ''' # 處理答題卡答題區(qū)域部分 contours_roi = find_contour(aim_otsu) title, answer = [], [] for c in contours_roi: x, y, w, h = cv2.boundingRect(c) if x >= dist and y <= dist: answer.append(c) if x < dist and y > dist: title.append(c) # 冒泡排序 title = bubble_sort(title, "title") answer = bubble_sort(answer, "answer") # 判卷 result = np.zeros(60, dtype=np.int8) for title_number in range(60): miny = cv2.boundingRect(title[title_number % 20])[1] x, y, w, h = cv2.boundingRect(answer[int(title_number / 20 + 1) * 4 - 1]) x1 = cv2.boundingRect(answer[int(title_number / 20 + 1) * 4 - 4])[0] maxx, maxy = x + w, miny + h cv2.rectangle(img_dst, (x1, miny), (maxx, maxy), blue, 1) roi = aim_otsu[miny:maxy, x1:maxx] grade = count(roi) result[title_number] = grade return img_dst, result # 主要步驟 def run(img): ''' :param img: 可操作的原圖像 :return: 預(yù)處理后的圖像 ''' print("image.shape:", img.shape) # 最小值法求圖像灰度值 gray = graying(img) # 二值分割大津法 thresh = otsu(gray) img_open = opening(thresh, 1) return img_open from PIL import Image, ImageDraw, ImageFont font_china = ImageFont.truetype('simhei.ttf', 40, encoding="utf-8") def ChinaToImage(image, str, color): ''' :param image: 原圖像 :param str: 需要寫的字 :param color:畫筆顏色 :return:寫完字的圖像 ''' img_PIL = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(img_PIL) draw.text((20, 20), str, color,font=font_china) return cv2.cvtColor(np.asarray(img_PIL),cv2.COLOR_RGB2BGR) # 提示是否可以開始函數(shù) def hint(image, b): ''' :param image: 攝像頭畫面 :param b: 提示是否可以批卷 :return: 返回寫入提示的畫面 ''' str_s = '按下Esc退出!\n按下空格開始!' str_e = '按下Esc退出!\n請(qǐng)調(diào)整好角度!' if b: image = ChinaToImage(image, str_s, green) else: image = ChinaToImage(image, str_e, red) return image # 查找答題卡輪廓,提示是否可以開始 def star_bool(image): ''' :param image: 攝像頭畫面 :return: 是否可以開始批卷 ''' image_gray = graying(image) edged = cv2.Canny(image_gray, 10, 100) con = find_contour(edged) b = False points = None for c in con: cv2.drawContours(image, [c], -1, red, line_w) perimeter = cv2.arcLength(c, True) w, h = cv2.minAreaRect(c)[1] if w == 0 or h == 0 or w/h < 0.6: continue if perimeter < 200: continue approx = cv2.approxPolyDP(c, 0.02 * perimeter, True) if len(approx) != 4: continue b = True points = approx hull = cv2.convexHull(c) cv2.polylines(image, [hull], True, green, line_w) aim_c = None aim_otsu = None if b: try: points = points.reshape(4, 2) aim = wPs(image_gray, points) aim_c = wPs(image, points) aim = aim[dist:aim.shape[0] - dist, dist:aim.shape[1] - dist] aim_c = aim_c[dist:aim_c.shape[0] - dist, dist:aim_c.shape[1] - dist] aim_otsu = otsu(aim) cv2.imshow('aim_otsu', aim_otsu) except: print('角度誤差大!') return b, aim_c, aim_otsu # 批改函數(shù) def correct(model_answer, result): ''' :param model_answer: 該試卷正確答案 :param result: 識(shí)別答案 :return: 顯示批卷結(jié)果,顯示效果,可檢測對(duì)象 ''' if len(model_answer) != 60: print('答案模板數(shù)量不對(duì)!\n請(qǐng)重新設(shè)置答案。') return 0 # 成績 grade = {'score':0, 'no choice':0, 'mul':0} no_choice_number = [] mul_number = [] # 題的分值,topik考試基本每題兩分 cube = 2 # 計(jì)算分?jǐn)?shù) for index in range(len(model_answer)): if model_answer[index] > 4 or model_answer[index] < 1: print("答案模板有誤!\n請(qǐng)重新設(shè)置答案。") return 0 if result[index] == 0: no_choice_number.append(index+1) grade['no choice'] += 1 continue if result[index] == -2: mul_number.append(index+1) grade['mul'] += 1 continue if model_answer[index] == result[index]: grade['score'] += cube # 批卷完成 print('-' * 70) print('-' * 70) print('正確答案:\n', model_answer) print('識(shí)別結(jié)果:\n', result) print('-'*35) print('分值:', grade['score']) print('-' * 35) print('空選數(shù)量:', grade['no choice']) print('空選題號(hào):\n', no_choice_number) print('-' * 35) print('多選數(shù)量:', grade['mul']) print('多選題號(hào):\n', mul_number) print('-' * 70) print('-' * 70) def main(): # 該變量為本次試卷正確答案模板,需要根據(jù)試卷受到修改原本正確答案 model_answer = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4,] cap = cv2.VideoCapture(0) cv2.namedWindow("image", 0) cv2.resizeWindow("image", 640, 480) while True: sucess, img = cap.read() img_temp = img.copy() b, aim, aim_otsu = star_bool(img_temp) img_temp = hint(img_temp, b) cv2.imshow("image", img_temp) k = cv2.waitKey(16) # Esc結(jié)束 if k == 27: break # 空格按下開始 elif k == 32: try: img_dst, result = new_contours(aim, aim_otsu) correct(model_answer, result) cv2.imshow('answer_roi', img_dst) except: print("您拍答題卡的角度誤差過大") else: if cv2.waitKey(0) == 27: break else: continue cap.release() cv2.destroyAllWindows() if __name__=="__main__": main()
總結(jié)
到此這篇關(guān)于python利用opencv如何實(shí)現(xiàn)答題卡自動(dòng)判卷的文章就介紹到這了,更多相關(guān)python opencv答題卡自動(dòng)判卷內(nèi)容請(qǐng)搜索本站以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持本站!
版權(quán)聲明:本站文章來源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請(qǐng)保持原文完整并注明來源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學(xué)習(xí)參考,不代表本站立場,如有內(nèi)容涉嫌侵權(quán),請(qǐng)聯(lián)系alex-e#qq.com處理。