Skip to content
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

Blur Animation source code and simplified output #328

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
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.
164 changes: 164 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,164 @@
### 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 requires the Python module `tqdm` which can be installed with
# $ conda install tqdm
#
# The script can be executed with
# $ python create_blur_animation.py
#
# The script will prompt the user to enter the kernel size.
###

### 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"

### 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__":
# simple input to ask for kernel size
print("Please provide kernel size for mean filter blur animation")
kernel_size = int(input("> "))

while kernel_size % 2 == 0:
print("Please use an odd kernel size")
kernel_size = int(input("> "))

print("Creating blurred 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 change 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(1,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 to the right image (without padding) is the same size as the left image (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")