这次分享的为一个很理想的情况下的目标识别与分类,对象为螺丝、螺帽、圆环这三个东西,其实就是图一乐呵,为什么说理想化呢?看一下本文使用的实验图片。
可以看到,图像中三个目标非常清楚,因为该图背景非常单一,这张图为在我床单上拍的。最近也还在补充图像处理相关的数学基础理论,要达到能在复杂背景下的目标检测与识别,需要学习的地方还很多。
下面根据该顺序具体介绍相关代码以及效果。
首先是去除噪声,本文使用高斯滤波去噪。然后是去除背景,在这一步骤由于图像是理性化环境下拍摄的,所以特别好处理,只需要用大核卷积模糊图像来而得到背景图,然后使用图片与背景图进行差分,就可以达到背景去除的目的。最后是阈值化,选取阈值将图像变为二值图像,便于图像分割。
//获得背景图像
cv::Mat calculateLightPattern(cv::Mat img)
{
cv::Mat pattern;
//通过使用相对于图像大小的大内核尺寸模糊得到背景图
blur(img, pattern, cv::Size(img.cols / 3, img.cols / 3));
return pattern;
}
//移除背景
cv::Mat removeLight(cv::Mat img, cv::Mat pattern)
{
cv::Mat result;
cv::Mat img32, pattern32;
img.convertTo(img32, CV_32F);
pattern.convertTo(pattern32, CV_32F);
//通过背景图像移除背景
result = 1 - (pattern32/img32);
result = result * 255;
result.convertTo(result, CV_8U);
/*result = img-pattern;*/
return result;
}
以上为预处理阶段需要的子函数,在预处理函数中将两个函数调用
//预处理
cv::Mat preprocessImage(cv::Mat input)
{
cv::Mat result;
//去噪
cv::Mat img_noise;
GaussianBlur(input, img_noise, cv::Size(5, 5), 0, 0);
//去除背景
cv::Mat img_no_light;
cv::Mat light_pattern = calculateLightPattern(img_noise);
img_no_light = removeLight(img_noise, light_pattern);
//阈值化处理
threshold(img_no_light, result, 30, 255, cv::THRESH_BINARY);
return result;
}
在目标检测函数中调用,结果如下所示(第一幅图为灰度图像,第二幅图为通过图像模糊得到的背景图,第三幅图为通过背景图将灰度图像背景之后的结果图。得到结果图后阈值化,得到第四幅图):
本文中采用连通组件算法进行图像分割。因为上述操作已经将图像转为二值图像,采用八个或者四个连接像素来标记图像。如果两个像素具有相同的值并且是邻居,则把他们连接起来。得到一个个对象就是识别出来的结果,下面是图像识别函数
int targetDectation(std::string path)
{
//读取图像
cv::Mat imageGray,imageRGB = cv::imread(path, cv::IMREAD_COLOR);
if (imageRGB.empty())
{
std::cout << "Fail to read image:" << path << std::endl;
return -1;
}
cv::cvtColor(imageRGB, imageGray, cv::COLOR_RGB2GRAY);
cv::imshow("ImageGray", imageGray);
cv::waitKey(0);
//去除噪声
GaussianBlur(imageGray, imageGray, cv::Size(5, 5), 0, 0);//使用opencv自带高斯滤波预处理
//获得背景
cv::Mat imgPattern = calculateLightPattern(imageGray);
//移除背景
imageGray = removeLight(imageGray, imgPattern);
cv::imshow("ImageGray", imageGray);
cv::waitKey(0);
//阈值化
cv::Mat imageThr;
cv::threshold(imageGray, imageThr, 50, 255, cv::THRESH_BINARY);
cv::imshow("thr", imageThr);
cv::waitKey(0);
//通过连通组件算法分割
cv::Mat labels, stats, centroids;
int num_objects = connectedComponentsWithStats(imageThr, labels, stats, centroids);
//检查检测出的数目数量
if (num_objects < 2){
std::cout << "No objects detected" << std::endl;
return -1;
}
else{
std::cout << "Number of objects detected: " << num_objects - 1 << std::endl;
}
//展示图像分割结果
cv::Mat output = cv::Mat::zeros(imageThr.rows, imageThr.cols, CV_8UC3);
cv::RNG randomNumGenerator(0xFFFFFFFF);
for (int i = 1; i<num_objects; i++)
{
cv::Mat mask = labels == i;
output.setTo(randomColor(randomNumGenerator), mask);
//给目标赋上颜色以及面积标签
std::stringstream ss;
ss << "area: " << stats.at<int>(i, cv::CC_STAT_AREA);
putText(output, ss.str(), centroids.at<cv::Point2d>(i),
cv::FONT_HERSHEY_SIMPLEX,
0.4,
cv::Scalar(255, 255, 255));
}
imshow("Result", output);
cv::waitKey(0);
return 0;
}
以上两步检测出图像中的独立目标,而要想识别出目标是什么类型的物品,本文采用机器学习的方法对其进行分类识别。
在进行机器学习之前首先要确定要识别的类型物品的特征。本文要识别的物品为螺丝、螺母、圆环,通过观察,我选择提取这三个得目标为纵横比以及面积,代码如下:
//提取特征向量
std::vector< std::vector<float> > extractFeatures(cv::Mat img,
std::vector<int>* left = NULL, std::vector<int>* top = NULL)
{
std::vector< std::vector<float> > output;
std::vector<std::vector<cv::Point> > contours;
std::vector<cv::Vec4i> hierarchy;
//findContours会改变输入图像先备份
cv::Mat input = img.clone();
//检测对象
findContours(input, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
//检测结果检验
if (contours.size() == 0){
return output;
}
cv::RNG rng(0xFFFFFFFF);
for (int i = 0; i<contours.size(); i++)
{
cv::Mat mask = cv::Mat::zeros(img.rows, img.cols, CV_8UC1);
//使用值1绘制形状,可以求和统计面积
drawContours(mask, contours, i, cv::Scalar(1), cv::FILLED, cv::LINE_8, hierarchy, 1);
cv::Scalar area_s = sum(mask);
float area = area_s[0];//得到第一个特征面积
//如果面积大于最小阈值
float MIN_AREA = 500;
if (area>MIN_AREA)
{
cv::RotatedRect r = minAreaRect(contours[i]);
float width = r.size.width;
float height = r.size.height;
//第二个特征纵横比
float ar = (width<height) ? height / width : width / height;
std::vector<float> row;
row.push_back(area);
row.push_back(ar);
output.push_back(row);
if (left != NULL){
left->push_back((int)r.center.x);
}
if (top != NULL){
top->push_back((int)r.center.y);
}
}
}
return output;
}
以上代码为获得每个目标的特征提取。而要训练模型也需要读取存放训练图像的文件夹中的训练集来提取特征,代码如下:
bool readFolderAndExtractFeatures(std::string folder, int label, int num_for_test,
std::vector<float> &trainingData, std::vector<int> &responsesData,
std::vector<float> &testData, std::vector<float> &testResponsesData)
{
std::vector<cv::String> image_files;
cv::glob(folder, image_files);
int img_index = 0;
for (int i = 0; i<image_files.size(); i++)
{
cv::Mat frame = imread(image_files[i],cv::IMREAD_COLOR);
if (frame.empty())
{
std::cout << "未能读取图像" << std::endl;
return false;
}
cv::cvtColor(frame, frame, cv::COLOR_RGB2GRAY);
//预处理数据
cv::Mat pre = preprocessImage(frame);
//提取特征
std::vector<int> pos_top, pos_left;
std::vector< std::vector<float> > features = extractFeatures(pre);
for (int i = 0; i< features.size(); i++)
{
if (img_index >= num_for_test)
{
trainingData.push_back(features[i][0]);
trainingData.push_back(features[i][1]);
responsesData.push_back(label);
}
else
{
testData.push_back(features[i][0]);
testData.push_back(features[i][1]);
testResponsesData.push_back((float)label);
}
}
img_index++;
}
return true;
}
有了特征集之后,就要训练模型,并通过训练得出得的模型对图片进行预测。
//绘制样本空间图
void plotTrainData(cv::Mat trainData, cv::Mat labels, float *error = NULL)
{
float area_max, ar_max, area_min, ar_min;
area_max = ar_max = 0;
area_min = ar_min = 99999999;
//获取最大最小值标准化数轴
for (int i = 0; i< trainData.rows; i++){
float area = trainData.at<float>(i, 0);
float ar = trainData.at<float>(i, 1);
if (area > area_max)
area_max = area;
if (ar > ar_max)
ar_max = ar;
if (area < area_min)
area_min = area;
if (ar < ar_min)
ar_min = ar;
}
//创建绘制图片的最后结果
cv::Scalar green(0, 255, 0), blue(255, 0, 0), red(0, 0, 255);
cv::Mat plot = cv::Mat::zeros(512, 512, CV_8UC3);
for (int i = 0; i< trainData.rows; i++){
float area = trainData.at<float>(i, 0);
float ar = trainData.at<float>(i, 1);
int x = (int)(512.0f*((area - area_min) / (area_max - area_min)));
int y = (int)(512.0f*((ar - ar_min) / (ar_max - ar_min)));
// Get label
int label = labels.at<int>(i);
// Set color depend of label
cv::Scalar color;
if (label == 0)
color = green; // 螺母
else if (label == 1)
color = blue; // 圆环
else if (label == 2)
color = red; // 螺丝
circle(plot, cv::Point(x, y), 3, color, -1, 8);
}
if (error != NULL){
std::stringstream ss;
ss << "Error: " << *error << "\%";
putText(plot, ss.str().c_str(), cv::Point(20, 512 - 40), cv::FONT_HERSHEY_SIMPLEX, 0.75, cv::Scalar(200, 200, 200), 1, cv::LINE_AA);
}
cv::imshow("Plot", plot);
cv::waitKey(0);
}
以上为对训练模型的结果进行检验,并且将样本的特征空间图画出,因为只有两个特征,可以绘制平面图表示。
//创建模型
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
//训练模型并测试
int trainAndTest()
{
//创建存储训练和测试数据
std::vector< float > trainingData;
std::vector< int > responsesData;
std::vector< float > testData;
std::vector< float > testResponsesData;
//获取训练集特诊
int num_for_test = 20;//测试集数目
readFolderAndExtractFeatures("./nut/*.pgm", 0, num_for_test, trainingData, responsesData, testData, testResponsesData);
readFolderAndExtractFeatures("./ring/*.pgm", 1, num_for_test, trainingData, responsesData, testData, testResponsesData);
readFolderAndExtractFeatures("./screw/*.pgm", 2, num_for_test, trainingData, responsesData, testData, testResponsesData);
std::cout << "Num of train samples: " << responsesData.size() << std::endl;
std::cout << "Num of test samples: " << testResponsesData.size() << std::endl;
//合并所有数据
cv::Mat trainingDataMat(trainingData.size() / 2, 2, CV_32FC1, &trainingData[0]);
cv::Mat responses(responsesData.size(), 1, CV_32SC1, &responsesData[0]);
cv::Mat testDataMat(testData.size() / 2, 2, CV_32FC1, &testData[0]);
cv::Mat testResponses(testResponsesData.size(), 1, CV_32FC1, &testResponsesData[0]);
//设置参数
svm->setKernel(cv::ml::SVM::KernelTypes::CHI2);
svm->setType(cv::ml::SVM::Types::C_SVC);
svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER + cv::TermCriteria::EPS, 1000, FLT_EPSILON));
//训练模型
svm->train(trainingDataMat, cv::ml::ROW_SAMPLE, responses);
if (testResponsesData.size()>0)
{
//测试模型
cv::Mat testPredict;
svm->predict(testDataMat, testPredict);
cv::Mat errorMat = testPredict != testResponses;
float error = 100.0f * countNonZero(errorMat) / testResponsesData.size();
std::cout << "Error: " << error << "\%" << std::endl;
//绘制样本空间
plotTrainData(trainingDataMat, responses, &error);
}
else
{
plotTrainData(trainingDataMat, responses);
}
return 0;
}
//输入图像预测
int recognitationAndClassify(std::string path)
{
//读取图像
cv::Mat imageGray, imageRGB = cv::imread(path, cv::IMREAD_COLOR);
if (imageRGB.empty())
{
std::cout << "Fail to read image:" << path << std::endl;
return -1;
}
cv::cvtColor(imageRGB, imageGray, cv::COLOR_RGB2GRAY);
cv::imshow("ImageGray", imageGray);
cv::waitKey(0);
cv::Mat img_output = imageRGB.clone();
//预处理
//去除噪声
GaussianBlur(imageGray, imageGray, cv::Size(5, 5), 0, 0);//使用opencv自带高斯滤波预处理
//获得背景
cv::Mat imgPattern = calculateLightPattern(imageGray);
//移除背景
imageGray = removeLight(imageGray, imgPattern);
//阈值化
cv::Mat pre;
cv::threshold(imageGray, pre, 50, 255, cv::THRESH_BINARY);
cv::imshow("Binary image", pre);
cv::waitKey(0);
//提取特征
std::vector<int> pos_top, pos_left;
std::vector< std::vector<float> > features = extractFeatures(pre, &pos_left, &pos_top);
//训练模型
trainAndTest();
cv::Scalar green(0, 255, 0), blue(255, 0, 0), red(0, 0, 255);
for (int i = 0; i< features.size(); i++)
{
cv::Mat trainingDataMat(1, 2, CV_32FC1, &features[i][0]);
float result = svm->predict(trainingDataMat);
std::stringstream ss;
cv::Scalar color;
if (result == 0)
{
color = green;
ss << "NUT";
}
else if (result == 1){
color = blue;
ss << "RING";
}
else if (result == 2){
color = red;
ss << "SCREW";
}
putText(img_output,
ss.str(),
cv::Point2d(pos_left[i], pos_top[i]),
cv::FONT_HERSHEY_SIMPLEX,
0.4,
color);
}
cv::imshow("Result", img_output);
cv::waitKey(0);
}