格雷碼編碼+解碼+實現(Python)


格雷碼編碼+解碼+實現(Python)

點擊上方“計算機視覺工坊”,選擇“星標”

干貨第一時間送達

圖片

01 二值碼
02 格雷碼編碼
2.1 編碼優點
2.2 編碼生成
2.3 遞歸生成
2.4 二值碼轉換
2.5 編碼圖
03 格雷碼投影
3.1 投影圖案生成
3.2 DLP投影圖像
04 格雷碼解碼
4.1 全局/局部灰度閾值法
4.2 多幅圖像閾值法
4.3 特殊情況
05 參考文獻

01 二值碼

先來說結構光中最簡單的情況,時域上的編碼,由於極線約束的關系,我們只需要在單方向上進行編碼即可,我們以最簡單的兩灰度級三位二進制碼為例,這里有個區域,其中 亮區域對應編碼1, 暗區域對應編碼0,假設現在我們向被測物順序投射三幅二進制編碼圖案,如下所示: 圖片圖1 二進制碼的編碼與解碼原理現在,對於這些區域,對應的編碼如下:

圖片

這些區域都被我們編碼起來了,沒毛病!但是這樣的編碼雖然很簡單,但是存在問題!如果和格雷碼一比,你一定一眼就可以發現。
02 格雷碼編碼

2.1 編碼優點

二進制編碼缺點:相鄰區域的編碼的位數變化太多了!
那這會帶來什么問題?當然,在相機拍照清晰的情況下,這種編碼方式當然不會出現任何問題。但問題就出現在, 相機拍攝到的黑白相間的邊界點往往是一個過渡灰度,很容易導致解碼錯誤(0->1 or 1->0),這是自然二進制編碼解碼最容易出錯的點。而 格雷碼最大的特定是相鄰數字編碼只相差一位,它們的對比如下所示:

圖片

這有什么優點呢?格雷碼出錯的概率更小,因為相鄰區域的編碼只有一位差異,有兩種情況,假設編碼只有一位差異,這一位錯誤編碼出現在:
  • 非差異位:對這類編碼錯誤,我們完全可以進行補救,因為相鄰兩個像素的編碼應該是大部分相同的,我們可以對相鄰兩個像素的編碼進行糾正,而二進制碼可沒有這個編碼糾正機制;
  • 差異位:那無非是差一個像素而已,這時候我們無法區分這兩塊區域;
舉個例子,對001(1)區域,它最容易出現錯誤的區域是黑白相間的邊界處,錯誤的編碼:011:
  • 二值碼:3區域,差2個像素;
  • 格雷碼:2區域,差1個像素,
另外,在編碼的最后一幅圖像里,條紋都是非常細的,以上面3位編碼為例,查看編碼最后位,如果是:
  • 二值碼:01010101
  • 格雷碼:01100110
由於漫反射的原因,通常容易出錯的地方是黑白交錯的區域解碼,當條紋在最后一幅很細的時候,明顯格雷碼編碼條紋更粗,可能出錯的地方更少。不論你是否理解,格雷碼的主要優點就在於可以減小解碼過程中的錯誤率,當然它依然有二值碼一樣的缺點,主要在於在選取位數較多的時候,最后幾幅圖的格雷碼條紋會非常細,不容易分辨,因而我們通常只選取4位格雷碼進行編碼。這樣的處理精度並不高,這也是后面我們結合相移法來進行編碼、解碼的主要原因。
補充:格雷碼的其他應用格雷碼在傳統二進制控制系統中也有廣泛應用,例如數字3的表示法為011,要切換為鄰近的數字4,也就是 100時,裝置中的三個位元都得要轉換,因此於未完全轉換的過程時裝置會經歷短暫的,010、001、101、110、111等其中數種狀態,也就是代表着2、1、5、6、7,因此此種數字編碼方法於鄰近數字轉換時有比較大的誤差可能范圍。但這樣的轉換,對於一些追求硬件性能極限的嵌入式應用,比如說飛機的電傳系統中,這樣的翻轉來不及轉換很正常!這就很尷尬!相反,格雷碼只需要一位翻轉即可!

2.2 編碼生成

圖片

  1. 改變最右邊的為值;
  2. 改變右邊第一個為1的位元,其左邊位元值;
  3. 重復1、2步;
