第三章 二维图元的填充
- 扫描转换矩形
- 扫描转换多边形
- 逐点判断法
- 扫描线算法
- 边缘填充算法
- 扫描转换扇形
- 区域填充(种子填充法)
- 递归填充算法
- 扫描线算法
- 以图像填充区域
- 字符的表示与输出
扫描转换矩形
矩形作为特殊多边形, 单独处理的原因有
- 比一般多边形可简化计算
- 应用非常多, 如窗口系统
填充矩形时的共享边界的处理
原则:左闭右开,下闭上开
即矩形左边、下边的像素属于矩形
void FillRectangle (Rectangle *rect, int color)
{
int x,y;
for(y = rect->ymin; y < rect->ymax;y++)
{
for(x = rect->xmin;x < rect->xmax;x++)
{
PutPixel(x,y,color);
}
}
}
扫描转换多边形
多边形分为凸多边形、凹多边形、含内环的多边形
凸多边形: 任意两顶点间的连线均在多边形内部
凹多边形: 存在一对顶点,它们之间的连线部分或全部不在多边形内部
逐点判断法
算法思想
- 找到多边形包围盒, 遍历包围盒中每一个点
- 如果在多边形中就填充前景色
- 如果不在就填充背景色
问题有两个
- 如果获取多边形包围盒
- 轴对齐包围盒(Axis-Aligned Bounding Box, AABB)
- 最小旋转包围盒(Oriented Bounding Box, OBB)
- 极值投影法(Rotating Calipers)
- 圆形包围盒(Bounding Circle)
- 如何判断点是否在多边形内部
- 射线法
- 累计角度法
- 编码法
这里只介绍一种最常用的求多边形包围盒的方法,
AABB
法, 其他的算法可以自行了解
逐点判断法代码示例
#define MAX 100
typedef struct {
int PolygonNum; // 多边形顶点个数
Point vertexces[MAX]; // 多边形顶点数组
} Polygon; // 多边形结构
void FillPolygonPbyP(Polygon *P, int polygonColor) {
int x, y;
int xmin, xmax, ymin, ymax; // 找到多边形包围盒
FindAABB(P, &xmin, &xmax, &ymin, &ymax);
// 遍历图像包围盒中的所有像素
for (y = ymin; y < ymax; y++) {
for (x = xmin; x < xmax; x++) {
if (IsInside(P, x, y)) {
PutPixel(x, y, polygonColor); // 在多边形内部,画前景色
} else {
PutPixel(x, y, backgroundColor);// 在多边形外部,画全局背景色
}
}
}
}
轴对齐包围盒(Axis-Aligned Bounding Box, AABB)
初始化
xmin
,xmax
,ymin
,ymax
,并设置为多边形第一个顶点的坐标遍历多边形的所有顶点:
- 更新
xmin
为最小的 x 坐标- 更新
xmax
为最大的 x 坐标- 更新
ymin
为最小的 y 坐标- 更新
ymax
为最大的 y 坐标
xmin
,xmax
,ymin
,ymax
就是包围盒的边界
void FindAABB(Polygon *P, int *xmin, int *xmax, int *ymin, int *ymax) {
*xmin = *xmax = P->vertexces[0].x;
*ymin = *ymax = P->vertexces[0].y;
for (int i = 1; i < P->PolygonNum; i++) {
if (P->vertexces[i].x < *xmin) *xmin = P->vertexces[i].x;
if (P->vertexces[i].x > *xmax) *xmax = P->vertexces[i].x;
if (P->vertexces[i].y < *ymin) *ymin = P->vertexces[i].y;
if (P->vertexces[i].y > *ymax) *ymax = P->vertexces[i].y;
}
}
-
优点:
-
简单快速:该算法时间复杂度为 O(n),其中 n 是多边形顶点的数量。
-
适用于任意形状的多边形:对于任意形状的多边形都适用。
-
-
缺点:
- 不精确:因为包围盒的边总是与坐标轴对齐,对于旋转的多边形或非对齐的多边形,AABB 可能包含了更多的空白区域。
射线法
算法步骤
- 从待判别点v发出射线
- 求交点个数
k
k
的奇偶性决定了点与多边形的内外关系
- 奇数个交点, 点在多边形内部
- 偶数个交点, 点在多边形外部
缺陷:
- 射线通过顶点
- 顶点连接两条边,当射线穿过顶点时,射线既会与顶点连接的其中一条边相交,又会与另一条边相交。如果不做特殊处理,可能会将这个顶点算作两个交点,从而错误地增加交点数量
- 为了解决这个问题,常见的做法是忽略射线与顶点的交点,或根据相邻两条边的位置进行适当的调整,从而确保交点数量的准确性
- 对于边自交的多边形会产生误判, 如下图中
V0
在多边形内部却被判定为多边形外部
累计角度法
算法步骤
- 从v点向多边形P顶点发出射线,形成有向角
- 计算有向角的和,得出结论
问题
三角函数的周期性会导致夹角的表示不唯一,从而影响累计角度法的准确性
- 通过将每个夹角限制在
(−π,π]
范围内,可以确保累计角度准确反映点相对于多边形的内部或外部情况
∑ i = 1 n θ i = { 0 , 点 V 位于多边形外 ± 2 π , 点 V 位于多边形内 \sum_{i=1}^{n} \theta_i= \begin{cases} 0 \text{ , 点 V 位于多边形外} \\ \pm2\pi \text{ , 点 V 位于多边形内} \end{cases} i=1∑nθi={0 , 点 V 位于多边形外±2π , 点 V 位于多边形内
编码法
编码法是累计角度法的 离散方法
算法步骤
预处理,测试点在边上否?若是,判别结束。
以
V
为原点作局部坐标系,象限按逆时针(或顺时针)编码对各顶点进行编码,
Pi
编码为象限编码Ii
对各边进行编码,记为
△ P i P i + 1 = I p i + 1 − I p i △P_iP_{i+1}=I{p_{i+1}}-I_{pi} △PiPi+1=Ipi+1−Ipi
△PiPi+1
的周期为4,值在(-2,2]
求边编码的和, 其中
△PnPn+1 = △PnP0
∑ i = 0 n Δ P i P i + 1 = { 0 , 点 V 位于多边形外 ± 4 , 点 V 位于多边形内 \sum_{i=0}^n \Delta P_iP_{i+1}= \begin{cases} 0 \text{ , 点 V 位于多边形外} \\ \pm4 \text{ , 点 V 位于多边形内} \end{cases} i=0∑nΔPiPi+1={0 , 点 V 位于多边形外±4 , 点 V 位于多边形内
- 优点
- 程序简单
- 缺点
- 效率低
- 没有利用相邻像素之间的连贯性
- 效率低
扫描线算法
目标
- 利用相邻像素之间的连贯性,提高算法效率
处理对象
- 非自交多边形 (边与边之间除了顶点外无其它交点)
基本原理
- 一条扫描线与多边形的边有偶数个交点
算法步骤 ( 水平扫描线 )
- 求交:计算扫描线与多边形各边的交点
- 排序:把所有交点按
x
值递增顺序排序- 配对:第一个与第二个, 第三个与第四个等等, 每对交点代表扫描线与多边形的一个相交区间
- 填色:把相交区间内的象素置成多边形颜色,把相交区间外的象素置成背景色
算法思想
边的连贯性
相邻扫描线与边的交点的坐标有连贯性, 计算方法与直线光栅化思想一致, 用DDA算法
即第 i
条扫描线与多边形某边 Ej
的交点为 Xij
, 第 i+1
条扫描线与多边形某边 Ej
的交点为 X(i+1)j
, 边的斜率为 kj
, 则有
x
(
i
+
1
)
j
=
x
i
j
+
1
k
j
x_{{(i+1)}j}=x_{ij}+\frac 1 k_j
x(i+1)j=xij+k1j
将扫描线上的交点分为两类
- 交点是某条边的 上一交点的后继
- 此时可以套用上述公式
- 交点是某条边的 起始端点
- 此时边的下端点就是交点, 无需计算
// 定义边的数据结构为
typedef struct{
int ymax;
float x, deltax;
struct Edge *nextEdge;
} Edge;
// ymax 边的上端点y坐标
// x 初值为边下端点的x坐标,AEL中为当前扫描线与边的交点的X坐标;
// deltax 边的斜率的倒数
// nextEdge 指向下一条边的指针
为了跟踪当前扫描线和多边形的交点,以确定该行哪些像素应该被填充, 提出以下两个概念
- 边的分类表 (ET)
- 活性边表 (AEL)
边的分类表 (ET)
按照边的下端点 y 坐标
对非水平边进行分类的指针数组
- 下端点
y
坐标值等于i
的边属于第i
类 - 同一类中的边按
x
值 (x
相等的按deltax
排)递增顺序排列。
以下图多边形为例
该多边形的 ET
如下图
活性边表 (AEL)
当处理一条扫描线时,仅对多边形与它相交的边进行求交运算
把与当前扫描线相交的边称为活性边,并把它们按与扫描线交点 x 坐标递增
的顺序存放在一个链表中,称此链表为活性边表(AEL)
对于上面提到的多边形, y=6
与 y=7
两条扫描线对应的 AEL
如下
算法步骤
- 建立ET
- 将扫描线纵坐标
y
的初值置为ET中非空元素的最小序号,如在上图中,y=1- 置AEL为空
- 执行下列步骤直至ET和AEL都为空
- 如ET中的第
y
类非空,则将其中的所有边取出并插入AEL中- 如果有新边插入AEL,则对AEL中各边排序
- 对AEL中的边两两配对,(1和2为一对,3和4为一对,…),将每对边中
x
坐标按规则取整,获得有效的填充区段,再填充- 将当前扫描线纵坐标
y
值递增1
- 将AEL中满足
y=ymax
的边删去, 因为每条边被看作下闭上开的)- 对AEL中剩下的每一条边的x递增自身的
deltax
值,即x = x+deltax
边缘填充算法
光栅图形中,如果某区域已填上颜色值
M
,当做偶数次求余运算,该区域颜色不变;做奇数次求余运算,则该区域颜色变为值为M余
的颜色实际上色中可以在 背景色 与 目标色 中来回切换实现类似效果
这一规律应用于多边形扫描转换,就为边缘填充算法
- 对于每条扫描线和每条多边形边的交点,将该扫描线上交点右方的所有象素取余
以扫描线为中心的边缘填充算法
- 将当前扫描线的所有像素着色为 背景色
- 对于每条扫描线和每条多边形边的交点,将该扫描线上交点右方的所有象素
- 若为第
奇数
个交点, 取 目标色- 若为第
偶数
个交点, 取 背景色
- 这里的每个交点都是不用排序的, 交点的顺序对结果没有影响, 但由于求交点是从左往右的, 一般也是从左向右来处理
- 以边为中心的边缘填充算法
- 将绘图窗口的背景色置为
M余
- 对多边形的每一条非水平边做:从该边上的每个象素开始向右求余
- 优点
- 算法简单
- 缺点
- 对于复杂图形,每一象素可能被访问多次,输入/输出的量比扫描线算法大得多,造成速度较慢
- 适合用于具有帧缓存的图形系统, 提前处理后, 按扫描线顺序读出帧缓存的内容,送入显示设备
扫描转换扇形
扇形区域的描述
- 半径,起始角,终止角 ( 圆心不在原点的, 通过坐标转换到原点再处理 )
原理:同扫描转换多边形 ( 圆弧与扫描线交点直接带入 y 值算出来 )
问题:如何确定扫描线与直线段和圆弧段的相交顺序
- 对不同情况分类
令点P1,P2满足:θ1 < θ2
按P1和P2所处象限的不同,需要分成 4×4=16
种 情况来讨论
先讨论 P1
在第一象限的四种情况
P2 在第一象限
0 < Y < Yp1
时左侧是扫描线与直线段OP2
的交点, 右侧是与直线段OP1
的交点Yp1 < Y < Yp2
时左侧是扫描线与直线段OP2
的交点, 右侧是与圆弧P1P2
的交点如下图
P2 在第二象限
分为两种情况
Yp1 < Yp2
Yp1 > Yp2
- P2 在第三象限
- P2 在第四象限
当 P1 在其它象限时
- 将扇形顺时针转动, 使 P1 落在第一象限内, 扫描完成后直接将整个扇形逆时针旋转回原来的位置
扫描扇形的其他方法
用多边形逼近扇形的圆弧部分, 再用扫描多边形的算法来填充扇形