Contents:

- Basic NumPy functionalities
- Time shifting: the first pitfall
- Frequency shifting
- Time scaling
- Image translation
- Image modulation: Holograms
- Scaling images
- Watermarks

Numpy is *the* basic library for scientific
programming in Python and it has its own implementation of the
fast Fourier transform (FFT) algorithm.
A summary of all Fourier-related functions is given
in the NumPy docs.
Let me highlight the most essential functions here:

`np.fft.fft`

:*Compute the one-dimensional discrete Fourier Transform.*`np.fft.fftfreq`

:*Return the Discrete Fourier Transform sample frequencies.*This function is used to obtain the frequencies corresponding to the output of`np.fft.fft`

for data visualization and postprocessing purposes.`np.fft.fftshift`

:*Shift the zero-frequency component to the center of the spectrum.*By default, the zero-frequency component is the first element of the array returned by`np.fft.fft`

and negative frequencies are located in the second half of the array. For data visualization, we need to have the zero-frequency component at the center of the array. This function handles odd- and even- length arrays correctly and should be used instead of manual solutions.

Let us attempt to perform the Fourier transform of a Gaussian signal

\[g(t) = \frac{1}{\sigma \sqrt{2 \pi}} e^{-\frac{1}{2} \left( \frac{t-\tau}{\sigma} \right)^2}.\]The Fourier transform of a Gaussian signal is also Gaussian, which makes it easy to check the result.

```
import matplotlib.pylab as plt
import numpy as np
# Gaussian signal
# (parameters are chosen such that both signal and FT plot nicely)
N = 100
time, dt = np.linspace(0, 10, N, endpoint=False, retstep=True)
sigma = .25
tau = 5
sig = 1/(sigma * np.sqrt(2*np.pi)) * np.exp(-1/2 * ((time-tau) / sigma)**2)
freq = np.fft.fftfreq(N, dt)
ft_sig = np.fft.fft(sig)
fig = plt.figure(figsize=(7, 3))
ax1 = plt.subplot(121, title="signal")
ax1.plot(time, sig)
ax1.set_xlabel("time $t$ [s]")
ax1.set_ylabel("amplitude $g$ [a.u.]")
ax2 = plt.subplot(122, title="Fourier transform")
ax2.plot(np.fft.fftshift(freq), np.fft.fftshift(ft_sig.real))
ax2.set_xlabel("frequency $f$ [Hz]")
ax2.set_ylabel("amplitude $G$ [a.u.]")
plt.tight_layout()
plt.savefig("shift_pitfall.png", dpi=120)
plt.close()
```

What happened? The Fourier transformed signal rapidly changes signs while
one could only make out a Gaussian envelope. To understand
what went wrong, we need to take a closer look at what `numpy.fft.fft`

actually does.

First, let us consider the continuous Fourier transform $G(f)$ of a signal $g(t)$,

\[G(f) = \int_{-\infty}^{\infty} g(t) \cdot e^{-2\pi i ft}\,dt.\]In order to discretize this equation, we replace the integral by a sum of $N$ points, forcing us to reduce the integration interval from $(-\infty, \infty)$ to $(0, N)$. Furthermore, we choose the substitutions $f \rightarrow k, k \in \Bbb N$ and $t \rightarrow n/N, n \in \Bbb N$, which leads to the normalization $dt \rightarrow 1$. The discrete signals are now described as $G_k = G(f_k)$ and $g_n = g(t_n)$. The discrete Fourier transform can thus be written as

\[G_k = \sum_{n=0}^{N-1} g_n \cdot e^{-\frac {2\pi i}{N} k n}.\]Note how the definition of $t=0$ has become $n=0$, which brings us back to
the original problem. The origin of $g_k$ is located at $k=0$ (not at
the center of the array $k = N/2$). Thus, in order to get the
Fourier transform of our Gaussian signal right, we would have to shift $g_k$ such
that its maximum is located at $k=0$ (the first element of the array).
We could achieve this by means of `np.fft.fftshift`

, but that only works
as long as the center of $g$ coincides exactly with $n=N/2$.
A more elegant solution is to directly correct for the temporal shift $\tau$
after the Fourier transform. Let’s consider a shifted function $g(t-\tau)$
in the equation of the continuous Fourier transform:

We would like to get rid of the shift $\tau$ and thus substitute $t \rightarrow t + \tau$.

\[G(f) = \int_{-\infty}^{\infty} g(t) \cdot e^{-2\pi i f(t+\tau)}\,dt.\]This step only affects the Fourier kernel and results in the additional term $\exp (- 2 \pi i f \tau)$ which can be pulled out of the integral. This oscillatory term is a simple time shift and explains the artifacts in the figure above. We can correct for this time shift by multiplying $G_k$ with $\exp (+ 2 \pi i f \tau)$:

```
# correct for time shift `tau`
ft_cor = np.fft.fft(sig) * np.exp(2*np.pi*1j*freq*tau)
fig = plt.figure(figsize=(7, 3))
ax1 = plt.subplot(121, title="signal")
ax1.plot(time, sig)
ax1.set_xlabel("time $t$ [s]")
ax1.set_ylabel("amplitude $g$ [a.u.]")
ax2 = plt.subplot(122, title="time-shift corrected Fourier transform")
ax2.plot(np.fft.fftshift(freq), np.fft.fftshift(ft_cor.real))
ax2.set_xlabel("frequency $f$ [Hz]")
ax2.set_ylabel("amplitude $G$ [a.u.]")
plt.tight_layout()
plt.savefig("shift_corrected.png", dpi=120)
plt.close()
```

Note that this correction works for *any* real-valued $\tau$ (as long as
the support of $g(t)$ is within the interval $0\,\text{s} < t < 10\,\text{s}$).

In some cases, it can be useful to manipulate a signal such that it shows up at a predefined frequency in Fourier space. A frequency shift can be described with

\[G(f-f_0) = \int_{-\infty}^{\infty} g(t) \cdot e^{-2\pi i (f-f_0)t}\,dt.\]In other words, the signal $g(t)$ must be multiplied by the complex exponential $\exp(-2\pi i f_0t)$ to shift its Fourier transform by $f_0$. Here is an example for a shift by 2.2 Hz.

```
# multiply input with complex exponential
sig_shift = sig * np.exp(2*np.pi*1j*(time-tau)*2.2)
ft_shift = np.fft.fft(sig_shift) * np.exp(2*np.pi*1j*freq*tau)
fig = plt.figure(figsize=(7, 3))
ax1 = plt.subplot(121, title="signal × complex exponential")
ax1.plot(time, sig_shift.real)
ax1.set_xlabel("time $t$ [s]")
ax1.set_ylabel("amplitude $g$ [a.u.]")
ax2 = plt.subplot(122, title="frequency-shifted Fourier transform")
ax2.plot(np.fft.fftshift(freq), np.fft.fftshift(ft_shift.real))
ax2.set_xlabel("frequency $f$ [Hz]")
ax2.set_ylabel("amplitude $G$ [a.u.]")
plt.tight_layout()
plt.savefig("shift_frequency.png", dpi=120)
plt.close()
```

Note again that $\tau$ must be included to correctly shift the
frequency, hence the term `time-tau`

in the complex exponential.

The time scaling property of the Fourier transform states that a change of the sampling frequency in the input signal is equivalent to a scaled signal in Fourier space.

\[\frac{1}{a} G(f/a) = \int_{-\infty}^{\infty} g(at) \cdot e^{-2\pi i f t}\,dt\]In this example, the time axis is scaled by a factor of two, which leads to a Fourier signal that is scaled by a factor of two and narrowed by a factor of one half.

```
# scale by a factor of 2
freq_sc = np.fft.fftfreq(N, dt/2)
time_sc = time / 2
tau_sc = tau / 2
sig_sc = 1/(sigma * np.sqrt(2*np.pi)) * np.exp(-1/2 *
((time_sc-tau_sc) / sigma)**2)
ft_sc = np.fft.fft(sig_sc) * np.exp(2*np.pi*1j*freq_sc*tau_sc)
ft_sc = np.fft.fftshift(ft_sc)
freq = np.fft.fftshift(freq)
freq_sc = np.fft.fftshift(freq_sc)
fig = plt.figure(figsize=(7, 3))
ax1 = plt.subplot(121, title="time-scaled signal")
ax1.plot(time, sig_sc.real)
ax1.set_xlabel("time $t$ [s]")
ax1.set_ylabel("amplitude $g$ [a.u.]")
ax2 = plt.subplot(122, title="scaled Fourier transform")
ax2.plot(freq, ft_sc.real)
ax2.set_xlabel("frequency $f$ [Hz]")
ax2.set_ylabel("amplitude $G$ [a.u.]")
plt.tight_layout()
plt.savefig("scale_time.png", dpi=120)
plt.close()
```

