给定一个边与边可能相交的多边形,求它的轮廓线

大家好,我是前端西瓜哥。

最近遇到一个需求,给定一个多边形(边与边可能相交),求这个多边形的轮廓线

图片

需要注意的是,轮廓线多边形内不能有空洞,使用的不是常见的非零绕数规则(nonzero)以及奇偶规则(odd-even)。

整体思路

  1. 计算多边形各边的交点,求出一个有多边形点和交点信息的邻接表。

  2. 从最下方的点开始,找出与其相邻节点中夹角最小的点保存到路径中,不断重复这个行为,直到点又回到起点位置。这里部分借鉴了凸包算法的其中一种叫做 Jarvis步进法 的解法。

原理很简单,就是代码太多,容易写错,需要多调试。

图片

演示 demo

为了验证算法的正确性,我用 Canvas 写了个的简单交互 demo。

效果演示:

图片

项目地址:

https://github.com/F-star/polygon-alg

Demo 地址:

https://f-star.github.io/polygon-alg/

下面我们看具体实现。

预处理

第一步是预处理。

目标多边形会使用点数组表示,比如:

const points = [{ x: 0, y: 0 },{ x: 6, y: 0 },{ x: 0, y: 10 },{ x: 6, y: 10 },
];

然后我们做去重,如果连续的多个点的位置 "相同",其实就等价于一个,保留一个就好。

不然后面找路径的时候,会出现零向量的计算导致报错。

function dedup(points: Point[]) {const newPoints: Point[] = [];const size = points.length;for (let i = 0; i < size; i++) {const p = points[i];const nextP = points[(i + 1) % size];if (p.x !== nextP.x || p.y !== nextP.y) {newPoints.push(p);}}return newPoints;
}

接着我们需要基于这个点数组,计算邻接表。

邻接表是一种表示图(Graph)的数据结构,记录每个点相邻的点有哪些。

下面我们会以这个 “8” 字形多边形为例,进行讲解。

图片

观察图示,可以得邻接表为:

[/* 0 */ [3, 1], // 表示 0 和 3、1 相连/* 1 */ [0, 2],/* 2 */ [1, 3],/* 3 */ [2, 0],
];

求初始邻接表的算法实现为:

// 求多边形的邻接表,size 为多边形点的数量
function getAdjList(size: number) {const adjList: number[][] = [];for (let i = 0; i < size; i++) {const left = i - 1 < 0 ? size - 1 : i - 1;const right = (i + 1) % size;adjList.push([left, right]);}return adjList;
}

需要求解的轮廓线多边形的点不一定是目标多边形上的点,也有可能是交点

所以我们首先要做的是 求出目标多边形上的所有交点,并更新邻接表,得到一个额外带有交点信息的多边形邻接表

图片

我们来看看具体要怎么实现。

求交点以及更新邻接表

这里需要一个求两线段交点的算法。刚好我写过,思路是解二元一次方程组,请看这篇文章:《解析几何:计算两条线段的交点》

用法为:

getLineSegIntersection({ x: 1, y: 1 }, { x: 4, y: 4 },{ x: 1, y: 4 }, { x: 4, y: 1 }
);
// { x: 2.5, y: 2.5 }

我们需要遍历多边形的所有边,计算其和其他不相邻边的交点。

