Skip to content

Commit c4378b9

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

File tree

2 files changed

+103
-64
lines changed

2 files changed

+103
-64
lines changed

dvr_scan/scanner.py

Lines changed: 85 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,12 @@ 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+
# TODO(v1.7): We really need three variables here:
267+
# - how much time after an event ends should we wait for more motion <-- window
268+
# - how much time before/after an event should we include in the video
269+
270+
self._time_before_event = None # -tb/--time-before-event
271+
self._time_post_event = None # -tp/--time-post-event
268272
self._use_pts = None # --use_pts
269273

270274
# Region Parameters (set_region)
@@ -471,8 +475,8 @@ def set_event_params(
471475
"""Set motion event parameters."""
472476
assert self._input.framerate is not None
473477
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)
478+
self._time_before_event = FrameTimecode(time_pre_event, self._input.framerate)
479+
self._time_post_event = FrameTimecode(time_post_event, self._input.framerate)
476480
self._use_pts = use_pts
477481

478482
def set_thumbnail_params(self, thumbnails: str = None):
@@ -693,8 +697,8 @@ def scan(self) -> Optional[DetectionResult]:
693697
)
694698

695699
# 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)
700+
post_event_len: int = self._time_post_event.frame_num // (self._frame_skip + 1)
701+
pre_event_len: int = self._time_before_event.frame_num // (self._frame_skip + 1)
698702
min_event_len: int = max(self._min_event_len.frame_num // (self._frame_skip + 1), 1)
699703

700704
# Calculations below rely on min_event_len always being >= 1 (cannot be zero)
@@ -705,21 +709,19 @@ def scan(self) -> Optional[DetectionResult]:
705709
# need to compensate for rounding errors when we corrected it for frame skip. This is
706710
# important as this affects the number of frames we consider for the actual motion event.
707711
if not self._use_pts:
708-
start_event_shift: int = self._pre_event_len.frame_num + min_event_len * (
712+
start_event_shift: int = self._time_before_event.frame_num + min_event_len * (
709713
self._frame_skip + 1
710714
)
711715
else:
712716
start_event_shift_ms: float = (
713-
self._pre_event_len.get_seconds() + self._min_event_len.get_seconds()
717+
self._time_before_event.get_seconds() + self._min_event_len.get_seconds()
714718
) * 1000
715719

716720
# Length of buffer we require in memory to keep track of all frames required for -l and -tb.
717721
buff_len = pre_event_len + min_event_len
718722
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
723+
last_frame_above_threshold = 0
724+
last_frame_above_threshold_ms = 0
723725

724726
if self._bounding_box:
725727
self._bounding_box.set_corrections(
@@ -825,6 +827,11 @@ def scan(self) -> Optional[DetectionResult]:
825827
)
826828
)
827829

830+
#
831+
# TODO: Make a small state diagram and create a new state enum to handle different
832+
# merging modes, etc.
833+
#
834+
828835
# Last frame was part of a motion event, or still within the post-event window.
829836
if in_motion_event:
830837
# If this frame still has motion, reset the post-event window.
@@ -839,67 +846,39 @@ def scan(self) -> Optional[DetectionResult]:
839846
#
840847
# TODO(#72): We should wait until the max of *both* the pre-event and post-
841848
# event windows have passed. Right now we just consider the post-event window.
849+
# We should also allow configuring overlap behavior:
850+
# - normal: If any new motion is found within max(time_pre_event, time_post_event),
851+
# it will be merged with the preceeding event.
852+
# - extended: Events that have a gap of size (time_pre_event + time_post_event)
853+
# between each other will be merged.
842854
else:
843855
num_frames_post_event += 1
844-
if num_frames_post_event >= post_event_len:
856+
if num_frames_post_event >= (pre_event_len + post_event_len):
845857
in_motion_event = False
846-
847-
logger.debug(
848-
"event %d high score %f" % (1 + self._num_events, self._highscore)
858+
# TODO: We can't throw these frames away, they might be needed for the
859+
# next event to satisfy it's own pre_event_len.
860+
buffered_frames = []
861+
event = self._on_event_end(
862+
last_frame_above_threshold,
863+
last_frame_above_threshold_ms,
864+
event_start,
849865
)
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))
866+
event_list.append(event)
890867
if self._output_mode != OutputMode.SCAN_ONLY:
891868
encode_queue.put(MotionEvent(start=event_start, end=event_end))
892-
893869
# Send frame to encode thread.
894870
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-
)
871+
encode_frame = EncodeFrameEvent(
872+
frame_bgr=frame.frame_bgr,
873+
timecode=frame.timecode,
874+
bounding_box=bounding_box,
875+
score=frame_score,
902876
)
877+
if num_frames_post_event < post_event_len:
878+
encode_queue.put(encode_frame)
879+
else:
880+
buffered_frames.append(encode_frame)
881+
903882
# Not already in a motion event, look for a new one.
904883
else:
905884
# Buffer the required amount of frames and overlay data until we find an event.
@@ -1017,6 +996,48 @@ def scan(self) -> Optional[DetectionResult]:
1017996

1018997
return DetectionResult(event_list, frames_processed)
1019998

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