指针式仪表读数算法主要用于工业变电站环境,变电站环境复杂,仪表类型众多。仪表图像通过巡检机器人的可见光相机采集,由于机器人在巡检过程中行走路线较为固定,相机难以正对仪表平面采集图像,导致获得的仪表图像会因为拍摄位置、角度、光照等因素的影响产生较大的差异,图像可能存在仪表模糊和被遮挡等情况,这对于仪表图像的直接识别增加了难度。因此,为了提高仪表识别的准确度,识别前需要对图像进行仪表区域定位,排除背景干扰信息,提高后续表盘信息识别的准确性。
Hough变换是一种专门用于检测物体几何形状的图像处理算法,最开始用于检测图像中的直线,经过改进后可用于检测多种形状,例如圆形、椭圆和多边形等。指针式仪表轮廓为圆形或者方形,因此,可以使用Hough变换检测图像中的圆形或者方形形状,检测出的位置即为仪表所在的位置。
Hough变换圆检测的原理和二维空间直线检测原理类似,只不过直线检测检测中只有两个自由度,而圆的一般方程为
那么在图像空间有三个自由度,即圆心a, b和半径r,那么就需要更大的计算量。在圆检测过程中,可以先设定半径r的取值范围,相当于提供一个先验设定,然后在二维空间内寻找a和b,减少算法计算量。
标准的Hough变换圆检测方法是通过直接累加法来实现的,这种方法存在计算量大,耗时多等问题,在很多场合很难使用,因此提出了很多改进的Hough变换圆检测方法,例如利用梯度信息的圆检测方法在一定程度上提高了圆形物体的检测速度。
通过Hough变换直线检测和圆检测的原理和过程可以看出,要在图像中检测出目标的位置,首先需要提取图像边缘信息,然后再遍历所有像素点,并进行Hough变换,最后检测结果得到目标物体信息,这个过程增加了计算量,降低了目标检测的速度。同时,在变电站实际环境中,背景干扰信息众多,存在很多的圆形相似物体,并且因为机器人拍摄路径不易,采集的图像中的仪表大小不一,导致仪表的圆半径大小不一,并且当相机镜头不正对仪表的表盘时,采集的图像中的仪表发生尺度变换,圆形仪表可能在图像中变成不规则的椭圆形状,这些原因都会给Hough变换圆检测带来困难。基于Hough变换的仪表定位算法如下。
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
int main()
{
Mat Image = imread("../3.jpg");
Size dsize = Size(Image.cols * 0.5, Image.rows * 0.5);
Mat srcImage(dsize, Image.type());
resize(Image, srcImage, srcImage.size());
Mat midImage, dstImage;
cvtColor(srcImage, midImage, COLOR_BGR2GRAY); //转化边缘检测后的图为灰度图
GaussianBlur(midImage, midImage, Size(9, 9), 2, 2);
vector<Vec3f> circles;
HoughCircles(midImage, circles, HOUGH_GRADIENT, 1, midImage.rows / 5, 150, 100, 0, 0);
for (size_t i = 0; i < circles.size(); i++)
{
Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
int radius = cvRound(circles[i][2]);
//绘制圆心
circle(srcImage, center, 3, Scalar(0, 255, 0), -1, 8, 0);
//绘制圆轮廓
circle(srcImage, center, radius, Scalar(0, 255, 0), 3, 8, 0);
}
imwrite("../circles_detection.jpg", srcImage);
imshow("Output", srcImage);
waitKey(0);
return 0;
}
图像的模板匹配算法是一种基于图像特征检测、特征描述和特征匹配的算法。特征检测是指检测图像中满足用户定义的特征,在确定稳定的特征集后,提取特征的位置、方向、颜色、纹理和尺度等信息。特征描述是指使用一定的描述规则对特征信息进行量化分析并表征其特征信息。特征匹配是指对提取的特征信息进行筛选,将不满足条件的特征信息进行剔除,感兴趣的特征保留。特征检测中常用的方法有Harris角点检测,SIFT特征检测,SURF特征点检测,ORB特征点检测和SAFT特征点检测等,以SURF特征点检测方法为例,基于模板匹配的仪表检测方法如下:
/*
* @概述: 采用SURF算子在场景中进行仪表检测
* @类和函数: SurfFeatureDetector + SurfDescriptorExtractor + FlannBasedMatcher + findHomography + perspectiveTransform
* @实现步骤:
* Step 1: 在图像中使用SURF算法SurfFeatureDetector检测关键点
* Step 2: 对检测到的每一个关键点使用SurfDescriptorExtractor计算其特征向量(也称描述子)
* Step 3: 使用FlannBasedMatcher通过特征向量对关键点进行匹配,使用阈值剔除不好的匹配
* Step 4: 利用findHomography基于匹配的关键点找出相应的透视变换
* Step 5: 利用perspectiveTransform函数映射点群,在场景中获取目标的位置
*/
#include
#include
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/nonfree/features2d.hpp" //SurfFeatureDetector实际在该头文件中
#include "opencv2/features2d/features2d.hpp" //FlannBasedMatcher实际在该头文件中
#include "opencv2/calib3d/calib3d.hpp" //findHomography所需头文件
#include
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
Mat imgObject = imread("../1.jpg", CV_LOAD_IMAGE_GRAYSCALE);
Mat imgScene = imread("../192.168.1.34_01_2019032017182917.jpg", CV_LOAD_IMAGE_GRAYSCALE);
resize(imgObject, imgObject, Size(imgObject.rows*0.5, imgObject.cols*0.5)); //对图片进行修改
resize(imgScene, imgScene, Size(imgScene.rows, imgScene.cols*0.4)); //对图片进行修改
if (!imgObject.data || !imgScene.data)
{
cout << " --(!) Error reading images " << endl;
return -1;
}
double begin = clock();
// Step 1: 使用SURF算子检测特征点
int minHessian = 400;
SurfFeatureDetector detector(minHessian);
vector<KeyPoint> keypointsObject, keypointsScene;
detector.detect(imgObject, keypointsObject);
detector.detect(imgScene, keypointsScene);
cout << "object--number of keypoints: " << keypointsObject.size() << endl;
cout << "scene--number of keypoints: " << keypointsScene.size() << endl;
// Step 2: 使用SURF算子提取特征(计算特征向量)
SurfDescriptorExtractor extractor;
Mat descriptorsObject, descriptorsScene;
extractor.compute(imgObject, keypointsObject, descriptorsObject);
extractor.compute(imgScene, keypointsScene, descriptorsScene);
// Step 3: 使用FLANN法进行匹配
FlannBasedMatcher matcher;
vector< DMatch > allMatches;
matcher.match(descriptorsObject, descriptorsScene, allMatches);
cout << "number of matches before filtering: " << allMatches.size() << endl;
//-- 计算关键点间的最大最小距离
double maxDist = 0;
double minDist = 200;
for (int i = 0; i < descriptorsObject.rows; i++)
{
double dist = allMatches[i].distance;
if (dist < minDist)
minDist = dist;
if (dist > maxDist)
maxDist = dist;
}
printf(" max dist : %f \n", maxDist);
printf(" min dist : %f \n", minDist);
//-- 过滤匹配点,保留好的匹配点(这里采用的标准:distance<3*minDist)
vector< DMatch > goodMatches;
for (int i = 0; i < descriptorsObject.rows; i++)
{
if (allMatches[i].distance < 4 * minDist)
goodMatches.push_back(allMatches[i]);
}
cout << "number of matches after filtering: " << goodMatches.size() << endl;
//-- 显示匹配结果
Mat resultImg;
drawMatches(imgObject, keypointsObject, imgScene, keypointsScene,
goodMatches, resultImg, Scalar::all(-1), Scalar::all(-1), vector<char>(),
DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS //不显示未匹配的点
);
//-- 输出匹配点的对应关系
for (int i = 0; i < goodMatches.size(); i++)
printf(" good match %d: keypointsObject [%d] -- keypointsScene [%d]\n", i,
goodMatches[i].queryIdx, goodMatches[i].trainIdx);
///-- Step 4: 使用findHomography找出相应的透视变换
vector<Point2f> object;
vector<Point2f> scene;
for (size_t i = 0; i < goodMatches.size(); i++)
{
//-- 从好的匹配中获取关键点: 匹配关系是关键点间具有的一 一对应关系,可以从匹配关系获得关键点的索引
//-- e.g. 这里的goodMatches[i].queryIdx和goodMatches[i].trainIdx是匹配中一对关键点的索引
object.push_back(keypointsObject[goodMatches[i].queryIdx].pt);
scene.push_back(keypointsScene[goodMatches[i].trainIdx].pt);
}
Mat H = findHomography(object, scene, CV_RANSAC);
///-- Step 5: 使用perspectiveTransform映射点群,在场景中获取目标位置
std::vector<Point2f> objCorners(4);
objCorners[0] = cvPoint(0, 0);
objCorners[1] = cvPoint(imgObject.cols, 0);
objCorners[2] = cvPoint(imgObject.cols, imgObject.rows);
objCorners[3] = cvPoint(0, imgObject.rows);
std::vector<Point2f> sceneCorners(4);
perspectiveTransform(objCorners, sceneCorners, H);
//-- 在被检测到的目标四个角之间划线
line(resultImg, sceneCorners[0] + Point2f(imgObject.cols, 0), sceneCorners[1] + Point2f(imgObject.cols, 0), Scalar(0, 255, 0), 4);
line(resultImg, sceneCorners[1] + Point2f(imgObject.cols, 0), sceneCorners[2] + Point2f(imgObject.cols, 0), Scalar(0, 255, 0), 4);
line(resultImg, sceneCorners[2] + Point2f(imgObject.cols, 0), sceneCorners[3] + Point2f(imgObject.cols, 0), Scalar(0, 255, 0), 4);
line(resultImg, sceneCorners[3] + Point2f(imgObject.cols, 0), sceneCorners[0] + Point2f(imgObject.cols, 0), Scalar(0, 255, 0), 4);
//-- 显示检测结果
imshow("detection result", resultImg);
double end = clock();
cout << "\nSURF--elapsed time: " << (end - begin) / CLOCKS_PER_SEC * 1000 << " ms\n";
waitKey(0);
return 0;
}
检测结果如下
以上两种方法,在室内光线较好的环境下,如果采集到高清的仪表图像,通过以上方法可以得到准确的检测结果。但是,如果该算法应用到室外复杂环境,当采集仪表图像受雨雾、遮挡、光照等影响时,采集到模糊图像将会严重影响到仪表的检测精度,而基于深度学习的方法抗干扰性强,可以应对各种复杂环境。本文以YOLOv3目标检测算法为例,通过模型训练得到仪表检测模型。
基于yolov3训练的仪表识别模型及配置文件下载链接:基于YOLOv3的仪表检测模型及配置文件
#include
#include
#include
#include
#include
#include
#include
// Remove the bounding boxes with low confidence using non-maxima suppression
void postprocess(cv::Mat& frame, std::vector<cv::Mat>& outs);
// Get the names of the output layers
std::vector<cv::String> getOutputsNames(const cv::dnn::Net& net);
// Draw the predicted bounding box
void drawPred(int classId, float conf, int left, int top, int right, int bottom, cv::Mat& frame);
// Initialize the parameters
float confThreshold = 0.5; // Confidence threshold
float nmsThreshold = 0.4; // Non-maximum suppression threshold
int inpWidth = 416; // Width of network's input image
int inpHeight = 416; // Height of network's input image
std::vector<std::string> classes;
int main(int argc, char** argv)
{
// Load names of classes, configuration and weight files
std::string classesFile = "../meter.names";
cv::String modelConfiguration = "../yolov3.cfg";
cv::String modelWeights = "../yolov3.weights";
std::ifstream classNamesFile(classesFile.c_str());
if (classNamesFile.is_open())
{
std::string className = "";
while (std::getline(classNamesFile, className))
classes.push_back(className);
}
else{
std::cout<<"can not open classNamesFile"<<std::endl;
}
// Load the network
cv::dnn::Net net = cv::dnn::readNetFromDarknet(modelConfiguration, modelWeights);
std::cout<<"Read Darknet..."<<std::endl;
net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
// Process frames.
std::cout <<"Processing..."<<std::endl;
//cv::VideoCapture cap(0);
cv::Mat frame;
frame = imread("../test.jpg");
//show frame
cv::imshow("frame",frame);
// Create a 4D blob from a frame.
cv::Mat blob;
cv::dnn::blobFromImage(frame, blob, 1/255.0, cv::Size(inpWidth, inpHeight), cv::Scalar(0,0,0), true, false);
//Sets the input to the network
net.setInput(blob);
// Runs the forward pass to get output of the output layers
std::vector<cv::Mat> outs;
net.forward(outs, getOutputsNames(net));
// Remove the bounding boxes with low confidence
postprocess(frame, outs);
// Put efficiency information. The function getPerfProfile returns the
// overall time for inference(t) and the timings for each of the layers(in layersTimes)
std::vector<double> layersTimes;
double freq = cv::getTickFrequency() / 1000;
double t = net.getPerfProfile(layersTimes) / freq;
std::string label = cv::format("Inference time for a frame : %.2f ms", t);
cv::putText(frame, label, cv::Point(0, 15), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 255));
// Write the frame with the detection boxes
cv::Mat detectedFrame;
frame.convertTo(detectedFrame, CV_8U);
//show detectedFrame
cv::imshow("detectedFrame",detectedFrame);
waitKey(0);
return 0;
}
// Get the names of the output layers
std::vector<cv::String> getOutputsNames(const cv::dnn::Net& net)
{
static std::vector<cv::String> names;
if (names.empty())
{
//Get the indices of the output layers, i.e. the layers with unconnected outputs
std::vector<int> outLayers = net.getUnconnectedOutLayers();
//get the names of all the layers in the network
std::vector<cv::String> layersNames = net.getLayerNames();
// Get the names of the output layers in names
names.resize(outLayers.size());
for (size_t i = 0; i < outLayers.size(); ++i)
names[i] = layersNames[outLayers[i] - 1];
}
return names;
}
// Remove the bounding boxes with low confidence using non-maxima suppression
void postprocess(cv::Mat& frame, std::vector<cv::Mat>& outs)
{
std::vector<int> classIds;
std::vector<float> confidences;
std::vector<cv::Rect> boxes;
for (size_t i = 0; i < outs.size(); ++i)
{
// Scan through all the bounding boxes output from the network and keep only the
// ones with high confidence scores. Assign the box's class label as the class
// with the highest score for the box.
float* data = (float*)outs[i].data;
for (int j = 0; j < outs[i].rows; ++j, data += outs[i].cols)
{
cv::Mat scores = outs[i].row(j).colRange(5, outs[i].cols);
cv::Point classIdPoint;
double confidence;
// Get the value and location of the maximum score
cv::minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
if (confidence > confThreshold)
{
int centerX = (int)(data[0] * frame.cols);
int centerY = (int)(data[1] * frame.rows);
int width = (int)(data[2] * frame.cols);
int height = (int)(data[3] * frame.rows);
int left = centerX - width / 2;
int top = centerY - height / 2;
classIds.push_back(classIdPoint.x);
confidences.push_back((float)confidence);
boxes.push_back(cv::Rect(left, top, width, height));
}
}
}
// Perform non maximum suppression to eliminate redundant overlapping boxes with
// lower confidences
std::vector<int> indices;
cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);
for (size_t i = 0; i < indices.size(); ++i)
{
int idx = indices[i];
cv::Rect box = boxes[idx];
drawPred(classIds[idx], confidences[idx], box.x, box.y,
box.x + box.width, box.y + box.height, frame);
}
}
// Draw the predicted bounding box
void drawPred(int classId, float conf, int left, int top, int right, int bottom, cv::Mat& frame)
{
//Draw a rectangle displaying the bounding box
cv::rectangle(frame, cv::Point(left, top), cv::Point(right, bottom), cv::Scalar(0, 0, 255));
//Get the label for the class name and its confidence
std::string label = cv::format("%.2f", conf);
if (!classes.empty())
{
CV_Assert(classId < (int)classes.size());
label = classes[classId] + ":" + label;
}
else
{
std::cout<<"classes is empty..."<<std::endl;
}
//Display the label at the top of the bounding box
int baseLine;
cv::Size labelSize = cv::getTextSize(label, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
top = std::max(top, labelSize.height);
cv::putText(frame, label, cv::Point(left, top), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255,255,255));
}