0


智能车摄像头开源—3 图像基础处理、迭代优化与效果展示

一、前言

  1. 本文主要讲解一些基本的处理,如图像压缩、起点找寻、阈值处理等。同时展示自适应八向迷宫的运行效果。
  2. 同时声明本文内容皆由作者实践得出,并不保证绝对正确,仅供入门者学习和参考
  3. ** 本文处理的图像大小为 80 * 60**

二、摄像头采集与屏幕初始化

2.1 初始化摄像头和TFT180屏幕

  1. 直接调用逐飞库:
  1. /**
  2. * 函数功能: 初始化 TFT180 和 总钻风摄像头
  3. * 特殊说明: 与 Cammer_Init_IPS200 函数只可调用其中一个,当都不调用时,默认开启摄像头初始化
  4. * 形 参: 无
  5. * 示例: Cammer_Init_TFT180();
  6. * 返回值: 无
  7. */
  8. void Cammer_Init_TFT180(void) //初始化摄像头和显示屏 *
  9. {
  10. TFT180_Show_Init();
  11. tft180_show_string(0,0,"mt9v034 init.");
  12. while(1)
  13. {
  14. if(mt9v03x_init()) //摄像头初始化
  15. {
  16. tft180_show_string(0,16,"mt9v034 reinit.");
  17. }
  18. else
  19. {
  20. break;
  21. }
  22. }
  23. tft180_show_string(0,16,"init success.");
  24. tft180_clear();
  25. }

2.2 初始化摄像头和IPS200屏幕

  1. 直接调用逐飞库函数:
  1. /**
  2. * 函数功能: 初始化 IPS200 和 总钻风摄像头
  3. * 特殊说明: 与 Cammer_Init_TFT180 函数只可调用其中一个,当都不调用时,默认开启摄像头初始化
  4. * 形 参: 无
  5. * 示例: Cammer_Init_IPS200();
  6. * 返回值: 无
  7. */
  8. void Cammer_Init_IPS200(void) //初始化摄像头和显示屏 *
  9. {
  10. IPS200_Show_Init();
  11. ips200_show_string(0,0,"mt9v034 init.");
  12. while(1)
  13. {
  14. if(mt9v03x_init()) //摄像头初始化
  15. {
  16. ips200_show_string(0,16,"mt9v034 reinit.");
  17. }
  18. else
  19. {
  20. break;
  21. }
  22. }
  23. ips200_show_string(0,16,"init success.");
  24. ips200_clear();
  25. }

2.3 单独初始化摄像头

  1. 直接调用逐飞库函数:
  1. /**
  2. * 函数功能: 初始化总钻风摄像头
  3. * 特殊说明: 当 Cammer_Init_TFT180 和 Cammer_Init_IPS200 都不调用时,默认开启摄像头初始化
  4. * 形 参: 无
  5. * 示例: Cammer_Init();
  6. * 返回值: 无
  7. */
  8. void Cammer_Init(void) //初始化摄像头和显示屏 *
  9. {
  10. while(1)
  11. {
  12. if(mt9v03x_init()) //摄像头初始化
  13. {
  14. }
  15. else
  16. {
  17. break;
  18. }
  19. }
  20. }

