[轉]使用 OpenCV 識別 QRCode

原文鏈接

背景

識別二維碼的項目數不勝數囱怕,每次都是開箱即用功舀,方便得很萍倡。
這次想用 OpenCV 從零識別二維碼,主要是溫習一下圖像處理方面的基礎概念辟汰,熟悉 OpenCV 的常見操作列敲,以及了解二維碼識別和編碼的基本原理。
作者本人在圖像處理方面還是一名新手帖汞,采用的方法大多原始粗暴戴而,如果有更好的解決方案歡迎指教。

QRCode

二維碼有很多種翩蘸,這里我選擇的是比較常見的 QRCode 作為探索對象所意。QRCode 全名是 Quick Response Code,是一種可以快速識別的二維碼催首。
尺寸
QRCode 有不同的 Version 扶踊,不同的 Version 對應著不同的尺寸。將最小單位的黑白塊稱為 module 郎任,則 QRCode 尺寸的公式如下:

Version V = ((V-1)*4 + 21) ^ 2 modules

常見的 QRCode 一共有40種尺寸:

Version 1 : 21 * 21 modules
Version 2 : 25 * 25 modules

Version 40: 177 * 177 modules

分類

QRCode 分為 Model 1秧耗、Model 2、Micro QR 三類:

  • Model 1 :是 Model 2 和 Micro QR 的原型舶治,有 Version 1 到 Version 14 共14種尺寸分井。
  • Model 2 :是 Model 1 的改良版本,添加了對齊標記霉猛,有 Version 1 到 Version 40 共40種尺寸尺锚。
  • Micro QR :只有一個定位標記,最小尺寸是 11*11 modules 惜浅。

組成


QRCode 主要由以下部分組成:

  • 1 - Position Detection Pattern:位于三個角落瘫辩,可以快速檢測二維碼位置。
  • 2 - Separators:一個單位寬的分割線赡矢,提高二維碼位置檢測的效率杭朱。
  • 3 - Timing Pattern:黑白相間,用于修正坐標系吹散。
  • 4 - Alignment Patterns:提高二維碼在失真情況下的識別率。
  • 5 - Format Information:格式信息八酒,包含了錯誤修正級別和掩碼圖案空民。
  • 6 - Data:真正的數據部分。
  • 7 - Error Correction:用于錯誤修正,和 Data 部分格式相同界轩。

具體的生成原理和識別細節(jié)可以閱讀文末的參考文獻画饥,比如耗子叔的這篇《二維碼的生成細節(jié)和原理》。
由于二維碼的解碼步驟比較復雜浊猾,而本次學習重點是數字圖像處理相關的內容抖甘,所以本文主要是解決二維碼的識別定位問題,數據解碼的工作交給第三方庫(比如 ZBAR)完成葫慎。

OpenCV

在開始識別二維碼之前衔彻,還需要補補課,了解一些圖像處理相關的基本概念偷办。

contours

輪廓(contour)可以簡單理解為一段連續(xù)的像素點艰额。比如一個長方形的邊,比如一條線椒涯,比如一個點柄沮,都屬于輪廓。而輪廓之間有一定的層級關系废岂,以下圖為例:


主要說明以下概念:

  • external & internal:對于最大的包圍盒而言祖搓,2 是外部輪廓(external),2a 是內部輪廓(internal)湖苞。
  • parent & child:2 是 2a 的父輪廓(parent)拯欧,2a 是 2 的子輪廓(child),3 是 2a 的子輪廓袒啼,同理哈扮,3a 是 3 的子輪廓,4 和 5 都是 3a 的子輪廓蚓再。
  • external | outermost:0翔曲、1惧财、2 都屬于最外圍輪廓(outermost)。
  • hierarchy level:0、1杯巨、2 是同一層級(same hierarchy),都屬于 hierarchy-0 畅形,它們的第一層子輪廓屬于 hierarchy-1 蛉顽。
  • first child:4 是 3a 的第一個子輪廓(first child)。實際上 5 也可以矾端,這個看個人喜好了掏击。

在 OpenCV 中,通過一個數組表達輪廓的層級關系:

[Next, Previous, First_Child, Parent]

  • Next:同一層級的下一個輪廓秩铆。在上圖中砚亭, 0 的 Next 就是 1 灯变,1 的 Next 就是 2 ,2 的 Next 是 -1 捅膘,表示沒有下一個同級輪廓添祸。
  • Previous:同一層級的上一個輪廓。比如 5 的 Previous 是 4寻仗, 1 的 Previous 就是 0 刃泌,0 的 Previous 是 -1 。
  • First_Child:第一個子輪廓署尤,比如 2 的 First_Child 就是 2a 耙替,像 3a 這種有兩個 Child ,只取第一個沐寺,比如選擇 4 作為 First_Child 林艘。
  • Parent:父輪廓,比如 4 和 5 的 Parent 都是 3a 混坞,3a 的 Parent 是 3 狐援。

