多段圆弧拟合离散点实现切线连续
使用多段圆弧来拟合一个由离散点组成的曲线,并且保证切线连续。也就是说,生成的每一段圆弧之间在连接点处必须有一阶导数连续,也就是切线方向相同。

点集分割
确保每个段的终点是下一段的起点,相邻段共享连接点,避免连接点位于数据点之间。
int totalPoints = envelopePoints.size();
int m = (totalPoints - 1) / numArcs; // 每段间隔数,点数为m+1for (int i = 0; i < numArcs; ++i) {int startIdx = i * m;int endIdx = (i == numArcs - 1) ? (totalPoints - 1) : (startIdx + m);std::vector<gp_Pnt> segmentPoints(envelopePoints.begin() + startIdx, envelopePoints.begin() + endIdx + 1);// 处理该段
}
圆弧拟合约束
问题描述
给定起点 $ S(S_x, S_y) $、终点 $E(E_x, E_y) $ 和起点处的切线方向 $ \mathbf{T}(t_x, t_y) $,求解经过 $ S $ 和 $ E $ 的圆弧,并保证在 $ S $ 处的切线方向为 $ \mathbf{T} $。

推导过程
1. 圆心位置约束
圆心 $ C(O_x, O_y) $ 必须位于 $ S $ 点处与切线方向 $ \mathbf{T} $ 垂直的直线上。设 $ \mathbf{N}(-t_y, t_x) $ 为 $ \mathbf{T} $ 的法线方向,则圆心可表示为:
C = S + s ⋅ N C = S + s \cdot \mathbf{N} C=S+s⋅N
即:
O x = S x − s ⋅ t y O_x = S_x - s \cdot t_y Ox=Sx−s⋅ty
O y = S y + s ⋅ t x O_y = S_y + s \cdot t_x Oy=Sy+s⋅tx
其中,$ s $ 是圆心到$ S $点沿法线方向的距离,正负号表示方向。
2. 圆心到终点的距离约束
圆心到终点 $ E $ 的距离必须等于到起点$ S $ 的距离:
( E x − O x ) 2 + ( E y − O y ) 2 = ( S x − O x ) 2 + ( S y − O y ) 2 \sqrt{(E_x - O_x)^2 + (E_y - O_y)^2} = \sqrt{(S_x - O_x)^2 + (S_y - O_y)^2} (Ex−Ox)2+(Ey−Oy)2=(Sx−Ox)2+(Sy−Oy)2
平方后得到:
( E x − O x ) 2 + ( E y − O y ) 2 = ( S x − O x ) 2 + ( S y − O y ) 2 (E_x - O_x)^2 + (E_y - O_y)^2 = (S_x - O_x)^2 + (S_y - O_y)^2 (Ex−Ox)2+(Ey−Oy)2=(Sx−Ox)2+(Sy−Oy)2
3. 代入圆心表达式
将 $ O_x $ 和 $ O_y $ 代入上式:
( E x − ( S x − s ⋅ t y ) ) 2 + ( E y − ( S y + s ⋅ t x ) ) 2 = ( S x − ( S x − s ⋅ t y ) ) 2 + ( S y − ( S y + s ⋅ t x ) ) 2 (E_x - (S_x - s \cdot t_y))^2 + (E_y - (S_y + s \cdot t_x))^2 = (S_x - (S_x - s \cdot t_y))^2 + (S_y - (S_y + s \cdot t_x))^2 (Ex−(Sx−s⋅ty))2+(Ey−(Sy+s⋅tx))2=(Sx−(Sx−s⋅ty))2+(Sy−(Sy+s⋅tx))2
简化右边:
( s ⋅ t y ) 2 + ( s ⋅ t x ) 2 = s 2 ( t x 2 + t y 2 ) = s 2 (s \cdot t_y)^2 + (s \cdot t_x)^2 = s^2 (t_x^2 + t_y^2) = s^2 (s⋅ty)2+(s⋅tx)2=s2(tx2+ty2)=s2
左边展开:
( E x − S x + s ⋅ t y ) 2 + ( E y − S y − s ⋅ t x ) 2 (E_x - S_x + s \cdot t_y)^2 + (E_y - S_y - s \cdot t_x)^2 (Ex−Sx+s⋅ty)2+(Ey−Sy−s⋅tx)2
= ( E x − S x ) 2 + 2 s ⋅ t y ( E x − S x ) + s 2 t y 2 + ( E y − S y ) 2 − 2 s ⋅ t x ( E y − S y ) + s 2 t x 2 = (E_x - S_x)^2 + 2s \cdot t_y (E_x - S_x) + s^2 t_y^2 + (E_y - S_y)^2 - 2s \cdot t_x (E_y - S_y) + s^2 t_x^2 =(Ex−Sx)2+2s⋅ty(Ex−Sx)+s2ty2+(Ey−Sy)2−2s⋅tx(Ey−Sy)+s2tx2
= ( E x − S x ) 2 + ( E y − S y ) 2 + s 2 ( t x 2 + t y 2 ) + 2 s [ t y ( E x − S x ) − t x ( E y − S y ) ] = (E_x - S_x)^2 + (E_y - S_y)^2 + s^2 (t_x^2 + t_y^2) + 2s [t_y (E_x - S_x) - t_x (E_y - S_y)] =(Ex−Sx)2+(Ey−Sy)2+s2(tx2+ty2)+2s[ty(Ex−Sx)−tx(Ey−Sy)]
由于 $ t_x^2 + t_y^2 = 1 $,上式进一步简化为:
= ( E x − S x ) 2 + ( E y − S y ) 2 + s 2 + 2 s [ t y ( E x − S x ) − t x ( E y − S y ) ] = (E_x - S_x)^2 + (E_y - S_y)^2 + s^2 + 2s [t_y (E_x - S_x) - t_x (E_y - S_y)] =(Ex−Sx)2+(Ey−Sy)2+s2+2s[ty(Ex−Sx)−tx(Ey−Sy)]
4. 建立方程求解 $ s $
将左边和右边代入距离约束方程:
( E x − S x ) 2 + ( E y − S y ) 2 + s 2 + 2 s [ t y ( E x − S x ) − t x ( E y − S y ) ] = s 2 (E_x - S_x)^2 + (E_y - S_y)^2 + s^2 + 2s [t_y (E_x - S_x) - t_x (E_y - S_y)] = s^2 (Ex−Sx)2+(Ey−Sy)2+s2+2s[ty(Ex−Sx)−tx(Ey−Sy)]=s2
化简得到:
( E x − S x ) 2 + ( E y − S y ) 2 + 2 s [ t y ( E x − S x ) − t x ( E y − S y ) ] = 0 (E_x - S_x)^2 + (E_y - S_y)^2 + 2s [t_y (E_x - S_x) - t_x (E_y - S_y)] = 0 (Ex−Sx)2+(Ey−Sy)2+2s[ty(Ex−Sx)−tx(Ey−Sy)]=0
解得:
s = ( E x − S x ) 2 + ( E y − S y ) 2 − 2 [ t y ( E x − S x ) − t x ( E y − S y ) ] s = \frac{(E_x - S_x)^2 + (E_y - S_y)^2}{-2 [t_y (E_x - S_x) - t_x (E_y - S_y)]} s=−2[ty(Ex−Sx)−tx(Ey−Sy)](Ex−Sx)2+(Ey−Sy)2
5. 计算圆心和半径
代入 $ s$ 的值,得到圆心坐标:
O x = S x − s ⋅ t y O_x = S_x - s \cdot t_y Ox=Sx−s⋅ty
O y = S y + s ⋅ t x O_y = S_y + s \cdot t_x Oy=Sy+s⋅tx
半径为:
R = ∣ s ∣ R = |s| R=∣s∣
6. 终点切线方向
圆弧在终点 $E $ 处的切线方向由圆心到$ E$ 的径向向量的垂直方向确定。径向向量为:
R = ( E x − O x , E y − O y ) \mathbf{R} = (E_x - O_x, E_y - O_y) R=(Ex−Ox,Ey−Oy)
切线方向为:
T E = ( R y , − R x ) \mathbf{T}_E = (R_y, -R_x) TE=(Ry,−Rx)
归一化后得到终点处的切线方向。
7. 代码实现
// 计算必须经过起点和终点的圆弧参数
double Sx = arc.start.X();
double Sy = arc.start.Y();
double Ex = arc.end.X();
double Ey = arc.end.Y();
double tx = arc.startTangent.X();
double ty = arc.startTangent.Y();double numerator = (Ex - Sx) * (Ex - Sx) + (Ey - Sy) * (Ey - Sy); // SE_length squared
double denominator = 2 * (ty * (Sx - Ex) - tx * (Sy - Ey));if (std::abs(denominator) < 1e-6) {// 处理直线段情况arc.center = gp_Pnt(0, 0, 0);arc.radius = 0;arc.endTangent = arc.startTangent;arcs.push_back(arc);currentTangent = arc.endTangent;continue;
}double s = numerator / denominator;
double Ox = Sx - s * ty;
double Oy = Sy + s * tx;
arc.center = gp_Pnt(Ox, Oy);
arc.radius = std::abs(s);
切线连续性处理
计算每段圆弧终点处的切线方向,并传递给下一段作为起点切线方向。
// 计算终点切线方向
gp_Vec radialVec(arc.center, arc.end);
if (radialVec.Magnitude() < 1e-6) {arc.endTangent = currentTangent; // 避免除零
} else {radialVec.Normalize();arc.endTangent = gp_Dir(radialVec.Y(), -radialVec.X(), 0);
}
currentTangent = arc.endTangent;
完整代码实现
#include <vector>
#include <gp_Pnt.hxx>
#include <gp_Dir.hxx>
#include <gp_Vec.hxx>
#include <cmath>struct ArcSegment {gp_Pnt start;gp_Dir startTangent;gp_Pnt end;gp_Dir endTangent;gp_Pnt center;double radius;
};std::vector<ArcSegment> fitArcsWithTangentContinuity(const std::vector<gp_Pnt>& envelopePoints, int numArcs) {std::vector<ArcSegment> arcs;if (envelopePoints.size() < 2 || numArcs < 1) return arcs;int totalPoints = envelopePoints.size();int m = (totalPoints - 1) / numArcs; // 每段间隔数,点数为m+1gp_Dir currentTangent;for (int i = 0; i < numArcs; ++i) {int startIdx = i * m;int endIdx = (i == numArcs - 1) ? (totalPoints - 1) : (startIdx + m);if (startIdx >= envelopePoints.size() || endIdx >= envelopePoints.size() || startIdx > endIdx) {break; // Invalid segment, skip}std::vector<gp_Pnt> segmentPoints(envelopePoints.begin() + startIdx, envelopePoints.begin() + endIdx + 1);if (segmentPoints.size() < 2) {continue; // Not enough points to define a segment}ArcSegment arc;arc.start = segmentPoints.front();arc.end = segmentPoints.back();// Determine start tangent directionif (i == 0) {// First segment: use direction from first two pointsgp_Vec initialVec(segmentPoints[0], segmentPoints[1]);if (initialVec.Magnitude() < 1e-6) {currentTangent = gp_Dir(1, 0, 0); // Default direction} else {initialVec.Normalize();currentTangent = gp_Dir(initialVec);}arc.startTangent = currentTangent;} else {arc.startTangent = currentTangent;}double tx = arc.startTangent.X();double ty = arc.startTangent.Y();// Calculate parameters for constrained circle passing through start and end pointsdouble Sx = arc.start.X();double Sy = arc.start.Y();double Ex = arc.end.X();double Ey = arc.end.Y();double numerator = (Ex - Sx) * (Ex - Sx) + (Ey - Sy) * (Ey - Ey);double denominator = 2 * (ty * (Sx - Ex) - tx * (Sy - Ey));if (std::abs(denominator) < 1e-6) {// Handle straight line case (infinite radius)arc.center = gp_Pnt(0, 0, 0); // Not meaningful for straight linearc.radius = 0;arc.endTangent = arc.startTangent;arcs.push_back(arc);currentTangent = arc.endTangent;continue;}double s = numerator / denominator;// Calculate circle parametersdouble Ox = Sx - s * ty;double Oy = Sy + s * tx;arc.center = gp_Pnt(Ox, Oy);arc.radius = std::abs(s);// Calculate end tangent directiongp_Vec radialVec(arc.center, arc.end);if (radialVec.Magnitude() < 1e-6) {arc.endTangent = currentTangent; // Avoid division by zero} else {radialVec.Normalize();arc.endTangent = gp_Dir(radialVec.Y(), -radialVec.X(), 0);}currentTangent = arc.endTangent;arcs.push_back(arc);}return arcs;
}
关键点说明
- **点集分割:**通过均匀分割确保相邻段共享连接点,避免位置不连续。
- **圆弧约束拟合:**使用几何约束直接计算必须经过起点和终点的圆弧,保证位置连续。
- **切线连续性:**通过传递切线方向确保每段圆弧在连接点处切线方向一致。
- **特殊情况处理:**当分母接近零时,处理为直线段,避免计算错误。
此方案在保证切线连续的同时,简化了计算逻辑,适用于大多数离散点拟合场景。
相关文章:
多段圆弧拟合离散点实现切线连续
使用多段圆弧来拟合一个由离散点组成的曲线,并且保证切线连续。也就是说,生成的每一段圆弧之间在连接点处必须有一阶导数连续,也就是切线方向相同。 点集分割 确保每个段的终点是下一段的起点,相邻段共享连接点,避免连接点位于数…...
烧结银:解锁金刚石超强散热潜力
烧结银:解锁金刚石超强散热潜力 在材料科学与热管理领域,金刚石凭借超高的热导率,被誉为 “散热之王”,然而,受限于其特殊的性质,金刚石在实际应用中难以充分发挥散热优势。而烧结银AS9335的出现&#x…...
【蓝桥杯】第十四届C++B组省赛
⭐️个人主页:小羊 ⭐️所属专栏:蓝桥杯 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~ 目录 试题A:日期统计试题B:01串的熵试题C:冶炼金属试题D:飞机降落试题E:接…...
企业级海外网络专线行业应用案例及服务商推荐
在全球化业务快速发展的今天,传统网络技术已难以满足企业需求。越来越多企业开始选择新型海外专线解决方案,其中基于SD-WAN技术的企业级海外网络专线备受关注。这类服务不仅能保障跨国数据传输,还能根据业务需求灵活调整网络配置。接下来我们…...
阿里云服务器安装docker以及mysql数据库
(1) 官方下载路径 官方下载地址: Index of linux/static/stable/x86_64/阿里云镜像地址: https://mirrors.aliyun.com/docker-ce/下载最新的 Docker 二进制文件:wget https://download.docker.com/linux/static/stable/x86_64/docker-20.10.23.tgz登录到阿里云服务…...
力扣经典算法篇-5-多数元素(哈希统计,排序,摩尔投票法)
题干: 给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的,并且给定的数组总是存在多数元素。 示例 1: 输入:nums [3,2,3] 输出&…...
axios介绍以及配置
Axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js 环境中进行 HTTP 请求。 一、特点与基本用法 1.特点 浏览器兼容性好:能在多种现代浏览器中使用,包括 Chrome、Firefox、Safari 等。支持 Promise API:基于 Prom…...
深入解析:HarmonyOS Design设计语言的核心理念
深入解析:HarmonyOS Design设计语言的核心理念 在当今数字化迅速发展的时代,用户对操作系统的体验要求越来越高。华为的HarmonyOS(鸿蒙操作系统)应运而生,旨在为用户提供全场景、全设备的智慧体验。其背后的设计语言—…...
大数据技术之Scala:特性、应用与生态系统
摘要 Scala 作为一门融合面向对象编程与函数式编程范式的编程语言,在大数据领域展现出独特优势。本文深入探讨 Scala 的核心特性,如函数式编程特性、类型系统以及与 Java 的兼容性等。同时,阐述其在大数据处理框架(如 Apache Spa…...
程序化广告行业(47/89):竞价指标剖析与流量对接要点
程序化广告行业(47/89):竞价指标剖析与流量对接要点 大家好!一直以来,我都希望能和大家一同深入探索程序化广告行业的奥秘,这也是我持续撰写这一系列博客的动力。今天,咱们接着来剖析程序化广告…...
dfs记忆化搜索刷题 + 总结
文章目录 记忆化搜索 vs 动态规划斐波那契数题解代码 不同路径题解代码 最长递增子序列题解代码 猜数字大小II题解代码 矩阵中的最长递增路径题解代码 总结 记忆化搜索 vs 动态规划 1. 记忆化搜索:有完全相同的问题/数据保存起来,带有备忘录的递归 2.记忆…...
vue2 全局封装axios统一管理api
在vue项目中,经常会使用到axios来与后台进行数据交互,axios丰富的api满足我们基本的需求。但是对于项目而言,每次都需要对异常进行捕获或者处理的话,代码会很繁重冗余。我们需要将其公共部分封装起来,比如异常处理&…...
大模型有哪些算法
大模型(Large-scale Models)通常指参数量大、架构复杂、在特定任务或领域表现出色的深度学习模型。这些模型的算法核心往往基于Transformer 架构及其变体,同时结合了大规模数据、硬件加速和优化技巧。以下是当前主流大模型及其核心算法的分类…...
【Linux】进程的详讲(中上)
目录 📖1.什么是进程? 📖2.自己写一个进程 📖3.操作系统与内存的关系 📖4.PCB(操作系统对进程的管理) 📖5.真正进程的组成 📖6.形成进程的过程 📖7、Linux环境下的进程知识 7.1 task_s…...
Python Cookbook-4.17 字典的并集与交集
任务 给定两个字典,需要找到两个字典都包含的键(交集),或者同时属于两个字典的键(并集)。 解决方案 有时,尤其是在Python2.3中,你会发现对字典的使用完全是对集合的一种具体化的体现。在这个要求中,只需要考虑键&am…...
优选算法的巧思之径:模拟专题
专栏:算法的魔法世界 个人主页:手握风云 目录 一、模拟 二、例题讲解 2.1. 替换所有的问号 2.2. 提莫攻击 2.3. Z字形变换 2.4. 外观数列 2.5. 数青蛙 一、模拟 模拟算法说简单点就是照葫芦画瓢,现在草稿纸上模拟一遍算法过程…...
【云服务器】在Linux CentOS 7上快速搭建我的世界 Minecraft 服务器搭建,并实现远程联机,详细教程
【云服务器】在Linux CentOS 7上快速搭建我的世界 Minecraft 服务器搭建,详细详细教程 一、 服务器介绍二、下载 Minecraft 服务端三、安装 JDK 21四、搭建服务器五、本地测试连接六、添加服务,并设置开机自启动 前言: 推荐使用云服务器部署&…...
文本分析(非结构化数据挖掘)——特征词选择(基于TF-IDF权值)
TF-IDF是一种用于信息检索和文本挖掘的常用加权算法,用于评估一个词在文档或语料库中的重要程度。它结合了词频(TF)和逆文档频率(IDF)两个指标,能够有效过滤掉常见词(如“的”、“是”等&#x…...
【JavaSE】小练习 —— 图书管理系统
【JavaSE】JavaSE小练习 —— 图书管理系统 一、系统功能二、涉及的知识点三、业务逻辑四、代码实现4.1 book 包4.2 user 包4.3 Main 类4.4 完善管理员菜单和普通用户菜单4.5 接着4.4的管理员菜单和普通用户菜单,进行操作选择(1查找图书、2借阅图书.....…...
命令模式介绍及应用案例
命令模式介绍 命令模式(Command Pattern) 是一种行为设计模式,它将请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,并且支持请求的排队、记录日志、撤销操作等功能。命令模式的核心思想是将“请求”封…...
多线程(多线程案例)(续~)
目录 一、单例模式 1. 饿汉模式 2. 懒汉模式 二、阻塞队列 1. 阻塞队列是什么 2. 生产者消费者模型 3. 标准库中的阻塞队列 4. 自实现阻塞队列 三、定时器 1. 定时器是什么 2. 标准库中的定时器 欢迎观看我滴上一篇关于 多线程的博客呀,直达地址…...
python笔记之函数
函数初探 python在要写出函数很简单,通过关键字def即可写出,简单示例如下 def add(a, b):return ab 以上即可以定义出一个简单的函数:接收两个变量a和b,返回a和b相加的结果,当然这么说也不全对,原因就是…...
合合信息大模型加速器2.0实测:当AI开始“读心术“与“考古“
凌晨三点的编辑部,我盯着屏幕上密密麻麻的财务报表和如天书般的专利图纸,感觉咖啡因正在大脑中上演"黑凤凰"式崩溃。这时,合合信息的AI助手突然开口:"您需要的是自动关联32个数据表,还是让模型直接生成…...
一个判断A股交易状态的python脚本
最近在做股票数据相关的项目,需要用到判断某一天某个时刻A股的状态,比如休市,收盘,交易中等,发动脑筋想了一下,这个其实还是比较简单的,这里我把实现方法分享给大家。 思路 当天是否休市 对于某…...
【go】数组与切片
数组Array 重点: 数组是值类型 数组的定义 var 数组名 [数组大小] 数据类型,例如var intArr [5] int,定义完数组后数组里的元素有默认值。、 数组的地址&intArr、&intArr[0]。 数组占据连续的内存。 int通常是4字节(32位&…...
hadoop集群配置-scp命令
scp 命令用于在不同主机之间复制文件或目录,在Hadoop集群配置中常用于将配置文件或相关资源分发到各个节点。以下是 scp 命令的基本用法和在Hadoop集群配置中的示例: 基本语法 scp [-r] [源文件或目录] [目标用户目标主机:目标路径] - -r :…...
闪记(FlashNote):让灵感快速成文的轻量级笔记工具
闪记(FlashNote):让灵感快速成文的轻量级笔记工具 你是否经常遇到这样的情况:桌面上放了一大堆的新建123.txt,想记录一个想法,应该是一键开个一个快捷键然后瞬间记录就自动保存了,现在的很多笔记…...
打车APP订单系统逻辑梳理与实现
一、逻辑分析 打车 APP 订单系统是整个打车业务的核心,负责处理从乘客下单到行程结束的一系列流程,涉及乘客、司机和平台三方的交互。 乘客端 下单:乘客打开 APP,输入上车地点、目的地,选择车型等信息后提交订单。此时…...
《大模型部署》——ollama下载及大模型本地部署(详细快速部署)
ollama Ollama 是一款开源跨平台的大语言模型(LLM)运行工具,旨在简化本地部署和管理 AI 模型的流程。 下载ollama 进入官网下载https://ollama.com/ 选择需要的系统下载 下载完成后直接进行安装 下载大模型 选择想要部署的模型&#…...
【蓝桥杯速成】| 17.完全背包(一维easy版)
题目一:爬楼梯 问题描述 57. 爬楼梯(第八期模拟笔试) 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬至多m (1 < m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢? 注意:给定 n 是一个正整…...
