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

10 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.

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.

  • Sana: hi steve, could you explain to me how i would be able to use the dir function, to do a loop through a directory...
  • Nishtha: Sir, I have preprocessed the image in following steps: [1] adaptive histogram equalization [2] thresholding...
  • Kristof: I also strongly support the idea. I have just recently bumped into the problem that im2single was not...
  • Steve: David—I’ m glad you found it useful!
  • David Lalejini: I found your example very useful for finding connected nodes in a large set of input pairs. I start...
  • tommy: Dear Steve, I have a question,please if you are kind to help me regarding the accumulator array dimensions of...
  • Steve: Abc—I don’t know how to distinguish the faces. You might try posting your question in the MATLAB...
  • Manju: well if we have a few ovals within each other like in a cell how do we measure the distance from the center...
  • Steve: Manju—What do you mean? How is each region defined?
  • Manju: if we have 2-3 regions within each other how do we measure the regions of each one?

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