Skip to content

Commit

Permalink
Add basic thumbnail generation based on highest scoring frame (#169)
Browse files Browse the repository at this point in the history
* Add basic thumbnail generation based on highest scoring frame in an event.

This is a very basic cut of this functionality and doesn't comprehend certain
options like comp_file.  But it can still be quite useful as is.

fixes #159

* Default thumbnail option to None (disabled)

* Thumbnail code formatting cleanups

---------

Co-authored-by: goatzilla <[email protected]>
  • Loading branch information
goatzillax and goatzillax authored Jul 15, 2024
1 parent 30cfeff commit 386208c
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 0 deletions.
8 changes: 8 additions & 0 deletions dvr_scan/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,14 @@ def get_cli_parser(user_config: ConfigRegistry):
' output will be appended to the existing contents.'),
)

parser.add_argument(
'--thumbnails',
metavar='method',
type=str,
default=None,
help=('Produce event thumbnail(s).'),
)

parser.add_argument(
'-v',
'--verbosity',
Expand Down
2 changes: 2 additions & 0 deletions dvr_scan/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ def from_config(config_value: str, default: 'RGBValue') -> 'RGBValue':
'bounding-box-color': RGBValue(0xFF0000),
'bounding-box-thickness': 0.0032,
'bounding-box-min-size': 0.032,
'thumbnails': None,
}
"""Mapping of valid configuration file parameters and their default values or placeholders.
The types of these values are used when decoding the configuration file. Valid choices for
Expand All @@ -358,6 +359,7 @@ def from_config(config_value: str, default: 'RGBValue') -> 'RGBValue':
'output-mode': ['scan_only', 'opencv', 'copy', 'ffmpeg'],
'verbosity': ['debug', 'info', 'warning', 'error'],
'bg-subtractor': ['MOG2', 'CNT', 'MOG2_CUDA'],
'thumbnails': ['highscore'],
}
"""Mapping of string options which can only be of a particular set of values. We use a list instead
of a set to preserve order when generating error contexts. Values are case-insensitive, and must be
Expand Down
2 changes: 2 additions & 0 deletions dvr_scan/cli/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ def run_dvr_scan(settings: ProgramSettings) -> ty.List[ty.Tuple[FrameTimecode, F
time_post_event=settings.get('time-post-event'),
)

scanner.set_thumbnail_params(thumbnails=settings.get('thumbnails'),)

scanner.set_video_time(
start_time=settings.get_arg('start-time'),
end_time=settings.get_arg('end-time'),
Expand Down
48 changes: 48 additions & 0 deletions dvr_scan/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,15 @@ def __init__(self,
self._mask_writer: Optional[cv2.VideoWriter] = None
self._num_events: int = 0

# Thumbnail production (set_thumbnail_params)
self._thumbnails = None
self._highscore = 0
self._highframe = None

# Make sure we initialize defaults now that we loaded the input videos.
self.set_detection_params()
self.set_event_params()
self.set_thumbnail_params()
self.set_video_time()

@property
Expand Down Expand Up @@ -433,6 +439,9 @@ def set_event_params(self,
self._pre_event_len = FrameTimecode(time_pre_event, self._input.framerate)
self._post_event_len = FrameTimecode(time_post_event, self._input.framerate)

def set_thumbnail_params(self, thumbnails: str = None):
self._thumbnails = thumbnails

def set_video_time(self,
start_time: Optional[Union[int, float, str]] = None,
end_time: Optional[Union[int, float, str]] = None,
Expand Down Expand Up @@ -665,6 +674,11 @@ def scan(self) -> Optional[DetectionResult]:
if frame_score >= self._max_threshold:
frame_score = 0
above_threshold = frame_score >= self._threshold

if above_threshold and frame_score > self._highscore:
self._highscore = frame_score
self._highframe = frame.frame_bgr

event_window.append(frame_score)
# The first frame fed to the detector can sometimes produce unreliable results due
# to it not having any previous information to compare against.
Expand Down Expand Up @@ -706,6 +720,24 @@ def scan(self) -> Optional[DetectionResult]:
num_frames_post_event += 1
if num_frames_post_event >= post_event_len:
in_motion_event = False

logger.debug("event %d high score %f" %
(1 + self._num_events, self._highscore))
if self._thumbnails == "highscore":
video_name = get_filename(
path=self._input.paths[0], include_extension=False)
output_path = (
self._comp_file if self._comp_file else OUTPUT_FILE_TEMPLATE.format(
VIDEO_NAME=video_name,
EVENT_NUMBER='%04d' % (1 + self._num_events),
EXTENSION='jpg',
))
if self._output_dir:
output_path = os.path.join(self._output_dir, output_path)
cv2.imwrite(output_path, self._highframe)
self._highscore = 0
self._highframe = None

# Calculate event end based on the last frame we had with motion plus
# the post event length time. We also need to compensate for the number
# of frames that we skipped that could have had motion.
Expand Down Expand Up @@ -782,6 +814,22 @@ def scan(self) -> Optional[DetectionResult]:
# curr_pos already includes the presentation duration of the frame.
event_end = FrameTimecode(self._input.position.frame_num, self._input.framerate)
event_list.append(MotionEvent(start=event_start, end=event_end))

logger.debug("event %d high score %f" % (1 + self._num_events, self._highscore))
if self._thumbnails == "highscore":
video_name = get_filename(path=self._input.paths[0], include_extension=False)
output_path = (
self._comp_file if self._comp_file else OUTPUT_FILE_TEMPLATE.format(
VIDEO_NAME=video_name,
EVENT_NUMBER='%04d' % (1 + self._num_events),
EXTENSION='jpg',
))
if self._output_dir:
output_path = os.path.join(self._output_dir, output_path)
cv2.imwrite(output_path, self._highframe)
self._highscore = 0
self._highframe = None

if self._output_mode != OutputMode.SCAN_ONLY:
encode_queue.put(MotionEvent(start=event_start, end=event_end))

Expand Down

0 comments on commit 386208c

Please sign in to comment.