Skip to content

Commit 42c3ebb

Browse files
committed
VideoReader more effective verbose, reset_delay for pause end
1 parent 51470b7 commit 42c3ebb

File tree

1 file changed

+83
-47
lines changed

1 file changed

+83
-47
lines changed

pcv/vidIO.py

Lines changed: 83 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,9 @@ def __init__(self, id, *args, display='frame', delay=None, quit=ord('q'),
315315

316316
self._api_preference = kwargs.get('apiPreference', None)
317317

318-
def __enter__(self):
318+
def __enter__(self, force=False):
319319
''' Enter a re-entrant context for this camera. '''
320-
if not self.isOpened():
320+
if force or not self.isOpened():
321321
if self._api_preference:
322322
self.open(self._id, self._api_preference)
323323
else:
@@ -353,7 +353,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback):
353353
except cv2.error as e:
354354
print('Failed to destroy window(s)', e)
355355

356-
waitKey(1) # allow the GUI manager to update
356+
waitKey(3) # allow the GUI manager to update
357357

358358
def __iter__(self):
359359
return self
@@ -577,11 +577,10 @@ def _grabber(self):
577577
self._wait_until_needed()
578578
# read the latest frame
579579
read_success, frame = super(ContextualVideoCapture, self).read()
580-
if not read_success:
581-
raise IOError('Failure to read frame from camera.')
582580

583581
# apply any desired pre-processing and store for main thread
584-
self._preprocessed = self._preprocess(frame)
582+
self._preprocessed = self._preprocess(frame) if read_success \
583+
else None
585584
# inform that image is ready for access/main processing
586585
self._inform_image_ready()
587586

@@ -619,10 +618,13 @@ def read(self, image=None):
619618
self._wait_for_camera_image()
620619
preprocessed = self._preprocessed
621620
self._get_latest_image()
622-
self.image = self._process(preprocessed)
623-
if image is not None:
624-
image = self.image
625-
self.status = True
621+
if preprocessed is None:
622+
self.status, self.image = False, None
623+
else:
624+
self.image = self._process(preprocessed)
625+
if image is not None:
626+
image = self.image
627+
self.status = True
626628
return self.status, self.image
627629

628630

@@ -637,7 +639,8 @@ class VideoReader(LockedCamera):
637639
'proportion' : cv2.CAP_PROP_POS_AVI_RATIO,
638640
}
639641

640-
FASTER, SLOWER, REWIND, FORWARD, RESET = (ord(key) for key in 'wsadr')
642+
FASTER, SLOWER, REWIND, FORWARD, RESET, RESTART = \
643+
(ord(key) for key in 'wsadrb')
641644
FORWARD_DIRECTION, REVERSE_DIRECTION = 1, -1
642645
MIN_DELAY = 1 # integer milliseconds
643646

