0


第18届全国大学生智能汽车竞赛四轮车开源讲解【2】--图像

开源汇总写在下面

第18届全国大学生智能汽车竞赛四轮车开源讲解_Joshua.X的博客-CSDN博客

一、图像的基本参数

volatile uint8 mt9v03x_finish_flag = 0;   // 一场图像采集完成标志位
uint8 mt9v03x_image[MT9V03X_H][MT9V03X_W];//采集到的图像数据

基本参数有两个,一个是采集标志位,一个是图像数组。

1.标志位

标志位很好理解,当摄像头采集完一帧图像,标志位会被置一,可以在主循环中不断读取标志位、当标志位是1时,你就可以读取该帧图像,处理完图像再把标志位清零,让他开始下一帧数据的采集。

根据习惯不同,也可以先清零标志位,再处理图像;或者先处理图像,再清零标志位。

理论上是有区别,个人实际使用感觉没什么差异。

if(mt9v03x_finish_flag)//先处理图像,再清除标志位
{
    Threshold=My_Adapt_Threshold(mt9v03x_image[0],MT9V03X_W, MT9V03X_H);//大津算阈值
    Image_Binarization(Threshold);//图像二值化
    mt9v03x_finish_flag=0;//标志位清除
}
if(mt9v03x_finish_flag)//先清除志位,再处理图像
{
    mt9v03x_finish_flag=0;//标志位清除
    Threshold=My_Adapt_Threshold(mt9v03x_image[0],MT9V03X_W, MT9V03X_H);//大津计算阈值
    Image_Binarization(Threshold);//图像二值化
}

本人习惯使用第一种处理方法,先将图像处理后,再清零标志位,让他写入下一帧数据,防止处理过程中,原始图像数组被写入下一帧数据。实际上我队友用的第二种,对实际使用也没有影响。

2.灰度数组

uint8 mt9v03x_image[MT9V03X_H][MT9V03X_W];

这就是一个二维数组,长和宽就是在第一章我们定义的图像大小,MT9V03X_H代表图像的行数(在循环中一般是i),MT9V03X_W代表列数(在循环中一般是j),每个二维数组的值代表他的颜色。

我们看到的所有图像都是由一个个像素组成,常见的彩图的每个像素是RGB三个通道的数组值决定的,我们智能车常用的摄像头用的是灰度图像,只有一个通道。像素点数据用0~255的数字来表示颜色。


数字与颜色对应关系见图

其中,0是黑,255是白。其中越接近0越靠近黑,越靠近255越接近白。为了在后面便于使用,我们使用了两个宏定义。这两个宏定义在后面就会使用到。

(灰度值也对应了摄像头数组的数据类型uint8,就是8位数的数据范围0~255)

#define IMG_BLACK     0X00      //0x00是黑
#define IMG_WHITE     0Xff      //0xff为白

需要注意的是,坐标系问题。

本文使用的坐标系是和数组一样的坐标系,也就是数组的(0,0)点位图像的的左上角,与数组的访问下标规则一样。并非数学上经常使用的直角空间坐标系,数学上常用的坐标系左下角是原点。

本文及以后所有图像都基于此坐标系

另一个需要注意的问题是数据的范围问题,就例如上图,我开的是18*9的一个数组,但是访问是时候,最边界的一个数据的坐标是17和8。因为数组访问的是偏移量,最开始的一位是0,所以大家在遍历图像时,请注意数据范围,不要越界操作。

下面两种方法都可以,不要弄混就好。不然数据越界轻则卡死,重则数据异常查不出bug。

(卡死,你能立刻反应到有bug;数据异常,很难想到是数组越界,更多的以为算法算错了)

//访问范围<MT9V03X_H
for(i=0;i<MT9V03X_H;i++)
{
    for(j=0;j<MT9V03X_W;j++)
    {
     //处理摄像头数据
    }
}

//访问范围<=MT9V03X_H-1
for(i=0;i<=MT9V03X_H-1;i++)
{
    for(j=0;j<=MT9V03X_W-1;j++)
    {
     //处理摄像头数据
    }
}

二、图像的基本处理

1.二值化

一幅灰度图的每个像素值从0~255,总共256个数值。二值化就是将这256个数进行“两极分化”,要么是0要么是255(0xFF)。

