认识图像:
首先,咱来了解一下图像的基本含义,图像是人类视觉的基础,是自然事物的客观反映,“图”是物体反射或透射光的分布,“像“是人的视觉系统所接受的图在人脑中所形成的印象或认识,照片、绘画、手写汉字、心电图等都是图像(来自百度百科对“图像”一词的解释)。
图像类型:
广义上,图像就是所有具有视觉效果的画面,图像根据图像记录方式的不同可分为两大类:模拟图像和数字图像。模拟图像可以通过某种物理量(如光、电等)的强弱变化来记录图像亮度信息,例如模拟电视图像;而数字图像则是用计算机存储的数据来记录图像上各点的亮度信息。
彩色图像:
彩色图像可以理解为一个像素点的亮度信息是由RGB三原色的不同配比表示;其中R、G、B三种颜色都被分为了0-255共256个色阶,每个像素点的信息通过三原色的色阶搭配进行表示,举例如下图该像素点的红色色阶是205,绿色色阶是89,蓝色色阶是68,所以这一个像素点的数据集合就是(205,89,68)也就是说彩色数字图像在计算机内部就是由这样一个个三原色组合而成的,做个简单的排列组合,如果要把所有的颜色用0、1、2这样的数字表示,则需要256×256×256个数,也就是一个24位的数,一个像素点就是一个24位的数据,可想一副图像的数据量会有多么庞大,显然一般单片机是没法完成这么大的数据处理的,这也就是为什么智能车中很少有人使用彩色摄像头的原因。(但智慧视觉组别因为要对图像彩色进行识别,所以要使用例如逐飞科技的凌瞳等摄像头采集彩色图像进行处理,该组别使用的单片机性能也非常强悍)。
灰度图像:
灰度图像,也就是我们智能车使用的灰度摄像头(逐飞总钻风,龙邱神眼等)所采集那种图像,不是彩色画面,但也不是非黑即白,而是将黑色分成了0-255共256个色阶,整幅图像的每一个像素都是用0-255中间的一个数值来表示的就类似于上面提到的二维数组Image_Data[120][188]中的数据。对比彩色图像,可以发现一个像素点只需要一个8位数据表示即可,数据量相对彩色图像是不是大大减少了。
二值化图像:
二值化图像就是整个图像中只有黑和白两种颜色,如下图所示:
以上是关于图像的一些简单的介绍。
人眼看到的图像:
智能车摄像头看到的图像:
单片机内部图像的存储是一个二维数组Image_Data[120][188],数组中的每个数据对应图像中该点位置的亮度信息。(这里由于数据量太大,截图不清晰,仅截取了整幅图像的部分)
我最终处理的图像:
这张是通过上位机传回来的二值化以后的赛道图像数组:(跟上图并非同一赛道)
这三者都是图像的表示形式,彩色图像可以通过计算公式转换成灰度图像,灰度图像也可以通过二值化处理转换成黑白图像。彩色图像只有在智慧视觉方面的组别使用,而且用的时候,逐飞会提供相应的培训,我这里就不讨论彩色图像。
二值化处理:
上面提到了,灰度图像的二维数组里面储存了每一个像素点的亮度值,而赛道和背景对于光的反射能力不同,从而导致摄像头采集到的亮度信息不同。而赛道本只有白色和深蓝色(后面深蓝色我统称为黑色),我们是不是可以取定一个值,令小于这个值的全部判定为黑色(0),大于这个值的判定为白色(255)(黑色反射光的能力差,从而摄像头采集的灰度值小)。我们称这个取定的值为阈值Threshold(0-255)。获取阈值,并将灰度图像转发为二值化图像的过程称为二值化。
阈值的获取:
对于这个阈值的获取,最简单的方法就是凭经验取一个值,然后观察二值化的效果,来调整这个值,直到获得比较理想的结果。
这种方案存在的缺陷是,如果同一场地的不同位置光照强度不同,那一个阈值就不能同时满足在不同的地方都获得很好的二值化效果,而且到了不同的场地都要调试一次阈值。
优点只有一个,省去了单片机计算阈值的时间。
这种方法只适合光照情况十分理想的场地,比如17届西部赛是采用线上赛的形式进行的,而且比赛场地就在我们自己的赛道,我们赛道光照十分理想,那么我们就可以采取这种方案。其他情况基本都不用这种方法。
既然固定阈值的方法用不了,那有没有一种方法可以根据采集的到的每一幅图像都能自己匹配一个阈值呢!这样无论小车位于什么样的光照情况下,都能用一个理想的阈值来对采集到的图像进行二值化。答案是肯定的,这就是上面提到的单片机计算阈值。
计算阈值的方法有很多,主流的有大津法,平均阈值法,soble算子法。
大津法:
大津法是通过获取一帧图像的灰度分布直方图,并根据直方图的波峰和波谷计算出灰度的中间值,根据中间值进行二值化处理,实现一个动态的阈值调整。想要学习这个算法的参考B站工训大魔王的【智能车制作加餐:摄像头数字图像处理算法-哔哩哔哩】 这里粘贴一份龙邱提供的大津法的代码
。
// An highlighted block
/*************************************************************************
* 函数名称:short GetOSTU (unsigned char tmImage[LCDH][LCDW])
* 功能说明:大津法求阈值大小
* 参数说明:tmImage : 图像数据
* 函数返回:无
* 修改时间:2011年10月28日
* 备 注: 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算法在处理光照不均匀的图像的时候,效果会明显不好,因为利用的是全局像素信息。
*************************************************************************/
short GetOSTU (unsigned char tmImage[LCDH][LCDW])
{
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 < LCDH; j++)
{
for (i = 0; i < LCDW; 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; //返回最佳阈值;
}
最开始的大津法要遍历很多次图像,是非常吃算力的,很多单片机都是跑不动。如果你想知道你的单片机计算阈值需要的时间,可以用逐飞科技的历程提供的代码。(逐飞提供的历程一定要自己好好看一下,你以后要用到的很多功能逐飞的历程里面都提供了)。
systick_start(STM1); //使用STM1 进行计时
systick_delay_ms(STM0, 100); //延时100MS 使用STM0定时器 也可以使用STM1定时器
time = systick_getval_ms(STM1); //读取STM1计时时间
printf("delay time: %dms\n", time); //将获取的时间打印出来
同时,大津法在处理光照不均匀的图像的时候,效果会明显不好,因为利用的是全局像素信息。
既然有这些缺点,那么很多学校都在原本大津法的基础上进行了改进。
针对大津法要遍历整幅图像,因此很吃算力的问题,很多人提出的方案就是将图像进行压缩。利用的原理是图像灰度值的变化是呈现阶梯状的,很少发生突变。那么我们将每一列的多个像素点的值用一个像素点的值来代替,同理每一行的值也可以。
拿行列压缩比都是2,总压缩比是4来举例。每一列左右相邻的两个像素点用一个像素点的值来表示,每一行上下相邻的两个像素点也用一个像素点来代替。这样就可以将图像的压缩4倍,可以大大减小计算阈值的工作量,以前要用4ms的程序,现在就只用1ms了。(这里补充一点,一点要注意程序运行的时间和硬件的性能相配合,比如s3010舵机的工作频率是50hz,那它每20ms就可以进行一次打角(1s== 1000ms, 1000ms/50 = 20ms)也就意味着你获得舵机打角值的所有代码一定要控制在20ms以下,而且尽量要让它有几毫秒的空闲时间 )。
这里引用以下列压缩四倍的例子来帮助大家理解一下:
原始图像:
横向压缩两倍:
横向压缩四倍:
通过上面的例子可以看到,在横向压缩两倍以后,图像还很清晰的,即使横向压缩四倍以后,图像的特征依旧保存的很完整。当然图像的压缩难免会损失一些细节,但就计算阈值来说,这一点几乎可以忽略。(同时,这里我布置一个任务,大家把原图像的阈值,压缩 四倍的阈值和压缩8倍的阈值算出来,看看他们的区别)。但压缩后会导致一个问题,就是如果你显示压缩后的图像的话,在屏幕上会很难看清,用1.8寸TFT屏幕的话是基本看不清楚的。但我看有人使用ips显示过压缩后的图像,感觉勉强还是能看清楚的。
这里再提供两种思路,一:计算阈值的时候使用压缩的图像,显示和后面处理图像的时候用原始图像;二:显示的时候用原始图像,计算阈值和处理用压缩后的图像,这个方法是很多人使用的,但这个方法要在显示的时候下功夫,需要调试一下。(我的建议是使用最后一种,将性能和调车体验都做到了最好)。
这里附上龙邱的压缩图像的代码:
// An highlighted block
/*************************************************************************
* 函数名称:void Get_Use_Image (void)
* 功能说明:把摄像头采集到原始图像,缩放到赛道识别所需大小
* 参数说明:无
* 函数返回:无
* 修改时间:2020年10月28日
* 备 注: IMAGEW为原始图像的宽度,神眼为188,OV7725为320
* IMAGEH为原始图像的高度,神眼为120,OV7725为240
*************************************************************************/
void Get_Use_Image(void)
{
short i = 0, j = 0, row = 0, line = 0;
for (i = 0; i < IMAGEH; i += 2) //神眼高 120 / 2 = 60,
// for (i = 0; i < IMAGEH; i += 3) //OV7725高 240 / 3 = 80,
{
for (j = 0; j <= IMAGEW; j += 2) //神眼宽188 / 2 = 94,
// for (j = 0; j <= IMAGEW; j += 3) //OV7725宽320 / 3 = 106,
{
Image_Use[row][line] = Image_Data[i][j];
line++;
}
line = 0;
row++;
}
}
我建议大家可以采用指针改进这个代码,可以减少一个for循环,能提高一点性能。
针对大津法对于光照不均匀的情况效果不好的改进思路是对阈值进行限幅,这里提供一下中南大学的代码(惭愧的是,中南大学的代码我没看懂,但我摸索了一下,更改了里面的部分代码,能调试出理想的图像)
//---------------------------------------------------------------------------------------
// @brief 二值化
// @param image 图像数组
// @param col 宽
// @param row 高
// @param pixel_threshold 阈值分离
// @return threshold
// @date
// @author 中南大学
// Sample usage: Threshold_Deal(image_yuanshi[0], MT9V03X_W, MT9V03X_H, Threshold_detach);
//---------------------------------------------------------------------------------------
uint8 Threshold_Deal(uint8* image, uint16 col, uint16 row, uint32 pixel_threshold)
{
#define GrayScale 256
uint16 width = col;
uint16 height = row;
int pixelCount[GrayScale];
float pixelPro[GrayScale];
int i, j;
int pixelSum = width * height;
uint8 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 += 1)
{
for (j = 0; j < width; j += 1)
{
// if((sun_mode&&data[i*width+j]<pixel_threshold)||(!sun_mode))
//{
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 < pixel_threshold; 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 = (uint8)j; //本来这里没有强制类型转换的,我自己加的
}
if (deltaTmp < deltaMax)
{
break;
}
}
return threshold;
}
到这里大津法就基本介绍完了,因为大津法是最主流,也是我最推荐的方法,所以其他两种方法我就简单介绍一下。
平均阈值法:
平均阈值就是计算整幅图像阈值的平均值作为阈值。(我不推荐)
这里提供一下杜哥用指针写的平均阈值代码:
/************************************************************************
* @name void Aver_Threshold(unsigned char *p, float *t, int pixel_num)
* @func 平均阈值
* @param *p指向存储好的图像地址,*t指向阈值变量的地址pixel_num像素点数
* @brief Aver_Threshold1(&Image, &Threshold, MT9V03X_W*MT9V03X_H)
* @return NULL
* @date 2022年4月15日
* @author 智科创新201 杜旭斌
************************************************************************/
void Aver_Threshold(unsigned char *p, float *t, int pixel_num)
{
for (int i = 0; i < pixel_num; i++)//循环遍历图像
*t += (float)*(p + i);//指向连续内存地址,取值转化
*t /= pixel_num;//计算像素均值
*t += 20;//根据环境调整阈值
}
Soble算子:
soble算子,是局部二值化,可以很大程度的减小算力,而且二值化的图像很像水墨画,但它除了边界其它黑色的地方是白色的,这一点会给写程序留下极大的隐患。(我不推荐)
/*!
* @brief 基于soble边沿检测算子的一种边沿检测
*
* @param imageIn 输入数组
* imageOut 输出数组 保存的二值化后的边沿信息
* Threshold 阈值
*
* @return
*
* @note
* @example
*
* @date 2020/5/15
*/
void lq_sobel (unsigned char imageIn[LCDH][LCDW], unsigned char imageOut[LCDH][LCDW], unsigned char Threshold)
{
/** 卷积核大小 */
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[1] = -(short) imageIn[i - 1][j - 1] + (short) imageIn[i + 1][j - 1] //{{-1, -1, -1},
- (short) imageIn[i - 1][j] + (short) imageIn[i + 1][j] // { 0, 0, 0},
- (short) imageIn[i - 1][j + 1] + (short) imageIn[i + 1][j + 1]; // { 1, 1, 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];
}
}
if (temp[0] > Threshold)
{
imageOut[i][j] = 1;
}
else
{
imageOut[i][j] = 0;
}
}
}
}
我暂时没有找到当时拍的照片,你们可以用龙邱的代码自己试试看。
到这里,主流的二值化获取阈值的方案就讲完了。我最推荐的方案是用压缩四倍的图像结合中南大学的上下限阈值方案。
获得阈值以后就是二值化了,这里附上最常见的二值化方案:
//---------------------------------------------------------------------------------------
// @brief 二值化
// @param
// @return void
// @date
// @author 中南大学
// Sample usage: Get01change_Dajin
//---------------------------------------------------------------------------------------
for (i = 0; i < LCDH; i++)
{
for (j = 0; j < LCDW; j++)
{
if (Image_Use[i][j] > Threshold) //数值越大,显示的内容越多,较浅的图像也能显示出来
Bin_Image[i][j] = 0; //0为最黑
else
Bin_Image[i][j] = 255; //255为最白
}
}
我不建议二值化的结果用1表示白,因为你显示的时候,1还是显示仅次于0的黑色,还要转化一下。
再附上我推荐的二值化代码:
//---------------------------------------------------------------------------------------
// @brief 二值化
// @param
// @return void
// @date
// @author 中南大学
// Sample usage: Get01change_Dajin
//---------------------------------------------------------------------------------------
uint8 Threshold; //阈值
uint8 Threshold_static = 225; //阈值静态下限225
uint16 Threshold_detach = 300; //阳光算法分割阈值
void Get01change_Dajin()
{
Threshold = Threshold_Deal(image_yuanshi[0], MT9V03X_W, MT9V03X_H, Threshold_detach);
if (Threshold < Threshold_static)
{
Threshold = Threshold_static;
}
uint8 thre;
for(uint8 y = 0; y < MT9V03X_H; y++) //这里考虑到了图像边缘的光照偏弱的问题
{
for(uint8 x = 0; x < MT9V03X_W; x++)
{
if (x <= 15)
thre = Threshold - 10;
else if (x >= MT9V03X_W-15)
thre = Threshold - 10;
else
thre = Threshold;
if (image_yuanshi[y][x] >thre) //数值越大,显示的内容越多,较浅的图像也能显示出来
image_01[y][x] = 255; //白
else
image_01[y][x] = 0; //黑
}
}
}
这个要结合中南的求阈值方法使用,其他求方法也可以用,不过要更改部分代码。
小结:
图像二值化的过程:
1.图像转存
2.求阈值
3.二值化
4.腐蚀(图像滤波)
至此,2,3步都讲解完了,图像转存的意思就是不使用直接处理场中断采集图像的数组,而是自己定义一个数组,把场中断采集图像的二维数组转存在我们自己定义的数组里面。原因就是系统采集和我们处理都用一个数组的话,很可能导致不可预测的问题。
这里附上转存代码:
//-----------------------------------------------------------------------------------------
// @brief 图像转存
// @param * p 储存采集到图像的数组
// @param *q 自己定义的数组
// @param pixel_num 数组大小
// @return void
// @date
// @author xiaodong
// Sample usage: Transfer_Camera(mt9v03x_image[0], image_yuanshi[0], MT9V03X_W*MT9V03X_H);
//-----------------------------------------------------------------------------------------
void Transfer_Camera(uint8 * p, uint8 *q, int16 pixel_num)
{
for(int16 i = 0; i < pixel_num; i++)
*(q +i) = *(p +i);
}
二值化以后的图像会存在一定的噪点,比如黑色的背景中间可能会突然出现一个白点,这是不符合实际情况的。可以通过算法去除掉这一个噪点,但效果有限,能起一个锦上添花的效果吧。
这里附上代码和效果图:
//-----------------------------------------------------------------------------------------
// @brief 图像滤波
// @param void
// @return void
// @date
// @author xiaodong
// Sample usage: Pixle_Filter();
//-----------------------------------------------------------------------------------------
void Pixle_Filter()
{
for(uint8 y = 10; y < MT9V03X_H-10; y++)
{
for(uint8 x = 10; x < MT9V03X_W -10; x++)
{
if((image_01[y][x] == 0) && (image_01[y-1][x] + image_01[y+1][x] + image_01[y][x-1] + image_01[y][x+1] >= 3*255))
{ //一个黑点的上下左右的白点大于等于三个,令这个点为白
image_01[y][x] = 255;
}
else if((image_01[y][x] != 0) && (image_01[y-1][x] + image_01[y+1][x] + image_01[y][x-1] + image_01[y][x+1] < 2*255))
{//一个白点的上下左右的黑点大于等于三个,令这个点为黑
image_01[y][x] = 0;
}
}
}
}
效果图:
至此,图像二值化的所有内容都介绍完了。但都是分散的,大家可能得不到一个具体的认识。现在我再附上我的代码,让大家有个整体的认识。以后大家看代码也不用一直翻来翻去。
//---------------------------------------------------------------------------------------
// @brief 图像二值化函数
// @param void
// @return void
// @date
// @author xiaodong
// Sample usage: Camera_Display();
//---------------------------------------------------------------------------------------
void Camera_Display(void)
{
if(mt9v03x_finish_flag == 1) //mt9v03x_finish_flag 是逐飞库里面场中断的标志位
{
Transfer_Camera(mt9v03x_image[0], image_yuanshi[0], MT9V03X_W*MT9V03X_H); //图像转存
mt9v03x_finish_flag = 0; //在图像使用完毕后 务必清除标志位,否则不会开始采集下一幅图像
Get01change_Dajin(); //图像二值化
Pixle_Filter(); //腐蚀(像素滤波)
}
}
void Transfer_Camera(uint8 * p, uint8 *q, int16 pixel_num)
{
for(int16 i = 0; i < pixel_num; i++)
*(q +i) = *(p +i);
}
uint8 Threshold; //阈值
uint8 Threshold_static = 225; //阈值静态下限225
uint16 Threshold_detach = 300; //阳光算法分割阈值
void Get01change_Dajin()
{
Threshold = Threshold_Deal(image_yuanshi[0], MT9V03X_W, MT9V03X_H, Threshold_detach);
if (Threshold < Threshold_static)
{
Threshold = Threshold_static;
}
uint8 thre;
for(uint8 y = 0; y < MT9V03X_H; y++) //这里考虑到了图像边缘的光照偏弱的问题
{
for(uint8 x = 0; x < MT9V03X_W; x++)
{
if (x <= 15)
thre = Threshold - 10;
else if (x >= MT9V03X_W-15)
thre = Threshold - 10;
else
thre = Threshold;
if (image_yuanshi[y][x] >thre) //数值越大,显示的内容越多,较浅的图像也能显示出来
image_01[y][x] = 255; //白
else
image_01[y][x] = 0; //黑
}
}
}
uint8 Threshold_Deal(uint8* image, uint16 col, uint16 row, uint32 pixel_threshold)
{
#define GrayScale 256
uint16 width = col;
uint16 height = row;
int pixelCount[GrayScale];
float pixelPro[GrayScale];
int i, j;
int pixelSum = width * height;
uint8 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 += 1)
{
for (j = 0; j < width; j += 1)
{
// if((sun_mode&&data[i*width+j]<pixel_threshold)||(!sun_mode))
//{
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 < pixel_threshold; 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 = (uint8)j; //本来这里没有强制类型转换的,我自己加的
}
if (deltaTmp < deltaMax)
{
break;
}
}
return threshold;
}
void Pixle_Filter()
{
for(uint8 y = 10; y < MT9V03X_H-10; y++)
{
for(uint8 x = 10; x < MT9V03X_W -10; x++)
{
if((image_01[y][x] == 0) && (image_01[y-1][x] + image_01[y+1][x] + image_01[y][x-1] + image_01[y][x+1] >= 3*255))
{ //一个黑点的上下左右的白点大于等于三个,令这个点为白
image_01[y][x] = 255;
}
else if((image_01[y][x] != 0) && (image_01[y-1][x] + image_01[y+1][x] + image_01[y][x-1] + image_01[y][x+1] < 2*255))
{//一个白点的上下左右的黑点大于等于三个,令这个点为黑
image_01[y][x] = 0;
}
}
}
}
附上我的效果图:
灰度图像:
二值化图像:
这二值化效果已经是最后的水平了,毕竟是移植更改的中南大学的代码,所以我建议大家多看人家开源的代码,这是取得成绩最快的方式,但人家开源的东西也不是说很容易就能看懂的,因为很多高水平的学校写代码指针结构体等用的非常多,就很难看懂,所以做智能车的人要坐的住冷板凳,如果你们一个队伍的人可以每人端杯茶坐在一起讨论代码的话,你们一定能进国赛的,这也是我们要你们软硬件都学的主要原因。
我很后悔的是没有看中南大学的搜线代码,我自己写的搜线代码才100行左右,中南祖传的搜线代码有700行,很多情况都已经考虑好了,看懂了可以提升很多的。
最后,我说明为什么花那么多时间来介绍二值化,为什么不直接处理灰度,毕竟逐飞科技的文章里面一直提倡我们直接处理灰度。我自己也是尝试过一段时间直接处理灰度,效果确实很好,可以在很大程度上减少光照对于寻找边线的影响,而且少了计算阈值的时间。但灰度图像比二值化图像写元素代码要复杂许多,而且逐飞也没有开源代码。目前开源的代码也基本全是二值化的,移植也方便。将图像进行压缩以后再计算阈值也不用花多少时间。所以灰度对比二值化,没有突出优点,我不推荐使用灰度图像。至于小钻风硬件二值化摄像头,我也不推荐,因为操作空间太小了。