结构光之格雷码编码加相移算法详解与实现(多种编码程序)

2021年9月16日 10点热度 0条评论 来源: 视觉小新

格雷码+相移法既可以减少格雷码的编码位数,加快解码速度,也可以弥补单纯的相移法和格雷码法的对不连续位置难以重建的缺点。

操作过程如下:

采用格雷码与相移结合的时间编码方法,具体的编码方法为:首先向被测物投射一系列格雷码黑白条纹图案,其中具有相同编码
的区域作为一个编码周期,然后再采用四步相移法,依次投射四幅相移图案,使得每个编码区域被进一步连续细分。但投射的格雷码图案和相移图案必须满足如下关系:格雷码图案的最小周期为相移图案周期的 4 倍,理论上格雷码周期边界与相移周期边界要严格对应。

 

格雷码的编码算法为:

1,由已有的前面的格雷码生成后面的格雷码

假设已经生成了k位格雷码,那么k+1位格雷码的生成方式为

(1) 按序在k位格雷码前插入一位0,生成一组编码

(2)按逆序在k位格雷码前插入一位1,生成另外一组编码

(3)两组编码合起来就是k+1位格雷码。

如下例:

已有2位格雷码:00, 01, 11, 10,要生成3位格雷码,采用此算法:

(1)按序在各码前插入0,生成 000,001, 011,010;

(2)按逆序在各码前插入1,生成 110,111, 101,100;

(3)将两组编码组合起来:000, 001, 011, 010, 110, 111, 101, 100,为3位格雷码。

 

另外一种算法与此算法类似,不同的是插入的位是在格雷码的后面:

对于k位格雷码,在各格雷码后面分别插入0, 1 或 1, 0,生成两个编码,所有插入完成后组合起来的编码为k+1位格雷码。

如已有2位格雷码:00,01,11,10,生成3位格雷码,采用此算法:

(1)在00编码后面分别插入0,1,生成 000, 001;

(2)在01编码后面分别插入1,0,生成 011, 010;

(3)在11编码后面分别插入0,1,生成 110, 111;

(4)在10编码后面分别插入1,0,生成 101,100;

(5)将生成的编码组合起来:000, 001, 011, 010, 110, 111, 101, 100,为3位格雷码。

对格雷码编码测试代码如下(有详细的注释):

#include <iostream>
#include <vector>
#include <string>
#include <time.h>
 
void GrayCodeOne(int num);
void GrayCodeTwo(int num);
 
using namespace std;
 
int main()
{
	int count;
	cout << "Input Code Number:";  
	cin >> count;//输入一个整形数
 
	cout << "Produce Gray Code using method 1" << endl;
	clock_t beginOne = clock();
	GrayCodeOne(count);
	clock_t endOne = clock();
	cout << "Gray Code First Method using time: " << (endOne - beginOne) << endl;
 
	cout << "Produce Gray Code using method 2" << endl;
	clock_t beginTwo = clock();
	GrayCodeTwo(count);
	clock_t endTwo = clock();
	cout << "Gray Code Second Method using time: " << (endTwo - beginTwo) << endl;
 
	return 0;
}
 
// Method to produce gray code using method inserting 0 in front of old gray code by positive  
//方法1中的已有的格雷码序列,正向前面一位加0,逆向前面一位加1
// and inserting 1 in front of old gray code by nagative.
void GrayCodeOne(int num)
{
	if (num < 1)
	{
		cout << "Error input Integer" << endl;
		return;
	}
 
	vector<string> codeVec;  //定义一个字符串类型的向量 用来存放编码序列
 
	int cIdx = 1;
	for (; cIdx <= num; cIdx++)
	{
                //如果num=1,则for循环只有一次,codeVec大小就为2,里面放的是0,1
		//开始向量为空,for循环的第一次,该向量大小一定小于2,直接存放0,1,
                //存放一次结束后向量大小为2,则进入下面的else里
                if (codeVec.size() < 2)  
		{
			codeVec.push_back("0");
			codeVec.push_back("1");
		}
		else
		{
                        //如果codeVec大小大于等于2个,则开始在,0和1上进行下面操作
			vector<string> tranVec;  
			tranVec.resize(2 * codeVec.size());  //定义一个中转向量,确定tranVec的大小为原有元素向量的2倍
			int tranIdx = 0;
                        //迭代器对向量进行遍历
			vector<string>::iterator codeIter = codeVec.begin();
			for (; codeIter != codeVec.end(); codeIter++)
			{
                                //在正序输出的每个元素的前面加0
				string str = "0";
				str.append(*codeIter);
				tranVec[tranIdx++] = str;//将加0后的元素重新存到转移向量
			}
 
			vector<string>::reverse_iterator rCodeIter = codeVec.rbegin();  //逆序迭代器,rbegin指向容器c的最后一个元素
			for (; rCodeIter != codeVec.rend(); rCodeIter++)
			{
                                //在逆序输出的每个元素的前面加1
				string str = "1";
				str.append(*rCodeIter);
				tranVec[tranIdx++] = str; 
			}
 
			codeVec.assign(tranVec.begin(), tranVec.end());
                        //将区间tranVec[first,last)的元素全部赋值到当前的codeVec容器中
                        //就是当前依序所编码的格雷码
		}
	}
 
	//vector<string>::iterator vecIter = codeVec.begin();
	//for (; vecIter != codeVec.end(); vecIter++)
	//{
	//	cout << *vecIter << endl;
	//}
 
	return;
}
 