由于比赛规则规定的很清楚,赛道是蓝底白皮。这两者之间颜色差异很大,我们只需要分得清蓝色的是赛道背景,白色的是我们的赛道即可,没必要分析出其他的信息。

所以我们可以简单粗暴的将摄像头数据进行二值化处理,将0~255的像素数值,找到一个合适的阈值,直接变成0或者255这样只有两个值,也就是“非黑即白”。黑色的就是蓝布0,白色的就是赛道255。这就是二值化。


比赛场地铺设规范中有提到蓝色背景布

第十八届全国大学生智能车竞赛赛场赛道铺设规范(实际稿件)_卓晴的博客-CSDN博客

当然,二值化必定会损失一些信息,但是只要前期图像获取的比较好,配上合理的二值化,损失的信息如果都是无关信息,那么对我们就没有影响,就可以忽略不计。


彩色图像 灰度图像 二值化图像

二值化效果如下,只要做到白的是赛道,黑色的是背景蓝布,这就可以了,图像中间的光斑,后文会提到解决办法的。

二值化前后

二值化代码也很简单,找到一个阈值,大于该阈值的,给255,小于该阈值的,给0。

需要注意的是,我对图像二值化处理后,我没有把他放在原数组,而是新开了一个数组,后续所有图像识别操作都是对这个新数组进行识别。这样可以尽可能的避免刚二值化处理的一幅图像,摄像头采集的新的数据写入,覆盖掉我们二值化的数据。

uint8 image_two_value[MT9V03X_H][MT9V03X_W];//二值化后的原数组

二值化代码如下:

/*-------------------------------------------------------------------------------------------------------------------
  @brief     图像二值化处理函数
  @param     二值化阈值
  @return    NULL
  Sample     Image_Binarization(Threshold);//图像二值化
  @note      二值化后直接访问image_two_value[i][j]这个数组即可
-------------------------------------------------------------------------------------------------------------------*/
void Image_Binarization(int threshold)//图像二值化
{
    uint16 i,j;
    for(i=0;i<MT9V03X_H;i++)
    {
        for(j=0;j<MT9V03X_W;j++)//灰度图的数据只做判断,不进行更改,二值化图像放在了新数组中
        {
            if(mt9v03x_image[i][j]>=threshold)
                image_two_value[i][j]=IMG_WHITE;//白
            else
                image_two_value[i][j]=IMG_BLACK;//黑
        }
    }
}

1.1 大津法

大津法应该是二值化中比较出名的算法,个人理解如下。

由于图像的灰度范围已知,为0255。那么我就去计算每个像素值的点的然个数,后就可以得到一张灰度直方图,横坐标是0255,纵坐标是每个像素值点的个数。由于赛道的特殊性,会在在深色(蓝色)区域附近,在白色的区域附近会有两个尖峰,那我们就在这两个尖峰中间,找到一个最低值,作为阈值。对图像进行分割,大于该阈值的,直接给255小于该阈值的给0。

注:以上仅作为个人理解,真实性没有任何保证。


灰度直方图

但是大津法有个弊端,由于需要遍历全图进行像素点的数值计算,那么我遍历一张18070的图,就起码需要18070=12600次访问,再加上一些计算处理,其实是比较费时间的,一般的单片机需要几ms来对图像进行大津法+二值化处理,略费时间。所以不建议将图像开的太大。

大津法参考代码参考如下

