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

news2024/10/5 17:25:22

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

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

图片

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

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

相关文章

2.23 Qt day4 事件机制+定时器事件+键盘事件+鼠标事件

思维导图&#xff1a; 做一个闹钟&#xff0c;在行编辑器里输入定闹钟的时间&#xff0c;时间到了就语音播报文本里的内容&#xff0c;播报五次 widget.h&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QDebug>//输出类 #include<…

JSON(javaScript Object Notation,Js对象标记)—我耀学IT

Json是一种轻量级的数据交换格式&#xff0c;目前使用非常广泛&#xff0c;是一种轻量级的数据交换格式。易于人阅读和编写&#xff0c;可以在多种语言之间进行数据交换 。同时也易于机器解析和生成 1.1json的值: 值可以是对象、数组、数字、字符串或者三个字面值(false、nul…

990-05产品经理:为什么商业价值是 IT 成功的关键

In today’s digital era, CIOs must shift(转移) their priorities from cost cutting to driving revenue(收入), and from process engineering to exploiting data if they want to achieve a set of broader business outcomes. Furthermore, understanding how to measur…

Mac OS 下载安装与破解Typora

文章目录 下载Typora破解Typora1. 进入安装目录2. 找到并打开Lincense文件3. 修改激活状态4. 重新打开Typora 下载Typora 官网地址&#xff1a;typora官网 下载最新Mac版&#xff0c;正常安装即可 破解Typora 打开typora,可以看到由于未激活&#xff0c;提示使用期限还剩下15…

09 呼吸灯

呼吸灯简介 呼吸灯实际展示的效果就是一个 LED 灯的亮度由亮到暗&#xff0c;再由暗到亮的变化过程&#xff0c;并且该过程是循环往复的&#xff0c;像呼吸一样那么有节奏。 呼吸灯通常是采用 PWM(Pulse Width Modulation&#xff0c;即脉冲宽度调制) 的方式实现&#xff0c;在…

超强随机短视频源码自带视频带支付源码

1.开启是否连续自动播放 2.支持手动点击看下一个 3.支持引流跳官方地址&#xff0c;产品地址&#xff0c;可以设置跳转地址 4.简洁大气&#xff0c;支持网站基础信息设置 5.支持设置定时多少时间弹广告 6.支持PC手机设置弹广告图片与点击后跳转链接 源码免费下载地址专业…

【Unity】双击C#脚本文件以单个文件打开(Visual Studio)、父类找不到、引用找不到、无法跳转等问题

问题&#xff1a;新安装一个Unity后&#xff0c;突然发现在工程里双击C#脚本&#xff0c;会一个一个打开&#xff0c;虽然也是用VS软件打开了&#xff0c;但是它无法被正确识别为Unity工程的C#脚本&#xff0c;也就是说所有命名空间无效&#xff0c;因为没关联上整个工程的解决…

OSCP靶场--Slort