來解釋下,以3位格雷碼為例,從原始的值 0(000):
  1. 步驟1,改變最右邊值:001
  2. 步驟2,改變右邊第一個為1的位元,其左邊的位元:011
  3. 步驟1,改變最右邊值:010
  4. 步驟2,改變右邊第一個為1的位元,其左邊的位元:110
  5. 步驟1,改變最右邊值:111
  6. 步驟2,改變右邊第一個為1的位元,其左邊的位元:101
如果按照這個步驟來生成格雷碼,對計算機來說,每次要去找右邊第一個1,然后去翻轉,其實是很麻煩的,而且這里其實有些操作是冗余的。

2.3 遞歸生成

我們來看格雷碼其它的特點:
  1. 除了最高位(左邊第一位),格雷碼的位元完全對稱
    • 第一個00,和最后個00;
    • 第二個01,和最后個01;
  2. 而最高位的規律就更容易了,前面的格雷碼為0,后面的為1

圖片

所以,格雷碼的生成步驟:
  1. 產生0,1兩個字符串;0、1
  2. 在第一步基礎上:
    • 每個字符串前都+0->0+0、0+1
    • 翻轉首個元素,其余對稱:1+1、1+0
  • 最終:00、01、11、10
  1. 在上一步基礎上:
    • 每個字符串前都+0->0+00、0+01、0+11、0+10
    • 翻轉首字符,其余對稱:1+10、1+11、1+01、1+00
  • 最終:000、001、011、010、110、111、101、100
之后遞歸即可!我們用C++代碼來實現一下,采用遞歸的形式:
/*==================================================@Project:GrayCode@File : main@Desc :生成格雷碼----------------------------------------------------@Author :Jianbin Cao@Email : fly_cjb@163.com@Date :2020/11/10 20:40==================================================*/#include <iostream>#include <vector>#include <cassert>using namespace std;

vector<string> GrayCode(int n) { if (n < 1) { cout << "格雷碼數量必須大於0" << endl; assert(0); } else if (n == 1) { vector<string> code; code.emplace_back("0"); code.emplace_back("1"); return code; } else { vector<string> code; vector<string> code_pre = GrayCode(n - 1); for (int idx = 0; idx < code_pre.size(); ++idx) { code.push_back("0" + code_pre[idx]); } for (int idx = int(code_pre.size() - 1); idx >= 0; --idx) { code.push_back("1" + code_pre[idx]); } return code; }}

int main(){ int n = 4; vector<string> gray_code = GrayCode(n); for (auto &g : gray_code){ cout << g << endl; }}

2.4 二值碼轉換

三步:
  1. 最高位保留
  2. 格雷碼的次高位:二進制碼最高位與次高位的亦或操作;
  3. 其余位的格雷碼依次類推圖片
vector<int> GrayCode2(int n){ int count = 1 << n; vector<int> res(count,0); for(int i = 1 ; i < count; i ++) { int bin = i,cur = bin >> (n - 1); for(int k = n - 1;k > 0;k --) cur = (cur << 1) + (((bin >> k) & 1) ^ ((bin >>(k - 1)) & 1)); res[i] = cur; } return res;}vector<int> gray_code2 = GrayCode2(n); for (auto &g : gray_code2){ cout << (bitset<n>)g << endl; }

2.5 編碼圖

圖片圖2 相移+格雷碼編碼圖(查看格雷碼部分)[3]注:

圖片

03 格雷碼投影

3.1 投影圖案生成