/*-------------------------------------------------------------------------------------------------------------------
  @brief     普通大津求阈值
  @param     image       图像数组
             width       列 ,宽度
             height      行,长度
  @return    threshold   返回int类型的的阈值
  Sample     threshold=my_adapt_threshold(mt9v03x_image[0],MT9V03X_W, MT9V03X_H);//普通大津
  @note      据说没有山威大津快,我感觉两个区别不大
-------------------------------------------------------------------------------------------------------------------*/
int My_Adapt_Threshold(uint8*image,uint16 width, uint16 height)   //大津算法,注意计算阈值的一定要是原图像
{
    #define GrayScale 256
    int pixelCount[GrayScale];
    float pixelPro[GrayScale];
    int i, j;
    int pixelSum = width * height/4;
    int  threshold = 0;
    uint8* data = image;  //指向像素数据的指针
    for (i = 0; i < GrayScale; i++)
    {
        pixelCount[i] = 0;
        pixelPro[i] = 0;
    }
    uint32 gray_sum=0;
    for (i = 0; i < height; i+=2)//统计灰度级中每个像素在整幅图像中的个数
    {
        for (j = 0; j <width; j+=2)
        {
            pixelCount[(int)data[i * width + j]]++;  //将当前的点的像素值作为计数数组的下标
            gray_sum+=(int)data[i * width + j];       //灰度值总和
        }
    }
    for (i = 0; i < GrayScale; i++) //计算每个像素值的点在整幅图像中的比例
    {
        pixelPro[i] = (float)pixelCount[i] / pixelSum;
    }
    float w0, w1, u0tmp, u1tmp, u0, u1, u, deltaTmp, deltaMax = 0;
    w0 = w1 = u0tmp = u1tmp = u0 = u1 = u = deltaTmp = 0;
    for (j = 0; j < GrayScale; j++)//遍历灰度级[0,255]
    {
        w0 += pixelPro[j];  //背景部分每个灰度值的像素点所占比例之和   即背景部分的比例
        u0tmp += j * pixelPro[j];  //背景部分 每个灰度值的点的比例 *灰度值
        w1=1-w0;
        u1tmp=gray_sum/pixelSum-u0tmp;
        u0 = u0tmp / w0;              //背景平均灰度
        u1 = u1tmp / w1;              //前景平均灰度
        u = u0tmp + u1tmp;            //全局平均灰度
        deltaTmp = w0 * pow((u0 - u), 2) + w1 * pow((u1 - u), 2);//平方
        if (deltaTmp > deltaMax)
        {
            deltaMax = deltaTmp;//最大类间方差法
            threshold = j;
        }
        if (deltaTmp < deltaMax)
        {
            break;
        }
    }
    if(threshold>255)
        threshold=255;
    if(threshold<0)
        threshold=0;
  return threshold;
}

这里还有我找到的山威的快速大津,使用效果没什么差别,据说计算速度会快一些。

/*-------------------------------------------------------------------------------------------------------------------
  @brief     快速大津求阈值,来自山威
  @param     image       图像数组
             col         列 ,宽度
             row         行,长度
  @return    null
  Sample     threshold=my_adapt_threshold(mt9v03x_image[0],MT9V03X_W, MT9V03X_H);//山威快速大津
  @note      据说比传统大津法快一点,实测使用效果差不多
-------------------------------------------------------------------------------------------------------------------*/
int my_adapt_threshold(uint8 *image, uint16 col, uint16 row)   //注意计算阈值的一定要是原图像
{
    #define GrayScale 256
    uint16 width = col;
    uint16 height = row;
    int pixelCount[GrayScale];
    float pixelPro[GrayScale];
    int i, j;
    int pixelSum = width * height/4;
    int threshold = 0;
    uint8* data = image;  //指向像素数据的指针
    for (i = 0; i < GrayScale; i++)
    {
        pixelCount[i] = 0;
        pixelPro[i] = 0;
    }
    uint32 gray_sum=0;
    //统计灰度级中每个像素在整幅图像中的个数
    for (i = 0; i < height; i+=2)
    {
        for (j = 0; j < width; j+=2)
        {
            pixelCount[(int)data[i * width + j]]++;  //将当前的点的像素值作为计数数组的下标
            gray_sum+=(int)data[i * width + j];       //灰度值总和
        }
    }
    //计算每个像素值的点在整幅图像中的比例
    for (i = 0; i < GrayScale; i++)
    {
        pixelPro[i] = (float)pixelCount[i] / pixelSum;
    }
    //遍历灰度级[0,255]
    float w0, w1, u0tmp, u1tmp, u0, u1, u, deltaTmp, deltaMax = 0;
    w0 = w1 = u0tmp = u1tmp = u0 = u1 = u = deltaTmp = 0;
    for (j = 0; j < GrayScale; j++)
    {
        w0 += pixelPro[j];  //背景部分每个灰度值的像素点所占比例之和   即背景部分的比例
        u0tmp += j * pixelPro[j];  //背景部分 每个灰度值的点的比例 *灰度值
        w1=1-w0;
        u1tmp=gray_sum/pixelSum-u0tmp;
        u0 = u0tmp / w0;              //背景平均灰度
        u1 = u1tmp / w1;              //前景平均灰度
        u = u0tmp + u1tmp;            //全局平均灰度
        deltaTmp = w0 * pow((u0 - u), 2) + w1 * pow((u1 - u), 2);
        if (deltaTmp > deltaMax)
        {
            deltaMax = deltaTmp;
            threshold = j;
        }
        if (deltaTmp < deltaMax)
        {
            break;
        }
    }
    return threshold;
}