// Method to produce gray code using method inserting 0/1 in the back of first gray code
// then inserting 1/0 in the back of next gray code.
void GrayCodeTwo(int num)
{
	if (num < 1)
	{
		cout << "Input error Integer" << endl;
		return;
	}
 
	vector<string> codeVec;
 
	int cIdx = 1;
	for (; cIdx <= num; cIdx++)
	{
		if (codeVec.size() < 2)
		{
			codeVec.push_back("0");
			codeVec.push_back("1");
		}
		else
		{
			vector<string> tranVec;
			int tranIdx = 0;
			int cIdx = codeVec.size();
 
			tranVec.resize(2 * cIdx);
			for (int vIdx = 0; vIdx < cIdx; vIdx++)
			{
				string str = codeVec[vIdx];
				if (0 == (vIdx % 2))
				{
					string str0 = str;
					str0.append("0");
					tranVec[tranIdx++] = str0;
 
					string str1 = str;
					str1.append("1");
					tranVec[tranIdx++] = str1;
				}
				else
				{
					string str0 = str;
					str0.append("1");
					tranVec[tranIdx++] = str0;
 
					string str1 = str;
					str1.append("0");
					tranVec[tranIdx++] = str1;
				}
			}
 
			codeVec.assign(tranVec.begin(), tranVec.end());
		}
	}
 
	//vector<string>::iterator vecIter = codeVec.begin();
	//for (; vecIter != codeVec.end(); vecIter++)
	//{
	//	cout << *vecIter << endl;
	//}
 
	return;
}

2、由二进制转换为格雷码

这里的二进制数是整型数,直接对整型数进行操作获得格雷码。

static unsigned int binaryToGray(unsigned int num) {
    return (num >> 1) ^ num;
}

          unsigned int型数据到格雷码的转换,最高可转换32位自然二进制码

          int型数据到格雷码的转换,最高可转换31位自然二进制码。

 

格雷码图案的生成:

上面测试程序为一维格雷码的生成程序,下面为二维的格雷码图案的生成:

格雷码图案的生成是在一维格雷生成的原理上将一位编码扩展到一列的编码。

该图从上往下为格雷码递增级数,每一级为一幅编码投影图案的黑白分布图样,从左至右,编码值从“0000000”按照格雷码编码依次递增至“1000000”。

投影图案设计

采用与投影图案 μ、ν  轴两个方向相同的两个系列格雷码编码,其中每个方向最高级别格雷码宽度为 1 个像素,该设计方法具体分如下 2步:

1)确定投影图案分辨率,假设水平 μ 方向上编码条纹设为 m 级,竖直 ν 方向上编码条纹设为 n 级,则投影图案在 μ 方向上像素个数为 2^m,ν 方向上像素个数为 2^n,即投影图案分辨率是 2^m×2^n。  
2)投影图案分辨率确定后,使用格雷码编码方法,设计两个方向的编码投影图案,共生成 μ 方向上 m 张格雷码的编码图案,每张格雷码级别依次递增,和 ν 方向上 n 张格雷码的编码图案,每张格雷码级别依次递增。

设计代码(有注释)如下:

//设置的水平方向的条纹数是10,垂直方向的条纹数是6
static unsigned int Nhorz = 10;
static unsigned int Nvert = 6;

#ifndef log2f
#define log2f(x) (log(x)/log(2.0))
#endif

using namespace std;

/*
 * The purpose of this function is to convert an unsigned
 * binary number to reflected binary Gray code.
 *
 * The operator >> is shift right. The operator ^ is exclusive or.
 * Source: http://en.wikipedia.org/wiki/Gray_code
 */
