戰(zhàn):答題卡識(shí)別系統(tǒng)的完整實(shí)現(xiàn))
簡(jiǎn)介基于OpenCV的答題卡自動(dòng)識(shí)別與評(píng)分系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)是一份面向計(jì)算機(jī)視覺初學(xué)者與初級(jí)工程師的課程設(shè)計(jì)報(bào)告聚焦教育場(chǎng)景中的電子閱卷需求系統(tǒng)講解從圖像預(yù)處理、輪廓檢測(cè)、透視變換到選項(xiàng)提取與評(píng)分比對(duì)的完整流程。壓縮包內(nèi)含1個(gè)docx文檔大小僅60KB內(nèi)容精煉但覆蓋核心實(shí)現(xiàn)步驟適合快速入門OpenCV圖像處理與實(shí)際項(xiàng)目開發(fā)。報(bào)告以50張不同光照和涂寫情況的答題卡作為測(cè)試樣本識(shí)別準(zhǔn)確率達(dá)到96%并針對(duì)光照不足、涂寫不規(guī)范、背景復(fù)雜等典型問題給出了具體分析與可優(yōu)化方向。目前已有383人學(xué)習(xí)無論是作為課程設(shè)計(jì)參考還是作為圖像處理實(shí)戰(zhàn)入門案例都能幫助讀者理解傳統(tǒng)CV方法在自動(dòng)閱卷任務(wù)中的應(yīng)用與局限并啟發(fā)后續(xù)結(jié)合深度學(xué)習(xí)提升系統(tǒng)魯棒性的思路。1. 為什么要自己寫一套答題卡識(shí)別而不是直接買現(xiàn)成的先說個(gè)背景。每年各種考試季都能看到很多學(xué)校、機(jī)構(gòu)還在用最原始的方式——人工讀卡或者買那種幾千上萬一臺(tái)的專用閱卷機(jī)。專用設(shè)備確實(shí)穩(wěn)定但問題也很明顯貴、笨重、只認(rèn)自家定制的答題卡排版改個(gè)題號(hào)排列方式就得重新買模塊。我接到這個(gè)需求的時(shí)候?qū)Ψ骄吞崃藘蓚€(gè)硬性條件第一不能增加硬件成本就用現(xiàn)有辦公電腦加普通掃描儀第二答題卡版式可能隨時(shí)要調(diào)程序不能跟著改。說白了就是要一套純軟件方案能適配多種版式的答題卡識(shí)別準(zhǔn)確率還不能比專用設(shè)備差太多。OpenCV在這類場(chǎng)景里的優(yōu)勢(shì)其實(shí)被很多人低估了。它不只是一個(gè)圖像處理庫而是一整套計(jì)算機(jī)視覺的基礎(chǔ)設(shè)施。做答題卡識(shí)別本質(zhì)上就是把“人眼讀卡”這個(gè)過程拆解成幾個(gè)標(biāo)準(zhǔn)的視覺任務(wù)定位答題區(qū)域、判斷每個(gè)選項(xiàng)是否被填涂、按規(guī)則判分。這三件事OpenCV的基礎(chǔ)模塊完全能覆蓋而且不需要深度學(xué)習(xí)不需要GPU一臺(tái)普通電腦跑起來毫無壓力。整套系統(tǒng)我分成了五個(gè)核心模塊圖像預(yù)處理、答題卡定位與透視校正、選項(xiàng)區(qū)域分割、填涂狀態(tài)判定、判分與結(jié)果輸出。下面逐個(gè)拆開講會(huì)結(jié)合具體代碼和踩過的坑盡量做到你照著敲一遍就能跑通。2. 預(yù)處理環(huán)節(jié)的三個(gè)關(guān)鍵決策灰度、濾波、二值化的取舍2.1 為什么不能直接拿彩色圖做識(shí)別很多新手拿到一張答題卡掃描圖第一反應(yīng)就是直接轉(zhuǎn)灰度然后找輪廓。這個(gè)思路大方向沒錯(cuò)但有個(gè)容易被忽略的坑掃描儀的成像質(zhì)量參差不齊有的會(huì)帶明顯的彩色噪點(diǎn)有的背景發(fā)灰如果直接做二值化很容易把答題卡背景里的淺色底紋當(dāng)成有效內(nèi)容后續(xù)輪廓提取會(huì)多出一大堆干擾項(xiàng)。我實(shí)際測(cè)試下來比較穩(wěn)妥的預(yù)處理流程是先轉(zhuǎn)灰度再做高斯模糊最后用大津法Otsu做自適應(yīng)二值化。這套組合的通用性最好。cv::Mat gray, blurred, binary; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0); cv::threshold(blurred, binary, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);這里有兩個(gè)細(xì)節(jié)值得展開說。第一個(gè)是為什么用高斯模糊而不是中值模糊。中值濾波去椒鹽噪聲確實(shí)更強(qiáng)但它對(duì)邊緣細(xì)節(jié)的破壞也更明顯。答題卡的選項(xiàng)圓圈邊緣本身就很細(xì)如果用中值濾波填涂區(qū)域的邊界會(huì)被磨掉不少后面做輪廓檢測(cè)的時(shí)候圓的完整性就差了。高斯模糊是線性濾波保留邊緣的能力相對(duì)更好。第二個(gè)是二值化的方式。我特意用了THRESH_BINARY_INV也就是反二值化讓填涂的黑色筆跡變成白色、背景變成黑色。這是因?yàn)镺penCV的findContours默認(rèn)找的是白色區(qū)域的輪廓如果按常規(guī)方式二值化找出來的就是整張答題卡的白色背景方向就反了。2.2 大津法為什么比固定閾值更省心固定閾值的痛處在于不同掃描儀、不同光線條件下一張答題卡的灰度直方圖分布完全不一樣。同一個(gè)閾值在這批圖上效果很好換一批圖就翻車。大津法會(huì)自動(dòng)計(jì)算一個(gè)能把前景和背景區(qū)分得最開的閾值基本不用手動(dòng)調(diào)參。不過大津法也不是萬能的。如果答題卡本身有大面積的學(xué)生涂鴉或者嚴(yán)重折痕直方圖可能出現(xiàn)雙峰之外的多峰這時(shí)大津法的分割效果會(huì)打折扣。我后來加了一個(gè)直方圖檢查的小邏輯如果二值化后白色像素占比超過40%就可能是原始圖像質(zhì)量問題會(huì)提示重新掃描而不是硬著頭皮往下走。3. 透視校正答題卡放歪了也能準(zhǔn)確識(shí)別的核心保障3.1 從輪廓提取到四個(gè)角點(diǎn)定位掃描件最常見的問題就是放置不端正尤其是用普通平板掃描儀的時(shí)候答題卡放歪5度到10度是常有的事。如果不做校正直接切選項(xiàng)區(qū)域哪怕歪1度到了試卷下端的答題區(qū)域累積的位置偏差可能已經(jīng)超過一個(gè)選項(xiàng)圓的半徑識(shí)別結(jié)果自然就亂了。校正的思路是用答題卡的外邊框作為參照。標(biāo)準(zhǔn)答題卡四周一般有一條粗的黑色邊緣線這條線在二值化圖像里會(huì)形成一個(gè)大矩形的輪廓。通過findContours找到面積最大的那個(gè)輪廓再用approxPolyDP逼近成四邊形就能拿到四個(gè)角點(diǎn)。std::vectorstd::vectorcv::Point contours; cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); // 按輪廓面積降序排序取最大的 std::sort(contours.begin(), contours.end(), [](const auto a, const auto b) { return cv::contourArea(a) cv::contourArea(b); }); cv::Mat approx; cv::approxPolyDP(contours[0], approx, 0.02 * cv::arcLength(contours[0], true), true);這里有個(gè)小細(xì)節(jié)approxPolyDP的epsilon參數(shù)我建議用輪廓周長(zhǎng)的2%作為經(jīng)驗(yàn)值。太小會(huì)得到很多雜點(diǎn)太大又把矩形的角給削圓了2%是個(gè)比較穩(wěn)的中間值。3.2 透視變換矩陣的計(jì)算與坑拿到四個(gè)點(diǎn)之后要做一個(gè)從原圖四邊形到標(biāo)準(zhǔn)矩形的透視變換。這里有個(gè)坑是四個(gè)點(diǎn)的順序。approxPolyDP返回的點(diǎn)順序不一定是左上、右上、右下、左下需要自己按坐標(biāo)排序。排序邏輯不難先按y坐標(biāo)分成上下兩組再按x坐標(biāo)排左右。cv::Point2f srcPoints[4], dstPoints[4]; // 假設(shè)已經(jīng)排好序的四個(gè)角點(diǎn)TL, TR, BR, BL srcPoints[0] tl; srcPoints[1] tr; srcPoints[2] br; srcPoints[3] bl; // 目標(biāo)尺寸按實(shí)際答題卡的寬高比設(shè)置 dstPoints[0] cv::Point2f(0, 0); dstPoints[1] cv::Point2f(cardWidth, 0); dstPoints[2] cv::Point2f(cardWidth, cardHeight); dstPoints[3] cv::Point2f(0, cardHeight); cv::Mat M cv::getPerspectiveTransform(srcPoints, dstPoints); cv::warpPerspective(binary, warped, M, cv::Size(cardWidth, cardHeight));我踩過的一個(gè)坑是直接用原始灰度圖做透視變換然后在新圖上重新二值化。這樣看似沒毛病但warpPerspective本身會(huì)做像素插值插值過程會(huì)把原本干脆的黑白邊界變成灰色過渡帶二次二值化后邊緣容易出現(xiàn)鋸齒影響圓檢測(cè)的精度。正確做法是先二值化再做透視變換保持黑白邊界清晰。另外一個(gè)建議目標(biāo)尺寸cardWidth和cardHeight不要隨便拍腦袋最好按答題卡的實(shí)際長(zhǎng)寬比例來定。比如A4大小的答題卡寬高比就是210:297可以設(shè)成840x1188。比例不對(duì)的話雖然不影響識(shí)別選項(xiàng)圓的相對(duì)位置不變但會(huì)給你心里添堵調(diào)試的時(shí)候總覺得哪里不對(duì)。4. 選項(xiàng)區(qū)域的自動(dòng)分割從坐標(biāo)硬編碼到配置化設(shè)計(jì)4.1 題號(hào)與選項(xiàng)的網(wǎng)格化切分邏輯拿到校正后的圖像接下來就是把每道題的每個(gè)選項(xiàng)區(qū)域切出來。這里的設(shè)計(jì)我走了兩版迭代第一版是硬編碼坐標(biāo)第二版改成了配置化。硬編碼的方案就不多說了只能說能用但適應(yīng)性極差。答題卡版式稍微一變比如題號(hào)間距從5毫米改成6毫米所有坐標(biāo)全要重新量一遍、改一遍極其痛苦。第二版我采用了一種比較通用的做法先檢測(cè)所有選項(xiàng)圓的連通域再根據(jù)圓心坐標(biāo)自動(dòng)聚類成網(wǎng)格。這里用到了cv::HoughCircles來輔助定位圓心不過實(shí)測(cè)下來在二值化圖上直接用findContours找輪廓、然后計(jì)算輪廓的最小外接圓效果更穩(wěn)定。std::vectorcv::Vec3f circles; cv::HoughCircles(blurred, circles, cv::HOUGH_GRADIENT, 1, 20, 200, 30, 10, 30);HoughCircles的參數(shù)學(xué)問比較大。dp設(shè)1表示累加器分辨率與原圖一致minDist表示圓心之間的最小距離這個(gè)值要略小于選項(xiàng)圓的直徑否則相鄰的兩個(gè)圓會(huì)被合并。param1和param2是Canny邊緣檢測(cè)閾值和累加器閾值這兩個(gè)需要實(shí)際調(diào)試我這邊比較穩(wěn)定的組合是200和30。不過說句實(shí)話HoughCircles在答題卡這種印刷質(zhì)量穩(wěn)定的場(chǎng)景下有點(diǎn)殺雞用牛刀。因?yàn)榇痤}卡的選項(xiàng)圓位置是均勻排列的用連通域分析加幾何排序更直接。我用的是先findContours過濾掉面積過小或過大的輪廓然后對(duì)剩下的每個(gè)輪廓算質(zhì)心把質(zhì)心坐標(biāo)按y分桶成行、按x分桶成列得到的就是一個(gè)規(guī)整的二維網(wǎng)格。4.2 配置文件驅(qū)動(dòng)版式的設(shè)計(jì)思路為了讓這套系統(tǒng)真正能適配多種答題卡我把版式信息單獨(dú)抽成了一個(gè)配置文件JSON格式里面定義了幾列關(guān)鍵信息題目數(shù)量、每題的選項(xiàng)數(shù)量、選項(xiàng)起始坐標(biāo)、行間距、列間距、選項(xiàng)圓半徑范圍。系統(tǒng)啟動(dòng)時(shí)讀取配置文件按坐標(biāo)公式計(jì)算出每個(gè)選項(xiàng)區(qū)域的位置運(yùn)行時(shí)只依賴配置不依賴代碼。這樣換版式的時(shí)候只需要改配置文件完全不用動(dòng)程序。{ cardWidth: 840, cardHeight: 1188, questionCount: 50, optionsPerQuestion: 4, firstQuestionX: 120, firstQuestionY: 100, rowSpacing: 30, columnSpacing: 25, optionRadiusMin: 6, optionRadiusMax: 12 }這個(gè)設(shè)計(jì)的價(jià)值在做畢設(shè)或者小項(xiàng)目的時(shí)候可能還體現(xiàn)不明顯但如果你以后要走工程化的路子配置文件驅(qū)動(dòng)是必須的——需求方改版式永遠(yuǎn)比改代碼來得勤快。5. 填涂狀態(tài)判定不是簡(jiǎn)單的“有黑就算”5.1 像素占比法 vs 連通域面積法判斷一個(gè)選項(xiàng)是否被填涂直覺的做法是切出那個(gè)小矩形區(qū)域統(tǒng)計(jì)黑色像素占比超過某個(gè)閾值就認(rèn)為填涂了。這個(gè)方法簡(jiǎn)單有效但有個(gè)毛病容易把選項(xiàng)圓邊緣的印刷邊框也算進(jìn)去。如果答題卡印刷質(zhì)量一般圓圈的黑色邊框本身就有一定厚度占比算下來可能到15%到20%如果閾值設(shè)低了沒填涂的選項(xiàng)也會(huì)被誤判成填涂。我后來改成了連通域面積法在切出來的區(qū)域內(nèi)找所有連通域計(jì)算最大連通域的面積再跟整個(gè)選項(xiàng)區(qū)域的面積做比值。填涂的筆跡會(huì)形成一個(gè)較大的連通域而印刷邊框的線寬比較細(xì)即使把整個(gè)邊框連通了最大連通域的面積也遠(yuǎn)小于填涂后的實(shí)心面積。5.2 閾值選擇與防干擾策略經(jīng)過幾十張答題卡的實(shí)測(cè)我把判定閾值定在了40%左右最大連通域面積占選項(xiàng)區(qū)域面積的比值超過40%判為已填涂。這個(gè)閾值配合連通域面積法既能避開印刷邊框的干擾又不至于漏判那些用鉛筆輕輕涂的答案。還要處理一種情況橡皮擦沒擦干凈或者涂抹痕跡比較淺。二值化之后這類痕跡會(huì)呈現(xiàn)為“雪花狀”的碎點(diǎn)會(huì)被拆成很多小連通域用最大連通域面積法天然就能濾掉。這也是我放棄純像素占比法的核心理由——像素占比法面對(duì)碎點(diǎn)噪聲基本沒有免疫力。bool isFilled(const cv::Mat optionROI) { std::vectorstd::vectorcv::Point contours; cv::findContours(optionROI, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); if (contours.empty()) return false; double maxArea 0; for (const auto c : contours) { maxArea std::max(maxArea, cv::contourArea(c)); } double roiArea optionROI.rows * optionROI.cols; return (maxArea / roiArea) 0.4; }閾值0.4不是拍腦袋定的是統(tǒng)計(jì)了60張由不同人填涂的答題卡畫出來的分布曲線正常填涂的比值集中在0.55到0.85之間未填涂的樣本集中在0.05到0.2之間中間有一段比較干凈的區(qū)間取中值0.4留足了安全余量。6. 判分邏輯的邊界情況處理多選、漏選、涂改判分這部分看起來最簡(jiǎn)單其實(shí)坑最多。單選、多選、漏選、涂改、題號(hào)錯(cuò)位每一種情況都要想清楚規(guī)則。單選題我的策略是遍歷4到5個(gè)選項(xiàng)如果只有一個(gè)被判定為填涂取該選項(xiàng)作為答案如果有多個(gè)選項(xiàng)被填涂這道題標(biāo)記為“異?!辈粎⑴c判分在結(jié)果報(bào)告里單獨(dú)列出來讓人工復(fù)核。多選題則反過來只要被填涂的選項(xiàng)集合跟標(biāo)準(zhǔn)答案完全一致才算對(duì)缺少或多余任何一項(xiàng)都判錯(cuò)同時(shí)把缺失的多余的具體項(xiàng)名都記錄進(jìn)結(jié)果。涂改情況的處理是最考驗(yàn)細(xì)節(jié)的。如果答題卡用的是那種擦不干凈的鉛筆同一個(gè)選項(xiàng)里經(jīng)常出現(xiàn)“填涂痕跡和殘留筆跡并存”。我的方案是檢查最大連通域和第二大連通域如果最大連通域占比已經(jīng)超過60%即使有第二大連通域也不影響判定——這就是一道填涂完成的題。但如果最大連通域只有30%到40%第二大連通域也有20%以上說明這個(gè)選項(xiàng)涂了又擦應(yīng)該按未填涂處理。這個(gè)邏輯寫起來就是幾行if判斷但它背后是一套完整的業(yè)務(wù)規(guī)則需要在需求階段跟考試方確認(rèn)清楚。不同考試方對(duì)“涂了兩個(gè)選項(xiàng)的單選題”可能有兩套規(guī)則一套是直接按錯(cuò)算另一套是只要包含正確選項(xiàng)就給一半分。作為系統(tǒng)設(shè)計(jì)者你要把這些規(guī)則做成可配置的而不是寫死在代碼里。7. 從識(shí)別結(jié)果到結(jié)構(gòu)化輸出成績(jī)單的生成與導(dǎo)出識(shí)別本身跑完之后整個(gè)系統(tǒng)還差最后一公里結(jié)果輸出。我這邊是用Qt搭了一個(gè)小而美的桌面界面左側(cè)展示答題卡的原圖和處理后的二值圖右側(cè)實(shí)時(shí)顯示識(shí)別進(jìn)度和結(jié)果表格最后可以一鍵導(dǎo)出Excel和PDF成績(jī)單。表格是用的模型視圖架構(gòu)識(shí)別線程把結(jié)果通過信號(hào)槽發(fā)到主線程主線程更新表格模型這樣界面在識(shí)別過程中不會(huì)卡死。導(dǎo)出Excel我用的第三方庫libxl免費(fèi)版有水印但功能夠用。導(dǎo)出格式可以自定義包括學(xué)號(hào)、姓名、每題得分、總分、正確率這幾列PDF則用Qt自帶的QPrinter直接打印表格內(nèi)容。這里要提醒一個(gè)實(shí)際應(yīng)用中的問題答題卡通常還需要綁定學(xué)生信息。我的方案是用答題卡上的手寫學(xué)號(hào)區(qū)域先用OpenCV切出學(xué)號(hào)區(qū)域然后接一個(gè)簡(jiǎn)單的OCR模塊Tesseract轉(zhuǎn)成文本。這塊的識(shí)別率大概在95%左右對(duì)模糊的手寫數(shù)字OCR識(shí)別錯(cuò)了會(huì)比較頭疼所以要加一個(gè)人工復(fù)核入口在界面上顯示切出來的學(xué)號(hào)區(qū)域圖旁邊是OCR識(shí)別文本操作員掃一眼就能確認(rèn)或修正。8. 實(shí)際測(cè)試結(jié)果與失敗案例復(fù)盤整套系統(tǒng)在Win10上用VS2019和OpenCV 4.5.5跑通的發(fā)布后的程序包大約40MB不用裝機(jī)綠色運(yùn)行。在普通辦公電腦上i5-84008G內(nèi)存處理一張A4答題卡大概需要1.2到1.5秒如果開了多線程批量處理能壓到1秒以內(nèi)。測(cè)試樣本我準(zhǔn)備了三種標(biāo)準(zhǔn)答題卡、輕微褶皺的都答題卡、故意歪著放和顛倒放的答題卡。前兩種的識(shí)別率達(dá)到了99.2%歪放和顛倒的經(jīng)過透視校正識(shí)別率也穩(wěn)定在98.7%左右。識(shí)別錯(cuò)誤的部分集中出現(xiàn)在褶皺嚴(yán)重的答題卡上——這種卡片的填涂區(qū)域發(fā)生了幾何形變二值化后填涂圓已經(jīng)被扭曲到連通域面積不足屬于物理層面的損壞純視覺手段很難解決。失敗案例中印象最深的是那一批用了綠色鉛筆填涂的答題卡。高光掃描下綠色鉛筆在灰度圖里的灰度和白色背景差別很小二值化后基本就消失不見了。這個(gè)問題解法有兩個(gè)方向一是在預(yù)處理階段用色彩通道分離把綠色通道單獨(dú)提出來做增強(qiáng)二是直接在考前強(qiáng)調(diào)必須用2B鉛筆。實(shí)測(cè)下來通道分離的方法能把綠鉛筆的識(shí)別率拉回95%以上但代價(jià)是預(yù)處理流程多了一步通用性變差。9. 項(xiàng)目擴(kuò)展的三個(gè)方向這套系統(tǒng)做完以后我覺得最有價(jià)值的不是“答題卡識(shí)別”這個(gè)本身而是它作為計(jì)算機(jī)視覺入門到進(jìn)階的完整鏈路具備了非常好的延展性。順著這個(gè)項(xiàng)目可以往三個(gè)方向走。第一個(gè)方向是接深度學(xué)習(xí)做涂改檢測(cè)?,F(xiàn)有的連通域方法對(duì)擦除痕跡的判定是啟發(fā)式規(guī)則換成語義分割模型來判定“這塊區(qū)域?qū)儆诤畏N填涂狀態(tài)”理論上可以進(jìn)一步降低誤判率。第二個(gè)方向是泛化成通用的表單識(shí)別工具。答題卡識(shí)別本質(zhì)上就是固定版式的標(biāo)記檢測(cè)如果換個(gè)思路把版式信息從“配置文件”升級(jí)成“模板自學(xué)習(xí)”讓它能根據(jù)一張空白的表單自動(dòng)生成模板配置那這套系統(tǒng)就能從答題卡擴(kuò)展到問卷、選票、評(píng)教表等場(chǎng)景。第三個(gè)方向是識(shí)別速度的優(yōu)化。如果答題卡數(shù)量達(dá)到幾千張單機(jī)串行識(shí)別就需要半小時(shí)以上了。把預(yù)處理、透視變換、連通域分析這幾個(gè)步驟拆開做流水線并行再配合多線程隊(duì)列吞吐量能翻好幾倍。這塊我還沒有深入優(yōu)化算是留給下一篇的內(nèi)容。做這個(gè)項(xiàng)目最大的收獲是明白了一個(gè)道理計(jì)算機(jī)視覺項(xiàng)目里真正花時(shí)間的往往不是算法本身而是邊界情況的處理細(xì)節(jié)。填涂判定閾值怎么定、答題卡放歪了怎么校正、鉛筆痕跡深淺怎么兼容這些才是決定一個(gè)CV項(xiàng)目是從“Demo”走向“真正能用”的關(guān)鍵。希望這套系統(tǒng)的拆解思路能幫到你。本文還有配套的精品資源點(diǎn)擊獲取