Project 2

Fun with Filters and Frequencies

Fun with Filters

What is a filter? Google says:

A filter is a device, material, or program that removes unwanted parts or elements from a substance, signal, or data set while letting the desired parts pass through

In this part of the project I remove “unwanted parts” from images. A natural question is, “what can we easily remove from an image?” You could, of course, crop out some parts of the image and call that a filter. It is, but it is a boring one.

Let’s try to extract the edges from an image. In order to do this, we must discuss something called “convolution,” a word which derives not from “convolute” but “convolve.” I think of convolution as a sort of spatial “mixing” of the original image according to some rule. Consider the following rule: the pixel in position $(x, y)$ in the new image comes from position $(x-1, y)$ in the old image. We can conveniently describe this modification with a filter:

\[\begin{bmatrix} \color{blue}{0} & 0 & 0 \\ \color{green}{1} & \color{red}{0} & 0 \\ 0 & 0 & 0 \end{bmatrix}\]

A filter essentially “stamps” several scaled and shifted copies of the original image, and then adds them all up. In this case, the result is a single copy of the original image shifted leftward by $1$ pixel, since the only non-zero entry (in green) stamps out a copy shifted leftward relative to the filter’s center. To further develop your understanding, consider the following tweaks:

Consider now the “box filter” below:

\[\begin{bmatrix} \frac{1}{9} & \frac{1}{9} & \frac{1}{9} \\ \frac{1}{9} & \frac{1}{9} & \frac{1}{9} \\ \frac{1}{9} & \frac{1}{9} & \frac{1}{9} \end{bmatrix}\]

This will add nine copies of the original image, each shifted by 1 pixel in a different direction and scaled to be $1 / 9$ as bright. I interpret this filter as something which “spreads out” the energy formerly concentrated in just one pixel. Suppose the original image had a single bright pixel at $(x, y)$; the filter spreads this energy to all its neighbors, so that the resulting image has $9$ dimmer pixels instead. Thus, this filter1 results in a blur:

Copies of the cameraman scaled by a box filter and added up; then full, same, and valid crops

Note that we had to deal with some edge cases, in our dealing with the edges. Hence we define three ‘modes’ of discrete convolution:

That’s a visual and intuitive explanation of convolution. I hope it was not too convoluted! It turns out there there is a mathemical equation to model the adding-up of scaled and shifted copies of an image according to a filter. Here is the first part of some code that computes this, in case you are grading me:

def homemade_conv2D(s, f, mode):
    # ---- original dimensions ----
    h_s, w_s = s.shape
    h_f, w_f = f.shape

    # ---- padding ----
    s_padded = zero_pad(s, f, mode)

    # ---- determine result size ----
    res = get_empty_res(s, f, mode)
    h_r, w_r = res.shape

The details of zero padding and the size of the empty result image are determined by our choice of ‘mode’ according to the edge cases explained above. For example, in the valid case, the image is not zero padded so that we only add up entries which exist in the original matrix, and the empty result matrix is made to be smaller than the original image.

Then we can compute each filtered pixel one-by-one, by scaling and adding source pixels according to the filter.

# ---- flip-and-drag style convolution ----
ff = np.flip(f)
for n in range(h_r):
    for m in range(w_r):
        px = 0.0
        for u in range(h_f):
            for v in range(w_f):
                value = s_padded[n+u, m+v]
                weight = ff[u, v]
                px += value * weight
        res[n, m] = px

The code below - which I call “cookie-cutter” style because it slices out the region of the original image which contributes to each filtered pixel, like a cookie-mold does to a sheet of dough - does the same thing, but faster. This is because numpy operations are “vectorized,” i.e., faster than python for loops. In fact, you could do better still with scipy.signal.convolve2d, which calls highly optimized C code to compute a convolution.

# ---- cookie-cutter style convolution ----
ff = np.flip(f)
for n in range(h_r):
    for m in range(w_r):
        patch = s_padded[n:n+h_f, m:m+w_f]
        res[n, m] = np.sum(patch * ff)

Check out the results of some convolutions computed with the following filters:

Notice that the dx and dy filters show edges in only one direction; we can emphasize edges in all directions by computing the gradient $\text{dx}^2 + \text{dy}^2$.

