此文系第16届智能车智能视觉组-上海交通大学AuTop战队开源算法讲解第一部分--二值化的技巧,专栏及开源方案链接:

llo:第16届智能车智能视觉组-上海交通大学AuTop战队开源汇总


视觉巡线(一)二值化的技巧

在智能车比赛当中的,如果使用摄像头进行巡线,二值化几乎是一个必不可少的一步。

二值化可以看作通过某种映射,将原始灰度图中的某个像素变换为0或255。

  1. 固定阈值二值化

最简单的二值化方式是固定阈值二值化,即

[公式]

其中x为某个像素的坐标,

[公式] 为该像素二值化后的值,

 [公式] 为该像素二值化前的值,

 [公式] 为亮度阈值。

可以看出这种二值化算法是最简单的算法,其C语言代码实现可以写成

void threshold(uint8_t *img_data, uint8_t *output_data, int width, int height, int thres){
  for(int y=0; y<height; y++){
    for(int x=0; x<width; x++){
      output_data[x+y*width] = img_data[x+y*width]>thres ? 255 : 0;
    }
  }
}

由于RT1064芯片具有Cache,故采用外层循环遍历纵坐标,内层循环遍历横坐标的方式,可以减少Cache miss以加速代码运行速度。

在固定阈值二值化中,二值化阈值成为决定二值化效果的关键因素。

自行开发的上位机中的原始图像

在这张测试图片中,相机曝光偏高,成像亮度较高。同时具有一定程度的反光。我们首先使用固定阈值二值化对其进行处理。

二值化阈值为156

当阈值偏低时,可以发现在二值图中难以分辨出赛道的边界,故这是一次失败的二值化。

二值化阈值为227

当把二值化阈值调高,二值图中即可清晰分辨出赛道的边界。

但由于实际情况下可能面临光照条件不同,比如白天和晚上的环境亮度不同,这时就需要调整二值化阈值。更有甚者,同一条赛道的不同位置光照条件也不同,此时采用固定阈值肯定是没法覆盖不同的环境条件。

2. 大津法(OTSU)阈值

为了解决不同光照条件下,二值化阈值不同的问题,最显而易见的方法就是根据当前图片的亮度情况动态决定二值化阈值

一个显而易见的方法就是统计图片的灰度分布情况,然后取平均数或者中数作为二值化阈值。这种实现最为简单。

然而这种方法却并不十分合理。由于是取中数或平均数,则意味着这种方法会极大的受到众数的影响。而实际情况,白色赛道在整张图像中的占比并不大,采用这种方式确定阈值不一定有效。

大津法阈值采用最大类间方差的原理,适合于图像灰度分布整体呈现“双峰”的情况。大津法会自动找出一个阈值,使得分割后的两部分类间方差最大。由于比赛过程中,摄像头画幅中几乎只有白色赛道和蓝色底布,所以灰度分布比较符合“双峰”这个条件,大津法不失为一种不错的算法。

灰度直方图

由于大津法源码较长,在这里就不放了,可以去我们的开源代码中阅读。

3. 自适应阈值

大津法对每张待处理的图片单独计算一个二值化阈值,这种方法较好的解决了白天和晚上光照强度不同而导致阈值变化的问题。但仍然有不足之处。

由于大津法得出二值化阈值后,还是采用的固定阈值二值化的方法,即整张图像采用同一个阈值。这并不能很好的解决反光问题和赛道小范围内光照变化的问题。

严重光照不均匀的情况

如上图,由于该处赛道上方有一个光源,导致小范围内的亮度突变。这显然不满足大津法的“双峰”的假设,可想而知大津法得出的结果将无法正常使用。

大津法二值化结果

处理这种图片的关键在于,由于同一张图片的不同区域有着不同的亮度分布,我们无法使用一个全局一致的二值化阈值将图片分割开来。所以我们对图片不同位置采用不同阈值

那么自然我们得出一个简单的处理方法,对于每个像素点,取它周围一小片区域计算得出一个二值化阈值,并对这个像素进行二值化。参考之前的讲过的大津法,一小片区域的二值化阈值可以通过中位数,平均数或者大津法确定。在我们的代码实现中,采用了7x7的区域和平均数的确定方法。

另外,当这一小片区域亮度差异不大时,我们可能并不希望算法“强行”找到一个阈值,将这片区域分割开来,所以对于算法找出的局部阈值,我们将其减去一个经验性的参数(通常取2-5即可),避免算法将其强行分割。

自适应二值化的结果(两条横线为代码可视化故意绘制的,并非自适应二值化的结果)

可以看出自适应二值化可以较好的保留赛道的边界轮廓,便于后续的边界提取算法。另外,可以发现自适应二值化在蓝布区域得出了很多“噪点”,这是由于先前提到过的算法“强行”分割每一小片区域而产生的。但这并不影响后续的边界提取,所以可以不管。

由于每个像素在进行二值化的时候都要分析周围7x7的区域,对整张图进行一次二值化等效于进行了7x7+1=50次全图的遍历,所以这是一个复杂度极大的算法。在后续的边界提取代码中,我们会讲解如何极大的降低该算法的复杂度(RT1064上2ms内完成376x240的图片的处理)。

该算法的C语言实现为

void adaptiveThreshold(uint8_t* img_data, uint8_t* output_data, int width, int height, int block, uint8_t clip_value){
  assert(block % 2 == 1); // block必须为奇数
  int half_block = block / 2;
  for(int y=half_block; y<height-half_block; y++){
    for(int x=half_block; x<width-half_block; x++){
      // 计算局部阈值
      int thres = 0;
      for(int dy=-half_block; dy<=half_block; dy++){
        for(int dx=-half_block; dx<=half_block; dx++){
          thres += img_data[(x+dx)+(y+dy)*width];
        }
      }
      thres = thres / (block * block) - clip_value;
      // 进行二值化
      output_data[x+y*width] = img_data[x+y*width]>thres ? 255 : 0;
    }
  }
}

注意,由于每个像素需要参考它周围block*block的区域,所以图像最外圈的局域不能进行二值化,否则可能会出现数组越界的问题。

4. 总结

固定阈值二值化 大津阈值二值化 自适应阈值二值化
不同图片间 相同阈值 不同阈值 不同阈值
同一图片不同区域 相同阈值 相同阈值 不同阈值
计算量 较小 极大

SJTU-AuTop完整开源方案链接,如果觉得我们的方案对您有帮助,请在github上帮忙点个star吧:)

llo:第16届智能车智能视觉组-上海交通大学AuTop战队开源汇总