0

I am creating a flood fill algorithm in C++ using Eclipse IDE. The algorithm contains a vector of vectors called image. A square is drawn in this image based on user input, segmenting the image to two regions (inside and outside the square). A clicked point is taken in as input. If this point is inside the square, all points inside the square will be changed to a fill_value (in this case, 25). If it is outside the square, all pixels outside the square changes to fill_value. Here is the code:

#include <iostream>
#include <vector>
#include <queue>
#include <cstddef>
#include <stdexcept>

class Point
{
    std::size_t x_cord;
    std::size_t y_cord;
public:
    Point(std::size_t x, std::size_t y):x_cord{x}, y_cord{y}
    {

    }
    std::size_t x() const
    {
        return x_cord;
    }
    std::size_t y() const
    {
        return y_cord;
    }

};

bool check_point(Point pt, std::size_t x_dim, std::size_t y_dim)
{
    if(pt.x() >= 0 && pt.x() < x_dim && pt.y() >= 0 && pt.y() < y_dim)
    {
        return true;
    }
    return false;
}

void get_neighbors(Point& curr_point, std::queue<Point>& q, std::vector<std::vector<int>>& image, int old_val)
{
    std::vector<Point> neighbors;
    std::size_t x_dim = image.size();
    std::size_t y_dim;
    if(x_dim > 0)
    {
        y_dim = image[0].size();
    }
    if(check_point(Point{curr_point.x() - 1, curr_point.y()}, x_dim, y_dim) && image[curr_point.x() - 1][curr_point.y()] == old_val)
    {
        q.push(Point{curr_point.x() - 1, curr_point.y()});
    }
    if(check_point(Point{curr_point.x(), curr_point.y() - 1}, x_dim, y_dim) && image[curr_point.x()][curr_point.y() - 1] == old_val)
    {
        q.push(Point{curr_point.x(), curr_point.y() - 1});
    }
    if(check_point(Point{curr_point.x() + 1, curr_point.y()}, x_dim, y_dim) && image[curr_point.x() + 1][curr_point.y()] == old_val)
    {
        q.push(Point{curr_point.x() + 1, curr_point.y()});
    }
    if(check_point(Point{curr_point.x(), curr_point.y() + 1}, x_dim, y_dim) && image[curr_point.x()][curr_point.y() + 1] == old_val)
    {
        q.push(Point{curr_point.x(), curr_point.y() + 1});
    }
}

void flood_fill(std::vector<std::vector<int>>& image, Point clicked, int new_val)
{
    int old_val = image[clicked.x()][clicked.y()];
    std::queue<Point> q;
    q.push(clicked);
    while(!q.empty())
    {
        Point curr_point = q.front();
        get_neighbors(curr_point, q, image, old_val);
        image[curr_point.x()][curr_point.y()] = new_val;
        q.pop();
    }
}

void draw_square(std::vector<std::vector<int>>& image, Point top_left_corner, int length)
{
    std::size_t x_0 = top_left_corner.x();
    std::size_t y_0 = top_left_corner.y();
    std::size_t x;
    std::size_t y;
    for(x = x_0; x < x_0 + length; x++)
    {
        image[x][y_0] = 1;
        image[x][y_0 + length - 1] = 1;
    }
    for(y = y_0; y < y_0 + length; y++)
    {
        image[x_0][y] = 1;
        image[x_0 + length - 1][y] = 1;
    }
}

void print_image(std::vector<std::vector<int>>& image, std::size_t x_dim, std::size_t y_dim)
{
    for(std::size_t i = 0; i < x_dim; i++)
    {
        for(std::size_t j = 0; j < y_dim; j++)
        {
            std::cout << image[i][j] << "\t";
        }
        std::cout << "\n";
    }
    std::cout << "\n";
}

int main()
{
    try
    {
        std::size_t x_dim, y_dim;
        std::size_t x, y;
        std::size_t c_x = 0;
        std::size_t c_y = 0;
        int length;
        int fill_value = 25;
        std::cout << "Enter the dimensions of the image: \n";
        std::cin >> x_dim >> y_dim;
        std::vector<std::vector<int>> image(x_dim, std::vector<int>(y_dim, 0));
        std::cout << "Enter the top left point coordinates and length for the square: \n";
        std::cin >> x >> y >> length;
        Point top_left_corner{x, y};
        if(!check_point(top_left_corner, x_dim, y_dim) || !check_point(Point{top_left_corner.x() + length - 1, top_left_corner.y() + length - 1}, x_dim, y_dim))
        {
            throw std::out_of_range{"Invalid Access"};
        }
        draw_square(image, top_left_corner, length);
        std::cout << "Before Flood Fill: \n";
        print_image(image, x_dim, y_dim);
        std::cout << "Enter point to be clicked: \n";
        std::cin >> c_x >> c_y;
        Point clicked{c_x, c_y};
        //std::cout << "here1\n";
        if(!check_point(clicked, x_dim, y_dim))
        {
            throw std::out_of_range{"Invalid Access"};
        }
        std::cout << "here2\n";
        flood_fill(image, clicked, fill_value);
        std::cout << "After Flood Fill: \n";
        print_image(image, x_dim, y_dim);
    }
    catch(std::out_of_range& e)
    {
        std::cerr << e.what() << "\n";
    }
    return 0;
}