OSCP靶场–Slort 考点(1.php 远程文件包含 2.定时任务提权) 1.nmap扫描 ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.178.53 -sV -sC -p- --min-rate 5000 Starting Nmap 7.92 ( https://nmap.org ) at 2024-02-24 04:37 EST Nmap scan report for 192.168.178.53 …

Windows安装PHP及在VScode中配置插件,使用PHP输出HelloWorld

安装PHP PHP官网下载地址(8.3版本)&#xff1a;PHP For Windows&#xff1a;二进制文件和源代码发布 点击下载.zip格式压缩包&#xff1a; 历史版本在Old archives中下载。推荐在Documentation download中下载官方文档&#xff0c;方便学习。 下载完成后在一个顺眼的地方解压压…

Spring Boot 项目集成camunda流程引擎

使用camunda开源工作流引擎有&#xff1a;通过docker运行、使用springboot集成、部署camunda发行包、基于源代码编译运行等多种方式。 其中&#xff0c;通过源代码编译运行的方式最为复杂&#xff0c;具体参考&#xff1a;https://lowcode.blog.csdn.net/article/details/1362…

vivado VHDL Objects、VHDL实体描述

VHDL对象包括&#xff1a;信号、变量、常量和运算符。 信号 在中声明VHDL信号&#xff1a; •体系结构声明部分&#xff1a;在该体系结构内的任何位置使用VHDL信号。 •一个块&#xff1a;在该块中使用VHDL信号。 使用<信号分配运算符分配VHDL信号。 signal sig1 : std…

企业计算机服务器中了malloxx勒索病毒怎么办?Malloxx勒索病毒解密数据恢复

网络技术的不断更新与发展&#xff0c;为企业的发展提供了强有力数据支撑&#xff0c;在企业的生产运营过程中&#xff0c;企业数据扮演着重要的角色&#xff0c;通过企业数据可以更好地总结调整企业的规划发展方向与日常数据统计&#xff0c;但利用网络技术的支撑就要防范网络…

软考41-上午题-【数据库】-关系代数运算3-外连接

一、外连接 连接的拓展&#xff0c;处理由于连接运算而缺失的信息。 1-1、回顾自然连接 1-2、左外连接 示例&#xff1a; 左边的表&#xff0c;数值是全的 1-3、右外连接 示例&#xff1a; 右边的表&#xff0c;数值是全的 1-4、全外连接 示例&#xff1a; 自然连接左外连接…

Sora:颠覆性AI视频生成工具

Sora是一款基于人工智能&#xff08;AI&#xff09;技术的视频生成工具&#xff0c;它彻底改变了传统视频制作的模式&#xff0c;为创作者提供了高效、便捷、高质量的视频内容生成方式。通过深度学习和自然语言处理等先进技术&#xff0c;Sora实现了从文字描述到视频画面的自动…

备考2024年汉字小达人:历年考题练一练-18道选择题

今天为大家分享汉字小达人的备考学习资源&#xff0c;通过参加没有报名费、人人可参加的汉字小达人比赛&#xff0c;激发孩子学习语文的兴趣&#xff0c;并且提升语文学习成绩。 汉字小达人的两轮比赛&#xff08;区级自由报名活动、市级活动&#xff09;的选择题主要有六种题型…

Mean Teacher的调研与学习

Mean Teacher的调研与学习 0 FQA:1 Mean Teacher1.1 Mean Teacher简介1.2 回顾Π-Model 和 Temporal Ensembling1.3 Mean Teacher 0 FQA: Q1&#xff0c;什么是Mean Teacher&#xff1f; MT的训练方式是怎样的&#xff1f;A1: Mean Teacher是一种基于一致性正则化的半监督学习…

【web】nginx+php环境搭建-关键点(简版)

一、nginx和php常用命令 命令功能Nginxphp-fpm启动systemctl start nginxsystemctl start php-fpm停止systemctl stop nginxsystemctl stop php-fpm重启systemctl restart nginxsystemctl restart php-fpm查看启动状态systemctl status nginxsystemctl status php-fpm开机自启…

读书笔记之《大加速》:为什么我们的生活节奏越来越快?

《大加速—为什么我们的生活越来越快》作者是 [英] 罗伯特科尔维尔&#xff0c;原作名: The Great Acceleration:How the World is getting Faster,Faster&#xff0c;2019年出版。 罗伯特科尔维尔(Robert Colvile),是一位政治评论员、作家、资深互联网媒体人。作品刊登在英国《…

《C++面向对象程序设计》✍学习笔记

C的学习重点 C 这块&#xff0c;重点需要学习的就是一些关键字、面向对象以及 STL 容器的知识&#xff0c;特别是 STL&#xff0c;还得研究下他们的一些源码&#xff0c;下面是一些比较重要的知识&#xff1a; 指针与引用的区别&#xff0c;C 与 C 的区别&#xff0c;struct 与…

【Spring MVC】处理器映射器:AbstractHandlerMethodMapping源码分析

目录 一、继承体系 二、HandlerMapping 三、AbstractHandlerMapping 四、AbstractHandlerMethodMapping 4.1 成员属性 4.1.1 MappingRegistry内部类 4.2 AbstractHandlerMethodMapping的初始化 4.3 getHandlerInternal()方法&#xff1a;根据当前的请求url&#xff0c;…