結合格雷碼生成和編碼圖,這段代碼就很好寫了,我們來寫一下,這回我們用Python來寫(人生苦短!):
import cv2import numpy as np
class GrayCode: codes = np.array([]) k2code = {} k2v = {} v2k = {}
def __init__(self, n:int=3): self.n = n self.codes = self.__creatCode(self.n)
# 從k(idx)轉換到格雷碼 for k in range(2**n): self.k2code[k] = self.__k2code(k) # 從格雷碼轉換到v for k in range(2 ** n): self.k2v[k] = self.__k2v(k) # 從v轉換到k(idx) for k, v in self.k2v.items(): self.v2k[v] = k
def toPattern(self, idx:int, cols:int = 1280, rows:int = 800): assert (idx >= 0) row = self.codes[idx, :] one_row = np.zeros([cols], np.uint8) assert (cols % len(row) == 0) per_col = int(cols / len(row))
for i in range(len(row)): one_row[i * per_col : (i + 1) * per_col] = row[i] pattern = np.tile(one_row, (rows, 1)) * 255 return pattern
def __creatCode(self, n:int): code_temp = GrayCode.__createGrayCode(n) codes = [] for row in range(len(code_temp[0])): c = [] for idx in range(len(code_temp)): c.append(int(code_temp[idx][row])) codes.append(c) return np.array(codes, np.uint8)
def __k2code(self, k): col = self.codes[:, k] code = "" for i in col: code += str(i) return code
def __k2v(self, k): col = list(self.codes[:, k]) col = [str(i) for i in col] code = "".join(col) return int(code, 2)
@staticmethod def __createGrayCode(n:int): if n < 1: print("輸入數字必須大於0") assert (0); elif n == 1: code = ["0", "1"] return code else: code = [] code_pre = GrayCode.__createGrayCode(n - 1) for idx in range(len(code_pre)): code.append("0" + code_pre[idx]) for idx in range(len(code_pre) - 1, -1, -1): code.append("1" + code_pre[idx]) return code

if __name__ == '__main__': n = 8 g = GrayCode(n) print("code") print(g.codes) print("\nk -> code") print(g.k2code) print("\nk -> v") print(g.k2v) print("\nv -> k") print(g.v2k) for i in range(n): pattern = g.toPattern(i) title = str(i) + "-img" cv2.imshow(title, pattern) cv2.waitKey(0) cv2.destroyWindow(title)

3.2 DLP投影圖像

參考鏈接:DLP LightCrafter4500投影圖像步驟整理(一)

04 格雷碼解碼

格雷碼的解碼很簡單,只需要把投影的結構光還原回十進制數字,我們就能知道相機中像素點  對應於投影圖片的哪一列。但現在問題的關鍵是,我們相機捕獲回來的編碼圖案, 由於物體材料表面反光等因素,可能暗的地方不是那么暗,亮的地方不是那么亮,這將會給正確解碼工作帶來一定難度!換句話說,如何對相機捕獲到的結構光進行 准確的二值化操作

4.1 全局/局部灰度閾值法

最簡單的方法是設置一個全局灰度閾值,對於灰度值:高於閾值的像素點:1、低於閾值的像素點:0。或者利用局部自適應閾值對圖片進行二值化操作,比如: 利用每個像素點周邊的灰度信息進行二值化,但這類方法,由於使用結構光的環境往往是復雜的,比如說, 同樣的結構光,打在黑色物體表面的亮度,它就會比白色物體表面的亮度要低,這意味着同樣的光條紋在不同物體上獲取的灰度值不同,所以往往不能夠滿足格雷碼解碼的二值化需求!舉個例子,光部分打在高反射區域(亮度高),部分打在漫反射區域(亮度暗),這類局部自適應閾值法就不能很好適應這種場景。

4.2 多幅圖像閾值法

雖然由於環境光、以及物體表面材料原因,一副圖像中像素的灰度值通常是不均勻的,我們無法直接利用一張圖像中呈現的灰度信息對結構光進行解碼,但是我們可以利用結構光一連串圖片來幫助獲取像素點當前是亮條紋還是暗條紋。以5位的格雷碼為例,其需要投影5張結構光圖案: 圖片圖3  五位格雷碼投影圖案假設有一個編碼為11011的格雷碼條紋打在物體表面上,在連續投影的5張格雷碼圖案中,物體表面被編碼照射區域,其既經歷暗條紋(編碼0),又經歷亮條紋(1),下面這條結論式確定無疑的: 對於同一位置,其被亮條紋照射到的亮度總是高於其被暗條紋照射的亮度!那么對於一個像素點在一張圖片中的二值化,我們可以這樣操作:首先,找到像素點在一連串格雷碼圖片中的最大灰度值,記為,最小灰度值,記為 ,對於每張圖像,我們計算下面這個值:

圖片

圖片