static unsigned int binaryToGray(unsigned int num) {
    return (num >> 1) ^ num;
}

/*
 * From Wikipedia: http://en.wikipedia.org/wiki/Gray_code
 * The purpose of this function is to convert a reflected binary
 * Gray code number to a binary number.
*/
static unsigned grayToBinary(unsigned num, unsigned numBits)
{
    for (unsigned shift = 1; shift < numBits; shift <<= 1){
        num ^= num >> shift;
    }
    return num;
}

/*
 * Function takes the decimal number
 * Function takes the Nth bit (1 to 31)
 * Return the value of Nth bit from decimal
 * Source: http://icfun.blogspot.com/2009/04/get-n-th-bit-value-of-any-integer.html
 */
static int get_bit(int decimal, int N){

    // Shifting the 1 for N-1 bits   
    int constant = 1 << (N-1);   //将00000001左移N-1位

    // If the bit is set, return 1
    if( decimal & constant ){
        return 1;
    }

    // If the bit is not set, return 0
    return 0;
}

static inline int powi(int num, unsigned int exponent){
    // NOT EQUIVALENT TO pow()
    if(exponent == 0)
        return 1;

    float res = num;
    for(unsigned int i=0; i<exponent-1; i++)
        res *= num;

    return res;
}

// Encoder   参数为:行 列 方向
EncoderGrayCode::EncoderGrayCode(unsigned int _screenCols, unsigned int _screenRows, CodecDir _dir) : Encoder(_screenCols, _screenRows, _dir){

    N = 2;  //初始值为2

    // Set total pattern number
    if(dir & CodecDirHorizontal)
        this->N += Nhorz;         //如果编码为水平方向,N为2+Nhorz  N=12

    if(dir & CodecDirVertical)    //如果编码为水平方向,N为2+Nvert  N=8
        this->N += Nvert;

    // Encode every pixel column     //编码每一像素列  NbitsHor=10
    int NbitsHorz = ceilf(log2f((float)screenCols));  //先将列数转变为浮点数,再将列数取log2f运算,将float类型的小数去掉,然后进一,

    // Number of vertical encoding patterns  同上 针对每一行  9.58去掉小数进1为10,即NbitsVert=10
    int NbitsVert = ceilf(log2f((float)screenRows));

	//patterns的前两个元素为patternOn和patternOff
    cv::Mat patternOn(1, 1, CV_8UC3, cv::Scalar(0));  //定义一个1x1的patternOn矩阵
    patternOn.at<cv::Vec3b>(0,0) = cv::Vec3b(255, 255, 255);   //矩阵的(0,0)点定义为(255, 255, 255)白色
    patterns.push_back(patternOn); 

    cv::Mat patternOff(1, 1, CV_8UC3, cv::Scalar(0));  //定义一个1x1的patternOff矩阵
    patterns.push_back(patternOff);


    if(dir & CodecDirHorizontal){
        // Precompute horizontally encoding patterns
        for(unsigned int p=0; p<Nhorz; p++){
            cv::Mat patternP(1, screenCols, CV_8UC3);   //定义一个1行xscreenCols列的patternP矩阵

            // Loop through columns in first row       从第一行遍历所有列
            for(unsigned int j=0; j<screenCols; j++){
                unsigned int jGray = binaryToGray(j);   //将二进制转换为格雷码

                // Amplitude of channels   设置每个通道的灰度值
                float amp = get_bit(jGray, NbitsHorz-p);
                patternP.at<cv::Vec3b>(0,j) = cv::Vec3b(255.0*amp,255.0*amp,255.0*amp);
            }
            patterns.push_back(patternP);
        }
    }
    if(dir & CodecDirVertical){
        // Precompute vertical encoding patterns     
        for(unsigned int p=0; p<Nvert; p++){
            cv::Mat patternP(screenRows, 1, CV_8UC3);    //定义一个screenRows行x1列的patternP矩阵

            // Loop through rows in first column       从第一列遍历所有行
            for(unsigned int i=0; i<screenRows; i++){

                unsigned int iGray = binaryToGray(i);

                // Amplitude of channels
                float amp = get_bit(iGray, NbitsVert-p);
                patternP.at<cv::Vec3b>(i,0) = cv::Vec3b(255.0*amp,255.0*amp,255.0*amp);
            }
            patterns.push_back(patternP);
        }
    }

    #if 0
        for(unsigned int i=0; i<patterns.size(); i++){
            std::stringstream fileNameStream;
            fileNameStream << "pattern_" << std::setw(2) << std::setfill('0') << i << ".bmp";
            cv::imwrite(fileNameStream.str(), patterns[i]);
        }

    #endif
}

