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

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

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

图片

需要注意的是,轮廓线多边形内不能有空洞,使用的不是常见的非零绕数规则(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://xiahunao.cn/news/2806617.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot3+Vue3 基础知识(持续更新中~)

bean 把方法的返回结果注入到ioc中 1: 2: 3: 组合注解封装 实战篇&#xff1a; 解析token&#xff1a; 统一携带token&#xff1a; 驼峰命名与下划线命名转换&#xff1a; NotEmpty!!! mybatis&#xff1a; PageHelper设置后&#xff0c;会将pageNum,和pageSize自己拼接…

代码随想录算法训练营第四一天 | 背包问题

目录 背包问题01背包二维dp数组01背包一维 dp 数组&#xff08;滚动数组&#xff09;分割等和子集 LeetCode 背包问题 01背包 有n件物品和一个最多能背重量为 w 的背包&#xff0c;第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。每件物品只能用一次&#x…

Python urllib、requests、HTMLParser

HTTP协议 HTTP 协议&#xff1a;一般指HTTP(超文本传输)协议。 HTTP是为Web浏览器和Web服务器之间的通信而设计的&#xff0c;基于TCP/IP通信协议嘞传递数据。 HTTP消息结构 客户端请求消息 客户端发送一个HTTP请求到服务器的请求消息包括以下格式 请求行(request line)请求…

【前端素材】推荐优质后台管理系统Start Admin平台模板(附源码)

一、需求分析 后台管理系统是一种用于管理网站、应用程序或系统的工具&#xff0c;它通常作为一个独立的后台界面存在&#xff0c;供管理员或特定用户使用。下面详细分析后台管理系统的定义和功能&#xff1a; 1. 定义 后台管理系统是一个用于管理和控制网站、应用程序或系统…

Linux——静态库

Linux——静态库 静态库分析一下 ar指令生成静态库静态库的使用第三方库优化一下 gcc -I(大写的i) -L -l(小写的l)&#xff0c;头文件搜索路径&#xff0c;库文件搜索路径&#xff0c;连接库 今天我们来学习静态库的基本知识。 静态库 在了解静态库之前&#xff0c;我们首先来…

复旦大学MBA聚劲联合会:洞见智慧,拓宽思维格局及国际化视野

12月2日&#xff0c;“焕拥时代 俱创未来”聚劲联合会俱创会年度盛典暨俱乐部募新仪式圆满收官。16家复旦MBA俱乐部、200余名同学、校友、各界同仁齐聚复旦管院&#xff0c;一起在精彩纷呈的圆桌论坛里激荡思想&#xff0c;在活力四射的俱乐部风采展示中凝聚力量。      以…

CSS 的圆角矩形

CSS 的圆角矩形 通过 border-radius 属性使矩形边框带圆角效果成为圆角矩形 语法&#xff1a;border-radius: length; length 是内切圆的半径&#xff0c;其数值越大, 弧线越明显 border-radius 属性值描述length定义圆角的形状%以百分比定义圆角的形状 生成圆形 让 border-…

高和汽车停工停产,创始人丁磊终于发话了!2024的冷门项目,投入小,但是真的很赚钱!

高和创始人丁磊站在停产停工的工厂呢&#xff0c; 环顾冷清❄️的四周&#xff0c;眉头紧锁&#x1f623;&#xff0c; 停顿片刻后对旁边同样愁眉苦脸的员工说道&#xff1a; 非常抱歉&#xff0c;因为经营的失误&#xff0c;面临了停产停工的窘境。 在互联网&#x1f517;、物…

C/C++的内存管理(2)——new与delete的内核与本质

内存管理 operator new 与 operator delete函数回看new与delete的实现内置类型自定义类型 常见面试题 我们已经知道了new与delete的用法及其好处&#xff0c;发现它似乎与C语言中的动态内存开辟的函数&#xff08;malloc/calloc/realloc&#xff09;不同 在这里我们特别指出&am…

二进制部署k8s集群之cni网络插件

目录 k8s的三种网络模式 pod内容器之间的通信 同一个node节点中pod之间通信 不同的node节点的pod之间通信 flannel网络插件 flannel的三种工作方式 VxLAN host-GW UDP Flannel udp 模式 Flannel VXLAN 模式 flannel插件的三大模式的总结 calico网络插件 k8s 组网…

命令绕过 [安洵杯 2019]easy_web1

打开题目 打开题目在URL处看到cmd&#xff0c;本能的直接用系统命令ls 发现被过滤了。又注意到imgTXpVek5UTTFNbVUzTURabE5qYz0似乎是一串base64 拿去base64解码 再hex解码一次得到555.png 再将其hex加密 base64加密 反向推出index.php的payload:?imgTmprMlJUWTBOalUzT0RK…

通过Colab部署Google最新发布的Gemma模型

Gemma的简单介绍 Gemma 是一系列轻量级、最先进的开放式模型&#xff0c;采用与创建 Gemini 模型相同的研究和技术而构建。 Gemma 由 Google DeepMind 和 Google 的其他团队开发&#xff0c;其灵感来自 Gemini&#xff0c;其名称反映了拉丁语 gemma&#xff0c;意思是“宝石”…

Promise相关理解记录

一、Promise基础定义相关 Promise是一个构造函数&#xff0c;调用时需要使用new关键字 Promise是解决回调地狱的一种异步解决方式 Promise有三个状态&#xff1a;pending(进行中)、fulfilled(成功)、rejected(失败) Promise的状态只会从 pending→fulfilled 或者 pending→…

Frida javascript hook 检测设备信息获取等

对 Android 应用进行 hook 常见的有 Xposed、Frida 等&#xff0c;Xposed 有时候可能不尽人意&#xff0c;或许您可以试试 Frida ~ frida -U -f com.primer.gamecerter -l hookStartActivity.js TODO 后续是否可以对检测数据&#xff08;堆栈、类名、方法名、参数、返回值&…

如何控制负压电源芯片的EN

上文我们探讨了如何将负压控制信号转变成正压&#xff0c;这样的信号通常是由负压的芯片产生的&#xff0c;比如负电压的电源管理芯片的power good信号&#xff0c;那么负压芯片应该由谁来控制呢&#xff1f;如何实现对负压电源管理芯片的有效控制呢&#xff1f;   举例&…

Yolov9全文翻译!

Yolo v9全文翻译 论文链接&#xff1a;&#x1f47f; YOLOv9: Learning What You Want to Learn Using Programmable Gradient Information 代码链接&#xff1a;&#x1f47f; https://github.com/WongKinYiu/yolov9/tree/main 大量图片来袭&#xff01;

Linux课程三课---Linux开发环境的使用(yum的相关)

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

2023全新UI千月影视APP源码 | 前后端完美匹配、后端基于ThinkPHP框架

应用介绍 本文来自&#xff1a;2023全新UI千月影视APP源码 | 前后端完美匹配、后端基于ThinkPHP框架 - 源码1688 简介&#xff1a; 2023全新UI千月影视APP源码 | 前后端完美匹配、后端基于thinkphp框架 图片&#xff1a;

使用向量数据库pinecone构建应用02:检索增强生成RAG

Building Applications with Vector Databases 下面是这门课的学习笔记&#xff1a;https://www.deeplearning.ai/short-courses/building-applications-vector-databases/ Learn to create six exciting applications of vector databases and implement them using Pinecon…

提升装备制造企业竞争力:2023年CRM选型与应用完全解读

在加快产业转型升级的大背景下&#xff0c;高端装备制造业既面临机遇也面临挑战。随着公司规模的不断壮大&#xff0c;再加上装备制造业营销体系及服务体系管理体系的复杂性&#xff0c;一些问题逐渐暴露出来&#xff0c;装备制造业企业需要根据自身业务需求和管理流程选择合适…