config.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. import base64
  2. import json
  3. import os
  4. from typing import Dict
  5. import cv2
  6. import matplotlib.pyplot as plt
  7. import numpy as np
  8. import voluptuous as vol
  9. import yaml
  10. DETECTORS_SCHEMA = vol.Schema(
  11. {
  12. vol.Required(str): {
  13. vol.Required('type', default='edgetpu'): vol.In(['cpu', 'edgetpu']),
  14. vol.Optional('device', default='usb'): str
  15. }
  16. }
  17. )
  18. DEFAULT_DETECTORS = {
  19. 'coral': {
  20. 'type': 'edgetpu',
  21. 'device': 'usb'
  22. }
  23. }
  24. MQTT_SCHEMA = vol.Schema(
  25. {
  26. vol.Required('host'): str,
  27. vol.Optional('port', default=1883): int,
  28. vol.Optional('topic_prefix', default='frigate'): str,
  29. vol.Optional('client_id', default='frigate'): str,
  30. 'user': str,
  31. 'password': str
  32. }
  33. )
  34. SAVE_CLIPS_SCHEMA = vol.Schema(
  35. {
  36. vol.Optional('max_seconds', default=300): int,
  37. vol.Optional('clips_dir', default='/media/frigate/clips'): str,
  38. vol.Optional('cache_dir', default='/tmp/cache'): str
  39. }
  40. )
  41. FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','panic']
  42. FFMPEG_INPUT_ARGS_DEFAULT = ['-avoid_negative_ts', 'make_zero',
  43. '-fflags', 'nobuffer',
  44. '-flags', 'low_delay',
  45. '-strict', 'experimental',
  46. '-fflags', '+genpts+discardcorrupt',
  47. '-rtsp_transport', 'tcp',
  48. '-stimeout', '5000000',
  49. '-use_wallclock_as_timestamps', '1']
  50. FFMPEG_OUTPUT_ARGS_DEFAULT = ['-f', 'rawvideo',
  51. '-pix_fmt', 'yuv420p']
  52. GLOBAL_FFMPEG_SCHEMA = vol.Schema(
  53. {
  54. vol.Optional('global_args', default=FFMPEG_GLOBAL_ARGS_DEFAULT): [str],
  55. vol.Optional('hwaccel_args', default=[]): [str],
  56. vol.Optional('input_args', default=FFMPEG_INPUT_ARGS_DEFAULT): [str],
  57. vol.Optional('output_args', default=FFMPEG_OUTPUT_ARGS_DEFAULT): [str]
  58. }
  59. )
  60. FILTER_SCHEMA = vol.Schema(
  61. {
  62. str: {
  63. vol.Optional('min_area', default=0): int,
  64. vol.Optional('max_area', default=24000000): int,
  65. vol.Optional('threshold', default=0.85): float
  66. }
  67. }
  68. )
  69. def filters_for_all_tracked_objects(object_config):
  70. for tracked_object in object_config.get('track', ['person']):
  71. if not 'filters' in object_config:
  72. object_config['filters'] = {}
  73. if not tracked_object in object_config['filters']:
  74. object_config['filters'][tracked_object] = {}
  75. return object_config
  76. OBJECTS_SCHEMA = vol.Schema(vol.All(filters_for_all_tracked_objects,
  77. {
  78. vol.Optional('track', default=['person']): [str],
  79. # TODO: this should populate filters for all tracked objects
  80. vol.Optional('filters', default = {}): FILTER_SCHEMA.extend({ str: {vol.Optional('min_score', default=0.5): float}})
  81. }
  82. ))
  83. DEFAULT_CAMERA_SAVE_CLIPS = {
  84. 'enabled': False
  85. }
  86. DEFAULT_CAMERA_SNAPSHOTS = {
  87. 'show_timestamp': True,
  88. 'draw_zones': False,
  89. 'draw_bounding_boxes': True,
  90. 'crop_to_region': True
  91. }
  92. CAMERA_FFMPEG_SCHEMA = vol.Schema(
  93. {
  94. vol.Required('input'): str,
  95. 'global_args': [str],
  96. 'hwaccel_args': [str],
  97. 'input_args': [str],
  98. 'output_args': [str]
  99. }
  100. )
  101. CAMERAS_SCHEMA = vol.Schema(
  102. {
  103. str: {
  104. vol.Required('ffmpeg'): CAMERA_FFMPEG_SCHEMA,
  105. vol.Required('height'): int,
  106. vol.Required('width'): int,
  107. 'fps': int,
  108. 'mask': str,
  109. vol.Optional('best_image_timeout', default=60): int,
  110. vol.Optional('zones', default={}): {
  111. str: {
  112. vol.Required('coordinates'): vol.Any(str, [str]),
  113. vol.Optional('filters', default={}): FILTER_SCHEMA
  114. }
  115. },
  116. vol.Optional('save_clips', default=DEFAULT_CAMERA_SAVE_CLIPS): {
  117. vol.Optional('enabled', default=False): bool,
  118. vol.Optional('pre_capture', default=30): int,
  119. 'objects': [str],
  120. },
  121. vol.Optional('snapshots', default=DEFAULT_CAMERA_SNAPSHOTS): {
  122. vol.Optional('show_timestamp', default=True): bool,
  123. vol.Optional('draw_zones', default=False): bool,
  124. vol.Optional('draw_bounding_boxes', default=True): bool,
  125. vol.Optional('crop_to_region', default=True): bool,
  126. 'height': int
  127. },
  128. 'objects': OBJECTS_SCHEMA
  129. }
  130. }
  131. )
  132. FRIGATE_CONFIG_SCHEMA = vol.Schema(
  133. {
  134. vol.Optional('detectors', default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
  135. 'mqtt': MQTT_SCHEMA,
  136. vol.Optional('save_clips', default={}): SAVE_CLIPS_SCHEMA,
  137. vol.Optional('ffmpeg', default={}): GLOBAL_FFMPEG_SCHEMA,
  138. vol.Optional('objects', default={}): OBJECTS_SCHEMA,
  139. vol.Required('cameras', default={}): CAMERAS_SCHEMA
  140. }
  141. )
  142. class DetectorConfig():
  143. def __init__(self, config):
  144. self._type = config['type']
  145. self._device = config['device']
  146. @property
  147. def type(self):
  148. return self._type
  149. @property
  150. def device(self):
  151. return self._device
  152. class MqttConfig():
  153. def __init__(self, config):
  154. self._host = config['host']
  155. self._port = config['port']
  156. self._topic_prefix = config['topic_prefix']
  157. self._client_id = config['client_id']
  158. self._user = config.get('user')
  159. self._password = config.get('password')
  160. @property
  161. def host(self):
  162. return self._host
  163. @property
  164. def port(self):
  165. return self._port
  166. @property
  167. def topic_prefix(self):
  168. return self._topic_prefix
  169. @property
  170. def client_id(self):
  171. return self._client_id
  172. @property
  173. def user(self):
  174. return self._user
  175. @property
  176. def password(self):
  177. return self._password
  178. class SaveClipsConfig():
  179. def __init__(self, config):
  180. self._max_seconds = config['max_seconds']
  181. self._clips_dir = config['clips_dir']
  182. self._cache_dir = config['cache_dir']
  183. @property
  184. def max_seconds(self):
  185. return self._max_seconds
  186. @property
  187. def clips_dir(self):
  188. return self._clips_dir
  189. @property
  190. def cache_dir(self):
  191. return self._cache_dir
  192. class FfmpegConfig():
  193. def __init__(self, global_config, config):
  194. self._input = config.get('input')
  195. self._global_args = config.get('global_args', global_config['global_args'])
  196. self._hwaccel_args = config.get('hwaccel_args', global_config['hwaccel_args'])
  197. self._input_args = config.get('input_args', global_config['input_args'])
  198. self._output_args = config.get('output_args', global_config['output_args'])
  199. @property
  200. def input(self):
  201. return self._input
  202. @property
  203. def global_args(self):
  204. return self._global_args
  205. @property
  206. def hwaccel_args(self):
  207. return self._hwaccel_args
  208. @property
  209. def input_args(self):
  210. return self._input_args
  211. @property
  212. def output_args(self):
  213. return self._output_args
  214. class FilterConfig():
  215. def __init__(self, config):
  216. self._min_area = config['min_area']
  217. self._max_area = config['max_area']
  218. self._threshold = config['threshold']
  219. self._min_score = config.get('min_score')
  220. @property
  221. def min_area(self):
  222. return self._min_area
  223. @property
  224. def max_area(self):
  225. return self._max_area
  226. @property
  227. def threshold(self):
  228. return self._threshold
  229. @property
  230. def min_score(self):
  231. return self._min_score
  232. class ObjectConfig():
  233. def __init__(self, global_config, config):
  234. self._track = config.get('track', global_config['track'])
  235. if 'filters' in config:
  236. self._filters = { name: FilterConfig(c) for name, c in config['filters'].items() }
  237. else:
  238. self._filters = { name: FilterConfig(c) for name, c in global_config['filters'].items() }
  239. @property
  240. def track(self):
  241. return self._track
  242. @property
  243. def filters(self) -> Dict[str, FilterConfig]:
  244. return self._filters
  245. class CameraSnapshotsConfig():
  246. def __init__(self, config):
  247. self._show_timestamp = config['show_timestamp']
  248. self._draw_zones = config['draw_zones']
  249. self._draw_bounding_boxes = config['draw_bounding_boxes']
  250. self._crop_to_region = config['crop_to_region']
  251. self._height = config.get('height')
  252. @property
  253. def show_timestamp(self):
  254. return self._show_timestamp
  255. @property
  256. def draw_zones(self):
  257. return self._draw_zones
  258. @property
  259. def draw_bounding_boxes(self):
  260. return self._draw_bounding_boxes
  261. @property
  262. def crop_to_region(self):
  263. return self._crop_to_region
  264. @property
  265. def height(self):
  266. return self._height
  267. class CameraSaveClipsConfig():
  268. def __init__(self, config):
  269. self._enabled = config['enabled']
  270. self._pre_capture = config['pre_capture']
  271. self._objects = config.get('objects')
  272. @property
  273. def enabled(self):
  274. return self._enabled
  275. @property
  276. def pre_capture(self):
  277. return self._pre_capture
  278. @property
  279. def objects(self):
  280. return self._objects
  281. class ZoneConfig():
  282. def __init__(self, name, config):
  283. self._coordinates = config['coordinates']
  284. self._filters = { name: FilterConfig(c) for name, c in config['filters'].items() }
  285. if isinstance(self._coordinates, list):
  286. self._contour = np.array([[int(p.split(',')[0]), int(p.split(',')[1])] for p in self._coordinates])
  287. elif isinstance(self._coordinates, str):
  288. points = self._coordinates.split(',')
  289. self._contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
  290. else:
  291. print(f"Unable to parse zone coordinates for {name}")
  292. self._contour = np.array([])
  293. self._color = (0,0,0)
  294. @property
  295. def coordinates(self):
  296. return self._coordinates
  297. @property
  298. def contour(self):
  299. return self._contour
  300. @contour.setter
  301. def contour(self, val):
  302. self._contour = val
  303. @property
  304. def color(self):
  305. return self._color
  306. @color.setter
  307. def color(self, val):
  308. self._color = val
  309. @property
  310. def filters(self):
  311. return self._filters
  312. class CameraConfig():
  313. def __init__(self, name, config, cache_dir, global_ffmpeg, global_objects):
  314. self._name = name
  315. self._ffmpeg = FfmpegConfig(global_ffmpeg, config['ffmpeg'])
  316. self._height = config.get('height')
  317. self._width = config.get('width')
  318. self._frame_shape = (self._height, self._width)
  319. self._frame_shape_yuv = (self._frame_shape[0]*3//2, self._frame_shape[1])
  320. self._fps = config.get('fps')
  321. self._mask = self._create_mask(config.get('mask'))
  322. self._best_image_timeout = config['best_image_timeout']
  323. self._zones = { name: ZoneConfig(name, z) for name, z in config['zones'].items() }
  324. self._save_clips = CameraSaveClipsConfig(config['save_clips'])
  325. self._snapshots = CameraSnapshotsConfig(config['snapshots'])
  326. self._objects = ObjectConfig(global_objects, config.get('objects', {}))
  327. self._ffmpeg_cmd = self._get_ffmpeg_cmd(cache_dir)
  328. self._set_zone_colors(self._zones)
  329. def _create_mask(self, mask):
  330. if mask:
  331. if mask.startswith('base64,'):
  332. img = base64.b64decode(mask[7:])
  333. np_img = np.fromstring(img, dtype=np.uint8)
  334. mask_img = cv2.imdecode(np_img, cv2.IMREAD_GRAYSCALE)
  335. elif mask.startswith('poly,'):
  336. points = mask.split(',')[1:]
  337. contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
  338. mask_img = np.zeros(self.frame_shape, np.uint8)
  339. mask_img[:] = 255
  340. cv2.fillPoly(mask_img, pts=[contour], color=(0))
  341. else:
  342. mask_img = cv2.imread(f"/config/{mask}", cv2.IMREAD_GRAYSCALE)
  343. else:
  344. mask_img = None
  345. if mask_img is None or mask_img.size == 0:
  346. mask_img = np.zeros(self.frame_shape, np.uint8)
  347. mask_img[:] = 255
  348. return mask_img
  349. def _get_ffmpeg_cmd(self, cache_dir):
  350. ffmpeg_output_args = self.ffmpeg.output_args
  351. if self.fps:
  352. ffmpeg_output_args = ["-r", str(self.fps)] + ffmpeg_output_args
  353. if self.save_clips.enabled:
  354. ffmpeg_output_args = [
  355. "-f",
  356. "segment",
  357. "-segment_time",
  358. "10",
  359. "-segment_format",
  360. "mp4",
  361. "-reset_timestamps",
  362. "1",
  363. "-strftime",
  364. "1",
  365. "-c",
  366. "copy",
  367. "-an",
  368. f"{os.path.join(cache_dir, self.name)}-%Y%m%d%H%M%S.mp4"
  369. ] + ffmpeg_output_args
  370. return (['ffmpeg'] +
  371. self.ffmpeg.global_args +
  372. self.ffmpeg.hwaccel_args +
  373. self.ffmpeg.input_args +
  374. ['-i', self.ffmpeg.input] +
  375. ffmpeg_output_args +
  376. ['pipe:'])
  377. def _set_zone_colors(self, zones: Dict[str, ZoneConfig]):
  378. # set colors for zones
  379. all_zone_names = zones.keys()
  380. zone_colors = {}
  381. colors = plt.cm.get_cmap('tab10', len(all_zone_names))
  382. for i, zone in enumerate(all_zone_names):
  383. zone_colors[zone] = tuple(int(round(255 * c)) for c in colors(i)[:3])
  384. for name, zone in zones.items():
  385. zone.color = zone_colors[name]
  386. @property
  387. def name(self):
  388. return self._name
  389. @property
  390. def ffmpeg(self):
  391. return self._ffmpeg
  392. @property
  393. def height(self):
  394. return self._height
  395. @property
  396. def width(self):
  397. return self._width
  398. @property
  399. def fps(self):
  400. return self._fps
  401. @property
  402. def mask(self):
  403. return self._mask
  404. @property
  405. def best_image_timeout(self):
  406. return self._best_image_timeout
  407. @property
  408. def mqtt(self):
  409. return self._mqtt
  410. @property
  411. def zones(self)-> Dict[str, ZoneConfig]:
  412. return self._zones
  413. @property
  414. def save_clips(self):
  415. return self._save_clips
  416. @property
  417. def snapshots(self):
  418. return self._snapshots
  419. @property
  420. def objects(self):
  421. return self._objects
  422. @property
  423. def frame_shape(self):
  424. return self._frame_shape
  425. @property
  426. def frame_shape_yuv(self):
  427. return self._frame_shape_yuv
  428. @property
  429. def ffmpeg_cmd(self):
  430. return self._ffmpeg_cmd
  431. class FrigateConfig():
  432. def __init__(self, config_file=None, config=None):
  433. if config is None and config_file is None:
  434. raise ValueError('config or config_file must be defined')
  435. elif not config_file is None:
  436. config = self._load_file(config_file)
  437. config = FRIGATE_CONFIG_SCHEMA(config)
  438. config = self._sub_env_vars(config)
  439. self._web_port = config['web_port']
  440. self._detectors = { name: DetectorConfig(d) for name, d in config['detectors'].items() }
  441. self._mqtt = MqttConfig(config['mqtt'])
  442. self._save_clips = SaveClipsConfig(config['save_clips'])
  443. self._cameras = { name: CameraConfig(name, c, self._save_clips.cache_dir, config['ffmpeg'], config['objects']) for name, c in config['cameras'].items() }
  444. self._ensure_dirs()
  445. def _sub_env_vars(self, config):
  446. frigate_env_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')}
  447. if 'password' in config['mqtt']:
  448. config['mqtt']['password'] = config['mqtt']['password'].format(**frigate_env_vars)
  449. for camera in config['cameras'].values():
  450. camera['ffmpeg']['input'] = camera['ffmpeg']['input'].format(**frigate_env_vars)
  451. return config
  452. def _ensure_dirs(self):
  453. cache_dir = self.save_clips.cache_dir
  454. clips_dir = self.save_clips.clips_dir
  455. if not os.path.exists(cache_dir) and not os.path.islink(cache_dir):
  456. os.makedirs(cache_dir)
  457. if not os.path.exists(clips_dir) and not os.path.islink(clips_dir):
  458. os.makedirs(clips_dir)
  459. def _load_file(self, config_file):
  460. with open(config_file) as f:
  461. raw_config = f.read()
  462. if config_file.endswith(".yml"):
  463. config = yaml.safe_load(raw_config)
  464. elif config_file.endswith(".json"):
  465. config = json.loads(raw_config)
  466. return config
  467. @property
  468. def detectors(self) -> Dict[str, DetectorConfig]:
  469. return self._detectors
  470. @property
  471. def mqtt(self):
  472. return self._mqtt
  473. @property
  474. def save_clips(self):
  475. return self._save_clips
  476. @property
  477. def cameras(self) -> Dict[str, CameraConfig]:
  478. return self._cameras