Many applications of the Fourier transform involve image analysis. It is possible to perform the trivial task of image translation with the Fourier transform. If the image is translated by a non-integer amount of pixels, then the interpolation takes place with the Fourier kernel (sine and cosine functions). For this example, we use a downscaled image of the lunar eclipse, recorded on July 27th 2018.

```
import matplotlib.image as mpimg
moon = mpimg.imread("moon_small.png")
fy = np.fft.fftfreq(moon.shape[0]).reshape(-1, 1)
fx = np.fft.fftfreq(moon.shape[1]).reshape(1, -1)
ft_moon = np.fft.fft2(moon) * np.exp(2*np.pi*1j*(fx*10.5 + fy*10))
moon_tr = np.fft.ifft2(ft_moon)
fig = plt.figure(figsize=(7, 3.6))
ax1 = plt.subplot(121, title="moon")
ax1.imshow(moon, cmap="gray", interpolation="none")
ax2 = plt.subplot(122, title="translated moon")
ax2.imshow(moon_tr.real, cmap="gray", interpolation="none")
plt.tight_layout()
plt.savefig("moon_translated.png", dpi=120)
plt.close()
```

The image is translated by 10 pixels along the y-axis and by 10.5 pixels along the x-axis. The resulting interpolation along the x-axis leads to horizontal ringing artifacts.

Performing image translation with the Fourier transform might be fast, but for higher accuracy, other interpolation methods (e.g. splines) might be better suited, especially when sharp boundaries (dark-bright) are present.

The Fourier transform can be used for the analysis of digital holograms. In the life sciences, digital holographic imaging is used to quantify the refractive index of cells. To achieve that, a laser beam is split into two beams, one passes through the sample and the other serves as a reference. When these two beams are brought back together at a slightly tilted angle, they generate an interference pattern, periodic stripes that can be recorded with a regular camera, that is modulated by the phase delay introduced by the varying refractive index of the sample.

The example hologram shows an HL60 cell - the intensity data clearly reveals a cell, but we are after the phase data. The modulation of the phase data becomes visible when tracing the interference pattern (dark stripes) through the cell: they appear to be deformed at the cell boundary. This modulation can be extracted with Fourier analysis. The interference pattern can be described as a cosine function, whose Fourier transform are two delta functions, the so-called sidebands. Isolating one of those sidebands (see arrow in the image below) and performing an inverse Fourier transform, reveals the part of the light that passed through the cell and the phase delay can be computed.

```
cell = mpimg.imread("cell_hologram.png")
ft_cell = np.fft.fft2(cell)
ft_cell_copy = np.copy(ft_cell)
# suppress central band
ft_cell[0, :] = 0
ft_cell[:, 0] = 0
# determine sideband position
xmax = np.argmax(np.max(ft_cell, axis=1))
ymax = np.argmax(ft_cell[xmax])
# move sideband to zero frequency
ft_cell_rolled = np.roll(ft_cell, (-xmax, -ymax), axis=(0, 1))
# apply sideband filter
ft_cell_rolled[20:-20, :] = 0
ft_cell_rolled[:, 20:-20] = 0
# invert to get sideband modulation
modulation = np.fft.ifft2(ft_cell_rolled)
# compute phase
phase = np.angle(modulation)
fig = plt.figure(figsize=(7, 2.8))
ax1 = plt.subplot(131, title="hologram")
ax1.imshow(cell.real, cmap="gray", interpolation="bilinear")
ax2 = plt.subplot(132, title="Fourier transform")
ax2.imshow(np.fft.fftshift(np.log(1 + np.abs(ft_cell_copy))),
interpolation="none")
ax2.arrow(37, 50, 32, 11, head_length=10, head_width=10, fc='w', ec='w')
ax3 = plt.subplot(133, title="wrapped phase")
ax3.imshow(phase, cmap="coolwarm", interpolation="none")
for ax in [ax1, ax2, ax3]:
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout(w_pad=0, pad=0, h_pad=0)
plt.savefig("cell_modulation.png", dpi=120)
plt.close()
```

