計算機視覺cv入門之答題卡自動批閱

????????前邊我們已經講解了使用cv2進行圖像預處理與邊緣檢測等方面的知識,這里我們以答題卡自動批閱這一案例來實操一下。

大致思路

? ? ? ? 答題卡自動批閱的大致流程可以分為這五步:圖像預處理-尋找考試信息區域與涂卡區域-考生信息區域OCR識別-涂卡區域填涂答案判斷-圖像中標記結果

接下來我們按照這五步來進行講解。

圖像預處理

答題卡獲取首先,在網上隨便找一張答題卡圖片

由于這里我只需要考生信息與填途題目,所以只是截取了左上角這一部分作為我們后續的目標。?

?接著,我們使用圖像編輯軟件將考生信息填入,并將10道題目進行填涂。

讀取圖像?

# #讀取答題卡圖片
import cv2
import matplotlib.pyplot as plt
src_image=cv2.imread(filename='answercard4.jpg',flags=cv2.IMREAD_COLOR_RGB)
height,width=src_image.shape[:2]
plt.xticks(range(0,width,10),minor=True)
plt.yticks(range(0,height,10),minor=True)
plt.imshow(src_image)

????????這里我使用matplotlib的imshow函數來顯示圖像,這樣在jupyter環境中可以不打開任何彈窗直接顯示圖像,比較方便。

轉為灰度圖

#轉為灰度圖
gray_image=cv2.cvtColor(src=src_image,code=cv2.COLOR_RGB2GRAY)
plt.title('原始圖像(灰度圖)')
plt.imshow(gray_image,cmap='gray')

????????

????????將原始圖像轉化為灰度圖是為了后續的檢測等操作,在計算機視覺任務中,基本上所有的操作都是針對灰度圖來進行的,灰度圖是將原始圖像的多個通道按照一定權重求和疊加而來,這樣一來多通道變成了單通道(Gray=w_1*B+w_2*G+w_3*R),在計算量上也會比較友好。

?閾值化

#閾值化
thresh,binary_image=cv2.threshold(src=gray_image,thresh=128,maxval=255,type=cv2.THRESH_OTSU+cv2.THRESH_BINARY)
plt.imshow(binary_image,cmap='gray')

????????閾值化是為了更好的查找輪廓。這里閾值化我們使用cv2.THRESH+cv2.THRESH-OTSU方法來自動對圖像進行二值化閾值分割。?

考生信息與答題區域分割