The gradient picks up “edges” in small “noisy” details, which isn’t really what we want with an edge detector. Check out the background and foreground - textured parts of the scene, like grass, show up in the gradient image. We can fix this by thresholding, i.e., setting all image values below a certain value to $0$:

Cameraman gradient thresholded at 10 percent
10% of max
Cameraman gradient thresholded at 20 percent
20% of max
Cameraman gradient thresholded at 50 percent
50% of max

I think the $20\%$ threshold looks best, because it removes most of the noisy details but retains edges. Notice that the $50\%$ threshold is too high, harming the edges’ visual integrity.

Here is another way to remove the “noisy” details, using the fact that a blur averages each pixel with its neighbors, thereby smoothing out rough textures. For example, see the selfies above, where the gaussian blur removed sharp edges between buildings in the background. A clever trick is to incorporate a blur into our edge detector. The animation below explains the strategy.2

Animation of DOG edge detection

It turns out that convolution is associative. In english, the derivative of a blurred image is equivalent to the blurred derivative of an image. These blurred derivatives are called “derivative of gaussian” (DOG) filters. You compute one by convolving a derivative with a gaussian.

Here are some results of applying a DOG3 and then thresholding:

Cameraman edges from a 7 by 7 derivative of gaussian, thresholded at 40 percent
7×7 blur kernel · 40% threshold
Cameraman edges from a 21 by 21 derivative of gaussian, thresholded at 30 percent
21×21 blur kernel · 30% threshold

Applying more blur indeed removes more of the “undesired” background edges. However, this also blurs the edges, so I prefer the mild blur because the edges are sharper.

There is actually another strategy for edge detection, called the Laplacian, which basically tells us how much each point differs the local average. You can approximate it with a $2\text{D}$ filter:

Cameraman after a basic laplacian filter
laplacian
Cameraman laplacian thresholded at 10 percent
10% threshold

The results aren’t quite as good; the threshold actually removes some edges (see the cameraman’s coat) we’d rather keep before the textured grass. The results aren’t quite as good as the gradient because pixels in “noisy” areas, i.e. grass, tend to be very different from the local average. The Laplacian likes peaks or troughs in pixel brightness, which is not exactly what we want from an edge detector; rather, we want to detect sharp change in one direction.

Applying a blur is therefore more important for the laplacian; a blur effectively averages pixels which removes exactly the type of non-edge “noise” we don’t want to see. Check out the results of applying a blurred Laplacian - I coin this filter the “LOG” - and then thresholding.

Cameraman laplacian from a 7 by 7 blur, thresholded at 30 percent
7×7 LOG · 30% threshold
Cameraman laplacian from a 21 by 21 blur, thresholded at 30 percent
21×21 LOG · 30% threshold

Fun with Frequencies

To sharpen an image means to emphasize the edges. How can we do this with filters? Consider the results of applying gaussian filters shown in Part 1. These give a blurry, low-frequency estimate of the original image. Intuitively, subtracting the blurry image from the original will give a high-frequency estimate.

Notice that the high-frequency estimate contains edges. Therefore, we can sharpen by adding the high frequency estimate (scaled, depending on your desired “sharp level”) to the original.

You can also play with the blur level; more blur $\rightarrow$ less frequencies in the low frequency estimate $\rightarrow$ more frequencies in the high frequency estimate that get “boosted” by the sharpening filter.

Also, this strategy ultimately does not add new high frequency information, rather it emphasizes the existing high frequency information. Hence, if you try to sharpen a blurred image, the results will not be as good. Confirm this visually by turning on ‘pre-blur.’

blur
pre-blur
Taj Mahal sharpened with alpha 1
×1
Taj Mahal sharpened with alpha 5
×5
Taj Mahal sharpened with alpha 15
×15
Gateway of India sharpened with alpha 1
×1
Gateway of India sharpened with alpha 5
×5
Gateway of India sharpened with alpha 15
×15

Hybrid Images

It turns out that human vision relies on lower spatial frequencies to view far-away objects. I think that this is because the angular resolution of human vision is (roughly) fixed; from far away, the eye can only resolve features with greater separation, compared to the features resolvable from nearby. Hence, we simply cannot resolve the high frequency details that are resolvable up close.

