Niblack
Niblack是局部二值化算法里思路简单,同时效果稳定的算法了。中心思想是T = m + s*k,T为以像素点为中心的一个w * w区域内所有像素点灰度值的平均值,s为矫正系数,k为w * w个像素点灰度的标准差
这里省略后面运算标准差的计算(K66算不过来),单用平均值作为单个像素的阈值,效果也很不错。
同时。,为了减少代码运算量,运用积分图法计算全局像素的灰度和,从而大大减少代码的重复运算量,K66终于不掉帧了(积分图法自行百度,百度讲得很清楚)
uint32 temp[70][186];
uint16 T[70][186];
int max(int a,int b)
{
return a >= b ? a : b;
}
int min(int a,int b)
{
return a <= b ? a : b;
}
void Niblack()
{
uint8_t *p_image = &image[30][1];
uint8_t *p_Pixels = &P_Pixel[0][0];
uint16 *p_T = &T[0][0];
uint8_t w = 60;
int m = 70;
int n = 186;
int j = 0;
float sum = 0;
float average = 0;
//创建积分图
temp[0][0] = image[30][1];
for (j = 1;j < n;j++)
temp[0][j] = image[30][1 + j] + temp[0][j - 1];
for (i = 1;i < m;i++)
temp[i][0] = image[30 + i][1] + temp[i - 1][0];
for (i = 1;i < m;i++)
{
for (j = 1;j < n;j++)
{
temp[i][j] = image[30 + i][1 + j] + temp[i][j - 1] + temp[i - 1][j] - temp[i - 1][j - 1];
}
}
//开始计算阈值
for (i = 0;i < m;i++)
{
for (j = 0;j < n;j++)
{
int a1 = max(i - w - 1,0);
int a2 = min(i + w - 1,m - 1);
int b1 = max(j - w - 1,0);
int b2 = min(j + w - 1,n - 1);
sum = temp[a1][b1] - temp[a1][b2] - temp[a2][b1] + temp[a2][b2];
average = sum /((a2 - a1)*(b2 - b1));
*p_T = (uint16)average;
p_T++;
}
}
p_T = &T[0][0];
for (i = 0;i<m;i++)
{
for (j = 0;j<n;j++)
{
if (*p_image > (uint8_t)((*p_T) * 0.95f))
{
*p_Pixels = white;
}
else
{
*p_Pixels = black;
}
p_Pixels++;
p_image++;
p_T++;
}
p_image+=2;
}
}
最后二值化时,增加一个小于1的系数,能对平滑局域产生的为噪点很好的抑制,对大津无法处理的环内黑线有良好的处理效果
I = imread('图像路径');
I = rgb2gray(I);
figure(1);
imshow(I);
w = 60;
[m,n] = size(I);
T = zeros(m,n);
temp = zeros(m,n);
back = zeros(m,n);
%创建积分图
temp(1,1) = I(1,1);
for j = 2:n
temp(1,j) = double(I(1,j)) + temp(1,j - 1);
end
for i = 2:m
temp(i,1) = double(I(i,1)) + temp(i - 1,1);
end
for i = 2:m
for j = 2:n
temp(i,j) = double(I(i,j)) + temp(i,j - 1) + temp(i - 1,j) - temp(i - 1,j - 1);
end
end
for i = 1:m
for j = 1:n
a1 = max(i - w,1);
a2 = min(i + w,m);
b1 = max(j - w,1);
b2 = min(j + w,n);
sum = temp(a1,b1) - temp(a1,b2) - temp(a2,b1) + temp(a2,b2);
average = double(sum) /((a2 - a1)*(b2 - b1));
% s = 0;
% for k = a1:a2
% for l = b1:b2
% s = s + (uint32(I(k,l)) - average)*(uint32(I(k,l)) - average);
% end
% end
% s= sqrt(double(s)/((a2 - a1)*(b2 - b1)));
T(i,j) = average + 0;
end
end
for i = 1:m
for j = 1:n
if I(i,j) > T(i,j)*0.95
back(i,j) = uint8(255);
else
back(i,j) = uint8(0);
end
end
end
figure(2);
imshow(back);
左大津,右Niblack(还有两个参数可以调参,一个是w,还有一个是最后阈值时T乘的小于1的数)
在k66上运算大津一次大约为2.3ms,Niblack。。算了吧。
尝试改进大津
由于Niblack的运算时序还是使k66爆炸(同时TC264方面虽然是双核,但传图处理再赛道识别是一套连续的操作,实际上还是单核在处理,然后264单个核比不过K66。。。),于是转换一下思路,能不能把大津改了?就试了一下,效果还挺好
首先中心思路是赛道图像中间比较亮(一般大津不出问题)两边边缘部分较暗,在赛道部分灰度比中间部分小10左右(有时更多),于是大津边缘糊的便是由于全局阈值,有部分边缘赛道被划入到背景中,同理赛道的反光是由于蓝布上光线太亮,部分区域灰度值过大,被划入目标(赛道)中。基于此,分块大津将赛道图像分成三份(左,中,右),分别处理三份光线条件不同的图像(同时大津的时序也并没有增加多少,仅是多判断了两次类间方差),用三个阈值处理一份图像,可以比较有效抑制模糊和反光问题。
(左边赛道糊的部分减少,右上方反光部分得到抑制)
可惜分块阈值的先天缺陷是一旦某快阈值出现问题,将出现图像上的分块
(这里是由于右边分块时白色赛道太少,于是。。大津方差算出来阈值误差较大。)
为了解决这部分阈值错误的bug,便利用大津算出来的部分图像信息,灰度和以及背景像素点占比来判断这种错误阈值问题,思路是统计学。。。。灰度和与像素点比例之间存在一定的关系,即比例越大,灰度和会越小,而反常情况时,灰度和与比例之间会有很大的误差(与正常情况相比)
(图中右块灰度和17左右时,比例正常应在0.9左右)
然后这种bug一般发生在灰度和很小或者很大时(相对于正常目标和背景像素点比较平均时),因为这时候目标和背景像素点极有可能某个部分占有很大比例,使得大津的二值化变成一种强制化的操作,类似于鸡蛋里挑骨头。于是判断条件的一个比较好的思路便是先判断灰度和过大或过小,再进行比例和阈值差(与中间图像)的判断
判出错误情况后,再将中间部分的阈值(中间一般情况下没有差错,除了坡道)减去一个常数后, 再进行阈值分割,便可以较好得处理赛道图像
float bin_float[256]; //灰度比例直方图
int size = 70 * 186;
float u = 0; //全图平均灰度
float w0 = 0;
float u0 = 0; //前景灰度
byte Bin_Array[256];
int i;
float gray_hh = 0;//前景灰度和
float var = 0;//方差
float maxvar = 0;//最大方差
float maxgray = 0;//最大灰度占比
float maxbin = 0;
struct size_point
{
int x0;
int y0;
int x1;
int y1;
};
struct size_point ostu_point[3]={
{0,0,40,69},
{41,0,135,69},
{136,0,185,69},
};
void Ostu(int x,int y)//大津
{
int j, k;
uint8_t (*p_image)[188] = &image[17];
for (k = 0; k < 3; k++)
{
maxvar = 0;
w0 = 0;
u = 0;
gray_hh = 0;
var = 0;
Thresholds[k] = 0;
for (i = 0; i < 256; i++)
{ bin_float[i] = 0; }
for (i = ostu_point[k].y0; i <= ostu_point[k].y1; i++)
{
for (j = ostu_point[k].x0; j <= ostu_point[k].x1; j++)
{
++bin_float[*(*(p_image + i) + j)];
}
}
size = (ostu_point[k].y1 - ostu_point[k].y0 + 1) * (ostu_point[k].x1 - ostu_point[k].x0 + 1);
for (i = 0; i < 256; i++)
{
bin_float[i] = bin_float[i] / size;
u += i * bin_float[i];
}
//创建比例灰度直方图
for (i = 0; i < 256; i++)
{
w0 += bin_float[i];
gray_hh += i * bin_float[i]; //灰度和
u0 = gray_hh / w0;
var = (u0 - u) * (u0 - u) * w0 / (1 - w0);
if (var > maxvar)
{
maxgray = gray_hh;
maxbin = w0;
maxvar = var;
Thresholds[k] = (byte)i;
}
}
if (k == 0)
{
if (gray_hh > 15 && gray_hh <= 33)
{
if (maxbin < 0.9f)
{
Thresholds[k] = (byte)(Thresholds[1] - 3);
}
}
else if (gray_hh > 41 && gray_hh <= 47)
{
if (maxbin < 0.64f || maxbin > 0.76f)
{
Thresholds[k] = (byte)(Thresholds[1] - 3);
}
}
else if (gray_hh > 50 && gray_hh <= 60)
{
if (maxbin < 0.42f || maxbin > 0.58f)
{
Thresholds[k] = (byte)(Thresholds[1] - 3);
}
}
//SetText("右边var:" + maxvar + " 阈值:" + Thresholds[k] + " 比例:" + maxbin + " 灰度和" + gray_hh);
}
else if (k == 1)
{
if(gray_hh > 69 && gray_hh < 80)
{
if (maxbin > 0.15f)
{
Thresholds[k] = (byte)(Thresholds[0] + 3);
}
}
//SetText("中间var:" + maxvar + " 阈值:" + Thresholds[k] + " 比例:" + maxbin + " 灰度和" + gray_hh);
}
else if (k == 2)
{
if (maxbin < 0.85f && gray_hh < 28)
{
Thresholds[k] = (byte)(Thresholds[1] - 3);
}
else if(gray_hh > 69 && gray_hh < 79)
{
if (maxbin < 0.5f || maxbin > 0.15f)
{
Thresholds[k] = (byte)(Thresholds[1] - 3);
}
}
//SetText("左边var:" + maxvar + " 阈值:" + Thresholds[k] + " 比例:" + maxbin + " 灰度和" + gray_hh);
}
for (i = 0; i < Thresholds[k]; i++)
{
Bin_Array[i] = black;
}
for (i = Thresholds[k]; i < 256; i++)
{
Bin_Array[i] = white;
}
for (i = ostu_point[k].y0; i <= ostu_point[k].y1; i++)
{
for (j = ostu_point[k].x0; j <= ostu_point[k].x1; j++)
{
P_Pixel[i][j] = Bin_Array[*(*(p_image + i) + j)];
}
}
}
}
之后还有一些无法处理的问题,之后再想想有什么方法解决吧,或许还可以有更好的办法改大津。
无法处理的情况:坡道部分开到天上去了,上面灯光太亮白色的天灰板都变黑了。。
处理不好的情况:当整幅图灰度直方图是比较标准的双峰时,强行分块会产生反效果
抽一半改进大津
最近发现,对于186*70的灰度图,遍历所有的点和只用一半的点(隔行取点)算出来的大津几乎没有差别(最多差2),基于此,那我们便可以用多出来的时序做点其他的事啦。
由于在分块大津中,我们很难找到一个标准值去修正强行二值化时错误的阈值,所以我们先算一遍一半大津,再分三块进行一半大津,在一半整体大津的基础上,与算出来的三块大津值进行一个互补,这样做虽然效果不会有单用三块大津值明显,但是稳定性拉满,只要大津不出错,修正后的大津阈值也不会出错;而且有了标准值后,出现强行二值化情况也可以很好得到避免(即与标准值相差一定值以上时)
if (My_Abs(Thresholds - Thresholds[k]) >= 45)//相差太大直接用标准值
{
Thresholds[k] = Thresholds;
}
else
{
Thresholds[k] = (uint8_t)(Thresholds[k] + 0.5f * (Thresholds - Thresholds[k]));//这里是取了平均,可以通过调整占比适应不同的光照条件
}
这应该是最后的一版改大津,快比赛了(混队友的,我很久没调过车了,孙哥nb),我也没有什么想法,代码写得很烂,很多都没法在k66上使用,也没办法很好解决问题,等比赛结束再写感想吧。
参考:改进Niblack算法及其在不均匀光照条件下的应用_贾坤昊
https://kns.cnki.net/KCMS/detail/detail.aspx?dbcode=CJFQ&dbname=CJFDLAST2019&filename=RJDK201904020&v=MTMwMDRSN3FmWXVSbkZ5L2tXcnpLTnlmUFpiRzRIOWpNcTQ5SFpJUjhlWDFMdXhZUzdEaDFUM3FUcldNMUZyQ1U=