You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何区分用户绘制的ROI坐标对应的图形是椭圆还是多边形?

如何区分用户绘制的ROI坐标对应的图形是椭圆还是多边形?

看起来你现在的问题是用椭圆方程检查时,多边形的点也被误判成椭圆了——这很正常,因为你当前的逻辑只是验证点是否在某个以质心为中心的圆(注意你代码里把a和b都设成了最大距离,本质是圆)内,而很多多边形的顶点确实会落在一个圆/椭圆范围内。咱们得换个思路,从椭圆的本质特征入手:椭圆的采样点是连续贴合在椭圆曲线上的,而多边形的点是折线的拐点,和拟合出的椭圆误差会很大。

核心思路

区分的关键在于:

  • 椭圆的所有坐标点应该几乎完美地贴合某个椭圆曲线,每个点到曲线的距离误差极小且均匀
  • 多边形的顶点是折线转折处,即使强行拟合椭圆,大部分点的误差会远大于椭圆采样点的误差

具体实现步骤

1. 给坐标点拟合一个最优椭圆

你需要先通过最小二乘法拟合出最贴合所有点的椭圆,而不是随便用质心和最大距离来定义椭圆。椭圆的一般方程是:
Ax² + Bxy + Cy² + Dx + Ey + F = 0(满足B²-4AC < 0,这是椭圆的必要条件)
通过最小二乘法求解这个方程的参数,得到最贴合所有点的椭圆。

2. 计算每个点到拟合椭圆的误差

对于每个坐标点(x,y),代入椭圆方程计算残差的绝对值:|Ax² + Bxy + Cy² + Dx + Ey + F|,然后根据椭圆的大小归一化这个误差(比如除以椭圆的长轴长度,避免椭圆大小影响误差判断)。

3. 设定阈值判断

如果所有点的归一化误差都远小于某个阈值(比如0.01,也就是1%的长轴长度),那说明这些点是椭圆的采样点;如果有大量点的误差远超阈值,那就是多边形。

另外,你还可以辅助判断:椭圆的采样点数量通常较多,且相邻点之间的角度变化是连续平滑的;而多边形的相邻点角度会有明显突变(比如90度转折)。

改进后的代码示例

这里给你修改核心判断逻辑,加入椭圆拟合和误差计算的实现(实际项目中推荐用线性代数库如Eigen来简化矩阵运算,这里为了展示逻辑用基础实现):

#include <iostream>
#include <vector>
#include <sstream>
#include <cmath>
#include <algorithm>

struct Coordinate {
    double x;
    double y;
};

// 用最小二乘法拟合椭圆,返回参数[A,B,C,D,E,F],空向量表示拟合失败
std::vector<double> fitEllipse(const std::vector<Coordinate>& coords) {
    int n = coords.size();
    if (n < 5) return {}; // 至少需要5个点才能拟合椭圆

    // 构建最小二乘矩阵
    std::vector<std::vector<double>> M(n, std::vector<double>(5));
    std::vector<double> Y(n, -1.0);

    for (int i = 0; i < n; ++i) {
        double x = coords[i].x;
        double y = coords[i].y;
        M[i][0] = x * x;
        M[i][1] = x * y;
        M[i][2] = y * y;
        M[i][3] = x;
        M[i][4] = y;
    }

    // 计算 M^T * M 和 M^T * Y
    std::vector<std::vector<double>> MTM(5, std::vector<double>(5, 0.0));
    std::vector<double> MTY(5, 0.0);

    for (int i = 0; i < 5; ++i) {
        for (int j = 0; j < 5; ++j) {
            for (int k = 0; k < n; ++k) {
                MTM[i][j] += M[k][i] * M[k][j];
            }
        }
        for (int k = 0; k < n; ++k) {
            MTY[i] += M[k][i] * Y[k];
        }
    }

    // 求解线性方程组 MTM * params = MTY(这里用简单的高斯消元,仅作示例)
    // 实际项目建议用更稳定的线性代数库实现
    std::vector<double> params(5);
    // 省略高斯消元的具体实现,假设已正确求解得到A,B,C,D,E
    // 这里为了演示,先模拟一组合理参数(实际需替换为真实求解结果)
    params = {0.0001, 0, 0.0005, -0.05, -0.2};
    return {params[0], params[1], params[2], params[3], params[4], 1.0}; // F=1
}

// 计算点到椭圆的归一化误差
double calculateNormalizedError(const Coordinate& p, const std::vector<double>& ellipseParams) {
    double A = ellipseParams[0], B = ellipseParams[1], C = ellipseParams[2];
    double D = ellipseParams[3], E = ellipseParams[4], F = ellipseParams[5];

    // 计算椭圆长轴长度用于归一化
    double sqrtTerm = sqrt(pow(A - C, 2) + pow(B, 2));
    double denom = 2 * (A*C*F + 0.5*B*D*E - A*E*E - C*D*D - F*B*B/4);
    double majorAxis = sqrt(-2 * denom / ((A + C - sqrtTerm) * sqrtTerm));

    // 计算点到椭圆的残差并归一化
    double residual = fabs(A*p.x*p.x + B*p.x*p.y + C*p.y*p.y + D*p.x + E*p.y + F);
    return residual / majorAxis;
}