二、图像压缩

  1. 得到摄像头的一帧图像后,复制图像是必须的,这样可以在处理图像时仍可接收图像,即接收图像和处理图像同时进行。
  2. 压缩图像可以大幅缩减后续计算量,同时保留必要的细节,但是压缩不可过小,80 * 60已经很小了,建议不要再小于这个尺寸,当然求取边线的算法处理够快的话,可以直接处理188 * 120的图像,这样可以更好地适应局部反光和留存细节。图像再大没有意义。此处算法无难度,不过多赘述:
  1. /**
  2. * 函数功能: 复制并压缩图像,将 188 * 120 图像压缩为 80 * 60 大小
  3. * 特殊说明: 总钻风使用手册中说明:图像分辨率为 752 * 480, 376 * 240, 188 * 120 这三种分辨率视野是一样的,三者呈整数倍关系
  4. * 其他分辨率是通过裁减得到的(这个裁减包含比188 * 120小的任何分辨率,如 94 * 60),如376 * 240 的视野反而比752 * 400 的视野广
  5. * 此处将总钻风传回图像 188 * 120 压缩为 80 * 60, 所以将 j 乘系数 2.35(188 / 80)
  6. * 经实际测试,当设置图像大小为 94 * 60 时,传回的图像视野是 188 * 120 的四分之一,虽然也和 752 * 480 呈整数倍关系,但和上述情况不同
  7. *
  8. * 注意复制是必须的,这样在处理复制图像时,原图像变量就可以正常接收摄像头数据
  9. * 形 参: 无
  10. * 示例: Copy_Zip_Image();
  11. * 返回值: 无
  12. */
  13. void Copy_Zip_Image(void) //*****
  14. {
  15. uint8 i,j;
  16. if(mt9v03x_finish_flag == 1 && Inverse_Flag == 0) //mt9v03x_finish_flag:逐飞库定义摄像头采集标志位,采集完一帧图像会挂1
  17. //Inverse_Flag:自定义逆透视标志位,挂1时会得到一张逆透视图像,根据自己需求而定
  18. {
  19. for(i = 0; i < Image_Y; i++)
  20. {
  21. for(j = 0; j < Image_X; j++)
  22. {
  23. Find_Line_Image[i][j] = mt9v03x_image[i * 2][(uint8)(j * 2.35)]; //将188 * 120图像压缩为 80 * 60大小,X轴比为2.35,Y轴比为2
  24. }
  25. }
  26. if(Image_Count_Flag == 1) //Image_Count_Flag:自定义开启图像计数标志位,挂1时开启图像计数,即每采集一张图像计数+1;挂0时图像计数清零
  27. {
  28. Image_Count ++;
  29. }
  30. else if(Image_Count_Flag == 0)
  31. {
  32. Image_Count = 0;
  33. }
  34. // Image_Num ++;
  35. mt9v03x_finish_flag = 0; //注意清掉图像采集完成标志位
  36. }
  37. else if(mt9v03x_finish_flag == 1 && Inverse_Flag == 1)
  38. {
  39. for(i = 0; i < Image_Y; i++)
  40. {
  41. for(j = 0; j < Image_X; j++)
  42. {
  43. Find_Line_Image[i][j] = mt9v03x_image[i * 2][(uint8)(j * 2.35f)];
  44. }
  45. }
  46. Get_Inverse_Perspective_Image(Find_Line_Image, I_Perspective_Image); //逆透视处理函数,在另处详细说明
  47. if(Image_Count_Flag == 1)
  48. {
  49. Image_Count ++;
  50. }
  51. else if(Image_Count_Flag == 0)
  52. {
  53. Image_Count = 0;
  54. }
  55. // Image_Num ++;
  56. mt9v03x_finish_flag = 0;
  57. }
  58. }

注:图像大小是工程早期就要确立的,比如确定80 * 60的图像,那么整个比赛期间就不要再更改了,更改整个代码都要大动筋骨。同时,摄像头位对X,Y的坐标值应该很敏感,后期若该变图像大小,会导致写代码时会因固有的印象经常写出bug。

