G'MIC Adventure #6: Playing with the Wave Equation

Hey everyone!

Context.

It’s been a while since I wrote an episode of #gmic-adventures, so I’ve decided to write a new one!

I’m going to try to show you how to use the G’MIC’s scripting language to implement an evolution of the Wave Equation, and see if we can create cool effects on images with that.

I don’t have a specific plan in mind, so I’ll be improvising a bit, but I’ll start by briefly describing what the wave equation is, how to discretize it, and how to implement it on a regular grid (2D to start with). Then we’ll see where that takes us.

Implementing the Wave Equation.

According to the wikipedia page,

The wave equation is a second-order linear partial differential equation for the description of waves or standing wave fields such as mechanical waves (e.g. water waves, sound waves and seismic waves) or electromagnetic waves (including light waves)

The equation itself is given by:

\frac{\partial^2 u}{\partial t^2} = c^2\;\left(\frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2}\right)

(I removed the z component in the sum because in our case, we’ll implement it only in a 2D space).

u is the 2D field that is subject to the wave equation. Think of it as an image of pointwise temperatures or as a height field, or whatever physical quantity you prefer :), but as an image that evolves over time. So basically, u depends on three variables: x,y (spatial coordinates) and t (evolution time).
Also in the equation, u is considered to be a continuous enough function, that’s why we can write derivatives (In fact, u must actually be C^², which means twice continuously differentiable, but that’s a math detail we won’t necessarily be concerned with here.)

Now, the question is: How do we “convert” this equation into a piece of code ? That seems a bit tricky at a first glance, but that’s actually not that hard and I’ll show you how to deal with it.

1. Discretization of second derivatives

In the Wave Equation, you mainly have second derivatives, one along the time dimension (left part of the equation, i.e. \frac{\partial^2 u}{\partial t^2}), and the others along the spatial dimensions (right part of the equation, i.e. \frac{\partial^2 u}{\partial x^2} and \frac{\partial^2 u}{\partial y^2}).

So, the first thing is we really need to understand how to estimate second derivatives from an algorithmic point of view. One solution actually comes from the Talor Expansion, which gives an approximation of a continuous function u, around a point x, assuming we also know the values of its N first derivatives at the same point x.

Suppose you want to evaluate the function u at a point that is very close to x (but not at x directly), e.g at x + dx, where dx is a small quantity that is known (and close to 0).
The Taylor expansion of u tells you that a good approximation of u(x + dx) is:

u(x + dx) \approx u(x) + dx\;\frac{\partial u}{\partial x}(x) + \frac{dx^2}{2}\;\frac{\partial^2 u}{\partial x^2}(x)

Now, if you do the same for a point located at x - dx, then:

u(x - dx) \approx u(x) - dx\;\frac{\partial u}{\partial x}(x) + \frac{dx^2}{2}\;\frac{\partial^2 u}{\partial x^2}(x)

And the trick is: summing these two equations gives:

u(x + dx) + u(x - dx) \approx 2\;u(x) + dx^2\;\frac{\partial^2 u}{\partial x^2}(x)

And thus you get one way to estimate the second derivative \frac{\partial^2 u}{\partial x^2} of the function u, if you know the values of u(x), u(x - dx) and u(x + dx):

\frac{\partial^2 u}{\partial x^2}(x) \approx \frac{1}{dx^2}\;\left(u(x + dx) + u(x - dx) - 2\;u(x)\right)

Now you get it. It’s relatively easy to compute the second derivatives of a function u along one of its variable.
In practice, u is defined on a discrete grid, so for estimating the spatial second derivatives \frac{\partial^2 u}{\partial x^2} and \frac{\partial^2 u}{\partial y^2}, we usually choose dx=1 and dy=1 (that’s not so close to 0, but that’s the minimal distance we can choose if we want to avoid spatial value interpolation). And for the variable t, we can choose a dt closer to 0, as we won’t have to actually store all the values of the u function for all possible t.

2. Putting all this together.

At the end of the day, it’s easy to show that a possible discrete approximation of the right part of the Wave Equation is (when choosing dx = dy = 1) :

\begin{array}{l} c^2\;\left(\frac{\partial^2 u^t}{\partial x^2}(x,y) + \frac{\partial^2 u^t}{\partial y^2}(x,y)\right) \approx \\ c^2\;\left( u^t(x+1,y) + u^t(x-1,y) + u^t(x,y+1) + u^t(x,y-1) - 4\;u^t(x,y) \right) \end{array}

