实验目的

  1. 掌握种子填充法(递归法和基于栈的算法),扫描线种子填充法

  2. 了解像素的四联通,八联通以及两种联通的边界和内点表示的方法[1]

  3. 掌握常见的种子填充算法以及效率的分析

实验内容

种子填充法

给图形上色,就像给水池注水一样。想象您有一个不规则形状的图形,比如用黑色笔画的一个封闭圆圈,现在您想用红色油漆填满它,这就是”种子填充”要做的事情。

种子填充的思路,就像您在待填充区域内任意选择一个点(”种子”),然后让这个颜色像水一样,从这个点向四周”流淌”或”蔓延”,直到遇到边界或流满了整个区域为止。

填充的判断依据(水往哪里流?)

填充算法的关键在于判断一个像素点是否应该被上色。这里有两种判断方法,对应着水的流淌目标

边界表示 (Boundary Fill)

找边界法:水(新颜色)向外流淌时,如果遇到墙壁(边界色),就停止流淌。只要没遇到墙壁,水就继续流。图形的边界颜色是已知的(例如黑色边框),我们只填充边界色内部的区域。

内点表示 (Flood Fill)

找内色法:水(新颜色)向外流淌时,只填充和种子点颜色相同(内色)的区域。一旦遇到其他颜色或边界色,就停止。适用于填充一个已经被上过色的、由多种颜色包围的区域。例如,把一个蓝色的区域改为黄色。

蔓延的方式:水怎么流?

“水”从一个像素点流向相邻像素点的方式也有两种,这决定了填充的形状和速度:

四连通 (4-Connected)

1763259290846.png

十字蔓延法:水只向上下左右四个方向流淌。从中心点出发,只检查 N, S, E, W
四个邻居。简单、速度快,但可能无法填充对角线连接的区域。

八连通 (8-Connected)

1763259306980.png

米字蔓延法:水向上下左右,以及四个对角线方向流淌(共八个方向)。
从中心点出发,检查所有八个邻居。更加完整、不易遗漏,能处理斜线连接的区域。

基于递归的种子填充

  • 原理:

以种子像素为起点,递归检查其邻域像素是否满足”既不是边界色也不是已填充色”的条件;若满足,则对该像素着色并继续对其邻域进行同样的判断,从而沿四邻域或八邻域方向逐步向外扩散,实现区域填充。

  • 不足:
  1. 当填充区域较大或边界复杂时,递归调用层级会迅速增加,最终可能超过系统允许的最大栈深度,导致程序崩溃。

  2. 效率较低,重复访问像素,递归过程中可能对同一像素进行多次读取与判断,特别是边界附近区域,会造成不必要的
    glReadPixels 开销,影响填充效率。没有显式记录”已访问”状态

  3. 在凹多边形、细长通道等形状中递归扩散容易出现”漏”或”爆”的情况,尤其是在颜色比较不精确时更容易错填或溢出到图形外部。(特别是八联通的时候,判断的时候如果原本的直线比较细,很容易溢出到要填充的多边形的外面)

  • 思路:
  1. 取当前种子像素的颜色

  2. 若当前像素颜色既不是目标色也不是边界色则填充,且将其邻域像素依次作为种子像素重复步骤(1)、(2),直到递归结束

  • 关键代码
  1. 四邻域的递归种子填充算法
// A1_1:四邻域的种子填充法的递归填充算法
void SeedFilling_4near(int x, int y)
{
    float color[3];
    glReadPixels(x, y, 1, 1, GL_RGB, GL_FLOAT, color); /*获取屏幕上特定位置的像素颜色值 void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *data);*/
    cout << x << "," << y << ":" << color[0] << color[1] << color[2] << endl;
    if (!same_color(color, newcolor) && !same_color(color, boundarycolor)) /* 因为边界像素已经着色,且种子像素是在内部,因此基于连通性的扩散到边界像素就会停止,因此不需要想光栅化一样确定内外像素,只需要判定颜色即可*/
    {
        draw_a_point(x, y);
        SeedFilling_4near(x + 1, y);
        SeedFilling_4near(x - 1, y);
        SeedFilling_4near(x, y + 1);
        SeedFilling_4near(x, y - 1);
    }
}
  1. 八邻域的递归种子填充算法
// A1_2:八邻域的种子填充法的递归填充算法
void SeedFilling_8near(int x, int y)
{
    float color[3];
    glReadPixels(x, y, 1, 1, GL_RGB, GL_FLOAT, color); /*获取屏幕上特定位置的像素颜色值 void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *data);*/
    cout << x << "," << y << ":" << color[0] << color[1] << color[2] << endl;
    if (!same_color(color, newcolor) && !same_color(color, boundarycolor)) /* 因为边界像素已经着色,且种子像素是在内部,因此基于连通性的扩散到边界像素就会停止,因此不需要想光栅化一样确定内外像素,只需要判定颜色即可*/
    {
        draw_a_point(x, y);
        SeedFilling_8near(x + 1, y);
        SeedFilling_8near(x - 1, y);
        SeedFilling_8near(x, y + 1);
        SeedFilling_8near(x, y - 1);
        SeedFilling_8near(x + 1, y + 1);
        SeedFilling_8near(x - 1, y + 1);
        SeedFilling_8near(x + 1, y - 1);
        SeedFilling_8near(x - 1, y - 1);
    }
}

基于栈的种子填充

  • 原理:

使用显式栈结构代替系统递归调用栈:以种子像素为起点入栈,每次弹出一个像素,判断其颜色是否满足”既不是边界色也不是已填充色”的条件,若满足则着色并将其邻域符合条件的像素继续压入栈中,从而沿四邻域或八邻域方向逐步扩散完成区域填充。

  • 不足:
  1. 仍存在较大的空间开销:虽然避免了函数递归导致的系统栈溢出,但当填充区域非常大时,显式栈中会同时存放大量待处理像素,依然可能占用较多内存。

  2. 访问像素仍可能重复、效率一般:如果没有额外的访问标记,算法依旧完全依赖颜色判断,可能对同一像素多次入栈和检查,配合频繁的
    glReadPixels 调用,会带来一定性能损失。

  3. 对边界精度依赖较强:与递归法类似,在凹多边形、细长通道以及边界线较细、颜色存在浮点误差的场景下,若边界判定不够准确,仍然有可能出现局部漏填或少量越界填充的问题。

  • 思路:

(1) 种子像素入栈

(2) 当栈非空时,重复执行下述三步:

1)栈顶像素出栈

2)将出栈像素置成多边形色

3)按四向或八向检查相邻像素,若某个像素不在边界且未置成多边形色,则把该像素入栈。

  • 关键代码
// A2:用栈实现的种子填充算法
void StackSeedFilling(int x, int y)
{
    stack<point> q;
    q.push(point(x, y));
    while (!q.empty())
    {
        point now = q.top();
        q.pop();
        int nowx = now.x, nowy = now.y;
        float color[3];
        glReadPixels(nowx, nowy, 1, 1, GL_RGB, GL_FLOAT, color);
        if (!same_color(color, newcolor) && !same_color(color, boundarycolor)) /* 因为边界像素已经着色,且种子像素是在内部,因此基于连通性的扩散到边界像素就会停止,因此不需要想光栅化一样确定内外像素,只需要判定颜色即可*/
        {
            draw_a_point(nowx, nowy);
            q.push(point(nowx + 1, nowy));
            q.push(point(nowx - 1, nowy));
            q.push(point(nowx, nowy + 1));
            q.push(point(nowx, nowy - 1));
        }
    }
}

基于广度优先搜索的种子填充算法

  • 原理:

另一种形式的基于栈的种子填充算法,使用队列代替递归/栈,以种子像素为起点入队,每次从队头取出一个像素进行颜色判断,若满足”既不是边界色也不是已填充色”的条件,则对其着色并把其邻域像素继续入队,从而按”波前”方式在四邻域或八邻域方向逐层扩散完成区域填充。

  • 不足:

和基于栈的种子填充算法相同

  • 思路:

(1) 将初始种子像素坐标压入队列;

(2) 当队列非空时循环执行:

1)从队头取出一个像素,读取其颜色;

2)若该像素既不是边界色也不是填充色,则对其着色;

3)按四向或八向检查其邻域像素,若某个邻域像素在窗口范围内且颜色满足"可填充"条件,则把该像素压入队列,等待后续处理;

(3)
当队列为空时,说明所有与种子像素连通的区域像素均已处理完毕,填充结束。

  • 关键代码
// A3:用广度优先搜索实现的种子填充算法
void BFSSeedFilling(int x, int y)
{
    queue<point> q;
    q.push(point(x, y));
    while (!q.empty())
    {
        point now = q.front();
        q.pop();
        int nowx = now.x, nowy = now.y;
        float color[3];
        glReadPixels(nowx, nowy, 1, 1, GL_RGB, GL_FLOAT, color);
        if (!same_color(color, newcolor) && !same_color(color, boundarycolor))
        {
            draw_a_point(nowx, nowy);
            q.push(point(nowx + 1, nowy));
            q.push(point(nowx - 1, nowy));
            q.push(point(nowx, nowy + 1));
            q.push(point(nowx, nowy - 1));
        }
    }
}

扫描线种子填充算法

  • 原理:

以种子像素所在的扫描线为基础,先在该行上向左右扩展形成连续的填充线段(区间),再在该区间的上下相邻扫描线上寻找新的种子区间,从而通过”按行扩展 +
垂直传播”的方式完成整块区域的填充。

  • 不足:
  1. 相比简单的递归或栈 /
    队列像素级扩散,扫描线种子填充需要维护区间端点(xl,
    xr)、上下扫描线的交叠关系等,算法实现更复杂,容易在细节上出错。

  2. 对边界连续性和正确性要求较高:边界如果存在断裂、锯齿较严重或线条过细,容易导致扫描线在寻找左右终止位置时出现误判,从而造成局部漏填或越界填充。

  3. 更适合规则区域,但对极端复杂形状不够直观:对于凹多边形、包含很多细长通道或孤立小区域的图形,扫描线在划分区间和寻找上下交叠区间时逻辑较绕,不如简单的
    4 邻域 / 8 邻域扩散直观。

  • 思路:

1763259663217.png

1763259673699.png

图片来源:计算机图形学——区域填充算法(基本光栅图形算法)

(1) 初始化时,向堆栈压入种子像素,当堆栈不为空时,重复执行以下各步。

(2) 从包含种子像素的堆栈中推出区间段内的种子像素。

(3) 沿着扫描线,对种子像素的左右像素进行填充,直至遇到边界像素为止。

(4) 区间段内的最左和最右像素标记为xl, xr,在此区间[xl,
xr]内,检查与当前扫描线相邻的上下两条扫描线是否全为边界像素或已被填充过。(有交集又没有填充)

(5)
如果经过测试,这些扫描线上的像素段需要填充,则在xl和xr区间范围内,把每一像素段的最右像素作为种子像素,并压入堆栈。

  • 关键代码
// B1:扫描线种子填充法
// 输入:多边形顶点数据,顺时针排列
// 输出:多边形内部需要填充的点集
void ScanningSeedFilling(int x, int y)
{
    stack<point> q;
    q.push(point(x, y));
    while (!q.empty())
    {
        point now = q.top();
        q.pop();
        // now.y是当前的扫描线
        int seed_x = now.x, seed_y = now.y;
        float color[3];
        glReadPixels(seed_x, seed_y, 1, 1, GL_RGB, GL_FLOAT, color);

        // 如果种子点已经填充过或是边界,跳过
        if (same_color(color, newcolor) || same_color(color, boundarycolor))
            continue;

        int xl = seed_x;
        // 向左填充,找到最左边界xl
        while (!same_color(color, boundarycolor) && !same_color(color, newcolor))
        {
            draw_a_point(xl, seed_y);
            xl--;
            glReadPixels(xl, seed_y, 1, 1, GL_RGB, GL_FLOAT, color);
        }

        xl++; // 回退到最后一个有效像素

        // 向右填充,找到最右边界xr
        int xr = seed_x + 1;
        glReadPixels(xr, seed_y, 1, 1, GL_RGB, GL_FLOAT, color);
        while (!same_color(color, boundarycolor) && !same_color(color, newcolor))
        {
            draw_a_point(xr, seed_y);
            xr++;
            glReadPixels(xr, seed_y, 1, 1, GL_RGB, GL_FLOAT, color);
        }
        xr--; // 回退到最后一个有效像素
        // 检查上下两条扫描线y+1和y-1,在区间[xl, xr]内找到每一段的最右像素作为种子
        int y_above = seed_y + 1;
        int y_below = seed_y - 1;

        // 检查上方扫描线
        for (int i = xl; i <= xr; i++)
        {
            float color_above[3];
            glReadPixels(i, y_above, 1, 1, GL_RGB, GL_FLOAT, color_above);
            if (!same_color(color_above, boundarycolor) && !same_color(color_above, newcolor))
            {
                // 找到这一段的最右像素
                int j = i;
                while (j <= xr)
                {
                    glReadPixels(j, y_above, 1, 1, GL_RGB, GL_FLOAT, color_above);
                    if (same_color(color_above, boundarycolor) || same_color(color_above, newcolor))
                        break;
                    j++;
                }
                // 压入最右像素作为种子
                q.push(point(j - 1, y_above));
                i = j; // 跳过已处理的段
            }
        }

        // 检查下方扫描线
        for (int i = xl; i <= xr; i++)
        {
            float color_below[3];
            glReadPixels(i, y_below, 1, 1, GL_RGB, GL_FLOAT, color_below);
            if (!same_color(color_below, boundarycolor) && !same_color(color_below, newcolor))
            {
                // 找到这一段的最右像素
                int j = i;
                while (j <= xr)
                {
                    glReadPixels(j, y_below, 1, 1, GL_RGB, GL_FLOAT, color_below);
                    if (same_color(color_below, boundarycolor) || same_color(color_below, newcolor))
                        break;
                    j++;
                }
                // 压入最右像素作为种子
                q.push(point(j - 1, y_below));
                i = j; // 跳过已处理的段
            }
        }
    }
}

实验原理或思路

实现区域填充算法,包括递归种子填充、基于栈的种子填充、基于广度优先搜索的种子填充以及扫描线种子填充法,并通过
OpenGL 完成交互式可视化验证。

  1. 算法原理理解与对比分析
  1. 递归种子填充:从种子点出发,递归检查并填充邻域像素,结构简单但容易造成深层递归导致栈溢出,并且颜色判断可能引起重复访问或越界填充。

  2. 基于栈的种子填充:使用显式栈代替递归进行深度优先扩展,避免了系统栈溢出问题,但仍可能占用较多内存,同时依靠颜色判断会重复访问像素。

  3. 基于广度优先搜索(BFS)的种子填充:利用队列实现按层扩散的区域生长,填充过程直观、不会栈溢出,但队列中可能同时存放大量像素,空间需求较高。

  4. 扫描线种子填充算法:以扫描线为单位向左右扩展形成连续区间,再在上下扫描线上搜索新种子,效率高、像素访问少,但实现复杂,对边界连续性要求更高。

实验过程及结果

由于关键代码已经在实验内容部分展示了,这里我只展示了运行的结果,并将完整的程序放在了本段的末尾

演示视频链接

任务1: 基于递归的种子填充

  • 运行结果

1763259978496.png

1763260167311.png

1763259997818.png

任务2: 基于栈的种子填充

  • 运行结果

1763260026558.png

1763260121963.png

1763260050189.png

任务3: 基于广度优先搜索的种子填充算法

  • 运行结果

1763260059707.png

1763260080993.png

1763260072375.png

任务4: 扫描线种子填充算法

  • 运行结果

1763260091189.png

1763260103145.png

1763260110531.png

完整程序代码

#include <windows.h>
#define GLUT_DISABLE_ATEXIT_HACK
#ifdef __APPLE__
#include <GLUT/glut.h>
#else
// #include "../gl/GL.H"
// #include "../gl/GLU.H"
// #include "../gl/GLUT.H"
#include <GL/glut.h>
#endif

#include <stdlib.h>
#include <iostream>
#include <vector>
#include <queue>
#include <stack>
#include <GL/glut.h>
using namespace std;
int window_width = 800, window_height = 600;
static int slices = 16;
static int stacks = 16;

// 像素点的大小设置
static float point_size = 1.0f;        // 填充点的默认大小
static float vertex_point_size = 8.0f; // 顶点的大小(可选,用于区分顶点和填充点)

struct point
{
    int x, y;
    point() {}
    point(int xx, int yy) : x(xx), y(yy) {}
};
vector<point> vertexs; // 多边形顶点集合

float newcolor[3] = {0, 0, 1};      // 填充颜色
float boundarycolor[3] = {0, 0, 0}; // 边界颜色

void draw_a_point(int x, int y);
void draw_vertex(int x, int y);
bool same_color(float *color1, float *color2);
void SeedFilling(int x, int y);
void StackSeedFilling(int x, int y);
void ScanningSeedFilling(int x, int y);
void mymouse(int button, int state, int x, int y);

// 画点并着色(填充点)
void draw_a_point(int x, int y)
{
    glPointSize(point_size); // 设置点的大小
    glBegin(GL_POINTS);
    glColor3fv(newcolor);
    glVertex2f(x, y);
    glEnd();
    glFlush();
}

// 绘制顶点(使用不同的点大小)
void draw_vertex(int x, int y)
{
    glPointSize(vertex_point_size); // 设置顶点的大小
    glBegin(GL_POINTS);
    glColor3fv(newcolor); // 顶点使用填充颜色,或者可以用boundarycolor
    glVertex2f(x, y);
    glEnd();
    glFlush();
}

// 判断函数
bool same_color(float *color1, float *color2)
{
    bool is_same = true;
    for (int i = 0; i < 3; i++)
    {
        if (color1[i] != color2[i])
        {
            is_same = false;
            break;
        }
    }
    return is_same;
}

// A1_1:四邻域的种子填充法的递归填充算法
void SeedFilling_4near(int x, int y)
{
    float color[3];
    glReadPixels(x, y, 1, 1, GL_RGB, GL_FLOAT, color); /*获取屏幕上特定位置的像素颜色值 void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *data);*/
    cout << x << "," << y << ":" << color[0] << color[1] << color[2] << endl;
    if (!same_color(color, newcolor) && !same_color(color, boundarycolor)) /* 因为边界像素已经着色,且种子像素是在内部,因此基于连通性的扩散到边界像素就会停止,因此不需要想光栅化一样确定内外像素,只需要判定颜色即可*/
    {
        draw_a_point(x, y);
        SeedFilling_4near(x + 1, y);
        SeedFilling_4near(x - 1, y);
        SeedFilling_4near(x, y + 1);
        SeedFilling_4near(x, y - 1);
    }
}

// A1_2:八邻域的种子填充法的递归填充算法
void SeedFilling_8near(int x, int y)
{
    float color[3];
    glReadPixels(x, y, 1, 1, GL_RGB, GL_FLOAT, color); /*获取屏幕上特定位置的像素颜色值 void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *data);*/
    cout << x << "," << y << ":" << color[0] << color[1] << color[2] << endl;
    if (!same_color(color, newcolor) && !same_color(color, boundarycolor)) /* 因为边界像素已经着色,且种子像素是在内部,因此基于连通性的扩散到边界像素就会停止,因此不需要想光栅化一样确定内外像素,只需要判定颜色即可*/
    {
        draw_a_point(x, y);
        SeedFilling_8near(x + 1, y);
        SeedFilling_8near(x - 1, y);
        SeedFilling_8near(x, y + 1);
        SeedFilling_8near(x, y - 1);
        SeedFilling_8near(x + 1, y + 1);
        SeedFilling_8near(x - 1, y + 1);
        SeedFilling_8near(x + 1, y - 1);
        SeedFilling_8near(x - 1, y - 1);
    }
}

// A2:用栈实现的种子填充算法
void StackSeedFilling(int x, int y)
{
    stack<point> q;
    q.push(point(x, y));
    while (!q.empty())
    {
        point now = q.top();
        q.pop();
        int nowx = now.x, nowy = now.y;
        float color[3];
        glReadPixels(nowx, nowy, 1, 1, GL_RGB, GL_FLOAT, color);
        if (!same_color(color, newcolor) && !same_color(color, boundarycolor)) /* 因为边界像素已经着色,且种子像素是在内部,因此基于连通性的扩散到边界像素就会停止,因此不需要想光栅化一样确定内外像素,只需要判定颜色即可*/
        {
            draw_a_point(nowx, nowy);
            q.push(point(nowx + 1, nowy));
            q.push(point(nowx - 1, nowy));
            q.push(point(nowx, nowy + 1));
            q.push(point(nowx, nowy - 1));
        }
    }
}

// A3:用广度优先搜索实现的种子填充算法
void BFSSeedFilling(int x, int y)
{
    queue<point> q;
    q.push(point(x, y));
    while (!q.empty())
    {
        point now = q.front();
        q.pop();
        int nowx = now.x, nowy = now.y;
        float color[3];
        glReadPixels(nowx, nowy, 1, 1, GL_RGB, GL_FLOAT, color);
        if (!same_color(color, newcolor) && !same_color(color, boundarycolor))
        {
            draw_a_point(nowx, nowy);
            q.push(point(nowx + 1, nowy));
            q.push(point(nowx - 1, nowy));
            q.push(point(nowx, nowy + 1));
            q.push(point(nowx, nowy - 1));
        }
    }
}

// B1:扫描线种子填充法
// 输入:多边形顶点数据,顺时针排列
// 输出:多边形内部需要填充的点集

void ScanningSeedFilling(int x, int y)
{
    stack<point> q;
    q.push(point(x, y));
    while (!q.empty())
    {
        point now = q.top();
        q.pop();
        // now.y是当前的扫描线
        int seed_x = now.x, seed_y = now.y;
        float color[3];
        glReadPixels(seed_x, seed_y, 1, 1, GL_RGB, GL_FLOAT, color);

        // 如果种子点已经填充过或是边界,跳过
        if (same_color(color, newcolor) || same_color(color, boundarycolor))
            continue;

        int xl = seed_x;
        // 向左填充,找到最左边界xl
        while (!same_color(color, boundarycolor) && !same_color(color, newcolor))
        {
            draw_a_point(xl, seed_y);
            xl--;
            glReadPixels(xl, seed_y, 1, 1, GL_RGB, GL_FLOAT, color);
        }

        xl++; // 回退到最后一个有效像素

        // 向右填充,找到最右边界xr
        int xr = seed_x + 1;
        glReadPixels(xr, seed_y, 1, 1, GL_RGB, GL_FLOAT, color);
        while (!same_color(color, boundarycolor) && !same_color(color, newcolor))
        {
            draw_a_point(xr, seed_y);
            xr++;
            glReadPixels(xr, seed_y, 1, 1, GL_RGB, GL_FLOAT, color);
        }
        xr--; // 回退到最后一个有效像素
        // 检查上下两条扫描线y+1和y-1,在区间[xl, xr]内找到每一段的最右像素作为种子
        int y_above = seed_y + 1;
        int y_below = seed_y - 1;

        // 检查上方扫描线
        for (int i = xl; i <= xr; i++)
        {
            float color_above[3];
            glReadPixels(i, y_above, 1, 1, GL_RGB, GL_FLOAT, color_above);
            if (!same_color(color_above, boundarycolor) && !same_color(color_above, newcolor))
            {
                // 找到这一段的最右像素
                int j = i;
                while (j <= xr)
                {
                    glReadPixels(j, y_above, 1, 1, GL_RGB, GL_FLOAT, color_above);
                    if (same_color(color_above, boundarycolor) || same_color(color_above, newcolor))
                        break;
                    j++;
                }
                // 压入最右像素作为种子
                q.push(point(j - 1, y_above));
                i = j; // 跳过已处理的段
            }
        }

        // 检查下方扫描线
        for (int i = xl; i <= xr; i++)
        {
            float color_below[3];
            glReadPixels(i, y_below, 1, 1, GL_RGB, GL_FLOAT, color_below);
            if (!same_color(color_below, boundarycolor) && !same_color(color_below, newcolor))
            {
                // 找到这一段的最右像素
                int j = i;
                while (j <= xr)
                {
                    glReadPixels(j, y_below, 1, 1, GL_RGB, GL_FLOAT, color_below);
                    if (same_color(color_below, boundarycolor) || same_color(color_below, newcolor))
                        break;
                    j++;
                }
                // 压入最右像素作为种子
                q.push(point(j - 1, y_below));
                i = j; // 跳过已处理的段
            }
        }
    }
}

void mymouse(int button, int state, int x, int y)
{
    /*指定并绘制多边形顶点*/
    if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) /* GLUT_LEFT_BUTTON=鼠标左键, GLUT_DOWN=按下按键*/
    {
        // 使用draw_vertex函数绘制顶点
        draw_vertex(x, window_height - y);

        point p(x, window_height - y);
        vertexs.push_back(p);
        cout << "顶点" << vertexs.size() << ": (" << x << ", " << y << ")" << endl;
    }

    /*结束顶点指定,顺序连接顶点得到封闭多边形*/
    if (button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN)
    {
        glClearColor(1, 1, 1, 1); // 设置绘制窗口颜色为白色
        glColor3fv(boundarycolor);

        glBegin(GL_LINES);
        for (int i = 0; i < vertexs.size(); i++)
        {
            if (i == vertexs.size() - 1) // 画完最后一个点,使其闭合
            {
                glVertex2f(vertexs[0].x, vertexs[0].y);
                glVertex2f(vertexs[i].x, vertexs[i].y);
            }
            else
            {
                glVertex2f(vertexs[i].x, vertexs[i].y);
                glVertex2f(vertexs[i + 1].x, vertexs[i + 1].y);
            }
        }
        glEnd();
        glFlush();
        vertexs.clear();
    }

    /*指定种子点,并开始填充*/
    if (button == GLUT_MIDDLE_BUTTON && state == GLUT_DOWN)
    {
        cout << "种子像素: (" << x << ", " << y << ")" << endl;

// %%%%%%%%%在这里切换填充算法%%%%%%%%%%%
        // SeedFilling_4near(x, window_height - y); // 四邻域的递归种子填充算法
        // SeedFilling_8near(x, window_height - y); // 八邻域的递归种子填充算法
        // StackSeedFilling(x, window_height - y); // 栈种子填充算法
        BFSSeedFilling(x, window_height - y); // 广度优先搜索种子填充算法
        // ScanningSeedFilling(x, window_height - y); // 扫描线种子填充算法
    }
}

void display()
{
    glClear(GL_COLOR_BUFFER_BIT);
    glColor3f(1.0, 0.0, 0.0);
    glPointSize(point_size); // 使用全局的点大小设置
    glBegin(GL_POINTS);
    glEnd();
    glFlush();
}

int main(int argc, char *argv[])
{
    cout << "点击鼠标左键画点;" << endl
         << "点击鼠标右键结束画点,形成多边形;" << endl
         << "点击鼠标中键确定区域填充种子点位置。" << endl;
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
    glutInitWindowPosition(400, 150);
    glutInitWindowSize(window_width, window_height);
    glutCreateWindow("填充算法-谭棵");
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0, window_width, 0, window_height);
    glClearColor(1, 1, 1, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    glutDisplayFunc(&display);
    glutMouseFunc(&mymouse);
    glutMainLoop();
    return 0;
}