推荐博客:YangKang`s Blog
很久一段时间没有更新自己的博客了,这期间的确很压抑,深深的陷入了一个矢量图填充的项目中。当多件事牵连在一起的时候,真一种捉襟见肘的感觉。不管怎样,也算是失之东隅,收之桑榆吧。
一、算法简析:
扫描线填充算法的基本思想是:用水平扫描线从上到下(或从下到上)扫描由多条首尾相连的线段构成的多边形,每根扫描线与多边形的某些边产生一系列交点。将这些交点按照x坐标排序,将排序后的点两两成对,作为线段的两个端点,以所填的颜色画水平直线。多边形被扫描完毕后,颜色填充也就完成了。扫描线填充算法也可以归纳为以下4个步骤:
(1)求交点。即扫描线和多边形的交点。
(2)交点排序。
(3)对排序后的点两两匹配。
(4)更新扫描线,判断是否完成多边形扫描。
算法的关键是第一步,求交点 。
如上图,可以发现多边形与扫描线的交点有如下 二个特点:
(1) 每次只有相关的几条边可能与扫描线有交点,不必对所有的边进行求交计算;
(2)相邻的扫描线与同一直线段的交点存在步进关系。
第一个特点是显而易见的,为了减少计算量,扫描线算法需要维护一张“活动边Active Edge组成的表,称为“活化边表Active Edge Table(AET)”。例如扫描线4的“活化边表” p1p2和p3p4两条边组成。
第二个特点,可以进步证明:假设当前扫描线与多边形的某一条边的交点已经通过直线段求交算法计算出来,得到交点的坐标为(x, y),则下一条扫描线与这条边的交点不需要再求交计算,通过步进关系可以直接得到新交点坐标为(x + △x, y + 1)。△x为y增加1个单位时,x增加的单位。即多边形边界的斜率的倒数。
总而言之,言而总之,“活动边表”是扫描线填充算法的核心,保证了填充时不需太多的求交点运算。为了方便活性边表的建立与更新,还需要建立一个边表NET(New Edge Table)来存放所有的边。
再来讨论边的数据结构:扫描线填充算法只关注交点的x坐标,即交点的横坐标;处理下一条扫描线交点时,根据 △x直接得出;还需要边的最大y坐标y_max作为活边表和扫描结束的判断依据;了便于插入和修改,定义为链表结构。
所以,我们定义边的数据结构如下,AET和NET的基本类型均为边类型Edge。
typedef struct tagEdge{ int y_max; // 边的最大y坐标 float x; // 与扫描线交点x坐标 float dx; // 斜率的倒数,Δx Edge * pNext; // 下一条边 } Edge;
边表NET是根据每条边的y_max为索引边类型Edge数组。看到这个结构是不是非常像HashMap。没错,这就是一个哈希表。但对其对象操作相当繁琐。
上图的多边形建立的NET如下:
活化边表AET是根据NET中的所有边Edge类型的y_max进行判断,可能相交的直线添加到AET。如上图形的AET如下:
接下来就是直接扫描处理了,将的得到的点两两连线。
二、代码实现
//扫描线填充算法 CPtrArray & CShapeFiller::scanLineFill(CPtrArray &m_tranArrShape){ CPtrArray *pArr = new CPtrArray(); float y_max = (float)INT_MIN; // y坐标值的最大值 float y_min = (float)INT_MAX; // y坐标值的最小值 //获取y坐标值最大和最小值 GetYMaxMin(y_max, y_min); //最大最小值取整 int iy_max = (int)y_max; int iy_min = (int)y_min; //y_max 取整的误差 float y_deviation = y_min - (float)iy_min; /* -------------------------- 定义边表NET及活化边表AET ---------------------- */ //活化边表AET:元素与当前扫描线相交 基本元素:边Edge. //边表NET: 按边的下端点的Y坐标对非水平的边指针数组 Edge *pAET=NULL; // 活化边表的表头指针 Edge **pNET=NULL; // 边表的表头指针 pAET = new Edge(); //初始化活动表头指针,第一个元素不用 pAET->pNext = NULL; /* ---------------------- 初始化边表NET --------------------------- */ //边表NET数组的长度 int length = (iy_max-iy_min); // 初始化边表NET,第一个元素不用 pNET=new Edge*[length + 1]; //初始化边表NET中的每一个指针元素,赋予内存空间 for(int i = 0;i <= length;i++) { pNET[i] = new Edge(); pNET[i]->pNext = NULL; } //将所有的边添加到NET边表中 CreateNET(pNET,iy_min); /* --------------------------- 扫描线填充 ----------------------- */ // 扫描线填充,从最小y坐标开始扫描,下闭上开 for(int i=iy_min;i < iy_max;i += 1) { //活动边表AET的头指针 Edge *pEdgeFirst=pNET[i-iy_min]; //初始化活化边表AET InitializeAET(pEdgeFirst,pAET); //对当前AET活化边表进行X坐标值升序排序 SortAcendX(pAET); //添加间距参数判断 //若间距为0 ,不填充 if (lineSpacing ==0) { for(int i=0;i <=length;i++) if(pNET[i]) delete pNET[i]; if(pAET) delete pAET; if(pNET) delete[] pNET; //释放指针申请的内存空间 m_tranArrShape.RemoveAll(); return pArr; } else { //间距不为0,填充实现 if(i%lineSpacing==0) { // 遍历活边表,将坐标点存入CPtrArray对象中 SavePoints(pAET,i,*pArr,y_deviation,x0,y0); } } // 更新扫描线 UpdateScanLine(pAET,i); } // 删除边表 for(int i=0;i <=length;i++) if(pNET[i]) delete pNET[i]; if(pAET) delete pAET; if(pNET) delete[] pNET; //释放指针申请的内存空间 m_tranArrShape.RemoveAll(); return *pArr; }
由于该项目有一定的特殊性,故洒家在此就不提供每个函数的详细代码。如需了解,欢迎一起交流。
三、问题解析
然后,在此过程中遇到很多一个比较大的问题。当扫描线经过多边形顶点时,会出现缺填充或者误填充嗯的问题。
当时是想着判断交点奇偶性,上下顶点对填充没有影响,但是左右顶点对填充会有较大的影响。
左右顶点判断(逆时针为正)
左顶点――P1、P2和P3的y坐标满足条件:y1 < y2 < y3;
右顶点――P1、P2和P3的y坐标满足条件:y1 > y2 > y3;
左右顶点修正:
对于左顶点的情况,采用的修正方法是修改以左顶点为终点的那条边的区间,将顶点排除在区间之外,也就是删除这条边的终点,这样在计算交点时,就可以少计算一个交点,平衡和交点奇偶个数。结合前文定义的“边”数据结构:AET,只要将该边的y_max修改为y_max – 1就可以了。右顶点不处理,左开右闭的策略。
然而,这样虽然能够解决大多数的顶点缺填问题,但是对于几个左右顶点连续排列在一条水平线上时,会出现误填。
改进的算法:
后来意识到扫描的基本思想的在图形的内部填充,填充图形也在图形内部,改进的算法是直接判断得到的两两匹配的点连成的线段是否在图形内部。从而,这个问题得到了完好的解决。
以上算法的都是水平线填充,其实斜线填充经过坐标的一系列的平移旋转变换,最终也可以用以上的算法实现。 同时还要注意阈值处理,浮点型数据的精度问题。
四、总结
有时候,我们可能看到一些现象,遇到一些问题,形象化给我们的解决方案是:针对这些现象去解决这些问题,然后先天地给予各种前提,假设种种情景,然后经过这种自以为的这种逻辑判断,最后检验其正确性,殊不知,有些事情是设想之外的,或者说前提有无穷多。前提都不成立,结果又何从可信。正如扫描线相交于多边形顶点,单一顶点处理可能会解决所有问题,但任意的顶点组合怎么办呢? 透过现象看本质,也许就会发现起初的前提只是冰山一角,
九牛一毛而已。透过浮云才能与真理的光芒相拥。
相关推荐
**C++实现扫描线填充算法** 扫描线填充算法是一种在计算机图形学中广泛使用的填充算法,主要用于二维图形的内部填充。这种算法通过沿图形的边界垂直地扫过一系列水平线,然后对每条扫描线上的边界点进行处理,从而...
3. **扫描线填充**(Scan Line Filling): 扫描线算法首先将多边形沿Y轴分解成一系列水平线段,然后对每条线段上的点进行填充。在Android中,可使用两个优先级队列分别存储y值最小和最大的边,然后按顺序处理这些...
种子填充算法(Seed Filling Algorithm)是一种在计算机图形学领域中常见的图像处理技术,主要用于颜色连通区域的填充。在2D图像上,用户选择一个或多个“种子”像素,算法会根据一定的规则将与这些种子相邻且颜色...
边标志填充算法(Edge Flag Filling Algorithm)是一种常见的用于在屏幕上填充多边形内部的方法。这个算法简单而有效,尤其适用于二维图形渲染。接下来,我们将深入探讨边标志填充算法的工作原理、实现步骤以及它在...
线性填充的实现通常基于扫描线算法,例如Bresenham's Line Algorithm可以用于高效地绘制线段。在每行扫描时,找到线段的起点和终点,判断这些点是否位于当前扫描线内,如果在则填充相应的像素。对于多边形,需要处理...
扫描线种子填充算法结合了扫描线技术和种子填充,通常用于处理复杂的图形。它首先按行扫描图像,然后根据扫描线上的种子点,利用边界跟踪或扫描转换技术,逐行填充颜色。这种方法特别适合处理具有垂直或水平边界的...
在计算机图形学中,有效边表(Active Edge Table,简称AET)填充算法是一种用于多边形扫描线填充的算法。该算法通过管理多边形边界的有效边来实现高效填充。下面是对提供的部分内容中出现的概念和技术点进行详细解释...
在扫描线算法中,我们可以使用一种叫做杨氏算法(Yao's Algorithm)或者扫描线填充算法(Scan-Line Filling Algorithm)的方法。对于每一条扫描线,我们维护一个边的队列,当扫描线遇到五边形的上边界时,将边添加到...
**扫描线填充算法** 在计算机图形学中,扫描线填充是一种常见的图像处理技术,用于将图形内部区域填充颜色。该算法通常应用于二维图形的绘制,特别是在计算机绘图软件和游戏开发中。它的工作原理是通过水平地遍历...
该算法可以细分为几类,如注入填充算法(Flood Fill Algorithm)、边界填充算法(Boundary Fill Algorithm)和扫描线种子填充算法等等。 在开始介绍种子填充算法之前,需要首先介绍两个概念,即“4-联通算法”和“8...
在给定文件的标题和描述中,提及了几个关键概念和算法:Sutherland-Hodgman和Weiler-Atherton算法用于多边形裁剪,扫描线(Scanfilling)和洪水填充(Floodfilling)用于多边形填充。 ### 多边形裁剪算法 多边形...
空间填充曲线在理论和实际应用中均显示出了巨大的潜力,从高效的空间索引到复杂数据的空间分析,都离不开这种曲线的帮助。随着GIS和其他相关技术的不断进步,空间填充曲线及其变种很可能将继续成为数据库研究中的一...
"filling_line.cpp"文件很可能是实现这一算法的源代码。通常,这个文件会包含定义边界的函数、计算斜率和间距的函数、生成和扫描填充线的函数,以及可能的优化策略的实现。 总之,多边形影线填充是计算机图形学中...
在实际应用中,它不仅可以用于简单的图形填充,还可以用于复杂的图像处理和分析任务,例如物体识别、图像分割和边缘检测等。 文章中提及的关键词“HoleFilling”指的是孔洞填充;“ComputerGraphics”代表计算机...
种子填充(Seed Filling)算法是计算机图形学中的一种基础填充技术,常用于图像处理和绘图软件中,例如在绘图软件中对选定区域进行颜色填充。在这个C#实现的种子填充项目中,我们可以通过VS2005开发环境来理解和实践...
这种方法的优势在于,它能够将复杂的多维空间问题转化为简单的一维线性问题,从而简化查询和计算过程。 在传统的关系数据库管理系统中,空间数据的存储和查询往往是一个难题。因为关系模型不擅长处理非结构化的空间...
"User-Guided Line Art Flat Filling With Split Filling Mechanism" 本文介绍了一种基于深度学习的用户指导下的线艺术平面填充框架,该框架能够计算用户颜色涂鸦的“影响区域”,即用户涂鸦应该传播和影响的区域。...
种子填充(Seed Filling)算法,也称为区域生长算法,是一种在数字图像处理中常见的填充工具,常用于图像分析和处理。在这个特定的案例中,我们关注的是四联通种子填充算法,这是一种基于像素邻接关系的填充方式。四...