三、总初始化函数

  1. 十分建议写一个总初始化函数,并把所有初始化丢进去,然后用标志位来开关这些初始化。这里给个例子:
  1. uint8 Other_Show_Flag = 0;
  2. uint8 Show_Flag = 0;
  3. void All_Init(uint8 pit_flag, uint8 show_flag, uint8 other_show_flag, uint8 WiFi_send_flag, uint8 LED_screen_flag, uint8 TOF_flag)
  4. {
  5. Other_Show_Flag = other_show_flag; //自定义其余信息显示标志位,主要用于测试代码时,要在代码运行内部显示一些参数用于找出异常和错误
  6. //同时又要经常开启或关闭,不想一直注释或删掉,就可以if判断这个标志位是否为1,进而打开或关闭这些显示
  7. if(pit_flag == 1) //中断初始化
  8. {
  9. pit_ms_init(CCU60_CH0, 10);
  10. }
  11. if(show_flag == 1) //摄像头和图像初始化
  12. {
  13. // Cammer_Init();
  14. Cammer_Init_TFT180();
  15. }
  16. else if(show_flag == 2)
  17. {
  18. // Cammer_Init();
  19. Cammer_Init_IPS200();
  20. }
  21. else
  22. {
  23. Cammer_Init();
  24. }
  25. if(WiFi_send_flag == 1) //WiFi图传初始化
  26. {
  27. WiFi_Send_Init();
  28. }
  29. if(LED_screen_flag == 1) //灯光秀初始化
  30. {
  31. LED_Screen_Init();
  32. }
  33. if(TOF_flag == 1) //TOF测距模块初始化
  34. {
  35. TOF_Init();
  36. }
  37. // wireless_uart_init();
  38. }
  1. 而后在主函数中如下调用:
  1. All_Init( 0, //是否开启中断标志位 //0:关闭 1:开启
  2. 2, //是否开启屏幕显示标志位 //0:关闭 1:TFT180显示 2:IPS200显示 (默认开启摄像头初始化)
  3. 0, //是否开启其余显示标志位 //0:关闭 1:开启
  4. 0, //WiFi图传初始化标志位 //0:关闭 1:开启
  5. 0, //LED点阵屏初始化标志位 //0:关闭 1:开启
  6. 0); //TOF模块初始化标志位 //0: 关闭 1:开启
  1. 这样通过01赋值即可开关控制所有初始化,对于后期调试代码,或是比赛时临时更改都非常方便明了。

四、图像补黑框

  1. 对于爬线算法,不论是迷宫还是八邻域,当遇到十字或弯道时,会有一侧或两侧丢线(即图像内没有赛道边线),这个时候爬线算法该怎么处理处理呢?答案是在爬线前补黑框,对于八邻域或常规的迷宫算法,只需在图像的 **上、左、右** 边缘补一格宽度的黑框即可。
  2. 那么黑框怎么补?将指定的图像行或列原灰度值更改为指定的灰度值(对于二值化图像来说,指定灰度值为0;对于灰度图像来说,为了更好地融入背景环境,灰度值就得通过算法求取了,算法在下方会讲解)。
  3. 但对于上交自适应迷宫,或本人优化后的自适应八向迷宫来说,黑框就不能在图像最边缘补了,而是要间隔一行取补黑框。因为计算阈值是 5 * 5 大小,对于每一次的中心点,上下左右最少得有两格像素宽度。(未使用此算法的直接看代码就可以)
  4. 对于算法可看我之前的文章:智能车摄像头开源—1.2 核心算法:自适应八向迷宫(下)
  5. 实际效果如图:(这里用高斯模糊处理了赛道边线,但对黑框无影响,或者应该称之为灰框)

(黑框灰度值经过算法处理,与背景融合度较高,但想来不难分辨,相必看懂这张图像就可以理解如何在最边缘补黑框了)

  1. 如此以后黑框就可牢牢锁住边线。那对于自适应迷宫算法为什么不直接补灰度值为0的黑框呢?
  2. 在小车运行时,会遇到环岛、十字、弯道等导致赛道边线丢失的情况,此时,爬线算法由原先沿着赛道边线爬取转为沿着黑框爬取,那么不可避免的会经过黑框与赛道边线的交接区域。在交接区域,若黑框的灰度值为0,当 5 * 5 区间计算阈值时,由于黑框的原因,会导致阈值被大幅拉低,直接将赛道边线判定为白,意味着交接失败,导致后续爬线紊乱。
  3. 至于为什么补一格宽,而不补两格宽,是为了更好地保留原图像信息,进而使算法更精确。
  4. 上代码,初始第一张图像黑框使用固定的灰度值,后续由算法处理得到:
  1. uint8 Black_Box_Value_FFF = 50;
  2. uint8 Black_Box_Value_FF = 50;
  3. uint8 Black_Box_Value_F = 50;
  4. //画黑框(必须为一个像素宽度,边界务必空出一格)
  5. /**
  6. * 函数功能: 图像补黑框
  7. * 特殊说明: 注意黑框与边界间隔一格像素宽度
  8. * 形 参: uint8 black_box_value 黑框的灰度值
  9. * uint8(*image)[Image_X] 要补黑框的图像
  10. *
  11. * 示例: Draw_Black_Box(Black_Box_Value, Find_Line_Image);;
  12. * 返回值: 无
  13. */
  14. void Draw_Black_Box(uint8 black_box_value, uint8(*image)[Image_X])
  15. {
  16. uint8 i,j;
  17. Black_Box_Value_FFF = Black_Box_Value_FF;
  18. Black_Box_Value_FF = Black_Box_Value_F;
  19. Black_Box_Value_F = black_box_value;
  20. black_box_value = 0.5 * Black_Box_Value_F + 0.3 * Black_Box_Value_FF + 0.2 * Black_Box_Value_FFF; //滤波
  21. Black_Box_Value = black_box_value;
  22. for(i = 1; i < 60; i++)
  23. {
  24. image[i][Image_X - 2] = black_box_value;
  25. image[i][1] = black_box_value;
  26. }
  27. for(j = 1; j < Image_X - 2; j++)
  28. {
  29. image[1][j] = black_box_value;
  30. }
  31. }

