Steve on Image Processing

February 25th, 2008

Neighbor indexing

Earlier this year I wrote about logical indexing and linear indexing.

Now I want to finish this little indexing series with an example of a technique I call neighbor indexing. I've written about neighbor indexing before, but in this post I'll show a more complete example of its application to image processing.

Many image processing operations can be classified as recursive neighborhood operations. (Thanks to Chris for suggesting this term to me.) You do something to each neighbor of a pixel, then you do something to each neighbor of those neighbors, and so on. Flood filling is a good example, and neighbor indexing is a good way to implement this type of operation in MATLAB. If you implement your own image processing algorithms in MATLAB, you'll probably find neighbor indexing useful.

I'll start by reviewing the basics of neighbor indexing, and then I'll show how to use it to implement flood filling.

Let's make a sample 5-by-5 matrix and consider three seed locations, or starting locations.

A = magic(5)
A =

    17    24     1     8    15
    23     5     7    14    16
     4     6    13    20    22
    10    12    19    21     3
    11    18    25     2     9

Starting locations at (2,2), (1,4), (3,4):

rows = [2 1 3];
cols = [2 4 4];

As you'll see, the neighbor indexing technique is based on linear indexing, so let's convert the starting locations to linear indices:

idx = sub2ind(size(A), rows, cols)
idx =

     7    16    18

The linear indices can be used to extract the pixel values of each of the three seed locations:

A(idx)
ans =

     5     8    20

How do you find the linear indices of the eastern neighbor of each of the starting locations?

Answer: Add the number of rows

M = size(A, 1);
east_neighbors = idx + M
east_neighbors =

    12    21    23

M is called the linear offset for the eastern direction.

A(east_neighbors) gives you the pixel values of the eastern neighbors:

A(east_neighbors)
ans =

     7    15    22

What is the linear offset for the northern direction?

northern_offset = -1;
northern_neighbors = idx + northern_offset
northern_neighbors =

     6    15    17

OOPS! The (1,4) pixel doesn't have a northern neighbor! Subtracting 1 from the linear indexing of a pixel on the top row will just give you the pixel on the bottom row of the previous column.

The neighbor indexing technique DOES NOT WORK FOR PIXELS ON THE BORDERS!

Therefore some form of padding is usually necessary.

Here are the offsets corresponding to all eight neighbor directions:

 East         M
 Southeast    M + 1
 South        1
 Southwest   -M + 1
 West        -M
 Northwest   -M - 1
 North       -1
 Northeast    M - 1

The function bsxfun gives you a way to compute all the neighbors of a set of pixels in a single, vectorized expression.

For example, suppose the linear indices of our seed pixels are:

idx = [275 2348 8100]
idx =

         275        2348        8100

And we are interested in finding the north, east, south, and west neighbors of all these pixels:

% Assume a 100-by-100 image for this example.
M = 100;
neighbor_offsets = [-1, M, 1, -M]';

Add every neighbor_offset to every linear index:

neighbors = bsxfun(@plus, idx, neighbor_offsets)
neighbors =

         274        2347        8099
         375        2448        8200
         276        2349        8101
         175        2248        8000

Now we have enough indexing tools to implement a flood-fill algorithm. Let's try it with this image and starting location:

bw = bwperim(imread('circles.png'));
imshow(bw)

r = 100;
c = 100;
hold on
plot(c, r, 'g*')
hold off

Pad the image with ones. This step will prevent the flood filling operation from reaching the border of the image.

bw = padarray(bw, [1 1], 1);

Initialize a linear index array containing the "active pixels." This is the set of pixels to be changed from black to white. Remember that we have to add 1 to the original row and column coordinates because of the padding step above.

active_pixels = sub2ind(size(bw), r+1, c+1);
bw_out = bw;
M = size(bw, 1);
neighbor_offsets = [-1, M, 1, -M];

Our main processing loop will iterate until there are no more active pixels. In each loop iteration, we set the active pixels to 1 in the output image. Then we get all the neighbors of all the active pixels, and we make those neighbors the new active pixels.

That's the basic logic, but there are two special steps you should keep in mind if you use this technique yourself. First, we need to remove from the active pixel list the pixels that are already 1. If we don't do this, the flood fill operation will not stop at the white boundaries.

Second, we need to "dedup" the active pixel list at each iteration. That is, when you find all the neighbors of all the active pixels, there will be duplicates in the list. If you don't prune out the duplicates at each iteration, the number of duplicates can quickly grow to be quite large, causing the algorithm to run slowly and to use a lot of memory.

Here's the main processing loop:

while ~isempty(active_pixels)
    % Set the active pixels to 1.
    bw_out(active_pixels) = 1;

    % The new active pixels list is the set of neighbors of the
    % current list.
    active_pixels = bsxfun(@plus, active_pixels, neighbor_offsets);
    active_pixels = active_pixels(:);

    % Remove from the active_pixels list pixels that are already
    % 1.  This is how we keep the flood fill operation "inside"
    % the white boundaries.
    active_pixels(bw_out(active_pixels)) = [];

    % Remove duplicates from the list.
    active_pixels = unique(active_pixels);
end

Remember to remove the extra padding:

bw_out = bw_out(2:end-1, 2:end-1);

And here's the final result:

imshow(bw_out)


Get the MATLAB code

Published with MATLAB® 7.5