It works fine for some input. However, consider the following input (The array after Before Flood Fill is a program output, not an input):

Enter the dimensions of the image: 
20 20
Enter the top left point coordinates and length for the square: 
15 15 4
Before Flood Fill: 
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   1   1   1   1   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   1   0   0   1   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   1   0   0   1   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   1   1   1   1   0   
0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   

Enter point to be clicked: 
1 2

The program takes up a lot of processing after this and does not proceed or does not get terminated. My idea was that this is due to inefficient implementation of flood_fill function. When I use a debugger std::cout << "here2\n"; statement prints here2 on the console while it does not get printed while I simply run the program. So, I am not sure if it is flood_fill which is causing this issue or is it something else.

  1. Why is the behavior different while running and debugging?

  2. Kindly provide suggestions to debug.

Note: My idea was that since the value is being changed, they will automatically fail the check for being an eligible neighbor. But I see the problem with my code. By the time, the value is changed, a particular neighbor might be added many times. Both answers helped me identify this. Thank you to both.

skr
  • 914
  • 3
  • 18
  • 35
  • 1
    Different behaviour while debugging versus running in other ways is usually a sign of code over-writing memory it shouldn't (debugging, or compiling without optimisation, changes layout of data in memory used by your program, therefore the consequences of bad operations can change). You're using a `vector>` using variables as indices over a range unrelated to the actual sizes of the vectors - possibly you're running over. Beyond that, it's hard to give specific advice, since there is too much code. Read up on how to create a [mcve] - to increase chances of getting useful advice. – Peter Aug 27 '18 at 09:24
  • Your implementation of the flood fill algorithm is probably totally bogous. Flood fill on such a small image should be instantaneous even with debug builds. You need to debug this yourself using your debugger. Try to create an image as small as possible for debugging purposes. – Jabberwocky Aug 27 '18 at 09:27
  • Yes it is bogus because the point range checks are failing, as explained in my answer. The `std::size_t`s are underflowing. – N00byEdge Aug 27 '18 at 09:27
  • 1
    Fyi, `y_dim` in `get_neighbors` used indeterminately if you ever feed that function an empty vector of vectors for `image`. In such a case `y_dim`, which is not initialized, remains indeterminate when passed repeatedly to `check_point`. Prolly wanna do something about that. – WhozCraig Aug 27 '18 at 09:43
  • floodfill will eventualy pass to get_neighbors point with x==0 or y==0 or both, which in turn will lead to access element of vector with index -1 which is ub – Andrew Kashpur Aug 27 '18 at 09:51

2 Answers2

2

Your backtracking is awful. queue gets filled with millions of duplicated points. You should mark all the collected points at each iteration so they won't be added into queue again:

void get_neighbors(Point& curr_point, std::queue<Point>& q, std::vector<std::vector<int>>& image, int old_val, int new_val)
{
    std::size_t x_dim = image.size();
    std::size_t y_dim;
    if(x_dim > 0)
    {
        y_dim = image[0].size();
    }
    image[curr_point.x()][curr_point.y()] = new_val;
    if(check_point(Point{curr_point.x() - 1, curr_point.y()}, x_dim, y_dim) && image[curr_point.x() - 1][curr_point.y()] == old_val)
    {
        q.push(Point{curr_point.x() - 1, curr_point.y()});
        image[q.back().x()][q.back().y()] = new_val;
    }
    if(check_point(Point{curr_point.x(), curr_point.y() - 1}, x_dim, y_dim) && image[curr_point.x()][curr_point.y() - 1] == old_val)
    {
        q.push(Point{curr_point.x(), curr_point.y() - 1});
        image[q.back().x()][q.back().y()] = new_val;
    }
    if(check_point(Point{curr_point.x() + 1, curr_point.y()}, x_dim, y_dim) && image[curr_point.x() + 1][curr_point.y()] == old_val)
    {
        q.push(Point{curr_point.x() + 1, curr_point.y()});
        image[q.back().x()][q.back().y()] = new_val;
    }
    if(check_point(Point{curr_point.x(), curr_point.y() + 1}, x_dim, y_dim) && image[curr_point.x()][curr_point.y() + 1] == old_val)
    {
        q.push(Point{curr_point.x(), curr_point.y() + 1});
        image[q.back().x()][q.back().y()] = new_val;
    }
}

