Cris’ Image Analysis Blog

theory, methods, algorithms, applications

A simple implementation of snakes

In this post I want to show how easy it is to implement snakes using MATLAB and DIPimage. The next release of DIPimage will have some functionality implementing snakes, and this post is based on code written for that. The first implementation of snakes I made for DIPimage used a custom class, dip_snake, to simplify usage. We decided eventually to do without the class. The code discussed here uses that first implementation, which you can download here [Note: the code in the ZIP file requires DIPimage 2, and is not compatible with DIPimage 3]. The most important bits of the code are identical to that in the next release of DIPimage, but the wrapping is not. Adding the class is a nice excuse to show how to modify DIPimage to understand a new parameter type. I will discuss that in a future post.

Snake theory

A snake (a.k.a. active contour) is a flexible 2D line, which is moved around the image to minimize an energy functional. The line is parametrized by through a variable \(p\) that goes from 0 to 1. Two functions of this variable, \(x(p)\) and \(y(p)\) , define the coordinates of the points along the line. For simplicity, we use the vector \(\vec{s}(p) = (x(p),y(p))^T\) . More often than not, the snake is implemented as a closed contour, because it is most often used to segment an object. This is enforced setting \(\vec{s}(0) = \vec{s}(1)\) .

The energy functional that we want to minimize is defined as:

\[ E = \int{ E_\mathrm{int}\left(\vec{s}(p)\right) + E_\mathrm{ext}\left(\vec{s}(p)\right) \mathrm{d}p }\;. \]

\(E_\mathrm{int}\) is an internal energy, meant to force the snake to be small and smooth. It avoids obviously wrong solutions. \(E_\mathrm{ext}\) is an external energy, the one responsible for the snake finding the edges of an object in the image. \(E_\mathrm{ext}\) is therefore defined on the image. A common external energy is the inverse of the gradient magnitude: low energies at the location of the edges, higher energies everywhere else. This energy definition comes from the original paper by Kass, Witkin and Terzopoulos (1988).

Let’s discuss the internal energy first. It is defined as