關于輪廓層級的問題,參考閱讀:《Tutorial: Contours Hierarchy

findContours

了解了 contour 相關的基礎概念之后究孕,接下來就是在 OpenCV 里的具體代碼了啥酱。
findContours 是尋找輪廓的函數,函數定義如下:

cv2.findContours(image, mode, method) → image, contours, hierarchy

其中:

  • image:資源圖片厨诸,8 bit 單通道镶殷,一般需要將普通的 BGR 圖片通過 cvtColor 函數轉換。

  • mode:邊緣檢測的模式微酬,包括:

  • CV_RETR_EXTERNAL:只檢索最大的外部輪廓(extreme outer)绘趋,沒有層級關系,只取根節(jié)點的輪廓颗管。

  • CV_RETR_LIST:檢索所有輪廓陷遮,但是沒有 Parent 和 Child 的層級關系,所有輪廓都是同級的垦江。

  • CV_RETR_CCOMP:檢索所有輪廓帽馋,并且按照二級結構組織:外輪廓和內輪廓。以前面的大圖為例比吭,0绽族、1、2衩藤、3吧慢、4、5 都屬于第0層赏表,2a 和 3a 都屬于第1層娄蔼。

  • CV_RETR_TREE:檢索所有輪廓怖喻,并且按照嵌套關系組織層級底哗。以前面的大圖為例岁诉,0、1跋选、2 屬于第0層涕癣,2a 屬于第1層,3 屬于第2層前标,3a 屬于第3層坠韩,4、5 屬于第4層炼列。

  • method:邊緣近似的方法只搁,包括:

  • CV_CHAIN_APPROX_NONE:嚴格存儲所有邊緣點,即:序列中任意兩個點的距離均為1俭尖。

  • CV_CHAIN_APPROX_SIMPLE:壓縮邊緣氢惋,通過頂點繪制輪廓。

drawContours

drawContours 是繪制邊緣的函數稽犁,可以傳入 findContours
函數返回的輪廓結果焰望,在目標圖像上繪制輪廓。函數定義如下:

Python: cv2.drawContours(image, contours, contourIdx, color) → image

其中:

  • image:目標圖像已亥,直接修改目標的像素點熊赖,實現繪制。

  • contours:需要繪制的邊緣數組虑椎。

  • contourIdx:需要繪制的邊緣索引震鹉,如果全部繪制則為 -1。

  • color:繪制的顏色捆姜,為 BGR 格式的 Scalar 传趾。

  • thickness:可選,繪制的密度娇未,即描繪輪廓時所用的畫筆粗細墨缘。

  • lineType: 可選,連線類型零抬,分為以下幾種:

  • LINE_4:4-connected line镊讼,只有相鄰的點可以連接成線,一個點有四個相鄰的坑位平夜。

  • LINE_8:8-connected line蝶棋,相鄰的點或者斜對角相鄰的點可以連接成線,一個點有四個相鄰的坑位和四個斜對角相鄰的坑位忽妒,所以一共有8個坑位玩裙。

  • LINE_AA:antialiased line兼贸,抗鋸齒連線。

  • hierarchy:可選吃溅,如果需要繪制某些層級的輪廓時作為層級關系傳入溶诞。

  • maxLevel:可選,需要繪制的層級中的最大級別决侈。如果為1螺垢,則只繪制最外層輪廓,如果為2赖歌,繪制最外層和第二層輪廓枉圃,以此類推。

moments

矩(moment)起源于物理學的力矩庐冯,最早由阿基米德提出孽亲,后來發(fā)展到統(tǒng)計學,再后來到數學進行歸納展父。本質上來講返劲,物理學和統(tǒng)計學的矩都是數學上矩的特例。
物理學中的矩表示作用力促使物體繞著支點旋轉的趨向犯祠,通俗理解就像是擰螺絲時用的扭轉的力旭等,由矢量和作用力組成。
數學中的矩用來描述數據分布特征的一類數字特征衡载,例如:算術平均數搔耕、方差、標準差痰娱、平均差弃榨,這些值都是矩。在實數域上的實函數 f(x) 相對于值 c 的 n 階矩為:


常用的矩有兩類:

  • 原點矩(raw moment):相對原點的矩梨睁,即當 c 為 0 的時候鲸睛。1階原點矩為期望,也成為中心坡贺。
  • 中心矩(central moment):相對于中心點的矩官辈,即當 c 為 E(x) 的時候。1階中心矩為0遍坟,2階中心矩為方差拳亿。

到了圖像處理領域,對于灰度圖(單通道愿伴,每個像素點由一個數值來表示)而言肺魁,把坐標看成二維變量 (X, Y),那么圖像可以用二維灰度密度函數 I(x, y) 來表示隔节。
簡單來講鹅经,圖像的矩就是圖像的像素相對于某個點的分布情況統(tǒng)計寂呛,是圖像的一種特征描述。

raw moment

圖像的原點矩(raw moment)是相對于原點的矩瘾晃,公式為:


對于圖像的原點矩而言:

  • M00 相當于權重系數為 1 贷痪。將所有 I(x, y) 相加,對于二值圖像而言酗捌,相當于將每個點記為 1 然后求和呢诬,也就是圖像的面積;對于灰度圖像而言胖缤,則是圖像的灰度值的和。
  • M10 相當于權重為 x 阀圾。對二值圖像而言哪廓,相當于將所有的 x 坐標相加。
  • M01 相當于權重為 y 初烘。對二值圖像而言涡真,相當于將所有的 y 坐標相加。
    圖像的幾何中心(centroid)等于 (M10 / M00 , M01 / M00)肾筐。
central moment

圖像的中心矩(central moment)是相對于幾何中心的矩哆料,公式為:


可以看到,中心矩表現的是圖像相對于幾何中心的分布情況吗铐。一個通用的描述中心矩和原點矩關系的公式是:

中心矩在圖像處理中的一個應用便是尋找不變矩(invariant moments)东亦,這是一個高度濃縮的圖像特征。
所謂的不變性有三種唬渗,分別對應圖像處理中的三種仿射變換:

  • 平移不變性(translation invariants):中心矩本身就具有平移不變性典阵,因為它是相對于自身的中心的分布統(tǒng)計,相當于是采用了相對坐標系镊逝,而平移改變的是整體坐標壮啊。
  • 縮放不變性(scale invariants):為了實現縮放不變性,可以構造一個規(guī)格化的中心矩撑蒜,即將中心矩除以 (1+(i+j)/2) 階的0階中心矩歹啼,具體公式見 《Wiki: scale invariants》。
  • 旋轉不變性(rotation invariants):通過2階和3階的規(guī)格化中心矩可以構建7個不變矩組座菠,構成的特征量具有旋轉不變性狸眼。具體可以看 《Wiki: rotation invariants》。

Hu moment 和 Zernike moment 之類的內容就不繼續(xù)展開了辈灼,感興趣的可以翻閱相關文章份企。

OpenCV + QRCode

接下來就是將 QRCode 和 OpenCV 結合起來的具體使用了。
初步構想的識別步驟如下:

  • 加載圖像巡莹,并且進行一些預處理司志,比如通過高斯模糊去噪甜紫。
  • 通過 Canny 邊緣檢測算法,找出圖像中的邊緣
  • 尋找邊緣中的輪廓骂远,將嵌套層數大于 4 的邊緣找出囚霸,得到 Position Detection Pattern 。
  • 如果上一步得到的結果不為 3 激才,則通過 Timing Pattern 去除錯誤答案拓型。
  • 計算定位標記的最小矩形包圍盒,獲得三個最外圍頂點瘸恼,算出第四個頂點劣挫,從而確定二維碼的區(qū)域。
  • 計算定位標記的幾何中心东帅,連線組成三角形压固,從而修正坐標,得到仿射變換前的 QRCode 靠闭。

在接下來的內容里帐我,將會嘗試用 OpenCV 識別下圖中的二維碼:

加載圖像

首先加載圖像,并通過 matplotlib 顯示圖像查看效果:

%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
def show(img, code=cv2.COLOR_BGR2RGB):
    cv_rgb = cv2.cvtColor(img, code)
    fig, ax = plt.subplots(figsize=(16, 10))
    ax.imshow(cv_rgb)
    fig.show()
img = cv2.imread('1.jpg')
show(img)

OpenCV 中默認是 BGR 通道愧膀,通過 cvtColor
函數將原圖轉換成灰度圖:

img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
邊緣檢測