五、差比和

  1. 差比和原理是使用两个像素点灰度值(分别设值为 a b),使用式子 a - b) / (a + b),将比值左移七位(乘128倍放大,移位运算速度比直接乘更快,故不乘100),最后得到的值可以反应两个像素点灰度值的差异。当两者灰度值相差越大,结果便越大。最后将结果与阈值相比较,大于阈值时即可判定出现了灰度值快速变化,这也是爬线算法找起点的关键所在。
  1. /**
  2. * 函数功能: 差比和
  3. * 特殊说明: 用于爬线算法找起点
  4. * 形 参: int16 a 数值较大的灰度值
  5. * int16 b 数值较小的灰度值
  6. * uint8 compare_value 差比和阈值
  7. *
  8. * 示例: Compare_Num(image[start_row][i + 5], image[start_row][i], Compare_Value);
  9. * 返回值: 大于阈值返回1,否则返回0.
  10. */
  11. int16 Compare_Num(int16 a, int16 b, uint8 compare_value) //****
  12. {
  13. if((((a - b) << 7) / (a + b)) > compare_value)
  14. {
  15. return 1;
  16. }
  17. else
  18. {
  19. return 0;
  20. }
  21. }

六、爬线算法找起点

  1. 爬线算法又称生长算法,那么“生长”,就必定有种子,这个种子就是起点。车赛常用的生长算法为八邻域和迷宫巡线。
  2. 算法原理为从选定的一行图像中间开始(如我的图像宽度为80,那么就从 X = 40 开始),先向左,将每个点与其坐标 X + 5 的点进行差比和比较,当大于阈值时,就可判定为找到了左侧赛道边线。对于右侧,向右将每个点与其坐标 X - 5 的点进行差比和处理,当大于阈值时,就可判定为找到了右侧起点。
  1. /**
  2. * 函数功能: 爬线算法找起点
  3. * 特殊说明: 无
  4. * 形 参: uint8 start_row 找起点的图像行Y坐标
  5. * uint8(*image)[Image_X] 要处理的图像
  6. * uint8 *l_start_point 存储左侧起始点的数组(全局变量)
  7. * uint8 *r_start_point 存储右侧起始点的数组(全局变量)
  8. * uint8 l_border_x 向左找起点的截止点,最远找到这里就停止
  9. * uint8 r_border_x 向右找起点的截止点,最远找到这里就停止
  10. *
  11. * 示例: Get_Start_Point(Image_Y - 3, Find_Line_Image, Adaptive_L_Start_Point, Adaptive_R_Start_Point, 1, 78)
  12. * 返回值: 两边都找到返回1,否则返回0.
  13. */
  14. uint8 Get_Start_Point(uint8 start_row, uint8(*image)[Image_X], uint8 *l_start_point, uint8 *r_start_point, uint8 l_border_x, uint8 r_border_x) //*****
  15. {
  16. uint8 i = 0, j = 0;
  17. uint8 L_Is_Found = 0, R_Is_Found = 0; //找到起点时挂出对应标志位
  18. uint8 Start_X = 0; //起始X坐标,第一张图像取图像的行中点,后续图像用上一次图像左右两侧起始点的中间值
  19. uint8 Start_Row_0 = 0; //起始Y坐标
  20. Start_Row_0 = start_row;
  21. Start_X = Image_X / 2;
  22. //从中间往左边,先找起点
  23. for(j = 0; j < 10; j ++) //指定的行没找到起点时,向上走一行继续找,最多找十行
  24. {
  25. l_start_point[1] = start_row;//y
  26. r_start_point[1] = start_row;//y
  27. if(Start_Flag == 0 || Element_State == Zebra) //第一张图像和遇到斑马线时,起始X坐标选用图像的行中点
  28. {
  29. Start_X = Image_X / 2;
  30. }
  31. else
  32. {
  33. Start_X = (l_start_point[0] + r_start_point[0]) / 2; //否则起始X坐标用上一次图像左右两侧起始点的中间值
  34. }
  35. {
  36. for (i = Start_X; i > l_border_x - 1; i--) //向左找起始点
  37. {
  38. if (Compare_Num(image[start_row][i + 5], image[start_row][i], Compare_Value))//差比和为真
  39. {
  40. {
  41. l_start_point[0] = i; //找到后记录X坐标
  42. L_Is_Found = 1; //挂出找见标志位
  43. break;
  44. }
  45. }
  46. }
  47. for (i = Start_X; i < r_border_x + 1; i++) //向右找起始点
  48. {
  49. if (Compare_Num(image[start_row][i - 5], image[start_row][i], Compare_Value))//差比和为真
  50. {
  51. {
  52. r_start_point[0] = i;
  53. R_Is_Found = 1;
  54. break;
  55. }
  56. }
  57. }
  58. if(L_Is_Found && R_Is_Found)
  59. {
  60. Start_Flag = 1; //是否为第一张图像标志位
  61. return 1;
  62. }
  63. else
  64. {
  65. start_row = start_row - 1; //当此行有一侧没找到,就向上移动一行重新找
  66. }
  67. }
  68. }
  69. }

