第一类:图像处理
1.1 摄像头如何使用
我使用的是总钻风130°摄像头,拿到摄像头后,使用例程可以在IPS屏幕上看到一个灰度图像(原始图像)。这个原始图像我们一般都要进行处理,也可以不处理。我来介绍一下常用的三种处理方法.(1)图像二值化。(2)图像灰度。(3)边缘检测算法。
我用的是简单好用的图像二值化,所以我拿二值化进行讲解。我先简单介绍一下二值化图像,在原始图像的基础上,把他分为黑和白(即赛道是白,其它是黑)。
灰度图像有256个像素点 即0-255。0-255是什么意思,个人理解即颜色的深浅。0是黑,255是白。0-255是介于黑至白之间。对于一个灰度图像中,图像中每一个点都是在0-255之间,每一个点都有一个值.受光照影响,不同位置,不同时间,不同光照,每一个点的值都会不同。所以我们要对这个值进行区分黑白,要进行处理。处理的方法就是:大津法(还有其他方法不唯一,大津法用的人比较多)
大津法就是对你的灰度图像进行处理后得到一个阈值。输入进去一个二位数组图像,然后会返回给你一个阈值,然后你就用这个阈值进行二值化(即大于阈值为白(黑),小于阈值为黑(白)。即你就可以得到一个二值化图像。
//大津法求动态阈值
uint8 otsuThreshold(uint8 *image, uint16 col, uint16 row)
{
#define GrayScale 256
uint16 Image_Width = col;
uint16 Image_Height = row;
int X; uint16 Y;
uint8* data = image;
int HistGram[GrayScale] = {0};
uint32 Amount = 0;
uint32 PixelBack = 0;
uint32 PixelIntegralBack = 0;
uint32 PixelIntegral = 0;
int32 PixelIntegralFore = 0;
int32 PixelFore = 0;
double OmegaBack=0, OmegaFore=0, MicroBack=0, MicroFore=0, SigmaB=0, Sigma=0; // 类间方差;
uint8 MinValue=0, MaxValue=0;
uint8 Threshold = 0;
for (Y = 0; Y <Image_Height; Y++) //Y<Image_Height改为Y =Image_Height;以便进行 行二值化
{
//Y=Image_Height;
for (X = 0; X < Image_Width; X++)
{
HistGram[(int)data[Y*Image_Width + X]]++; //统计每个灰度值的个数信息
}
}
for (MinValue = 0; MinValue < 256 && HistGram[MinValue] == 0; MinValue++) ; //获取最小灰度的值
for (MaxValue = 255; MaxValue > MinValue && HistGram[MinValue] == 0; MaxValue--) ; //获取最大灰度的值
if (MaxValue == MinValue)
{
return MaxValue; // 图像中只有一个颜色
}
if (MinValue + 1 == MaxValue)
{
return MinValue; // 图像中只有二个颜色
}
for (Y = MinValue; Y <= MaxValue; Y++)
{
Amount += HistGram[Y]; // 像素总数
}
PixelIntegral = 0;
for (Y = MinValue; Y <= MaxValue; Y++)
{
PixelIntegral += HistGram[Y] * Y;//灰度值总数
}
SigmaB = -1;
for (Y = MinValue; Y < MaxValue; Y++)
{
PixelBack = PixelBack + HistGram[Y]; //前景像素点数
PixelFore = Amount - PixelBack; //背景像素点数
OmegaBack = (double)PixelBack / Amount;//前景像素百分比
OmegaFore = (double)PixelFore / Amount;//背景像素百分比
PixelIntegralBack += HistGram[Y] * Y; //前景灰度值
PixelIntegralFore = PixelIntegral - PixelIntegralBack;//背景灰度值
MicroBack = (double)PixelIntegralBack / PixelBack;//前景灰度百分比
MicroFore = (double)PixelIntegralFore / PixelFore;//背景灰度百分比
Sigma = OmegaBack * OmegaFore * (MicroBack - MicroFore) * (MicroBack - MicroFore);//g
if (Sigma > SigmaB)//遍历最大的类间方差g
{
SigmaB = Sigma;
Threshold = (uint8)Y;
}
}
return Threshold;
}
得到阈值进行二值化处理
uint8 bin_image[image_h][image_w];//图像数组
void turn_to_bin(void)
{
uint8 i,j;
image_thereshold = otsuThreshold(original_image[0], image_w, image_h);
for(i = 0;i<image_h;i++)
{
for(j = 0;j<image_w;j++)
{
if(original_image[i][j]>image_thereshold)bin_image[i][j] = white_pixel;
else bin_image[i][j] = black_pixel;
}
}
}
在二值化处理后我还在图像外圈增加了一个黑框,目的是防止程序卡死,代码如下。
/*
函数名称:void image_draw_rectan(uint8(*image)[image_w])
功能说明:给图像画一个黑框
参数说明:uint8(*image)[image_w] 图像首地址
函数返回:无
备 注:
example: image_draw_rectan(bin_image);
*/
void image_draw_rectan(uint8(*image)[image_w])
{
uint8 i = 0;
for (i = 0; i < image_h; i++)
{
image[i][0] = 0;
image[i][1] = 0;
image[i][image_w - 1] = 0;
image[i][image_w - 2] = 0;
}
for (i = 0; i < image_w; i++)
{
image[0][i] = 0;
image[1][i] = 0;
//image[image_h-1][i] = 0;
}
}
滤波函数
//定义膨胀和腐蚀的阈值区间
#define threshold_max 255*5//此参数可根据自己的需求调节
#define threshold_min 255*2//此参数可根据自己的需求调节
void image_filter(uint8(*bin_image)[image_w])//形态学滤波,简单来说就是膨胀和腐蚀的思想
{
uint16 i, j;
uint32 num = 0;
for (i = 1; i < image_h - 1; i++)
{
for (j = 1; j < (image_w - 1); j++)
{
//统计八个方向的像素值
num =
bin_image[i - 1][j - 1] + bin_image[i - 1][j] + bin_image[i - 1][j + 1]
+ bin_image[i][j - 1] + bin_image[i][j + 1]
+ bin_image[i + 1][j - 1] + bin_image[i + 1][j] + bin_image[i + 1][j + 1];
if (num >= threshold_max && bin_image[i][j] == 0)
{
bin_image[i][j] = 255;//白 可以搞成宏定义,方便更改
}
if (num <= threshold_min && bin_image[i][j] == 255)
{
bin_image[i][j] = 0;//黑
}
}
}
}
1.2 扫线
关于摄像头循迹,一般都是按照获取赛道中线,根据赛道中线来进行循迹的。如何获得赛道中线,我来简单介绍一下。在2.1中我们知道中线是按照左右边线获得的,而左右边线我们则需要在图像中进行处理获得,这个过程就是扫线。
在二值化图像的基础上进行扫线处理,以图像60行120列为例,从上到下为0-60行,我们取第60行中点(图像下面相比上面较稳定,所以我们从下面开始),也就第60行第60列为起始点往两边进行扫线。
#define Row 60 //图像行数
#define Col 80 //图像列数
一般来说,我们的车是放在赛道中间,即起始点为白点,然后我们往两边判断一直到黑点,我们记录下黑点坐标放进左右边线的数组里,若没有扫到黑点则记录图像数组边界值放进左右边线数组里。这样每一行都这样重复就可以获取左右边线数组。简单的扫线就这么结束了。
在八邻域找左右边线前,先找到边线的起始点
/*
函数名称:void get_start_point(uint8 start_row)
功能说明:寻找两个边界的边界点作为八邻域循环的起始点
参数说明:输入任意行数
函数返回:无
备 注:
example: get_start_point(image_h-2)
*/
uint8 start_point_l[2] = { 0 };//左边起点的x,y值
uint8 start_point_r[2] = { 0 };//右边起点的x,y值
uint8 get_start_point(uint8 start_row)
{
uint8 i = 0,l_found = 0,r_found = 0;
//清零
start_point_l[0] = 0;//x
start_point_l[1] = 0;//y
start_point_r[0] = 0;//x
start_point_r[1] = 0;//y
//从中间往左边,先找起点
for (i = image_w / 2; i > border_min; i--)
{
start_point_l[0] = i;//x
start_point_l[1] = start_row;//y
if (bin_image[start_row][i] == 255 && bin_image[start_row][i - 1] == 0)
{
//printf("找到左边起点image[%d][%d]\n", start_row,i);
l_found = 1;
break;
}
}
for (i = image_w / 2; i < border_max; i++)
{
start_point_r[0] = i;//x
start_point_r[1] = start_row;//y
if (bin_image[start_row][i] == 255 && bin_image[start_row][i + 1] == 0)
{
//printf("找到右边起点image[%d][%d]\n",start_row, i);
r_found = 1;
break;
}
}
if(l_found&&r_found)return 1;
else {
//printf("未找到起点\n");
return 0;
}
}
在找到起始点后进行循环
/*
函数名称:void search_l_r(uint16 break_flag, uint8(*image)[image_w],uint16 *l_stastic, uint16 *r_stastic,
uint8 l_start_x, uint8 l_start_y, uint8 r_start_x, uint8 r_start_y,uint8*hightest)
功能说明:八邻域正式开始找右边点的函数,输入参数有点多,调用的时候不要漏了,这个是左右线一次性找完。
参数说明:
break_flag_r :最多需要循环的次数
(*image)[image_w] :需要进行找点的图像数组,必须是二值图,填入数组名称即可
特别注意,不要拿宏定义名字作为输入参数,否则数据可能无法传递过来
*l_stastic :统计左边数据,用来输入初始数组成员的序号和取出循环次数
*r_stastic :统计右边数据,用来输入初始数组成员的序号和取出循环次数
l_start_x :左边起点横坐标
l_start_y :左边起点纵坐标
r_start_x :右边起点横坐标
r_start_y :右边起点纵坐标
hightest :循环结束所得到的最高高度
函数返回:无
备 注:
example:
search_l_r((uint16)USE_num,image,&data_stastics_l, &data_stastics_r,start_point_l[0],
start_point_l[1], start_point_r[0], start_point_r[1],&hightest);
*/
#define USE_num image_h*3 //定义找点的数组成员个数按理说300个点能放下,但是有些特殊情况确实难顶,多定义了一点
//存放点的x,y坐标
uint16 points_l[(uint16)USE_num][2] = { { 0 } };//左线
uint16 points_r[(uint16)USE_num][2] = { { 0 } };//右线
uint16 dir_r[(uint16)USE_num] = { 0 };//用来存储右边生长方向
uint16 dir_l[(uint16)USE_num] = { 0 };//用来存储左边生长方向
uint16 data_stastics_l = 0;//统计左边找到点的个数
uint16 data_stastics_r = 0;//统计右边找到点的个数
uint8 hightest = 0;//最高点
void search_l_r(uint16 break_flag, uint8(*image)[image_w], uint16 *l_stastic, uint16 *r_stastic, uint8 l_start_x, uint8 l_start_y, uint8 r_start_x, uint8 r_start_y, uint8*hightest)
{
uint8 i = 0, j = 0;
//左边变量
uint8 search_filds_l[8][2] = { { 0 } };
uint8 index_l = 0;
uint8 temp_l[8][2] = { { 0 } };
uint8 center_point_l[2] = { 0 };
uint16 l_data_statics;//统计左边
//定义八个邻域
static int8 seeds_l[8][2] = { {0, 1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1},{1, 0},{1, 1}, };
//{-1,-1},{0,-1},{+1,-1},
//{-1, 0}, {+1, 0},
//{-1,+1},{0,+1},{+1,+1},
//这个是顺时针
//右边变量
uint8 search_filds_r[8][2] = { { 0 } };
uint8 center_point_r[2] = { 0 };//中心坐标点
uint8 index_r = 0;//索引下标
uint8 temp_r[8][2] = { { 0 } };
uint16 r_data_statics;//统计右边
//定义八个邻域
static int8 seeds_r[8][2] = { {0, 1},{1,1},{1,0}, {1,-1},{0,-1},{-1,-1}, {-1, 0},{-1, 1}, };
//{-1,-1},{0,-1},{+1,-1},
//{-1, 0}, {+1, 0},
//{-1,+1},{0,+1},{+1,+1},
//这个是逆时针
l_data_statics = *l_stastic;//统计找到了多少个点,方便后续把点全部画出来
r_data_statics = *r_stastic;//统计找到了多少个点,方便后续把点全部画出来
//第一次更新坐标点 将找到的起点值传进来
center_point_l[0] = l_start_x;//x
center_point_l[1] = l_start_y;//y
center_point_r[0] = r_start_x;//x
center_point_r[1] = r_start_y;//y
//开启邻域循环
while (break_flag--)
{
//左边
for (i = 0; i < 8; i++)//传递8F坐标
{
search_filds_l[i][0] = center_point_l[0] + seeds_l[i][0];//x
search_filds_l[i][1] = center_point_l[1] + seeds_l[i][1];//y
}
//中心坐标点填充到已经找到的点内
points_l[l_data_statics][0] = center_point_l[0];//x
points_l[l_data_statics][1] = center_point_l[1];//y
l_data_statics++;//索引加一
//右边
for (i = 0; i < 8; i++)//传递8F坐标
{
search_filds_r[i][0] = center_point_r[0] + seeds_r[i][0];//x
search_filds_r[i][1] = center_point_r[1] + seeds_r[i][1];//y
}
//中心坐标点填充到已经找到的点内
points_r[r_data_statics][0] = center_point_r[0];//x
points_r[r_data_statics][1] = center_point_r[1];//y
index_l = 0;//先清零,后使用
for (i = 0; i < 8; i++)
{
temp_l[i][0] = 0;//先清零,后使用
temp_l[i][1] = 0;//先清零,后使用
}
//左边判断
for (i = 0; i < 8; i++)
{
if (image[search_filds_l[i][1]][search_filds_l[i][0]] == 0
&& image[search_filds_l[(i + 1) & 7][1]][search_filds_l[(i + 1) & 7][0]] == 255)
{
temp_l[index_l][0] = search_filds_l[(i)][0];
temp_l[index_l][1] = search_filds_l[(i)][1];
index_l++;
dir_l[l_data_statics - 1] = (i);//记录生长方向
}
if (index_l)
{
//更新坐标点
center_point_l[0] = temp_l[0][0];//x
center_point_l[1] = temp_l[0][1];//y
for (j = 0; j < index_l; j++)
{
if (center_point_l[1] > temp_l[j][1])
{
center_point_l[0] = temp_l[j][0];//x
center_point_l[1] = temp_l[j][1];//y
}
}
}
}
if ((points_r[r_data_statics][0]== points_r[r_data_statics-1][0]&& points_r[r_data_statics][0] == points_r[r_data_statics - 2][0]
&& points_r[r_data_statics][1] == points_r[r_data_statics - 1][1] && points_r[r_data_statics][1] == points_r[r_data_statics - 2][1])
||(points_l[l_data_statics-1][0] == points_l[l_data_statics - 2][0] && points_l[l_data_statics-1][0] == points_l[l_data_statics - 3][0]
&& points_l[l_data_statics-1][1] == points_l[l_data_statics - 2][1] && points_l[l_data_statics-1][1] == points_l[l_data_statics - 3][1]))
{
//printf("三次进入同一个点,退出\n");
break;
}
if (my_abs(points_r[r_data_statics][0] - points_l[l_data_statics - 1][0]) < 2
&& my_abs(points_r[r_data_statics][1] - points_l[l_data_statics - 1][1] < 2)
)
{
//printf("\n左右相遇退出\n");
*hightest = (points_r[r_data_statics][1] + points_l[l_data_statics - 1][1]) >> 1;//取出最高点
//printf("\n在y=%d处退出\n",*hightest);
break;
}
if ((points_r[r_data_statics][1] < points_l[l_data_statics - 1][1]))
{
printf("\n如果左边比右边高了,左边等待右边\n");
continue;//如果左边比右边高了,左边等待右边
}
if (dir_l[l_data_statics - 1] == 7
&& (points_r[r_data_statics][1] > points_l[l_data_statics - 1][1]))//左边比右边高且已经向下生长了
{
//printf("\n左边开始向下了,等待右边,等待中... \n");
center_point_l[0] = points_l[l_data_statics - 1][0];//x
center_point_l[1] = points_l[l_data_statics - 1][1];//y
l_data_statics--;
}
r_data_statics++;//索引加一
index_r = 0;//先清零,后使用
for (i = 0; i < 8; i++)
{
temp_r[i][0] = 0;//先清零,后使用
temp_r[i][1] = 0;//先清零,后使用
}
//右边判断
for (i = 0; i < 8; i++)
{
if (image[search_filds_r[i][1]][search_filds_r[i][0]] == 0
&& image[search_filds_r[(i + 1) & 7][1]][search_filds_r[(i + 1) & 7][0]] == 255)
{
temp_r[index_r][0] = search_filds_r[(i)][0];
temp_r[index_r][1] = search_filds_r[(i)][1];
index_r++;//索引加一
dir_r[r_data_statics - 1] = (i);//记录生长方向
//printf("dir[%d]:%d\n", r_data_statics - 1, dir_r[r_data_statics - 1]);
}
if (index_r)
{
//更新坐标点
center_point_r[0] = temp_r[0][0];//x
center_point_r[1] = temp_r[0][1];//y
for (j = 0; j < index_r; j++)
{
if (center_point_r[1] > temp_r[j][1])
{
center_point_r[0] = temp_r[j][0];//x
center_point_r[1] = temp_r[j][1];//y
}
}
}
}
}
//取出循环次数
*l_stastic = l_data_statics;
*r_stastic = r_data_statics;
}
随后两个函数提取左右边线
/*
函数名称:void get_left(uint16 total_L)
功能说明:从八邻域边界里提取需要的边线
参数说明:
total_L :找到的点的总数
函数返回:无
备 注:
example: get_left(data_stastics_l );
*/
uint8 l_border[image_h];//左线数组
uint8 r_border[image_h];//右线数组
uint8 center_line[image_h];//中线数组
void get_left(uint16 total_L)
{
uint8 i = 0;
uint16 j = 0;
uint8 h = 0;
//初始化
for (i = 0;i<image_h;i++)
{
l_border[i] = border_min;
}
h = image_h - 2;
//左边
for (j = 0; j < total_L; j++)
{
//printf("%d\n", j);
if (points_l[j][1] == h)
{
l_border[h] = points_l[j][0]+1;
}
else continue; //每行只取一个点,没到下一行就不记录
h--;
if (h == 0)
{
break;//到最后一行退出
}
}
}
/*
函数名称:void get_right(uint16 total_R)
功能说明:从八邻域边界里提取需要的边线
参数说明:
total_R :找到的点的总数
函数返回:无
备 注:
example:get_right(data_stastics_r);
*/
void get_right(uint16 total_R)
{
uint8 i = 0;
uint16 j = 0;
uint8 h = 0;
for (i = 0; i < image_h; i++)
{
r_border[i] = border_max;//右边线初始化放到最右边,左边线放到最左边,这样八邻域闭合区域外的中线就会在中间,不会干扰得到的数据
}
h = image_h - 2;
//右边
for (j = 0; j < total_R; j++)
{
if (points_r[j][1] == h)
{
r_border[h] = points_r[j][0] - 1;
}
else continue;//每行只取一个点,没到下一行就不记录
h--;
if (h == 0)break;//到最后一行退出
}
}
接下来是原理
假设现在有这样一幅图
首先就是这八个邻域:我定义了两个方向循环,一个顺时针,一个逆时针,循环起点为一号位置
上面的中心的假设为(1,1),那么程序中要表示这八个点也很简单,用中心点加上上边的变量就行了,如下图所示。
八个邻域知道了,那么八邻域循环简单来说就是以某一个点为中心点,根据限制条件来在八个邻域中找到满足条件的点记录下来作为新的中心点循环。
就比如上边的九个点,我们围绕中间的0为中心,从1号位开始,逆时针循环进行比对,当满足黑点跳到白点(即0-255)时,就更新坐标中心点(即填充的绿点),然后以新的中心点开始新的逆时针循环再找到下一个中心点,以此类推,紫色点就是下一个中心点。
一直这样循环下去,一直取黑点我们就能得到一条完整的边界了
于是乎,从屏幕中间分别向左右找到由白跨越到黑的点做为起点,然后从下往上执行邻域循环,,加上其他限制条件,就能得到如下结果
1.3 中线
我们是根据赛道中线进行循迹,所以中线的处理也非常重要。
中线 = (左边线+右边线)/2
中线 = 左边线+X
中线 = 右边线-X
X的值,根据实际情况来定。
下列代码作用为在屏幕上画出左右边界并求出中线,此时中线的横坐标就储存到了center_line这个数组中,循迹主要利用这个数组实现。
//根据最终循环次数画出边界点
for (i = 0; i < data_stastics_l; i++)
{
tft180_draw_point(points_l[i][0]+2, points_l[i][1], uesr_BLUE);//显示起点
}
for (i = 0; i < data_stastics_r; i++)
{
tft180_draw_point(points_r[i][0]-2, points_r[i][1], uesr_RED);//显示起点
}
for (i = hightest; i < image_h-1; i++)
{
center_line[i] = (l_border[i] + r_border[i]) >> 1;//求中线
//求中线最好最后求,不管是补线还是做状态机,全程最好使用一组边线,中线最后求出,不能干扰最后的输出
//当然也有多组边线的找法,但是个人感觉很繁琐,不建议
tft180_draw_point(center_line[i], i, uesr_GREEN);//显示起点 显示中线
tft180_draw_point(l_border[i], i, uesr_GREEN);//显示起点 显示左边线
tft180_draw_point(r_border[i], i, uesr_GREEN);//显示起点 显示右边线
}
程序实现效果如下