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 7 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 take a very simple grayscale 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 image is composed of only a single channel, but it would work as well on a multi-channel image.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Let's take a very simple grayscale 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 image is composed of only a single channel, but it would work as well on a multi-channel 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.

I suggested removing 'grayscale' not because it is wrong, of course, but just to avoid the repetition afterwards (when we mention it is single-channel) and because it's actually even 'simpler' than grayscale, it's binary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, thanks for the rephrasing. I implemented this change in the next commit.


![](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.
162 changes: 162 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,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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better/safer to specify package versions as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope it's not confusing to add an env.yml file in episodes/fig/source/06-blurring/ (what if there were more than one script in there?). Personally, I think it would be enough to 'copy-paste' the content of the env/requirements file right here, so you just append ==[version number] at the end of lines 13--16.

# - numpy
# - scipy
# - matplotlib
# - tqdm
# Install them with
# $ conda install numpy scipy matplotlib tqdm
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Install them with
# $ conda install numpy scipy matplotlib tqdm

And then users/readers are free to install the above-mentioned packages as they wish.

Copy link
Contributor Author

@marcodallavecchia marcodallavecchia Mar 16, 2025

Choose a reason for hiding this comment

The 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.

$ conda env export --from-history

name: img-proc
channels:
  - conda-forge
dependencies:
  - python=3.12
  - matplotlib
  - numpy
  - scipy
  - tqdm
$ conda list

# packages in environment at /home/mdallave/miniforge3/envs/img-proc:
#
# Name                    Version                   Build  Channel
_libgcc_mutex             0.1                 conda_forge    conda-forge
_openmp_mutex             4.5                       2_gnu    conda-forge
alsa-lib                  1.2.13               hb9d3cd8_0    conda-forge
brotli                    1.1.0                hb9d3cd8_2    conda-forge
brotli-bin                1.1.0                hb9d3cd8_2    conda-forge
bzip2                     1.0.8                h4bc722e_7    conda-forge
ca-certificates           2025.1.31            hbcca054_0    conda-forge
cairo                     1.18.4               h3394656_0    conda-forge
colorama                  0.4.6              pyhd8ed1ab_1    conda-forge
contourpy                 1.3.1           py312h68727a3_0    conda-forge
cycler                    0.12.1             pyhd8ed1ab_1    conda-forge
cyrus-sasl                2.1.27               h54b06d7_7    conda-forge
dbus                      1.13.6               h5008d03_3    conda-forge
double-conversion         3.3.1                h5888daf_0    conda-forge
expat                     2.6.4                h5888daf_0    conda-forge
font-ttf-dejavu-sans-mono 2.37                 hab24e00_0    conda-forge
font-ttf-inconsolata      3.000                h77eed37_0    conda-forge
font-ttf-source-code-pro  2.038                h77eed37_0    conda-forge
font-ttf-ubuntu           0.83                 h77eed37_3    conda-forge
fontconfig                2.15.0               h7e30c49_1    conda-forge
fonts-conda-ecosystem     1                             0    conda-forge
fonts-conda-forge         1                             0    conda-forge
fonttools                 4.56.0          py312h178313f_0    conda-forge
freetype                  2.13.3               h48d6fc4_0    conda-forge
graphite2                 1.3.13            h59595ed_1003    conda-forge
harfbuzz                  10.4.0               h76408a6_0    conda-forge
icu                       75.1                 he02047a_0    conda-forge
keyutils                  1.6.1                h166bdaf_0    conda-forge
kiwisolver                1.4.8           py312h84d6215_0    conda-forge
krb5                      1.21.3               h659f571_0    conda-forge
lcms2                     2.17                 h717163a_0    conda-forge
ld_impl_linux-64          2.43                 h712a8e2_4    conda-forge
lerc                      4.0.0                h27087fc_0    conda-forge
libblas                   3.9.0           31_h59b9bed_openblas    conda-forge
libbrotlicommon           1.1.0                hb9d3cd8_2    conda-forge
libbrotlidec              1.1.0                hb9d3cd8_2    conda-forge
libbrotlienc              1.1.0                hb9d3cd8_2    conda-forge
libcblas                  3.9.0           31_he106b2a_openblas    conda-forge
libclang-cpp19.1          19.1.7          default_hb5137d0_1    conda-forge
libclang13                19.1.7          default_h9c6a7e4_1    conda-forge
libcups                   2.3.3                h4637d8d_4    conda-forge
libdeflate                1.23                 h4ddbbb0_0    conda-forge
libdrm                    2.4.124              hb9d3cd8_0    conda-forge
libedit                   3.1.20250104    pl5321h7949ede_0    conda-forge
libegl                    1.7.0                ha4b6fd6_2    conda-forge
libexpat                  2.6.4                h5888daf_0    conda-forge
libffi                    3.4.6                h2dba641_0    conda-forge
libgcc                    14.2.0               h767d61c_2    conda-forge
libgcc-ng                 14.2.0               h69a702a_2    conda-forge
libgfortran               14.2.0               h69a702a_2    conda-forge
libgfortran5              14.2.0               hf1ad2bd_2    conda-forge
libgl                     1.7.0                ha4b6fd6_2    conda-forge
libglib                   2.82.2               h2ff4ddf_1    conda-forge
libglvnd                  1.7.0                ha4b6fd6_2    conda-forge
libglx                    1.7.0                ha4b6fd6_2    conda-forge
libgomp                   14.2.0               h767d61c_2    conda-forge
libiconv                  1.18                 h4ce23a2_1    conda-forge
libjpeg-turbo             3.0.0                hd590300_1    conda-forge
liblapack                 3.9.0           31_h7ac8fdf_openblas    conda-forge
libllvm19                 19.1.7               ha7bfdaf_1    conda-forge
liblzma                   5.6.4                hb9d3cd8_0    conda-forge
libnsl                    2.0.1                hd590300_0    conda-forge
libntlm                   1.8                  hb9d3cd8_0    conda-forge
libopenblas               0.3.29          pthreads_h94d23a6_0    conda-forge
libopengl                 1.7.0                ha4b6fd6_2    conda-forge
libpciaccess              0.18                 hd590300_0    conda-forge
libpng                    1.6.47               h943b412_0    conda-forge
libpq                     17.4                 h27ae623_0    conda-forge
libsqlite                 3.49.1               hee588c1_1    conda-forge
libstdcxx                 14.2.0               h8f9b012_2    conda-forge
libstdcxx-ng              14.2.0               h4852527_2    conda-forge
libtiff                   4.7.0                hd9ff511_3    conda-forge
libuuid                   2.38.1               h0b41bf4_0    conda-forge
libwebp-base              1.5.0                h851e524_0    conda-forge
libxcb                    1.17.0               h8a09558_0    conda-forge
libxcrypt                 4.4.36               hd590300_1    conda-forge
libxkbcommon              1.8.1                hc4a0caf_0    conda-forge
libxml2                   2.13.6               h8d12d68_0    conda-forge
libxslt                   1.1.39               h76b75d6_0    conda-forge
libzlib                   1.3.1                hb9d3cd8_2    conda-forge
matplotlib                3.10.1          py312h7900ff3_0    conda-forge
matplotlib-base           3.10.1          py312hd3ec401_0    conda-forge
munkres                   1.1.4              pyh9f0ad1d_0    conda-forge
mysql-common              9.0.1                h266115a_5    conda-forge
mysql-libs                9.0.1                he0572af_5    conda-forge
ncurses                   6.5                  h2d0b736_3    conda-forge
numpy                     2.2.3           py312h72c5963_0    conda-forge
openjpeg                  2.5.3                h5fbd93e_0    conda-forge
openldap                  2.6.9                he970967_0    conda-forge
openssl                   3.4.1                h7b32b05_0    conda-forge
packaging                 24.2               pyhd8ed1ab_2    conda-forge
pcre2                     10.44                hba22ea6_2    conda-forge
pillow                    11.1.0          py312h80c1187_0    conda-forge
pip                       25.0.1             pyh8b19718_0    conda-forge
pixman                    0.44.2               h29eaf8c_0    conda-forge
pthread-stubs             0.4               hb9d3cd8_1002    conda-forge
pyparsing                 3.2.1              pyhd8ed1ab_0    conda-forge
pyside6                   6.8.2           py312h91f0f75_1    conda-forge
python                    3.12.9          h9e4cc4f_1_cpython    conda-forge
python-dateutil           2.9.0.post0        pyhff2d567_1    conda-forge
python_abi                3.12                    5_cp312    conda-forge
qhull                     2020.2               h434a139_5    conda-forge
qt6-main                  6.8.2                h588cce1_0    conda-forge
readline                  8.2                  h8c095d6_2    conda-forge
scipy                     1.15.2          py312ha707e6e_0    conda-forge
setuptools                75.8.2             pyhff2d567_0    conda-forge
six                       1.17.0             pyhd8ed1ab_0    conda-forge
tk                        8.6.13          noxft_h4845f30_101    conda-forge
tornado                   6.4.2           py312h66e93f0_0    conda-forge
tqdm                      4.67.1             pyhd8ed1ab_1    conda-forge
tzdata                    2025a                h78e105d_0    conda-forge
unicodedata2              16.0.0          py312h66e93f0_0    conda-forge
wayland                   1.23.1               h3e06ad9_0    conda-forge
wheel                     0.45.1             pyhd8ed1ab_1    conda-forge
xcb-util                  0.4.1                hb711507_2    conda-forge
xcb-util-cursor           0.1.5                hb9d3cd8_0    conda-forge
xcb-util-image            0.4.0                hb711507_2    conda-forge
xcb-util-keysyms          0.4.1                hb711507_0    conda-forge
xcb-util-renderutil       0.3.10               hb711507_0    conda-forge
xcb-util-wm               0.4.2                hb711507_0    conda-forge
xkeyboard-config          2.43                 hb9d3cd8_0    conda-forge
xorg-libice               1.1.2                hb9d3cd8_0    conda-forge
xorg-libsm                1.2.6                he73a12e_0    conda-forge
xorg-libx11               1.8.12               h4f16b4b_0    conda-forge
xorg-libxau               1.0.12               hb9d3cd8_0    conda-forge
xorg-libxcomposite        0.4.6                hb9d3cd8_2    conda-forge
xorg-libxcursor           1.2.3                hb9d3cd8_0    conda-forge
xorg-libxdamage           1.1.6                hb9d3cd8_0    conda-forge
xorg-libxdmcp             1.1.5                hb9d3cd8_0    conda-forge
xorg-libxext              1.3.6                hb9d3cd8_0    conda-forge
xorg-libxfixes            6.0.1                hb9d3cd8_0    conda-forge
xorg-libxi                1.8.2                hb9d3cd8_0    conda-forge
xorg-libxrandr            1.5.4                hb9d3cd8_0    conda-forge
xorg-libxrender           0.9.12               hb9d3cd8_0    conda-forge
xorg-libxtst              1.2.5                hb9d3cd8_3    conda-forge
xorg-libxxf86vm           1.1.6                hb9d3cd8_0    conda-forge
zstd                      1.5.7                hb8e6e7a_1    conda-forge

And that's where I'm a bit unsure of copying/pasting this whole block inside the .py file.

Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 less markdown will pick it up
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the 'less markdown,' please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops sorry another typo.. It was supposed to be lesson. I fixed it in the next commit.

###

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

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