七、求取边线

  1. 前面的文章中讲过,就不过多赘述,这里给到链接:
  2. 智能车摄像头开源—1.2 核心算法:自适应八向迷宫(下)

八、二维边线提取一维边线

  1. 爬线算法得到的是二维数组边线,即存储了每个点的横坐标和纵坐标,每行可以有多个点。但一维边线是只存储X坐标,Y坐标来自于图像的行,即每行只可以有一个点。
  2. 二维边线信息量丰富,但不适宜中线的提取,因此一般会使用算法从二维边线提取出一维边线。同时一维边线还可以用于元素判断和处理的特殊场景,后续文案讲解。
  1. /**
  2. * 函数功能: 由二维边线数组提取一维边线
  3. * 特殊说明: 无
  4. * 形 参: uint16 l_total //左侧二维边线点的个数
  5. * uint16 r_total //右侧二维边线点的个数
  6. * uint8 start //起始行(图像底部)
  7. * uint8 end //截止行(图像顶部)
  8. * uint8 *l_border //存储左侧一维边线的数组
  9. * uint8 *r_border //存储右侧一维边线的数组
  10. * uint8(*l_line)[2] //存储左侧二维边线的数组
  11. * uint8(*r_line)[2] //存储右侧二维边线的数组
  12. *
  13. * 示例: Get_Border(L_Statics, R_Statics, Image_Y - 3, 2, L_Border, R_Border, L_Line, R_Line);
  14. * 返回值: 无
  15. */
  16. void Get_Border(uint16 l_total, uint16 r_total, uint8 start, uint8 end, uint8 *l_border, uint8 *r_border, uint8(*l_line)[2], uint8(*r_line)[2])
  17. {
  18. uint8 i = 0;
  19. uint16 j = 0;
  20. uint8 h = 0;
  21. for (i = 0; i < Image_Y; i++)
  22. {
  23. l_border[i] = X_Border_Min;
  24. r_border[i] = X_Border_Max; //右边线初始化放到最右边,左边线放到最左边,这样闭合区域外的中线就会在中间,不会干扰得到的数据
  25. }
  26. h = start;
  27. //右边
  28. for (j = 0; j < r_total; j++)
  29. {
  30. if (r_line[j][1] == h)
  31. {
  32. r_border[h] = r_line[j][0];
  33. }
  34. else
  35. {
  36. continue;//每行只取一个点,没到下一行就不记录
  37. }
  38. h--;
  39. if (h == end)
  40. {
  41. break;//到最后一行退出
  42. }
  43. }
  44. h = start;
  45. for (j = 0; j < l_total; j++)
  46. {
  47. if (l_line[j][1] == h)
  48. {
  49. l_border[h] = l_line[j][0];
  50. }
  51. else
  52. {
  53. continue;//每行只取一个点,没到下一行就不记录
  54. }
  55. h--;
  56. if (h == end)
  57. {
  58. break;//到最后一行退出
  59. }
  60. }
  61. }

