文章目录
- 实现思路
- 配置opencv
- 位置剪裁
- 实现代码
- 自适应中值滤波
- 实现代码
- 动态范围增强
- 实现代码
- 形态学处理
- 实现代码
- 图片预处理效果
- 计算帧差
- 连续帧帧差法原理和实现代码
- 实现代码
- K近邻实现
- 基本介绍
- 实现代码
- 这部分是手动实现的,并没有直接调用相关的库
- 完整的代码——调用opencv的特定的库实现的
- 实现过程
- 参考
实现思路
- 使用C++进行实现,开发平台是clion,并没有使用深度学习,使用opencv进行开发
配置opencv
-
一开始就出来幺蛾子,之前装好的opencv怎么都找不到包,弄了一个小时,夹着红框后面的三句,告诉clion我安装的opencv包的具体位置,可能是因为我之前没有编译成功,直接间别人编译好的直接下载下来的,不过我为什么不用python上面的opencv那。
位置剪裁
- 相机位置固定,拍摄的位置固定,背景干扰很少,只需要保存红框右半部分的内容即可,左半部分去除。
实现代码
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/core/core.hpp>
#include <iostream>
#include <fstream>
using namespace std;
using namespace cv;
struct RectPoint{
int x,y;
int width,height;
RectPoint(int x,int y,int width,int height):x(x),y(y),width(width),height(height){}
};
void ClipImage(String ImagePath,RectPoint rect,String TargetPath){
// 声明Mat保存图片
Mat img;
Rect m_select;
string line;
// 判定文件存在
ifstream input_file(ImagePath);
if (!input_file.is_open()) {
cerr << "Could not open the file - '"
<< ImagePath << "'" << endl;
return;
}
while(getline(input_file,line)){
// 获取文件后缀名,并进行拼接成目标文件名
string ImageName = line.substr(line.find_last_of('\\',line.size())+1);
string TargetName = TargetPath;
TargetName.append(ImageName);
// 读取并剪裁图片到特定大小保存
img = imread(line);
m_select = Rect(rect.x,rect.y,rect.width,rect.height);
Mat ROI = img(m_select);
imwrite(TargetName,ROI);
}
}
int main()
{
String TargetImage = R"(E:\CProject\impurityDetection\ClippedImage\)";
String SourceImage = R"(E:\CProject\impurityDetection\image\path.txt)";
ClipImage(SourceImage,RectPoint(600,0,1780,1480),TargetImage);
return 0;
}
自适应中值滤波
- 自适应中值滤波,是为了去除照相机的噪声同时,又不损坏原来的杂质信息,这里使用自适应中值滤波,具体实现如下。
实现代码
void Convolution(Mat &SourceImage,vector<int> &pixels,int x,int y ,int border){
for (int k = -border; k <= border; k++)
{
for (int l = -border; l <= border; l++)
{
pixels.push_back(SourceImage.at<uchar>(x+k, y+l));
}
}
}
Mat SelfAdaptMedianFilter(const Mat& SourceImage){
// 复制原图
Mat result = SourceImage.clone();
// 掩膜大小为3
int ksize = 3;
int border = ksize /2 ;
for (int i = border; i < SourceImage.rows - border; i++)
{
for (int j = border; j < SourceImage.cols - border; j++)
{
while(1){
// 提取当前像素的邻域
vector<int> pixels;
Convolution(result, pixels, i, j, border);
// 对邻域像素值进行排序
sort(pixels.begin(), pixels.end());
// 取中值作为当前像素的新值
int median = pixels[pixels.size() / 2];
int PMax = pixels[pixels.size() - 1];
int PMin = pixels[0];
//判定是否为异常值
if (median == PMax || median == PMin) {
// 掩膜再加2,重新计算
ksize += 2;
if (ksize + 2 > Nmax) {
result.at<uchar>(i, j) = median;
break;
}else{
// 判定传入的值,是否出现越界的情况
if((i - ksize / 2 < 0 || i + ksize / 2 < SourceImage.cols) ||
j - ksize / 2 < 0 || j + ksize / 2 > SourceImage.rows)
result.at<uchar>(i,j) = (PMax + PMin) / 2;
}
} else {
//中值不是极值,判定原像素点是不是极值,然后在进行输出
if (result.at<uchar>(i, j) == PMax || result.at<uchar>(i, j) == PMin)
result.at<uchar>(i,j) = median;
break;
}
}
}
}
return result;
}
动态范围增强
- 这里就是图像的直方图均衡化,使得整个模型的图片颜色对比度更大,显示出更多的细节。
实现代码
//对图片进行直方图均衡化,凸显出前后差异
Mat equalized;
// 转成二值化,并变为直方图均衡化
cvtColor(ROI, equalized, COLOR_BGR2GRAY);
equalizeHist(equalized, equalized);
形态学处理
- 膨胀操作 :通过增加图像中的物体的像素数量,使得物体的大小和面积增加,边缘变得更加明显,改变了原来字体粗细
- 腐蚀操作 :在卷积核大小中对图片进行卷积。取图像中(3 * 3)区域内的最小值。可以消除图像中的毛刺和噪声。改变了原来字体粗细
- 开运算 :先腐蚀运算,在进行膨胀运算。在不损害字体信息的情况下,去除了噪声和毛刺。通过调整卷积可以进一步减少噪声
- 闭运算 :先膨胀运算,在进行腐蚀运算。在不去除任何噪声的情况下,补全了缺失的信息。
- 这里选择开运算,具体实现代码如下,别的可以参考知乎链接
实现代码
// 对图像进行开运算,
Mat morphologied;
int size = 3;
// shape是内核的形状,size是内核的尺寸,锚点的位置,对于矩形来说,全部都是 1 ,不用调整
cv::Mat element = cv::getStructuringElement(cv::MORPH_RECT,
cv::Size(2*size + 1, 2*size+1),
cv::Point(size, size));
cout<<element<<endl;
morphologyEx(equalized, morphologied, cv::MORPH_OPEN, element);
图片预处理效果
计算帧差
- 主要有两种方法,分别是静态参考帧差分法和连续帧帧差法,下图为两个效果,很明显的可以就看到连续帧帧差法的效果更好,常见的静态帧是将多个帧进行平均,然后以平均之后的帧作为参考帧,然后后面每帧都是和当前帧作比较,没有动作的连续性。
连续帧帧差法原理和实现代码
-
实现原理如下图
-
使用absdiff函数,具体描述如下,计算frame1和frame2的帧差,然后将结果保存到framediff中
cv::Mat frameDiff;
cv::absdiff(frame1, frame2, frameDiff);
实现代码
//计算帧差,并将最终的结果进行返回
Mat DifFrame(Mat PreFrame,Mat CurFrame,Mat NextFrame){
//计算帧差
Mat FrameDiffPre , FrameDiffNext;
absdiff(PreFrame,CurFrame,FrameDiffPre);
absdiff(CurFrame,NextFrame,FrameDiffNext);
// 分别进行二值化,设定二值化的阈值
Mat BinImgPre , BinImgNext;
threshold(FrameDiffPre,BinImgPre,128,255,cv::THRESH_BINARY);
threshold(FrameDiffNext,BinImgPre,128,255,cv::THRESH_BINARY);
// 将图片进行与运算,然后将结果进行输出
Mat ResFrame;
bitwise_and(FrameDiffPre,FrameDiffNext,ResFrame);
//返回最终处理过后的帧
return ResFrame;
}
// 计算帧差的外部函数,读取原来处理过的文件,计算所有图片的帧差
void AllDifFrame(const string& ImagePath,const string& TargetPath){
// 保存文件名
string line;
// 判定文件存在
ifstream input_file(ImagePath);
if (!input_file.is_open()) {
cerr << "Could not open the file - '"
<< ImagePath << "'" << endl;
return;
}
// 需要提前读取两个,然后依次往后进行迭代
Mat PreFrame,CurFrame,NextFrame;
getline(input_file,line);
PreFrame = imread(line);
// 读取下一个需要处理的帧
getline(input_file,line);
CurFrame = imread(line);
// 逐行读取文件名
while(getline(input_file,line)){
// 当前需要处理的帧
NextFrame = imread(line);
// 获取文件后缀名,并进行拼接成目标文件名
string ImageName = line.substr(line.find_last_of('\\',line.size())+1);
string TargetName = TargetPath;
TargetName.append(ImageName);
// 三帧相邻出进行帧差运算,算出的帧差进行按位相与运算
Mat TempFrame;
absdiff(PreFrame,CurFrame,PreFrame);
absdiff(CurFrame,NextFrame,TempFrame);
bitwise_and(PreFrame,TempFrame,TempFrame);
//计算计算过进行保存
imwrite(TargetName,TempFrame);
// 三帧往前进行迭代
PreFrame = CurFrame;
CurFrame = NextFrame;
}
}
K近邻实现
基本介绍
-
思路 : 将样本在特征空间中,根据一定的分类方法,归类于与该样本最相似的K个样本中大多数的那一类。需要一部分已经标注过的样本,然后将未标注过的样本进行分类。
-
特点 :
- 对于异常目标的容忍度比较高,因为不依靠参数直接分类,而是寻找多个近邻目标的联合分类
- 支持多分类,对于类域交叉重叠依然有较好的分类能力,因为白酒中的杂质在特征上有很强的相似性,在类域上会有很强的重叠性
- 训练过程复杂度低:实时更新样本数据集,再识别一种杂质之后,将该样本增加到样本数据中,使得分类效果更加稳定。
-
实现过程 :
- 准备数据集:需要将要分类的目标的特征向量与一些已知分类的目标的特征向量组成一个数据集。
- 特征提取:使用OpenCV库提取目标的特征向量。
- 注意 这里提取的特征是完整的一幅图片,因为我这里已经对图片进行了预处理,让特征很明显,所以不需要进行特征提取
- 训练模型:使用KNN算法对数据集进行训练,生成分类器。
- 分类:使用训练好的分类器对新的目标进行分类。
-
补充
- 针对图片进行的KNN分类,是一种高维数据,KNN算法可以使用降维技术,来提高效率和准确率,比如说主成分分析(PCA)
- KNN,适用于小数据集和低维度数据集
- 可以通过对距离加权的方式,来提升预测的准确率
实现代码
这部分是手动实现的,并没有直接调用相关的库
// 定义一个结构体,用于保存数据点和标签
struct DataPoint {
vector<double> features; // 特征数据
int label; // 标签
};
// 计算欧氏距离
double euclideanDistance(const vector<double>& v1, const vector<double>& v2) {
double dist = 0.0;
for (int i = 0; i < v1.size(); i++) {
dist += pow((v1[i] - v2[i]), 2);
}
return sqrt(dist);
}
// 预测函数,返回预测标签
int predict(const vector<DataPoint>& trainData, const vector<double>& testData, const int k) {
// 对于测试集,计算每一个测试样本和数据集中的,所有样本的欧式距离
vector<pair<int, double>> distances; // 存储距离和标签
for (const auto & i : trainData) {
double distance = euclideanDistance(i.features, testData);
distances.emplace_back(i.label, distance);
}
// 根据结果进行排序,指定排序函数,仅仅对距离进行比较
sort(distances.begin(), distances.end(), [](pair<int, double> a, pair<int, double> b) {
return a.second < b.second;
});
// 获取最后k个样本最高的分类类别
vector<int> kLabels(k);
for (int i = 0; i < k; i++) {
kLabels.push_back(distances[i].first);
}
// 获取数量类别最大的分类
int maxLabel = 0;
int maxCount = 0;
for (auto iter = kLabels.begin();iter != kLabels.end();iter ++){
int times = count(kLabels.begin(),kLabels.end(),*(iter));
if(times > maxCount){
maxCount = times;
maxLabel = *(iter);
}
}
return maxLabel;
}
完整的代码——调用opencv的特定的库实现的
#include<opencv2/opencv.hpp>
#include<iostream>
#include <fstream>
using namespace std;
using namespace cv;
using namespace cv::ml;
// 读取图片并形成数据集
void KNNTraning(const string& ImagePath, Mat& Images, Mat& Labels) {
/*
* 将图片转成一维数据,并保存在MAT中
* path: 读取文件需要使用的路径
* images:保存所有图片的数据集
* labels:对应每一张图片的标签
*/
// 声明Mat保存图片
Mat img;
string line;
string ImgLabel;
int flag = 0;
// 判定文件存在
ifstream input_file(ImagePath);
if (!input_file.is_open()) {
cerr << "Could not open the file - '"
<< ImagePath << "'" << endl;
return;
}
while(getline(input_file,line)) {
// 获取文件后缀名,并进行拼接成目标文件名
string ImageName = line.substr(line.find_last_of('\\',line.size())+1);
if(ImageName != ImgLabel)
flag ++;
// 将图片转成一维的数据,并合并到一个大的mat中
img = imread(line, cv::IMREAD_GRAYSCALE);
Images.push_back(img.reshape(0,1));
Labels.push_back(flag);
}
// 将Mat转变成数据集
Images.convertTo(Images,CV_32F);
Images = Images / 255;
int nImgCount = Images.rows;
int nTrainCount = nImgCount / 4 * 3;
Mat trainData,trainLabels;
trainData = Images(Range(0,nTrainCount),Range::all());
trainLabels = Labels(Range(0,nTrainCount),Range::all());
Ptr<TrainData> tData = TrainData::create(trainData, ROW_SAMPLE, trainLabels);
// 训练KNN模型
Ptr<KNearest> model = KNearest::create();
// 设定k值,最终选取是哪个类别
model->setDefaultK(5);
// 设置分类还是回归,true为分类,false为回归
model->setIsClassifier(true);
// 设置训练数据,进行训练。就相当于将前面的标注数据和测试数据进行了一个训练绑定
model->train(tData);
model->save("model.xml");
}
float predict(string ImgPath,string modelPath){
// 加载模型
cv::Ptr<cv::ml::KNearest> knn = cv::ml::StatModel::load<cv::ml::KNearest>(modelPath);
//读取图片
Mat imgTest = imread(ImgPath);
imgTest.convertTo(imgTest,CV_32F);
imgTest = imgTest / 255;
imgTest = imgTest.reshape(0,1);
//输出预测结果
float ret = knn->predict(imgTest);
return ret;
}
实现过程
参考
- Opencv 图像处理之膨胀与腐蚀 【https://zhuanlan.zhihu.com/p/110330329】