啪啪,幾步搞定!用Python實(shí)現(xiàn)答題卡識(shí)別!
來(lái)源丨h(huán)ttps://blog.csdn.net/qq_44805233?type=blog
答題卡素材圖片:

思路
讀入圖片,做一些預(yù)處理工作。 進(jìn)行輪廓檢測(cè),然后找到該圖片最大的輪廓,就是答題卡部分。 進(jìn)行透視變換,以去除除答題卡外的多余部分,并且可以對(duì)答題卡進(jìn)行校正。 再次檢測(cè)輪廓,定位每個(gè)選項(xiàng)。 對(duì)選項(xiàng)圓圈先按照豎坐標(biāo)排序,再按照行坐標(biāo)排序,這樣就從左到右從上到下的獲得了每個(gè)選項(xiàng)輪廓。 對(duì)每個(gè)選項(xiàng)輪廓進(jìn)行檢查,如果某個(gè)選項(xiàng)輪廓中的白色點(diǎn)多,說(shuō)明該選項(xiàng)被選中,否則就是沒(méi)被選上。細(xì)節(jié)部分看過(guò)程:
1、預(yù)處理(去噪,灰度,二值化)
img?=?cv2.imread("1.png",1)
#高斯去噪
img_gs?=?cv2.GaussianBlur(img,[5,5],0)
#?轉(zhuǎn)灰度
img_gray?=?cv2.cvtColor(img_gs,cv2.COLOR_BGR2GRAY)
#?自適應(yīng)二值化
_,binary_img?=?cv2.threshold(img_gray,0,255,cv2.THRESH_OTSU|cv2.THRESH_BINARY)
注:cv2.THRESH_OTSU|cv2.THRESH_BINARY,該參數(shù)指的是自適應(yīng)閾值+反二值化,做自適應(yīng)閾值的時(shí)候閾值要設(shè)置為0

2、輪廓檢測(cè)
#?找輪廓
contours,?hierarchy?=?cv2.findContours(binary_img,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
#?按照輪廓的面積從大到小排序
cnts?=?sorted(contours,key?=?cv2.contourArea,reverse=True)
#?畫(huà)輪廓
draw_img?=?cv2.drawContours(img.copy(),cnts[0],-1,(0,255,255),2)
注:findContours函數(shù),傳入的圖像應(yīng)該是二值圖像,cv2.RETR_EXTERNAL指的是只檢測(cè)外部輪廓,cv2.CHAIN_APPROX_NONE指的返回輪廓上的所有點(diǎn)。

#?輪廓近似
#?閾值,一般為輪廓長(zhǎng)度的2%
alpha?=?0.02*cv2.arcLength(cnts[0],True)
approxCurve?=?cv2.approxPolyDP(cnts[0],alpha,True)
draw_img?=?cv2.drawContours(img.copy(),[approxCurve],-1,(255,0,0),2)
這里做輪廓近似的目的是,之前檢測(cè)到的輪廓看似是一個(gè)多邊形,其實(shí)本質(zhì)上是只是點(diǎn)集。
cv2.approxPolyDP(contour,epsilon,True),多邊形逼近,第一個(gè)參數(shù)是點(diǎn)集,第二個(gè)參數(shù)是精度(原始輪廓的邊界點(diǎn)與擬合多邊形之間的最大距離),第三個(gè)參數(shù)指新產(chǎn)生的輪廓是否需要閉合,返回值approxCurve為多邊形的點(diǎn)集(按照逆時(shí)針排序)。與該函數(shù)類(lèi)似的函數(shù)還有cv2.boundingRect(矩形包圍框)cv2.minAreaRect(最小包圍矩形框),cv2.minEnclosingCircle(最小包圍圓形)cv2.filtEllipse(最優(yōu)擬合橢圓)cv2.filtLine(最優(yōu)擬合直線),cv2.minEnclosingTriangle(最小外包三角形)

3、透視變換
#透視變換
#?矩形的四個(gè)頂點(diǎn)為approxCurve[0][0],approxCurve[1][0],approxCurve[2][0],approxCurve[3][0]
#?分別表示矩形的TL,BL,BR,TR四個(gè)點(diǎn)
a1?=?list(approxCurve[0][0])
a2?=?list(approxCurve[1][0])
a3?=?list(approxCurve[2][0])
a4?=?list(approxCurve[3][0])
#?原始矩陣
mat1?=?np.array([a1,a2,a3,a4],dtype?=?np.float32)
#?計(jì)算矩形的w和h
w1?=?int(np.sqrt((a1[0]-a4[0])**2+(a1[1]-a4[1])**2))
w2?=?int(np.sqrt((a2[0]-a3[0])**2+(a2[1]-a3[1])**2))
h1?=?int(np.sqrt((a1[0]-a2[0])**2+(a1[1]-a2[1])**2))
h2?=?int(np.sqrt((a3[0]-a4[0])**2+(a3[1]-a4[1])**2))
w,h=max(w1,w2),max(h1,h2)
#?計(jì)算透視變換后的坐標(biāo)
new_a1?=?[0,0]
new_a2?=?[0,h]
new_a3?=?[w,h]
new_a4?=?[w,0]
#?目標(biāo)矩陣
mat2?=?np.array([new_a1,new_a2,new_a3,new_a4],dtype?=?np.float32)
#?透視變換矩陣
mat?=?cv2.getPerspectiveTransform(mat1,mat2)
#?進(jìn)行透視變換
res?=?cv2.warpPerspective(img,mat,(w,h))
imshow((res))

透視變換的計(jì)算步驟:
首先獲取原圖多邊形的四個(gè)頂點(diǎn),注意頂點(diǎn)順序。 然后構(gòu)造原始頂點(diǎn)矩陣。 計(jì)算矩形長(zhǎng)寬,構(gòu)造變換后的目標(biāo)矩陣。 獲取原始矩陣到目標(biāo)矩陣的透視變換矩陣 進(jìn)行透視變換
4、輪廓檢測(cè),檢測(cè)每個(gè)選項(xiàng)
res_gray?=?cv2.cvtColor(res,cv2.COLOR_BGR2GRAY)
_,binary_res?=?cv2.threshold(res_gray,0,255,cv2.THRESH_OTSU|cv2.THRESH_BINARY_INV)
contours?=?cv2.findContours(binary_res,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)[0]
dst?=?cv2.drawContours(res.copy(),contours,-1,(0,0,255),1)
imshow(dst)

篩選選項(xiàng)輪廓
#?挑選合適的輪廓
def?check(contours):
????ans?=?[]
????for?i?in?contours:
????????area?=?float(cv2.contourArea(i))
????????length?=?float(cv2.arcLength(i,True))
????????if?area<=0?or?length<=0:
????????????continue
????????if?area/length?>7.05?and?area/length<10.5:
????????????ans.append(i)
????return?ans
ans_contours?=?check(contours)
dst_new?=?cv2.drawContours(res.copy(),ans_contours,-1,(0,255,255),3??)
imshow(dst_new)

5、畫(huà)輪廓的外接圓,排序,定位每個(gè)選項(xiàng)
#?遍歷每一個(gè)圓形輪廓,畫(huà)外接圓
circle?=?[]
for?i?in?ans_contours:
????(x,y),r?=?cv2.minEnclosingCircle(i)
????center?=?(int(x),int(y))
????r?=?int(r)
????circle.append((center,r))
#?按照外接圓的水平坐標(biāo)排序center[1],也就是圓心的高度h,或者y坐標(biāo)
circle.sort(key?=?lambda?x:x[0][1])
A?=?[]
for?i?in?range(1,6):
????now?=?circle[(i-1)*5:i*5]
????now.sort(key?=?lambda?x:x[0][0])
????A.extend(now)
每個(gè)選項(xiàng)按照?qǐng)A心從左到右,從上到下的順序保存在了A中
6、選項(xiàng)檢測(cè)
思路:對(duì)于A中的每個(gè)選項(xiàng)圓,計(jì)算它有所覆蓋的坐標(biāo),然后判斷這些坐標(biāo)在二值圖像中對(duì)應(yīng)的值,統(tǒng)計(jì)白色點(diǎn)的個(gè)數(shù), 如果白色點(diǎn)所占的比例比較大的話,說(shuō)明該選項(xiàng)被選中。
def?dots_distance(dot1,dot2):
????#計(jì)算二維空間中兩個(gè)點(diǎn)的距離
????return?((dot1[0]-dot2[0])**2+(dot1[1]-dot2[1])**2)**0.5
def?count_dots(center,radius):
????#輸入圓的中心點(diǎn)與半徑,返回圓內(nèi)所有的坐標(biāo)
????dots?=?[]
????for?i?in?range(-radius,radius+1):
????????for?j?in?range(-radius,radius+1):
????????????dot2?=?(center[0]+i,center[1]+j)
????????????if?dots_distance(center,dot2)?<=?radius:
????????????????dots.append(dot2)
????return?dots
?
da?=?[]
for?i?in?A:
????dots?=?count_dots(i[0],i[1])
????all_dots?=?len(dots)
????whilt_dots?=?0
????for?j?in?dots:
????????if?binary_res[j[1]][j[0]]?==?255:
????????????whilt_dots?=?whilt_dots+1
????if?whilt_dots/all_dots>=0.4:
????????da.append(1)
????else:
????????da.append(0)
da?=?np.array(da)
da?=?np.reshape(da,(5,5))

