在写工程时,经常会用到一些功能单一的函数。将其进行良好的封装,才便于下次使用。
设计思路
类对象的功能设计为:
- 在一张特定的图像中检测出若干条直线
- 每条直线有唯一id,方便外部储存和查找
- 支持通过某点查询到直线
- 可保存检测结果,便于二次免检测重用
- 检测参数可配置
- 使用opencv为基础库
代码细节
配置参数
- nModeValue设置输出的图像类型
- nCannyLowThreshold、nSobelKernelSize、hlThreshold、hlMinLineLength以及hlMaxLineGap,都是对应算法中的参数,相关意义参考其算法解释。
class ImgToCADConfig
{
public:
ImgToCADConfig() :
nModeValue(1), nCannyLowThreshold(50), nSobelKernelSize(1),
hlThreshold(25), hlMinLineLength(3), hlMaxLineGap(3),
maxSelectPixelDist(10)
{}
//bool load_config(const char* fnConfig);
//bool save_config(const char* fnConfig);
int nModeValue; //0为原图像,1为canny,2为sobel,3位Scharr,4为Laplacian,5为Laplacian增强对比度
int nCannyLowThreshold; //canny边缘检测低阈值
int nSobelKernelSize; //sobel算子内核大小
int hlThreshold; // 霍夫检测的阈值
int hlMinLineLength; // 线段以像素为单位的最小长度,根据应用场景设置
int hlMaxLineGap; // 同一方向上两条线段判定为一条线段的最大允许间隔(断裂),超过了设定值,则把两条线段当成一条线段,值越大,允许线段上的断裂越大,越有可能检出潜在的直线段
int maxSelectPixelDist; // 通过定点选择直线时的容错误差
};
自定义直线类型(如果要在此基础上做更广阔的开发,应当独立结构)
使用opencv中的cv::Vec4i 结构,保存线段。
struct LineWrapper
{
int id; // 线段id
float dist; // 临时使用的变量,其实不该在这里的
cv::Vec4i ln; // 线段两端点
cv::Point2f getVerticalPoint(int x, int y) const; // 求(x,y)到在线段上的垂点
LineWrapper() : id(-1), dist(0.0), ln() {}
};
检测结构
只希望提供简洁易用的功能。
class DLL_API ImgToCAD
{
// 几个友元函数,可以在外部进行自定义实现
friend void on_ModeType(int, void *);
friend void on_CannyType(int, void *);
friend void on_SobelType(int, void *);
friend void on_FindLine(int, void *);
friend void on_Mouse(int event, int x, int y, int, void*);
public:
ImgToCAD();
ImgToCAD(const ImgToCADConfig& _cfg);
// 对指定路径的图像进行处理,类中只保存最后一次的处理结果
bool process_image(const char* fnImg);
// 加载处理文件
bool load_edge_file(const char* fnEdgeIndex, const char* fnEdgeMat);
// 保存处理结果
bool save_edge_file(const char* fnEdgeIndex, const char* fnEdgeMat);
// 根据(x, y),选择一条直线,容错程度参考config.maxSelectPixelDist
int select_line(int x, int y) const;
// 根据id获得直线
const LineWrapper* get_line(int id) const;
public:
// 配置
ImgToCADConfig cfg;
// 检测完成后,可以显示出检测结果,默认为false
bool showImg;
private:
void FindLines(cv::Mat& matSrc);
void DrawSelected(cv::Mat& matSrc, int x, int y);
void Process();
private:
cv::Mat srcImage, dstImage, grayImage;
std::vector<LineWrapper> lineWrapper;
};
头文件
// stdafx.h
#pragma once
#ifdef _MSC_VER
#ifdef CAMERASETUPDLL_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif
#else
#define DLL_API
#endif
// ImgToCAD.h
#ifndef _IMGTOCAD_H_
#define _IMGTOCAD_H_
#include <opencv2/opencv.hpp>
#include "stdafx.h"
class DLL_API ImgToCADConfig
{
public:
ImgToCADConfig() :
nModeValue(1), nCannyLowThreshold(50), nSobelKernelSize(1),
hlThreshold(25), hlMinLineLength(3), hlMaxLineGap(3),
maxSelectPixelDist(10)
{}
//bool load_config(const char* fnConfig);
//bool save_config(const char* fnConfig);
int nModeValue; //0为原图像,1为canny,2为sobel,3位Scharr,4为Laplacian,5为Laplacian增强对比度
int nCannyLowThreshold; //canny边缘检测低阈值
int nSobelKernelSize; //sobel算子内核大小
int hlThreshold;
int hlMinLineLength;
int hlMaxLineGap;
int maxSelectPixelDist;
};
struct DLL_API LineWrapper
{
int id;
float dist;
cv::Vec4i ln;
cv::Point2f getVerticalPoint(int x, int y) const;
LineWrapper() : id(-1), dist(0.0), ln() {}
};
class DLL_API ImgToCAD
{
friend void on_ModeType(int, void *);
friend void on_CannyType(int, void *);
friend void on_SobelType(int, void *);
friend void on_FindLine(int, void *);
friend void on_Mouse(int event, int x, int y, int, void*);
public:
ImgToCAD();
ImgToCAD(const ImgToCADConfig& _cfg);
bool process_image(const char* fnImg);
bool load_edge_file(const char* fnEdgeIndex, const char* fnEdgeMat);
bool save_edge_file(const char* fnEdgeIndex, const char* fnEdgeMat);
int select_line(int x, int y) const;
const LineWrapper* get_line(int id) const;
public:
ImgToCADConfig cfg;
bool showImg;
private:
void FindLines(cv::Mat& matSrc);
void DrawSelected(cv::Mat& matSrc, int x, int y);
void Process();
private:
cv::Mat srcImage, dstImage, grayImage;
std::vector<LineWrapper> lineWrapper;
};
#endif
源代码
#include "ImgToCAD.h"
#include <fstream>
//https://www.cnblogs.com/mfryf/p/3491777.html
static double GetPointDistance(cv::Point p1, cv::Point p2)
{
return sqrt((p1.x - p2.x)*(p1.x - p2.x) + (p1.y - p2.y)*(p1.y - p2.y));
}
static float GetNearestDistance(cv::Point PA, cv::Point PB, cv::Point P3)
{
float a, b, c;
a = GetPointDistance(PB, P3);
if (a <= 0.00001)
return 0.0f;
b = GetPointDistance(PA, P3);
if (b <= 0.00001)
return 0.0f;
c = GetPointDistance(PA, PB);
if (c <= 0.00001)
return a;
if (a*a >= b * b + c * c)
return b;
if (b*b >= a * a + c * c)
return a;
float l = (a + b + c) / 2;
float s = sqrt(l*(l - a)*(l - b)*(l - c));
return 2 * s / c;
}
ImgToCAD::ImgToCAD() :
cfg(), showImg(false),
srcImage(), dstImage(), grayImage()
{
}
ImgToCAD::ImgToCAD(const ImgToCADConfig& _cfg) :
cfg(_cfg), showImg(false),
srcImage(), dstImage(), grayImage()
{
}
bool ImgToCAD::process_image(const char* fnImg)
{
srcImage = cv::imread(fnImg);
dstImage.create(srcImage.size(), srcImage.type());
Process();
return true;
}
bool ImgToCAD::load_edge_file(const char* fnEdgeIndex, const char* fnEdgeMat)
{
std::ifstream ifs(fnEdgeIndex);
if (!ifs.is_open())
return false;
for (std::string line; std::getline(ifs, line); )
{
LineWrapper lw;
char* strend = nullptr;
lw.id = strtol(line.c_str(), &strend, 10);
lw.ln[0] = strtol(strend + 1, &strend, 10);
lw.ln[1] = strtol(strend + 1, &strend, 10);
lw.ln[2] = strtol(strend + 1, &strend, 10);
lw.ln[3] = strtol(strend + 1, &strend, 10);
lineWrapper.push_back(lw);
}
return true;
}
bool ImgToCAD::save_edge_file(const char* fnEdgeIndex, const char* fnEdgeMat)
{
FILE* fpEdgeIndex = fopen(fnEdgeIndex, "wb");
for (auto& lw : lineWrapper)
{
char buf[100];
size_t len = sprintfse(buf, "%d %d %d %d %d\n", lw.id, lw.ln[0], lw.ln[1], lw.ln[2], lw.ln[3]);
fwrite(buf, 1, len, fpEdgeIndex);
}
fclose(fpEdgeIndex);
FILE* fpEdgeMat = fopen(fnEdgeMat, "wb");
fclose(fpEdgeMat);
std::vector<int> compression_params;
compression_params.push_back(cv::IMWRITE_PNG_COMPRESSION);
compression_params.push_back(9);
cv::imwrite(fnEdgeMat, dstImage, compression_params);
return true;
}
int ImgToCAD::select_line(int x, int y) const
{
if(showImg)
std::cout << "x=" << x << ",y=" << y << std::endl;
std::vector<LineWrapper> lineWrapperCp(lineWrapper);
for (size_t i = 0; i < lineWrapperCp.size(); i++)
{
LineWrapper& lw(lineWrapperCp[i]);
//lw.dist = getDist_P2L(cv::Point(x, y), cv::Point(lw.ln[0], lw.ln[1]), cv::Point(lw.ln[2], lw.ln[3]));
lw.dist = GetNearestDistance(cv::Point(lw.ln[0], lw.ln[1]), cv::Point(lw.ln[2], lw.ln[3]), cv::Point(x, y));
}
//find near 0
std::sort(lineWrapperCp.begin(), lineWrapperCp.end(), [](const LineWrapper& a, const LineWrapper& b) {
return std::abs(a.dist) < std::abs(b.dist);
});
if (lineWrapperCp[0].dist < 0 || lineWrapperCp[0].dist > cfg.maxSelectPixelDist)
return -1;
return lineWrapperCp[0].id;
}
const LineWrapper* ImgToCAD::get_line(int id) const
{
if (id < 0 || id >= lineWrapper.size())
return nullptr;
return &lineWrapper[id];
}
void ImgToCAD::FindLines(cv::Mat& matSrc)
{
cv::cvtColor(matSrc, grayImage, cv::COLOR_RGB2GRAY);
std::vector<cv::Vec4i> g_lines;
lineWrapper.clear();
//void HoughLinesP( InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength = 0, double maxLineGap = 0 );
cv::HoughLinesP(grayImage, g_lines, 1, CV_PI / 180, cfg.hlThreshold, cfg.hlMinLineLength, cfg.hlMaxLineGap);
for (size_t i = 0; i < g_lines.size(); i++)
{
cv::Vec4i ln = g_lines[i];
cv::line(matSrc, cv::Point(ln[0], ln[1]), cv::Point(ln[2], ln[3]), cv::Scalar(0, 0, 255));
LineWrapper lw;
lw.id = i;
lw.ln = ln;
lineWrapper.push_back(lw);
}
}
void ImgToCAD::DrawSelected(cv::Mat& matSrc, int x, int y)
{
//cv::putText(matSrc, "123", cv::Point(10, 50), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 255));
int lwid = select_line(x, y);
if (lwid <= -1)
return;
cv::Vec4i ln = lineWrapper[lwid].ln;
cv::line(matSrc, cv::Point(ln[0], ln[1]), cv::Point(ln[2], ln[3]), cv::Scalar(0, 255, 0), 2);
}
//定义回调函数
void ImgToCAD::Process()
{
cv::Mat g_grayImage1, g_grayImage2, g_tmpImage, g_tempImage, g_tmpImage1, g_tempImage1;
switch (cfg.nModeValue)
{
case 0://源图像
srcImage.copyTo(dstImage);
break;
case 1://canny边缘检测算法
cv::cvtColor(srcImage, grayImage, cv::COLOR_RGB2GRAY);
cv::blur(grayImage, g_tmpImage, cv::Size(3, 3));//先用均值滤波器进行平滑去噪
cv::Canny(g_tmpImage, g_tmpImage, cfg.nCannyLowThreshold, cfg.nCannyLowThreshold * 3);
dstImage = cv::Scalar::all(0);
srcImage.copyTo(dstImage, g_tmpImage);//将g_tmpImage作为掩码,来将原图像拷贝到输出图像中
break;
case 2://sobel边缘检测算法
cv::Sobel(srcImage, g_tmpImage, CV_16S, 1, 0, (2 * cfg.nSobelKernelSize + 1), 1, 1, cv::BORDER_DEFAULT);//求x方向梯度
cv::convertScaleAbs(g_tmpImage, g_tmpImage1);//计算绝对值,并将结果转换成8位
cv::Sobel(srcImage, g_tempImage, CV_16S, 0, 1, (2 * cfg.nSobelKernelSize + 1), 1, 1, cv::BORDER_DEFAULT);//求y方向梯度
cv::convertScaleAbs(g_tempImage, g_tempImage1);//计算绝对值,并将结果转换成8位
cv::addWeighted(g_tmpImage1, 0.5, g_tempImage1, 0.5, 0, dstImage);
break;
case 3://Scharr函数
cv::Scharr(srcImage, g_tmpImage, CV_16S, 1, 0, 1, 0, cv::BORDER_DEFAULT);//求x方向梯度
cv::convertScaleAbs(g_tmpImage, g_tmpImage1);//计算绝对值,并将结果转换成8位
cv::Sobel(srcImage, g_tempImage, CV_16S, 0, 1, 1, 0, cv::BORDER_DEFAULT);//求y方向梯度
cv::convertScaleAbs(g_tempImage, g_tempImage1);//计算绝对值,并将结果转换成8位
cv::addWeighted(g_tmpImage1, 0.5, g_tempImage1, 0.5, 0, dstImage);
break;
case 4://Laplacian边缘检测算法
cv::GaussianBlur(srcImage, g_tmpImage, cv::Size(3, 3), 0, 0, cv::BORDER_DEFAULT);//高斯滤波消除噪声
cv::cvtColor(g_tmpImage, g_grayImage1, cv::COLOR_RGB2GRAY);
cv::Laplacian(g_grayImage1, g_tmpImage, CV_16S, (cfg.nSobelKernelSize * 2 + 1), 1, 0, cv::BORDER_DEFAULT);
cv::convertScaleAbs(g_tmpImage, dstImage);
break;
case 5://利用原图像减去拉普拉斯图像增强对比度
cv::GaussianBlur(srcImage, g_tmpImage, cv::Size(3, 3), 0, 0, cv::BORDER_DEFAULT);//高斯滤波消除噪声
cv::cvtColor(g_tmpImage, g_grayImage2, cv::COLOR_RGB2GRAY);
cv::Laplacian(g_grayImage2, g_tmpImage, CV_16S, (cfg.nSobelKernelSize * 2 + 1), 1, 0, cv::BORDER_DEFAULT);
cv::convertScaleAbs(g_tmpImage, g_tmpImage1);
dstImage = g_grayImage2 - g_tmpImage1;
break;
}
FindLines(dstImage);
#ifdef _MSC_VER
if (showImg)
cv::imshow("EdgeDetector", dstImage);
#endif
}
// known: line ab, point c
// request: vertical point: d
// float abx = b.x - a.x;
// float aby = b.y - a.y;
// float acx = c.x - a.x;
// float acy = c.y - a.y;
// float f = (abx*acx + aby * acy) / (abx*abx + aby * aby); // 注意ab必须是直线上的两个不同点
// d.x = a.x + f * abx;
// d.y = a.y + f * aby;
cv::Point2f LineWrapper::getVerticalPoint(int x, int y) const
{
cv::Point2f d;
float abx = ln[2] - ln[0];
float aby = ln[3] - ln[1];
float acx = x - ln[0];
float acy = y - ln[1];
float f = (abx*acx + aby * acy) / (abx*abx + aby * aby); // 注意ab必须是直线上的两个不同点
d.x = ln[0] + f * abx;
d.y = ln[1] + f * aby;
return d;
}