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