這樣每個(gè)答題卡就轉(zhuǎn)換成了一個(gè)二維數(shù)組,接下來(lái)在做一些簡(jiǎn)單的收尾工作就可以了。
Python機(jī)器人公眾號(hào)正式上線了
推薦閱讀:
入門(mén):?最全的零基礎(chǔ)學(xué)Python的問(wèn)題? |?零基礎(chǔ)學(xué)了8個(gè)月的Python??|?實(shí)戰(zhàn)項(xiàng)目?|學(xué)Python就是這條捷徑
干貨:爬取豆瓣短評(píng),電影《后來(lái)的我們》?|?38年NBA最佳球員分析?|? ?從萬(wàn)眾期待到口碑撲街!唐探3令人失望? |?笑看新倚天屠龍記?|?燈謎答題王?|用Python做個(gè)海量小姐姐素描圖?|碟中諜這么火,我用機(jī)器學(xué)習(xí)做個(gè)迷你推薦系統(tǒng)電影
趣味:彈球游戲? |?九宮格? |?漂亮的花?|?兩百行Python《天天酷跑》游戲!
AI:?會(huì)做詩(shī)的機(jī)器人?|?給圖片上色?|?預(yù)測(cè)收入?|?碟中諜這么火,我用機(jī)器學(xué)習(xí)做個(gè)迷你推薦系統(tǒng)電影
小工具:?Pdf轉(zhuǎn)Word,輕松搞定表格和水印!?|?一鍵把html網(wǎng)頁(yè)保存為pdf!|??再見(jiàn)PDF提取收費(fèi)!?|?用90行代碼打造最強(qiáng)PDF轉(zhuǎn)換器,word、PPT、excel、markdown、html一鍵轉(zhuǎn)換?|?制作一款釘釘?shù)蛢r(jià)機(jī)票提示器!?|60行代碼做了一個(gè)語(yǔ)音壁紙切換器天天看小姐姐!|
年度爆款文案
點(diǎn)閱讀原文,看原創(chuàng)200個(gè)趣味案例!