有了灰度圖之后拦键,接下來用 Canny 邊緣檢測算法檢測邊緣。
Canny 邊緣檢測算法主要是以下幾個步驟:

  • 用高斯濾波器平滑圖像去除噪聲干擾(低通濾波器消除高頻噪聲)檩淋。
  • 生成每個點的亮度梯度圖(intensity gradients)芬为,以及亮度梯度的方向。
  • 通過非極大值抑制(non-maximum suppression)縮小邊緣寬度狼钮。非極大值抑制的意思是碳柱,只保留梯度方向上的極大值,刪除其他非極大值熬芜,從而實現銳化的效果莲镣。
  • 通過雙閾值法(double threshold)尋找潛在邊緣。大于高閾值為強邊緣(strong edge)涎拉,保留瑞侮;小于低閾值則刪除;不大不小的為弱邊緣(weak edge)鼓拧,待定半火。
  • 通過遲滯現象(Hysteresis)處理待定邊緣。弱邊緣有可能是邊緣季俩,也可能是噪音钮糖,判斷標準是:如果一個弱邊緣點附近的八個相鄰點中,存在一個強邊緣,則此弱邊緣為強邊緣店归,否則排除阎抒。

在 OpenCV 中可以直接使用 Canny 函數,不過在那之前要先用 GaussianBlur 函數進行高斯模糊:

img_gb = cv2.GaussianBlur(img_gray, (5, 5), 0)

接下來使用 Canny 函數檢測邊緣消痛,選擇 100 和 200 作為高低閾值:

edges = cv2.Canny(img_gray, 100 , 200)

執(zhí)行結果如下:


可以看到圖像中的很多噪音都被處理掉了且叁,只剩下了邊緣部分。

尋找定位標記

有了邊緣之后秩伞,接下來就是通過輪廓定位圖像中的二維碼逞带。二維碼的 Position Detection Pattern 在尋找輪廓之后,應該是有6層(因為一條邊緣會被識別出兩個輪廓纱新,外輪廓和內輪廓):


所以展氓,如果簡單處理的話,只要遍歷圖像的層級關系怒炸,然后嵌套層數大于等于5的取出來就可以了:

img_fc, contours, hierarchy = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
hierarchy = hierarchy[0]
found = []
for i in range(len(contours)):
    k = i
    c = 0
    while hierarchy[k][2] != -1:
        k = hierarchy[k][2]
        c = c + 1
    if c >= 5:
        found.append(i)
for i in found:
    img_dc = img.copy()
    cv2.drawContours(img_dc, contours, i, (0, 255, 0), 3)
    show(img_dc)

繪制結果如下:

定位篩選

接下來就是把所有找到的定位標記進行篩選带饱。如果剛好找到三個那就可以直接跳過這一步了。然而阅羹,因為這張圖比較特殊,找出了四個定位標記教寂,所以需要排除一個錯誤答案捏鱼。
講真,如果只靠三個 Position Detection Pattern 組成的直角三角形酪耕,是沒辦法從這四個當中排除錯誤答案的导梆。因為,一方面會有形變的影響迂烁,比如斜躺著的二維碼看尼,本身三個頂點連線就不是直角三角形;另一方面盟步,極端情況下藏斩,多余的那個標記如果位置比較湊巧的話,完全和正確結果一模一樣却盘,比如下面這種情況:


所以我們需要 Timing Pattern 的幫助狰域,也就是定位標記之間的黑白相間的那兩條黑白相間的線。解決思路大致如下:

  • 將4個定位標記兩兩配對
  • 將他們的4個頂點兩兩連線黄橘,選出最短的那兩根
  • 如果兩根線都不符合 Timing Pattern 的特征兆览,則出局
尋找定位標記的頂點

找的的定位標記是一個輪廓結果,由許多像素點組成塞关。如果想找到定位標記的頂點抬探,則需要找到定位標記的矩形包圍盒。先通過 minAreaRect
函數將檢查到的輪廓轉換成最小矩形包圍盒帆赢,并且繪制出來:

draw_img = img.copy()
for i in found:
    rect = cv2.minAreaRect(contours[i])
    box = np.int0(cv2.boxPoints(rect))
    cv2.drawContours(draw_img,[box], 0, (0,0,255), 2)
show(draw_img)

繪制如下:


這個矩形包圍盒的四個坐標點就是頂點小压,將它存儲在 boxes 中:

boxes = []
for i in found:
    rect = cv2.minAreaRect(contours[i])
    box = np.int0(cv2.boxPoints(rect))
    box = [tuple(x) for x in box]
    boxes.append(box)

定位標記的頂點連線
接下來先遍歷所有頂點連線线梗,然后從中選擇最短的兩根,并將它們繪制出來:

def cv_distance(P, Q):
    return int(math.sqrt(pow((P[0] - Q[0]), 2) + pow((P[1] - Q[1]),2)))