Note that the phase is wrapped in the interval $(0, 2\pi)$, i.e. there are $2\pi$ phase jumps (from red to blue) that have to be “unwrapped” for further analysis.

The Fourier transform can also be used to up- or downscale images. In the above example, the inverse Fourier transform was performed for a much larger frequency space than necessary, because we actually cropped the sideband to 40 by 40 pixels. If we only take the inverse Fourier transform of the cropped sideband, we get an idea of the actual image resolution. In short, upscaling with the Fourier transform means that the image is interpolated with cosine functions. On the other hand, downscaling with the Fourier transform means that high-frequency contributions are omitted. The illustration below additionally makes use of a phase unwrapping algorithm that is part of the scikit-image library.

```
from skimage.restoration import unwrap_phase
ft_cell_low = np.zeros((40, 40), dtype=complex)
ft_cell_low.flat[:] = ft_cell_rolled[ft_cell_rolled != 0]
modulation_low = np.fft.ifft2(ft_cell_low)
# compute phase
phase_low = unwrap_phase(np.angle(modulation_low))
phase = unwrap_phase(phase)
fig = plt.figure(figsize=(7, 3.6))
ax1 = plt.subplot(121, title="unwrapped phase")
ax1.imshow(phase, cmap="coolwarm", interpolation="none")
ax2 = plt.subplot(122, title="unwrapped phase (actual resolution)")
ax2.imshow(phase_low, cmap="coolwarm", interpolation="none")
plt.tight_layout()
plt.savefig("cell_downsampled.png", dpi=120)
plt.close()
```

A watermark is a modification of an image, often used to prevent (or track) the usage of an image by others. Watermarks are usually just image overlays, but they can also be hidden in Fourier space. Note that the modification in Fourier space results in distortions, present everywhere in the image, whose intensity depends on the number of frequencies used and the corresponding amplitudes. In this example, an image of the lunar eclipse is watermarked with a smiley.

```
moon = mpimg.imread("moon.png")
fy = np.fft.fftfreq(moon.shape[0]).reshape(-1, 1)
fx = np.fft.fftfreq(moon.shape[1]).reshape(1, -1)
ft_moon = np.fft.fft2(moon)
smile = mpimg.imread("smile.png")
smile_pad = np.zeros_like(moon, dtype=complex)
smile_pad[-(20+smile.shape[0]):-20, -(60+smile.shape[1]):-60] = smile
moon_mark = np.fft.ifft2(ft_moon + 10*smile_pad)
ft_mark = np.fft.fft2(moon_mark.real)
fig = plt.figure(figsize=(7, 2.8))
ax1 = plt.subplot(131, title="moon")
ax1.imshow(moon, cmap="gray", interpolation="none")
ax2 = plt.subplot(132, title="watermarked moon")
ax2.imshow(moon_mark.real, cmap="gray", interpolation="none")
ax3 = plt.subplot(133, title="Fourier transform")
ax3.imshow(np.log(1 + np.abs(np.fft.fftshift(ft_mark))), interpolation="none")
ax3.arrow(55, 30, -15, 15, head_length=8, head_width=8, fc='w', ec='w')
for ax in [ax1, ax2, ax3]:
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout(w_pad=0, pad=0, h_pad=0)
plt.savefig("moon_watermark.png", dpi=120)
plt.close()
```

Note that watermarks may also cover only a few pixels in Fourier space which are then not as easily spotted as in the example above. Also note that depending on the used frequencies of the watermark, scaling down the image might or might not remove the watermark.

]]>As it is with elevenies and haiku, the hexadecimal poem must, in contrast to previous attempts, obey a certain set of rules:

- It must consist of 16 lines with 16 ASCII characters each. If non-standard ASCII characters are used, an extended character set may be employed, e.g. Latin-1.
- It must be formatted as common hex editors would: Each line consists of three parts that are separated by four spaces. The first part enumerates the number of ASCII characters of the poem at the end of the line with a hexadecimal number as an eight-character string (leading zeros). The second part of the line consists of the hexadecimal representation of the poem’s ASCII characters, separated by single spaces. The last part of the line consists of the decoded, human-readable characters.
- The title (if applicable) is the first line of the poem.