#考生信息區域與答題區域分割
contours,hiercahy=cv2.findContours(binary_image,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
possible_rectangles=[]
answer_rectangle=[]
for points in contours:x,y,w,h=cv2.boundingRect(points)if 800<w*h<120000:possible_rectangles.append((x,y,w,h))
information_rectangles=[rect for rect in possible_rectangles if 100<rect[2]<140 and 30<rect[3]<60]#長在100~~60
answer_rectangle=sorted(possible_rectangles,key=lambda x:x[2]*x[3])[-2]
marked_img=src_image.copy()
information_images=[]
for rect in information_rectangles:x, y, w, h,=rectcv2.rectangle(marked_img, (x, y), (x+w, y+h), (0, 255, 0), 3)information_images.append(marked_img[y:y+h,x:x+w])
x,y,w,h=answer_rectangle
answer_area=marked_img[y:y+h,x:x+w]
answer_area=cv2.cvtColor(src=answer_area,code=cv2.COLOR_RGB2GRAY)
cv2.rectangle(marked_img,(x,y),(x+w,y+h),(255,0,0),3)
plt.xticks(range(0,marked_img.shape[1],10),minor=True)
plt.yticks(range(0,marked_img.shape[0],10),minor=True)
plt.imshow(marked_img)

?????????查找輪廓時我們通常使用findContours函數來進行查找(返回值為所有可能的輪廓點contours以及這些點之間的拓撲結構hierachy),考慮到要分割的區域都是矩形,因此我們可以在查找到的輪廓點中使用cv2.boundingrectangle函數來對查找到的輪廓進行矩形擬合

? ? ? ?然后,我們再使用cv2.drawContours函數將其在原始圖像中標記出來即可。

OCR識別

? ?這里我使用現成的OCR字符識別庫,這里我使用的是paddleocr

獲取方式

pip install paddlepaddle paddleocr

OCR識別

#使用paddleocr識別考生信息
student_information=[]
import torch
from paddleocr import PaddleOCR
ocr=PaddleOCR(lang="ch")
for image in information_images:result=ocr.ocr(image,cls=True)for line in result[0]:text=line[1][0]student_information.append(text)    
print(student_information)     

?結果:

答題區域答案識別

? ? ? ? ?這一步是整個任務的關鍵,但其實也比較簡單,就是按照查找到的填涂過的黑色矩形的位置來判斷,首先我們要在這個填涂答案的區域內定位所有黑色矩形的位置以及長和寬,然后根據以下的關系來判斷每一列的答案是ABCDE的哪一個,其中filled_area_top是指整個填涂答案中最頂部的位置,即A的位置(我的答案中有A,倘若沒有的話,也可以完全根據y坐標自行指定一個ABCDE所在的范圍),filled_area_bottom是整個填入答案中最底部的位置,即E的位置。


thresh,binary_answer_area=cv2.threshold(src=answer_area,thresh=128,maxval=255,type=cv2.THRESH_BINARY+cv2.THRESH_OTSU)
contours,hiercahy=cv2.findContours(image=binary_answer_area,mode=cv2.RETR_TREE,method=cv2.CHAIN_APPROX_SIMPLE)
filled_areas=[]
answers=[]
epsilon=5
true_answers=['C','A','D','A','C','C','B','E','A','D']
for points in contours:x,y,w,h=cv2.boundingRect(points)if 300<w*h<500:filled_areas.append((x,y,w,h))
filled_areas=sorted(filled_areas,key=lambda point:point[1])
filled_area_top,filled_area_bottom=filled_areas[0][1],filled_areas[-1][1]
filled_areas=sorted(filled_areas,key=lambda point:point[0])
score=0
total_num=len(filled_areas)
avg_score=100/total_num
plt.imshow(marked_img)
for i in range(len(filled_areas)):x,y,w,h=filled_areas[i]if 0<=(y-filled_area_top)<=epsilon:answers.append('A')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='A',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if epsilon<abs(y-filled_area_top)<=h+epsilon:answers.append('B')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='B',color='blue',size=15)if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if h+epsilon<abs(y-filled_area_top)<=2*h+epsilon:answers.append('C')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='C',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if 2*h+epsilon<abs(y-filled_area_top)<=3*h+epsilon:answers.append('D')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='D',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if 0<=filled_area_bottom-y<=epsilon:answers.append('E')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='E',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)
plt.text(x=width-100,y=50,s=score,color='red',size='20')
plt.text(x=width-100,y=70,s='———',color='red',size='15')
plt.text(x=width-100,y=75,s='———',color='red',size='15')
for info in student_information:print(info)
print(f'你的答案是:{answers}')
print(f'正確答案是:{true_answers}')
print(f'考試成績:{score}')

結果:?

完整代碼