def check(a, b):
    # 存儲 ab 數組里最短的兩點的組合
    s1_ab = ()
    s2_ab = ()
    # 存儲 ab 數組里最短的兩點的距離场航,用于比較
    s1 = np.iinfo('i').max
    s2 = s1
    for ai in a:
        for bi in b:
            d = cv_distance(ai, bi)
            if d < s2:
                if d < s1:
                    s1_ab, s2_ab = (ai, bi), s1_ab
                    s1, s2 = d, s1
                else:
                    s2_ab = (ai, bi)
                    s2 = d              
    a1, a2 = s1_ab[0], s2_ab[0]
    b1, b2 = s1_ab[1], s2_ab[1]
    # 將最短的兩個線畫出來
    cv2.line(draw_img, a1, b1, (0,0,255), 3)
    cv2.line(draw_img, a2, b2, (0,0,255), 3)
for i in range(len(boxes)):
    for j in range(i+1, len(boxes)):
        check(boxes[i], boxes[j])
show(draw_img)

繪制結果如下:


獲取連線上的像素值
有了端點連線缠导,接下來需要獲取連線上的像素值,以便后面判斷是否是 Timing Pattern 溉痢。
在這之前僻造,為了更方便的判斷黑白相間的情況,先對圖像進行二值化:

th, bi_img = cv2.threshold(img_gray, 100, 255, cv2.THRESH_BINARY)

接下來是獲取連線像素值孩饼。由于 OpenCV3 的 Python 庫中沒有 LineIterator
髓削,只好自己寫一個。在《OpenCV 3.0 Python LineIterator》這個問答里找到了可用的直線遍歷函數镀娶,可以直接使用立膛。
以一條 Timing Pattern 為例:


打印其像素點看下結果:

[ 255.  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.    0.
    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
    0.  255.  255.  255.    0.    0.    0.    0.    0.    0.    0.    0.
    0.    0.    0.    0.    0.    0.    0.    0.    0.  255.  255.  255.
  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.
  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.
    0.    0.    0.  255.  255.  255.  255.  255.  255.  255.  255.  255.
  255.    0.  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.
  255.  255.  255.  255.  255.  255.  255.  255.  255.]
修正端點位置

照理說, Timing Pattern 的連線梯码,像素值應該是黑白均勻相間才對宝泵,為什么是上面的這種一連一大片的結果呢?
仔細看下截圖可以發(fā)現轩娶,由于取的是定位標記的外部包圍盒的頂點儿奶,所以因為誤差會超出定位標記的范圍,導致沒能正確定位到 Timing Pattern 鳄抒,而是相鄰的 Data 部分的像素點闯捎。
為了修正這部分誤差,我們可以對端點坐標進行調整许溅。因為 Position Detection Pattern 的大小是固定的瓤鼻,是一個 1-1-3-1-1 的黑白黑白黑相間的正方形,識別 Timing Pattern 的最佳端點應該是最靠里的黑色區(qū)域的中心位置贤重,也就是圖中的綠色虛線部分:


所以我們需要對端點坐標進行調整茬祷。調整方式是,將一個端點的 x 和 y 值向另一個端點的 x 和 y 值靠近 1/14 個單位距離游桩,代碼如下:

a1 = (a1[0] + (a2[0]-a1[0])*1/14, a1[1] + (a2[1]-a1[1])*1/14)
b1 = (b1[0] + (b2[0]-b1[0])*1/14, b1[1] + (b2[1]-b1[1])*1/14)
a2 = (a2[0] + (a1[0]-a2[0])*1/14, a2[1] + (a1[1]-a2[1])*1/14)
b2 = (b2[0] + (b1[0]-b2[0])*1/14, b2[1] + (b1[1]-b2[1])*1/14)

調整之后的像素值就是正確的 Timing Pattern 了:

[ 255.  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.    0.
    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.  255.  255.
  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.    0.
    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.  255.  255.
  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.    0.
    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.  255.  255.
  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.    0.    0.
    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.  255.  255.
  255.  255.  255.  255.  255.  255.  255.  255.  255.  255.    0.    0.
    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.  255.  255.
  255.  255.  255.  255.  255.  255.  255.  255.  255.]
驗證是否是 Timing Pattern

像素序列拿到了牲迫,接下來就是判斷它是否是 Timing Pattern 了。 Timing Pattern 的特征是黑白均勻相間借卧,所以每段同色區(qū)域的計數結果應該相同盹憎,而且旋轉拉伸平移都不會影響這個特征。
于是铐刘,驗證方案是:

  • 先除去數組中開頭和結尾處連續(xù)的白色像素點陪每。
  • 對數組中的元素進行計數,相鄰的元素如果值相同則合并到計數結果中。比如 [0,1,1,1,0,0] 的計數結果就是 [1,3,2] 檩禾。
  • 計數數組的長度如果小于 5 挂签,則不是 Timing Pattern 。
  • 計算計數數組的方差盼产,看看分布是否離散饵婆,如果方差大于閾值,則不是 Timing Pattern 戏售。

