zoukankan      html  css  js  c++  java
  • 图形学入门(3)——区域填充算法(region filling)

    继续图形学之旅,我们已经解决了如何画线和画圆的问题,接下来要解决的是,如何往一个区域内填充颜色?对一个像素填充颜色只需调用SetPixel之类的函数就行了,所以这个问题其实就是:如何找到一个区域内的所有像素?

    区域的表示方法

    定义一个区域可以有两种方法,即内点表示法边界表示法,内点表示就是指用一种颜色表示区域内的点,只要当前像素是这种颜色就在区域内,边界表示就是用一种颜色表示区域边界,只要当前像素是这种颜色就表示到达了区域边界。

    简单的种子填充算法

    最简单暴力的填充算法即是从区域内一点出发,向四周扩散填充,到达区域边界时停止,常见的有四邻法八邻法两种,顾名思义,一个是向上下左右四个方向扩散填充,另一个是向周围八个方向扩散,四邻法可以确保不溢出区域边界,但有可能出现一次填不满区域的情况,八邻法则相反,一定能填充满当前区域,但有从对角线溢出边界的危险。

    边界表示的四邻法代码实现:

    void BoundaryFill4(HDC hdc, int x, int y, COLORREF boundaryColor, COLORREF newColor)
    {
    	COLORREF c = GetPixel(hdc, x, y);
    	if (c != newColor && c != boundaryColor)
    	{
    		SetPixel(hdc, x, y, newColor);
    		BoundaryFill4(hdc, x + 1, y, boundaryColor, newColor);
    		BoundaryFill4(hdc, x - 1, y, boundaryColor, newColor);
    		BoundaryFill4(hdc, x, y + 1, boundaryColor, newColor);
    		BoundaryFill4(hdc, x, y - 1, boundaryColor, newColor);
    	}
    }
    

    我们用之前学习的Bresenham画线算法画一个矩形,然后用这个算法填充它。

    	Bresenham_Line(150, 150, 150, 200, hdc, RGB(0, 0, 0));
    	Bresenham_Line(150, 200, 200, 200, hdc, RGB(0, 0, 0));
    	Bresenham_Line(200, 200, 200, 150, hdc, RGB(0, 0, 0));
    	Bresenham_Line(200, 150, 150, 150, hdc, RGB(0, 0, 0));
    	BoundaryFill4(hdc, 175, 175, RGB(0, 0, 0), RGB(255, 0, 0));
    

    运行效果:

    很显然,这种递归的填充算法简单好理解,但效率是不可接受的,显然我们需要提高算法效率,避免过多的递归调用。

    扫描线种子填充算法

    为了提高效率可以使用扫描线种子填充算法,这里的扫描线就是与x轴相平行的线,该算法可以由以下4个步骤实现:

    1. 初始化:堆栈置空,将初始种子点(x,y)入栈
    2. 出栈:若栈空则算法结束,否则取栈顶元素(x,y),以y作为当前扫描线
    3. 填充并确定种子点所在区段:从种子点(x,y)出发,向左右两个方向填充,直到边界。标记区段左右断点为xl和xr。
    4. 确定新的种子点:在区间[xl,xr]中检查与当前扫描线y上下相邻的两条扫描线上的像素。若存在非边界、未填充像素,则把每一区间最右像素作为种子点压入堆栈,返回第二步。

    代码实现:

    void ScanLineFill4(HDC hdc, int x, int y, COLORREF oldColor, COLORREF newColor)
    {
    	int xl, xr;
    	bool SpanNeedFill;
    	pair<int, int> seed;
    	stack<pair<int, int>> St;
    
    	seed.first = x; seed.second = y;
    	St.push(seed);
    	while (!St.empty())
    	{
    		seed = St.top();
    		St.pop();
    		y = seed.second;
    		x = seed.first;
    
    		while (GetPixel(hdc,x,y) == oldColor)//向右填充
    		{
    			SetPixel(hdc, x, y, newColor);
    			x++;
    		}
    		xr = x - 1;
    		x = seed.first - 1;
    		while (GetPixel(hdc, x, y) == oldColor)//向左填充
    		{
    			SetPixel(hdc, x, y, newColor);
    			x--;
    		}
    		xl = x + 1;
    
    		//处理上方的一条扫描线
    		x = xl;
    		y = y + 1;
    		while (x<xr)
    		{
    			SpanNeedFill = false;
    			while (GetPixel(hdc,x,y)==oldColor)
    			{
    				SpanNeedFill = true;
    				x++;
    			}
    			if (SpanNeedFill)
    			{
    				seed.first = x - 1; seed.second = y;
    				St.push(seed);
    				SpanNeedFill = false;
    			}
    			while (GetPixel(hdc, x, y) != oldColor && x < xr)x++;
    		}
    
    		//处理下方的一条扫描线
    		x = xl;
    		y = y - 2;
    		while (x < xr)
    		{
    			SpanNeedFill = false;
    			while (GetPixel(hdc, x, y) == oldColor)
    			{
    				SpanNeedFill = true;
    				x++;
    			}
    			if (SpanNeedFill)
    			{
    				seed.first = x - 1; seed.second = y;
    				St.push(seed);
    				SpanNeedFill = false;
    			}
    			while (GetPixel(hdc, x, y) != oldColor && x < xr)x++;
    		}
    	}
    }
    

    这次画一个不太规则的图形试试吧。

    Bresenham_Line(100, 100, 150, 150, hdc, RGB(0, 0, 0));
    Bresenham_Line(150, 150, 200, 100, hdc, RGB(0, 0, 0));
    Bresenham_Line(200, 100, 200, 300, hdc, RGB(0, 0, 0));
    Bresenham_Line(200, 300, 100, 300, hdc, RGB(0, 0, 0));
    Bresenham_Line(100, 300, 100, 100, hdc, RGB(0, 0, 0));
    ScanLineFill4(hdc, 150, 175, RGB(255, 255, 255), RGB(0, 255, 0));
    

    这次由于对每一个待填充区段只需要压栈一次,所以效率提高了,也没有堆栈溢出的危险。

    有序边表的扫描线算法

    接下来是最复杂的一种的扫描线算法,需要多边形的所有边信息,主要思想是求得每一条扫描线与多边形的交点,从而两两配对得到处在多边形内的区间,对这些区间进行上色,但要求扫描线与多边形的交点,直接暴力地遍历每条边肯定是不可行的,我们需要引入活性边表AET新边表NET来辅助计算。

    NET中存放的是在该扫描线第一次出现的边,也就是最低端点的y值等于当前扫描线位置的边,对每一个结点,需要存储当前x值、直线斜率倒数和直线最高点y值,如下图所示:

    通过NET就可以容易地得到AET,AET中存放的是扫描线与多边形的交点。我们从下往上遍历每条扫描线,对于扫描线i来说,将NET[i]中结点插入,将AET[i-1]中ymax=i的结点删除,其余结点将x值加上斜率倒数之后插入,就得到了AET[i]。

    有了AET之后,只需要配对每两个交点,把区间内像素上色就可以了,但要注意,由于要从左到右配对,所以

    AET表应时刻保持按x坐标递增排序。

    伪代码:

    void PolyFill(polygon,color)
    {
        初始化新边表NET和活性边表AET;
        for(每条扫描线i)
        {
            把ymin=i的边放进边表NET[i];
        }
        for(每条扫描线i)
        {
            把新边表NET[i]中结点插入AET[i](x坐标递增有序排列);
            AET[i-1]中ymax!=i的结点加入AET[i];
            遍历AET[i],把配对交点区间中像素上色;
        }
    }
    

    边界标志算法

    还有一种基于扫描线思想的边界标志算法,比较适合用硬件实现。基本思想是对多边形每条边进行扫描转换,找到多边形边界的所有像素,对每条与多边形相交的扫描线按从左到右的顺序扫描每个像素,用一个布尔值inside表示当前点是否在多边形内(初始为false),只要扫描到多边形边界像素,就把inside取反,若inside为真,则表示该点在多边形内,则填充该像素。

    伪代码:

    void edgemark_fill(polydef,color)
    {
        对多边形每条边扫描转换;
        inside=false;
        for(每条扫描线)
        {
            for(扫描线上每个像素)
            {
                if(该像素是边界像素)
                    inside=!inside;
                else if(inside==true)
                    SetPixel(x,y,color);
            }
        }
    }
    
  • 相关阅读:
    codevs 2632 非常好友
    codevs 1213 解的个数
    codevs 2751 军训分批
    codevs 1519 过路费
    codevs 1503 愚蠢的宠物
    codevs 2639 约会计划
    codevs 3369 膜拜
    codevs 3135 River Hopscotch
    数论模板
    JXOJ 9.7 NOIP 放松模拟赛 总结
  • 原文地址:https://www.cnblogs.com/LiveForGame/p/11790369.html
Copyright © 2011-2022 走看看