Skip to content

Blur Animation source code and simplified output #328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions episodes/06-blurring.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,7 @@ in [the scikit-image documentation](https://scikit-image.org/docs/dev/user_guide

::::::::::::::::::::::::::::::::::::::::::::::::::

This animation shows how the blur kernel moves along in the original image in
order to calculate the colour channel values for the blurred image.
Let's consider a very simple image to see blurring in action. The animation below shows how the blur kernel (large red square) moves along the image on the left in order to calculate the corresponding values for the blurred image (yellow central square) on the right. In this simple case, the original image is single-channel, but blurring would work likewise on a multi-channel image.

![](fig/blur-demo.gif){alt='Blur demo animation'}

Expand Down
Binary file added episodes/data/letterA.tif
Binary file not shown.
Binary file modified episodes/fig/blur-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
160 changes: 160 additions & 0 deletions episodes/fig/source/06-blurring/create_blur_animation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
### METADATA
# author: Marco Dalla Vecchia @marcodallavecchia
# description: Simple blurring animation of simple image
# data-source: letterA.tif was created using ImageJ (https://imagej.net/ij/)
###

### INFO
# This script creates the animated illustration of blurring in episode 6
###

### USAGE
# The script was written in Python 3.12 and required the following Python packages:
# - numpy==2.2.3
# - scipy==1.15.2
# - matplotlib==3.10.1
# - tqdm==4.67.1
#
# The script can be executed with
# $ python create_blur_animation.py
# The output animation will be saved directly in the fig folder where the markdown lesson file will pick it up
###

### POTENTIAL IMPROVEMENTS
# - Change colors for rectangular patches in animation
# - Ask for image input instead of hard-coding it
# - Ask for FPS as input
# - Ask for animation format output

# Import packages
import numpy as np
from scipy.ndimage import convolve
from matplotlib import pyplot as plt
from matplotlib import patches as p
from matplotlib.animation import FuncAnimation
from tqdm import tqdm

# Path to input and output images
data_path = "../../../data/"
fig_path = "../../../fig/"
input_file = data_path + "letterA.tif"
output_file = fig_path + "blur-demo.gif"

# Change here colors to improve accessibility
kernel_color = "tab:red"
center_color = "tab:olive"
kernel_size = 3

### ANIMATION FUNCTIONS
def init():
"""
Initialization function
- Set image array data
- Autoscale image display
- Set XY coordinates of rectangular patches
"""
im.set_array(img_convolved)
im.autoscale()
k_rect.set_xy((-0.5, -0.5))
c_rect1.set_xy((kernel_size / 2 - 1, kernel_size / 2 - 1))
return [im, k_rect, c_rect1]

def update(frame):
"""
Animation update function. For every frame do the following:
- Update X and Y coordinates of rectangular patch for kernel
- Update X and Y coordinates of rectangular patch for central pixel
- Update blurred image frame
"""
pbar.update(1)
row = (frame % total_frames) // (img_pad.shape[0] - kernel_size + 1)
col = (frame % total_frames) % (img_pad.shape[1] - kernel_size + 1)

k_rect.set_x(col - 0.5)
c_rect1.set_x(col + (kernel_size/2 - 1))
k_rect.set_y(row - 0.5)
c_rect1.set_y(row + (kernel_size/2 - 1))

im.set_array(all_frames[frame])
im.autoscale()

return [im, k_rect, c_rect1]

# MAIN PROGRAM
if __name__ == "__main__":

print(f"Creating blurring animation with kernel size: {kernel_size}")

# Load image
img = plt.imread(input_file)

### HERE WE USE THE CONVOLVE FUNCTION TO GET THE FINAL BLURRED IMAGE
# I chose a simple mean filter (equal kernel weights)
kernel = np.ones(shape=(kernel_size, kernel_size)) / kernel_size ** 2 # create kernel
# convolve the image, i.e., apply mean filter
img_convolved = convolve(img, kernel, mode='constant', cval=0) # pad borders with zero like below for consistency


### HERE WE CONVOLVE MANUALLY STEP-BY-STEP TO CREATE ANIMATION
img_pad = np.pad(img, (int(np.ceil(kernel_size/2) - 1), int(np.ceil(kernel_size/2) - 1))) # Pad image to deal with borders
new_img = np.zeros(img.shape, dtype=np.uint16) # this will be the blurred final image

# add first frame with complete blurred image for print version of GIF
all_frames = [img_convolved]

# precompute animation frames and append to the list
total_frames = (img_pad.shape[0] - kernel_size + 1) * (img_pad.shape[1] - kernel_size + 1) # total frames if by chance image is not squared
for frame in range(total_frames):
row = (frame % total_frames) // (img_pad.shape[0] - kernel_size + 1) # row index
col = (frame % total_frames) % (img_pad.shape[1] - kernel_size + 1) # col index
img_chunk = img_pad[row:row + kernel_size, col:col + kernel_size] # get current image chunk inside the kernel
new_img[row, col] = np.mean(img_chunk).astype(np.uint16) # calculate its mean -> mean filter
all_frames.append(new_img.copy()) # append to animation frames list

# We now have an extra frame
total_frames += 1

### FROM HERE WE START CREATING THE ANIMATION
# Initialize canvas
f, (ax1, ax2) = plt.subplots(ncols=2, figsize=(10, 5))

# Display the padded image -> this one won't change during the animation
ax1.imshow(img_pad, cmap="gray")
# Initialize the blurred image -> this is the first frame with already the final result
im = ax2.imshow(img_convolved, animated=True, cmap="gray")

# Define rectangular patches to identify moving kernel
k_rect = p.Rectangle((-0.5, -0.5), kernel_size, kernel_size, linewidth=2, edgecolor=kernel_color, facecolor="none", alpha=0.8) # kernel rectangle
c_rect1 = p.Rectangle(((kernel_size/2 - 1), (kernel_size/2 - 1)), 1, 1, linewidth=2, edgecolor=center_color, facecolor="none") # central pixel rectangle
# Add them to the figure
ax1.add_patch(k_rect)
ax1.add_patch(c_rect1)

# Fix limits of the image on the right (without padding) so that it is the same size as the image on the left (with padding)
ax2.set(
ylim=((img_pad.shape[0] - kernel_size / 2), -kernel_size / 2),
xlim=(-kernel_size / 2, (img_pad.shape[1] - kernel_size / 2))
)

# We don't need to see the ticks
ax1.axis("off")
ax2.axis("off")

# Create progress bar to visualize animation progress
pbar = tqdm(total=total_frames)

### HERE WE CREATE THE ANIMATION
# Use FuncAnimation to create the animation
ani = FuncAnimation(
f, update,
frames=range(total_frames),
interval=50, # we could change the animation speed
init_func=init,
blit=True
)

# Export animation
plt.tight_layout()
ani.save(output_file)
pbar.close()
print("Animation exported")