Skip to content

Commit fbdb1fe

Browse files
authored
Merge pull request #328 from marcodallavecchia/mdv/blur-anim
Blur Animation source code and simplified output
2 parents bfaf89e + 7689126 commit fbdb1fe

File tree

4 files changed

+161
-2
lines changed

4 files changed

+161
-2
lines changed

episodes/06-blurring.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,7 @@ in [the scikit-image documentation](https://scikit-image.org/docs/dev/user_guide
247247

248248
::::::::::::::::::::::::::::::::::::::::::::::::::
249249

250-
This animation shows how the blur kernel moves along in the original image in
251-
order to calculate the colour channel values for the blurred image.
250+
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.
252251

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

episodes/data/letterA.tif

1.11 KB
Binary file not shown.

episodes/fig/blur-demo.gif

-30.6 MB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
### METADATA
2+
# author: Marco Dalla Vecchia @marcodallavecchia
3+
# description: Simple blurring animation of simple image
4+
# data-source: letterA.tif was created using ImageJ (https://imagej.net/ij/)
5+
###
6+
7+
### INFO
8+
# This script creates the animated illustration of blurring in episode 6
9+
###
10+
11+
### USAGE
12+
# The script was written in Python 3.12 and required the following Python packages:
13+
# - numpy==2.2.3
14+
# - scipy==1.15.2
15+
# - matplotlib==3.10.1
16+
# - tqdm==4.67.1
17+
#
18+
# The script can be executed with
19+
# $ python create_blur_animation.py
20+
# The output animation will be saved directly in the fig folder where the markdown lesson file will pick it up
21+
###
22+
23+
### POTENTIAL IMPROVEMENTS
24+
# - Change colors for rectangular patches in animation
25+
# - Ask for image input instead of hard-coding it
26+
# - Ask for FPS as input
27+
# - Ask for animation format output
28+
29+
# Import packages
30+
import numpy as np
31+
from scipy.ndimage import convolve
32+
from matplotlib import pyplot as plt
33+
from matplotlib import patches as p
34+
from matplotlib.animation import FuncAnimation
35+
from tqdm import tqdm
36+
37+
# Path to input and output images
38+
data_path = "../../../data/"
39+
fig_path = "../../../fig/"
40+
input_file = data_path + "letterA.tif"
41+
output_file = fig_path + "blur-demo.gif"
42+
43+
# Change here colors to improve accessibility
44+
kernel_color = "tab:red"
45+
center_color = "tab:olive"
46+
kernel_size = 3
47+
48+
### ANIMATION FUNCTIONS
49+
def init():
50+
"""
51+
Initialization function
52+
- Set image array data
53+
- Autoscale image display
54+
- Set XY coordinates of rectangular patches
55+
"""
56+
im.set_array(img_convolved)
57+
im.autoscale()
58+
k_rect.set_xy((-0.5, -0.5))
59+
c_rect1.set_xy((kernel_size / 2 - 1, kernel_size / 2 - 1))
60+
return [im, k_rect, c_rect1]
61+
62+
def update(frame):
63+
"""
64+
Animation update function. For every frame do the following:
65+
- Update X and Y coordinates of rectangular patch for kernel
66+
- Update X and Y coordinates of rectangular patch for central pixel
67+
- Update blurred image frame
68+
"""
69+
pbar.update(1)
70+
row = (frame % total_frames) // (img_pad.shape[0] - kernel_size + 1)
71+
col = (frame % total_frames) % (img_pad.shape[1] - kernel_size + 1)
72+
73+
k_rect.set_x(col - 0.5)
74+
c_rect1.set_x(col + (kernel_size/2 - 1))
75+
k_rect.set_y(row - 0.5)
76+
c_rect1.set_y(row + (kernel_size/2 - 1))
77+
78+
im.set_array(all_frames[frame])
79+
im.autoscale()
80+
81+
return [im, k_rect, c_rect1]
82+
83+
# MAIN PROGRAM
84+
if __name__ == "__main__":
85+
86+
print(f"Creating blurring animation with kernel size: {kernel_size}")
87+
88+
# Load image
89+
img = plt.imread(input_file)
90+
91+
### HERE WE USE THE CONVOLVE FUNCTION TO GET THE FINAL BLURRED IMAGE
92+
# I chose a simple mean filter (equal kernel weights)
93+
kernel = np.ones(shape=(kernel_size, kernel_size)) / kernel_size ** 2 # create kernel
94+
# convolve the image, i.e., apply mean filter
95+
img_convolved = convolve(img, kernel, mode='constant', cval=0) # pad borders with zero like below for consistency
96+
97+
98+
### HERE WE CONVOLVE MANUALLY STEP-BY-STEP TO CREATE ANIMATION
99+
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
100+
new_img = np.zeros(img.shape, dtype=np.uint16) # this will be the blurred final image
101+
102+
# add first frame with complete blurred image for print version of GIF
103+
all_frames = [img_convolved]
104+
105+
# precompute animation frames and append to the list
106+
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
107+
for frame in range(total_frames):
108+
row = (frame % total_frames) // (img_pad.shape[0] - kernel_size + 1) # row index
109+
col = (frame % total_frames) % (img_pad.shape[1] - kernel_size + 1) # col index
110+
img_chunk = img_pad[row:row + kernel_size, col:col + kernel_size] # get current image chunk inside the kernel
111+
new_img[row, col] = np.mean(img_chunk).astype(np.uint16) # calculate its mean -> mean filter
112+
all_frames.append(new_img.copy()) # append to animation frames list
113+
114+
# We now have an extra frame
115+
total_frames += 1
116+
117+
### FROM HERE WE START CREATING THE ANIMATION
118+
# Initialize canvas
119+
f, (ax1, ax2) = plt.subplots(ncols=2, figsize=(10, 5))
120+
121+
# Display the padded image -> this one won't change during the animation
122+
ax1.imshow(img_pad, cmap="gray")
123+
# Initialize the blurred image -> this is the first frame with already the final result
124+
im = ax2.imshow(img_convolved, animated=True, cmap="gray")
125+
126+
# Define rectangular patches to identify moving kernel
127+
k_rect = p.Rectangle((-0.5, -0.5), kernel_size, kernel_size, linewidth=2, edgecolor=kernel_color, facecolor="none", alpha=0.8) # kernel rectangle
128+
c_rect1 = p.Rectangle(((kernel_size/2 - 1), (kernel_size/2 - 1)), 1, 1, linewidth=2, edgecolor=center_color, facecolor="none") # central pixel rectangle
129+
# Add them to the figure
130+
ax1.add_patch(k_rect)
131+
ax1.add_patch(c_rect1)
132+
133+
# 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)
134+
ax2.set(
135+
ylim=((img_pad.shape[0] - kernel_size / 2), -kernel_size / 2),
136+
xlim=(-kernel_size / 2, (img_pad.shape[1] - kernel_size / 2))
137+
)
138+
139+
# We don't need to see the ticks
140+
ax1.axis("off")
141+
ax2.axis("off")
142+
143+
# Create progress bar to visualize animation progress
144+
pbar = tqdm(total=total_frames)
145+
146+
### HERE WE CREATE THE ANIMATION
147+
# Use FuncAnimation to create the animation
148+
ani = FuncAnimation(
149+
f, update,
150+
frames=range(total_frames),
151+
interval=50, # we could change the animation speed
152+
init_func=init,
153+
blit=True
154+
)
155+
156+
# Export animation
157+
plt.tight_layout()
158+
ani.save(output_file)
159+
pbar.close()
160+
print("Animation exported")

0 commit comments

Comments
 (0)