And same for the left part (except we keep dt as a parameter):

\frac{\partial^2 u^t}{\partial t^2}(x,y) = \frac{1}{dt^2}\left(u^{t + dt}(x,y) + u^{t - dt}(x,y) - 2\;u^t(x,y)\right)

Putting all this together leads to a discretized Wave Equation, the one we will actually implement:

\begin{array}{l} u^{t+dt}(x,y) = \left(dt^2c^2\;( u^t(x+1,y) + u^t(x-1,y) + u^t(x,y+1) + u^t(x,y-1) - 4\;u^t(x,y) )\\ + 2\;u^t(x,y)\right) - u^{t-dt}(x,y) \end{array}

3. Stop talking, show me the code!

In G’MIC, a first, naive implementation of this equation will be:

foo :
  400,400,1,1x2 => u_t_1,u_t # Initialize 2D functions u() at time t-1 and t
  noise[u_t] 0.01,2 # Put some spikes in u^t
  repeat inf { # Evolution loop
    +f[u_t] " # Compute u() at time t+1
      const boundary = 1;
      const dt = 0.5;
      const c = 1;
      uxx = j(-1) + j(1) - 2*i; # Second derivative of u along x
      uyy = j(0,-1) + j(0,1) - 2*i; # Second derivative of u along y
      dt^2*c^2*(uxx + uyy) + 2*i - i(#$u_t_1)"
    w.
    wait 20

    # Prepare for next iteration.
    rm[u_t_1] =>[u_t,-1] u_t_1,u_t
  }

And the result, while being not impressive, looks like waves evolution anyway :wink: :

BTW, this code is not optimal and does not produce something very interesting right now. But improving all this will be the goal of the next post. Stay tuned!

4. Reducing reflected waves at the boundaries

As you can see in the video above, waves tends to be reflected when they reach the image boundaries. It’s actually not that easy to mitigate this effect, so we’ll use here a bit of a dirty trick. The idea is to create an attenuation mask that is simply an image with values in range [0,1] (1 near the center, and 0 near the boundaries), that will be multiplied to u^{t+dt} each time it is computed.

Below is a video of a wave evolving, that shows the effect of this attenuation mask:

5. Simplify the implementation using convolutions

In image/signal processing, a process that locally computes a weighted sum of neighboring pixels is known as a convolution, and your keen eye will surely have noticed that the discretized wave equation can indeed be simplified using a convolution product:

u^{t+dt} = u^t*K - u^{t-dt}

where K is the convolution kernel defined by:

K = \left( \begin{array}{ccc} 0 & c^2dt^2 & 0\\ c^2dt^2 & -4c^2dt^2 + 2 & c^2dt^2\\ 0 & c^2dt^2 & 0 \end{array} \right)

Good news: G’MIC has a built-in convolution operator, which can be used to simplify the code.

Now, with these two tricks, the code becomes:

foo :
  400,400,1,1x2 => u_t_1,u_t # Initialize 2D functions u() at time t-1 and t
  100%,100% =. 1,50%,50% blur. 100% n. 0,1 pow. 0.05 => attenuation_mask
  dtc2:=0.25^2 (0,1,0;1,-4,1;0,1,0) *. $dtc2 eval "i(1,1)+=2" => convolution_kernel

  repeat inf { # Evolution loop
    =[u_t] {cos($>/6)},50%,50% # Put an oscillating value at the image center
    +convolve[u_t] [convolution_kernel] -. [u_t_1] # Compute u^{t+dt}
    *. [attenuation_mask] # Attenuate waves reflection at image boundaries
    w. wait 20

    # Prepare for next iteration.
    rm[u_t_1] =>[u_t,-1] u_t_1,u_t
  }
2 Likes

This is something I wanted to do, and wow, you’re doing it better than I envisioned.

1 Like

6. Wave effect over an image

Well the next step is indeed very simple : what I do now is I assume that the function u^t is a height map associated to a Lambertian surface, and I estimate a light map from it (here, just compute \nabla u^t\,.\,(1,1)^T, which is the Lambertian reflectance).
Then, I apply this light map to an image.
Nothing really smart, but doing this, I get one of the first image effect I had in mind !

Result:
wave_img

Source code: (slight variation of the previous one):

foo :
  sp colorful,400 => img
  50%,50%,1,1x2 => u_t_1,u_t # Initialize 2D functions u() at time t-1 and t
  100%,100% =. 1,50%,50% blur. 100% n. 0,1 pow. 0.05 => attenuation_mask
  dtc2:=0.25^2 (0,1,0;1,-4,1;0,1,0) *. $dtc2 eval "i(1,1)+=2" => convolution_kernel

  repeat inf { # Evolution loop
    =[u_t] {cos($>/6)},50%,50% # Put an oscillating value at the image center

    +convolve[u_t] [convolution_kernel] -. [u_t_1] # Compute u^{t+dt}
    *. [attenuation_mask] # Attenuate waves reflection at image boundaries
    rm[u_t_1] =>[u_t,-1] u_t_1,u_t

    # Calculate light map, and apply it over the color image.
    +g[u_t] xy b[-2,-1] 1 +[-2,-1] *. 1000 ri. [img] ++[img] . c. 0,255

    w. 200%,200% wait 20
    rm[-2,-1]
  }

@David_Tschumperle Missed opportunity by not having the wave propagating from the eyes or mouth, but that might scare folks away from G’MIC! :stuck_out_tongue_winking_eye:

Sometimes I wonder if they aren’t already!

7. Let’s become anisotropic!

While brainstorming how to create another fun effect based on the wave equation, I took another look at the Wikipedia page and at the alternative, more compact way of writing the Wave Equation, namely:

\frac{\partial^2 u}{\partial t^2} = c^2\,\Delta u

where \Delta u = \frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2} is known as the Laplacian of u.