Here are two examples. Number one will help you get started with Python.

```
00000010 72 69 66 65 20 6d 61 6c 70 72 61 63 74 69 63 65 rife malpractice
00000020 6e 65 77 63 6f 6d 65 72 20 73 6e 61 6b 69 73 74 newcomer snakist
00000030 64 65 6c 69 63 61 74 65 20 68 65 69 67 68 74 73 delicate heights
00000040 73 65 65 6d 69 6e 67 6c 79 20 62 6c 69 67 68 74 seemingly blight
00000050 6e 6f 74 65 73 20 77 65 6e 74 20 61 6d 69 73 73 notes went amiss
00000060 63 61 74 20 6d 61 64 65 20 6d 65 20 74 68 69 73 cat made me this
00000070 62 69 72 64 73 20 74 68 65 72 65 20 72 65 73 74 birds there rest
00000080 63 61 73 65 73 20 77 6f 6e 27 74 20 6d 65 73 73 cases won't mess
00000090 6d 75 74 61 62 6c 65 20 73 74 61 6e 64 61 72 64 mutable standard
000000A0 6d 79 73 74 69 63 61 6c 20 63 6f 6e 64 75 63 74 mystical conduct
000000B0 73 63 6f 70 65 20 61 6c 74 65 72 61 74 69 6f 6e scope alteration
000000C0 77 6f 72 6c 64 20 64 6f 6d 69 6e 61 74 69 6f 6e world domination
000000D0 6e 6f 62 6c 65 20 63 68 61 6e 67 65 6c 69 6e 67 noble changeling
000000E0 69 6e 68 65 72 69 74 65 64 20 70 69 70 70 69 6e inherited pippin
000000F0 66 61 74 68 6f 6d 6c 65 73 73 20 70 72 69 6e 74 fathomless print
00000100 66 6f 72 20 58 65 72 6f 78 20 61 20 68 69 6e 74 for Xerox a hint
```

The second poem is a translation of Ode to Spot to hexadecimal.

```
00000010 68 65 78 61 64 65 63 69 6d 61 6c 20 73 70 6f 74 hexadecimal spot
00000020 65 63 63 65 20 66 65 6c 69 73 20 63 61 74 75 73 ecce felis catus
00000030 63 61 72 6e 69 76 6f 72 65 20 73 74 61 74 75 73 carnivore status
00000040 65 73 74 68 65 73 69 61 20 66 6f 63 75 73 65 64 esthesia focused
00000050 6e 61 74 75 72 65 20 6d 6f 6d 65 6e 74 6f 75 73 nature momentous
00000060 69 6e 66 72 61 73 6f 75 6e 64 20 77 6f 72 64 73 infrasound words
00000070 6a 6f 79 6f 75 73 20 64 69 73 63 6f 75 72 73 65 joyous discourse
00000080 61 6c 74 72 75 69 73 74 69 63 20 73 68 69 6e 65 altruistic shine
00000090 6e 65 65 64 20 73 74 72 6f 6b 65 20 6d 69 6e 65 need stroke mine
000000A0 63 6f 6e 73 74 69 74 75 65 6e 74 20 74 61 69 6c constituent tail
000000B0 62 61 6c 61 6e 63 65 20 70 72 65 76 61 69 6c 73 balance prevails
000000C0 66 65 65 6c 69 6e 67 20 63 6f 6e 76 65 79 65 64 feeling conveyed
000000D0 67 6f 72 67 65 6f 75 73 20 64 69 73 70 6c 61 79 gorgeous display
000000E0 70 65 72 63 65 70 74 75 61 6c 20 61 72 72 61 79 perceptual array
000000F0 63 61 6e 27 74 20 63 6f 6d 70 72 65 68 65 6e 64 can't comprehend
00000100 73 75 63 68 20 74 72 75 65 20 66 72 69 65 6e 64 such true friend
```

