线缆颜色顺序检测
问题描述
从下图中得到线缆的颜色顺序
问题分析
- 光照条件变化时,同样颜色的线缆在图像中颜色深度会有变化,所以不能使用RGB颜色空间,应该使用符合人类颜色距离认知的颜色空间,比如HSV和LAB。
- 所有的线缆都是并排的一起的,所以识别时可以只识别它们的边界线,任意相邻边界线的中间区域就是线缆。
- 线缆只在竖直方向上摆放,为提高算法速度,可以只在高度方向上截取一段进行处理。
算法流程
图片预处理
图片读取(从文件或相机)
Mat src = imread("D:/1.jpg", IMREAD_COLOR);
//从src中截取一个区域,等比例缩放
int cor = src.cols * 0.2;
float ratio = src.rows / src.cols;
Mat srctemp(src, Rect(cor, (int)(cor*ratio), src.cols - 2 * cor, src.rows - 2 * (int)(cor*ratio)));
Size dsize = Size(src.cols, src.rows);
resize(srctemp, src_all, dsize);
对图片中间区域截取一段
int position = 550;
src_all.convertTo(bgr, CV_32FC3, 1.0 / 255, 0);
Mat roi(bgr, Rect(0, position, 1920, 20));
imwrite("roi.jpg", roi);
颜色空间转换
cvtColor(roi, hsv, CV_BGR2HSV);
边界识别
首先在处理过的图片长度方向上得到其hsv颜色空间的平均值
float hh[1920] = { 0 };
float ss[1920] = { 0 };
float vv[1920] = { 0 };
float var[1920] = { 0 };
for (int i = 0; i < hsv.size().width; i++)
{
float h, s, v;
h = 0;
s = 0;
v = 0;
for (int j = 0; j < 2; j++) //hsv.size().height
{
Vec3f vec3f = hsv.at<Vec3f>(j, i);
h += vec3f[0];
s += vec3f[1];
v += vec3f[2];
}
hh[i] = h;
ss[i] = s * 255;
vv[i] = v * 255;
}
然后对这3个1920的数组绘制折线图
从图中可以分析出,任意两种颜色变化的边界处,其H,S,V值都在突变,突变在数学上的表征但是导数。然后这H,S,V三条线的导数相乘,得到的函数极大值点就是线缆的分界线。
分析虽简单,但在实现时要考虑几个问题:
- 数组是离散的,怎么求导
- 在跳变区域,其中一个导数值是0,相乘结果为0,不是极大值。
- 极值点附近函数值和极值相等,极值点横坐标怎么获取
如上分析的简单算法实现
float x[1920] = { 0 };
float y[1920] = { 0 };
float z[1920] = { 0 };
float xx[1920] = { 0 };
float yy[1920] = { 0 };
float zz[1920] = { 0 };
float sum[1920] = { 0 };
for (int i = 3; i < 1917; i++)
{
x[i] = abs(hh[i + 3] - hh[i - 3]);
xx[i] = (hh[i + 3] + hh[i + 2] + hh[i + 1] + hh[i - 3] + hh[i - 2] + hh[i - 1] - 6 * hh[i]);
y[i] = (ss[i + 3] + ss[i + 2] + ss[i + 1] + ss[i - 3] + ss[i - 2] + ss[i - 1] - 6 * ss[i]);
if (ss[i] < 50) y[i] = 0;
if (vv[i] < 100) y[i] = 1;
yy[i] = abs(ss[i + 3] - ss[i - 3]);
if (ss[i] < 50) yy[i] = 0;
z[i] = (vv[i + 3] + vv[i + 2] + vv[i + 1] + vv[i - 3] + vv[i - 2] + vv[i - 1] - 6 * vv[i]);
zz[i] = abs(vv[i + 3] - vv[i - 3]);
sum[i] = abs(x[i] * xx[i] * y[i] * yy[i] * z[i] * zz[i]);
if (sum[i] > 400000000)
sum[i] = 400000000;
sum[i] /= 1000000000;
sum[i] *= 25;
if (sum[i] < 10) sum[i] = 0;
}
int count = 0;
int tempcount = 0;
int left[50] = { 0 };
int right[50] = { 0 };
for (int i = 1; i < 1920; i++)
{
if (sum[i] - sum[i - 1] < 0 && sum[i] == 0)
{
tempcount = count;
left[tempcount] = i;
}
if (sum[i] - sum[i - 1] > 0 && sum[i - 1] == 0)
{
right[tempcount] = i;
count++;
}
}
算法处理完成后结果
可以看到除了白色和灰色不能分离外,其他都已经分离。
颜色识别
由于受光照强度和色温变化的影响,颜色识别要用到标定,然后得到颜色和对应标定H,S,V值,在使用时直接找欧式距离最短的标定颜色。
//使用欧式距离的方法判断颜色
struct colorspace
{
string colorname;
float h;
float s;
float v;
};
//需要事先标定
colorspace cs[11] = {
//颜色 h s v
{ "黑色", 236, 0, 0 },
{ "白色", 236, 0, 1 },
{ "灰色", 236, 0, 0.5 },
{ "褐色", 10,0.47,0.30 },
{ "橘色", 19,0.73,0.68 },
{ "黄色", 56,0.69,0.65 },
{ "绿色", 160,0.95,0.34 },
{ "蓝色", 226,0.98,0.45 },
{ "紫色", 285,0.50,0.31 },
{ "红色", 340,0.83,0.66 },
{ "背景", 105,0.05,0.54} };
//对框选区域进行颜色识别
for (int c = 0; c < rightborder.size(); c++)
{
float h, s, v;
h = 0;
s = 0;
v = 0;
for (int i = leftborder.at(c); i < rightborder.at(c); i++)
{
for (int j = 0; j < 20; j++) //hsv.size().height
{
Vec3f vec3f = hsv.at<Vec3f>(j, i);
h += vec3f[0];
s += vec3f[1];
v += vec3f[2];
}
}
h = h / (20 * (rightborder.at(c) - leftborder.at(c)));
s = s / (20 * (rightborder.at(c) - leftborder.at(c)));
v = v / (20 * (rightborder.at(c) - leftborder.at(c)));
cout << h << " " << s << " " << v << endl;
int minindex = 0;
int mindistance = 400000;
for (int i = 0; i < 11; i++) //背景不参与运算
{
int distance = (h - cs[i].h)*(h - cs[i].h) +
(s - cs[i].s)*(s - cs[i].s) * 129600 +
(v - cs[i].v)*(v - cs[i].v) * 129600;
if (distance < mindistance)
{
minindex = i;
mindistance = distance;
}
}
cout << cs[minindex].colorname << endl;
}
最终识别结果
优化方向
- 最近发现使用CIELAB颜色空间比使用HSV颜色空间效果更好,可以把使用HSV方法不能区分的白色和灰色区分出来。
- 线缆的分区其实是有空间约束的,如果能加到算法中,会对识别效率和准确率有一定的提高。
- 颜色的识别与光源的色温和光强关系很大,但是没有一个光源条件对所以的颜色区分度都能达到最好。比如10lx,4000k对蓝绿色区分最好,20lx,6500k对黄绿色区分最好。所以还需要在实际使用时根据识别要求选择合适的光源。