基于OpenCV(C++)实现的亚像素级简易直线卡尺测量工具

摘要:根据一维边缘提取原理,基于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

评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值