123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- """``scenedetect.frame_timecode`` Module
- This module implements :class:`FrameTimecode` which is used as a way for PySceneDetect to store
- frame-accurate timestamps of each cut. This is done by also specifying the video framerate with the
- timecode, allowing a frame number to be converted to/from a floating-point number of seconds, or
- string in the form `"HH:MM:SS[.nnn]"` where the `[.nnn]` part is optional.
- See the following examples, or the :class:`FrameTimecode constructor <FrameTimecode>`.
- ===============================================================
- Usage Examples
- ===============================================================
- A :class:`FrameTimecode` can be created by specifying a timecode (`int` for number of frames,
- `float` for number of seconds, or `str` in the form "HH:MM:SS" or "HH:MM:SS.nnn") with a framerate:
- .. code:: python
- frames = FrameTimecode(timecode = 29, fps = 29.97)
- seconds_float = FrameTimecode(timecode = 10.0, fps = 10.0)
- timecode_str = FrameTimecode(timecode = "00:00:10.000", fps = 10.0)
- Arithmetic/comparison operations with :class:`FrameTimecode` objects is also possible, and the
- other operand can also be of the above types:
- .. code:: python
- x = FrameTimecode(timecode = "00:01:00.000", fps = 10.0)
- # Can add int (frames), float (seconds), or str (timecode).
- print(x + 10)
- print(x + 10.0)
- print(x + "00:10:00")
- # Same for all comparison operators.
- print((x + 10.0) == "00:01:10.000")
- :class:`FrameTimecode` objects can be added and subtracted, however the current implementation
- disallows negative values, and will clamp negative results to 0.
- .. warning::
- Be careful when subtracting :class:`FrameTimecode` objects or adding negative
- amounts of frames/seconds. In the example below, ``c`` will be at frame 0 since
- ``b > a``, but ``d`` will be at frame 5:
- .. code:: python
- a = FrameTimecode(5, 10.0)
- b = FrameTimecode(10, 10.0)
- c = a - b # b > a, so c == 0
- d = b - a
- assert(c == 0)
- assert(d == 5)
- """
- import math
- from typing import Union
- MAX_FPS_DELTA: float = 1.0 / 100000
- """Maximum amount two framerates can differ by for equality testing."""
- _SECONDS_PER_MINUTE = 60.0
- _SECONDS_PER_HOUR = 60.0 * _SECONDS_PER_MINUTE
- _MINUTES_PER_HOUR = 60.0
- class FrameTimecode:
- """Object for frame-based timecodes, using the video framerate to compute back and
- forth between frame number and seconds/timecode.
- A timecode is valid only if it complies with one of the following three types/formats:
- 1. Timecode as `str` in the form "HH:MM:SS[.nnn]" (`"01:23:45"` or `"01:23:45.678"`)
- 2. Number of seconds as `float`, or `str` in form "SSSS.nnnn" (`"45.678"`)
- 3. Exact number of frames as `int`, or `str` in form NNNNN (`456` or `"456"`)
- """
- def __init__(self,
- timecode: Union[int, float, str, 'FrameTimecode'] = None,
- fps: Union[int, float, str, 'FrameTimecode'] = None):
- """
- Arguments:
- timecode: A frame number (int), number of seconds (float), or timecode (str in
- the form `'HH:MM:SS'` or `'HH:MM:SS.nnn'`).
- fps: The framerate or FrameTimecode to use as a time base for all arithmetic.
- Raises:
- TypeError: Thrown if either `timecode` or `fps` are unsupported types.
- ValueError: Thrown when specifying a negative timecode or framerate.
- """
-
-
-
-
- self.framerate = None
- self.frame_num = None
-
- if isinstance(timecode, FrameTimecode):
- self.framerate = timecode.framerate
- self.frame_num = timecode.frame_num
- if fps is not None:
- raise TypeError('Framerate cannot be overwritten when copying a FrameTimecode.')
- else:
-
- if fps is None:
- raise TypeError('Framerate (fps) is a required argument.')
- if isinstance(fps, FrameTimecode):
- fps = fps.framerate
-
- if not isinstance(fps, (int, float)):
- raise TypeError('Framerate must be of type int/float.')
- if (isinstance(fps, int) and not fps > 0) or (isinstance(fps, float)
- and not fps >= MAX_FPS_DELTA):
- raise ValueError('Framerate must be positive and greater than zero.')
- self.framerate = float(fps)
-
- if isinstance(timecode, str):
- self.frame_num = self._parse_timecode_string(timecode)
- else:
- self.frame_num = self._parse_timecode_number(timecode)
-
- def get_frames(self) -> int:
- """Get the current time/position in number of frames. This is the
- equivalent of accessing the self.frame_num property (which, along
- with the specified framerate, forms the base for all of the other
- time measurement calculations, e.g. the :meth:`get_seconds` method).
- If using to compare a :class:`FrameTimecode` with a frame number,
- you can do so directly against the object (e.g. ``FrameTimecode(10, 10.0) <= 10``).
- Returns:
- int: The current time in frames (the current frame number).
- """
- return self.frame_num
-
- def get_framerate(self) -> float:
- """Get Framerate: Returns the framerate used by the FrameTimecode object.
- Returns:
- float: Framerate of the current FrameTimecode object, in frames per second.
- """
- return self.framerate
- def equal_framerate(self, fps) -> bool:
- """Equal Framerate: Determines if the passed framerate is equal to that of this object.
- Arguments:
- fps: Framerate to compare against within the precision constant defined in this module
- (see :data:`MAX_FPS_DELTA`).
- Returns:
- bool: True if passed fps matches the FrameTimecode object's framerate, False otherwise.
- """
- return math.fabs(self.framerate - fps) < MAX_FPS_DELTA
-
- def get_seconds(self) -> float:
- """Get the frame's position in number of seconds.
- If using to compare a :class:`FrameTimecode` with a frame number,
- you can do so directly against the object (e.g. ``FrameTimecode(10, 10.0) <= 1.0``).
- Returns:
- float: The current time/position in seconds.
- """
- return float(self.frame_num) / self.framerate
-
- def get_timecode(self, precision: int = 3, use_rounding: bool = True) -> str:
- """Get a formatted timecode string of the form HH:MM:SS[.nnn].
- Args:
- precision: The number of decimal places to include in the output ``[.nnn]``.
- use_rounding: Rounds the output to the desired precision. If False, the value
- will be truncated to the specified precision.
- Returns:
- str: The current time in the form ``"HH:MM:SS[.nnn]"``.
- """
-
- secs = self.get_seconds()
- hrs = int(secs / _SECONDS_PER_HOUR)
- secs -= (hrs * _SECONDS_PER_HOUR)
- mins = int(secs / _SECONDS_PER_MINUTE)
- secs = max(0.0, secs - (mins * _SECONDS_PER_MINUTE))
- if use_rounding:
- secs = round(secs, precision)
- secs = min(_SECONDS_PER_MINUTE, secs)
-
- if int(secs) == _SECONDS_PER_MINUTE:
- secs = 0.0
- mins += 1
- if mins >= _MINUTES_PER_HOUR:
- mins = 0
- hrs += 1
-
- msec = format(secs, '.%df' % (precision + 1)) if precision else ''
-
- msec_str = msec[-(2 + precision):-1]
- secs_str = f"{int(secs):02d}{msec_str}"
-
- return '%02d:%02d:%s' % (hrs, mins, secs_str)
-
- def previous_frame(self) -> 'FrameTimecode':
- """Return a new FrameTimecode for the previous frame (or 0 if on frame 0)."""
- new_timecode = FrameTimecode(self)
- new_timecode.frame_num = max(0, new_timecode.frame_num - 1)
- return new_timecode
- def _seconds_to_frames(self, seconds: float) -> int:
- """Convert the passed value seconds to the nearest number of frames using
- the current FrameTimecode object's FPS (self.framerate).
- Returns:
- Integer number of frames the passed number of seconds represents using
- the current FrameTimecode's framerate property.
- """
- return round(seconds * self.framerate)
- def _parse_timecode_number(self, timecode: Union[int, float]) -> int:
- """ Parse a timecode number, storing it as the exact number of frames.
- Can be passed as frame number (int), seconds (float)
- Raises:
- TypeError, ValueError
- """
-
-
- if isinstance(timecode, int):
- if timecode < 0:
- raise ValueError('Timecode frame number must be positive and greater than zero.')
- return timecode
-
- elif isinstance(timecode, float):
- if timecode < 0.0:
- raise ValueError('Timecode value must be positive and greater than zero.')
- return self._seconds_to_frames(timecode)
-
- elif isinstance(timecode, FrameTimecode):
- return timecode.frame_num
- elif timecode is None:
- raise TypeError('Timecode/frame number must be specified!')
- else:
- raise TypeError('Timecode format/type unrecognized.')
- def _parse_timecode_string(self, input: str) -> int:
- """Parses a string based on the three possible forms (in timecode format,
- as an integer number of frames, or floating-point seconds, ending with 's').
- Requires that the `framerate` property is set before calling this method.
- Assuming a framerate of 30.0 FPS, the strings '00:05:00.000', '00:05:00',
- '9000', '300s', and '300.0' are all possible valid values, all representing
- a period of time equal to 5 minutes, 300 seconds, or 9000 frames (at 30 FPS).
- Raises:
- ValueError: Value could not be parsed correctly.
- """
- assert not self.framerate is None
- input = input.strip()
-
- if input.isdigit():
- timecode = int(input)
- if timecode < 0:
- raise ValueError('Timecode frame number must be positive.')
- return timecode
-
- elif input.find(":") >= 0:
- values = input.split(":")
- hrs, mins = int(values[0]), int(values[1])
- secs = float(values[2]) if '.' in values[2] else int(values[2])
- if not (hrs >= 0 and mins >= 0 and secs >= 0 and mins < 60 and secs < 60):
- raise ValueError('Invalid timecode range (values outside allowed range).')
- secs += (hrs * 60 * 60) + (mins * 60)
- return self._seconds_to_frames(secs)
-
- if input.endswith('s'):
- input = input[:-1]
- if not input.replace('.', '').isdigit():
- raise ValueError('All characters in timecode seconds string must be digits.')
- as_float = float(input)
- if as_float < 0.0:
- raise ValueError('Timecode seconds value must be positive.')
- return self._seconds_to_frames(as_float)
- def __iadd__(self, other: Union[int, float, str, 'FrameTimecode']) -> 'FrameTimecode':
- if isinstance(other, int):
- self.frame_num += other
- elif isinstance(other, FrameTimecode):
- if self.equal_framerate(other.framerate):
- self.frame_num += other.frame_num
- else:
- raise ValueError('FrameTimecode instances require equal framerate for addition.')
-
- elif isinstance(other, float):
- self.frame_num += self._seconds_to_frames(other)
- elif isinstance(other, str):
- self.frame_num += self._parse_timecode_string(other)
- else:
- raise TypeError('Unsupported type for performing addition with FrameTimecode.')
- if self.frame_num < 0:
- self.frame_num = 0
- return self
- def __add__(self, other: Union[int, float, str, 'FrameTimecode']) -> 'FrameTimecode':
- to_return = FrameTimecode(timecode=self)
- to_return += other
- return to_return
- def __isub__(self, other: Union[int, float, str, 'FrameTimecode']) -> 'FrameTimecode':
- if isinstance(other, int):
- self.frame_num -= other
- elif isinstance(other, FrameTimecode):
- if self.equal_framerate(other.framerate):
- self.frame_num -= other.frame_num
- else:
- raise ValueError('FrameTimecode instances require equal framerate for subtraction.')
-
- elif isinstance(other, float):
- self.frame_num -= self._seconds_to_frames(other)
- elif isinstance(other, str):
- self.frame_num -= self._parse_timecode_string(other)
- else:
- raise TypeError('Unsupported type for performing subtraction with FrameTimecode: %s' %
- type(other))
- if self.frame_num < 0:
- self.frame_num = 0
- return self
- def __sub__(self, other: Union[int, float, str, 'FrameTimecode']) -> 'FrameTimecode':
- to_return = FrameTimecode(timecode=self)
- to_return -= other
- return to_return
- def __eq__(self, other: Union[int, float, str, 'FrameTimecode']) -> 'FrameTimecode':
- if isinstance(other, int):
- return self.frame_num == other
- elif isinstance(other, float):
- return self.get_seconds() == other
- elif isinstance(other, str):
- return self.frame_num == self._parse_timecode_string(other)
- elif isinstance(other, FrameTimecode):
- if self.equal_framerate(other.framerate):
- return self.frame_num == other.frame_num
- else:
- raise TypeError(
- 'FrameTimecode objects must have the same framerate to be compared.')
- elif other is None:
- return False
- else:
- raise TypeError('Unsupported type for performing == with FrameTimecode: %s' %
- type(other))
- def __ne__(self, other: Union[int, float, str, 'FrameTimecode']) -> bool:
- return not self == other
- def __lt__(self, other: Union[int, float, str, 'FrameTimecode']) -> bool:
- if isinstance(other, int):
- return self.frame_num < other
- elif isinstance(other, float):
- return self.get_seconds() < other
- elif isinstance(other, str):
- return self.frame_num < self._parse_timecode_string(other)
- elif isinstance(other, FrameTimecode):
- if self.equal_framerate(other.framerate):
- return self.frame_num < other.frame_num
- else:
- raise TypeError(
- 'FrameTimecode objects must have the same framerate to be compared.')
- else:
- raise TypeError('Unsupported type for performing < with FrameTimecode: %s' %
- type(other))
- def __le__(self, other: Union[int, float, str, 'FrameTimecode']) -> bool:
- if isinstance(other, int):
- return self.frame_num <= other
- elif isinstance(other, float):
- return self.get_seconds() <= other
- elif isinstance(other, str):
- return self.frame_num <= self._parse_timecode_string(other)
- elif isinstance(other, FrameTimecode):
- if self.equal_framerate(other.framerate):
- return self.frame_num <= other.frame_num
- else:
- raise TypeError(
- 'FrameTimecode objects must have the same framerate to be compared.')
- else:
- raise TypeError('Unsupported type for performing <= with FrameTimecode: %s' %
- type(other))
- def __gt__(self, other: Union[int, float, str, 'FrameTimecode']) -> bool:
- if isinstance(other, int):
- return self.frame_num > other
- elif isinstance(other, float):
- return self.get_seconds() > other
- elif isinstance(other, str):
- return self.frame_num > self._parse_timecode_string(other)
- elif isinstance(other, FrameTimecode):
- if self.equal_framerate(other.framerate):
- return self.frame_num > other.frame_num
- else:
- raise TypeError(
- 'FrameTimecode objects must have the same framerate to be compared.')
- else:
- raise TypeError('Unsupported type for performing > with FrameTimecode: %s' %
- type(other))
- def __ge__(self, other: Union[int, float, str, 'FrameTimecode']) -> bool:
- if isinstance(other, int):
- return self.frame_num >= other
- elif isinstance(other, float):
- return self.get_seconds() >= other
- elif isinstance(other, str):
- return self.frame_num >= self._parse_timecode_string(other)
- elif isinstance(other, FrameTimecode):
- if self.equal_framerate(other.framerate):
- return self.frame_num >= other.frame_num
- else:
- raise TypeError(
- 'FrameTimecode objects must have the same framerate to be compared.')
- else:
- raise TypeError('Unsupported type for performing >= with FrameTimecode: %s' %
- type(other))
-
-
- def __int__(self) -> int:
- return self.frame_num
- def __float__(self) -> float:
- return self.get_seconds()
- def __str__(self) -> str:
- return self.get_timecode()
- def __repr__(self) -> str:
- return '%s [frame=%d, fps=%.3f]' % (self.get_timecode(), self.frame_num, self.framerate)
- def __hash__(self) -> int:
- return self.frame_num
|