天天看點

【轉載】項目實戰—答題卡識别判卷續(十五)

在之前的輪廓檢測之後,接下來我們需要開始進行對圖像進行Transform變換,進而對圖像進行校正。

view plaincopy to clipboardprint?
# 對原始圖像和灰階圖都進行四點Transform變換  
paper = four_point_transform(image, docCnt.reshape(4, 2))  
warped = four_point_transform(gray, docCnt.reshape(4, 2)) 
           

我們使用了four_point_transform函數,它将輪廓的(x, y) 坐标以一種特别、可重複的方式整理,并且對輪廓包圍的區域進行****變換。暫時我們隻需要知道它的變換效果就行了。

【轉載】項目實戰—答題卡識别判卷續(十五)

可以看到,圖像經過變化之後已經被校正,接下來可以進行下一步的處理了。

我們從原始圖像中擷取了答題卡,并應用****變換擷取90度俯視效果。

下面要對題目進行判斷了。

這一步開始于二值化,或者說是圖像的前景和後景的分離/門檻值處理。

view plaincopy to clipboardprint?
# 對灰階圖應用大津二值化算法  
thresh = cv2.threshold(warped, 0, 255,  
    cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]  
           
【轉載】項目實戰—答題卡識别判卷續(十五)

現在,我們的圖像是一個純粹二值圖像了。

圖像的背景是黑色的,而前景是白色的。

這二值化使得我們能夠再次應用輪廓提取技術,以找到每個題目中的氣泡選項。

view plaincopy to clipboardprint?
# 在二值圖像中查找輪廓,然後初始化題目對應的輪廓清單  
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,  
    cv2.CHAIN_APPROX_SIMPLE)  
cnts = cnts[0] if imutils.is_cv2() else cnts[1]  
questionCnts = []  
   
# 對每一個輪廓進行循環處理  
for c in cnts:  
    # 計算輪廓的邊界框,然後利用邊界框資料計算寬高比  
    (x, y, w, h) = cv2.boundingRect(c)  
    ar = w / float(h)  
   
    # 為了辨識一個輪廓是一個氣泡,要求它的邊界框不能太小,在這裡邊至少是20個像素,而且它的寬高比要近似于1  
    if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:  
        questionCnts.append(c)  
           

我們由二值圖像中的輪廓,擷取輪廓邊界框,利用邊界框資料來判定每一個輪廓是否是一個氣泡,如果是,将它加入題目清單questionCnts。

将我們得到的題目清單中的輪廓在圖像中畫出,得到下圖:

【轉載】項目實戰—答題卡識别判卷續(十五)

隻有題目氣泡區域被圈出來了,而其它地方沒有。

接下來就是閱卷了:

view plaincopy to clipboardprint?
# 以從頂部到底部的方法将我們的氣泡輪廓進行排序,然後初始化正确答案數的變量。  
questionCnts = contours.sort_contours(questionCnts,  
    method="top-to-bottom")[0]  
correct = 0  
   
# 每個題目有5個選項,是以5個氣泡一組循環處理  
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):  
    # 從左到右為目前題目的氣泡輪廓排序,然後初始化被塗畫的氣泡變量  
    cnts = contours.sort_contours(questionCnts[i:i + 5])[0]  
    bubbled = None  
           

首先,我們對questionCnts進行從上到下的排序,使得靠近頂部的一行氣泡在清單中最先出現。然後對每行氣泡應用從左到右的排序,使左邊的氣泡在隊列中先出現。解釋下,就是氣泡輪廓按縱坐标先排序,并排的5個氣泡輪廓縱坐标相差不大,總會被排在一起,而且每組氣泡之間按從上到下的順序排列,然後再将每組輪廓按橫坐标分出先後。

【轉載】項目實戰—答題卡識别判卷續(十五)

第二步,我們需要判斷哪個氣泡被填充了。我們可以利用二值圖像中每個氣泡區域内的非零像素點數量來進行判斷。

view plaincopy to clipboardprint?
    # 對一行從左到右排列好的氣泡輪廓進行周遊  
    for (j, c) in enumerate(cnts):  
        # 構造隻有目前氣泡輪廓區域的掩模圖像  
        mask = np.zeros(thresh.shape, dtype="uint8")  
        cv2.drawContours(mask, [c], -1, 255, -1)  
   
        # 對二值圖像應用掩模圖像,然後就可以計算氣泡區域内的非零像素點。  
        mask = cv2.bitwise_and(thresh, thresh, mask=mask)  
        total = cv2.countNonZero(mask)  
   
        # 如果像素點數最大,就連同氣泡選項序号一起記錄下來  
        if bubbled is None or total > bubbled[0]:  
            bubbled = (total, j)  
           

接着就是查找答案字典,判斷正誤了。

如果氣泡作答是對的,則用綠色圈起來,如果不對,就用紅色圈出正确答案:

view plaincopy to clipboardprint?
# USAGE  
 # python test_grader.py --image test_01.png  
   
 # import the necessary packages  
 from imutils.perspective import four_point_transform  
 from imutils import contours  
 import numpy as np  
 import argparse  
 import imutils  
 import cv2  
   
 # construct the argument parse and parse the arguments  
 ap = argparse.ArgumentParser()  
 ap.add_argument("-i", "--image", required=True,  
     help="path to the input image")  
 args = vars(ap.parse_args())  
   
 # define the answer key which maps the question number  
 # to the correct answer  
 ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}  
   
 # load the image, convert it to grayscale, blur it  
 # slightly, then find edges  
 image = cv2.imread(args["image"])  
 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)  
 blurred = cv2.GaussianBlur(gray, (5, 5), 0)  
 edged = cv2.Canny(blurred, 75, 200)  
   
 # find contours in the edge map, then initialize  
 # the contour that corresponds to the document  
 cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,  
     cv2.CHAIN_APPROX_SIMPLE)[0]  
 # cnts = cnts[0] if imutils.is_cv2() else cnts[1]  
 docCnt = None  
   
 # ensure that at least one contour was found  
 if len(cnts) > 0:  
     # sort the contours according to their size in  
     # descending order  
     cnts = sorted(cnts, key=cv2.contourArea, reverse=True)  
   
     # loop over the sorted contours  
     for c in cnts:  
         # approximate the contour  
         peri = cv2.arcLength(c, True)  
         approx = cv2.approxPolyDP(c, 0.02 * peri, True)  
   
         # if our approximated contour has four points,  
         # then we can assume we have found the paper  
         if len(approx) == 4:  
             docCnt = approx  
             break  
   
 # apply a four point perspective transform to both the  
 # original image and grayscale image to obtain a top-down  
 # birds eye view of the paper  
 paper = four_point_transform(image, docCnt.reshape(4, 2))  
 warped = four_point_transform(gray, docCnt.reshape(4, 2))  
   
 # apply Otsu's thresholding method to binarize the warped  
 # piece of paper  
 thresh = cv2.threshold(warped, 0, 255,  
     cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]  
   
 # find contours in the thresholded image, then initialize  
 # the list of contours that correspond to questions  
 cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,  
     cv2.CHAIN_APPROX_SIMPLE)[0]  
 # cnts = cnts[0] if imutils.is_cv2() else cnts[1]  
 questionCnts = []  
   
 # loop over the contours  
 for c in cnts:  
     # compute the bounding box of the contour, then use the  
     # bounding box to derive the aspect ratio  
     (x, y, w, h) = cv2.boundingRect(c)  
     ar = w / float(h)  
   
     # in order to label the contour as a question, region  
     # should be sufficiently wide, sufficiently tall, and  
     # have an aspect ratio approximately equal to 1  
     if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:  
         questionCnts.append(c)  
   
 # sort the question contours top-to-bottom, then initialize  
 # the total number of correct answers  
 questionCnts = contours.sort_contours(questionCnts,  
     method="top-to-bottom")[0]  
 correct = 0  
   
 # each question has 5 possible answers, to loop over the  
 # question in batches of 5  
 for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):  
     # sort the contours for the current question from  
     # left to right, then initialize the index of the  
     # bubbled answer  
     cnts = contours.sort_contours(questionCnts[i:i + 5])[0]  
     bubbled = None  
   
     # loop over the sorted contours  
     for (j, c) in enumerate(cnts):  
         # construct a mask that reveals only the current  
         # "bubble" for the question  
         mask = np.zeros(thresh.shape, dtype="uint8")  
         cv2.drawContours(mask, [c], -1, 255, -1)  
   
         # apply the mask to the thresholded image, then  
         # count the number of non-zero pixels in the  
         # bubble area  
         mask = cv2.bitwise_and(thresh, thresh, mask=mask)  
         total = cv2.countNonZero(mask)  
   
         # if the current total has a larger number of total  
         # non-zero pixels, then we are examining the currently  
         # bubbled-in answer  
         if bubbled is None or total > bubbled[0]:  
             bubbled = (total, j)  
   
     # initialize the contour color and the index of the  
     # *correct* answer  
     color = (0, 0, 255)  
     k = ANSWER_KEY[q]  
   
     # check to see if the bubbled answer is correct  
     if k == bubbled[1]:  
         color = (0, 255, 0)  
         correct += 1  
   
     # draw the outline of the correct answer on the test  
     cv2.drawContours(paper, [cnts[k]], -1, color, 3)  
   
 # grab the test taker  
 score = (correct / 5.0) * 100  
 print("[INFO] score: {:.2f}%".format(score))  
 cv2.putText(paper, "{:.2f}%".format(score), (10, 30),  
     cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)  
 cv2.imshow("Original", image)  
 cv2.imshow("Exam", paper)  
 cv2.waitKey(0)  
           

以上是所有的代碼,現在來看示範結果:

【轉載】項目實戰—答題卡識别判卷續(十五)
【轉載】項目實戰—答題卡識别判卷續(十五)
【轉載】項目實戰—答題卡識别判卷續(十五)
【轉載】項目實戰—答題卡識别判卷續(十五)
【轉載】項目實戰—答題卡識别判卷續(十五)

檢視文章彙總頁https://blog.csdn.net/weixin_44237705/article/details/107864965

更多openvino技術資訊可以入群交流~

申請備注:CSDN

【轉載】項目實戰—答題卡識别判卷續(十五)

繼續閱讀