Long story short: While working on my PhD thesis on image regularization years ago, I had the opportunity to work on locally anisotropic versions of the Laplacian, that is, Laplacian operators that are oriented differently at each point in the image (to be more precise, oriented mainly along the contours of the image structures).

The term anisotropic here refers to how an equation behaves in terms of preferred orientations. In the original Wave Equation, the behavior is isotropic, as it is clear that no specific orientation is preferred over another one (the waves behave the same along all spatial directions).

An anisotropic version of the Laplacian can change that behavior. I won’t enter into the math details, but one way to do that is to “bend” the Laplacian operator locally using a tensor (i.e. a 2x2 SPD matrix), which means in practice replace \Delta u by \text{trace}(DH), where D is your local tensor (pointwise defined), and H is your local Hessian matrix. The principal eigenvectors of D gives you the preferred orientation of this bended version of the Laplacian operator.

So, now, we can rather consider this anisotropic version of the Wave Equation:

\frac{\partial^2 u}{\partial t^2} = c^2\,\text{trace}(DH)

and design a tensor field D that is mainly oriented along the image contours.
Luckily, G’MIC already has a command that does that on its own : diffusiontensors.

Enough chatter, here’s the modified code :

Source code:

foo :
  sp colorful,400 => img
  +diffusiontensors. 0,1,0,6 => tensors
  +gradient_norm[img] f. "u>0.99 && i>20" => sources
  100%,100%,1,1x2 => u_t_1,u_t # Initialize 2D functions u() at time t-1 and t

  repeat inf { # Evolution loop
    +*[sources] {cos($>/100)} j[u_t] .,0,0,0,0,1,[sources] rm. # Put oscillating values at the sources
    +f[u_t] " # Compute u() at time t+1
      const boundary = 1;
      const dtc2 = 0.5^2;
      uxx = j(-1,0) + j(1,0) - 2*i;
      uyy = j(0,-1) + j(0,1) - 2*i;
      uxy = 0.25*(j(-1,-1) + j(1,1) - j(-1,1) - j(1,-1));
      T = I(#$tensors);
      lap = T[0]*uxx + 2*T[1]*uxy + T[2]*uyy;
      dtc2*(lap) + 2*i - i(#$u_t_1)"
    c. -1,1
    rm[u_t_1] =>[u_t,-1] u_t_1,u_t

    +g[u_t] xy a[-2,-1] c *. 10 +warp[img] .,1,1,3
    w. 200%,200% rm.
    wait 20
  }

Result:
And here is the corresponding evolution of the Wave equation, showed in terms of local image warping. Nice, isn’t it ?

1 Like

Perhaps this could be used to create watercolors effects, with a bit of deformations and noise here and there? I wonder.
Back to hibernation

1 Like