🎯导读:本文介绍了多边形像素化与扫描线化的基本概念及其在图形处理中的重要应用。通过一种优化算法------Y向连贯性算法,解决了传统方法中多次求交点和排序的问题,提高了效率。该算法利用直线方程信息跟踪交点变化,并通过维护边表和有效边集合来减少不必要的计算。提供的Java代码示例展示了如何实现这一算法,包括构建边表、管理有效边集合以及生成扫描线列表的具体步骤。通过此方法,可以有效地实现多边形的快速渲染。
多边形的像素化和扫描线化
-
多边形的像素化是将多边形转换为像素表示的过程。这意味着将多边形的形状在像素级别上进行近似和表示,以便在屏幕或图像中显示出来。在像素化过程中,需要考虑多边形的顶点位置、边的走向以及与像素网格的关系,以确定哪些像素应该被着色来表示多边形。
-
扫描线化则是一种常用于多边形绘制的算法。扫描线化是像素化的优化版本,像素化会导致一个多边形需要用大量的像素点来进行描述,占用内存较高,性能不好。扫描线化则是将处于同一行或者同一列的像素点拼接起来表示。
【应用场景】
多边形的像素化和扫描线化是图形处理和可视化的基础技术,在多个领域有重要应用。
- 在计算机图形学、游戏开发中用于渲染逼真图像;
- 在图像处理和特效制作中控制操作范围和效果;
- 在地理信息系统绘制地图;
- 在工业设计和 CAD 软件中设计产品;
- 对众多图形相关应用至关重要。
问题介绍
给定一个多边形,可能是凸多边形,也可能是凹多边形,现需要生成一系列线条将多边形描述出来,示例如下图 
原始方法
遇到这个问题,大家首先想到的方法可能是:使用一系列的竖线来和多边形进行相交,得到几个交点,然后将交点按照z轴坐标值进行升序排序,最后再以两个点为一组来形成扫描线。这样确实很容易理解,但是性能不好,因为需要多次求交点和多次对交点进行排序。
为了优化这个过程,使用连贯性算法进行优化
连贯性算法
避免多次求交点
如何避免多次求交点呢?其实非常简单,就是利用直线函数 y=kx+b 的信息即可,例如x每增加1,y就增加 k 。如下面的例子,假如一开始就知道P点的坐标,那么线段与扫描线1、扫描线2的交点并不需要再去用直线相交公式计算,直接使用 y=kx+b 即可得到

如何避免多次排序
如下图所示,当扫描线在x=[0,10]之间移动时,永远只有上下两个交点,且P2永远在P1上面,那只要x在[0,10]之间移动时,只需要根据直线的表达式来对两个点的坐标进行更新即可,不需要排序两个点。当x>10之后,有新的边和扫描线相交,这时候会出现更多的交点,此时才需要对交点进行排序,大大减少了排序的次数

算法流程
- 维护边表:首先维护一个边表,遍历多边形的每一条边,将边放到对应的桶中;
- 扫描线生成 :然后维护一个有效边集合,将y开始向右移动,每次移动一个长度单位,在本文中移动1。y的初始值是多边形所有点y坐标值的最小值。在移动的过程中,还需要做如下三件事:
- 当前y坐标值是否有边失效?当扫描线和边不相交时,边就失效,将其从有效边集合中移除;
- 是否有新的有效边加入?随着扫描线的移动,当扫描线会接触到新的变时,需要将其添加到有效边集合中,这时候会产生新的交点,注意此时需要重新对有效边集合的边进行排序了;
- 扫描线每沿着y轴移动距离 deltaY,z 增加 k*deltaY 。
给定一个多边形如下图所示,该多边形有7个点,7条边。下面将使用该多边形作为例子,进行算法的流程演示。