九、阈值处理与图像迭代

  1. 此处处理只针对于自适应(八向)迷宫,未使用此算法的可以略过。
  2. 相必 1 + 1 = 2 都会吧,那就放心往下看。
  3. 在使用算法求取边线时,我们记录了每个中心点的阈值,将些阈值进行特殊处理,可以得到判断斑马线的阈值,下张图像差比和找起点的阈值和补黑框的灰度值。实现真正意义上的图像迭代。(变量名应该已经很明了了)
  1. uint8 Adaptive_L_Thres_Max = 0; //左侧阈值最大值
  2. uint8 Adaptive_R_Thres_Max = 0; //右侧阈值最大值
  3. uint8 Adaptive_L_Thres_Min = 0; //左侧阈值最小值
  4. uint8 Adaptive_R_Thres_Min = 0; //右侧阈值最小值
  5. uint8 Adaptive_Thres_Average = 0; //阈值均值
  6. uint8 Last_Adaptive_Thres_Average = 0; //用于阈值均值滤波
  7. /**
  8. * 函数功能: 提取阈值中的最大最小值,并求出阈值均值
  9. * 特殊说明: 计算时间小于5us
  10. * 形 参: 无
  11. *
  12. * 示例: Thres_Record_Process();
  13. * 返回值: 无
  14. */
  15. void Thres_Record_Process(void)
  16. {
  17. uint8 i = 0;
  18. uint8 Left_Temp_Value_1 = 0;
  19. uint32 Left_Temp_Value_2 = 0;
  20. uint8 Right_Temp_Value_1 = 0;
  21. uint32 Right_Temp_Value_2 = 0;
  22. uint8 L_Average_Thres = 0;
  23. uint8 R_Average_Thres = 0;
  24. Adaptive_L_Thres_Max = 0;
  25. Adaptive_R_Thres_Max = 0;
  26. Adaptive_L_Thres_Min = 0;
  27. Adaptive_R_Thres_Min = 0;
  28. Adaptive_L_Thres_Max = L_Thres_Record[0];
  29. Adaptive_L_Thres_Min = L_Thres_Record[0];
  30. Adaptive_R_Thres_Max = R_Thres_Record[0];
  31. Adaptive_R_Thres_Min = R_Thres_Record[0];
  32. for(i = 0; i < Adaptive_L_Statics; i += 2) //间隔取值即可,减少计算量
  33. {
  34. if(L_Line[i][0] != 2 && L_Line[i][1] != 2) //舍去位于黑框上的边线点阈值,“2”即左边线位于黑框上时的X坐标
  35. {
  36. if(L_Thres_Record[i] < Adaptive_L_Thres_Min)
  37. {
  38. Adaptive_L_Thres_Min = L_Thres_Record[i];
  39. }
  40. if(L_Thres_Record[i] > Adaptive_L_Thres_Max)
  41. {
  42. Adaptive_L_Thres_Max = L_Thres_Record[i];
  43. }
  44. Left_Temp_Value_1 ++;
  45. Left_Temp_Value_2 += L_Thres_Record[i];
  46. }
  47. }
  48. for(i = 0; i < Adaptive_R_Statics; i += 2)
  49. {
  50. if(Adaptive_R_Line[i][0] != 77 && Adaptive_R_Line[i][1] != 2) //与左侧同理
  51. {
  52. if(R_Thres_Record[i] < Adaptive_R_Thres_Min)
  53. {
  54. Adaptive_R_Thres_Min = R_Thres_Record[i];
  55. }
  56. if(R_Thres_Record[i] > Adaptive_R_Thres_Max)
  57. {
  58. Adaptive_R_Thres_Max = R_Thres_Record[i];
  59. }
  60. Right_Temp_Value_1 ++;
  61. Right_Temp_Value_2 += R_Thres_Record[i];
  62. }
  63. }
  64. if(Left_Temp_Value_1 == 0) //当左侧边线全部位于边界上时,直接将阈值均值取0
  65. {
  66. L_Average_Thres = 0;
  67. }
  68. else //求出左侧阈值均值
  69. {
  70. L_Average_Thres = (uint8)(Left_Temp_Value_2 / Left_Temp_Value_1);
  71. }
  72. if(Right_Temp_Value_1 == 0) //与左侧同理
  73. {
  74. R_Average_Thres = 0;
  75. }
  76. else
  77. {
  78. R_Average_Thres = (uint8)(Right_Temp_Value_2 / Right_Temp_Value_1);
  79. }
  80. if(Image_Num <= 1) //前两张图像直接求均值
  81. {
  82. Last_Adaptive_Thres_Average = (uint8)((L_Average_Thres + R_Average_Thres) / 2);
  83. }
  84. else
  85. {
  86. if(My_ABS_uint8(L_Average_Thres - R_Average_Thres) >= 40) //当两侧边线的阈值均值相差过大时,进一步处理
  87. {
  88. if(My_ABS_uint8(L_Average_Thres - Last_Adaptive_Thres_Average) <= My_ABS_uint8(R_Average_Thres - Last_Adaptive_Thres_Average)) //选取两侧阈值均值最接近上次图像阈值均值的值作为此次的值
  89. {
  90. Adaptive_Thres_Average = (uint8)((Last_Adaptive_Thres_Average + L_Average_Thres) / 2);
  91. Last_Adaptive_Thres_Average = Adaptive_Thres_Average;
  92. }
  93. else
  94. {
  95. Adaptive_Thres_Average = (uint8)((Last_Adaptive_Thres_Average + R_Average_Thres) / 2);
  96. Last_Adaptive_Thres_Average = Adaptive_Thres_Average;
  97. }
  98. }
  99. else //当两侧阈值均值相差不大时,直接求三者均值
  100. {
  101. Adaptive_Thres_Average = (uint8)((Last_Adaptive_Thres_Average + L_Average_Thres + R_Average_Thres) / 3);
  102. Last_Adaptive_Thres_Average = Adaptive_Thres_Average;
  103. }
  104. }
  105. //获得判断斑马线的阈值
  106. Zbra_Thres = Adaptive_Thres_Average - 10;
  107. //获得起始点差比和的阈值
  108. if(Adaptive_Thres_Average >= 100 && Adaptive_Thres_Average <= 140) //阈值均值落在100 - 140之间,说明图像亮度很合适,此时将差比和阈值设为20即可
  109. {
  110. Compare_Value = 20;
  111. }
  112. else if(Adaptive_Thres_Average < 100)
  113. {
  114. Compare_Value = 20 - (uint8)(((float)(100 - Adaptive_Thres_Average) / 60.0f) * 10.0f); //当小于100时,适当拉低差比和阈值,使其在图像较暗的情况下更容易找出赛道边线
  115. }
  116. else if(Adaptive_Thres_Average > 140)
  117. {
  118. Compare_Value = 20 - (uint8)(((float)(Adaptive_Thres_Average - 140) / 60.0f) * 10.0f); //当大于140时,适当拉低差比和阈值,使其在图像较亮的情况下更容易找出赛道边线
  119. }
  120. //求黑框灰度值
  121. Black_Box_Value = (uint8)(0.45f * (float)sqrt(Adaptive_L_Thres_Min * Adaptive_L_Thres_Min + Adaptive_R_Thres_Min * Adaptive_R_Thres_Min)) + (uint8)(0.1f * (float)Black_Box_Value_1);
  122. //0.45f即0.9 * 0.5,0.9为权重,0.5可自己调节。Black_Box_Value_1为两侧爬线起点的灰度值均值(起点值灰度值为图像中赛道黑线的灰度值,不可为黑框灰度值,可以自己写算法处理下)
  123. //或者Black_Box_Value_1直接丢20 ~ 50之间的值即可
  124. }
  1. 至于差比和阈值、斑马线阈值、黑框灰度值的计算公式是怎么得到的,只能说是随便丢的,但是实际测试效果很好,可根据自己需求去调整。黑框灰度值与赛道蓝布背景的融合度越高越好。
  2. 至此,整个代码和算法处理就真正活了起来,算法可以自己优化参数,以增强其适应性。这也是我比较推荐的代码方式,如何让代码活起来、动起来,是很烧脑,同时也很有趣的一件事。
  3. 至于得到的边线阈值均值,可以直接拿这个参数来二值化图像,是的,你没有看错,就是二值化图像。我之前做过测试,效果还是非常好的,但是没什么必要。因为已经得到了边线。
  4. 同时边线阈值均值打在屏幕上,可以在比赛时,不看图像直接调曝光值,因为阈值均值就反映了图像的亮暗程度。均值阈值在60 ~ 150 之间为宜,范围也是比较宽泛,上场打开摄像头,一看值比较合理,直接上去跑就完事。个人实测效果是非常好的。至于摄像头参数设置,可以参考我这篇文案:智能车摄像头开源—2 摄像头参数设置经验分享。

