前言
首先要明確我們的任務。要想解數獨,需要進行計算,圖片格式的數字肯定是不行的,所以必須把圖片上的數字轉換爲實實在在的數字才能進行計算。要得到實實在在的數字,我們需要做的是對圖片上的數字進行提取和識別。本文先說第一步,圖片中數字的提取。
在一年之前,我曾用C++嘗試過opencv解數獨,但由於當時水平有限,未能完成。當時的成果就是透視變換的應用和方格數字的提取。現在稍微簡化一下工作,不再從傾斜的數獨圖片中提取數獨,而是直接用正拍且已經提取好的數獨開始處理。這裏用到的數獨圖片如下圖所示:
方法
1.以前的方法
從上圖這樣的九宮格圖片中提取數字,我以前用的方法是,先利用輪廓提取,通過輪廓的面積進行篩選,得到所有的81個小方格;然後對檢測小方格中是否有黑色像素以及像素的多少(排除噪音)來判定哪個小方格中有數字;最後對有數字的小方格再次進行輪廓提取得到數字的輪廓和輪廓外包矩形。
此方法實現起來相對來說比較麻煩,思路僅供參考。
2.本次所用方法
在仔細研究了opencv輪廓提取函數findContours()之後,發現利用輪廓的層級結構會更加簡單。作爲本節最主要的函數,有必要稍微多說幾句。
cv2.findContours(image, mode, method[, contours[, hierarchy[, offset] ] ]) → contours, hierarchy
在Python中,findContours()接受如下參數並返回contours和hierarchy。
1.image
源圖像,一般爲8爲單通道圖像,更具體來說,二值圖像。其他情況暫且不論。
2.mode
輪廓檢索模式,簡要介紹幾種:
cv2.RETR_EXTERNAL
只檢測外輪廓。對所有輪廓設置hierarchy[i][2]=hierarchy[i][3]=-1
cv2.RETR_LIST
提取所有輪廓,並放置在list中,檢測到的輪廓不建立等級關係。cv2.RETR_TREE
提取所有輪廓,建立網狀的輪廓結構。
3.method
輪廓的近似辦法,是提取輪廓上所有像素點,還是隻提取關鍵的一些點。比如一條線段是提取所有點還是隻提取兩個端點。
4.contours
檢測到的輪廓,爲組成輪廓的點集。
5.hierarchy
下面詳述。
hierarchy
什麼是層級結構呢?我們檢測輪廓的時候,有時候可能會出現其中一個輪廓包含了另外一個輪廓,比如同心圓。這裏我們認爲外側輪廓爲父輪廓
,內側被包含的爲子輪廓
。同一級別的又有前一個輪廓
和後一個輪廓
。總的來說,hierarchy表達的是不同輪廓之間的 關係和聯繫。
這樣,每一個輪廓都會有[Next, Previous, First_Child, Parent]
上面說到,cv2.RETR_EXTERNAL
只檢測外輪廓。對所有輪廓設置hierarchy[i][2]=hierarchy[i][3]=-1
。由於只檢測最外圍輪廓,所有檢測到的輪廓肯定沒有父輪廓和子輪廓,所有層級結構的第三個和第四個元素都設置爲-1。
看下圖:
如果只檢測最外圍輪廓,那麼只會檢測到輪廓0
、1
和2
。
如果建立層級關係,以輪廓3
爲例,那麼它的父輪廓是2a
,子輪廓是3a
,沒有前一輪廓和後一輪廓,設爲-1。所以它的hierarchy應該是[-1,-1,3a,2a]
如果是輪廓2
,那麼它的前一輪廓就是1
,子輪廓是2a
,沒有後一輪廓和父輪廓。所以它的hierarchy應該是[-1,1,2a,-1]
有興趣的可以仔細看看,沒興趣的可以略過。興趣更濃的可以去看opencv文檔,那裏的講解更加詳細。
這裏就說這麼多,對於我們本節的內容來說,已經夠了。
上面說了啥
我覺得大部分人這個時候還會問,上面說了這麼一堆到底是要幹什麼???因爲這裏確實不是那麼清晰明瞭。
別忘了我們本節的目的是要提取數字,什麼樣的輪廓包含數字?
一般來說經過前面的閾值分割得到二值圖像,然後從二值圖像中提取的輪廓是這樣的。這是處理的比較好的情況下:
顯然最最外面的那個包圍所有的就是0號輪廓,裏面的九九八十一個小方格就是0號輪廓的子輪廓。而每一個已知數字的輪廓都是對應方格的子輪廓。
提取數字
所有我們的辦法就是先提取方格,然後提取數字。
八十一個小方格有什麼特點?父輪廓都是0號輪廓!所以:
boxes = []
for i in range(len(hierarchy[0])):
if hierarchy[0][i][3] == 0:
boxes.append(hierarchy[0][i])
不記得的可以上翻看一下hierarchy是不是第四個元素表示父輪廓。
然後從小方格中提取數字輪廓。數字輪廓的有什麼特點?其父輪廓有子輪廓,也即是說包含子輪廓的小方格里面就有數字。所以:
for j in range(len(boxes)):
if boxes[j][2] != -1:
x,y,w,h = cv2.boundingRect(contours[boxes[j][2]])
number_boxes.append([x,y,w,h])
不記得的可以上翻看一下hierarchy是不是第三個元素表示子輪廓。不等於-1表示存在。
最後把檢測到的數字畫出來就可以得到下面的這幅圖了。
代碼
# -*- coding: UTF-8 -*-
import cv2
img = cv2.imread('001.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
## 閾值分割
ret,thresh = cv2.threshold(gray,200,255,1)
## 對二值圖像執行膨脹操作
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS,(5, 5))
dilated = cv2.dilate(thresh,kernel)
## 輪廓提取,cv2.RETR_TREE表示建立層級結構
image, contours, hierarchy = cv2.findContours(dilated,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
## 提取小方格,其父輪廓都爲0號輪廓
boxes = []
for i in range(len(hierarchy[0])):
if hierarchy[0][i][3] == 0:
boxes.append(hierarchy[0][i])
## 提取數字,其父輪廓都存在子輪廓
number_boxes = []
for j in range(len(boxes)):
if boxes[j][2] != -1:
#number_boxes.append(boxes[j])
x,y,w,h = cv2.boundingRect(contours[boxes[j][2]])
number_boxes.append([x,y,w,h])
img = cv2.rectangle(img,(x-1,y-1),(x+w+1,y+h+1),(0,0,255),2)
cv2.namedWindow("img", cv2.WINDOW_NORMAL);
cv2.imshow("img", img)
cv2.waitKey(0)
下一步
數字已經提取出來,下一步就該是數字的識別了…
公衆號CVPy,分享OpenCV和Python的實戰內容。每一篇都會放出完整的代碼。歡迎關注。