const size = points.length;for (let i = 0; i < size - 2; i++) {const line1Start = points[i];const line1End = points[i + 1];let j = i + 2;for (; j < size; j++) {const line2EndIdx = (j + 1) % size;if (i === line2EndIdx) {// 相邻点没有计算交点的意义continue;}const line2Start = points[j];const line2End = points[line2EndIdx];const crossPt = getLineSegIntersection(line1Start,line1End,line2Start,line2End);// 找到一个交点if (crossPt) {// ... 更新邻接表// ...}}
}

为记录新的交点在哪四个点之间,我们要维护一个表。

它的 key 代表某条线段,value 为一个有序数组,记录落在该线段上的点,以及它们到线段起点的距离。该数组按距离从小到排序。

// [某条线]: [到线起点的距离, 在 points 中的索引值]
// 如:{ '2-3', [[0, 2], [43, 5], [92, 3]] }
const map = new Map<string, [number, number][]>();

线段 1-2 初始化时,表为

{‘1-2’: [[0, 1], // 点 1,距离起点 0[96, 2], // 点 2,距离起点 96]
}

边 1-2 和 3-0 计算得到一个交点(我们记为点 4)。把交点存到 crossPts 数组中。

接着求交点 4 在 1-2 中距离起点(即点 1)的距离,基于它判断落在 1-2 中哪两个点之间。结果是在点 1 和 点 2 之间,更新这两个点的邻接点数组,将其中的 1 和 2 替换为 5

1: [0, 2] --> 1: [0, 4]
2: [1, 3] --> 2: [4, 3]

最后是更新 map 表:

{‘1-2’: [[0, 1], // 点 1,距离起点 0[0, 4], // 点 4,距离起点 40[96, 2], // 点 2,距离起点 96]
}

另一条相交边 3-0 同理。

代码实现:

// [某条线]: [到线起点的距离, 在 points 中的索引值]
// 如:{ '2-3', [[0, 2], [43, 5], [92, 3]] }
const map = new Map<string, [number, number][]>();
const crossPts: Point[] = [];
const size = points.length;// ...
if (crossPt) {crossPts.push(crossPt);const crossPtAdjPoints: number[] = [];const crossPtIdx = size + crossPts.length - 1;/************ 计算 line1Dist 并更新 line1 两个点对应的邻接表 ********/{const line1Key = `${i}-${i + 1}`;if (!map.has(line1Key)) {map.set(line1Key, [[0, i],[distance(line1Start, line1End), i + 1],]);}const line1Dists = map.get(line1Key)!;// 计算相对 line1Start 的距离const crossPtDist = distance(line1Start, crossPt);// 看看在哪两个点中间const [_left, _right] = getRange(line1Dists.map((item) => item[0]),crossPtDist);const left = line1Dists[_left][1];const right = line1Dists[_right][1];crossPtAdjPoints.push(left, right);// 更新邻接表const line1StartAdjPoints = adjList[left];replaceIdx(line1StartAdjPoints, left, crossPtIdx);replaceIdx(line1StartAdjPoints, right, crossPtIdx);const line1EndAdjPoints = adjList[right];replaceIdx(line1EndAdjPoints, left, crossPtIdx);replaceIdx(line1EndAdjPoints, right, crossPtIdx);// 更新 map[line1Key] 数组line1Dists.splice(_right, 0, [crossPtDist, crossPtIdx]);}/************ 计算 line2Dist 并更新 line2 两个点对应的邻接表 ********/{const line2Key = `${j}-${line2EndIdx}`;// ...这里和上面一样的,读者感兴趣可以把这两段代码复用为一个方法}// 更新邻接表adjList.push(crossPtAdjPoints);
}

步进法找路径

上面我们得到了带交点的多边形邻接表,必要的点的数据都准备好了,下一步就是一从一个点出发走出一条多边形的路径。

(1)取左下角点作为起点

找顶点(不包括交点)中最靠下的点,如果有多个,取最靠左的。这个点一定是轮廓多边形的一个点。

(2)步进,取角度最小的邻接点为路径的下一个点

计算当前点到上一个点的向量,和当前点到其他邻接点相邻点向量逆时针夹角。找出其中夹角最小的邻接点,作为下一个点,不断步进,直到当前点为路径起点。

对于起点,它没有前一个点,用一个向右向量  (1, 0) 参与计算。

图片

const allPoints = [...points, ...crossPts];// 1. 找到最下边的点,如果有多个 y 相同的点,取最左边的点
let bottomPoint = points[0];
let bottomIndex = 0;
for (let i = 1; i < points.length; i++) {const p = points[i];if (p.y > bottomPoint.y || (p.y === bottomPoint.y && p.x < bottomPoint.x)) {bottomPoint = p;bottomIndex = i;}
}const outlineIndices = [bottomIndex];// 2. 遍历,找逆时针的下一个点
const MAX_LOOP = 9999;
for (let i = 0; i < MAX_LOOP; i++) {const prevIdx = outlineIndices[i - 1];const currIdx = outlineIndices[i];const prevPt = allPoints[prevIdx];const currPt = allPoints[currIdx];const baseVector =i == 0? { x: 1, y: 0 } // 对于起点,它没有前一个点,用向右的向量: {x: prevPt.x - currPt.x,y: prevPt.y - currPt.y,};const adjPtIndices = adjList[outlineIndices[i]];let minRad = Infinity;let minRadIdx = -1;for (const index of adjPtIndices) {if (index === prevIdx) {continue;}const p = allPoints[index];const rad = getVectorRadian(currPt.x, currPt.y, p.x, p.y, baseVector);if (rad < minRad) {minRad = rad;minRadIdx = index;}}if (minRadIdx === outlineIndices[0]) {break; // 回到了起点,结束}outlineIndices.push(minRadIdx);
}
if (outlineIndices.length >= MAX_LOOP) {console.error(`轮廓多边形计算失败,超过最大循环次数 ${MAX_LOOP}`);
}// outlineIndices 为我们需要的轮廓线多边形

这里有个求两向量夹角的方法要实现,这里不具体展开了。

简单来说就是通过点积公式计算夹角,但夹角只在 0 到 180 之间,这里需要再利用叉积的特性判断顺时针还是逆时针,将顺时针的夹角用 360 减去。

结尾

算法的整体思路大概就是这样。

这里有几个优化点。

首先判断大小的场景可进行优化,比如求距离时使用了开方,其实没必要开方。

因为 a^2 < b^2 是可以推导出 a < b 的,所以可直接对比距离的平方,我这里是为了让读者方便理解,故意简化了。对比夹角的大小同理,可改为对比投影加夹角方向。

此外还有一些边缘情况没有测试和处理。

比如多个交点的位置是 “相同” 的,最好做一个合并操作(否则在一些非常特定的场景可能会有问题)。

我是前端西瓜哥,欢迎关注我,学习更多平面解析几何知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/701132.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java+SpringBoot,打造极致申报体验

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

2024全国水科技大会暨流域水环境治理与水生态修复论坛(六)

论坛召集人 冯慧娟 中国环境科学研究院流域中心研究员 刘 春 河北科技大学环境与工程学院院长、教授 一、会议背景 为深入贯彻“山水林田湖是一个生命共同体”的重要指示精神&#xff0c;大力实施生态优先绿色发展战略&#xff0c;积极践行人、水、自然和谐共生理念&…

opencascade在vs和qt下改变视图方向和设置线框模式

一.改变视图方向&#xff08;以顶部视图为例&#xff09; 1.在qt的界面代码中设置好 2.在view.h中设置好槽函数 3.在lzzcad.cpp中设置槽与信号的连接&#xff0c;并在工具栏上显示 4.在view.cpp中给出函数实现 5.给出快捷键实现方式 二.设置线框模式 同上&#xff0c;加入函数…

[深度学习]yolov9+deepsort+pyqt5实现目标追踪

【YOLOv9DeepSORTPyQt5追踪介绍】 随着人工智能技术的飞速发展&#xff0c;目标追踪在视频监控、自动驾驶等领域的应用日益广泛。其中&#xff0c;YOLOv9作为先进的目标检测算法&#xff0c;结合DeepSORT多目标追踪算法和PyQt5图形界面库&#xff0c;能够为用户提供高效、直观…

python-可视化篇-简单-条形图输出主要省份GDP排名情况

条形图输出主要省份GDP排名情况 代码 gdp广东:97277.77:107671.07 江苏:92595.40:99631.52 山东:76469.70:71067.5 浙江:56197.00:62353 河南:48055.90:54259.2 四川:40678.10:46615.82 湖北:39366.60:45828.31 湖南:36425.78:39752.12 河北:36010.30:35104.5 福建:35804.04:…

windows安装 RabbitMQ

首先打开 RabbitMQ 官网&#xff0c;点击 Get Started(开始) 点击 Download Installation(下载安装)。 这里提供了两种方式进行安装&#xff0c;我们使用第二种方法。 使用 chocolatey以管理用户身份使用官方安装程序 往下滑&#xff0c;第二种方法需要 Erlang 的依赖&#x…

mongoose httpserver浅析

文章目录 前言一、结构体及其功能二、函数MG_LOGmg_http_listenmg_mgr_poll question参考链接 前言 mongoose是一款基于C/C的网络库&#xff0c;可以实现TCP, UDP, HTTP, WebSocket, MQTT通讯。mongoose是的嵌入式网络程序更快、健壮&#xff0c;易于实现。 mongoose只有mong…

qt波位图

1&#xff0c;QPainter 绘制&#xff0c;先绘制这一堆蓝色的东西, 2&#xff0c;在用定时器&#xff1a;QTimer&#xff0c;配合绘制棕色的圆。用到取余&#xff0c;取整 #pragma once#include <QWidget> #include <QPaintEvent>#include <QTimer>QT_BEGIN_…

LangChain Agent v0.2.0简明教程 (上)

快速入门指南 – LangChain中文网 langchain源码剖析系列课程 九天玩转Langchain! 1. LangChain是什么2. LangChain Expression Language (LCEL)Runnable 接口3. Model I/O3.1 Prompt Templates3.2 Language Model3.3 Output ParsersUse case(Q&A with RAG)1. LangChain…

【踩坑】PyTorch中指定GPU不生效和GPU编号不一致问题

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 指定GPU不生效问题 解释&#xff1a;就是使用os.environ["CUDA_VISIBLE_DEVICES"] "1"后&#xff0c;后面使用起来仍然是cuda0. 解决&#xff1a;在最开头就使用 import os os.environ[&…

sentinel整合nacos在gateway中实现限流

sentinel整合nacos在gateway中实现限流 一、应用层面完成网关整合nacos和sentinel实现限流 前沿 启动nacos与sentinel的jar的启动&#xff0c;这里不细讲 sentinel官网 https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5 sentinel 下载地址 https://github.com/…

车载电子电器架构 —— 电气架构开发计划

车载电子电器架构 —— 电气架构开发计划 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明…

实现KingSCADA系统按钮弹窗出现位置随点击位置变化。

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 在用KingSCADA做项目时&#xff0c;当我们点击不同的控制按钮&#xff0c;都可以弹出对应的控制弹窗。 在常规不做设置的情况下弹窗都是出现在固定的位置&#xff0c;要么一直出现在左上角&#xff0c;要么一直出现在…

【Java】常用实用类及java集合框架(实验六)

目录 一、实验目的 二、实验内容 三、实验小结 3.1 常用实用类 3.2 Java集合框架 一、实验目的 1、掌握java常用类的方法 2、掌握String类与数值类型数据的相互转化 3、掌握正则表达式的应用 4、掌握常用集合的创建和操作方法 二、实验内容 1、菜单的内容如下&#x…

南邮概率统计与随机过程练习册答案

**南京邮电大学** **概率统计与随机过程练习册答案简介** 本文档是一份精心整理的南京邮电大学概率统计与随机过程课程的练习册答案集。它旨在为学习该课程的学生提供一个详尽的解题参考,帮助他们更好地理解和掌握概率论与统计学的基本概念和方法。 **内容概览:** - **章节…

抖音视频评论数据提取软件|抖音数据抓取工具

一、开发背景&#xff1a; 在业务需求中&#xff0c;我们经常需要下载抖音视频。然而&#xff0c;在网上找到的视频通常只能通过逐个复制链接的方式进行抓取和下载&#xff0c;这种操作非常耗时。我们希望能够通过关键词自动批量抓取并选择性地下载抖音视频。因此&#xff0c;为…

git 拉取远程分支到本地

背景&#xff1a; 我的 github 上的远程仓库上除了 main 分支外还提交了好几个别的分支&#xff0c;现在我换机器了&#xff0c;git clone 原仓库后只剩 main 分支&#xff0c;我要把其他分支拉下来到本地。 1. 查看所有远程remote分支 git branch -r 比如我这里&#xff1…

深入浅出:探究过完备字典矩阵

在数学和信号处理的世界里&#xff0c;我们总是在寻找表达数据的最佳方式。在这篇博文中&#xff0c;我们将探讨一种特殊的矩阵——过完备字典矩阵&#xff0c;这是线性代数和信号处理中一个非常有趣且实用的概念。 什么是过完备字典矩阵&#xff1f; 首先&#xff0c;我们先…

认识K8S

K8S K8S 的全称为 Kubernetes (K12345678S) 是一个跨主机容器编排工具 作用 用于自动部署、扩展和管理“容器化&#xff08;containerized&#xff09;应用程序”的开源系统。 可以理解成 K8S 是负责自动化运维管理多个容器化程序&#xff08;比如 Docker&#xff09;的集群…

unity学习(40)——创建(create)角色脚本(panel)——UI

1.点击不同的头像按钮&#xff0c;分别选择职业1和职业2&#xff0c;create脚本中对应的函数。 2.调取inputfield中所输入的角色名&#xff08;限制用户名长度为7字符&#xff09;&#xff0c;但愿逆向的服务器可以查重名&#xff1a; 3.点击头衔&#xff0c;显示选择的职业&a…