11# DigiPyRo is a program with two main functions:
22# 1. Digital rotation of movies.
33# 2. Single-particle tracking.
4+ # All of its functionalities can be accessed through the GUI window which appears when DigiPyRo is run.
45# See the README and instructables for further documentation, installation instructions and examples.
56
67
1920### Helper Functions ###
2021########################
2122
23+ ## Helper Functions: Section 1 -- User-Interaction Functions
24+ ## The majority of functions in this section relate to user-identification of the region of interest (ROI) which will be digitally rotated,
25+ ## or the intialization of single-particle tracking
26+
27+ # Allows user to manually identify center of rotation
2228def centerClick (event , x , y , flags , param ):
2329 global center , frame
2430 clone = frame .copy ()
@@ -34,6 +40,7 @@ def centerImg(img, x_c, y_c): # shifts image so that it is centered at (x_c, y_c
3440 shiftMatrix = np .float32 ([[1 , 0 , dx ], [0 , 1 , dy ]])
3541 return cv2 .warpAffine (img , shiftMatrix , (width , height ))
3642
43+ # User drags mouse and releases along a diameter of the particle to set an approximate size and location of particle for DPR to search for
3744def locate (event , x , y , flags , param ):
3845 global frame , particleStart , particleEnd , particleCenter , particleRadius
3946 clone = frame .copy ()
@@ -48,6 +55,7 @@ def locate(event, x, y, flags, param):
4855 cv2 .imshow ('Locate Ball' , frame )
4956 frame = clone .copy () # resets to original image
5057
58+ # User clicks points along the circumference of a circular ROI. This function records the points and calculates the best-fit circle through the points.
5159def circumferencePoints (event , x , y , flags , param ):
5260 global npts , center , frame , xpoints , ypoints , r , poly1 , poly2
5361 if event == cv2 .EVENT_LBUTTONDOWN :
@@ -75,7 +83,8 @@ def circumferencePoints(event, x, y, flags, param):
7583 cv2 .circle (frame , center , r , (0 ,255 ,0 ), 1 )
7684 cv2 .imshow ('CenterClick' , frame )
7785 frame = clone .copy ()
78-
86+
87+ # The same as "circumferencePoints", except this calculates a polygon ROI. The center is calculated as the "center of mass" of the polygon
7988def nGon (event , x , y , flags , param ):
8089 global npts , center , frame , xpoints , ypoints , r , poly1 , poly2
8190 if event == cv2 .EVENT_LBUTTONDOWN :
@@ -101,7 +110,7 @@ def nGon(event, x, y, flags, param):
101110 cv2 .imshow ('CenterClick' , frame )
102111 frame = clone .copy ()
103112
104-
113+ # Removes the most recently clicked point in the array of circle/polygon circumference points.
105114def removePoint (orig ):
106115 global npts , center , frame , xpoints , ypoints , r , poly1 , poly2 , custMask
107116 if npts == 0 :
@@ -147,6 +156,7 @@ def removePoint(orig):
147156 cv2 .circle (frame , center , r , (0 ,255 ,0 ), 1 )
148157 cv2 .imshow ('CenterClick' , frame )
149158
159+ # Calculates the center and radius of the best-fit circle through an array of points (by least-squares method)
150160def calc_center (xp , yp ):
151161 n = len (xp )
152162 circleMatrix = np .matrix ([[np .sum (xp ** 2 ), np .sum (xp * yp ), np .sum (xp )], [np .sum (xp * yp ), np .sum (yp ** 2 ), np .sum (yp )], [np .sum (xp ), np .sum (yp ), n ]])
@@ -161,6 +171,7 @@ def calc_center(xp, yp):
161171 diam = d ** (0.5 )
162172 return np .array ([int (xc ), int (yc ), int (diam / 2 )])
163173
174+ # Adds diagnostic information, including time and physical/digital rotations to each frame of the movie
164175def annotateImg (img , i ):
165176 font = cv2 .FONT_HERSHEY_TRIPLEX
166177
@@ -170,10 +181,6 @@ def annotateImg(img, i):
170181
171182 img [25 :25 + spinlab .shape [0 ], (width - 25 )- spinlab .shape [1 ]:width - 25 ] = spinlab
172183
173- #perStamp = 'Period (T): ' + str(round(per,1)) + ' s'
174- #perLoc = (25, height-75)
175- #cv2.putText(img, perStamp, perLoc, font, 1, (255, 255, 255), 1)
176- #timestamp = 'Time: ' + str(round(((i/fps)/per),1)) + ' T'
177184 timestamp = 'Time: ' + str (round ((i / fps ),1 )) + ' s'
178185 tLoc = (width - 225 , height - 25 )
179186 cv2 .putText (img , timestamp , tLoc , font , 1 , (255 , 255 , 255 ), 1 )
@@ -198,9 +205,10 @@ def annotateImg(img, i):
198205 cv2 .putText (img , drpm , dLoc , font , 1 , (255 , 255 , 255 ), 1 )
199206 cv2 .putText (img , crpm , cLoc , font , 1 , (255 , 255 , 255 ), 1 )
200207
208+ # Displays instructions on the screen for identifying the circle/polygon of interest
201209def instructsCenter (img ):
202210 font = cv2 .FONT_HERSHEY_PLAIN
203- line1 = 'Click on 3 or more points along the border of the circle'
211+ line1 = 'Click on 3 or more points along the border of the circle or polygon '
204212 line1Loc = (25 , 50 )
205213 line2 = 'around which the movie will be rotated.'
206214 line2Loc = (25 , 75 )
@@ -214,6 +222,7 @@ def instructsCenter(img):
214222 cv2 .putText (img , line3 , line3Loc , font , 1 , (255 , 255 , 255 ), 1 )
215223 cv2 .putText (img , line4 , line4Loc , font , 1 , (255 , 255 , 255 ), 1 )
216224
225+ # Displays instructions for drawing a circle around the ball.
217226def instructsBall (img ):
218227 font = cv2 .FONT_HERSHEY_PLAIN
219228 line1 = 'Click and drag to create a circle around the ball.'
@@ -229,7 +238,28 @@ def instructsBall(img):
229238 cv2 .putText (img , line2 , line2Loc , font , 1 , (255 , 255 , 255 ), 1 )
230239 cv2 .putText (img , line3 , line3Loc , font , 1 , (255 , 255 , 255 ), 1 )
231240 cv2 .putText (img , line4 , line4Loc , font , 1 , (255 , 255 , 255 ), 1 )
241+
242+ ## Helper Functions: Section 2 -- Mathematical Utility Functions
243+
244+ # 2nd-order central difference method for calculating the derivative of unevenly spaced data
245+ def calcDeriv (f , t ):
246+ df = np .empty (len (f ))
247+ df [0 ] = (f [1 ] - f [0 ]) / (t [1 ] - t [0 ])
248+ df [len (f )- 1 ] = (f [len (f )- 1 ] - f [len (f )- 2 ]) / (t [len (f )- 1 ] - t [len (f )- 2 ])
249+ df [1 :len (f )- 1 ] = f [0 :len (f )- 2 ]* ((t [1 :len (f )- 1 ] - t [2 :len (f )]) / ((t [0 :len (f )- 2 ] - t [1 :len (f )- 1 ])* (t [0 :len (f )- 2 ] - t [2 :len (f )]))) + f [1 :len (f )- 1 ]* (((2 * t [1 :len (f )- 1 ]) - t [0 :len (f )- 2 ] - t [2 :len (f )]) / ((t [1 :len (f )- 1 ] - t [0 :len (f )- 2 ])* (t [1 :len (f )- 1 ] - t [2 :len (f )]))) + f [2 :len (f )]* ((t [1 :len (f )- 1 ] - t [0 :len (f )- 2 ]) / ((t [2 :len (f )] - t [0 :len (f )- 2 ])* (t [2 :len (f )] - t [1 :len (f )- 1 ])))
250+ return df
251+
252+ # Calculates a polynomial fit of degree "deg" though an array of data "y" with corresponding x values "x"
253+ def splineFit (x , y , deg ):
254+ fit = np .polyfit (x , y , deg )
255+ yfit = np .zeros (len (y ))
256+ for i in range (deg + 1 ):
257+ yfit += fit [i ]* (x ** (deg - i ))
258+ return yfit
232259
260+
261+ # The following functions assist in estimating the coefficient of friction of the user's table by fitting their data
262+ # to a damped harmonic oscillator. This functionality is not implemented in the current release of DigiPyRo
233263def errFuncPolar (params , data ):
234264 modelR = np .abs (params [0 ]* np .exp (- data [0 ]* params [3 ]* params [1 ])* np .cos ((params [3 ]* data [0 ]* ((1 - (params [1 ]** 2 ))** (0.5 ))) - params [2 ]))
235265 modelTheta = createModelTheta (data [0 ], params , data [2 ][0 ])
@@ -269,43 +299,26 @@ def createModelTheta(t, bestfit, thetai):
269299
270300 return theta
271301
272- def calcDeriv2 (f , t ):
273- return np .gradient (f ) / np .gradient (t )
274-
275- def calcDeriv (f , t ):
276- df = np .empty (len (f ))
277- df [0 ] = (f [1 ] - f [0 ]) / (t [1 ] - t [0 ])
278- df [len (f )- 1 ] = (f [len (f )- 1 ] - f [len (f )- 2 ]) / (t [len (f )- 1 ] - t [len (f )- 2 ])
279- df [1 :len (f )- 1 ] = f [0 :len (f )- 2 ]* ((t [1 :len (f )- 1 ] - t [2 :len (f )]) / ((t [0 :len (f )- 2 ] - t [1 :len (f )- 1 ])* (t [0 :len (f )- 2 ] - t [2 :len (f )]))) + f [1 :len (f )- 1 ]* (((2 * t [1 :len (f )- 1 ]) - t [0 :len (f )- 2 ] - t [2 :len (f )]) / ((t [1 :len (f )- 1 ] - t [0 :len (f )- 2 ])* (t [1 :len (f )- 1 ] - t [2 :len (f )]))) + f [2 :len (f )]* ((t [1 :len (f )- 1 ] - t [0 :len (f )- 2 ]) / ((t [2 :len (f )] - t [0 :len (f )- 2 ])* (t [2 :len (f )] - t [1 :len (f )- 1 ])))
280- return df
281-
282- def splineFit (x , y , deg ):
283- fit = np .polyfit (x , y , deg )
284- yfit = np .zeros (len (y ))
285- for i in range (deg + 1 ):
286- yfit += fit [i ]* (x ** (deg - i ))
287- return yfit
288-
289302#####################
290303### Main function ###
291304#####################
292305
293306def start ():
294- vid = cv2 .VideoCapture (filenameVar .get ())
307+ vid = cv2 .VideoCapture (filenameVar .get ()) # input video
295308
296- global width , height , numFrames , fps , fourcc , video_writer , spinlab , npts
297- npts = 0
298- spinlab = cv2 .imread ('SpinLabUCLA_BW_strokes.png' )
299- width = int (vid .get (cv2 .cv .CV_CAP_PROP_FRAME_WIDTH ))
300- height = int (vid .get (cv2 .cv .CV_CAP_PROP_FRAME_HEIGHT ))
309+ global width , height , numFrames , fps , fourcc , video_writer , spinlab , npts # declare these variables as global so they can be used by helper functions without being explicitly passed as arguments
310+ npts = 0 # number of user-clicked points along circumference of circle/polygon
311+ spinlab = cv2 .imread ('SpinLabUCLA_BW_strokes.png' ) # spinlab logo to display in upper right corner of output video
312+ width = int (vid .get (cv2 .cv .CV_CAP_PROP_FRAME_WIDTH ))
313+ height = int (vid .get (cv2 .cv .CV_CAP_PROP_FRAME_HEIGHT )) # read the width and height of input video. output video will have matching dimensions
301314 fps = fpsVar .get ()
302315 fileName = savefileVar .get ()
303- fourcc = cv2 .cv .CV_FOURCC ('m' ,'p' ,'4' ,'v' )
304- video_writer = cv2 .VideoWriter (fileName + '.avi' , fourcc , fps , (width , height ))
316+ fourcc = cv2 .cv .CV_FOURCC ('m' ,'p' ,'4' ,'v' ) # codec for output video
317+ video_writer = cv2 .VideoWriter (fileName + '.avi' , fourcc , fps , (width , height )) # VideoWriter object for editing and saving the output video
305318
306- spinlab = cv2 .resize (spinlab ,(int (0.2 * width ),int ((0.2 * height )/ 3 )), interpolation = cv2 .INTER_CUBIC )
319+ spinlab = cv2 .resize (spinlab ,(int (0.2 * width ),int ((0.2 * height )/ 3 )), interpolation = cv2 .INTER_CUBIC ) # resize spinlab logo based on input video dimensions
307320
308- global naturalRPM , physicalRPM , digiRPM , camRPM , dtheta , per , custMask
321+ global naturalRPM , physicalRPM , digiRPM , camRPM , dtheta , per , custMask # declare these variables as global so they can be used by helper functions without being explicitly passed as arguments
309322 naturalRPM = tableRPMVar .get ()
310323 naturalOmega = (naturalRPM * 2 * np .pi )/ 60
311324 physicalRPM = physRPMVar .get ()
@@ -543,6 +556,10 @@ def start():
543556
544557 video_writer .release ()
545558
559+ #######################
560+ ### Create GUI menu ###
561+ #######################
562+
546563root = Tk ()
547564root .title ('DigiPyRo' )
548565startButton = Button (root , text = "Start!" , command = start )
0 commit comments