十、综合梳理

这里进行归纳总结,实际使用时代码按以下流程:

  1. 所有设备初始化

  2. 读取图像并压缩复制

  3. 图像补黑框

  4. 差比和找爬线起始点

  5. 自适应八向迷宫爬取二维边线

  6. 二维边线提取一维边线

  7. 阈值处理与迭代参数计算

    1. 至此,摄像头图像基础处理与边线提取(底层)部分就已讲解完毕。建议写一个函数,直接将上述流程丢入函数内,就可一键调用提取出边线信息。

十一、效果展示

展示均为一维边线

1、十字效果展示—正常灯光(有补线处理)

  1. ![](https://i-blog.csdnimg.cn/direct/4af6846e2dc24c8fb4a90062bfb32b74.jpeg)

2、圆环效果展示—正常灯光(有单边巡线处理)

3、弯道效果展示—正常灯光

4、直线效果展示—正常灯光

5、实验室开灯,遇强光效果展示


手机俯拍

手机位于摄像头视角

算法运行效果

  1. 可以看出近端的强光对算法来说几乎没有难度

6、实验室关灯,环岛遇强光效果展示


手机俯拍

手机位于摄像头视角

算法运行效果

7、实验室关灯,由暗区域过渡到高亮区域效果展示


手机俯拍

手机位于摄像头视角

算法运行效果

  1. 显然有很大难度,左侧出现了部分边线紊乱,但算法依然抗住了。

8、实验室关灯,由高亮区域过渡到暗区域效果展示


手机俯拍

算法运行效果

  1. 可以看出远端出现了部分紊乱,但仍能保证车正常通过。

9、一张抽象的图片

  1. 当时拿起车模偶然保存到的一张图片,可以看到在很恶劣的情况下仍能保证正常运行。所以说算法的上限还是很高的。
  2. 上述处理的都是80 * 60的图像,但显然188 * 120的图像会对光线敏感不均的图像能表现出更好的适应性,如果选取我的算法,还是建议处理188 * 120图像,一张图下来也不会超过1ms

智能车摄像头开源—1.1 核心算法:自适应八向迷宫(上)

智能车摄像头开源—1.2 核心算法:自适应八向迷宫(下)

智能车摄像头开源—2 摄像头基础参数设置经验分享

智能车摄像头开源—3 图像基础处理、迭代优化与效果展示


本文转载自: https://blog.csdn.net/lh66969696/article/details/142988183
版权归原作者 三唐队队长 所有, 如有侵权,请联系我们删除。

“智能车摄像头开源—3 图像基础处理、迭代优化与效果展示”的评论:

还没有评论