摘要:根据一维边缘提取原理,基于OpenCV(c++)库开发了简易直线卡尺测量工具,能实现亚像素级精度。使用OpenCV库读取图片,在图片上画出指定个数的ROI区域、参考直线以及ROI区域的箭头方向,定义容器记录相关数据。使用OpenCV的回调函数以及编写的旋转函数,使得整个ROI区域、参考线和箭头可由鼠标拖动,键盘能够操作旋转。根据记录的数据计算每个ROI区域的采样点,使用双线性插值算法获取采样点的灰度值,再计算每组采样点的灰度平均值作为新的一组数据,将其求导并过滤,随后获取这组数据的谷值和峰值。引入距离和对比度评分系统,计算谷值或峰值与参考直线的距离以及将对比度除以255,为距离和对比度赋上不同的权重,计算最终得分获取得分最高的谷值或峰值。获取谷值或峰值附近的三个点拟合抛物线并求最大值或最小值,获取最值对应点实际坐标,使用RANSAC算法进行过滤,过滤之后拟合成直线。
c++代码如下:
#include <opencv2/opencv.hpp>
#include <iostream>
#include <cmath>
#include"Scale_image.h"
#include <algorithm>
#include <Eigen/Dense>
using namespace cv;
//用opencv(c++)版在图片上生成一条直线,把直线十等分,会得到十个点,在十个点的基础上生成矩形构成的roi,
//我还需要这一条直线和十个矩形roi作为一个整体能够用鼠标拖动
//直线的起始点和结束点尽量在图片的中心区域
//直线和矩形在原图上显示出来
//20240109 实现按键旋转直线
//20240110 实现鼠标拖动直线
// 定义全局变量
cv::Mat tempImg;
cv::Mat tempImg_Scale;
cv::Point2d startPt, endPt; // 参考直线的起始点和结束点
bool isDragging = false; // 标记当前是否正在进行拖动操作
cv::Point2d dragStart; // 记录拖动开始时鼠标的位置
cv::Point2d rotate_endPt;//定义直线旋转后的终点坐标
std::vector<cv::Point2d> leftArrowTip_set;//箭头左侧端点集合
std::vector<cv::Point2d> rightArrowTip_set;//箭头右侧端点集合
// 矩形的长和宽
double rectLength = 10.0;
double rectWidth = 16.0;
// 计算向量的长度
double length(const cv::Point2d& vec) {
return std::sqrt(vec.x * vec.x + vec.y * vec.y);
}
// 计算向量的单位向量
cv::Point2d normalize(const cv::Point2d& vec) {
double len = length(vec);
return cv::Point2d(vec.x / len, vec.y / len);
}
// 计算与给定向量垂直的向量
cv::Point2d perpendicular(const cv::Point2d& vec) {
return cv::Point2d(-vec.y, vec.x);
}
// 在图像上绘制带箭头的线
// img:要绘制箭头的图像
// start:箭头的起始点
// end:箭头的结束点
// color:箭头的颜色,默认为绿色(0, 255, 0)
// thickness:箭头线的宽度,默认为 1
// lineType:线的类型,默认为抗锯齿线 cv::LINE_AA
// arrowLength:箭头长度与箭身长度的比例因子,默认为 0.1
void drawArrow(cv::Mat& img, const cv::Point2d& start, const cv::Point2d& end,
const cv::Scalar& color = cv::Scalar(0, 255, 0), int thickness = 1,
int lineType = cv::LINE_AA, double arrowLength = 0.2) {
// 绘制箭身
cv::line(img, start, end, color, thickness, lineType);
// 计算箭头方向向量
cv::Point2d arrowDir = end - start;
// cout << end << "456" << start << endl;
// 计算箭头向量,这里通过将单位箭头方向向量乘以箭头长度(通过比例因子和箭身长度计算)
// 单位箭头方向向量 * 比例因子 * 箭身长度
cv::Point2d arrowVec = normalize(arrowDir) * arrowLength * length(arrowDir);
// 计算箭头左侧端点
// 从箭头终点开始,减去垂直于箭头方向向量的一部分(控制箭头宽度)
// 再减去箭头向量
cv::Point2d leftArrowTip = end - perpendicular(arrowDir) * 0.08 * arrowLength * length(arrowDir) - arrowVec;
// 计算箭头右侧端点
// 从箭头终点开始,加上垂直于箭头方向向量的一部分(控制箭头宽度)
// 再减去箭头向量
cv::Point2d rightArrowTip = end + perpendicular(arrowDir) * 0.08 * arrowLength * length(arrowDir) - arrowVec;
// 绘制箭头左侧线
cv::line(img, end, leftArrowTip, color, thickness, lineType);
// 绘制箭头右侧线
cv::line(img, end, rightArrowTip, color, thickness, lineType);
}
// 旋转直线功能函数
void rotate(cv::Mat img, int angle, int rotate_keys, cv::Point2d* startPt, cv::Point2d* endPt)//旋转直线
{
/*
rotate_keys:为设置按键按了几次参数
angle:按下之后旋转多少度
*/
const double PI = 3.14159265358979323846;
// 计算 (x2 - x1)^2 + (y2 - y1)^2
double squaredDiffX = std::pow(endPt->x - startPt->x, 2);
double squaredDiffY = std::pow(endPt->y - startPt->y, 2);
// 计算距离
double R = std::sqrt(squaredDiffX + squaredDiffY);
//std::cos 函数的参数是以弧度为单位的,而不是角度。弧度 = 角度×(π / 180)。
endPt->x = startPt->x + R * std::cos((rotate_keys * angle) * (PI / 180.0)); //旋转后的终点坐标的x值
endPt->y = startPt->y - R * std::sin((rotate_keys * angle) * (PI / 180.0)); //旋转后的终点坐标的y值
}
// 获取参考直线的起始点和终止点
void get_line_point(cv::Mat& img, cv::Point2d* startPt, cv::Point2d* endPt)
{
// 初始化直线的起始点和结束点,尽量在图片中心区域
int centerX = img.cols / 2; // 计算图像宽度方向的中心位置
int centerY = img.rows / 2; // 计算图像高度方向的中心位置
*startPt = Point2d(centerX - 100, centerY); // 设置直线起始点,在中心位置左侧100像素
*endPt = Point2d(centerX + 100, centerY); // 设置直线结束点,在中心位置右侧100像素
//std::cout << *endPt << std::endl;
}
// 绘制roi区域
void drawing_roi(int numbers_roi, cv::Mat& img) {
/*
numbers_roi:指定矩形框的数量
*/
// 清空容器,防止上一轮的位置还留在容器中
leftArrowTip_set.clear();
rightArrowTip_set.clear();
// 计算直线方向向量
cv::Point2d lineDir = endPt - startPt;//坐标相减可以表示为一个向量
// 计算向量的长度
double lenth = std::sqrt(lineDir.x * lineDir.x + lineDir.y * lineDir.y);//sqrt是开根号
// 计算向量的单位向量
cv::Point2d unitLineDir = cv::Point2d(lineDir.x / lenth, lineDir.y / lenth);
//cv::Point2d unitLineDir = normalize(lineDir);
// 计算直线的总长度;
double totalLength = cv::norm(lineDir);
// 计算相邻矩形中心的间距
double interval = totalLength / numbers_roi;
for (int i = 0; i < numbers_roi; ++i) {
// 计算矩形中心位置
cv::Point2d rectCenter = startPt + unitLineDir * interval * i;
// 计算与给定向量垂直的向量
cv::Point2d perpendicularDir = cv::Point2d(-unitLineDir.y, unitLineDir.x);
// 计算矩形的四个顶点
cv::Point2d topLeft = rectCenter - perpendicularDir * rectWidth / 2 - unitLineDir * rectLength / 2;
cv::Point2d bottomLeft = rectCenter + perpendicularDir * rectWidth / 2 - unitLineDir * rectLength / 2;
cv::Point2d bottomRight = rectCenter + perpendicularDir * rectWidth / 2 + unitLineDir * rectLength / 2;
cv::Point2d topRight = rectCenter - perpendicularDir * rectWidth / 2 + unitLineDir * rectLength / 2;
//绘制矩形
cv::line(img, topLeft, topRight, cv::Scalar(0, 255, 0), 1);
cv::line(img, topRight, bottomRight, cv::Scalar(0, 255, 0), 1);
cv::line(img, bottomRight, bottomLeft, cv::Scalar(0, 255, 0), 1);
cv::line(img, bottomLeft, topLeft, cv::Scalar(0, 255, 0), 1);
// 计算短边中心线的起点和终点
cv::Point2d shortCenterStart = rectCenter - perpendicularDir * rectWidth / 2;
cv::Point2d shortCenterEnd = rectCenter + perpendicularDir * rectWidth / 2;
// 绘制带箭头的短边中心线
drawArrow(img, shortCenterStart, shortCenterEnd);
leftArrowTip_set.push_back(shortCenterStart);// 将目前roi区域的箭头的左侧端点加入容器中
rightArrowTip_set.push_back(shortCenterEnd);// 将目前roi区域的箭头的右侧端点加入容器中
}
}
// 确定旋转矩形内采样点
std::vector<std::vector<cv::Point2d>> getSamplingPoints(const cv::Point2d& start, const cv::Point2d& end, double width, int num_points_per_side, int numbers_point) {
/*
* std::vector<std::vector<cv::Point2d>> 表示返回值类型,是一个二维向量,其中外层向量存储每组采样点,内层向量存储每组采样点中的两个点(矩形中心线两侧的点);
* width 是旋转矩形的宽度;
* num_points_per_sid:垂直于中心线取的点的个数;
* numbers_point:需要在一个roi区域中取多少组采样点;
* 访问 samplingPoints 中的元素代码示例
外层 for 循环遍历 samplingPoints 中的每组采样点。
内层 for 循环遍历每组中的每个点,输出其坐标。
for (size_t i = 0; i < samplingPoints.size(); ++i) {
std::cout << "Group " << i << " of sampling points: " << std::endl;
for (size_t j = 0; j < samplingPoints[i].size(); ++j) {
cv::Point2d point = samplingPoints[i][j];
std::cout << " Point " << j << ": (" << point.x << ", " << point.y << ")" << std::endl;
}
*/
// 定义采样点容器
std::vector<std::vector<cv::Point2d>> samplingPoints;
// 定义具体存储每一组采样点的容器
std::vector<cv::Point2d> pair_r;
// 表示箭头的向量
cv::Point2d Arrow_lineDir = end - start;//坐标相减可以表示为一个向量
//float c = Arrow_lineDir.x * Arrow_lineDir.x + Arrow_lineDir.y * Arrow_lineDir.y;
// 计算箭头向量的长度
double Arrow_lineDir_lenth = std::sqrt(Arrow_lineDir.x * Arrow_lineDir.x + Arrow_lineDir.y * Arrow_lineDir.y);//sqrt是开根号
// 计算箭头向量的单位向量
cv::Point2d Arrow_unitLineDir = cv::Point2d(Arrow_lineDir.x / Arrow_lineDir_lenth, Arrow_lineDir.y / Arrow_lineDir_lenth);
// 计算箭头的总长度
double Arrow_lenth = cv::norm(Arrow_lineDir);
// 计算箭头线上每一组采样点的间隔
double Groups_gap_arrow = Arrow_lenth / static_cast<double>(numbers_point);
// 计算垂直于直线上每一组采样点的间隔
double Groups_gap = width / static_cast<double>(num_points_per_side);
for (int i = 0; i < numbers_point; i++) {
// 计算每一组取样点在箭头线上的向量
cv::Point2d sample_point_grous = start + Groups_gap_arrow * Arrow_unitLineDir * i;
// 正方向
// 表示与箭头向量垂直的向量
cv::Point2d Arrow_lineDir_vertical_r = cv::Point2d(-Arrow_lineDir.y, Arrow_lineDir.x);
// 计算箭头向量的长度
double Arrow_lineDir_vertical_lenth_r = std::sqrt(Arrow_lineDir_vertical_r.x * Arrow_lineDir_vertical_r.x + Arrow_lineDir_vertical_r.y * Arrow_lineDir_vertical_r.y);//sqrt是开根号
// 计算与箭头向量垂直的向量的单位向量
cv::Point2d Arrow_lineDir_vertical_unitLineDir_r = cv::Point2d(Arrow_lineDir_vertical_r.x / Arrow_lineDir_vertical_lenth_r, Arrow_lineDir_vertical_r.y / Arrow_lineDir_vertical_lenth_r);
for (int j = 0; j < num_points_per_side; j++) {
// 计算具体的采样点的位置
cv::Point2d pairs = sample_point_grous + j * Groups_gap * Arrow_lineDir_vertical_unitLineDir_r;
pair_r.push_back(pairs);
}
//反方向
// 表示与箭头向量垂直的向量
cv::Point2d Arrow_lineDir_vertical_l = cv::Point2d(Arrow_lineDir.y, -Arrow_lineDir.x);
// 计算箭头向量的长度
double Arrow_lineDir_vertical_lenth_l = std::sqrt(Arrow_lineDir_vertical_l.x * Arrow_lineDir_vertical_l.x + Arrow_lineDir_vertical_l.y * Arrow_lineDir_vertical_l.y);//sqrt是开根号
// 计算与箭头向量垂直的向量的单位向量
cv::Point2d Arrow_lineDir_vertical_unitLineDir_l = cv::Point2d(Arrow_lineDir_vertical_l.x / Arrow_lineDir_vertical_lenth_l, Arrow_lineDir_vertical_l.y / Arrow_lineDir_vertical_lenth_l);
for (int j = 0; j < num_points_per_side; j++) {
// 计算具体的采样点的位置
cv::Point2d pairs = sample_point_grous + j * Groups_gap * Arrow_lineDir_vertical_unitLineDir_l;
pair_r.push_back(pairs);
}
//for (const auto& pairs : pair_r)
//{
// // 要绘制叉号的坐标点
// cv::Point2d point = pairs;
// // 叉号的长度
// int crossLength = 1;
// // 叉号的颜色(BGR 格式,这里使用红色)
// cv::Scalar color(0, 0, 255);
// // 绘制第一条斜线
// cv::line(tempImg_Scale, cv::Point2d(point.x - crossLength, point.y - crossLength), cv::Point2d(point.x + crossLength, point.y + crossLength), color, 2);
// // 绘制第二条斜线
// cv::line(tempImg_Scale, cv::Point2d(point.x - crossLength, point.y + crossLength), cv::Point2d(point.x + crossLength, point.y - crossLength), color, 2);
// // 显示图像
// cv::imshow("Image with ROI", tempImg_Scale); // 在名为"Image with ROI"的窗口中显示绘制后的图像
//}
samplingPoints.push_back(pair_r);//samplingPoints是二维动态数组
pair_r.clear();// 循环一次需清除容器中的数据,否则数据将带入到下一轮中
}
return samplingPoints;//返回采样点
}
// 自定义双线性插值函数 并计算灰度值
std::vector<std::pair<int, double>> bilinearInterpolation(const Mat& image, const std::vector<std::vector<cv::Point2d>>& samplingPoints) {
/*
* 返回的容器中将包含每一组的采样点灰度值的平均值对应的组号
* std::pair只能存储两个元素
*/
// 定义一个roi中每一组灰度值的平均值以及对应的组号
std::pair<int, double> gray_value;
std::vector<std::pair<int, double>> re;
re.clear();
for (int i = 0; i < samplingPoints.size(); ++i) {//第几组
// 定义某一组的灰度值的和
double sum_gray_value = 0.0;
for (int j = 0; j < samplingPoints[i].size(); ++j) {
// 获取左上角的整数坐标
int x0 = static_cast<int>(samplingPoints[i][j].x); // 左上角的x坐标
int y0 = static_cast<int>(samplingPoints[i][j].y); // 左上角的y坐标
int x1 = x0 + 1; // 右上角的x坐标
int y1 = y0 + 1; // 左下角的y坐标
// 防止越界,保证在图像范围内
x1 = std::min(x1, image.cols - 1);
y1 = std::min(y1, image.rows - 1);
// 获取四个邻近像素值
double I00 = image.at<uchar>(y0, x0); // (x0, y0) 像素值
double I01 = image.at<uchar>(y0, x1); // (x1, y0) 像素值
double I10 = image.at<uchar>(y1, x0); // (x0, y1) 像素值
double I11 = image.at<uchar>(y1, x1); // (x1, y1) 像素值
// 水平方向插值
double R1 = I00 * (x1 - samplingPoints[i][j].x) + I01 * (samplingPoints[i][j].x - x0);
double R2 = I10 * (x1 - samplingPoints[i][j].x) + I11 * (samplingPoints[i][j].x - x0);
// 垂直方向插值
double result = R1 * (y1 - samplingPoints[i][j].y) + R2 * (samplingPoints[i][j].y - y0);
// 求一组灰度值的和
sum_gray_value = sum_gray_value + result;
}
// 求一组灰度值的平均值
double average_gray_value = sum_gray_value / samplingPoints[i].size();
// 将平均值加入到容器中
gray_value = { i,average_gray_value };
re.push_back(gray_value);
}
return re;
}
// 生成一维高斯核
std::vector<double> gaussianKernel(int size, double sigma) {
std::vector<double> kernel(size); // 存储高斯核的容器
int half_size = size / 2; // 核的一半大小
double sum = 0.0;
// 计算高斯核的每个值
for (int i = -half_size; i <= half_size; ++i) {
kernel[i + half_size] = exp(-0.5 * (i * i) / (sigma * sigma)); // 高斯公式
sum += kernel[i + half_size]; // 计算所有权重的总和,用于归一化
}
// 归一化,使得滤波器的总和为 1
for (int i = 0; i < size; ++i) {
kernel[i] /= sum; // 每个权重除以总和
}
return kernel; // 返回归一化后的高斯核
}
// 一维卷积函数
std::vector<double> convolve(const std::vector<double>& data, const std::vector<double>& kernel) {
int data_size = data.size(); // 数据的大小
int kernel_size = kernel.size(); // 高斯核的大小
int half_kernel = kernel_size / 2; // 高斯核的一半大小
std::vector<double> result(data_size, 0.0); // 存储卷积结果的容器,初始值为0
// 对数据进行卷积操作
for (int i = 0; i < data_size; ++i) {
double sum = 0.0; // 存储每次卷积的加权和
for (int j = -half_kernel; j <= half_kernel; ++j) {
int data_index = i + j; // 计算数据的位置
if (data_index >= 0 && data_index < data_size) { // 确保索引在数据范围内
sum += data[data_index] * kernel[j + half_kernel]; // 进行加权计算
}
}
result[i] = sum; // 存储卷积结果
}
return result; // 返回卷积结果
}
// 求离散导数并使用高斯滤波
std::vector<double> Discrete_deri(std::vector<std::pair<int, double>> Gray_value) {
/*
1.传入灰度值
2.返回求导并平滑后的值
*/
// 定义求导后的值
std::vector<double> dicreting_value;
// 使用前清空容器
dicreting_value.clear();
// *1 对离散数据求导
//for (int i = 1; i < Gray_value.size()-1; i++)
//{
// 离散导数求导公式 来自于机器视觉算法与应用公式3.112 第295页
// double dicreting = (Gray_value[i + 1].second - Gray_value[i - 1].second) / 2;
// 加入容器
// dicreting_value.push_back(dicreting);
//}
// *2 对离散数据求导
for (int i = 0; i < Gray_value.size(); i++)
{
// 第0项与最后一项不参与求导,提前加入
if (i == 0 || i == Gray_value.size() - 1) {
dicreting_value.push_back(Gray_value[i].second);
}
else {
// 离散导数求导公式 来自于机器视觉算法与应用公式3.112 第295页
double dicreting = (Gray_value[i + 1].second - Gray_value[i - 1].second) / 2;
// 加入容器
dicreting_value.push_back(dicreting);
}
}
// 生成高斯核
int kernel_size = 7; // 高斯核大小;其值一般为2*(3*sigma)+1
double sigma = 1.0; // 高斯核的标准差
std::vector<double> kernel = gaussianKernel(kernel_size, sigma); // 调用函数生成高斯核
// 对数据进行卷积(平滑处理)
std::vector<double> smoothed_data = convolve(dicreting_value, kernel);
// 打印求导后的数据
//std::cout << "求导后的数据:数量:" << dicreting_value.size() << std::endl;
//for (const auto& data : dicreting_value)
//{
// std::cout << "求导后的数据:具体数据:" << data << std::endl;
//}
// 返回平滑后的数据
return smoothed_data;
}
// 使用eigen库拟合抛物线 并求出最大值或最小值对应的横坐标
double fit_function(std::vector<double> Neighbors, std::vector<double> Indexes) {
// 三个点的坐标
double x1 = Indexes[0], y1 = Neighbors[0];
double x2 = Indexes[1], y2 = Neighbors[1];
double x3 = Indexes[2], y3 = Neighbors[2];
// 将 std::vector 转换为 Eigen::MatrixXd 类型
int n = Indexes.size(); // 数据点的数量
// 构建 X 矩阵,包含 x^2, x 和 1 三列
Eigen::MatrixXd X(n, 3);
for (int i = 0; i < n; ++i) {
X(i, 0) = Indexes[i] * Indexes[i]; // x^2
X(i, 1) = Indexes[i]; // x
X(i, 2) = 1; // 常数项
}
// 将 y_data 转换为 Eigen::VectorXd 类型
Eigen::VectorXd y(n);
for (int i = 0; i < n; ++i) {
y(i) = Neighbors[i];
}
// 使用最小二乘法解线性方程 X * [a, b, c] = y
Eigen::VectorXd coeff = X.colPivHouseholderQr().solve(y);
// 输出拟合的系数 a, b, c
std::cout << "拟合的系数:\n";
std::cout << "a = " << coeff(0) << "\n";
std::cout << "b = " << coeff(1) << "\n";
std::cout << "c = " << coeff(2) << "\n";
double a = coeff(0);
double b = coeff(1);
double c = coeff(2);
// 计算顶点的横坐标 x_v
double x_v = -b / (2 * a);
// 计算对应的纵坐标 y_v(即最大值或最小值)
double y_v = a * pow(x_v, 2) + b * x_v + c;
if (a > 0) {
std::cout << "The minimum value of the parabola is: " << y_v << " at x = " << x_v << std::endl;
}
else {
std::cout << "The maximum value of the parabola is: " << y_v << " at x = " << x_v << std::endl;
}
// 输出拟合的抛物线方程
std::cout << "拟合的抛物线方程为: y = " << coeff(0) << "x^2 + " << coeff(1) << "x + " << coeff(2) << std::endl;
std::vector<double> maxmin_xValue;
maxmin_xValue.push_back(x_v);
// 返回抛物线最大值最小值的x轴坐标。
return x_v;
}
// 鼠标回调函数,用于处理鼠标事件
void mouseCallback(int event, int x, int y, int flags, void* userdata) {
// 当鼠标左键按下时
if (event == EVENT_LBUTTONDOWN) {
isDragging = true; // 设置为正在拖动状态
dragStart = cv::Point(x, y); // 记录当前鼠标位置作为拖动起始点
}
// 当鼠标左键释放时
else if (event == EVENT_LBUTTONUP) {
isDragging = false; // 结束拖动状态
}
// 当鼠标移动且当前处于拖动状态时
else if (event == EVENT_MOUSEMOVE && isDragging) {
int dx = x - dragStart.x; // 计算鼠标在x方向上的移动距离
int dy = y - dragStart.y; // 计算鼠标在y方向上的移动距离
// 拖动直线
startPt.x += dx; // 更新直线起始点的x坐标
startPt.y += dy; // 更新直线起始点的y坐标
endPt.x += dx; // 更新直线结束点的x坐标
endPt.y += dy; // 更新直线结束点的y坐标
//用于清空图片之前绘制的内容,避免大量重复的roi区域以及直线
// 缩放图片
Scale_image scale;
tempImg_Scale = scale.Scale_show_return(0.3, "Image with ROI", &tempImg);
// 绘制直线、roi以及箭头
cv::line(tempImg_Scale, startPt, endPt, Scalar(0, 0, 255), 1);
drawing_roi(10, tempImg_Scale);
cv::imshow("Image with ROI", tempImg_Scale); // 在名为"Image with ROI"的窗口中显示绘制后的图像
dragStart = Point(x, y); // 更新拖动起始点为当前鼠标位置
}
}
// 计算一个点到直线的垂直距离,直线由参数 Vec4f 表示(vx, vy, x0, y0)
float pointLineDistance(const Point2f& pt, const Vec4f& line) {
// 直线参数:vx 和 vy 是直线的方向向量,x0 和 y0 是直线上的一个点(通常为拟合点)
float vx = line[0], vy = line[1];
float x0 = line[2], y0 = line[3];
// 使用点到直线的垂直距离公式
// 点到直线的距离公式为: |vx * (pt.y - y0) - vy * (pt.x - x0)| / sqrt(vx^2 + vy^2)
return abs(vx * (pt.y - y0) - vy * (pt.x - x0)) / sqrt(vx * vx + vy * vy);
}
// RANSAC算法
// 通过随机选择两个点拟合直线,并评估所有点到该直线的距离
// 在每次迭代中,如果内点(距离小于阈值的点)数量比当前最大值更多,则更新最佳模型
// 输入:
// - points:所有的数据点
// - inliers:输出的内点集合
// - distanceThreshold:距离阈值,用于判断点是否为内点
/**/
// RANSAC算法过滤数据点
std::vector<cv::Point2d> ransacFilter(const std::vector<cv::Point2d>& points, double distanceThreshold) {
// 用于记录到目前为止找到的最佳直线所对应的内点数量,初始化为 0
// 在 RANSAC 算法的每次迭代中,会不断计算当前随机选取的两个点所确定直线的内点数量,
// 并与 bestInliersCount 进行比较,如果当前内点数量更多,则更新 bestInliersCount
int bestInliersCount = 0;
// 用于存储到目前为止找到的最佳直线所对应的内点集合,初始为空
// 当在某次迭代中发现当前直线的内点数量超过了之前记录的 bestInliersCount 时,
// 会将当前直线的内点集合赋值给 bestInliers
std::vector<cv::Point2d> bestInliers;
int n = points.size();
std::srand(static_cast<unsigned int>(std::time(nullptr)));
int maxIterations = log(1 - 0.99) / (log(1 - (1.00 / n))) * 10;
for (int i = 0; i < maxIterations; ++i) {
// 随机选择两个点
int index1 = std::rand() % points.size();
int index2 = std::rand() % points.size();
while (index2 == index1) {
index2 = std::rand() % points.size();
}
cv::Point2f p1 = points[index1];
cv::Point2f p2 = points[index2];
// 计算直线方程 ax + by + c = 0
double a = p2.y - p1.y;
double b = p1.x - p2.x;
double c = p2.x * p1.y - p1.x * p2.y;
std::vector<cv::Point2d> inliers;
for (const auto& point : points) {
// 计算点到直线的距离
double distance = std::abs(a * point.x + b * point.y + c) / std::sqrt(a * a + b * b);
if (distance < distanceThreshold) {
inliers.push_back(point);
}
}
// 更新最佳内点集合
if (inliers.size() > bestInliersCount) {
bestInliersCount = inliers.size();
bestInliers = inliers;
}
}
return bestInliers;
}
// 确定边缘点在图像中的位置
void define_position_image(std::vector<double> x_v) {
/*
x_v: 抛物线的最大值最小值的x轴坐标,为垂直于参考线某一组,为double类型
arrow_start_p: 箭头的起始位置
arrow_end_p: 箭头的结束位置
*/
// 定义需要拟合成直线的点,取某一组与箭头的交点
cv::Point2d lines_point;
std::vector<cv::Point2d> lines_point_v;
// 对每一个矩形进行操作
for (int i = 0; i < leftArrowTip_set.size(); i++)
{
// 箭头左侧端点(箭头起始点)
// 表示箭头的向量
cv::Point2d Arrow_lineDir = rightArrowTip_set[i] - leftArrowTip_set[i];//坐标相减可以表示为一个向量
// 计算箭头向量的长度
double Arrow_lineDir_lenth = std::sqrt(Arrow_lineDir.x * Arrow_lineDir.x + Arrow_lineDir.y * Arrow_lineDir.y);//sqrt是开根号
// 计算箭头向量的单位向量
cv::Point2d Arrow_unitLineDir = cv::Point2d(Arrow_lineDir.x / Arrow_lineDir_lenth, Arrow_lineDir.y / Arrow_lineDir_lenth);
// 计算具体位置
lines_point = leftArrowTip_set[i] + Arrow_unitLineDir * x_v[i] * (rectWidth / 100);
// 加入容器中
lines_point_v.push_back(lines_point);
}
// 在图上绘制这些点
for (const auto& p : lines_point_v) {
cv::circle(tempImg_Scale, p, 3, Scalar(255, 0, 0), -1); // 蓝色的圆表示数据点
}
// RANSAC参数
double distanceThreshold = 1.0;
// 使用RANSAC算法过滤数据点
std::vector<cv::Point2d> filteredPoints = ransacFilter(lines_point_v, distanceThreshold);
// 使用cv::fitLine进行直线拟合
cv::Vec4f line;
cv::fitLine(filteredPoints, line, cv::DIST_L2, 0, 0.01, 0.01);
// 计算直线上的两个端点以绘制直线
cv::Point2f linePoint(line[2], line[3]);
cv::Point2f lineDirection(line[0], line[1]);
// 选择 1000 这个数值,是因为它足够大,能够保证计算出来的端点超出图像的边界,进而让直线可以完整地跨越图像显示区域
// 直线的一个端点
cv::Point2f pt1 = linePoint - lineDirection * 1000;
// 直线的另一个端点
cv::Point2f pt2 = linePoint + lineDirection * 1000;
// 绘制拟合的直线
cv::line(tempImg_Scale, pt1, pt2, cv::Scalar(255, 0, 0), 1);
// 显示图像
cv::imshow("Image with ROI", tempImg_Scale); // 在名为"Image with ROI"的窗口中显示绘制后的图像
// 清空容器
lines_point_v.clear();
}
// 打印数据中的谷值和峰值
void findPeaksAndValleys(const std::vector<double> data, std::vector<double>& Discrete_Peak_Index, std::vector<double>& Discrete_Valleys_Index) {
int n = data.size();
// 检查数组是否至少有3个元素才能定义峰值或谷值
if (n < 3) {
std::cout << "数据太少,无法找到峰值或谷值。" << std::endl;
return;
}
std::cout << "峰值的索引: ";
for (double i = 1; i < n - 1; ++i) {
// 找到峰值
if (data[i] > data[i - 1] && data[i] > data[i + 1]) {
std::cout << i << " ";
Discrete_Peak_Index.push_back(i);
}
}
std::cout << "\n谷值的索引: ";
for (double i = 1; i < n - 1; ++i) {
// 找到谷值
if (data[i] < data[i - 1] && data[i] < data[i + 1]) {
std::cout << i << " ";
Discrete_Valleys_Index.push_back(i);
}
}
std::cout << std::endl;
}
int run() {
int rotate_keys = 1;
// 读取图片
cv::Mat img = cv::imread("D:\\Code\\C++\\picture-lab\\08.bmp");
if (img.empty()) {
std::cout << "Could not open or find the image" << std::endl;
return -1; // 如果图片读取失败,输出错误信息并退出程序
}
// 转换为灰度图像
//cv::Mat img;
//int channels = image.channels(); // 获取通道数
//if (channels == 3) {
// cv::cvtColor(image, img, cv::COLOR_BGR2GRAY);
//}
// 克隆原始图像
tempImg = img.clone(); // 用于在上面绘制直线和ROI,避免直接修改原始图像
// 缩放图片
Scale_image scale;
tempImg_Scale = scale.Scale_show_return(0.3, "Image with ROI", &tempImg);
// 获取直线的初始起点和终点
get_line_point(tempImg_Scale, &startPt, &endPt);
// 创建窗口并设置鼠标回调
//namedWindow("Image with ROI", WINDOW_NORMAL); // 创建一个名为"Image with ROI"的窗口
cv::setMouseCallback("Image with ROI", mouseCallback); // 为该窗口设置鼠标回调函数,以便处理鼠标事件
// 绘制直线roi
cv::line(tempImg_Scale, startPt, endPt, Scalar(0, 0, 255), 1);
drawing_roi(10, tempImg_Scale);//画roi以及箭头
// 显示图像
cv::imshow("Image with ROI", tempImg_Scale); // 在名为"Image with ROI"的窗口中显示绘制后的图像
while (true)
{
// 按ESC键退出
if (waitKey(1) == 27) {
break; // 如果用户按下ESC键(ASCII码为27),退出循环,结束程序
}
int key = cv::waitKey();
if (key == 82) { // 'R' key ASCII value
//旋转直线以及roi区域
rotate(tempImg_Scale, 15, rotate_keys, &startPt, &endPt);
rotate_keys += 1;//记录按了几次R键
//用于清空之前绘制的内容
cv::Mat tempImg_new = scale.Scale_show_return(0.3, "Image with ROI", &tempImg);
// 绘制直线、roi以及箭头
cv::line(tempImg_new, startPt, endPt, Scalar(0, 0, 255), 1);
drawing_roi(10, tempImg_new);
cv::imshow("Image with ROI", tempImg_new); // 在名为"Image with ROI"的窗口中显示绘制后的图像
std::cout << startPt << endPt << std::endl;
}
if (key == 65)// 'A' key ASCII value
{
// 定义容器-存储拟合成直线的点
std::vector<double> x_value;
// 使用前清空容器
x_value.clear();
// 矩形的宽度
double width = 10;
for (int k = 0; k < leftArrowTip_set.size(); k++)
{
std::cout << k << std::endl;
//从容器中取出每个roi箭头的起点
cv::Point2d start = leftArrowTip_set[k];
//从容器中取出每个roi箭头的终点
cv::Point2d end = rightArrowTip_set[k];//对的
// 确定采样点
std::vector<std::vector<cv::Point2d>> samplingPoints = getSamplingPoints(start, end, width, 5, 100);
//输出所有采样点的数据,检查是否正确存储
// for (int i = 0; i < samplingPoints.size(); ++i) {
// std::cout << "Group " << i + 1 << " points:\n";
// for (int j = 0; j < samplingPoints[i].size(); ++j) {
// std::cout << "(" << samplingPoints[i][j].x << ", " << samplingPoints[i][j].y << ") ";
// }
// std::cout << std::endl;
//}
// 计算采样点灰度值
std::vector<std::pair<int, double>> grayValues = bilinearInterpolation(tempImg_Scale, samplingPoints);
for (const auto& a : grayValues)
{
// cout << a.second << endl;
}
// 对数据求导并平滑
std::vector<double> Discrete_deri_smooth = Discrete_deri(grayValues);// 先求导后平滑
for (const auto& c : Discrete_deri_smooth)
{
std::cout << c << std::endl;
}
// 定义存储Discrete_deri_smooth谷值和峰值引用的容器
std::vector<double> Discrete_Peak_Index;// 峰值
std::vector<double> Discrete_Valleys_Index;// 谷值
// 定义最终得分容器
std::vector<double> terminal_score_v;
// 获取谷值和峰值的索引
findPeaksAndValleys(Discrete_deri_smooth, Discrete_Peak_Index, Discrete_Valleys_Index);
// 由白到黑
for (const auto& x : Discrete_Valleys_Index)
{
/* 计算距离得分
距离越近得分越大,所以用1减去,计算公式:1-abs(x-50)/50
其中:x为索引,50为参考直线在索引的位置,因为每个roi区域取100组数据
*/
double distance_score = 1 - abs(x - 50) / 50;
// 计算对比度得分
double contrast_score = abs(Discrete_deri_smooth[x]) / 255;
// 计算最终得分
double terminal_score = sqrt(0.3*distance_score + 0.7*contrast_score);// 0.3和0.7是距离得分与对比度得分的权重
// 将得分存入容器
terminal_score_v.push_back(terminal_score);
}
// 提取谷值索引容器中得分最大时对应的值
double hg_score_index;
// 提取处理后的数据容器中得分最大时对应的具体值
double hg_score_value;
// 定义谷值、峰值附近的索引以及值的容器
std::vector<double> valley_Neighbors, peak_Neighbors;// 谷值、峰值附近的值
std::vector<double> valley_Indexes, peak_Indexes;// 谷值、峰值附近的索引
// 使用 std::max_element 获取容器中的最大值的迭代器[引用]
auto maxIt = std::max_element(terminal_score_v.begin(), terminal_score_v.end());
// 如果容器不为空,计算最大值的索引
if (maxIt != terminal_score_v.end()) {
// 通过距离计算索引
int index = std::distance(terminal_score_v.begin(), maxIt);
std::cout << "The maximum value is: " << *maxIt << ", at index: " << index << std::endl;
// 提取谷值索引容器中得分最大时对应的值
hg_score_index=Discrete_Valleys_Index[index];// 此值为索引
// 将此点以及附近的两个点的索引加入容器
valley_Indexes.push_back(hg_score_index - 1);
valley_Indexes.push_back(hg_score_index);
valley_Indexes.push_back(hg_score_index+1);
std::cout << "处理后的数据:" << Discrete_Valleys_Index[index] << std::endl;
// 提取处理后的数据容器中得分最大时对应的具体值
hg_score_value = Discrete_deri_smooth[hg_score_index];// 此值为处理后的数据值
// 将此点以及附近的两个点的值加入容器
valley_Neighbors.push_back(Discrete_deri_smooth[hg_score_index-1]);
valley_Neighbors.push_back(hg_score_value);
valley_Neighbors.push_back(Discrete_deri_smooth[hg_score_index + 1]);
}
else {
std::cout << "The vector is empty!" << std::endl;
}
std::cout << "附近的三个点" << valley_Neighbors[0] << valley_Neighbors[1] << valley_Neighbors[2] << std::endl;
std::cout << "附近的三个索引" << valley_Indexes[0] << valley_Indexes[1] << valley_Indexes[2] << std::endl;
// 根据三个点拟合抛物线并计算最大或最小值的x轴坐标
double x_v = fit_function(valley_Neighbors, valley_Indexes);
// 加入容器
x_value.push_back(x_v);
}
// 定义边缘点在图片中的位置
define_position_image(x_value);
}
}
return 0;
}
Scale_image.cpp:
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
#include <vector>
#include "Scale_image.h"
cv::Mat show_image;//定义一个缩放后的图片
//缩放并返回图像
cv::Mat Scale_image::Scale_show_return(const double scaleFactor, const std::string str, cv::Mat* paramater_image)
{
//scaleFactor为缩放比例,值介于0-1之间。
//str为传入的窗口的名字
//paramater_image是需要传进来的图片数据
cv::resize(*paramater_image, show_image, cv::Size(), scaleFactor, scaleFactor, cv::INTER_LINEAR);
cv::imshow(str, show_image);//显示缩放后的图片
return show_image;
}
//缩放显示的图像,无返回
void Scale_image::Scale_show(const double scaleFactor, const std::string str, cv::Mat* paramater_image)
{
//scaleFactor为缩放比例,值介于0-1之间。
//str为传入的窗口的名字
//paramater_image是需要传进来的图片数据
cv::resize(*paramater_image, show_image, cv::Size(), scaleFactor, scaleFactor, cv::INTER_LINEAR);
cv::imshow(str, show_image);//显示缩放后的图片
}
Scale_image.h:
#pragma once
#ifndef Scale_image_H
#define Scale_image_H
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
#include <vector>
class Scale_image
{
public:
cv::Mat Scale_show_return(const double scaleFactor, const std::string str, cv::Mat* paramater_image);
void Scale_show(const double scaleFactor, const std::string str, cv::Mat* paramater_image);
};
#endif
888