We can take advantage of this to do a cool trick with some high- and low- frequency estimates, obtained in the same way as in the previous section. In particular, we can add the low frequencies from one image ($A$) to another image’s ($B$) high frequencies. From up close, you’ll be able to see the high frequency details and resolve $A$, but from far away you’ll only see the low frequency estimate of $B$.

I implemented high pass and low pass filters as a function of cycles per pixel; then created high- and low- frequency estimates; and then added them together. I also added some gain to the high frequency image, to account for the fact that most energy of natural images is in the low frequencies, and we removed those. After tweaking these parameters4, I got the following results:

zoom
Nutmeg, the high-frequency source
nutmeg · near
Derek, the low-frequency source
derek · far
Hybrid of nutmeg and derek
hybrid

You can see the blurry background image by squinting or by walking a few meters away from the screen. Pressing the ‘out’ button downsamples the source image, which doesn’t really use the frequency sensitivity of your eyes, but it emphasizes the fact that the blurry image lives in the low frequencies.

zoom
Anakin, the high-frequency source
anakin · near
Vader, the low-frequency source
vader · far
Hybrid of anakin and vader
hybrid
zoom
East Wing of the White House, the high-frequency source
East wing
"Ballroom" at the White House, the low-frequency source
"Ballroom"
Hybrid of White House before and after
hybrid

Let’s see what happens in the frequency domain for my favorite image of the three, Anakin. The frequency domain is a representation of an image by the frequencies that make it up. It turns out that convolution in space is equivalent to multiplication in frequency.5

We see that a LPF removed all the little speckles from the outer regions of the frequency domain. The only information (i.e. bright pixels) that survives is near the low frequencies in the center.

The effect of a HPF is harder to notice because the high frequencies are naturally much lower-energy (less bright) to begin with. If you look closely, the result looks darker in the center, relative to the rest of the frequency domain.

Adding these, we get the following result frequency representation. Notice that it looks much more similar to the frequency representations we started with than either of the high- or low- passed versions did alone.

Blending Images

How can we blend images together? Simply cropping one image onto another won’t look right. The solution is to use Gaussian Stacks and Laplacian Stacks, which allow us to view and modify images at multiple scales.

A Gaussian Stack is a sequence of subsequently lower-detail images, obtained by applying low pass filters with increasingly narrow cutoffs. A Laplacian stack takes the difference between the layers of the Gaussian Stack, yielding band-pass frequency estimates of an image. Intuitively, each layer of the Laplacian Stack contains a different level of detail, and we can recover the original image by recombining these levels.

I think the following animation explains this better than I could with words:

Animation of Gaussian and Laplacian stacks on an apple

This is exactly what Burt and Adelson used in 1983 to combine images of an apple and an orange. They used such a multi-scale stack to blend gradually the blurry estimate at the lowest level, but more abruptly combine the high detail levels. Why does this work? Well, it smoothly blends the smooth changes, and quickly blends the quick changes! The result looks way nicer than you’d get with any single-scale combination, where you’d have to compromise the gradual changes with an abrupt transition or vice versa.

Check out the sequence of half-images below, and the blended counterpart. Notice that the detail images have a clearly defined boundary, but there a smoother transtion in the low frequency images.

Oraple level 2, left apple contribution
Level 2 · Left
Oraple level 2, right orange contribution
Level 2 · Right
Oraple level 2, blended
Level 2 · Blended
Oraple level 4, left apple contribution
Level 4 · Left
Oraple level 4, right orange contribution
Level 4 · Right
Oraple level 4, blended
Level 4 · Blended
Oraple level 6, left apple contribution
Level 6 · Left
Oraple level 6, right orange contribution
Level 6 · Right
Oraple level 6, blended
Level 6 · Blended

By adding together the blended levels we reconstruct the legendary oraple:

Final oraple blend of apple and orange

I had some fun blending together images. As cool as the final blended result is, it is more fun in my opinion to compare the blended sub-levels (I picked my favorites for each). The low-frequency images transition smoothly, so there is no abrupt change in brightness, while the high frequency images change abruptly, so that small details don’t overlap in the transition region.

Here is a blended picture of the white house, before and after the beginning of construction on a vital national security enhancement:

White House blend level 1, left
Level 1 · Left
White House blend level 1, right
Level 1 · Right
White House blend level 1, blended
Level 1 · Blended
White House blend level 3, left
Level 3 · Left
White House blend level 3, right
Level 3 · Right
White House blend level 3, blended
Level 3 · Blended
White House blend level 5, left
Level 5 · Left
White House blend level 5, right
Level 5 · Right
White House blend level 5, blended
Level 5 · Blended
Final White House blend
Result