维护边表
如何判定一条边要存储到哪个桶中? 边的两个点分别为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( y 1 , z 1 ) (y_1 , z_1) </math>(y1,z1)、 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( y 2 , z 2 ) (y_2 , z_2) </math>(y2,z2), <math xmlns="http://www.w3.org/1998/Math/MathML"> m i n Y = m i n ( y 1 , y 2 ) minY = min(y1 , y2) </math>minY=min(y1,y2),将边放到 <math xmlns="http://www.w3.org/1998/Math/MathML"> y = m i n Y y=minY </math>y=minY 对应的桶中。例如 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 6 P 5 P_6P_5 </math>P6P5就放在 <math xmlns="http://www.w3.org/1998/Math/MathML"> y = 2 y=2 </math>y=2 的桶中。
在桶中,边需要存储什么信息呢? 每条边需要存储三个信息
- 当前y坐标值对应线段的z坐标值,对于边 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 6 P 7 P_6P_7 </math>P6P7,当 <math xmlns="http://www.w3.org/1998/Math/MathML"> y = 2 y=2 </math>y=2 时, <math xmlns="http://www.w3.org/1998/Math/MathML"> z = 3 z=3 </math>z=3
- 边的最大 <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y 坐标值,如边 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 6 P 7 P_6P_7 </math>P6P7 的最大 <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y坐标值为7
- 边的斜率 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k ,即 <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y增加 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 时, <math xmlns="http://www.w3.org/1998/Math/MathML"> z z </math>z 增加多少 
扫描线生成
如何排序? 首先优先按照z的值来升序排序,对于z相同的边,再按照k升序排序
扫描线的生成过程和有效边表的更新流程如下图所示:
代码实现
【实体类:Edge,用于在边表和有效边集合中存储数据】
java
package com.dam.entity.sanLine;
/**
* @Author dam
* @create 2023/9/15 14:38
*/
public class Edge {
public double z;
public double yMax;
/**
* y加一时,z的增量
*/
public double k;
public Edge(double z, int yMax, double k) {
this.z = z;
this.yMax = yMax;
this.k = k;
}
}
【针对零件点集的纵向扫描线生成方法】
java
/**
* 扫描线生成,使用连贯性算法
*
* @param part
*/
private void vScanLineConstruct1(Part part) {
List<Integer> vSLineList = new ArrayList<>();
// 边表
HashMap<Integer, List<Edge>> edgeTable = new HashMap<>();
/*
边表构造
遍历每一条边,将边的信息放入到相应的桶中,即放入边的两点中y值较小的那个桶中
*/
for (int i = 0; i < part.offSetOuterContour.size(); i++) {
double[] pointI = part.offSetOuterContour.get((i) % part.offSetOuterContour.size());
double[] pointJ = part.offSetOuterContour.get((i + 1) % part.offSetOuterContour.size());
// 两个点中较小的y
int yMin = Math.min((int) Math.round(pointI[0]), (int) Math.round(pointJ[0]));
int yMax = Math.max((int) Math.round(pointI[0]), (int) Math.round(pointJ[0]));
if (yMin == yMax) {
// 对于垂直线,不需要添加到边表中
continue;
}
double z = (int) Math.round(pointI[0]) < (int) Math.round(pointJ[0]) ? pointI[1] : pointJ[1];
Edge edge = new Edge((int) Math.round(z), yMax, MathUtil.getKOfLine(pointI[0], pointI[1], pointJ[0], pointJ[1]));
if (!edgeTable.containsKey(yMin)) {
List<Edge> edgeList = new ArrayList<>();
edgeList.add(edge);
edgeTable.put(yMin, edgeList);
} else {
edgeTable.get(yMin).add(edge);
}
}
/*
扫描线构造
*/
List<Edge> activeEdgeList = new ArrayList<>();
for (int y = 0; y < part.pixelNumInWidDirection; y++) {
/// 判断是否有无效边需要移除
int i = 0;
while (i < activeEdgeList.size()) {
Edge edge = activeEdgeList.get(i);
if (edge.yMax == y) {
// 当边的yMax==y,该边开始无效,移除边
activeEdgeList.remove(i);
} else {
i++;
}
}
/// 判断是否有新的有效边加入,如果有的话,需要重新排序
List<Edge> edgeList = edgeTable.get(y);
if (edgeList != null && edgeList.size() > 0) {
// 需要将新的边添加到有效边集合中
activeEdgeList.addAll(edgeList);
// 因为有新边加入,需要重新排序,首先优先按照z的值来升序排序,对于z相同的,按照k升序排序
Collections.sort(activeEdgeList, ((o1, o2) -> {
if (o1.z > o2.z) {
return 1;
} else if (o1.z < o2.z) {
return -1;
} else {
if (o1.k > o2.k) {
return 1;
} else if (o1.k < o2.k) {
return -1;
} else {
return 0;
}
}
}));
}
/// 构造扫描线
for (int j = 0; j < activeEdgeList.size(); j += 2) {
vSLineList.add(y);
vSLineList.add((int)activeEdgeList.get(j).z);
vSLineList.add((int)Math.ceil(activeEdgeList.get(j + 1).z));
// 进行增量计算,将z的值增加
activeEdgeList.get(j).z += activeEdgeList.get(j).k;
activeEdgeList.get(j + 1).z += activeEdgeList.get(j + 1).k;
}
}
vLineListSort(vSLineList);
part.vSLineList = vSLineList;
}
当然,这个扫描线生成方法你们并不能直接调用,因为我没有将实体类Part的代码放出来,读者只需要参照上面的思路稍微做一些修改即可,非常简单。除此之外,上面是生成纵线扫描线的方法,生成横线扫描线的方法也类似,举一反三即可
效果测试
     