代碼如下:

def isTimingPattern(line):
    # 除去開頭結尾的白色像素點
    while line[0] != 0:
        line = line[1:]
    while line[-1] != 0:
        line = line[:-1]
    # 計數連續(xù)的黑白像素點
    c = []
    count = 1
    l = line[0]
    for p in line[1:]:
        if p == l:
            count = count + 1
        else:
            c.append(count)
            count = 1
        l = p
    c.append(count)
    # 如果黑白間隔太少侨核,直接排除
    if len(c) < 5:
        return False
    # 計算方差,根據離散程度判斷是否是 Timing Pattern
    threshold = 5
    return np.var(c) < threshold

對前面的那條連線檢測一下灌灾,計數數組為:

[11, 12, 11, 12, 11, 12, 11, 13, 11]

方差為 0.47 搓译。其他非 Timing Pattern 的連線方差均大于 10 。

找出錯誤的定位標記

接下來就是利用前面的結果除去錯誤的定位標記了锋喜,只要兩個定位標記的端點連線中能找到 Timing Pattern 些己,則這兩個定位標記有效,把它們存進 set 里:

valid = set()
for i in range(len(boxes)):
    for j in range(i+1, len(boxes)):
        if check(boxes[i], boxes[j]):
            valid.add(i)
            valid.add(j)
print valid

結果是:

set([1, 2, 3])

好了嘿般,它們中出了一個叛徒段标,0、1炉奴、2怀樟、3 四個定位標記,0是無效的盆佣,1、2械荷、3 才是需要識別的 QRCode 的定位標記共耍。

找出二維碼

有了定位標記之后,找出二維碼就輕而易舉了吨瞎。只要找出三個定位標記輪廓的最小矩形包圍盒痹兜,那就是二維碼的位置了:

contour_all = np.array([])
while len(valid) > 0:
    c = found[valid.pop()]
    for sublist in c:
        for p in sublist:
            contour_all.append(p)
rect = cv2.minAreaRect(contour_ALL)
box = cv2.boxPoints(rect)
box = np.array(box)
draw_img = img.copy()
cv2.polylines(draw_img, np.int32([box]), True, (0, 0, 255), 10)
show(draw_img)

繪制結果如下:

小結

后面仿射變換后坐標修正的問題實在是寫不動了,這篇就先到這里吧颤诀。
回頭看看字旭,是不是感覺繞了個大圈子?
『費了半天勁崖叫,只是為了告訴我第0個定位標記是無效的遗淳,我看圖也看出來了啊心傀!』
是的屈暗,不過代碼里能看到的只是像素值和它們的坐標,為了排除這個錯誤答案確實花了不少功夫。
不過這也是我喜歡做數字圖像處理的原因之一:可用函數數不勝數养叛,專業(yè)概念層出不窮种呐,同樣的一個問題,不同的人去解決弃甥,就有著不同的答案爽室,交流的過程便是學習的過程。

參考文獻:

二維碼的生成細節(jié)和原理
What is a QR code?
ISO/IEC 18004: QRCode Standard
What Are The Different Sections In A QR Code?
Decoding small QR codes by hand
How data matrix codes work
QR Code Tutorial
How to Read QR Symbols Without Your Mobile Telephone
OpenCV: QRCode detection and extraction
Tutorial Python: Contours Hierarchy
Wiki: Pixel Connectivity
Image Processing: Connect
Wiki: Image Moment
Wiki: Moment (Mathematics)
圖像的矩特征
統(tǒng)計數據的形態(tài)特征
圖像的矩(Image Moments)
OpenCV Doc: Structural analysis and shape descriptors
CS7960 AdvImProc MomentInvariants
OpenCV Doc: Canny
Wiki: Canny Edge Detector
Wiki: Hysteresis
OpenCV 3.0 Python LineIterator

完整代碼:

# -*- coding: utf-8 -*-
"""
Spyder Editor

This is a temporary script file.
"""

import cv2
import math
from matplotlib import pyplot as plt
import numpy as np

def show(img, code=cv2.COLOR_BGR2RGB):
    cv_rgb = cv2.cvtColor(img, code)
    fig, ax = plt.subplots(figsize=(16, 10))
    ax.imshow(cv_rgb)
    fig.show()
    
img = cv2.imread('qr_test.jpg')

