Skip to content

Commit 47737b3

Browse files
committed
[scanner] Extend merge window to the sum of pre and post event length #72
1 parent 573f4ec commit 47737b3

File tree

2 files changed

+100
-64
lines changed

2 files changed

+100
-64
lines changed

dvr_scan/scanner.py

Lines changed: 82 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,9 @@ def __init__(
263263

264264
# Motion Event Parameters (set_event_params)
265265
self._min_event_len = None # -l/--min-event-length
266-
self._pre_event_len = None # -tb/--time-before-event
267-
self._post_event_len = None # -tp/--time-post-event
266+
self._min_event_dist = None # --min-event-dist # TODO: Implement this to fix #72.
267+
self._time_before_event = None # -tb/--time-before-event
268+
self._time_post_event = None # -tp/--time-post-event
268269
self._use_pts = None # --use_pts
269270

270271
# Region Parameters (set_region)
@@ -471,8 +472,8 @@ def set_event_params(
471472
"""Set motion event parameters."""
472473
assert self._input.framerate is not None
473474
self._min_event_len = FrameTimecode(min_event_len, self._input.framerate)
474-
self._pre_event_len = FrameTimecode(time_pre_event, self._input.framerate)
475-
self._post_event_len = FrameTimecode(time_post_event, self._input.framerate)
475+
self._time_before_event = FrameTimecode(time_pre_event, self._input.framerate)
476+
self._time_post_event = FrameTimecode(time_post_event, self._input.framerate)
476477
self._use_pts = use_pts
477478

478479
def set_thumbnail_params(self, thumbnails: str = None):
@@ -693,8 +694,8 @@ def scan(self) -> Optional[DetectionResult]:
693694
)
694695

695696
# Correct event length parameters to account frame skip.
696-
post_event_len: int = self._post_event_len.frame_num // (self._frame_skip + 1)
697-
pre_event_len: int = self._pre_event_len.frame_num // (self._frame_skip + 1)
697+
post_event_len: int = self._time_post_event.frame_num // (self._frame_skip + 1)
698+
pre_event_len: int = self._time_before_event.frame_num // (self._frame_skip + 1)
698699
min_event_len: int = max(self._min_event_len.frame_num // (self._frame_skip + 1), 1)
699700

700701
# Calculations below rely on min_event_len always being >= 1 (cannot be zero)
@@ -705,21 +706,19 @@ def scan(self) -> Optional[DetectionResult]:
705706
# need to compensate for rounding errors when we corrected it for frame skip. This is
706707
# important as this affects the number of frames we consider for the actual motion event.
707708
if not self._use_pts:
708-
start_event_shift: int = self._pre_event_len.frame_num + min_event_len * (
709+
start_event_shift: int = self._time_before_event.frame_num + min_event_len * (
709710
self._frame_skip + 1
710711
)
711712
else:
712713
start_event_shift_ms: float = (
713-
self._pre_event_len.get_seconds() + self._min_event_len.get_seconds()
714+
self._time_before_event.get_seconds() + self._min_event_len.get_seconds()
714715
) * 1000
715716

716717
# Length of buffer we require in memory to keep track of all frames required for -l and -tb.
717718
buff_len = pre_event_len + min_event_len
718719
event_end = self._input.position
719-
if not self._use_pts:
720-
last_frame_above_threshold = 0
721-
else:
722-
last_frame_above_threshold_ms = 0
720+
last_frame_above_threshold = 0
721+
last_frame_above_threshold_ms = 0
723722

724723
if self._bounding_box:
725724
self._bounding_box.set_corrections(
@@ -825,6 +824,11 @@ def scan(self) -> Optional[DetectionResult]:
825824
)
826825
)
827826

827+
#
828+
# TODO: Make a small state diagram and create a new state enum to handle different
829+
# merging modes, etc.
830+
#
831+
828832
# Last frame was part of a motion event, or still within the post-event window.
829833
if in_motion_event:
830834
# If this frame still has motion, reset the post-event window.
@@ -839,67 +843,39 @@ def scan(self) -> Optional[DetectionResult]:
839843
#
840844
# TODO(#72): We should wait until the max of *both* the pre-event and post-
841845
# event windows have passed. Right now we just consider the post-event window.
846+
# We should also allow configuring overlap behavior:
847+
# - normal: If any new motion is found within max(time_pre_event, time_post_event),
848+
# it will be merged with the preceeding event.
849+
# - extended: Events that have a gap of size (time_pre_event + time_post_event)
850+
# between each other will be merged.
842851
else:
843852
num_frames_post_event += 1
844-
if num_frames_post_event >= post_event_len:
853+
if num_frames_post_event >= (pre_event_len + post_event_len):
845854
in_motion_event = False
846-
847-
logger.debug(
848-
"event %d high score %f" % (1 + self._num_events, self._highscore)
855+
# TODO: We can't throw these frames away, they might be needed for the
856+
# next event to satisfy it's own pre_event_len.
857+
buffered_frames = []
858+
event = self._on_event_end(
859+
last_frame_above_threshold,
860+
last_frame_above_threshold_ms,
861+
event_start,
849862
)
850-
if self._thumbnails == "highscore":
851-
video_name = get_filename(
852-
path=self._input.paths[0], include_extension=False
853-
)
854-
output_path = (
855-
self._comp_file
856-
if self._comp_file
857-
else OUTPUT_FILE_TEMPLATE.format(
858-
VIDEO_NAME=video_name,
859-
EVENT_NUMBER="%04d" % (1 + self._num_events),
860-
EXTENSION="jpg",
861-
)
862-
)
863-
if self._output_dir:
864-
output_path = os.path.join(self._output_dir, output_path)
865-
cv2.imwrite(output_path, self._highframe)
866-
self._highscore = 0
867-
self._highframe = None
868-
869-
# Calculate event end based on the last frame we had with motion plus
870-
# the post event length time. We also need to compensate for the number
871-
# of frames that we skipped that could have had motion.
872-
# We also add 1 to include the presentation duration of the last frame.
873-
if not self._use_pts:
874-
event_end = FrameTimecode(
875-
1
876-
+ last_frame_above_threshold
877-
+ self._post_event_len.frame_num
878-
+ self._frame_skip,
879-
self._input.framerate,
880-
)
881-
assert event_end.frame_num >= event_start.frame_num
882-
else:
883-
event_end = FrameTimecode(
884-
(last_frame_above_threshold_ms / 1000)
885-
+ self._post_event_len.get_seconds(),
886-
self._input.framerate,
887-
)
888-
assert event_end.get_seconds() >= event_start.get_seconds()
889-
event_list.append(MotionEvent(start=event_start, end=event_end))
863+
event_list.append(event)
890864
if self._output_mode != OutputMode.SCAN_ONLY:
891865
encode_queue.put(MotionEvent(start=event_start, end=event_end))
892-
893866
# Send frame to encode thread.
894867
if in_motion_event and self._output_mode == OutputMode.OPENCV:
895-
encode_queue.put(
896-
EncodeFrameEvent(
897-
frame_bgr=frame.frame_bgr,
898-
timecode=frame.timecode,
899-
bounding_box=bounding_box,
900-
score=frame_score,
901-
)
868+
encode_frame = EncodeFrameEvent(
869+
frame_bgr=frame.frame_bgr,
870+
timecode=frame.timecode,
871+
bounding_box=bounding_box,
872+
score=frame_score,
902873
)
874+
if num_frames_post_event < post_event_len:
875+
encode_queue.put(encode_frame)
876+
else:
877+
buffered_frames.append(encode_frame)
878+
903879
# Not already in a motion event, look for a new one.
904880
else:
905881
# Buffer the required amount of frames and overlay data until we find an event.
@@ -1017,6 +993,48 @@ def scan(self) -> Optional[DetectionResult]:
1017993

1018994
return DetectionResult(event_list, frames_processed)
1019995

996+
def _on_event_end(
997+
self,
998+
last_frame_above_threshold,
999+
last_frame_above_threshold_ms,
1000+
event_start,
1001+
) -> MotionEvent:
1002+
logger.debug("event %d high score %f" % (1 + self._num_events, self._highscore))
1003+
if self._thumbnails == "highscore":
1004+
video_name = get_filename(path=self._input.paths[0], include_extension=False)
1005+
output_path = (
1006+
self._comp_file
1007+
if self._comp_file
1008+
else OUTPUT_FILE_TEMPLATE.format(
1009+
VIDEO_NAME=video_name,
1010+
EVENT_NUMBER="%04d" % (1 + self._num_events),
1011+
EXTENSION="jpg",
1012+
)
1013+
)
1014+
if self._output_dir:
1015+
output_path = os.path.join(self._output_dir, output_path)
1016+
cv2.imwrite(output_path, self._highframe)
1017+
self._highscore = 0
1018+
self._highframe = None
1019+
1020+
# Calculate event end based on the last frame we had with motion plus
1021+
# the post event length time. We also need to compensate for the number
1022+
# of frames that we skipped that could have had motion.
1023+
# We also add 1 to include the presentation duration of the last frame.
1024+
if not self._use_pts:
1025+
event_end = FrameTimecode(
1026+
1 + last_frame_above_threshold + self._time_post_event.frame_num + self._frame_skip,
1027+
self._input.framerate,
1028+
)
1029+
assert event_end.frame_num >= event_start.frame_num
1030+
else:
1031+
event_end = FrameTimecode(
1032+
(last_frame_above_threshold_ms / 1000) + self._time_post_event.get_seconds(),
1033+
self._input.framerate,
1034+
)
1035+
assert event_end.get_seconds() >= event_start.get_seconds()
1036+
return MotionEvent(start=event_start, end=event_end)
1037+
10201038
def _decode_thread(self, decode_queue: queue.Queue):
10211039
try:
10221040
while not self._stop.is_set():

tests/test_scan_context.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,21 @@ def test_start_duration(traffic_camera_video):
237237
event_list = [(event.start.frame_num, event.end.frame_num) for event in event_list]
238238
# The set duration should only cover the middle event.
239239
compare_event_lists(event_list, TRAFFIC_CAMERA_EVENTS[1:2], EVENT_FRAME_TOLERANCE)
240+
241+
242+
TRAFFIC_CAMERA_EVENTS_MERGE_WITHIN_TIME_BEFORE = [
243+
(2, 149),
244+
(306, 576),
245+
]
246+
247+
248+
def test_merge_within_time_before(traffic_camera_video):
249+
"""Test setting time_pre_event."""
250+
scanner = MotionScanner([traffic_camera_video])
251+
scanner.set_regions(regions=[TRAFFIC_CAMERA_ROI])
252+
scanner.set_event_params(min_event_len=4, time_pre_event=52)
253+
event_list = scanner.scan().event_list
254+
event_list = [(event.start.frame_num, event.end.frame_num) for event in event_list]
255+
compare_event_lists(
256+
event_list, TRAFFIC_CAMERA_EVENTS_MERGE_WITHIN_TIME_BEFORE, EVENT_FRAME_TOLERANCE
257+
)

0 commit comments

Comments
 (0)