12 Responses to “Neighbor indexing”

  1. Brett Shoelson replied on :

    Great column, Steve–very englightening. Really nice motivation/usage for the somewhat esoteric BSXFUN.

    I initially wondered if repeated calls to UNIQUE were presenting a bottleneck, but the code runs very fast (profiling on my machine: 234 calls to unique took 30 msec), and the alternate approaches I tried–including using SETDIFF to compare a dynamic list of evaluated pixels–all increased the processing time considerably. (I love how easy it is to compare alternate approaches in MATLAB!)

  2. Administrator replied on :

    Brett—I understand your initial reaction to the use of UNIQUE, and I have profiled it myself in the past. I haven’t found it to be a bottleneck, and the code sure does tend to bog down if you leave it out!

  3. Urs (us) Schwarz replied on :

    i fully agree with brett: great blog, steve!
    here’s what i would do to decrease processing time by about 30%, though

    while ~isempty(active_pixels)
        bw_out(active_pixels)=1;
        active_pixels=bsxfun(@plus,active_pixels(:),neighbor_offsets);
        active_pixels(bw_out(active_pixels))=[];
    % a poor man's UNIQUE...
        active_pixels=sort(active_pixels);
        active_pixels=active_pixels(histc(active_pixels,active_pixels)~=0);
    end
    

    urs
    (i hope this input window does NOT wrap the code…?)

  4. Pat Finder replied on :

    Where is bsxfun implemented? I have a collection of toolboxes, but don’t have bscfun.
    - Pat

  5. Steve replied on :

    Pat—bsxfun is in MATLAB. It was introduced in R2007a.

  6. Malambo replied on :

    Steve,
    I have tried using the bsxfun to get 8-neighbors for first pixel. One of the values i am getting does not make sense. I am working with an 81 by 83 image and 81 is returned as one of the neighbors for pixel with linear index 1. My code is as shown below:

    seed_pt=1;
    neighbor_offsets =[M, M+1, 1, -M+1, -M, -M-1, -1, M-1];
    nghood = bsxfun(@plus, seed_pt, neighbor_offsets);
    nghood=nghood';
    nd=size(nghood,1);
    cnt=1;
    clear local_data
    for j=1:nd
           x0=nghood(j);
           yn=ceil(x0/M); %convert to column coordinate
           xn=rem(x0,M);  %convert to row coordinate
           if xn==0, xn=M;end
           if (((xn>=1)&&(yn>=1))&&((xn<=M)&&(yn<=N)))
             local_data(cnt,:)=reshape(I(xn,yn,:),1,3);
             cnt=cnt+1;
    
           end
    end
    
  7. Steve replied on :

    Malambo—Please look closely at the portion of my post where I say that “The neighbor indexing technique DOES NOT WORK FOR PIXELS ON THE BORDERS!” Also, you might consider using ind2sub to convert from linear indices to subscripts.

  8. Malambo replied on :

    Thanks Steve for your response.
    To avoid padding the image I decided to use this:

     if (((xn>=1)&&(yn>=1))&&((xn<=M)&&(yn<=N)))
    

    This restricts the coordinates of the pixels so that only those in the image will be assigned to the local_data matrix. This works well with 4-neighbor but there is one wrong index when I use the 8-neighbor offsets. This is what I really want to know.

  9. Steve replied on :

    Malambo—I have no idea what your for-loop is doing, but these three lines before the for-loop are getting off to a bad start:

    seed_pt=1;
    neighbor_offsets =[M, M+1, 1, -M+1, -M, -M-1, -1, M-1];
    nghood = bsxfun(@plus, seed_pt, neighbor_offsets);
    

    Your initial seed_pt is at the upper-left of the image, on the boundary. Therefore your computation of its neighborhood using neighbor indexing on the next two lines doesn’t work correctly.

  10. Malambo replied on :

    Steve,
    Thank you once more. I am using the flood filling ideas discussed here to implement a region growing segmentation. This blog has really help me to write code that runs fast.
    Let re-examine my code and i will get back to you.

  11. Subh replied on :

    Hi Steve,

    I would like to compute all 8 neighbors for each pixel in a 20×40 image matrix except for the borders (1-pixel width). I do the following:

      [cols, rows] = meshgrid(2:19, 2:39);
      id = sub2ind(size(img), rows, cols);
      M = size(img,1);
      msk = [-M, -M+1, 1, M+1, M, M-1, -1, -M-1];
      nbrs = bsxfun(@plus, id, msk);
    

    However, I get an error in the bsxfun():
    Non-singleton dimensions of the two input arrays must match each other.

    What should I be doing in order to get an 8-dimensional vector corresponding to each pixels in the ROI (2:19, 2:39) of my image? Is there a straight forward approach for this?

  12. Steve replied on :

    Subh—id needs to be a column vector.

    id = id(:);
    

Leave a Reply

Wrap code fragments inside <pre> tags, like this:

<pre class="code">
a = magic(3);
sum(a)
</pre>

If you have a "<" character in your code, either follow it with a space or replace it with "&lt;" (including the semicolon).


Steve Eddins manages the Image & Geospatial development team at The MathWorks and coauthored Digital Image Processing Using MATLAB. He writes here about image processing concepts, algorithm implementations, and MATLAB.

  • Frida: I totally agree. I want to find a global average threshold from a large number of images so my initial...
  • Steve: Dansheng—The following code will give you the binary image corresponding to a single object. L =...
  • Dansheng Song: Hi Steve, Your posts are very helpful for Matlab users. Do you know how can I get the binary image by...
  • DP: Thanks Steve. it did work. now can easily calculate DSC.
  • Steve: DP— A & B
  • DP: Hi Steve, Dice similarity coefficient is defined to find the overlapped regions between two objects in an image....
  • Steve: Searching— axis on
  • Steve: DP—Sorry, but I’ve never heard of dice similarity coefficients.
  • searching: Hey, I found this place extremely useful. I have a question though. Although I manage to plot my points on...
  • DP: Hi Steve, thanks for your email. i could not seperate those connected objects. Instead i am trying to compare...

These postings are the author's and don't necessarily represent the opinions of The MathWorks.