diff --git a/docs/docs.md b/docs/docs.md index bfeb98a..7a89f0f 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -179,7 +179,7 @@ dvr-scan -i video.mp4 --start-time 00:35:52 --duration 00:05:00 ``` - *
-fs num_frames, --frame-skip num_frames
Number of frames to skip after processing a given frame. Improves performance, at expense of frame and time accuracy, and may increase probability of missing motion events. If set, `-l`/`--min-event-length`, `-tb`/`--time-before-event`, and `-tp`/`--time-post-event` will all be scaled relative to the source framerate. Values above 1 or 2 are not recommended. + *
-fs num_frames, --frame-skip num_frames
Number of frames to skip after processing a given frame. Improves performance, at expense of frame and time accuracy, and may increase probability of missing motion events. If set, `-l`/`--min-event-length`, `-Y`/`--max-event-length`, `-tb`/`--time-before-event`, and `-tp`/`--time-post-event` will all be scaled relative to the source framerate. Values above 1 or 2 are not recommended.

When using output modes which re-encode processed frames (`encode`, `opencv`), skipped frames are not included. Set `-m`/`--output-mode` to `ffmpeg` or `copy` to include all frames from the input video when writing motion events to disk.

Although adjusted for frame skipping, bounding box smoothing may be inconsistent when using frame skipping. Set `-bb 0` to disable smoothing. @@ -199,6 +199,13 @@ All time values can be given as a timecode (`HH:MM:SS` or `HH:MM:SS.nnn`), in se ``` --min-event-length 2 ``` + + + *
-l time, --max-event-length time
Maximum amount of time/frames allowed into a single event. + +``` +--max-event-length 1000 +``` *
-tb time, --time-before-event time
Maximum amount of time to include before each event. @@ -515,6 +522,14 @@ All time values can be given as a timecode: (`HH:MM:SS` or `HH:MM:SS.nnn`), in s ``` + *
max-event-length
+ Maximum mount of time of a single event. + + ``` + max-event-length = 1000 + ``` + + *
time-before-event
Amount of time to include before a motion event. diff --git a/dvr-scan.cfg b/dvr-scan.cfg index 57e0866..a5adabb 100644 --- a/dvr-scan.cfg +++ b/dvr-scan.cfg @@ -101,6 +101,12 @@ # Amount of time which must have motion in each frame to trigger an event. #min-event-length = 2 +# Maximum amount of time of a single event. +#max-event-length = 1000 + +# Maximum amount of time of an event +#max-event-length = 999999 + # Amount of time to include before an event. #time-before-event = 1.5s diff --git a/dvr_scan/cli.py b/dvr_scan/cli.py index d2c7fb9..7e2b386 100644 --- a/dvr_scan/cli.py +++ b/dvr_scan/cli.py @@ -310,6 +310,18 @@ def get_cli_parser(user_config: ConfigRegistry): ), ) + parser.add_argument( + "-Y", + "--max-event-length", + metavar="time", + type=timecode_type_check("time"), + help=( + "Length of time that must contain motion before triggering a new event. Can be" + " specified as frames (123), seconds (12.3s), or timecode (00:00:01).%s" + % user_config.get_help_string("max-event-length") + ), + ) + parser.add_argument( "-tb", "--time-before-event", diff --git a/dvr_scan/config.py b/dvr_scan/config.py index c45c5d1..b0ff3b5 100644 --- a/dvr_scan/config.py +++ b/dvr_scan/config.py @@ -380,6 +380,7 @@ def from_config(config_value: str, default: "RGBValue") -> "RGBValue": "max-events": 0, "merge-window": MergeWindowValue(), "min-event-length": TimecodeValue("0.1s"), + "max-event-length": TimecodeValue("999999s"), "time-before-event": TimecodeValue("1.5s"), "time-post-event": TimecodeValue("2.0s"), "use-pts": False, diff --git a/dvr_scan/scanner.py b/dvr_scan/scanner.py index ee0899c..a95d06d 100644 --- a/dvr_scan/scanner.py +++ b/dvr_scan/scanner.py @@ -190,6 +190,7 @@ def __init__( # Motion Event Parameters (set_event_params) self._min_event_len = None # -l/--min-event-length + self._max_event_len = None # -Y/--max-event-length self._time_before_event = None # -tb/--time-before-event self._time_post_event = None # -tp/--time-post-event self._merge_window = None # --merge-window (None means auto: use -tp) @@ -409,6 +410,7 @@ def set_regions( def set_event_params( self, min_event_len: ty.Union[int, float, str] = "0.1s", + max_event_len: ty.Union[int, float, str] = "999999s", time_pre_event: ty.Union[int, float, str] = "1.5s", time_post_event: ty.Union[int, float, str] = "2s", merge_window: ty.Optional[ty.Union[int, float, str]] = None, @@ -419,6 +421,7 @@ def set_event_params( A `merge_window` of None or "auto" uses the value of `time_post_event`.""" assert self._input.framerate is not None self._min_event_len = FrameTimecode(min_event_len, self._input.framerate) + self._max_event_len = FrameTimecode(max_event_len, self._input.framerate) self._time_before_event = FrameTimecode(time_pre_event, self._input.framerate) self._post_event_len = FrameTimecode(time_post_event, self._input.framerate) if merge_window is None or ( @@ -667,9 +670,11 @@ def scan(self) -> ty.Optional[DetectionResult]: # from the average framerate, but emitted event boundaries are always exact (PTS-based). pre_event_len: int = self._time_before_event.frame_num // (self._frame_skip + 1) min_event_len: int = max(self._min_event_len.frame_num // (self._frame_skip + 1), 1) + max_event_len: int = max(self._max_event_len.frame_num // (self._frame_skip + 1), 1) # Calculations below rely on min_event_len always being >= 1 (cannot be zero) assert min_event_len >= 1, "min_event_len must be at least 1 frame" + assert max_event_len >= 1, "max_event_len must be at least 1 frame" # Presentation duration of one source frame at the average framerate. frame_duration: float = 1.0 / float(self._input.framerate) @@ -804,6 +809,11 @@ def scan(self) -> ty.Optional[DetectionResult]: or height_fraction > self._max_height ): frame_score = 0 + max_event_len_reached = False + if in_motion_event and event_start is not None: + if (frame.timecode.frame_num - event_start.frame_num) > max_event_len: + max_event_len_reached = True + frame_score = 0 above_threshold = frame_score >= self._threshold event_window.append(frame_score) @@ -867,7 +877,10 @@ def scan(self) -> ty.Optional[DetectionResult]: post_motion_frames = [] # Otherwise, we wait until the merge window has passed before ending # this motion event and start looking for a new one. - elif (frame.timecode.seconds - last_motion_time.seconds) >= merge_window_duration: + # If the event is longer than max_event_len, we end it immediately. + elif ( + frame.timecode.seconds - last_motion_time.seconds + ) >= merge_window_duration or max_event_len_reached: in_motion_event = False logger.debug("event %d high score %f" % (1 + len(event_list), self._highscore)) diff --git a/dvr_scan/shared/__init__.py b/dvr_scan/shared/__init__.py index 685234b..5111c62 100644 --- a/dvr_scan/shared/__init__.py +++ b/dvr_scan/shared/__init__.py @@ -226,6 +226,7 @@ def init_scanner( scanner.set_event_params( min_event_len=settings.get("min-event-length"), + max_event_len=settings.get("max-event-length"), time_pre_event=settings.get("time-before-event"), time_post_event=settings.get("time-post-event"), merge_window=settings.get("merge-window"),