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"),