Skip to content

Commit

Permalink
[gui] Add load/save
Browse files Browse the repository at this point in the history
  • Loading branch information
Breakthrough committed Oct 7, 2023
1 parent 50c352d commit d95bd7d
Show file tree
Hide file tree
Showing 6 changed files with 430 additions and 148 deletions.
5 changes: 0 additions & 5 deletions dvr-scan.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,6 @@
# from 3, 0 to disable, or -1 to auto-set using video resolution.
#kernel-size = -1

# Region of interest of the form (x, y) / (w, h), where x, y is the top left
# corner, and w, h is the width/height in pixels. Brackets, commas, and slahes
# are optional.
# region-of-interest = 100, 110 / 50, 50

# Integer factor to shrink video before processing. Values <= 1 have no effect.
#downscale-factor = 0

Expand Down
71 changes: 50 additions & 21 deletions dvr_scan/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@
"""

import argparse
import os
from typing import List, Optional

import dvr_scan
from dvr_scan.cli.config import ConfigRegistry, CHOICE_MAP, USER_CONFIG_FILE_PATH, ROIValue
from dvr_scan.cli.config import ConfigRegistry, CHOICE_MAP, USER_CONFIG_FILE_PATH
from dvr_scan.selection_window import Point, RegionValue

# Version string shown for the -v/--version CLI argument.
VERSION_STRING = f"""------------------------------------------------
DVR-Scan {dvr_scan.__version__}
------------------------------------------------
Copyright (C) 2016-2022 Brandon Castellano
Copyright (C) 2016-2023 Brandon Castellano
< https://github.com/Breakthrough/DVR-Scan >
"""

Expand Down Expand Up @@ -272,6 +274,8 @@ def __call__(self, parser, namespace, values, option_string=None):

class RoiAction(argparse.Action):

DEFAULT_ERROR_MESSAGE = "Region must be 3 or more points of the form X0 Y0 X1 Y1 X2 Y2 ..."

def __init__(self,
option_strings,
dest,
Expand All @@ -296,34 +300,50 @@ def __init__(self,
required=required,
help=help,
metavar=metavar)
self.file_list = []

def _is_deprecated(self) -> bool:
return '-roi' in self.option_strings
return '-roi' in self.option_strings or '--region-of-interest' in self.option_strings

def __call__(self, parser, namespace, values: List[str], option_string=None):
# Used to show warning to user if they use -roi instead of --roi.
# Used to show warning to user if they use -roi/--region-of-interest instead of -r/--roi.
if self._is_deprecated():
setattr(namespace, 'used_deprecated_roi_option', True)
# --roi/--region-of-interest specified without coordinates
# -r/--roi specified without coordinates
if not values:
if not hasattr(namespace, 'show_roi_window'):
setattr(namespace, 'show_roi_window', 1)
else:
namespace.show_roi_window += 1
return
# TODO(v1.6): Figure out how to represent multiple ROIs in config file,
# e.g.: region-of-interest = [10, 10 / 20, 20], [..]
# TODO(v1.6): Add backwards compatibility for --roi MAX_WIDTH MAX_HEIGHT syntax *only* for
# the deprecated -roi flag, not the new one.
try:
roi = ROIValue(' '.join(values))
except ValueError as ex:
raise argparse.ArgumentError(self,
'ROI must be rectangle of the form --roi X Y W H') from ex
regions = []
save_regions_to = None
if len(values) == 1:
try:
if os.path.exists(values[0]):
region_path = values[0]
with open(region_path, "rt") as region_file:
regions = list(
RegionValue(region) for region in filter(None, (
region.strip() for region in region_file.readlines())))
else:
save_regions_to = values[0]
except ValueError as ex:
prefix = f"Error loading region from {region_path}: "
message = " ".join(
str(arg) for arg in ex.args) if ex.args else RoiAction.DEFAULT_ERROR_MESSAGE
raise (argparse.ArgumentError(self, f"{prefix}{message}")) from ex
else:
try:
regions.append(RegionValue(" ".join(values)))
except ValueError as ex:
message = " ".join(str(arg) for arg in ex.args)
raise (argparse.ArgumentError(
self, message if message else RoiAction.DEFAULT_ERROR_MESSAGE)) from ex

# Append this ROI to any existing ones, if any.
items = getattr(namespace, 'region_of_interest', [])
items += [roi.value]
items += regions
setattr(namespace, 'region_of_interest', items)


Expand Down Expand Up @@ -514,21 +534,30 @@ def get_cli_parser(user_config: ConfigRegistry):
help=('Timecode to stop processing the input (see -st for valid timecode formats).'),
)

# Too much logic crammed in one flag, split it up:
#
# -r/--region [X0 Y0 X1 Y1 X2 Y2 ...]
# -R/--load-region [FILE]
# -s/--save-region [FILE]
#
parser.add_argument(
'--roi',
'--region-of-interest',
metavar='x0 y0 w h',
'-r',
'--region',
metavar='x0 y0 x1 y1 x2 y2',
dest='region_of_interest',
nargs='*',
action=RoiAction,
help=('Limit detection to specified region. Can specify as --roi to show popup window,'
' or specify the region in the form --roi x,y w,h (e.g. --roi 100 200 50 50)%s' %
help=('TODO. %s' %
(user_config.get_help_string('region-of-interest', show_default=False))),
)

# TODO(v1.7): Remove -roi (replaced by --roi/--region-of-interest).
# TODO(v1.7): Remove -roi (split up into other options).
parser.add_argument(
# TODO(v1.6): Re-add support for -roi MAX_WIDTH MAX_HEIGHT syntax until this is removed,
# replaced with config file options.
# TODO(v1.6): Restore this argument to the exact way it was except for the pop-up window.
'-roi',
'-region-of-interest',
metavar='x0 y0 w h',
dest='region_of_interest',
nargs='*',
Expand Down
37 changes: 22 additions & 15 deletions dvr_scan/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
from dvr_scan.detector import Rectangle

# Backwards compatibility for config options that were renamed/replaced.
# TODO(v1.6): Ensure 'region-of-interest' is deprecated and a warning to use -r/--roi with a file
# will be required in a subsequent release.
DEPRECATED_CONFIG_OPTIONS: Dict[str, str] = {
'timecode': 'time-code',
'timecode-margin': 'text-margin',
Expand Down Expand Up @@ -183,39 +185,43 @@ def from_config(config_value: str, default: 'KernelSizeValue') -> 'KernelSizeVal
'Size must be odd number starting from 3, 0 to disable, or -1 for auto.') from ex


class ROIValue(ValidatedValue):
"""Validator for a set of regions of interest."""
class RegionValueDeprecated(ValidatedValue):
"""Validator for deprecated region-of-interest values."""

_IGNORE_CHARS = [',', '/', '(', ')']
"""Characters to ignore."""

def __init__(self, value: Optional[str] = None):
if value is None:
self._value = Rectangle(0, 0, 0, 0)
else:
translation_table = str.maketrans({char: ' ' for char in ROIValue._IGNORE_CHARS})
def __init__(self, value: Optional[str] = None, allow_size: bool = False):
if value is not None:
translation_table = str.maketrans({char: ' ' for char in RegionValue._IGNORE_CHARS})
values = value.translate(translation_table).split()
if not len(values) == 4 and all([val.isdigit() for val in values]):
valid_lengths = (2, 4) if allow_size else (4,)
if not (len(values) in valid_lengths and all([val.isdigit() for val in values])
and all([int(val) >= 0 for val in values])):
raise ValueError()
self._value = Rectangle(*[int(val) for val in values])
self._value = [int(val) for val in values]
else:
self._value = None

@property
def value(self) -> Rectangle:
def value(self) -> Optional[List[int]]:
return self._value

def __repr__(self) -> str:
return str(self.value)

def __str__(self) -> str:
if self.value is not None:
return '(%d,%d)/(%d,%d)' % (self.value[0], self.value[1], self.value[2], self.value[3])
return str(self.value)

@staticmethod
def from_config(config_value: str, default: 'ROIValue') -> 'ROIValue':
def from_config(config_value: str, default: 'RegionValue') -> 'RegionValue':
try:
return ROIValue(config_value)
return RegionValue(config_value)
except ValueError as ex:
raise OptionParseFailure(
'ROI must be four positive integers of the form X, Y, W, H.') from ex
raise OptionParseFailure('ROI must be four positive integers of the form (x,y)/(w,h).'
' Brackets, commas, slashes, and spaces are optional.') from ex


class RGBValue(ValidatedValue):
Expand Down Expand Up @@ -311,7 +317,8 @@ def from_config(config_value: str, default: 'RGBValue') -> 'RGBValue':
'threshold': 0.15,
'kernel-size': KernelSizeValue(),
'downscale-factor': 0,
'region-of-interest': ROIValue(),
# TODO(v1.7): Remove, replaced with region files.
'region-of-interest': RegionValueDeprecated(),
'max-window-width': 0,
'max-window-height': 0,
'frame-skip': 0,
Expand Down
1 change: 1 addition & 0 deletions dvr_scan/cli/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ def run_dvr_scan(settings: ProgramSettings) -> ty.List[ty.Tuple[FrameTimecode, F
input_videos=settings.get_arg('input'),
frame_skip=settings.get('frame-skip'),
show_progress=not settings.get('quiet-mode'),
debug_mode=settings.get('debug'),
)

output_mode = (
Expand Down
16 changes: 8 additions & 8 deletions dvr_scan/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ class MotionScanner:
def __init__(self,
input_videos: List[AnyStr],
frame_skip: int = 0,
show_progress: bool = False):
show_progress: bool = False,
debug_mode: bool = False):
""" Initializes the MotionScanner with the supplied arguments.
Arguments:
Expand All @@ -236,6 +237,7 @@ def __init__(self,

self._in_motion_event: bool = False
self._show_progress: bool = show_progress
self._debug_mode: bool = debug_mode

# Output Parameters (set_output)
self._comp_file: Optional[AnyStr] = None # -o/--output
Expand Down Expand Up @@ -411,12 +413,7 @@ def set_detection_params(self,
self._downscale_factor = max(downscale_factor, 1)
assert kernel_size == -1 or kernel_size == 0 or kernel_size >= 3
self._kernel_size = kernel_size
if roi_list is not None:
if isinstance(roi_list, Rectangle):
self._roi_list = [roi_list]
else:
assert all(isinstance(roi, Rectangle) for roi in roi_list)
self._roi_list = list(roi_list)
# TODO(v1.6): Redo ROI list.
self._show_roi_window = show_roi_window
self._max_window_size = tuple(None if x is None else max(x, 0) for x in max_window_size)

Expand Down Expand Up @@ -514,7 +511,10 @@ def _select_roi(self) -> bool:
factor_h = frame_h / float(max_h) if max_h > 0 and frame_h > max_h else 1
factor_w = frame_w / float(max_w) if max_w > 0 and frame_w > max_w else 1
scale_factor = round(max(factor_h, factor_w))
roi_list = SelectionWindow(frame_for_crop, scale_factor).run()
# TODO(v1.6): Allow previewing a pre-defined set of polygons if they were loaded from
# command line or config file.
roi_list = SelectionWindow(
frame_for_crop, scale_factor, debug_mode=self._debug_mode).run()
logging.info(str(roi_list))
# TODO: Actually use ROI list now.
sys.exit(0)
Expand Down
Loading

0 comments on commit d95bd7d

Please sign in to comment.