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