@@ -86,7 +86,7 @@ def _construct_open_args(self):
8686 if self .api_preference is not None :
8787 args = [args [0 ], self .api_preference , * args [1 :]]
8888 return args
89-
89+
9090 def __enter__ (self ):
9191 ''' Re-entrant '''
9292 if not self .isOpened ():
@@ -137,7 +137,7 @@ def suggested_codec(cls, filename, exclude=[]):
137137
138138 @classmethod
139139 def from_camera (cls , filename , camera , fourcc = None , isColor = True ,
140- apiPreference = None , fps = - 3 ):
140+ apiPreference = None , fps = - 3 , ** kwargs ):
141141 ''' Returns a VideoWriter based on the properties of the input camera.
142142
143143 'filename' is the name of the file to save to.
@@ -150,6 +150,7 @@ def from_camera(cls, filename, camera, fourcc=None, isColor=True,
150150 If no processing is occurring, 'camera' is suggested, otherwise
151151 it is generally best to measure the frame output.
152152 Defaults to -3, to measure over 3 frames.
153+ 'kwrags' are any additional keyword arguments for initialisation.
153154
154155 '''
155156 if fourcc is None :
@@ -160,8 +161,9 @@ def from_camera(cls, filename, camera, fourcc=None, isColor=True,
160161 fps = camera .get ('fps' )
161162 elif fps < 0 :
162163 fps = camera .measure_framerate (- fps )
163-
164- return cls (filename , fourcc , fps , frameSize , isColor , apiPreference )
164+
165+ return cls (filename , fourcc , fps , frameSize , isColor , apiPreference ,
166+ ** kwargs )
165167
166168 def __repr__ (self ):
167169 return (f'{ self .__class__ .__name__ } (filename={ repr (self .filename )} , '
@@ -197,7 +199,7 @@ def __init__(self, *args, maxsize=0, verbose_exit=True, **kwargs):
197199 self ._verbose_exit = verbose_exit
198200
199201 def _initialise_writer (self , maxsize ):
200- ''' Start the Thread for grabbing images. '''
202+ ''' Start the Thread for writing images from the queue . '''
201203 self .max_queue_size = maxsize
202204 self ._write_queue = Queue (maxsize = maxsize )
203205 self ._image_writer = Thread (name = 'writer' , target = self ._writer ,
@@ -240,6 +242,57 @@ def __exit__(self, *args):
240242 print (f'Writing complete in { perf_counter ()- waited :.3f} s.' )
241243
242244
245+ class GuaranteedVideoWriter (VideoWriter ):
246+ ''' A VideoWriter with guaranteed output FPS.
247+
248+ Repeats frames when input too slow, and skips frames when input too fast.
249+
250+ '''
251+ def _initialise_writer (self , maxsize ):
252+ ''' Start the write-queue putter and getter threads. '''
253+ super ()._initialise_writer (maxsize )
254+ self ._period = 1 / self .fps
255+ self .latest = None
256+ self ._finished = Event ()
257+ self ._looper = Thread (name = 'looper' , target = self ._write_loop ,
258+ daemon = True )
259+ self ._looper .start ()
260+
261+ def _write_loop (self ):
262+ ''' Write the latest frame to the queue, at self.fps.
263+
264+ Repeats frames when input too slow, and skips frames when input too fast.
265+
266+ '''
267+ # wait until first image set, or early finish
268+ while self .latest is None and not self ._finished .is_set ():
269+ sleep (self ._period / 2 )
270+ prev = perf_counter ()
271+ self ._error = 0
272+ delay = self ._period - 5e-3
273+
274+ # write frames at specified rate until told to stop
275+ while not self ._finished .is_set ():
276+ super ().write (self .latest )
277+ new = perf_counter ()
278+ self ._error += self ._period - (new - prev )
279+ delay -= self ._error
280+ delay = max (delay , 0 ) # can't go back in time
281+ sleep (delay )
282+ prev = new
283+
284+ def write (self , img ):
285+ ''' Set the latest image. '''
286+ self .latest = img
287+
288+ def __exit__ (self , * args ):
289+ self ._finished .set ()
290+ self ._looper .join ()
291+ if self ._verbose_exit :
292+ print (f'Net timing error = { self ._error * 1e3 :.3f} ms' )
293+ super ().__exit__ (* args )
294+
295+
243296class OutOfFrames (StopIteration ):
244297 def __init__ (msg = 'Out of video frames' , * args , ** kwargs ):
245298 super ().__init__ (msg , * args , ** kwargs )
@@ -407,7 +460,8 @@ def headless_stream(self):
407460 for read_success , frame in self :
408461 if not read_success : break # camera disconnected
409462
410- def record_stream (self , filename , show = True , mouse_handler = DoNothing ()):
463+ def record_stream (self , filename , show = True , mouse_handler = DoNothing (),
464+ writer = VideoWriter ):
411465 ''' Capture and record stream, with optional display.
412466
413467 'filename' is the file to save to.
@@ -416,9 +470,12 @@ def record_stream(self, filename, show=True, mouse_handler=DoNothing()):
416470 'mouse_handler' is an optional MouseCallback instance determining
417471 the effects of mouse clicks and moves during the stream. It is only
418472 useful if 'show' is set to True. Defaults to DoNothing.
473+ 'writer' is a subclass of VideoWriter. Defaults to VideoWriter.
474+ Set to GuaranteedVideoWriter to allow repeated and skipped frames
475+ to better ensure a consistent output framerate.
419476
420477 '''
421- with VideoWriter .from_camera (filename , self ) as writer , mouse_handler :
478+ with writer .from_camera (filename , self ) as writer , mouse_handler :
422479 for read_success , frame in self :
423480 if read_success :
424481 if show :
@@ -876,7 +933,7 @@ def step_back(vid):
876933 # enable back-stepping if not currently permitted
877934 vid ._skip_frames = 0
878935 # make sure no unnecessary prints trigger from playback keys
879- vid ._verbose = False
936+ vid ._verbose = False
880937
881938 # go back a step
882939 vid ._direction = vid .REVERSE_DIRECTION
0 commit comments