# #讀取答題卡圖片
import cv2
import matplotlib.pyplot as plt
src_image=cv2.imread(filename='answercard4.jpg',flags=cv2.IMREAD_COLOR_RGB)
height,width=src_image.shape[:2]
plt.xticks(range(0,width,10),minor=True)
plt.yticks(range(0,height,10),minor=True)
plt.imshow(src_image)
#轉為灰度圖
gray_image=cv2.cvtColor(src=src_image,code=cv2.COLOR_RGB2GRAY)
plt.imshow(gray_image,cmap='gray')
thresh,binary_image=cv2.threshold(src=gray_image,thresh=128,maxval=255,type=cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
plt.imshow(binary_image,cmap='gray')
#考生信息區域與答題區域分割
contours,hiercahy=cv2.findContours(binary_image,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
possible_rectangles=[]
answer_rectangle=[]
for points in contours:x,y,w,h=cv2.boundingRect(points)if 800<w*h<120000:possible_rectangles.append((x,y,w,h))
information_rectangles=[rect for rect in possible_rectangles if 100<rect[2]<140 and 30<rect[3]<60]#長在100~~60
answer_rectangle=sorted(possible_rectangles,key=lambda x:x[2]*x[3])[-2]
marked_img=src_image.copy()
information_images=[]
for rect in information_rectangles:x, y, w, h,=rectcv2.rectangle(marked_img, (x, y), (x+w, y+h), (0, 255, 0), 3)information_images.append(marked_img[y:y+h,x:x+w])
x,y,w,h=answer_rectangle
answer_area=marked_img[y:y+h,x:x+w]
answer_area=cv2.cvtColor(src=answer_area,code=cv2.COLOR_RGB2GRAY)
cv2.rectangle(marked_img,(x,y),(x+w,y+h),(255,0,0),3)
plt.xticks(range(0,marked_img.shape[1],10),minor=True)
plt.yticks(range(0,marked_img.shape[0],10),minor=True)
plt.imshow(marked_img)
#使用paddleocr識別考生信息
student_information=[]
import torch
from paddleocr import PaddleOCR
ocr=PaddleOCR(lang="ch")
for image in information_images:result=ocr.ocr(image,cls=True)for line in result[0]:text=line[1][0]student_information.append(text)    
print(student_information)     thresh,binary_answer_area=cv2.threshold(src=answer_area,thresh=128,maxval=255,type=cv2.THRESH_BINARY+cv2.THRESH_OTSU)
contours,hiercahy=cv2.findContours(image=binary_answer_area,mode=cv2.RETR_TREE,method=cv2.CHAIN_APPROX_SIMPLE)
filled_areas=[]
answers=[]
epsilon=5
true_answers=['C','A','D','A','C','C','B','E','A','D']
for points in contours:x,y,w,h=cv2.boundingRect(points)if 300<w*h<500:filled_areas.append((x,y,w,h))
filled_areas=sorted(filled_areas,key=lambda point:point[1])
filled_area_top,filled_area_bottom=filled_areas[0][1],filled_areas[-1][1]
filled_areas=sorted(filled_areas,key=lambda point:point[0])
score=0
total_num=len(filled_areas)
avg_score=100/total_num
plt.imshow(marked_img)
for i in range(len(filled_areas)):x,y,w,h=filled_areas[i]if 0<=(y-filled_area_top)<=epsilon:answers.append('A')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='A',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if epsilon<abs(y-filled_area_top)<=h+epsilon:answers.append('B')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='B',color='blue',size=15)if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if h+epsilon<abs(y-filled_area_top)<=2*h+epsilon:answers.append('C')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='C',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if 2*h+epsilon<abs(y-filled_area_top)<=3*h+epsilon:answers.append('D')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='D',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)if 0<=filled_area_bottom-y<=epsilon:answers.append('E')plt.text(x=x+5,y=y+height-answer_area.shape[0],s='E',color='blue')if true_answers[i]==answers[i]:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='√',color='red',size=15)score+=avg_scoreelse:plt.text(x=x+5,y=y+h+height-answer_area.shape[0],s='X',color='red',size=15)
plt.text(x=width-100,y=50,s=score,color='red',size='20')
plt.text(x=width-100,y=70,s='———',color='red',size='15')
plt.text(x=width-100,y=75,s='———',color='red',size='15')
for info in student_information:print(info)
print(f'你的答案是:{answers}')
print(f'正確答案是:{true_answers}')
print(f'考試成績:{score}')

總結?

?

?

????????以上便是計算機視覺cv2入門之答題卡自動批閱的所有內容,所有代碼作者純手敲無任何AI,如果本文對你有用,還勞駕各位一鍵三連支持一下博主。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/78102.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/78102.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/78102.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

語音合成之一TTS技術發展史綜述

TTS技術發展史綜述 引言TTS技術的起源與早期探索基于規則的TTS系統&#xff1a;原理與發展共振峰合成技術&#xff1a;作用與影響拼接合成技術&#xff1a;發展與應用統計參數語音合成&#xff1a;以隱馬爾可夫模型&#xff08;HMM&#xff09;為例深度學習驅動的TTS&#xff1…