You may download these poems as binaries here and here. If you would like to get started yourself, you could use the example script that I used.

]]>The basic mode of operation of the artistic tomographic filter is to simulate the sinogram acquisition process and to reconstruct the original image from the simulated sinogram. Here is an example that illustrates the steps from original image to sinogram image to reconstructed image with 256 angles (deliberately matching the image dimensions 256x256 px for visualization purposes):

original | sinogram | reconstruction |

As mentioned above, the actual artistic potential of the filter will only be released, if the number of angles in the sinogram used for the reconstruction is reduced:

5 angles | 10 angles | 20 angles |

The resulting artifacts consist of streaks through the image that, when many of them intersect, reproduce the original image content. The filter can be modified in several different ways, some of which are covered further below.

The artistic tomographic filter uses the backprojection
algorithm I implemented in the Python library
radontea.
Radontea also comes with an implementation of the Radon transform which
is used to compute the sinogram from the input image. For loading and
writing images, imageio is used.
For Python 3, imageio and radontea can be installed from the Python package index via
`pip install imageio radontea`

.

To use the filter, download tomographic_filter.py and execute it like so:

`python tomographic_filter.py input_image.jpg`

This will produce a new image `input_image_tf.jpg`

, generated with
the standard filtering parameters.

The *tomographic_filter.py* script comes with a convenient
command line interface that allows to tune several parameters,
from Radon transform to backprojection, affecting the
artistic character of the filter.

By default, the filter uses 37 equally spaced angles with a full angular coverage (from 0° to 180°).

original [highres] | filter with defaults [highres] |

The angular coverage can be modified with the arguments `--cov-min`

(default 0) and `--cov-max`

(default 180),
leading to so-called *missing-angle artifacts*.

original [highres] | defaults [highres] | `--cov-max 120` [highres] |

To achieve a more chaotic behavior, the angles can be distributed randomly using the
`--randomness`

argument. To improve the recognizability of the object, `--weight-angles`

can
be combined with randomly distributed angles.

original [highres] | `--randomness 47` [highres] |
`--randomness 47` and `--weight-angles` [highres] |

Random distribution of angles can also be combined with partial angular coverage. The choice of angles affects the angular positions of the streaks, sometimes resulting in exaggerated edge contrast.

original [highres] | `--randomness 8472` , `--cov-min 20` , and `--cov-max 160` [highres] |
`--randomness 8472` and `--cov-min 70` [highres] |

For bright images (black images would have to be inverted first), it might also be worthwhile to disable the color normalization prior to computing the sinogram. This produces a ring-shaped artifact around the image.

`--no-normalize` and `--num-angles 20` |
`--no-normalize` [highres] |
`--no-normalize` , `--randomness 42` and `--weight-angles` [highres] |

The `--offset`

option allows to change the offset of the angles used.
The following script imports *tomographic_filter.py* as a module
and generates a series of 180 filtered images at a 1°-spacing.

```
import tomographic_filter
path_in = "crow.jpg"
for ii in range(0, 180):
path_out = "crow_tf_{:03d}.png".format(ii)
tomographic_filter.tomographic_filter(path_in=path_in,
path_out=path_out,
angle_offset=ii,
randomness=42,
num_angles=47,
weight_angles=True)
```

The resulting png files can be converted to a browser-compatible video using avconv/ffmpeg:

```
avconv -r 15 -i crow_tf_%03d.png -c:v vp8 -b:v 10M crow.webm
```

original | offset series video |

Angular offsets are also a nice way of visualizing missing-angle artifacts.
Recognizability of the original image often depends on the angular range
covered by the sinogram.
In the next example, the keyword arguments
`ival_coverage=(0,100)`

and `normalize=False`

were used.

original | offset series video |

The examples shown here do not exhaust the full capability of the tomographic filter.
Besides the various combinations of the filter parameters given,
other parameters
could be used, such as `padding`

or `filtering`

. In addition, a different reconstruction
algorithm, such as Fourier domain mapping could be used. A qualitative
comparison of the available algorithms can be found at the
radontea docs.

Install Python 3, `pip install imageio radontea`

, download
tomographic_filter.py,
and execute it, passing an image path as an argument.

`python tomographic_filter.py /path/to/image.jpg`

For a list of possible command-line arguments, use `--help`

.

`python tomographic_filter.py --help`

.