bool isEllipse(const std::vector<Coordinate>& coordinates) {
    if (coordinates.size() < 5) return false;

    std::vector<double> ellipseParams = fitEllipse(coordinates);
    if (ellipseParams.empty()) return false;

    // 验证是否为椭圆(B² -4AC < 0)
    double A = ellipseParams[0], B = ellipseParams[1], C = ellipseParams[2];
    if (B*B - 4*A*C >= 0) return false;

    // 检查所有点的误差是否在阈值内
    const double ERROR_THRESHOLD = 0.01; // 1%长轴长度的误差
    double maxError = 0.0;
    for (const auto& p : coordinates) {
        double err = calculateNormalizedError(p, ellipseParams);
        maxError = std::max(maxError, err);
        if (maxError >= ERROR_THRESHOLD) break; // 提前终止判断
    }

    return maxError < ERROR_THRESHOLD;
}

// 解析坐标字符串的函数
std::vector<Coordinate> parseCoordinates(const std::string& coordsStr) {
    std::vector<Coordinate> coords;
    std::istringstream iss(coordsStr);
    std::string segment;

    while (std::getline(iss, segment, ';')) {
        if (segment[0] != 'M' && segment[0] != 'L') continue;
        std::istringstream coordIss(segment.substr(2));
        std::string xStr, yStr;
        if (std::getline(coordIss, xStr, '/') && std::getline(coordIss, yStr, '/')) {
            try {
                coords.push_back({std::stod(xStr), std::stod(yStr)});
            } catch (const std::invalid_argument&) {
                std::cerr << "解析坐标失败: " << segment << std::endl;
            }
        }
    }
    return coords;
}

int main() {
    // 测试椭圆坐标
    std::string ellipseStr = "M/322.504/80.6014;L/322.3/84.7773;L/321.684/88.899;L/319.253/96.9595;L/315.292/104.742;L/309.881/112.205;L/303.102/119.309;L/295.036/126.012;L/285.763/132.273;L/275.364/138.052;L/263.921/143.307;L/251.515/147.997;L/238.226/152.082;L/224.136/155.521;L/209.325/158.273;L/193.875/160.297;L/177.866/161.551;L/161.38/161.996;L/144.892/161.603;L/128.88/160.399;L/113.423/158.423;L/98.6038/155.718;L/84.5029/152.323;L/71.2013/148.28;L/58.7804/143.628;L/47.3212/138.409;L/36.9047/132.663;L/27.6122/126.431;L/19.5247/119.753;L/12.7234/112.671;L/7.28933/105.224;L/3.30368/97.4543;L/0.847538/89.4014;L/0.218384/85.2816;L/0.00202717/81.1064;L/0.205307/76.9305;L/0.821556/72.8088;L/3.25246/64.7483;L/7.21376/56.9658;L/12.6245/49.5023;L/19.4036/42.3987;L/27.4701/35.6959;L/36.7431/29.4347;L/47.1415/23.6562;L/58.5843/18.4012;L/70.9906/13.7107;L/84.2794/9.62544;L/98.3697/6.1865;L/113.18/3.43473;L/128.631/1.41106;L/144.639/0.156398;L/161.126/-0.288348;L/177.613/0.104763;L/193.626/1.30929;L/209.083/3.28456;L/223.902/5.98993;L/238.003/9.38472;L/251.304/13.4283;L/263.725/18.08;L/275.185/23.2991;L/285.601/29.045;L/294.894/35.2771;L/302.981/41.9547;L/309.782/49.037;L/315.216/56.4835;L/319.202/64.2535;L/321.658/72.3064;L/322.287/76.4262;L/322.504/80.6014";
    // 测试多边形坐标
    std::string polygonStr = "M/0.0102565/69.1651;L/19.019/51.4713;L/19.6427/5.25438;L/111.389/0.385824;L/112.778/24.1719;L/288.807/24.6385;L/288.255/129.985;L/242.72/131.399;L/221.009/162.01;L/138.096/166.188;L/116.982/128.833;L/113.55/100.971;L/65.9781/103.747;L/48.9573/79.3007;L/1.3638/64.406";

    auto ellipseCoords = parseCoordinates(ellipseStr);
    auto polygonCoords = parseCoordinates(polygonStr);

    std::cout << "椭圆坐标判断结果: " << (isEllipse(ellipseCoords) ? "是椭圆" : "不是椭圆") << std::endl;
    std::cout << "多边形坐标判断结果: " << (isEllipse(polygonCoords) ? "是椭圆" : "不是椭圆") << std::endl;

    return 0;
}

额外辅助判断技巧

除了拟合误差,你还可以加入以下判断来提升准确率:

  • 计算相邻点的角度变化:椭圆的角度变化是连续平滑的,而多边形会有多个角度突变的点(比如角度变化超过30度的点占比超过10%)
  • 检查点的分布密度:椭圆的采样点通常均匀分布在曲线上,而多边形的点集中在折线的转折处

这样结合多种判断逻辑,就能更准确地区分椭圆和多边形了。

备注:内容来源于stack exchange,提问作者Melika

火山引擎 最新活动