大家在使用时也不必关心计算方法,只需要关心传入的图像,传出的阈值就好,如果认为处理过于复杂,有能力的朋友可以自行优化上述代码。

1.2 索贝尔算子Sobel operator

索贝尔算子是计算机视觉领域的一种重要处理方法。主要用于获得数字图像的一阶梯度,常见的应用和物理意义是边缘检测。索贝尔算子是把图像中每个像素的上下左右四领域的灰度值加权差,在边缘处达到极值从而检测边缘。

索贝尔算子主要用作边缘检测。在技术上,它是一离散性差分算子,用来运算图像亮度函数的梯度之近似值。在图像的任何一点使用此算子,将会产生对应的梯度矢量或是其法矢量。

索贝尔算子不但产生较好的检测效果,而且对噪声具有平滑抑制作用,但是得到的边缘较粗,且可能出现伪边缘。

这个算法是专业的图像边缘检测算法,核心原理是对数学公式的推导,大概就是什么计算相邻像素之间的梯度,也就是数值下降,上升的速度比较快吧,本人不懂。


Sobel边缘检测

参考代码如下(某邱的开源代码)

/*-------------------------------------------------------------------------------------------------------------------
  @brief     sobel二值化
  @param     imagein       原图数组
             imangeout     二值化后的数组
  @return    null
  Sample     lq_sobelAutoThreshold (mt9v03x_image[MT9V03X_H][MT9V03X_W],image_two_value[MT9V03X_H][MT9V03X_W])
  @note      会比大津慢一些,效果比大津法好不少
-------------------------------------------------------------------------------------------------------------------*/
void lq_sobelAutoThreshold (unsigned char imageIn[LCDH][LCDW], unsigned char imageOut[LCDH][LCDW])
{
    /**卷积核大小**/
    short KERNEL_SIZE = 3;
    short xStart = KERNEL_SIZE / 2;
    short xEnd = LCDW - KERNEL_SIZE / 2;
    short yStart = KERNEL_SIZE / 2;
    short yEnd = LCDH - KERNEL_SIZE / 2;
    short i, j, k;
    short temp[4];
    for (i = yStart; i < yEnd; i++)
    {
        for (j = xStart; j < xEnd; j++)
        {
            /* 计算不同方向梯度幅值  */
            temp[0] = -(short) imageIn[i - 1][j - 1] + (short) imageIn[i - 1][j + 1]//{{-1, 0, 1},
            - (short) imageIn[i][j - 1] + (short) imageIn[i][j + 1]                 // {-1, 0, 1},
            - (short) imageIn[i + 1][j - 1] + (short) imageIn[i + 1][j + 1];        // {-1, 0, 1}};
            
            temp[2] = -(short) imageIn[i - 1][j] + (short) imageIn[i][j - 1]        //{{0, -1, -1}
            - (short) imageIn[i][j + 1] + (short) imageIn[i + 1][j]                 // {1,  0, -1}
            - (short) imageIn[i - 1][j + 1] + (short) imageIn[i + 1][j - 1];        // {1,  1,  0}};

            temp[3] = -(short) imageIn[i - 1][j] + (short) imageIn[i][j + 1]        //{{-1, -1,  0}
            - (short) imageIn[i][j - 1] + (short) imageIn[i + 1][j]                 // {-1,  0,  1}
            - (short) imageIn[i - 1][j - 1] + (short) imageIn[i + 1][j + 1];        // { 0,  1,  1}};
            temp[0] = abs(temp[0]);
            temp[1] = abs(temp[1]);
            temp[2] = abs(temp[2]);
            temp[3] = abs(temp[3]);
            /* 找出梯度幅值最大值  */
            for (k = 1; k < 4; k++)
            {
                if (temp[0] < temp[k])
                {
                    temp[0] = temp[k];
                }
            }
            /* 使用像素点邻域内像素点之和的一定比例    作为阈值  */
            temp[3] = (short) imageIn[i - 1][j - 1] + (short) imageIn[i - 1][j] + (short) imageIn[i - 1][j + 1]
                    + (short) imageIn[  i  ][j - 1] + (short) imageIn[  i  ][j] + (short) imageIn[  i  ][j + 1]
                    + (short) imageIn[i + 1][j - 1] + (short) imageIn[i + 1][j] + (short) imageIn[i + 1][j + 1];

            if (temp[0] > temp[3] / 12.0f)
            {
                imageOut[i][j] = 0;
            }
            else
            {
                imageOut[i][j] = 0xff;
            }
        }
    }
}

