计算机图形学-多边形扫描转换与区域填充

        按钮的创建就不在这里过多赘述了,前面的文章已经反复展示了,这里直接上代码和结果。

一、有效边表法

vector元素构建NET表,定义结构体EdgeNode用来存放边的最小y坐标、当前x坐标、△x和边的最大y坐标。填充方法是void polygon_fill(vector<vector<int>>& polygon),主函数写在void CMFCApplication2View::On32777()中。通过绘图函数polygon点的坐标定义位置不同,展现出陀螺、棍子和沙漏的绘制,棍子上的绳子用的是math中的arc方法画曲线。我的想法是陀螺是两个等腰三角形底边拼在一起,而沙漏恰恰相反,是两个等腰三角形顶点拼在一起,棍子就是画一个宽很窄的矩形。这些图案画完后,再用写好的填充算法进行填充即可。

算法思路

多边形扫描转换是将多边形的顶点表示转换为点阵表示,从多边形给定的边界出发,求出位于其内部的各个像素,并将帧缓存内各对应元素设置为相应的颜色。因此我们需要首先确定多边形的范围,进而确定扫描线的范围。步骤如下:

        1‘初始化并建立NET表。

        2‘遍历每一条扫描线,对于每一个多边形点,寻找与其构成边的两点,如果寻找到的点在此点的上方(即y0小于y1),则将此边加入到ET[i]中(i对应的y0的坐标)。这样每次加入的边都是向上,不会重复。

        3‘将AET设为空。

        4‘执行以下步骤直至NET和AET都为空:

        a. 更新:如ET中的y非空,则将其中所有边取出并插入AET中。

        b. 填充:对AEL中的边两两配对,每对边中x坐标按规则取整,获得有效的填充区段,再填充。

        c. 排序:如果有新边插入AET,则对AET中各边排序。

        d. 删除:从扫描线中删除已经填充的区段。

        代码:

using namespace std;

struct EdgeNode {
	int ymin;  // 边的最小y坐标
	double x;  // 当前x坐标
	double dx; // △x
	int ymax;  // 边的最大y坐标
};

void polygon_fill(vector<vector<int>>& polygon) {//i*2矩阵
	int y_min = INT_MAX;
	int y_max = INT_MIN;

	// 找到多边形的最小和最大y坐标
	for (const auto& point : polygon) {
		y_min = min(y_min, point[1]);
		y_max = max(y_max, point[1]);
	}

	vector<vector<EdgeNode>> NET(y_max + 1);

	// 构造NET
	for (int i = 0; i < polygon.size(); i++) {
		int next_idx = (i + 1) % polygon.size();
		int x1 = polygon[i][0];
		int y1 = polygon[i][1];
		int x2 = polygon[next_idx][0];
		int y2 = polygon[next_idx][1];

		int ymin = min(y1, y2);
		int ymax = max(y1, y2);
		double dx = (double)(x2 - x1) / (y2 - y1);// △x

		if (ymax != ymin) {
			if (ymin == y1) {
				NET[ymin].push_back({ ymin, (double)(x1), dx, ymax });
			}
			else if (ymin == y2) {
				NET[ymin].push_back({ ymin, (double)(x2), dx, ymax });
			}
		}
	}

	vector<EdgeNode> AET;
	//int ynow = y_min; // 当前扫描线号

	for (int i = y_min; i <= y_max; i++) { // 对每一条扫描线
		for (auto& edge : NET[i]) { // 把ymin=i的边放入AET
			if (edge.ymin == i) {
				AET.push_back(edge);
			}
		}
	}
	for (int i = y_min; i <= y_max; i++) { // 对每一条扫描线
		// 使用插入排序法对AET按x坐标递增顺序排序
		for (int j = 1; j < AET.size(); j++) {
			int k = j - 1;
			EdgeNode key = AET[j];
			while (k >= 0 && AET[k].x > key.x) {
				AET[k + 1] = AET[k];
				k--;
			}

			AET[k + 1] = key;
		}

		// 若允许多边形的边自交,用冒泡排序法对AET重新排序
		for (int j = 0; j < AET.size() - 1; j++) {
			for (int k = 0; k < AET.size() - j - 1; k++) {
				if (AET[k].x > AET[k + 1].x) {
					swap(AET[k], AET[k + 1]);
				}
			}
		}
		// 遍历AET,找到配对的交点并填色
		for (int j = 0; j < AET.size(); j += 2) {
			int x_start = (int)(ceil(AET[j].x));
			int x_end = (int)(ceil(AET[j + 1].x));
			COLORREF rgb = RGB(0 + rand() % 255, 0 + rand() % 255, 0 + rand() % 254);

			for (int x = x_start; x < x_end; x++) {
				putpixel(x, i, rgb);
			}
		}

		// 遍历AET,更新x值,删除ymax=i的边
		vector<EdgeNode> new_AET;
		for (const auto& edge : AET) {
			if (edge.ymax > i) {
				EdgeNode new_edge = edge;
				new_edge.x += new_edge.dx;
				new_AET.push_back(new_edge);
			}
		}
		AET = new_AET;
	}
}
//主函数
void CMFCApplication2View::On32777()
{
	// TODO: 在此添加命令处理程序代码
	// 有效边表法(AET)绘制填充多边形
	initgraph(800, 480);

	vector<vector<int>> polygon = { {560,150}, {630,270 },{560,270},{630,150} };//漏斗形
	polygon_fill(polygon);
		
	polygon = { {110,220}, {190, 220},{150,270}};//陀螺
	polygon_fill(polygon);
	polygon = { {110,220}, {190,220},{150,170} };
	polygon_fill(polygon);

	arc(300,100,150,180,80,false);//绳子
	polygon = { {300,139}, {420,139},{420,140 }, {300,140}};//棍子
	polygon_fill(polygon);

	outtextxy(200, 320, "陀螺");//文字输出
	outtextxy (580 ,320 , "沙漏");//文字输出
	//system("pause");
	Sleep(3500);
	closegraph();
}

        效果:

二、种子填充法

        我在此用的四连通种子填充法之边界填充算法。边界填充算法强调边界的存在,只要是边界内的点无论是什么颜色,都替换成指定的颜色。因此判断边界内的点是否已填充,按照右左下上的顺序进行四次递归进行填充,选择这个顺序可以显示出一种从右往左拉上窗帘的视觉效果。我的填充方法名是BoundarySeedFill,主函数是OnBoundaryseedfill,建立对话框与用户进行互动。种子点坐标和填充区域由用户通过键盘进行输入,用Dlg函数获取值,再使用CDC类的Rectangle()函数画矩形,进行填充。

算法思路

1‘确定种子点:选择一个位于要填充的区域内的像素作为种子点。2‘确定填充区域:根据特定的规则,确定要填充的区域。这通常可以通过判断像素之间的颜色或属性是否相似来完成。3‘填充颜色:从种子点开始,将新的颜色或属性填充到相邻的像素中。这个过程可以按照一定的方向或顺序进行,例如从上到下、从左到右等。4‘判断停止条件:在填充过程中,需要判断何时停止填充。这通常可以通过判断像素之间的颜色或属性是否仍然相似来完成。如果相邻的像素不再满足填充条件,则停止填充。5‘继续填充:重复步骤3和4,直到所有满足填充条件的像素都被填充为新的颜色或属性。

        代码:

//种子填充法之边界填充算法(四连通)
//边界填充算法强调边界的存在,只要是边界内的点无论是什么颜色,都替换成指定的颜色。
//注释了左边着色的递归,产生窗户拉窗帘的效果

void CMFCApplication2View::BoundarySeedFill(CDC* pDC, int x, int y, COLORREF colorBoundary, COLORREF colorNew)
{
	COLORREF colorCurrent = pDC->GetPixel(x,y);
	if (colorCurrent != colorBoundary && colorCurrent != colorNew)
	{
		pDC->SetPixel(x, y, colorNew);
		//Sleep(10);

		BoundarySeedFill(pDC, x + 1, y, colorBoundary, colorNew);
		BoundarySeedFill(pDC, x, y + 1, colorBoundary, colorNew);//右左下上
		//BoundarySeedFill(pDC, x - 1, y, colorBoundary, colorNew);
		BoundarySeedFill(pDC, x, y - 1, colorBoundary, colorNew);//右左下上
	}

}

void CMFCApplication2View::OnBoundaryseedfill()
{
	//TODO: 在此添加命令处理程序代码
	boundarySeedDlg dlg;

	if (dlg.DoModal() == IDOK) {//输入框显示时
		//给全局变量赋值,获取对话框声明的变量
		bx1 = dlg.b_BoundarySeedX11;
		bx2 = dlg.b_BoundarySeedXX2;
		by1 = dlg.b_BoundarySeedY1;
		by2 = dlg.b_BoundarySeedYY2;
		bzx =dlg.b_BoundarySeedZX;
		bzy = dlg.b_BoundarySeedZY;
		
	}

	CDC* pDC = GetDC();//画矩形
	pDC->Rectangle(bx1, by1, bx2, by2);
	//pDC->Rectangle(200,200,300,300);

	BoundarySeedFill(pDC,bzx, bzy, BLACK, GREEN);
	//BoundarySeedFill(pDC,201, 230, BLACK, GREEN);

}

        效果:

     ​随便输入坐标点,比如(200,200),(300,300),(201,230),填充后如下图:

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值