在上述条纹生成的代码中,以横向的条纹图案,重要的步骤为

(1)设置两个一个像素大小的矩阵

(2)先分配一个空矩阵用来存放生成的pattern,从01两位格雷码开始开始

         内层for循环是对第一行的所有像素点按照编码的格雷码值进行灰度赋值,即

         如果P=0,则为第1张条纹图,用第一行每一列数(从0到1023)转换的格雷码值的 第10 位值去填充该列像素灰度值,

         这时,Mat矩阵中存放的是水平排布(竖直条纹)条纹第一行的一维行条纹。

        需要说明的是,这里的生成思路很巧妙

       首先是直接对图像的宽度进行格雷码转换,可以直接对任意的图像尺寸进行编码;

       同时对格雷码值取位操作,所取的位数正好分割了尺寸,每一行被分割成N=1024/2^(NbitsHorz-P)等份,每一份像素个数为

       1024/N。

       如果要生成一张图像,则需要将第一行的条纹进行复制,opencv里的repeat函数,矩阵拷贝的时候指定按x/y方向重复

      这里将第一行的条纹在Y方向按图像的行数重复,在X方向上重复为1次(不做操作),即可得到完整的一张格雷码图案。

(3)二进制数转为一维格雷码代码

(4)这个函数主要是将整型数所转换的一维格雷码的第N位对应的0,1状态取出

最后生成了横向条纹竖直分布的格雷码并存在了patterns中,纵向分布的类似。

Matlab代码:

function [P,offset] = graycode(width,height)

% % Define height and width of screen.
% width  = 1024;
% height = 768;