实际二值化速度要比大津法慢不少,但是效果要更棒,看大佬们有无时间对算法进行优化。

2.灰度

灰度巡线就是利用赛道的特点,在黑白交界处会发生非常明显的数值跳变,计算每一行的跳变,放大他,当大于或者小于某一阈值,认为找到边界。

由于本人没有使用灰度,所以不多做介绍,详细情况请参考下面的推文。

电磁及摄像头(总钻风)寻迹算法浅析--逐飞科技

直接处理灰度图好处如下:

  1. 减少运算量,二值化处理需要对全图进行遍历,浪费时间。
  2. 抗干扰能力强,对于大津法,在光线不均的情况下,阈值会爆炸,而灰度会保留更多信息。

其实最开始我也是想使用灰度图的,但是到后期控制逐渐成熟,对于车子也不敢有太大的改动,所以也就没用进行灰度处理,这里还是比较推荐各位使用灰度的。

3.图像压缩

图像压缩可以在视野广阔的前提下缩小数组,就是把一张很大的图片等比例进行缩小。这样必定会损失一部分细节,但是好处是可以显著降低图像体积,在后续处理的时候,对于全图的遍历扫线,找点的操作计算量会减小很多。

进行图像压缩的可以将原数组开的大一些,保证获得广阔的视野。然后每间隔几个点,取一个点作为有效点存起来,我印象中有些国赛的队伍甚至图像压缩到80*60。这样对后续图像处理速度会有质的提升。

由于我没有使用,代码就不放在下面了。原理就是间隔,按照比例选取点丢到另一个数组去即可。

这里提一个注意事项,在压缩时,尽量压缩整倍数。如果压缩的倍数不合理,会产生小数点丢弃的情况,这样会造成图像变歪。摄像头歪着图像才是正的。我们实验室的同学就发生过这样的情况。所以将原始图像调好后,选择合适的缩放比例,尽量去凑整倍数,这样才会有比较好的效果。

4.抗干扰

每年的线下赛举办方各不相同,现场情况更是无法估计,总会出现非常多的问题,光线就是摄像头最大的杀手之一。

这里提出几个抗干扰的方法。

1.遮光窗帘


未使用遮光窗帘

调车时要记得开灯,这里只是示意图,窗帘遮光性能很好。


使用遮光窗帘后

由于智能车对于光线要求高,这里的高值得是均匀度,只要是光线均匀,哪怕暗一些都无所谓。所以直接买一套遮光窗帘,屏蔽掉室外的太阳光,只使用室内的灯,会使光线条件变好。当然也要注意,整体变亮或者变暗,需要调整摄像头的曝光,让摄像头尽量得到比较适中的数据。

当然,比赛现场有没有,那得看命。

2.偏振片


摄像头前面那个大圆盘就是偏振片

偏振片可以清除掉特定角度的反射光线,所以我们在开头看到的灯光反射的亮斑,使用偏振片,根据实际情况调整他的角度,就可以直接滤掉。


旋转偏振片角度,滤掉某一特定角度的光

据我实际使用经验,偏振片+遮光窗帘就处理掉90%光线问题。

3.抹布

当光线效果是在不佳时,征得裁判员同意后,老老实实用抹布吧,有些情况是技术上无解的,得采取一些物理手段了。


十七届电子科技大学充电组国赛现场

到这里,我们就获取到了一帧可以直接处理的优秀图像了(二值化),具体如何提取赛道信息,提取那些信息,下期我们再讲。

希望能够帮助到一些人。

本人菜鸡一只,各位大佬发现问题欢迎留言指出。

标签: 汽车 c语言 stm32

本文转载自: https://blog.csdn.net/qq_58114029/article/details/132050763
版权归原作者 Joshua.X 所有, 如有侵权,请联系我们删除。

“第18届全国大学生智能汽车竞赛四轮车开源讲解【2】--图像”的评论:

还没有评论