Here is a blended picture of the Commissioner’s Plan of 1811 for Manhattan, and a screenshot I took of Manhattan from apple maps. It’s fascinating that the Manhattan map is 200+ years old!

NYC blend level 1, left
Level 1 · Left
NYC blend level 1, right
Level 1 · Right
NYC blend level 1, blended
Level 1 · Blended
NYC blend level 3, left
Level 3 · Left
NYC blend level 3, right
Level 3 · Right
NYC blend level 3, blended
Level 3 · Blended
Final NYC satellite and map blend
Result

Finally, we will repeat the exact same process as above with some irregular masks to do a more-interesting image blend. In particular, we will

  1. compute the laplacian stack of two base images, decomposing them by detail level (i.e. isolating one frequency band at a time)
  2. compute the gaussian stack of an irregular mask that covers some part of one of the images. like before, this allows us to abruptly change high frequency estimate while smoothly blending the low frequencies.
  3. apply the appropriate mask to each level of the Laplacian Stack
  4. add together the results for a cool blended image!

I did all the alignment and mask-creation with my all-time-favorite photo-editing suite: Google Slides.

For my first image, I selected this selfie I took of my brother and I; we dressed up as Donkey and Shrek for a race called Bay to Breakers, which happens each May in San Francisco. It felt right to add the real deal to this picture.

Swamp blend level 3, blended
Level 3 · Blended
Swamp blend level 5, blended
Level 5 · Blended
Final swamp blend of Shrek into a waterfall photo
Final result

While looking through my camera roll for more good images to blend of a more-personal flavor, I found a group photo taken on the last day of the MRI class of myself, my buddies Arvind and Parsa, and Miki and Rinni (who was the professor and TA respectively). Ever since taking this photo, I’ve really regretted including Arvind on this one.6 The guy is just so damn old! Anyway, with the knowledge gained in this project, I figured I could finally fix this tragedy! My solution: I stalked Arvind’s travel blog, locating a suitable picture of a more spry and youthful Arvind, and replaced the old fart fogging up this otherwise treasured memory of mine.

Yung Arvind blend level 5, blended
Level 5 · Blended
Yung Arvind blend level 7, blended
Level 7 · Blended
Final group photo blend with a younger Arvind
Final result

For my final swap, I want you to guess which of these is the original7:

Original Jeff and Ryan photo
Swapped Jeff and Ryan blend
  1. The GIF actually uses a $27 \times 27$ filter, chunked into $3 \times 3$ “pixels.” That is, each addition is actually the addition of all 9 copies coming from a $3 \times 3$ chunk. This exaggerates the effect of an analogous $3 \times 3$ box filter, for the sake of visualization. ↩

  2. I made the animation with pixel value cutoffs, but later decided it would be more useful to define a threshold based on the percent of the maximum pixel value. ↩

  3. Since convolution is also commutative, you’d get the same results regardless of the order in which the blur and the derivative are applied. There are are some tricks you have to play with these discrete and finite support image signals, however, regarding the convolution mode so that they match exactly. ↩

  4. HP · LP · GAIN cutoffs of: $0.082$ · $0.017$ · $5.7$ for derek / nutmeg, $0.076$ · $0.027$ · $2.8$ for anakin / vader, and $0.092$ · $0.18$ · $1.2$ for whitehouse. ↩

  5. Consider an ideal low-frequency-passing filter. In the space domain, you can apply a blur filter that smooths out the high-frequency details. Equivalently, you could look at the frequency domain, and set all the low frequencies to $0$ but leave the high frequencies alone. Hence you could implement a low-frequency-passing filter by multiplying the frequency representation with some sort of “mask” that is $1$ for frequencies you want to keep, and $0$s elsewhere. While the convolution-multiplication equivalence is complicated to prove, hopefully this example lends it some believability. ↩

  6. This is entirely a joke. If Arvind is an apple and I’m an orange, together we make a truly well-blended oraple. ↩

  7. Left. Jeffery’s face is way bigger than mine so this one took several attempts; notice that I had to increase the size of mine to fit over his. ↩