% Generate Gray codes for vertical and horizontal stripe patterns.
% See: http://en.wikipedia.org/wiki/Gray_code
P = cell(2,1); %胞元
offset = zeros(2,1);  
for j = 1:2
   
   % Allocate storage for Gray code stripe pattern.
   if j == 1
      N = ceil(log2(width));
      offset(j) = floor((2^N-width)/2);
   else
      N = ceil(log2(height));
      offset(j) = floor((2^N-height)/2);
   end
   P{j} = zeros(height,width,N,'uint8');
   
   % Generate N-bit Gray code sequence.
   B = zeros(2^N,N,'uint8');
   B_char = dec2bin(0:2^N-1);  %十进制转二进制
   for i = 1:N
      B(:,i) = str2num(B_char(:,i));
   end
   G = zeros(2^N,N,'uint8');
   G(:,1) = B(:,1);
   for i = 2:N
      G(:,i) = xor(B(:,i-1),B(:,i));
   end
   
   % Store Gray code stripe pattern.  B = repmat(A,m,n)  B 由 m×n 个 A 平铺而成
   if j ==1 
      for i = 1:N
         P{j}(:,:,i) = repmat(G((1:width)+offset(j),i)',height,1);
      end
   else
      for i = 1:N
         P{j}(:,:,i) = repmat(G((1:height)+offset(j),i),1,width);
      end
   end
   
end

matlab程序的思路:

此时,格雷码图案完全生成,根据格雷码图案生成相移条纹图案,格雷码的最小周期为T=log2(width),相移条纹的周期为T/4即可,具体生成过程在另一篇博文详细说明,这里不再重复。

格雷码编码程序实例3(C++),对应于解码程序。

// Generate Gray codes.     格雷码在生成时 通过sl_scan_cols和sl_scan_rows的状态来判断生成行还是列条纹图案 

int generateGrayCodes(int width, int height,     //以1024 * 768为例
					  IplImage**& gray_codes,    
					  int& n_cols, int& n_rows,
					  int& col_shift, int& row_shift, 
					  bool sl_scan_cols, bool sl_scan_rows){

	// Determine number of required codes and row/column offsets.
	if(sl_scan_cols){                            //判断是生成哪种条纹
		n_cols = (int)ceil(log2(width));         //n_cols  格雷码的张数,即格雷码的最小宽度   n_cols=10 ;ceil是求大于该数的整数
		col_shift = (int)floor((pow(2.0,n_cols)-width)/2);       //列偏移量  比宽度多出来的?  col_shift=0;foolr是求小于该数的整数
	}
	else{
		n_cols = 0;
		col_shift = 0;
	}
	if(sl_scan_rows){
		n_rows = (int)ceil(log2(height));      //ceil(9.58) = 10 =n_rows
		row_shift = (int)floor((pow(2.0,n_rows)-height)/2);      //行偏移量 row_shift = floor((1024-768)/2)=128
	}
	else{
		n_rows = 0;
		row_shift = 0;
	}	

	// Allocate Gray codes.   分配内存  
	gray_codes = new IplImage* [n_cols+n_rows+1];   //为什么要加1?加一张白色图案

	for(int i=0; i<(n_cols+n_rows+1); i++)  //i<21
		gray_codes[i] = cvCreateImage(cvSize(width,height), IPL_DEPTH_8U, 1);   //创建n_cols+n_rows+1 = 21张空图像内存,其中第一张为 全白图案

	int step = gray_codes[0]->widthStep/sizeof(uchar);  //widthStep=图像的宽度*通道数,即每一行需要的内存长度;step即每一行有多少个uchar类型的数据

	// Define first code as a white image.
	cvSet(gray_codes[0], cvScalar(255));

	// Define Gray codes for projector columns.    投影仪的列         三层循环!!!
	for(int c=0; c<width; c++){
		for(int i=0; i<n_cols; i++){     //i从0到9共循环10次,一行的每一像素都要循环10次,每循环一次就是对每一张图像的操作
			uchar* data = (uchar*)gray_codes[i+1]->imageData;       //data指针 指向格雷码图像的数据的地址

			if(i>0)  //从第二张图案开始  data[c]是一维数组,只能放一个值,应该是0或者1
				data[c] = (((c+col_shift) >> (n_cols-i-1)) & 1)^(((c+col_shift) >> (n_cols-i)) & 1);//先右移后与   i=1时,c分为3个部分,0-255,255-767,768-1023,编码分别为0,1,0 
			else
				data[c] = (((c+col_shift) >> (n_cols-i-1)) & 1);    //如果i=0时,c<=511,data[c]=0,c>511,data[c]=1

			//col_shift的作用:如果投影仪尺寸为912*1140,n_cols为10,则在编码时是以1024位来进行编码的,以c=0为例,会导致0-511=0,512-912=1,左右不等长
			//所以仍需要以512为中心,通过将0-912后移56变为56-968,这样512则变为了中心,左右编码位数相等。
			//像素值全部乘以255
			data[c] *= 255;  

			for(int r=1; r<height; r++)     //得到一行的编码后执行的平铺操作 
				data[r*step+c] = data[c];	//r*step为每一行所占的内存
		}
	}

	// Define Gray codes for projector rows.       投影仪的行
	for(int r=0; r<height; r++){
		for(int i=0; i<n_rows; i++){
			uchar* data = (uchar*)gray_codes[i+n_cols+1]->imageData; //格雷码的行列图案数据内存地址是连续的  
			if(i>0)
				data[r*step] = (((r+row_shift) >> (n_rows-i-1)) & 1)^(((r+row_shift) >> (n_rows-i)) & 1);
			else
				data[r*step] = (((r+row_shift) >> (n_rows-i-1)) & 1);
			data[r*step] *= 255;                   //data[r*step] = data[r*step]*255
			for(int c=1; c<width; c++)
				data[r*step+c] = data[r*step];	
		}
	}

	// Return without errors.
	return 0;
}

程序解读:

1,两个bool类型状态变量:sl_scan_cols,sl_scan_rows,判断生成行还是列条纹图案。

2,根据投影仪的投影图案的尺寸(以912*1140为例)来计算横向和纵向条纹图案的位数和生成的张数以及怕行和列的偏移量。

3,为格雷码的存储分配内存,并在第一张单独存储全白图案,其余格格雷图案内存连续存储。

4,通过3层循环来实现格雷码图案的生成,重点在每一行(列)格雷码编码的生成:

        i=0时,编码方式为(((c+col_shift) >> (n_cols-i-1)) & 1);       

        当i从1变到n_cols时,n_cols-i-1和n_cols-i的值为:

       该句代码则是对每一个像素点进行编码,并通过异或来获得编码值0或1,推导过程如下:

该程序与之前的不同之处在于:

(1)加入了行和列的偏移量,在编码时是:

col_shift的作用:如果投影仪尺寸为912*1140,n_cols为10,则在编码时是以1024位来进行编码的,以c=0为例,会导致0-511=0,512-912=1,左右不等长,所以仍需要以512为中心,通过将0-912后移56变为56-968,这样512则变为了中心,左右编码位数相等。

(2)数据全部在内存中操作,可以减小内存消耗,加快运算速度。

 

如果有疑问或者见到其他的不同的格雷码编码程序,请留言或QQ联系博主交流学习(857467352)。

    原文作者:视觉小新
    原文地址: https://blog.csdn.net/qq_15295565/article/details/99989922
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。