最近在做项目,涉及到车道的检测。由于在日光下,原本就是白色或者是黄色的车道线会比较得很不清晰,于是很自然的想到了灰度拉伸的方案,从少量数据来看的结果,是好的。以下图为例,左图是三通道图,中间是利用opencv的cvtColor函数转换的灰度图,可以看到在右侧日光照射下的车道显得很不清晰,所以对高灰度值区域进行灰度拉伸,可以得到有图,显然车道清晰了很多。虽然由此左侧的车道没有原本那么明显了,但也还在可以接受的范围。
然后今天在探索能否实现自适应的拉伸方案时,用了自己手机拍摄的一张图片观察处理前后的灰度直方图,发现了一些特殊的情况。绘制灰度直方图和实现灰度拉伸的函数如下。
void GrayLinearTransform(Mat input, Mat &output, double x1 = 160, double y1 = 120, double x2 = 220, double y2 = 240);
void CheckGrayHist(Mat img)
{
if (img.channels() == 3) {
/*三通道图像,暂时不符合我们单通道灰度直方的需求.*/
cout << "Error" << endl;
}
else {
/*图像是单通道的*/
//Mat gray;
//cvtColor(img, gray, COLOR_BGR2GRAY);
int histsize = 256;
float range[] = { 0, 256 };
const float * histrange(range);
//存放直方图的计算结果,是一个256*1的矩阵
Mat gray_hist;
calcHist(&img, 1, { 0 }, Mat(), gray_hist, 1, &histsize, &histrange, true, false);
//cout << gray_hist.cols << "," << gray_hist.rows << endl;
//定义每个灰度值在图上的宽度
int bin_w = 4;
int histImageHeight = 500;
int histImageWidth = bin_w*histsize;
//画出来是1024x500的图片,定义的时候是先行后列,与后续读取数值是不同的!!
Mat histImage(histImageHeight, histImageWidth, CV_8UC3, Scalar(0, 0, 0));
//对结果进行归一化,才可以绘制到图像中
normalize(gray_hist, gray_hist, 0, histImageHeight, NORM_MINMAX, -1, Mat());
for (int i = 1; i < histsize; i++)
{
//绘制直方图,用折线图来替代通常的直方图,意义是一致的
line(histImage, Point(bin_w *(i - 1), histImageHeight - cvRound(gray_hist.at(i - 1))),
Point(bin_w * i, histImageHeight - cvRound(gray_hist.at(i))), Scalar(255, 255, 255), 2);
}
//打印图片
namedWindow(OUTPUT_TITLE, WINDOW_NORMAL);
imshow(OUTPUT_TITLE, histImage);
//imwrite("hist01.jpg", histImage);
waitKey(0);
destroyAllWindows();
}
}
void GrayLinearTransform(Mat input, Mat & output, double x1, double y1, double x2, double y2)
{
/*灰度拉伸。经过几次试验发现,车道线无论是白色还是黄色,总会是图像中的高亮区域,无论白天或者是有路灯照射的
晚上,所以可以把拉伸区域定在高灰度值领域。我们将通过两个点(x1, y1), (x2, y2),其中 255 > x2 > x1, 255 > y2 > y1
来确定我们的分段函数的新形式。
| y1/x1*old_gray_value, if old_gray_value <= x1
new_gray_value = | (y2 - y1)/(x2 - x1)*(old_gray_value - x1) + y1, if x1 < old_gray_value <= x2
| (255 - y2)/(255 - x2)*(old_gray_value - x2) + y2, if x2 < old_gray_value
*/
for (int col = 0; col < input.cols; ++col)
{
for (int row = 0; row < input.rows; ++row)
{
//ogv = old gray value, 即原图的灰度值
double ogv = input.at(col, row);
if (ogv < x1) {
output.at(col, row) = static_cast(y1 / x1 * ogv);
}
else if (x1 <= ogv && ogv < x2) {
output.at(col, row) = static_cast((y2 - y1) / (x2 - x1)*(ogv - x1) + y1);
}
else {
output.at(col, row) = static_cast((255 - y2) / (255 - x2)*(ogv - x2) + y2); }
}
}
}
将前后两张直方图进行比较如上。我设置的划分多段函数的两个点分别是(160, 120),(220, 240),但从图片上看也确实可以看出把图片的高亮区域给拉伸得更加清晰了,但是直方图上却出现了很多奇怪的波形。虽然说灰度直方图,并不是完全连续的,没有必须是如第一张图那么连续的道理,但是通常来说这种很严重的波动必然是人为干预引起的。
原因也很显然,因为在做灰度拉伸时,是对灰度值带入到函数中,计算的结果基本都不是一个整数,所以需要取整。例如我们设置的两个点是(160, 120),(220, 240),就意味着把0-160的数值压缩到0-120,简单的就是0-4压缩到0-3。[0, 1, 2, 3, 4] ---> [0, 0.75, 1.5, 2.25, 3],如果是简单的向下取整,那么结果是[0, 0, 1, 2, 3],也就是把1的所有值都加到0中去,所以原本连续的0-4就变得不连续了,变得有一个明显的突起。那如果是把小的范围拉伸到大的范围呢?160-220拉伸到120-240,也就是原本60的跨度变成了120,例如160 --> 120, 161 ---> 122,所以121这个位置就空了出来,也就出现了上图中有一段有一半的点为0的区域,正是对应了120-240中的奇数区域。
说不会有什么影响,这个似乎倒不至于。对于肉眼所看到的图像并没有什么特别的边缘或者干扰,兴许有人会说那不要截断用cvRound有没有用?并没有哈哈哈,因为实质上都是把某个灰度值的所有点都给调整到另一个灰度值,只要是这种"全部"的操作,就会对灰度直方图有如上的影响。不过这种图像结果,实际上更多是因为我画图,并不是真正的直方图,而是将一个一个的点连接起来的折线图。如果画成直方图的形式应该就好了。
/*
for (int i = 1; i < histsize; i++)
{
line(histImage, Point(bin_w *(i - 1), histImageHeight - cvRound(gray_hist.at(i - 1))),
Point(bin_w * i, histImageHeight - cvRound(gray_hist.at(i))), Scalar(255, 255, 255), 2);
}
*/
for (int i = 0; i < histsize; i++)
{
//绘制直方图
line(histImage, Point(bin_w * i, histImageHeight),
Point(bin_w * i, histImageHeight - cvRound(gray_hist.at(i))), Scalar(255, 255, 255), 2);
}