目標檢測中的損失函數(一) | IoU GIoU DIoU CIoU EIoU Focal-EIoU

&#x1f680;該系列將會持續整理和更新BBR相關的問題&#xff0c;如有錯誤和不足懇請大家指正&#xff0c;歡迎討論&#xff01;&#xff01;&#xff01; &#x1f4e6;目標檢測的損失函數一般包含三個部分&#xff0c;分別是邊界框損失也可稱為定位損失、置信度損失和分類損…

結構型模式:適配器模式

什么是適配器模式&#xff1f; 適配器模式&#xff08;Adapter Pattern&#xff09;是一種常用的結構型設計模式&#xff0c;它的主要作用是將一個類的接口轉換成客戶端期望的另一個接口。就像現實生活中的各種轉接頭一樣&#xff0c;適配器模式使得原本因接口不兼容而無法一起…

AI Agent認知框架(ReAct、函數調用、計劃與執行、自問自答、批判修正、思維鏈、思維樹詳解和對比,最后表格整理總結

以下是主流AI Agent認知框架的詳細說明、對比及表格總結&#xff1a; 1. 各認知框架詳解 (1) ReAct (Reasoning Action) 定義&#xff1a;結合推理&#xff08;Reasoning&#xff09;和行動&#xff08;Action&#xff09;的循環過程。核心機制&#xff1a; 模型先推理&…

特征存儲的好處:特征存儲在機器學習開發中的優勢

隨著企業尋求提升機器學習生產力和運營能力 (MLOps),特征存儲 (Feature Store) 的普及度正在迅速提升。隨著 MLOps 技術的進步,特征存儲正成為機器學習基礎設施的重要組成部分,幫助企業提升模型的性能和解釋能力,并加速新模型與生產環境的集成。這些存儲充當集中式存儲庫,…

SPRING-AI 官方事例

springAI 關于最近看了很多SpringAi&#xff0c;閱讀很多代碼都感覺特別陌生 SpringAI依賴的springBoot版本都是3.3以上, 以及很多SpringAi都是依賴JDK版本最低17, 并且出現了很多新關鍵字例如 var,record 等寫法, 煙花繚亂得lambda 表達式&#xff0c; 到處都是使用build 構…

Visual Studio Code 使用tab鍵往左和往右縮進內容

使用VSCode寫東西&#xff0c;經常遇到多行內容同時縮進的情況&#xff0c;今天寫文檔的時候就碰到&#xff0c;記錄下來&#xff1a; 往右縮進 選中多行內容&#xff0c;點tab鍵&#xff0c;會整體往右縮進&#xff1a; 往左縮進 選中多行內容&#xff0c;按shifttab&am…

機器學習(7)——K均值聚類

文章目錄 1. K均值&#xff08;K-means&#xff09;聚類是什么算法&#xff1f;2. 核心思想2. 數學目標3. 算法步驟3.1. 選擇K個初始質心&#xff1a;3.2.迭代優化3.3. 重復步驟2和步驟3&#xff1a; 4. 關鍵參數5. 優缺點6. 改進變種7. K值選擇方法8. Python示例9. 應用場景10…

爬蟲案例-爬取某企數據

文章目錄 1、準備要爬取企業名稱數據表2、爬取代碼3、查看效果 1、準備要爬取企業名稱數據表 企業名稱紹興市袍江王新國家庭農場紹興市鄭杜糧油專業合作社紹興市越城區興華家庭農場紹興市越城區銳意家庭農場紹興市越城區青甸畈家庭農場紹興市袍江王新國家庭農場紹興市袍江月明…

足球 AI 智能體技術解析:從數據采集到比賽預測的全鏈路架構

一、引言 在足球運動數字化轉型的浪潮中&#xff0c;AI 智能體正成為理解比賽、預測賽果的核心技術引擎。本文從工程實現角度&#xff0c;深度解析足球 AI 的技術架構&#xff0c;涵蓋數據采集、特征工程、模型構建、實時計算到決策支持的全鏈路技術方案&#xff0c;揭示其背后…

怎么配置一個kubectl客戶端訪問多個k8s集群

怎么配置一個kubectl客戶端訪問多個k8s集群 為什么有的客戶端用token也訪問不了k8s集群&#xff0c;因為有的是把~/.kube/config文件&#xff0c;改為了~/.kube/.config文件&#xff0c;文件設置成隱藏文件了。 按照kubectl的尋找配置的邏輯&#xff0c;kubectl找不到要訪問集群…

[QMT量化交易小白入門]-四十六、年化收益率118%的回測參數,如何用貪心算法挑選50個兩兩相關性最小的ETF組合

本專欄主要是介紹QMT的基礎用法,常見函數,寫策略的方法,也會分享一些量化交易的思路,大概會寫100篇左右。 QMT的相關資料較少,在使用過程中不斷的摸索,遇到了一些問題,記錄下來和大家一起溝通,共同進步。 文章目錄 相關閱讀準備工作安裝所需庫導入所需模塊下載所有ETF數…

幾何編碼:啟用矢量模式地理空間機器學習

在 ML 模型中使用點、線和多邊形&#xff0c;將它們編碼為捕捉其空間屬性的向量。 自地理信息系統 (GIS) 誕生之初&#xff0c;“柵格模式”和“矢量模式”之間就存在著顯著的區別。在柵格模式下&#xff0c;數據以值的形式呈現在規則的網格上。這包括任何形式的圖像&#xff0…

Leetcode98、230:二叉搜索樹——遞歸學習

什么是二叉搜索樹&#xff1a;右子樹節點 > 根節點 > 左子樹節點&#xff0c; 二叉搜索樹中的搜索&#xff0c;返回給定值val所在的樹節點 終止條件為傳進來的節點為空、或者節點的值 val值&#xff0c;返回這個節點&#xff1b; 單程遞歸邏輯&#xff1a;定義一個resu…

每天學一個 Linux 命令(30):cut

??可訪問網站查看,視覺品味拉滿: http://www.616vip.cn/30/index.html cut 命令用于從文件或輸入流中提取文本的特定部分(如列、字符或字節位置)。它常用于處理結構化數據(如 CSV、TSV)或按固定格式分割的文本。以下是詳細說明和示例: 命令格式 cut [選項] [文件...]…

Tauri 2.3.1+Leptos 0.7.8開發桌面應用--Sqlite數據庫選中數據的表格輸出

在前期工作的基礎上&#xff08;Tauri 2.3.1Leptos 0.7.8開發桌面應用--Sqlite數據庫的寫入、展示和選擇刪除_tauri leptos sqlite 選擇刪除-CSDN博客&#xff09;&#xff0c;實現將選中的數據實時用表格展示出來&#xff0c;效果如下&#xff1a; 1. 后臺invoke調用命令 Tau…

使用Tauri 2.3.1+Leptos 0.7.8開發桌面小程序匯總

近期斷斷續續學習了Rust編程&#xff0c;使用Tauri 2.3.1Leptos 0.7.8開發了一個自用的桌面小程序。Win10操作系統&#xff0c;使用VS Code及rust analyzer插件搭建的開發環境&#xff0c;后期開始使用Roo Code綁定DeepSeek API 輔助編程&#xff0c;對我這個初學者編程幫助很大…

考研英一學習筆記

2024 年全國碩士研究生招生考試 英語&#xff08;一&#xff09;試題 &#xff08;科目代碼&#xff1a;201&#xff09; Section Ⅰ Use of English Directions: Read the following text. Choose the best word(s) for each numbered blank and mark A, B, C or D on the ANS…

【技術筆記】Cadence實現Orcad與Allegro軟件交互式布局設置

【技術筆記】Cadence實現Orcad與Allegro軟件交互式布局設置 更多內容見專欄&#xff1a;【硬件設計遇到了不少問題】、【Cadence從原理圖到PCB設計】 在做硬件pcb設計的時候&#xff0c;原理圖選中一個元器件&#xff0c;希望可以再PCB中可以直接選中。 為了達到原理圖和PCB兩兩…