img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img_gb = cv2.GaussianBlur(img_gray, (5, 5), 0)
edges = cv2.Canny(img_gray, 100 , 200)
img_fc, contours, hierarchy = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
hierarchy = hierarchy[0]
found = []
for i in range(len(contours)):
    k = i
    c = 0
    while hierarchy[k][2] != -1:
        k = hierarchy[k][2]
        c = c + 1   # count hierarchy
    if c >= 5:
        found.append(i) # store index

#for i in found:
#    img_dc = img.copy()
#    cv2.drawContours(img_dc, contours, i, (0, 255, 0), 3)
#    #show(img_dc)
# 對圖像進行二值化
th, bi_img = cv2.threshold(img_gray, 100, 255, cv2.THRESH_BINARY)
draw_img = img.copy()
boxes = []
for i in found:
    rect = cv2.minAreaRect(contours[i])
    box = np.int0(cv2.boxPoints(rect))
#    cv2.drawContours(draw_img,[box], 0, (0,0,255), 2)
    #box = map(tuple, box)
    box = [tuple(x) for x in box]
    boxes.append(box)
#show(draw_img) 
#print("Length of Boxes is ",len(boxes))

def createLineIterator(P1, P2, img):
    """
    Produces and array that consists of the coordinates and intensities of each pixel in a line between two points

    Parameters:
        -P1: a numpy array that consists of the coordinate of the first point (x,y)
        -P2: a numpy array that consists of the coordinate of the second point (x,y)
        -img: the image being processed

    Returns:
        -it: a numpy array that consists of the coordinates and intensities of each pixel in the radii (shape: [numPixels, 3], row = [x,y,intensity])     
    """
    #define local variables for readability
    imageH = img.shape[0]
    imageW = img.shape[1]
    P1X = P1[0]
    P1Y = P1[1]
    P2X = P2[0]
    P2Y = P2[1]

    #difference and absolute difference between points
    #used to calculate slope and relative location between points
    dX = P2X - P1X
    dY = P2Y - P1Y
    dXa = np.abs(dX)
    dYa = np.abs(dY)

    #predefine numpy array for output based on distance between points
    itbuffer = np.empty(shape=(np.maximum(dYa,dXa),3),dtype=np.float32)
    itbuffer.fill(np.nan)

    #Obtain coordinates along the line using a form of Bresenham's algorithm
    negY = P1Y > P2Y
    negX = P1X > P2X
    if P1X == P2X: #vertical line segment
        itbuffer[:,0] = P1X
        if negY:
            itbuffer[:,1] = np.arange(P1Y - 1,P1Y - dYa - 1,-1)
        else:
            itbuffer[:,1] = np.arange(P1Y+1,P1Y+dYa+1)              
    elif P1Y == P2Y: #horizontal line segment
        itbuffer[:,1] = P1Y
        if negX:
            itbuffer[:,0] = np.arange(P1X-1,P1X-dXa-1,-1)
        else:
            itbuffer[:,0] = np.arange(P1X+1,P1X+dXa+1)
    else: #diagonal line segment
        steepSlope = dYa > dXa
        if steepSlope:
            slope = dX.astype(np.float32)/dY.astype(np.float32)
            if negY:
                itbuffer[:,1] = np.arange(P1Y-1,P1Y-dYa-1,-1)
            else:
                itbuffer[:,1] = np.arange(P1Y+1,P1Y+dYa+1)
            itbuffer[:,0] = (slope*(itbuffer[:,1]-P1Y)).astype(np.int) + P1X
        else:
            slope = dY.astype(np.float32)/dX.astype(np.float32)
            if negX:
                itbuffer[:,0] = np.arange(P1X-1,P1X-dXa-1,-1)
            else:
                itbuffer[:,0] = np.arange(P1X+1,P1X+dXa+1)
            itbuffer[:,1] = (slope*(itbuffer[:,0]-P1X)).astype(np.int) + P1Y

    #Remove points outside of image
    colX = itbuffer[:,0]
    colY = itbuffer[:,1]
    itbuffer = itbuffer[(colX >= 0) & (colY >=0) & (colX<imageW) & (colY<imageH)]

    #Get intensities from img ndarray
    itbuffer[:,2] = img[itbuffer[:,1].astype(np.uint),itbuffer[:,0].astype(np.uint)]

    return itbuffer