@@ -646,6 +649,8 @@ def __init__(self, filename, *args, start=None, end=None, auto_delay=True,
646649
**kwargs):
647650
''' Initialise a video reader from the given file.
648651
652+
For default key-bindings see 'auto_delay' details.
653+
649654
'filename' is the string path of a video file. Depending on the file
650655
format some features may not be available.
651656
'start' and 'end' denote the respective times of playback, according
@@ -664,12 +669,14 @@ def __init__(self, filename, *args, start=None, end=None, auto_delay=True,
664669
'skip_frames' is True), and 'd' returning to forwards playback.
665670
The 'r' key can be pressed to reset to 1x speed and forwards
666671
direction playback. 'a' and 'd' can be used while paused to step
667-
back and forwards, regardless of skip_frames. These defaults can be
668-
overridden using the 'play_commands' and 'pause_effects' keyword
669-
arguments, supplying a dictionary of key ordinals that sets the
670-
desired behaviour. Note that the defaults are set internally, so to
671-
turn them off the dictionary must be used, with e.g.
672-
play_commands={ord('a'):lambda vid:None} to disable rewinding.
672+
back and forwards, regardless of skip_frames. 'b' can be used while
673+
playing or paused to jump the video back to its starting point.
674+
These defaults can be overridden using the 'play_commands' and
675+
'pause_effects' keyword arguments, supplying a dictionary of key
676+
ordinals that sets the desired behaviour. Note that the defaults
677+
are set internally, so to turn them off the dictionary must be
678+
used, with e.g. play_commands={ord('a'):lambda vid:None} to disable
679+
rewinding.
673680
'fps' is a float specifying the desired frames per second for playback.
674681
If left as None the fps is read from file, or if that fails is set
675682
to 25 by default. Value is ignored if 'auto_delay' is False.
@@ -684,8 +691,9 @@ def __init__(self, filename, *args, start=None, end=None, auto_delay=True,
684691
formats with slow video frame setting times, and inconsistent
685692
skipping amounts with 'auto_delay' may cause issues with
686693
time-dependent processing.
687-
'verbose' is a boolean determining if playback speed and direction
688-
changes are printed to the terminal. Defaults to True.
694+
'verbose' is a boolean determining if status updates (e.g. initial fps,
695+
and playback speed and direction changes) are printed. Defaults to
696+
True.
689697
690698
*args and **kwargs get passed up the inheritance chain, with notable
691699
keywords including the 'preprocess' and 'process' functions which
@@ -701,35 +709,36 @@ def __init__(self, filename, *args, start=None, end=None, auto_delay=True,
701709
self.filename = filename
702710
self._fps = fps or self.fps or 25 # user-specified or auto-retrieved
703711
self._period = 1e3 / self._fps
712+
self._verbose = verbose
713+
self.status = True
704714
self._initialise_delay(auto_delay)
705-
self._initialise_playback(start, end, skip_frames, verbose)
715+
self._initialise_playback(start, end, skip_frames)
706716

707717
def _initialise_delay(self, auto_delay):
708718
''' Determines the delay automatically, or leaves as None. '''
709719
if auto_delay:
710720
if self._fps == 0 or self._fps >= 1e3:
711-
print('failed to determine fps, setting to 25')
721+
self.verbos_print('failed to determine fps, setting to 25')
712722
self._period = 1e3 / 25
713723
# set a bit low to allow image read times
714-
self._delay = self._period - 5
724+
self._delay = self._period - 5
715725
else:
716726
self._delay = int(self._period)
717-
print('delay set automatically to',
718-
f'{self._delay}ms from fps={self._fps}')
727+
self.verbose_print('delay set automatically to',
728+
f'{self._delay}ms from fps={self._fps}')
719729
else:
720730
self._delay = None
721731
if self._destroy == -1:
722732
self._destroy = None
723733

724-
def _initialise_playback(self, start, end, skip_frames, verbose):
734+
def _initialise_playback(self, start, end, skip_frames):
725735
''' Set up playback settings as specified. '''
726736
self._wait_for_camera_image() # don't set frame while grabber running
727737

728738
self._set_start(start)
729739
self._set_end(end)
730740

731741
self._skip_frames = skip_frames
732-
self._verbose = verbose
733742
self._direction = self.FORWARD_DIRECTION
734743
self._speed = 1
735744
self._adjusted_period = self._period
@@ -741,20 +750,22 @@ def _initialise_playback(self, start, end, skip_frames, verbose):
741750
self.REWIND : self._go_back,
742751
self.FORWARD : self._go_forward,
743752
self.RESET : self._reset,
753+
self.RESTART : self.restart,
744754
**self._play_commands
745755
}
746756

747757
# add step back and forward functionality if keys not already used
748758
self._pause_effects = {
749759
self.REWIND : self.step_back,
750760
self.FORWARD : self.step_forward,
761+
self.RESTART : self.restart,
751762
**self._pause_effects
752763
}
753764

754765
# ensure time between frames is ignored while paused
755766
class LogDict(dict):
756767
def get(this, *args, **kwargs):
757-
self._prev = perf_counter() - (self._period - self.MIN_DELAY) / 1e3
768+
self.reset_delay()
758769
return dict.get(this, *args, **kwargs)
759770

760771
self._pause_effects = LogDict(self._pause_effects)
@@ -763,14 +774,14 @@ def get(this, *args, **kwargs):
763774

764775
def _set_start(self, start):
765776
''' Set the start of the video to user specification, if possible. '''
777+
self._frame = 0
766778
if start is not None:
767779
if self.set_timestamp(start):
768-
print(f'starting at {start}')
780+
self.verbose_print(f'starting at {start}')
769781
else:
770-
print('start specification failed, starting at 0:00')
771-
self._frame = 0
772-
else:
773-
self._frame = 0
782+
self.verbose_print('start specification failed, '
783+
'starting at 0:00')
784+
self._start = self._frame
774785

775786
def _set_end(self, end):
776787
''' Set playback to end where specified by user. '''
@@ -781,7 +792,15 @@ def _set_end(self, end):
781792
self._end = end
782793
self._end /= self._period # convert to number of frames
783794
else:
784-
self._end = np.inf
795+
self._end = self.get('num_frames') or np.inf
796+
797+
def verbose_print(self, *args, **kwargs):
798+
if self._verbose:
799+
print(*args, **kwargs)
800+
801+
# NOTE: key callbacks set as static methods for clarity/ease of reference
802+
# VideoReader to be modified gets passed in (so that external functions
803+
# can be used), so also having a reference to self would be confusing.
785804

786805
@staticmethod
787806
def _speed_up(vid):
@@ -798,8 +817,7 @@ def _slow_down(vid):
798817
def _register_speed_change(self):
799818
''' Update internals and print new speed. '''
800819
self._calculate_period()
801-
if self._verbose:
802-
print(f'speed set to {self._speed:.1f}x starting fps')
820+
self.verbose_print(f'speed set to {self._speed:.1f}x starting fps')
803821

804822
def _calculate_period(self):
805823
''' Determine the adjusted period given the speed. '''
@@ -817,32 +835,37 @@ def _calculate_frames(self):
817835
else 1)
818836
self._calculate_timestep()
819837

838+
def reset_delay(self):
839+
''' Resets the delay between frames.
840+
841+
Use to avoid fast playback/frame skipping after pauses.
842+
843+
'''
844+
self._prev = perf_counter() - (self._period - self.MIN_DELAY) / 1e3
845+
820846
@staticmethod
821847
def _go_back(vid):
822848
''' Set playback to backwards. '''
823849
if vid._skip_frames is not None:
824850
vid._direction = vid.REVERSE_DIRECTION
825-
if vid._verbose:
826-
print('Rewinding')
851+
vid.verbose_print('Rewinding')
827852
else:
828-
if vid._verbose:
829-
print('Cannot go backwards without skip_frames=True')
853+
vid.verbose_print('Cannot go backwards without skip_frames=True')
830854

831855
@staticmethod
832856
def _go_forward(vid):
833857
''' Set playback to go forwards. '''
834858
vid._direction = vid.FORWARD_DIRECTION
835-
if vid._verbose:
836-
print('Going forwards')
859+
vid.verbose_print('Going forwards')
837860

838861
@staticmethod
839862
def _reset(vid):
840863
''' Restore playback to 1x speed and forwards. '''
841864
vid._speed = 1
842865
vid._direction = vid.FORWARD_DIRECTION
843866
vid._calculate_period()
844-
if vid._verbose:
845-
print(f'Going forwards with speed set to 1x starting fps')
867+
vid.verbose_print('Going forwards with speed set to 1x starting fps '
868+
f'({vid._fps:.2f})')
846869

847870
@staticmethod
848871
def step_back(vid):
@@ -878,6 +901,15 @@ def step_forward(vid):
878901
# restore state
879902
vid._direction, vid._verbose = old_state
880903

904+
@staticmethod
905+
def restart(vid):
906+
''' Attempts to continue playback from the start of the video.
907+
908+
Respects user-defined start-point from initialisation.
909+
910+
'''
911+
vid.set_frame(vid._start)
912+
881913
@property
882914
def fps(self):
883915
''' The constant FPS assumed of the video file. '''
@@ -892,7 +924,7 @@ def frame(self):
892924
def set_frame(self, frame):
893925
''' Attempts to set the frame number, returns success.
894926
895-
'frame' is an integer greater than 0. Setting past the last frame
927+
'frame' is an integer >= 0. Setting past the last frame
896928
either has no effect or ends the playback.
897929
898930
self.set_frame(int) -> bool
@@ -964,9 +996,12 @@ def __next__(self):
964996
self._update_playback_settings()
965997

966998
self._prev = now
967-
self._update_frame_tracking()
968999

969-
return super().__next__()
1000+
self._update_frame_tracking()
1001+
read_success, frame = super().__next__()
1002+
if not read_success:
1003+
raise OutOfFrames
1004+
return read_success, frame
9701005

9711006
def _update_playback_settings(self):
9721007
''' Adjusts delay/frame skipping if error is sufficiently large. '''
@@ -1008,7 +1043,8 @@ def _update_frame_tracking(self):
10081043
else:
10091044
self._frame += 1
10101045

1011-
if self._frame > self._end:
1046+
if self.status == False or self._frame > self._end \
1047+
or self._frame < self._start:
10121048
raise OutOfFrames
10131049

10141050
def __repr__(self):

0 commit comments

Comments
 (0)