我一直在寻找一个可行的泛洪填充算法。我尝试了许多算法,只有“递归线性填充”算法表现得完全符合预期,但它有一个严重的缺点,即有时会导致堆栈溢出。 :(
我已经尝试了许多非递归实现,但它们都非常难以处理:要么留下奇怪的空洞,要么将整个区域填满(当它们应该被包围时)。
请问是否有任何非递归的泛洪填充源代码,使用C编写(或者是不太OOP,容易解开的C++)?
我一直在寻找一个可行的泛洪填充算法。我尝试了许多算法,只有“递归线性填充”算法表现得完全符合预期,但它有一个严重的缺点,即有时会导致堆栈溢出。 :(
我已经尝试了许多非递归实现,但它们都非常难以处理:要么留下奇怪的空洞,要么将整个区域填满(当它们应该被包围时)。
请问是否有任何非递归的泛洪填充源代码,使用C编写(或者是不太OOP,容易解开的C++)?
可以使用一个固定大小的 int 对数组来实现 int 对栈的堆栈 (例如像素图像的大小或其平方根),并使用 int 来跟踪顶部。
这是一些实现非递归泛洪填充的 C# 代码:
private static void Floodfill(byte[,] vals, Point q, byte SEED_COLOR, byte COLOR)
{
int h = vals.GetLength(0);
int w = vals.GetLength(1);
if (q.Y < 0 || q.Y > h - 1 || q.X < 0 || q.X > w - 1)
return;
Stack<Point> stack = new Stack<Point>();
stack.Push(q);
while (stack.Count > 0)
{
Point p = stack.Pop();
int x = p.X;
int y = p.Y;
if (y < 0 || y > h - 1 || x < 0 || x > w - 1)
continue;
byte val = vals[y, x];
if (val == SEED_COLOR)
{
vals[y, x] = COLOR;
stack.Push(new Point(x + 1, y));
stack.Push(new Point(x - 1, y));
stack.Push(new Point(x, y + 1));
stack.Push(new Point(x, y - 1));
}
}
}
这里有一些 C++ 代码可以实现你想要的功能。它使用了队列,并且在向队列插入元素时更加高效。
connectedRegion(const Point& source, RegionType& region, const Color target)
{
Color src_color = color_of(source, region);
if (region.count(source) == 0 || src_color == target)
return;
std::queue<Point> analyze_queue;
analyze_queue.push(source);
while (!analyze_queue.empty())
{
if (color_of(analyze_queue.front()) != src_color)
{
analyze_queue.pop();
continue;
}
Point leftmost_pt = analyze_queue.front();
leftmost_pt.col -= 1;
analyze_queue.pop();
Point rightmost_pt = leftmost_pt;
rightmost_pt.col += 2;
while (color_of(leftmost_pt, region) == src_color)
--leftmost_pt.col;
while (color_of(rightmost_pt, region) == src_color)
++rightmost_pt.col;
bool check_above = true;
bool check_below = true;
Point pt = leftmost_pt;
++pt.col;
for (; pt.col < rightmost_pt.col; ++pt.col)
{
set_color(pt, region, target);
Point pt_above = pt;
--pt_above.row;
if (check_above)
{
if (color_of(pt_above, region) == src_color)
{
analyze_queue.push(pt_above);
check_above = false;
}
}
else // !check_above
{
check_above = (color_of(pt_above, region) != src_color);
}
Point pt_below = pt;
++pt_below.row;
if (check_below)
{
if (color_of(pt_below, region) == src_color)
{
analyze_queue.push(pt_below);
check_below = false;
}
}
else // !check_below
{
check_below = (color_of(pt_below, region) != src_color);
}
} // for
} // while queue not empty
return connected;
}
typedef struct queue_ { struct queue_ *next; } queue_t;
typedef struct ffnode_ { queue_t node; int x, y; } ffnode_t;
/* returns the new head of the queue after adding node to the queue */
queue_t* enqueue(queue_t *queue, queue_t *node) {
if (node) {
node->next = queue;
return node;
}
return NULL;
}
/* returns the head of the queue and modifies queue to be the new head */
queue_t* dequeue(queue_t **queue) {
if (queue) {
queue_t *node = (*queue);
(*queue) = node->next;
node->next = NULL;
return node;
}
return NULL;
}
ffnode_t* new_ffnode(int x, int y) {
ffnode_t *node = (ffnode_t*)malloc(sizeof(ffnode_t));
node->x = x; node->y = y;
node->node.next = NULL;
return node;
}
void flood_fill(image_t *image, int startx, int starty,
color_t target, color_t replacement) {
queue_t *head = NULL;
ffnode_t *node = NULL;
if (!is_color(image, startx, starty, target)) return;
node = new_ffnode(startx, starty);
for ( ; node != NULL; node = (ffnode_t*)dequeue(&head)) {
if (is_color(image, node->x, node->y, target)) {
ffnode_t *west = node, *east = node;
recolor(image, node->x, node->y, replacement);
/* 1. move w to the west until the color of the node to the west
no longer matches target */
...
}
}
}
是否有证明所有递归函数都可以通过使用本地数据来模拟堆栈的迭代函数实现的证据?您可以使用std::vector创建类似算法的堆栈行为,而不会使堆栈溢出,因为它将使用堆。
编辑:我注意到您正在使用C语言,因此您可以通过realloc实现类似的行为,因为您需要向您将使用的任何数据结构的本地“堆栈”添加更多元素。
我不知道我的答案是否完全与你提出的问题相关,但接下来我提供我的Flood-Fill算法的C语言版本,它不使用递归调用。
2017年1月11日:新版本;已成功测试两个位图。
它只使用一个新点偏移量队列,作用于双缓冲区的窗口:WinnOffs-(WinDimX,WinDimY) 的 *VBuffer(屏幕或图像的副本),并且可选地写入洪水填充结果的掩码(*ExtraVBuff)。在调用之前必须使用0填充ExtraVBuff(如果您不需要掩码,则可以将ExtraVBuff设置为NULL);在调用之后使用它,您可以进行渐变洪水填充或其他绘画效果。NewFloodFill使用每像素32位,并且是一个C函数。我在1991年重新发明了这个算法(我用Pascal写过它),但现在它使用每像素32位的C语言编写;也不使用任何函数调用,仅在从队列中“弹出”后执行除法,并且永远不会溢出队列,如果它的大小正确(大约为图像像素的1/4),则始终允许正确地填充任何区域;我先展示c函数(FFILL.C),然后是测试程序(TEST.C):
#define IMAGE_WIDTH 1024
#define IMAGE_HEIGHT 768
#define IMAGE_SIZE IMAGE_WIDTH*IMAGE_HEIGHT
#define QUEUE_MAX IMAGE_SIZE/4
typedef int T_Queue[QUEUE_MAX];
typedef int T_Image[IMAGE_SIZE];
void NewFloodFill(int X,
int Y,
int Color,
int BuffDimX,
int WinOffS,
int WinDimX,
int WinDimY,
T_Image VBuffer,
T_Image ExtraVBuff,
T_Queue MyQueue)
/* Replaces all pixels adjacent to the first pixel and equal to this; */
/* if ExtraVBuff == NULL writes to *VBuffer (eg BUFFER of 786432 Pixel),*/
/* otherwise prepare a mask by writing on *ExtraVBuff (such BUFFER must */
/* always have the same size as *VBuffer (it must be initialized to 0)).*/
/* X,Y: Point coordinates' of origin of the flood-fill. */
/* WinOffS: Writing start offset on *VBuffer and *ExtraVBuff. */
/* BuffDimX: Width, in number of Pixel (int), of each buffer. */
/* WinDimX: Width, in number of Pixel (int), of the window. */
/* Color: New color that replace all_Pixel == origin's_point. */
/* WinDimY: Height, in number of Pixel (int), of the window. */
/* VBuffer: Pointer to the primary buffer. */
/* ExtraVBuff: Pointer to the mask buffer (can be = NULL). */
/* MyQueue: Pointer to the queue, containing the new-points' offsets*/
{
int VBuffCurrOffs=WinOffS+X+Y*BuffDimX;
int PixelIn=VBuffer[VBuffCurrOffs];
int QueuePnt=0;
int *TempAddr=((ExtraVBuff) ? ExtraVBuff : VBuffer);
int TempOffs1;
int TempX1;
int TempX2;
char FLAG;
if (0<=X && X<WinDimX && 0<=Y && Y<WinDimY) do
{
/* Fill to left the current line */
TempX2=X;
while (X>=0 && PixelIn==VBuffer[VBuffCurrOffs])
{
TempAddr[VBuffCurrOffs--]=Color;
--X;
}
TempOffs1=VBuffCurrOffs+1;
TempX1=X+1;
/* Fill to right the current line */
VBuffCurrOffs+=TempX2-X;
X=TempX2;
while (X+1<WinDimX && PixelIn==VBuffer[VBuffCurrOffs+1])
{
++X;
TempAddr[++VBuffCurrOffs]=Color;
}
TempX2=X;
/* Backward scan of the previous line; puts new points offset in Queue[] */
if (Y>0)
{
FLAG=1;
VBuffCurrOffs-=BuffDimX;
while (X-->=TempX1)
{
if (PixelIn!=VBuffer[VBuffCurrOffs] ||
ExtraVBuff && Color==ExtraVBuff[VBuffCurrOffs])
FLAG=1;
else
if (FLAG)
{
FLAG=0;
if (QueuePnt<QUEUE_MAX)
MyQueue[QueuePnt++]=VBuffCurrOffs;
}
--VBuffCurrOffs;
}
}
/* Forward scan of the next line; puts new points offset in Queue[] */
if (Y<WinDimY-1)
{
FLAG=1;
VBuffCurrOffs=TempOffs1+BuffDimX;
X=TempX1;
while (X++<=TempX2)
{
if (PixelIn!=VBuffer[VBuffCurrOffs] ||
ExtraVBuff && Color==ExtraVBuff[VBuffCurrOffs])
FLAG=1;
else
if (FLAG)
{
FLAG=0;
if (QueuePnt<QUEUE_MAX)
MyQueue[QueuePnt++]=VBuffCurrOffs;
}
++VBuffCurrOffs;
}
}
/* Gets a new point offset from Queue[] */
if (--QueuePnt>=0)
{
VBuffCurrOffs=MyQueue[QueuePnt];
TempOffs1=VBuffCurrOffs-WinOffS;
X=TempOffs1%BuffDimX;
Y=TempOffs1/BuffDimX;
}
/* Repeat the main cycle until the Queue[] is not empty */
} while (QueuePnt>=0);
}
这里有一个测试程序:
#include <stdio.h>
#include <malloc.h>
#include "ffill.c"
#define RED_COL 0xFFFF0000
#define WIN_LEFT 52
#define WIN_TOP 48
#define WIN_WIDTH 920
#define WIN_HEIGHT 672
#define START_LEFT 0
#define START_TOP 671
#define BMP_HEADER_SIZE 54
typedef char T_Image_Header[BMP_HEADER_SIZE];
void main(void)
{
T_Image_Header bmpheader;
T_Image *image;
T_Image *mask;
T_Queue *MyQueue;
FILE *stream;
char *filename1="ffill1.bmp";
char *filename2="ffill2.bmp";
char *filename3="ffill3.bmp";
int bwritten;
int bread;
image=malloc(sizeof(*image));
mask=malloc(sizeof(*mask));
MyQueue=malloc(sizeof(*MyQueue));
stream=fopen(filename1,"rb");
bread=fread(&bmpheader, 1, BMP_HEADER_SIZE, stream);
bread=fread((char *)image, 1, IMAGE_SIZE<<2, stream);
fclose(stream);
memset(mask,0,IMAGE_SIZE<<2);
NewFloodFill(START_LEFT,
START_TOP,
RED_COL,
IMAGE_WIDTH,
IMAGE_WIDTH*WIN_TOP+WIN_LEFT,
WIN_WIDTH,
WIN_HEIGHT,
*image,
NULL,
*MyQueue);
stream=fopen(filename2,"wb+");
bwritten=fwrite(&bmpheader, 1, BMP_HEADER_SIZE, stream);
bwritten=fwrite((char *)image, 1, IMAGE_SIZE<<2, stream);
fclose(stream);
stream=fopen(filename3,"wb+");
bwritten=fwrite(&bmpheader, 1, BMP_HEADER_SIZE, stream);
bwritten=fwrite((char *)mask, 1, IMAGE_SIZE<<2, stream);
fclose(stream);
free(MyQueue);
free(mask);
free(image);
}
创建半径为10个体素的球围绕起始点,并将该半径内的所有体素标记为“待访问”
如果当前体素 > 阈值,则插入1。
前往邻居[+1,-1,0](还要检查是否不重新访问任何体素),如果neighbor.getVoxVal = 0(目标体积的初始化值),则它落在球的边界上,在不同的堆栈中插入坐标。(这将是我们下一个球的起始点)
半径=半径+10(体素)
我在C语言中进行了一个简单的实现。
在我的例子中的背景:
#define EMPTY ' '
#define REVEALED '-'
#define GRID_SET(P, X) (grid[W * (P).y + (P).x] = (X))
typedef struct {
int x, y;
} vec2_t;
char grid[H * W] = "##########"
"##########"
"## #####"
"## ####"
"# ###"
"# #"
"# ##"
"## ###"
"##########"
"##########";
static bool is_empty(vec2_t p) {
return p.x >= 0 && p.x < W && p.y >= 0 && p.y < H &&
grid[W * p.y + p.x] == EMPTY;
}
static vec2_t queue_pop_front(vec2_t* q, int* top) {
vec2_t ret = q[0];
/* Shift. Note that `top` is not the last pushed value, but the next one */
*top -= 1;
for (int i = 0; i < *top; i++)
q[i] = q[i + 1];
return ret;
}
static void queue_push(vec2_t* q, int* top, vec2_t x) {
q[*top] = x;
*top += 1;
}
static void flood_fill(vec2_t p) {
/* First in, first out */
vec2_t* queue = malloc(W * H * sizeof(vec2_t));
int queue_pos = 0;
/* Push parameter and reveal it */
queue_push(queue, &queue_pos, p);
GRID_SET(p, REVEALED);
/* Queue is not empty */
while (queue_pos > 0) {
const vec2_t cur = queue_pop_front(queue, &queue_pos);
vec2_t up = { cur.x, cur.y - 1 };
if (is_empty(up)) {
queue_push(queue, &queue_pos, up);
GRID_SET(up, REVEALED);
}
vec2_t down = { cur.x, cur.y + 1 };
if (is_empty(down)) {
queue_push(queue, &queue_pos, down);
GRID_SET(down, REVEALED);
}
vec2_t left = { cur.x - 1, cur.y };
if (is_empty(left)) {
queue_push(queue, &queue_pos, left);
GRID_SET(left, REVEALED);
}
vec2_t right = { cur.x + 1, cur.y };
if (is_empty(right)) {
queue_push(queue, &queue_pos, right);
GRID_SET(right, REVEALED);
}
}
free(queue);
}
www.youtube.com/watch?v=4hQ1wA4Sl4c
这里有一个页面,我试图解释它的工作原理。 http://unity3dmc.blogspot.com/2017/02/ultimate-3d-floodfill-scanline.html?m=1
还有代码。
如果递归能够保持有序,那么递归将是最快的。
如果你通过数据网格(图像)进行递归,你也可以以网格格式存储递归的处理,因为处理步骤代表来自网格的像素,而不是将结果展开成树形格式。
对于我的数据结构,我使用了Dave Hanson的C语言接口与实现中的我不敢用简单递归尝试深度优先搜索;递归的深度仅受限于REDACTED,我的实验表明,即使是PROBLEM REDACTED也可能需要超过一百万的堆栈深度。因此,我将堆栈放在一个辅助数据结构中。使用明确的堆栈实际上使尝试广度优先搜索变得容易,并且事实证明,广度优先搜索可以使用比深度优先搜索少四十倍的空间。
Seq_T
;从深度优先到广度优先的更改只需要更改一个函数调用。