圖片圖4 格雷碼全暗/全亮區域[3]因為這些點不會經歷明暗變化,所以你真的不好判斷是亮條紋還是暗條紋。我們有很多辦法去避免這個現象,比如說:
  • 避開這個編碼(避開了意味着要多編碼)
  • 跟其他像素點亮度做比較(但是正如之前所說,由於物體表面材料屬性不同,這個方法缺乏魯棒性)
其中,有一種魯棒性比較好的解決方法是,額外讓所有編碼編碼位置都能經歷全0或者全1的過程,這也是傳統格雷碼結合相移技術需要額外投射兩幅全黑和全白圖案的原因,如圖4所示。另外一個方法,我們額外投射一條更細的編碼,如圖5所示,互補格雷碼結合相移。當然,實際情況當然不是不簡單的多投射一條更細的格雷碼這么簡單,但總的來說,我們總歸是有辦法解決的。 圖片圖5 互補格雷碼結合相移的編碼圖 [3]

4.3 特殊情況

但上述方法奏效的前提是,假設被亮條紋照射到的亮度總是高於該位置被暗條紋照射到的亮度。但滿足這個條件的前提是: 物體間沒有漫反射,以及投影投射的光之間不會發生互相干擾,這在大多數情況下是成立的。但是有一些特殊的位置,有可能 物體表面在亮條紋時,其亮度反而比經歷暗條紋時要暗!對於這類問題,可以參考論文[1]來解決!

05 參考文獻

[1]: Robust Pixel Classification for 3D Modeling with Structured Light[2]: High-accuracy, high-speed 3D structured light imaging techniques and potential applications to intelligent robotics[3]: 第十三公開課:基於格雷碼結合相移技術的高魯棒性高效率動態三維面形測量,四川大學,吳周傑[4]: 系列篇|結構光——格雷碼解碼方法,書涵 備注:作者也是我們 「3D視覺從入門到精通」特邀嘉賓: 一個超干貨的3D視覺學習社區本文僅做學術分享,如有侵權,請聯系刪文。 下載1在「計算機視覺工坊」公眾號后台回復: 深度學習,即可下載深度學習算法、3D深度學習、深度學習框架、目標檢測、GAN等相關內容近30本pdf書籍。
下載2在「計算機視覺工坊」公眾號后台回復: 計算機視覺,即可下載計算機視覺相關17本pdf書籍,包含計算機視覺算法、Python視覺實戰、Opencv3.0學習等。
下載3在「計算機視覺工坊」公眾號后台回復: SLAM,即可下載獨家SLAM相關視頻課程,包含視覺SLAM、激光SLAM精品課程。

重磅!計算機視覺工坊-學習交流群已成立

掃碼添加小助手微信,可申請加入3D視覺工坊-學術論文寫作與投稿 微信交流群,旨在交流頂會、頂刊、SCI、EI等寫作與投稿事宜。

同時也可申請加入我們的細分方向交流群,目前主要有ORB-SLAM系列源碼學習、3D視覺CV&深度學習SLAM三維重建點雲后處理自動駕駛、CV入門、三維測量、VR/AR、3D人臉識別、醫療影像、缺陷檢測、行人重識別、目標跟蹤、視覺產品落地、視覺競賽、車牌識別、硬件選型、深度估計、學術交流、求職交流等微信群,請掃描下面微信號加群,備注:”研究方向+學校/公司+昵稱“,例如:”3D視覺 + 上海交大 + 靜靜“。請按照格式備注,否則不予通過。添加成功后會根據研究方向邀請進去相關微信群。原創投稿也請聯系。

圖片▲長按加微信群或投稿

圖片

▲長按關注公眾號

3D視覺從入門到精通知識星球:針對3D視覺領域的知識點匯總、入門進階學習路線、最新paper分享、疑問解答四個方面進行深耕,更有各類大廠的算法工程人員進行技術指導。與此同時,星球將聯合知名企業發布3D視覺相關算法開發崗位以及項目對接信息,打造成集技術與就業為一體的鐵桿粉絲聚集區,近2000星球成員為創造更好的AI世界共同進步,知識星球入口:

學習3D視覺核心技術,掃描查看介紹,3天內無條件退款

圖片 圈里有高質量教程資料、可答疑解惑、助你高效解決問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM