-
-
Notifications
You must be signed in to change notification settings - Fork 125
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
Changes from 8 commits
d539366
3bda9ee
2999398
0e9ea4c
ed104ad
7583479
578667b
b97fc97
a502e34
bb1bf0d
9dd39d4
f1c6320
7689126
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,162 @@ | ||||||
### 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 following Python packages: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be better/safer to specify package versions as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you in favor of adding a env.yml or requirements.txt file in the source directory at that point? Otherwise I could change the phrasing with: The script was tested with the following Python packages There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hope it's not confusing to add an
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
# - numpy | ||||||
# - scipy | ||||||
# - matplotlib | ||||||
# - tqdm | ||||||
# Install them with | ||||||
# $ conda install numpy scipy matplotlib tqdm | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
And then users/readers are free to install the above-mentioned packages as they wish. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mkcor I see the point, we can go that way, and simply list the packages and let a user install them how they wish. I guess then the only issue I see that while I simply installed the 4 packages mentioned above the actual list is much longer because of dependencies.
And that's where I'm a bit unsure of copying/pasting this whole block inside the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I can see that what I'm asking for is a sort of hybrid between what you installed explicitly and what the environment actually contains with all specifications (see, for example: https://stackoverflow.com/a/64288844/3885713). But it's standard, e.g.: https://github.com/scikit-image/scikit-image/blob/660cdc9f6ae25eb99df03e737ea96277d6bfe026/requirements/default.txt There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, thanks a lot! I was not super aware that it was standard. I will proceed with this approach and make a new commit! |
||||||
# | ||||||
# 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("Creating blurred animation with kernel size:", kernel_size) | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
# 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 | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
# convolve the image i.e. apply mean filter | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
img_convolved = convolve(img, kernel, mode='constant', cval=0) # pad borders with zero like below for consistency | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
|
||||||
### 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 | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
new_img = np.zeros(img.shape, dtype=np.uint16) # this will be the blurred final image | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
# 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 | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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)) | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
# Display the padded image -> this one won't change during the animation | ||||||
ax1.imshow(img_pad, cmap='gray') | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
# Initialize the blurred image -> this is the first frame with already the final result | ||||||
im = ax2.imshow(img_convolved, animated=True, cmap='gray') | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
# 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 | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
c_rect1 = p.Rectangle(((kernel_size/2 - 1), (kernel_size/2 - 1)), 1, 1, linewidth=2, edgecolor=center_color, facecolor='none') # central pixel rectangle | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
# 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 | ||||||
marcodallavecchia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
init_func=init, | ||||||
blit=True | ||||||
) | ||||||
|
||||||
# Export animation | ||||||
plt.tight_layout() | ||||||
ani.save(output_file) | ||||||
pbar.close() | ||||||
print("Animation exported") |
Uh oh!
There was an error while loading. Please reload this page.