class HsvRangeFilterConfig(FilterConfig): """HSV range filter""" range: HsvColor = Field(default=HsvColor(h=10, s=75, v=75)) color: HsvColor = Field(default=HsvColor()) close: int = Field(default=0, ge=0, le=200) open: int = Field(default=0, ge=0, le=200) @property def ready(self) -> bool: return self.color != HsvColor() @property def c0(self) -> HsvColor: return self.color - self.range @property def c1(self) -> HsvColor: return self.color + self.range _resolve_close = validator('close', allow_reuse=True)(BaseConfig._odd_add) _resolve_open = validator('open', allow_reuse=True)(BaseConfig._odd_add) _close_limits = validator('close', pre=True, allow_reuse=True)(BaseConfig._int_limits) _open_limits = validator('open', pre=True, allow_reuse=True)(BaseConfig._int_limits)
class BackgroundFilterConfig(FilterConfig): """HSV range filter""" range: HsvColor = Field(default=HsvColor(h=10, s=75, v=75)) color: HsvColor = Field(default=HsvColor()) @property def ready(self) -> bool: return self.color != HsvColor() @property def c0(self) -> HsvColor: return self.color - self.range @property def c1(self) -> HsvColor: return self.color + self.range
def set(self, color: HsvColor = None) -> FilterConfig: # if isinstance(self.config.data, dict): todo: should be ok # self.config.data = self.implementation.config_class()(**self.config.data) if color is None: color = HsvColor(0,0,0) self.config(data=self.implementation.set_filter(self.config.data, color)) log.debug(f"Filter config: {self.config}") return self.config.data
def __init__(self, elements: Tuple[Instance, ...], global_config: FeatureConfig, config: Optional[dict] = None): self._skip = False self._ready = False self._elements = elements self._global_config = global_config if config is not None: self._config = global_config.__class__(**config) else: self._config = None self._color = HsvColor(h=0, s=200, v=255) # start out as red
def set_filter_click(self, relative_x: float, relative_y: float): log.debug(f'set_filter_click @ ({relative_x}, {relative_y})') click = ShapeCoo( x = relative_x, y = relative_y, shape = self.design.shape[::-1] ) hits = [mask for mask in self.masks if mask.contains(click)] if len(hits) == 1: hit = hits[0] frame = self.video.read_frame() click = self.transform.coordinate(click) color = HsvColor(*click.value(frame)) log.debug(f"color @ {click.idx}: {color}") hit.set_filter(color) for fs in self._featuresets.values(): for feature in fs.features: assert isinstance(feature, MaskFunction) try: if feature.mask == hit: # type: ignore feature._ready = True except AttributeError: pass self.get_colors() self.state_transition() self.event(PushEvent.CONFIG, self.get_config()) self.commit() streams.update() self.commit() elif len(hits) == 0: log.debug(f"no hit for {click.idx}") elif len(hits) > 1: self.notice( f"Multiple valid options: {[hit.name for hit in hits]}. " f"Select a point where masks don't overlap." )
def resolve_colors(self) -> Tuple[Color, ...]: guideline_colors = [ as_hsv(f._guideline_color()) for f in self._features ] min_v = 20.0 max_v = 255.0 tolerance = 15 bins: list = [] # todo: clean up binning for index, color in enumerate(guideline_colors): if not bins: bins.append([index]) else: in_bin = False for bin in bins: if abs( float(color.h) - np.mean([guideline_colors[i].h for i in bin])) < tolerance: bin.append(index) in_bin = True break if not in_bin: bins.append([index]) for bin in bins: if len(bin) < 4: increment = 60.0 else: increment = (max_v - min_v) / len(bin) for repetition, index in enumerate(bin): self._features[index].set_color( HsvColor(h=guideline_colors[index].h, s=220, v=int(max_v - repetition * increment))) self._colors = tuple([feature.color for feature in self._features]) return self.colors
def normalize_config(d: dict) -> dict: """Normalize a configuration dictionary to match the current version of `isimple.core.config` :param d: configuration dictionary :return: """ # If empty dict, return empty dict if len(d) == 0: return d # Deal with legacy formatting if VERSION not in d or CLASS not in d: if 'version' in d: # Support pre-0.3 config metadata field d[VERSION] = d.pop('version') d[CLASS] = VideoAnalyzerConfig.__name__ else: raise ValueError(f"No version or class info in config") def normalizing_to(version): log.debug( f"Normalizing configuration (from v{d[VERSION]} to v{version})") # VideoAnalyzerConfig is the only class that should be deserialized! # -> other classes are contained within it or should only be used # internally. Otherwise, this function would have to be a lot # more complex. todo: actually, no. # Just add some nested functions for the # internal fields, switch ~ class. # Some classes can use multiple functions; # go over the fields and pick out functions. if d[CLASS] == VideoAnalyzerConfig.__name__: if before_version(d[VERSION], '0.2.1'): normalizing_to('0.2.1') # Rename mask[i].filter.filter to mask[i].filter.data for m in d['masks']: m['filter']['data'] = m['filter'].pop('filter') if before_version(d[VERSION], '0.2.2'): normalizing_to('0.2.2') # Convert tuple string color '(0,0,0)' to HsvColor string 'HsvColor(h=0, s=0, v=0)' from ast import literal_eval as make_tuple # todo: this is unsafe! for m in d['masks']: if 'c0' in m['filter']['data']: m['filter']['data']['c0'] = HsvColor( *make_tuple(m['filter']['data']['c0'])) if 'c1' in m['filter']['data']: m['filter']['data']['c1'] = HsvColor( *make_tuple(m['filter']['data']['c1'])) if 'radius' in m['filter']['data']: m['filter']['data']['radius'] = HsvColor( *make_tuple(m['filter']['data']['radius'])) if before_version(d[VERSION], '0.3.1'): normalizing_to('0.3.1') # Rename TransformHandlerConfig 'coordinates' to 'roi' if 'transform' in d: if 'coordinates' in d['transform']: d['transform']['roi'] = d['transform'].pop('coordinates') if before_version(d[VERSION], '0.3.2'): normalizing_to('0.3.2') # Remove some fields that should be in the global settings to_remove = ('cache_dir', 'cache_size_limit', 'render_dir') if 'video' in d: for k in to_remove: d['video'].pop(k, None) if 'design' in d: for k in to_remove: d['design'].pop(k, None) if before_version(d[VERSION], '0.3.4'): normalizing_to('0.3.4') # Set frame_interval_setting to dt, the previous default if 'dt' in d: d['frame_initerval_setting'] = FrameIntervalSetting('dt') if before_version(d[VERSION], '0.3.5'): normalizing_to('0.3.5') # Convert roi from list to dict if 'transform' in d: if 'roi' in d['transform']: d['transform']['roi'] = { corner: { 'x': coordinate[0], 'y': coordinate[1] } for coordinate, corner in zip( json.loads(d['transform']['roi']), ['BL', 'TL', 'TR', 'BR']) } if before_version(d[VERSION], '0.3.6'): normalizing_to('0.3.6') # add ready & skip tags to mask if 'masks' in d: for m in d['masks']: m['skip'] = False try: m['ready'] = m['filter']['data']['c0'] != HsvColor() except KeyError: m['ready'] = False if before_version(d[VERSION], '0.3.7'): normalizing_to('0.3.7') # remove DesignFileHandlerConfig.keep_renders & VideoFileHandlerConfig.do_resolve_frame_number if 'video' in d: if 'do_resolve_frame_number' in d['video']: d['video'].pop('do_resolve_frame_number') if 'design' in d: if 'keep_renders' in d['design']: d['design'].pop('keep_renders') if before_version(d[VERSION], '0.3.8'): normalizing_to('0.3.8') # remove CachingInstance.cache_consumer if 'video' in d: if 'cache_consumer' in d['video']: d['video'].pop('cache_consumer') if 'design' in d: if 'cache_consumer' in d['design']: d['design'].pop('cache_consumer') if before_version(d[VERSION], '0.3.9'): normalizing_to('0.3.9') # remove mask.ready attribute if 'masks' in d: for mask in d['masks']: if 'ready' in mask: mask.pop('ready') if before_version(d[VERSION], '0.3.10'): normalizing_to('0.3.10') # move TransformConfig fields into TransformConfig.data if 'transform' in d: if not 'data' in d['transform']: d['transform']['data'] = {} d['transform']['data']['matrix'] = d['transform'].pop('matrix') if before_version(d[VERSION], '0.3.11'): normalizing_to('0.3.11') # remove matrix & inverse fields from TransformConfig.data if 'transform' in d: if 'data' in d['transform']: if 'matrix' in d['transform']['data']: d['transform']['data'].pop('matrix') if 'inverse' in d['transform']['data']: d['transform']['data'].pop('inverse') if before_version(d[VERSION], '0.3.12'): normalizing_to('0.3.12') # flip (bool, bool) to pydantic if 'transform' in d: if 'flip' in d['transform']: d['transform']['flip'] = { 'vertical': d['transform']['flip'][0], 'horizontal': d['transform']['flip'][1] } if before_version(d[VERSION], '0.3.13'): normalizing_to('0.3.13') # HsvRangeFilter: radius/c0/c1 -> range/color if 'masks' in d: for mask in d['masks']: if 'filter' in mask and mask['filter'][ 'type'] == 'HsvRangeFilter': if 'radius' in mask['filter']['data']: mask['filter']['data']['range'] = mask['filter'][ 'data'].pop('radius') if 'c0' in mask['filter']['data']: c0 = mask['filter']['data'].pop('c0') if 'c1' in mask['filter']['data']: mask['filter']['data'].pop('c1') if before_version(d[VERSION], '0.3.15'): normalizing_to('0.3.15') # rename parameters -> feature_parameters if 'parameters' in d: d['feature_parameters'] = d.pop('parameters') else: raise NotImplementedError # Remove non-standard fields config_type = ConfigType(d[CLASS]).get() for k in list(d.keys()): if k not in (VERSION, CLASS): if not k in config_type.__fields__: log.warning(f"Removed unexpected attribute " f"'{k}':{d.pop(k)} from {d[CLASS]}") # Remove timestamp & version info d.pop('timestamp', None) d.pop('version', None) untag(d) return d
def mean_color(self, filter: HsvRangeFilterConfig) -> Color: # S and V are arbitrary but work relatively well # for both overlay & plot colors return HsvColor(h=filter.color.h, s=255, v=200)
def ready(self) -> bool: return self.color != HsvColor()
import numpy as np import cv2 from isimple import get_logger, settings from isimple.config import extend, ConfigType, Field from isimple.core.interface import FilterConfig, FilterInterface, FilterType from isimple.maths.colors import Color, HsvColor, convert, WRAP log = get_logger(__name__) COLOR = HsvColor(h=0, s=0, v=0) @extend(ConfigType) class BackgroundFilterConfig(FilterConfig): """HSV range filter""" range: HsvColor = Field(default=HsvColor(h=10, s=75, v=75)) color: HsvColor = Field(default=HsvColor()) @property def ready(self) -> bool: return self.color != HsvColor() @property def c0(self) -> HsvColor: return self.color - self.range @property