大家心心念念的摄像头入门终于来啦,最近有些忙所以联手一个学弟一起给大家带来了这篇摄像头入门讲解,接下来我会尽量用通俗易懂的方式帮助大家入门摄像头,如有错误的地方还请批评指正。
首先附上一张车模照片:
这是一个摄像头的车模,智能车室内跑赛道的组别主要循迹方式有两种,一种是摄像头,一种是电磁,今天给大家讲讲摄像头循迹。图中的主控板模块,驱动模块,皆需要大家自己用AD或嘉立创EDA画板打板焊接,接下来我将一一向大家介绍一下各个模块的作用:对于有的模块,在电磁入门时已经详细说过了,大家有不懂的可以前往之前的文章进行观看 。
摄像头模块:摄像头相当于电磁组别中的电磁模块,相当于小车的眼睛,摄像头不同于电磁组的电磁模块需要自己打板焊接,摄像头是现成的模块,即插即用。
接下来我们说说摄像头获取的图像,大家都知道图片都是由一个个像素点组成。常见的彩图的每个像素是RGB三个通道的数组值决定的,我们智能车常用的摄像头用的是灰度摄像头,获取到的是灰度图像,只有一个通道,像素点数据用0~255的数字来表示颜色。
如上图所示0为黑色,255为白色,即越靠近0图像越黑,越靠近255图像越白。
这里我们通过宏定义来定义来定义黑白(以下0x~~表示16进制)
#define IMG_BLACK 0X00
#define IMG_WHITE 0Xff
我们可以通过调用逐飞库很容易的得到一份灰度图像
同时逐飞还贴心的为我们把图像信息存储到以下灰度数组中方便我们后续的调用处理
extern uint8 mt9v03x_image[MT9V03X_H][MT9V03X_W];
这里MT9V03X_H和MT9V03X_W分别代表图像的行和列(这里的参数可以根据自己需要更改,实际情况由自己的车决定)通常(0,0)代表的是图像的左上角,(MT9V03X_H,MT9V03X_W)代表的是图像的右下角,这里类似于一个坐标轴不在过多赘述。
二值化图像:二值化图像顾名思义将以上灰度图像从0~255的每一个像素点通过一个阈值来判断变为只有0和255的黑白图像。
二值化代码如下:
void erzhi()
{
for(int i = 0; i < MT9V03X_H; i++)
{
for(int j = 0; j < MT9V03X_W; j++)
{
if(image_h[i][j]>BlackThres)
mt9v03x_image[i][j]=IMG_WHITE;
else image_h[i][j]=IMG_BLACK;
}
}
}
这里的BlackThres为以上提到的阈值,这个阈值可以自己来固定一个阈值,也可以使用大津法来获取动态阈值。
由于图像的灰度范围已知,为0~255。那么我们就去计算每个像素值的点的个数,然后就可以得到一张灰度直方图,横坐标是0~255,纵坐标是每个像素值点的个数。根据赛道的蓝白蓝的特点,会在蓝,白色的区域附近会有两个尖峰,那我们就在这两个尖峰中间,找到一个最低值,作为阈值BlackThres。对图像进行分割,大于该阈值的,直接给255为白,小于该阈值的给0为黑。
以下就是大津法获取阈值的代码(大津法较为消耗芯片算力,根据自己的车的实际情况来调整图像大小或者更换别的方法,缺点就是大律法容易受到光线的影响)
/*************************************************************************
* 函数名称:short GetOSTU (unsigned char tmImage[LCDH][LCDW])
* 功能说明:大津法求阈值大小
* 参数说明:tmImage : 图像数据
* 函数返回:无
* 备 注: GetOSTU(Image_Use);//大津法阈值
Ostu方法又名最大类间差方法,通过统计整个图像的直方图特性来实现全局阈值T的自动选取,其算法步骤为:
1) 先计算图像的直方图,即将图像所有的像素点按照0~255共256个bin,统计落在每个bin的像素点数量
2) 归一化直方图,也即将每个bin中像素点数量除以总的像素点
3) i表示分类的阈值,也即一个灰度级,从0开始迭代 1
4) 通过归一化的直方图,统计0~i 灰度级的像素(假设像素值在此范围的像素叫做前景像素) 所占整幅图像
的比例w0, 并统计前景像素的平均灰度u0;统计i~255灰度级的像素(假设像素值在此范围的像素叫做背
景像素) * 所占整幅图像的比例w1,并统计背景像素的平均灰度u1;
5) 计算前景像素和背景像素的方差 g = w0*w1*(u0-u1) (u0-u1)
6) i++;转到4),直到i为256时结束迭代
7) 将最大g相应的i值作为图像的全局阈值
缺陷:OSTU算法在处理光照不均匀的图像的时候,效果会明显不好,因为利用的是全局像素信息。
*************************************************************************/
uint16 GetOSTU (unsigned char tmImage[Hang][Lie])
{
signed short i, j;
unsigned long Amount = 0;
unsigned long PixelBack = 0;
unsigned long PixelshortegralBack = 0;
unsigned long Pixelshortegral = 0;
signed long PixelshortegralFore = 0;
signed long PixelFore = 0;
float OmegaBack, OmegaFore, MicroBack, MicroFore, SigmaB, Sigma; // 类间方差;
signed short MinValue, MaxValue;
signed short Threshold = 0;
unsigned char HistoGram[256];
for (j = 0; j < 256; j++)
HistoGram[j] = 0; //初始化灰度直方图
for (j = 0; j < Hang; j++)
{
for (i = 0; i < Lie; i++)
{
HistoGram[tmImage[j][i]]++; //统计灰度级中每个像素在整幅图像中的个数
}
}
for (MinValue = 0; MinValue < 256 && HistoGram[MinValue] == 0; MinValue++); //获取最小灰度的值
for (MaxValue = 255; MaxValue > MinValue && HistoGram[MinValue] == 0; MaxValue--); //获取最大灰度的值
if (MaxValue == MinValue)
return MaxValue; // 图像中只有一个颜色
if (MinValue + 1 == MaxValue)
return MinValue; // 图像中只有二个颜色
for (j = MinValue; j <= MaxValue; j++)
Amount += HistoGram[j]; // 像素总数
Pixelshortegral = 0;
for (j = MinValue; j <= MaxValue; j++)
{
Pixelshortegral += HistoGram[j] * j; //灰度值总数
}
SigmaB = -1;
for (j = MinValue; j < MaxValue; j++)
{
PixelBack = PixelBack + HistoGram[j]; //前景像素点数
PixelFore = Amount - PixelBack; //背景像素点数
OmegaBack = (float) PixelBack / Amount; //前景像素百分比
OmegaFore = (float) PixelFore / Amount; //背景像素百分比
PixelshortegralBack += HistoGram[j] * j; //前景灰度值
PixelshortegralFore = Pixelshortegral - PixelshortegralBack; //背景灰度值
MicroBack = (float) PixelshortegralBack / PixelBack; //前景灰度百分比
MicroFore = (float) PixelshortegralFore / PixelFore; //背景灰度百分比
Sigma = OmegaBack * OmegaFore * (MicroBack - MicroFore) * (MicroBack - MicroFore); //计算类间方差
if (Sigma > SigmaB) //遍历最大的类间方差g //找出最大类间方差以及对应的阈值
{
SigmaB = Sigma;
Threshold = j;
}
}
return Threshold; //返回最佳阈值;
}
图像处理:通过以上方法我们已经可以得到一幅数据只有0和255构成的二值化黑白赛道图像,接下来我们对图像进行处理。已经获得了赛道的黑白图像,看过我前面电磁入门的人应该知道,接下来就是要根据这个图像找到赛道的中线,而电磁之所以简单就在于电磁板架在小车前面相当于摄像头看到的一行,而摄像头相当于几十排电感架在前面,所以摄像头无非就是用这几十排的数据进行处理得到赛道中线。
这里我们可以通过图像赛道中间255(一般都为白色)向左右两边不停的扫线直到扫到0(黑色)停止记录下左右边界数据存入数组中。同样我们也可以从最左边和最右边向着中间扫线同样也是找到黑白跳变点时记录下左右边界数据存入数组中。同样我们也可以通过八邻域爬线方法(理论上比传统扫线速度更快,但这必须保证图像最下方是存在白色)向左右两边爬线来记录数组。当然不同扫线都有不同优缺点,根据大家选择最适合自己的即可。扫线是为了获取左右边界的数组,为我们接下来的中线获取和元素处理打下基础。
中线获取很简单直接通过(左边界-右边界)/2来得到(是不是和之前电磁的类似),至于元素处理以后有机会在跟大家讨论。
我们的目的是想让车沿着中线走,最简单的办法就是通过一个设定的特定行来作为我们的引领行(此引领行也就类似于电磁车的那一排电感),车在赛道中间时那一行的中值为0偏离时把偏离的值反馈给舵机通过处理后使得舵机左右打脚(三轮就是给后轮差速)来使得引领行的中值为0,当然此方法对于低速车来说是非常便捷的但是车速提上来你的摄像头会抖动,这时单独靠引领行来进行方向决策会有一定问题,这时候我推荐大家使用加权平均来获取中值偏差。(这里主要是帮助新人入门我就不赘述了,感兴趣的同学可以去网上寻找加权平均的方法)。
最后当摄像头采集完一帧图像后别忘了清零标志位,否则在循环中它一直为1,只会停留在那一帧图像上,图像就会卡住不动。
if(mt9v03x_finish_flag)
{
mt9v03x_finish_flag = 0; //在图像使用完毕后 务必清除标志位,否则不会开始采集下一幅图像
Image_Get(); //获取图像
erzhi(); //获取的图像二值化,第一次二值化 灰度值为默认
image_processing(image_h); //二值化后的图像进行处理
BlackThres = GetOSTU (image_h); //大津法设定灰度值
}
舵机模块:
这里我使用的是四轮c车模作为演示,方向主要通过前轮的舵机来控制(需要提前调好舵机中值),通过图像处理得到的偏差进行pd位置式方向闭环(p越大打脚越大,d越大响应越快车子更稳但不能超调)处理后得到的值输出给舵机来控制舵机左右打角来让车身始终保持在赛道中间(当偏差越大大角幅度也应该越大)。
int dircontrol(int chazhi)//输入为赛道中线差值
{
int out_dw;
static int last_chazhi;
out_dw=(int)(chazhi*dir_p+(chazhi-last_chazhi)*dir_d);//只需自行定义一下dir_p和dir_d的数值即可
last_chazhi=chazhi;
return out_dw;
}
编码器模块:
图中两个绿色的就是编码器,编码器通过齿轮与电机齿轮衔接,另一端接线插在主控板上,随着电机的转动带动齿轮转动,齿轮带动编码器转动实时检测电机的转速反馈电机的实时转速,这也是速度环(速度闭环)的重要组成部分,编码器测的实时转速((左编码器+右编码器)/2)与你设定的目标速度进行比较(类似与中线差值的获得)得到的值作为速度环的输入,通过速度环pid(速度环一般采用pi控制)的调节让你的实时速度达到你设定的目标速度。
int speedcontrol(int ECPULSE,int aim_speed)//ECPULSE为编码器实时速度,aim_speed为目标速度
{
int out,Error;
Error=aim_speed-ECPULSE;
speed_jifen+=Error;
if(speed_jifen>500)speed_jifen=500;//积分需限幅
if(speed_jifen<-500)speed_jifen=-500;//积分需限幅
out=(int)(Error*speed_p+speed_jifen*speed_i);//只需自行定义一下speed_p和speed_i的数值即可
if(out>10000)out=10000;//电机输出限幅
if(out<-10000)out=-10000;//电机输出限幅
return out;
}
图中的驱动板是用来一端连接电机,一端连接主控板上pwm口用来控制电机速度,虽然直接给直流电机直接接电源也能转,但却不能调速,故还是要连接一个驱动板来驱动电机,以上介绍都仅为最基础的智能车必备外设模块,帮助大家快速入门,接下来用一张图总结一下:
最后给出一些建议:
①每辆车的摄像头的高度角度都是不同的建议前期花费一点时间调节,如果比赛不限制摄像头高度的话尽量至少保证摄像头在屏幕上显示的图像能照到实际离车最远2m左右,离车最近不得高于20cm左右。(当然2m实际上个人感觉还是比较近,但是为了保证近处能照到牺牲了部分远距离,具体情况还是根据自己车来决定)调整完毕后务必打上热熔胶固定死!!!!
②摄像头离地高度最好在25cm~30cm之间,当然具体还是根据大家车来决定,太高和太低都不利于元素识别。
③舵机调节中值时建议不要相信自己的眼睛,当你看着他是在中间的时候最好放在地上,沿着一条直线推着走个2m左右,如果偏差不大说明调的已经差不多了(因为存在机械误差不可能一直走直线)
④网上有很多大佬开源的代码,可以参考思路,但是不要直接使用大佬们的参数,还是那句话每个人每个车不一样,参数必然有很大差别,如果直接用会适得其反,反而会浪费时间和精力。
最后希望各位车友都能取得理想的成绩,虽然遗憾才是智能车的魅力,但希望大家都能不留遗憾,如果觉得写的还不错的话,给个一键三联哈,多谢大家的支持!(b站同名视频可观看)