Line Integral Convolution for Python, written in Rust
rLIC
(pronounced 'relic') is a minimal implementation of the Line Integral Convolution algorithm for in-memory numpy
arrays, written in Rust.
python -m pip install rLIC
rLIC
consists in a single Python function, rlic.convolve
, that convolves a
texture
image (usually noise) with a 2D vector field described by its
components u
and v
, via a 1D kernel
array. The result is an image where
pixel intensity is strongly correlated along field lines.
Let's see an example. We'll use matplotlib
to visualize inputs and outputs.
import matplotlib.pyplot as plt
import numpy as np
import rlic
SHAPE = NX, NY = (256, 256)
prng = np.random.default_rng(0)
texture = prng.random(SHAPE)
x = np.linspace(0, np.pi, NY)
U = np.broadcast_to(np.cos(2 * x), SHAPE)
V = np.broadcast_to(np.sin(x).T, SHAPE)
fig, axs = plt.subplots(ncols=2, sharex=True, sharey=True, figsize=(10, 5))
for ax in axs:
ax.set(aspect="equal", xticks=[], yticks=[])
ax = axs[0]
ax.set_title("Input texture (noise)")
ax.imshow(texture)
ax = axs[1]
ax.set_title("Input vector field")
Y, X = np.mgrid[0:NY, 0:NX]
ax.streamplot(X, Y, U, V)
Now let's compute some convolutions, varying the number of iterations
kernel = 1 - np.abs(np.linspace(-1, 1, 65))
fig_out, axs_out = plt.subplots(ncols=3, figsize=(15, 5))
for ax in axs_out:
ax.set(aspect="equal", xticks=[], yticks=[])
for n, ax in zip((1, 5, 100), axs_out, strict=True):
image = rlic.convolve(texture, U, V, kernel=kernel, iterations=n)
ax.set_title(f"Convolution result ({n} iteration(s))")
ax.imshow(image)
By default, the direction of the vector field affects the result. That is, the
sign of each component matters. Such a vector field is analogous to a velocity
field. However, the sign of u
or v
may sometimes be irrelevant, and only
their absolute directions should be taken into account. Such a vector field is
analogous to a polarization field. rLIC
supports this use case via an
additional keyword argument, uv_mode
, which can be either 'velocity'
(default), or 'polarization'
. In practice, the difference between these two
modes in only visible around sharps changes in sign in either u
or v
, and
with certain kernels.
Let's illustrate one such case
import matplotlib.pyplot as plt
import numpy as np
import rlic
SHAPE = NX, NY = (256, 256)
prng = np.random.default_rng(0)
texture = prng.random(SHAPE)
kernel = 1 - np.abs(np.linspace(-1, 1, 65, dtype="float64"))
U0 = np.ones(SHAPE)
ii = np.broadcast_to(np.arrange(NX), SHAPE)
U = np.where(ii<NX/2, -U0, U0)
V = np.zeros((NX, NX))
fig, axs = plt.subplots(ncols=3, sharex=True, sharey=True, figsize=(15, 5))
for ax in axs:
ax.set(aspect="equal", xticks=[], yticks=[])
ax = axs[0]
ax.set_title("Input vector field")
Y, X = np.mgrid[0:NY, 0:NX]
ax.streamplot(X, Y, U, V)
for uv_mode, ax in zip(("velocity", "polarization"), axs[1:], strict=True):
image = rlic.convolve(texture, U, V, kernel=kernel, uv_mode=uv_mode)
ax.set_title(f"{uv_mode=!r}")
ax.imshow(image)