def isTimingPattern(line):
    # 除去開頭結尾的白色像素點
    while line[0] != 0:
        line = line[1:]
    while line[-1] != 0:
        line = line[:-1]
    # 計數連續(xù)的黑白像素點
    c = []
    count = 1
    l = line[0]
    for p in line[1:]:
        if p == l:
            count = count + 1
        else:
            c.append(count)
            count = 1
        l = p
    c.append(count)
    # 如果黑白間隔太少淆攻,直接排除
    if len(c) < 5:
        return False
    # 計算方差阔墩,根據離散程度判斷是否是 Timing Pattern
    threshold = 5
    return np.var(c) < threshold
    
def cv_distance(P, Q):
    return int(math.sqrt(pow((P[0] - Q[0]), 2) + pow((P[1] - Q[1]),2)))
    
def check(a, b):
    # 存儲 ab 數組里最短的兩點的組合
    s1_ab = ()
    s2_ab = ()
    # 存儲 ab 數組里最短的兩點的距離,用于比較
    s1 = np.iinfo('i').max
    s2 = s1
    for ai in a:
        for bi in b:
            d = cv_distance(ai, bi)
            if d < s2:
                if d < s1:
                    s1_ab, s2_ab = (ai, bi), s1_ab
                    s1, s2 = d, s1
                else:
                    s2_ab = (ai, bi)
                    s2 = d

    a1, a2 = s1_ab[0], s2_ab[0]
    b1, b2 = s1_ab[1], s2_ab[1]
    
    a1 = (a1[0] + np.int0((a2[0]-a1[0])*1/14), a1[1] + np.int0((a2[1]-a1[1])*1/14))
    b1 = (b1[0] + np.int0((b2[0]-b1[0])*1/14), b1[1] + np.int0((b2[1]-b1[1])*1/14))
    a2 = (a2[0] + np.int0((a1[0]-a2[0])*1/14), a2[1] + np.int0((a1[1]-a2[1])*1/14))
    b2 = (b2[0] + np.int0((b1[0]-b2[0])*1/14), b2[1] + np.int0((b1[1]-b2[1])*1/14))
    
    # 將最短的兩個線畫出來
    #cv2.line(draw_img, a1, b1, (0,0,255), 3)
    #cv2.line(draw_img, a2, b2, (0,0,255), 3)
    lit1 = createLineIterator(a1,b1,bi_img)
    lit2 = createLineIterator(a2,b2,bi_img)
    if isTimingPattern(lit1[:,2]):
        return True
    elif isTimingPattern(lit2[:,2]):
        return True
    else:
        return False
    

valid = set()
for i in range(len(boxes)):
    for j in range(i+1, len(boxes)):
        if check(boxes[i], boxes[j]):
            valid.add(i)
            valid.add(j)
#show(draw_img)
print(valid)

contour_all = []
while len(valid) > 0:
    c = contours[found[valid.pop()]]
    for sublist in c:
        for p in sublist:
            contour_all.append(p)
            
rect = cv2.minAreaRect(np.array(contour_all))
box = np.array([cv2.boxPoints(rect)],dtype=np.int0)
cv2.polylines(draw_img, box, True, (0, 0, 255), 3)
show(draw_img)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末卜录,一起剝皮案震驚了整個濱河市戈擒,隨后出現的幾起案子,更是在濱河造成了極大的恐慌艰毒,老刑警劉巖筐高,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異丑瞧,居然都是意外死亡柑土,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門绊汹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來稽屏,“玉大人,你說我怎么就攤上這事西乖『疲” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵获雕,是天一觀的道長薄腻。 經常有香客問我梧宫,道長朱监,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任伞矩,我火速辦了婚禮楣颠,結果婚禮上尽纽,老公的妹妹穿的比我還像新娘。我一直安慰自己童漩,他們只是感情好弄贿,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著睁冬,像睡著了一般挎春。 火紅的嫁衣襯著肌膚如雪看疙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天直奋,我揣著相機與錄音能庆,去河邊找鬼。 笑死脚线,一個胖子當著我的面吹牛搁胆,可吹牛的內容都是我干的。 我是一名探鬼主播邮绿,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼渠旁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了船逮?” 一聲冷哼從身側響起顾腊,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挖胃,沒想到半個月后杂靶,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡酱鸭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年吗垮,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凹髓。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡烁登,死狀恐怖,靈堂內的尸體忽然破棺而出蔚舀,到底是詐尸還是另有隱情饵沧,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布赌躺,位于F島的核電站捷泞,受9級特大地震影響,放射性物質發(fā)生泄漏寿谴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一失受、第九天 我趴在偏房一處隱蔽的房頂上張望讶泰。 院中可真熱鬧,春花似錦拂到、人聲如沸痪署。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狼犯。三九已至余寥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間悯森,已是汗流浹背宋舷。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓢姻,地道東北人祝蝠。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像幻碱,于是被迫代替她去往敵國和親绎狭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內容