Skip to content

Commit 9f5f105

Browse files
committed
Added GuaranteedVideoWriter
1 parent 350a003 commit 9f5f105

3 files changed

Lines changed: 72 additions & 11 deletions

File tree

Overview.png

100755100644
-120 KB
Loading

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
_________________________________
2-
Version: 1.1.6
2+
Version: 1.1.7
33
Author: ES Alexander
4-
Release Date: 17/Oct/2020
4+
Release Date: 22/Dec/2020
55
_________________________________
66

77
# About
@@ -19,7 +19,7 @@ possible while ensuring ease of use and helpful errors/documentation.
1919
This library requires an existing version of `OpenCV` with Python bindings to be
2020
installed (e.g. `python3 -m pip install opencv-python`). Some features (mainly
2121
property access helpers) may not work for versions of OpenCV earlier than 4.2.0.
22-
The library was tested using Python 3.7.2, and is expected to work down to at least
22+
The library was tested using Python 3.8.5, and is expected to work down to at least
2323
Python 3.4 (although the integrated advanced features example uses matmul (@) for
2424
some processing, which was introduced in Python 3.5).
2525

@@ -75,6 +75,10 @@ process it can have CPU and power usage similar to that of `SlowCamera`.
7575
If using a video file to simulate a live camera stream, use `SlowCamera` or
7676
`LockedCamera` - `Camera` will skip frames.
7777

78+
There is also a `GuaranteedVideoWriter` class which guarantees the output framerate by
79+
repeating frames when given input too slowly, and skipping frames when input is too
80+
fast.
81+
7882
## Overview
7983
![Overview of classes diagram](https://github.com/ES-Alexander/pythonic-cv/blob/master/Overview.png)
8084

pcv/vidIO.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
243296
class 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

Comments
 (0)