\[ E_\mathrm{int}\left(\vec{s}(p)\right) = \frac{1}{2} \left\{ \alpha(p) |\vec{s}'(p)|^2 + \beta(p) |\vec{s}''(p)|^2 \right\}\;. \]

This looks rather complicated, but it need not be. For starters, \(\alpha\) and \(\beta\) are usually constant (the method has enough parameters as it is, there is no point in making two of the parameters different for every point along the snake!). So what we have left is the magnitude of the first derivative, which is larger for longer snakes, and the magnitude of the second derivative, which is larger for sharper bends. The first part keeps the snake short, the second part keeps it straight. The two parameters, \(\alpha\) and \(\beta\) , define the relative importance of these two terms.

To minimize this energy function we use the Euler-Lagrange equation. After quite some work we end up with the following equation:

\[ \alpha \vec{s}''(p) - \beta \vec{s}''''(p) - \nabla E_\textrm{ext}\left(\vec{s}(p)\right) = 0\;. \]

(By the way, when you derive this is when you understand why the original definition of the internal energy has that ½ in it!) \(\nabla E_\mathrm{ext}\) , the gradient of the external energy, is a force. We call this the external force, \(\vec{F}_\mathrm{ext}\) , and this is where most of the improvements to snakes were made. I won’t discuss them here.

Solving the equation above is accomplished with the gradient descent method: you convert the snake \(\vec{s}\) into a function of time, and replace the 0 with the partial derivative of \(\vec{s}\) to time. You can see the reason for this if you assume that, after a long time when the snake has converged to a minimum, it’s derivative to time will be zero, and so the equation above is satisfied.

\[ \frac{\partial \vec{s}(p)}{\partial t} = \alpha \vec{s}''(p,t) - \beta \vec{s}''''(p,t) + \vec{F}_\textrm{ext}\left(\vec{s}(p,t)\right)\;. \]

Next we need to discretize the parameter \(p\) so we can actually implement this numerically. So we set \(p = \nicefrac{i}{N}\) , with \(i\) going from 0 to \(N-1\) . Because the snake is periodic, \(\vec{s}[N] = \vec{s}[0]\) , \(\vec{s}[N+1] = \vec{s}[1]\) , etc. (Note I’m using the square brackets to denote the discrete indexing, as opposed to the round brackets for continuous-domain functions.) This is also the time when we split the vector \(\vec{s}\) into its two components \(x\) and \(y\) . The two components of the external force \(\vec{F}_\mathrm{ext}\) are \(f_x\) and \(f_y\) . We use the finite difference approximation to the spatial derivatives:

\[ x''[i] = x[i-1] - 2x[i] + x[i+1] \]

and

\[ x''''[i] = x[i-2]-4x[i-1]+6x[i]-4x[i+1]+x[i+2]\;. \]

This results in the following set of \(2N\) equations:

\begin{align*} \frac{\partial x_t[i]}{\partial t} &= \alpha(x_t[i-1]-2x_t[i]+x_t[i+1]) \\ &+ \beta(-x_t[i-2]+4x_t[i-1]-6x_t[i]+4x_t[i+1]-x_t[i+2]) \\ &+ f_x(x_t[i],y_t[i]) \\ \frac{\partial y_t[i]}{\partial t} &= \alpha(y_t[i-1]-2y_t[i]+y_t[i+1]) \\ &+ \beta(-y_t[i-2]+4y_t[i-1]-6y_t[i]+4y_t[i+1]-y_t[i+2]) \\ &+ f_y(x_t[i],y_t[i])\;. \end{align*}

We can write the whole thing, except the external force, as a \(N\) -by- \(N\) matrix \(A\) :

\[\begin{bmatrix} -2\alpha-6\beta & \alpha+4\beta & -\beta & 0 & 0 & 0 & \dots & -\beta & \alpha+4\beta \\ \alpha+4\beta & -2\alpha-6\beta & \alpha+4\beta & -\beta & 0 & 0 & \dots & 0 & -\beta \\ -\beta & \alpha+4\beta & -2\alpha-6\beta & \alpha+4\beta & -\beta & 0 & \dots & 0 & 0 \\ 0 & -\beta & \alpha+4\beta & -2\alpha-6\beta & \alpha+4\beta & -\beta & \dots & 0 & 0 \\ 0 & 0 & -\beta & \alpha+4\beta & -2\alpha-6\beta & \alpha+4\beta & \dots & 0 & 0 \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ -\beta & 0 & 0 & 0 & 0 & 0 & \dots & -2\alpha-6\beta & \alpha+4\beta \\ \alpha+4\beta & -\beta & 0 & 0 & 0 & 0 & \dots & \alpha+4\beta & -2\alpha-6\beta \end{bmatrix}\]

such that:

\begin{align*} \frac{\partial X_t}{\partial t} &= AX_t + f_x(X_t,Y_t) \\ \frac{\partial Y_t}{\partial t} &= AY_t + f_y(X_t,Y_t)\;. \end{align*}

Note the simple structure of this matrix. Only 5 diagonals have values in them, the rest of the matrix is 0, no matter how large \(N\) is. This is called a cyclic pentadiagonal banded matrix, and can easily be inverted using Cholesky decomposition. If the snake were not circular (the two ends not connected) this matrix would be even simpler!

The last step is to discretize the time parameter, adding a step size \(\gamma\) :

\[ \frac{\partial X_t}{\partial t} = \frac{X_t - X_{t-1}}{\gamma}\;. \]

To make this easy to solve, we need to do a little trick: we will assume that the movement of the snake is small enough that the external force doesn’t change much from one time step to the next. We can then substitute in the set of equations above the external force at time \(t-1\) , yielding a simple to solve set of equations:

\begin{align*} X_t &= (I-\gamma A)^{-1} \left\{ X_{t-1} + \gamma f_x(X_{t-1},Y_{t-1}) \right\} \\ Y_t &= (I-\gamma A)^{-1} \left\{ Y_{t-1} + \gamma f_y(X_{t-1},Y_{t-1}) \right\}\;. \end{align*}

Snake implementation

Finally! We’re ready to start writing code…

Let’s start with creating an image to test our algorithm on:

img = noise(50+100*gaussf(rr>85,2),'gaussian',20)

test image

We will also need an initial snake to deform:

x = 120+50*cos(0:0.1:2*pi)';
y = 140+60*sin(0:0.1:2*pi)';
hold on, plot([x;x(1)],[y;y(1)],'g')

test image with initial snake

Note that I duplicated the first point at the end when plotting, so that the snake would be a closed contour. I’ll define the parameters now. This is a tricky part, I played around with them until I was satisfied. I haven’t yet seen a clever way of determining these. Because there is no weight for the external force, the \(\gamma\) parameter needs to be increased if the external force is too weak. To compensate, both \(\alpha\) and \(\beta\) should be decreased by the same factor. Decreasing \(\beta\) makes the snake less smooth. Increasing \(\alpha\) increases the force that wants to shrink the snake; because we started inside the boundary, we want it very small.

alpha = 0.001;
beta = 0.4;
gamma = 100;
iterations = 50;

Next we will compute the matrix inversion. We need to create the matrix \(I - \gamma A\) and invert it. This is the most complex part of the whole algorithm, but in MATLAB it’s a breeze:

N = length(x);
a = gamma*(2*alpha+6*beta)+1;
b = gamma*(-alpha-4*beta);
c = gamma*beta;
P = diag(repmat(a,1,N));
P = P + diag(repmat(b,1,N-1), 1) + diag(   b, -N+1);
P = P + diag(repmat(b,1,N-1),-1) + diag(   b,  N-1);
P = P + diag(repmat(c,1,N-2), 2) + diag([c,c],-N+2);
P = P + diag(repmat(c,1,N-2),-2) + diag([c,c], N-2);
P = inv(P);

a, b and c are the elements along the 5 diagonals that are not 0. I used the function diag to populate the matrix. inv takes care of selecting an appropriate inversion algorithm. Nothing to worry about here. Note that we need to do this inversion only once, not at every iteration. The matrix is independent of the position of the snake.

Next up is the external force. We need to compute the gradient of the image representing the external energy. This is typically the gradient magnitude. Using DIPimage this is again a breeze! We’ll use gradmag and gradient:

f = gradient(gradmag(img,30));

I’ve used a very large value for the parameter \(\sigma\) here. This is because the snake starts off far away from the edge we’re looking for, and the external force is the only thing that is able to pull it towards these edges. Many of the improvements to the snake so far have been therefore to increase the “pull” range of edges by modifying the external force. Since I don’t want to go into that, I’ve opted to use a large \(\sigma\) , which spreads out the gradient over a large area. Another thing to note here is that f is a vector image, that is, each pixel has two values, one for the gradient along the x-axis, and one along the y-axis. f{1} extracts a scalar image containing only the x-component of the gradient, whereas f(100,100) extracts a vector corresponding to the pixel at (100,100).

The last thing to do is to get the value (at sub-pixel locations by interpolation) of the external force at the snake’s control points, and update the position of these control points according to the gradient descent equation we derived. We’ll do this in a loop, iterations times, and plot the resulting snake every 5 iterations. I’m plotting the final result in red:

for ii = 1:iterations
   % Calculate external force
   coords = [x,y];
   fex = get_subpixel(f{1},coords,'linear');
   fey = get_subpixel(f{2},coords,'linear');
   % Move control points
   x = P*(x+gamma*fex);
   y = P*(y+gamma*fey);
   if mod(ii,5)==0
      plot([x;x(1)],[y;y(1)],'b')
   end
end
plot([x;x(1)],[y;y(1)],'r')

test image with final segmentation

Ok. So now we’ve got a snake, and got it moving towards an edge! Too bad that the snake seems to converge to a point inside the edge. This is because the Gaussian gradient used to compute the external energy had a very large parameter \(\sigma\) . In a Gaussian smoothed image, edges always move inwards. We could run the loop above again, but using an external force computed with a smaller \(\sigma\) , since the snake is closer to the edge now. However, there are better alternatives to the external force that do not require this type of multi-scale approach. See for example gradient vector flow or vector field convolution. I won’t go into these, but the next release of DIPimage will have functions to compute these external forces.

Yet another modification to the snake is the so called balloon force. It is an internal force that moves the snake outwards (or inwards if you make it’s weight negative). It can very easily be added to the loop above, as you can see in the function snakeminimize. Go ahead, download and install it into your DIPimage directory [Note: the code in the ZIP file requires DIPimage 2, and is not compatible with DIPimage 3]. There’s a README file with instructions inside. That function uses a class dip_snake, which I will discuss in a future post.