格雷碼編碼+解碼+實現(Python)
點擊上方“計算機視覺工坊”,選擇“星標”
干貨第一時間送達
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,假設現在我們向被測物順序投射三幅二進制編碼圖案,如下所示:
02 格雷碼編碼
2.1 編碼優點
二進制編碼缺點:相鄰區域的編碼的位數變化太多了!那這會帶來什么問題?當然,在相機拍照清晰的情況下,這種編碼方式當然不會出現任何問題。但問題就出現在, 相機拍攝到的黑白相間的邊界點往往是一個過渡灰度,很容易導致解碼錯誤(0->1 or 1->0),這是自然二進制編碼解碼最容易出錯的點。而 格雷碼最大的特定是相鄰數字編碼只相差一位,它們的對比如下所示:
- 非差異位:對這類編碼錯誤,我們完全可以進行補救,因為相鄰兩個像素的編碼應該是大部分相同的,我們可以對相鄰兩個像素的編碼進行糾正,而二進制碼可沒有這個編碼糾正機制;
- 差異位:那無非是差一個像素而已,這時候我們無法區分這兩塊區域;
- 二值碼:3區域,差2個像素;
- 格雷碼:2區域,差1個像素,
- 二值碼:01010101
- 格雷碼:01100110
補充:格雷碼的其他應用格雷碼在傳統二進制控制系統中也有廣泛應用,例如數字3的表示法為011,要切換為鄰近的數字4,也就是 100時,裝置中的三個位元都得要轉換,因此於未完全轉換的過程時裝置會經歷短暫的,010、001、101、110、111等其中數種狀態,也就是代表着2、1、5、6、7,因此此種數字編碼方法於鄰近數字轉換時有比較大的誤差可能范圍。但這樣的轉換,對於一些追求硬件性能極限的嵌入式應用,比如說飛機的電傳系統中,這樣的翻轉來不及轉換很正常!這就很尷尬!相反,格雷碼只需要一位翻轉即可!
2.2 編碼生成
- 改變最右邊的為值;
- 改變右邊第一個為1的位元,其左邊位元值;
- 重復1、2步;
- 步驟1,改變最右邊值:001
- 步驟2,改變右邊第一個為1的位元,其左邊的位元:011
- 步驟1,改變最右邊值:010
- 步驟2,改變右邊第一個為1的位元,其左邊的位元:110
- 步驟1,改變最右邊值:111
- 步驟2,改變右邊第一個為1的位元,其左邊的位元:101
2.3 遞歸生成
我們來看格雷碼其它的特點:- 除了最高位(左邊第一位),格雷碼的位元完全對稱
- 第一個00,和最后個00;
- 第二個01,和最后個01;
- …
- 而最高位的規律就更容易了,前面的格雷碼為0,后面的為1
- 產生0,1兩個字符串;0、1
- 在第一步基礎上:
- 每個字符串前都+0->0+0、0+1
- 翻轉首個元素,其余對稱:1+1、1+0
- 最終:00、01、11、10
- 在上一步基礎上:
- 每個字符串前都+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
/*==================================================
@Project:GrayCode
@File : main
@Desc :生成格雷碼
----------------------------------------------------
@Author :Jianbin Cao
@Email : fly_cjb@163.com
@Date :2020/11/10 20:40
==================================================*/
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 二值碼轉換
三步:- 最高位保留
- 格雷碼的次高位:二進制碼最高位與次高位的亦或操作;
- 其余位的格雷碼依次類推
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 編碼圖

03 格雷碼投影
3.1 投影圖案生成
結合格雷碼生成和編碼圖,這段代碼就很好寫了,我們來寫一下,這回我們用Python來寫(人生苦短!):import cv2
import 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張結構光圖案:

- 避開這個編碼(避開了意味着要多編碼)
- 跟其他像素點亮度做比較(但是正如之前所說,由於物體表面材料屬性不同,這個方法缺乏魯棒性)

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天內無條件退款