void flood_fill(std::vector<std::vector<int>>& image, Point clicked, int new_val)
{
    int old_val = image[clicked.x()][clicked.y()];
    if(old_val == new_val)
    {
         return;
    }
    std::queue<Point> q;
    q.push(clicked);
    while(!q.empty())
    {
        Point curr_point = q.front();
        get_neighbors(curr_point, q, image, old_val, new_val);
        q.pop();
    }
}
user7860670
  • 35,849
  • 4
  • 58
  • 84
  • My idea was that since the value is being changed, they will automatically fail the check for being an eligible neighbor. But I see the problem with my code. By the time, the value is changed, a particular neighbor might be added many times. – skr Aug 27 '18 at 09:51
  • 1
    It should be noted that this does not work when `new_val == old_val`. That case should probably be handled. This also needs the change to signed values. – N00byEdge Aug 27 '18 at 09:59
  • @N00byEdge There is not need to change to signed values. And this works when new_val == old_val. – user7860670 Aug 27 '18 at 10:30
0

std::size_t is an unsigned integral type. When it reaches a negative value, it wraps around to its max value, a positive one. Therefore all your x >= 0 and y >= 0 are always true. Try swapping out all your size_t for something like int.

Your other problem is that your flood fill queue are reaching tremendous sizes. This is because points are added faster than they are removed. You need some way to tell if a point has been flood filled before:

void get_neighbors(Point& curr_point, std::queue<Point>& q, std::vector<std::vector<int>>& image, int old_val, std::vector<std::vector<bool>> &visited)
{
  std::vector<Point> neighbors;
  int x_dim = image.size();
  int y_dim;
  if (x_dim > 0)
  {
    y_dim = image[0].size();
  }
  if (check_point(Point{ curr_point.x() - 1, curr_point.y() }, x_dim, y_dim) && image[curr_point.x() - 1][curr_point.y()] == old_val && !visited[curr_point.x() - 1][curr_point.y()])
  {
    q.push(Point{ curr_point.x() - 1, curr_point.y() });
    visited[curr_point.x() - 1][curr_point.y()] = true;
  }
  if (check_point(Point{ curr_point.x(), curr_point.y() - 1 }, x_dim, y_dim) && image[curr_point.x()][curr_point.y() - 1] == old_val && !visited[curr_point.x()][curr_point.y() - 1])
  {
    q.push(Point{ curr_point.x(), curr_point.y() - 1 });
    visited[curr_point.x()][curr_point.y() - 1] = true;
  }
  if (check_point(Point{ curr_point.x() + 1, curr_point.y() }, x_dim, y_dim) && image[curr_point.x() + 1][curr_point.y()] == old_val && !visited[curr_point.x() + 1][curr_point.y()])
  {
    q.push(Point{ curr_point.x() + 1, curr_point.y() });
    visited[curr_point.x() + 1][curr_point.y()] = true;
  }
  if (check_point(Point{ curr_point.x(), curr_point.y() + 1 }, x_dim, y_dim) && image[curr_point.x()][curr_point.y() + 1] == old_val && !visited[curr_point.x()][curr_point.y() + 1])
  {
    q.push(Point{ curr_point.x(), curr_point.y() + 1 });
    visited[curr_point.x()][curr_point.y() - 1] = true;
  }
}

void flood_fill(std::vector<std::vector<int>>& image, Point clicked, int new_val)
{
  if (image.empty()) return;
  int old_val = image[clicked.x()][clicked.y()];
  std::vector<std::vector<bool>> visisted(image.size(), std::vector<bool>(image[0].size(), false));
  std::queue<Point> q;
  q.push(clicked);
  while (!q.empty())
  {
    Point curr_point = q.front();
    get_neighbors(curr_point, q, image, old_val, visisted);
    image[curr_point.x()][curr_point.y()] = new_val;
    q.pop();
  }
}
N00byEdge
  • 1,106
  • 7
  • 18
  • The down-vote is mine. Negative array indices cause undefined behaviour just as much as indices that are "wrapped around" to a very large value. – Peter Aug 27 '18 at 09:27
  • 1
    No, signed under-/overflow is not undefined behaviour, only signed is. The array accesses only happens after `check_point`, but that function fails for negative integers because of underflow. – N00byEdge Aug 27 '18 at 09:31
  • @N00byEdge Changing `std::size_t` to `int` has not yet fixed it. I am debugging the whole thing again to see what's wrong. – skr Aug 27 '18 at 09:34
  • After making the type change, now only your flood fill is broken. You're adding things back to the queue faster than you're removing them. You now just need to mark each point as visited. – N00byEdge Aug 27 '18 at 09:46
  • @N00byEdge - I didn't say unsigned overflow gives undefined behaviour. I said negative array indices do. At most, all your suggestion does is change details of whatever the real problem is. – Peter Aug 27 '18 at 09:46
  • Okay, answer all cleaned up. What I've provided now actually just works, provided that you've changed out all `std::size_t` for `int`. – N00byEdge Aug 27 '18 at 09:55
  • @Peter I think you can retract your downvote, now the answer is complete and correct. – Jabberwocky Aug 27 '18 at 10:08
  • You've should've deleted your answer because unsigned underflow has nothing to do with problem instead of appending content from correct answer. – user7860670 Aug 27 '18 at 10:32