config.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956
  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. from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
  11. DETECTORS_SCHEMA = vol.Schema(
  12. {
  13. vol.Required(str): {
  14. vol.Required('type', default='edgetpu'): vol.In(['cpu', 'edgetpu']),
  15. vol.Optional('device', default='usb'): str,
  16. vol.Optional('num_threads', default=3): int
  17. }
  18. }
  19. )
  20. DEFAULT_DETECTORS = {
  21. 'coral': {
  22. 'type': 'edgetpu',
  23. 'device': 'usb'
  24. }
  25. }
  26. MQTT_SCHEMA = vol.Schema(
  27. {
  28. vol.Required('host'): str,
  29. vol.Optional('port', default=1883): int,
  30. vol.Optional('topic_prefix', default='frigate'): str,
  31. vol.Optional('client_id', default='frigate'): str,
  32. 'user': str,
  33. 'password': str
  34. }
  35. )
  36. SAVE_CLIPS_RETAIN_SCHEMA = vol.Schema(
  37. {
  38. vol.Required('default',default=10): int,
  39. 'objects': {
  40. str: int
  41. }
  42. }
  43. )
  44. SAVE_CLIPS_SCHEMA = vol.Schema(
  45. {
  46. vol.Optional('max_seconds', default=300): int,
  47. vol.Optional('retain', default={}): SAVE_CLIPS_RETAIN_SCHEMA
  48. }
  49. )
  50. FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','fatal']
  51. FFMPEG_INPUT_ARGS_DEFAULT = ['-avoid_negative_ts', 'make_zero',
  52. '-fflags', '+genpts+discardcorrupt',
  53. '-rtsp_transport', 'tcp',
  54. '-stimeout', '5000000',
  55. '-use_wallclock_as_timestamps', '1']
  56. DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = ['-f', 'rawvideo',
  57. '-pix_fmt', 'yuv420p']
  58. RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-c", "copy", "-f", "flv"]
  59. SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "segment", "-segment_time",
  60. "10", "-segment_format", "mp4", "-reset_timestamps", "1", "-strftime",
  61. "1", "-c", "copy", "-an"]
  62. RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "segment", "-segment_time",
  63. "60", "-segment_format", "mp4", "-reset_timestamps", "1", "-strftime",
  64. "1", "-c", "copy", "-an"]
  65. GLOBAL_FFMPEG_SCHEMA = vol.Schema(
  66. {
  67. vol.Optional('global_args', default=FFMPEG_GLOBAL_ARGS_DEFAULT): vol.Any(str, [str]),
  68. vol.Optional('hwaccel_args', default=[]): vol.Any(str, [str]),
  69. vol.Optional('input_args', default=FFMPEG_INPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  70. vol.Optional('output_args', default={}): {
  71. vol.Optional('detect', default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  72. vol.Optional('record', default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  73. vol.Optional('clips', default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  74. vol.Optional('rtmp', default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  75. }
  76. }
  77. )
  78. MOTION_SCHEMA = vol.Schema(
  79. {
  80. 'threshold': vol.Range(min=1, max=255),
  81. 'contour_area': int,
  82. 'delta_alpha': float,
  83. 'frame_alpha': float,
  84. 'frame_height': int
  85. }
  86. )
  87. DETECT_SCHEMA = vol.Schema(
  88. {
  89. 'max_disappeared': int
  90. }
  91. )
  92. FILTER_SCHEMA = vol.Schema(
  93. {
  94. str: {
  95. vol.Optional('min_area', default=0): int,
  96. vol.Optional('max_area', default=24000000): int,
  97. vol.Optional('threshold', default=0.7): float
  98. }
  99. }
  100. )
  101. def filters_for_all_tracked_objects(object_config):
  102. for tracked_object in object_config.get('track', ['person']):
  103. if not 'filters' in object_config:
  104. object_config['filters'] = {}
  105. if not tracked_object in object_config['filters']:
  106. object_config['filters'][tracked_object] = {}
  107. return object_config
  108. OBJECTS_SCHEMA = vol.Schema(vol.All(filters_for_all_tracked_objects,
  109. {
  110. vol.Optional('track', default=['person']): [str],
  111. vol.Optional('filters', default = {}): FILTER_SCHEMA.extend({ str: {vol.Optional('min_score', default=0.5): float}})
  112. }
  113. ))
  114. def each_role_used_once(inputs):
  115. roles = [role for i in inputs for role in i['roles']]
  116. roles_set = set(roles)
  117. if len(roles) > len(roles_set):
  118. raise ValueError
  119. return inputs
  120. CAMERA_FFMPEG_SCHEMA = vol.Schema(
  121. {
  122. vol.Required('inputs'): vol.All([{
  123. vol.Required('path'): str,
  124. vol.Required('roles'): ['detect', 'clips', 'record', 'rtmp'],
  125. 'global_args': vol.Any(str, [str]),
  126. 'hwaccel_args': vol.Any(str, [str]),
  127. 'input_args': vol.Any(str, [str]),
  128. }], vol.Msg(each_role_used_once, msg="Each input role may only be used once")),
  129. 'output_args': {
  130. vol.Optional('detect', default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  131. vol.Optional('record', default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  132. vol.Optional('clips', default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  133. vol.Optional('rtmp', default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  134. }
  135. }
  136. )
  137. def ensure_zones_and_cameras_have_different_names(cameras):
  138. zones = [zone for camera in cameras.values() for zone in camera['zones'].keys()]
  139. for zone in zones:
  140. if zone in cameras.keys():
  141. raise ValueError
  142. return cameras
  143. CAMERAS_SCHEMA = vol.Schema(vol.All(
  144. {
  145. str: {
  146. vol.Required('ffmpeg'): CAMERA_FFMPEG_SCHEMA,
  147. vol.Required('height'): int,
  148. vol.Required('width'): int,
  149. 'fps': int,
  150. 'mask': vol.Any(str, [str]),
  151. vol.Optional('best_image_timeout', default=60): int,
  152. vol.Optional('zones', default={}): {
  153. str: {
  154. vol.Required('coordinates'): vol.Any(str, [str]),
  155. vol.Optional('filters', default={}): FILTER_SCHEMA
  156. }
  157. },
  158. vol.Optional('save_clips', default={}): {
  159. vol.Optional('enabled', default=False): bool,
  160. vol.Optional('pre_capture', default=5): int,
  161. vol.Optional('post_capture', default=5): int,
  162. 'objects': [str],
  163. vol.Optional('retain', default={}): SAVE_CLIPS_RETAIN_SCHEMA,
  164. },
  165. vol.Optional('record', default={}): {
  166. 'enabled': bool,
  167. 'retain_days': int,
  168. },
  169. vol.Optional('rtmp', default={}): {
  170. vol.Required('enabled', default=True): bool,
  171. },
  172. vol.Optional('snapshots', default={}): {
  173. vol.Optional('show_timestamp', default=True): bool,
  174. vol.Optional('draw_zones', default=False): bool,
  175. vol.Optional('draw_bounding_boxes', default=True): bool,
  176. vol.Optional('crop_to_region', default=True): bool,
  177. vol.Optional('height', default=175): int
  178. },
  179. 'objects': OBJECTS_SCHEMA,
  180. vol.Optional('motion', default={}): MOTION_SCHEMA,
  181. vol.Optional('detect', default={}): DETECT_SCHEMA
  182. }
  183. }, vol.Msg(ensure_zones_and_cameras_have_different_names, msg='Zones cannot share names with cameras'))
  184. )
  185. FRIGATE_CONFIG_SCHEMA = vol.Schema(
  186. {
  187. vol.Optional('database', default={}): {
  188. vol.Optional('path', default=os.path.join(CLIPS_DIR, 'frigate.db')): str
  189. },
  190. vol.Optional('model', default={'width': 320, 'height': 320}): {
  191. vol.Required('width'): int,
  192. vol.Required('height'): int
  193. },
  194. vol.Optional('detectors', default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
  195. 'mqtt': MQTT_SCHEMA,
  196. vol.Optional('logger', default={'default': 'info', 'logs': {}}): {
  197. vol.Optional('default', default='info'): vol.In(['info', 'debug', 'warning', 'error', 'critical']),
  198. vol.Optional('logs', default={}): {str: vol.In(['info', 'debug', 'warning', 'error', 'critical']) }
  199. },
  200. vol.Optional('save_clips', default={}): SAVE_CLIPS_SCHEMA,
  201. vol.Optional('record', default={}): {
  202. vol.Optional('enabled', default=False): bool,
  203. vol.Optional('retain_days', default=30): int,
  204. },
  205. vol.Optional('ffmpeg', default={}): GLOBAL_FFMPEG_SCHEMA,
  206. vol.Optional('objects', default={}): OBJECTS_SCHEMA,
  207. vol.Optional('motion', default={}): MOTION_SCHEMA,
  208. vol.Optional('detect', default={}): DETECT_SCHEMA,
  209. vol.Required('cameras', default={}): CAMERAS_SCHEMA
  210. }
  211. )
  212. class DatabaseConfig():
  213. def __init__(self, config):
  214. self._path = config['path']
  215. @property
  216. def path(self):
  217. return self._path
  218. def to_dict(self):
  219. return {
  220. 'path': self.path
  221. }
  222. class ModelConfig():
  223. def __init__(self, config):
  224. self._width = config['width']
  225. self._height = config['height']
  226. @property
  227. def width(self):
  228. return self._width
  229. @property
  230. def height(self):
  231. return self._height
  232. def to_dict(self):
  233. return {
  234. 'width': self.width,
  235. 'height': self.height
  236. }
  237. class DetectorConfig():
  238. def __init__(self, config):
  239. self._type = config['type']
  240. self._device = config['device']
  241. self._num_threads = config['num_threads']
  242. @property
  243. def type(self):
  244. return self._type
  245. @property
  246. def device(self):
  247. return self._device
  248. @property
  249. def num_threads(self):
  250. return self._num_threads
  251. def to_dict(self):
  252. return {
  253. 'type': self.type,
  254. 'device': self.device,
  255. 'num_threads': self.num_threads
  256. }
  257. class LoggerConfig():
  258. def __init__(self, config):
  259. self._default = config['default'].upper()
  260. self._logs = {k: v.upper() for k, v in config['logs'].items()}
  261. @property
  262. def default(self):
  263. return self._default
  264. @property
  265. def logs(self):
  266. return self._logs
  267. def to_dict(self):
  268. return {
  269. 'default': self.default,
  270. 'logs': self.logs
  271. }
  272. class MqttConfig():
  273. def __init__(self, config):
  274. self._host = config['host']
  275. self._port = config['port']
  276. self._topic_prefix = config['topic_prefix']
  277. self._client_id = config['client_id']
  278. self._user = config.get('user')
  279. self._password = config.get('password')
  280. @property
  281. def host(self):
  282. return self._host
  283. @property
  284. def port(self):
  285. return self._port
  286. @property
  287. def topic_prefix(self):
  288. return self._topic_prefix
  289. @property
  290. def client_id(self):
  291. return self._client_id
  292. @property
  293. def user(self):
  294. return self._user
  295. @property
  296. def password(self):
  297. return self._password
  298. def to_dict(self):
  299. return {
  300. 'host': self.host,
  301. 'port': self.port,
  302. 'topic_prefix': self.topic_prefix,
  303. 'client_id': self.client_id,
  304. 'user': self.user
  305. }
  306. class CameraInput():
  307. def __init__(self, global_config, ffmpeg_input):
  308. self._path = ffmpeg_input['path']
  309. self._roles = ffmpeg_input['roles']
  310. self._global_args = ffmpeg_input.get('global_args', global_config['global_args'])
  311. self._hwaccel_args = ffmpeg_input.get('hwaccel_args', global_config['hwaccel_args'])
  312. self._input_args = ffmpeg_input.get('input_args', global_config['input_args'])
  313. @property
  314. def path(self):
  315. return self._path
  316. @property
  317. def roles(self):
  318. return self._roles
  319. @property
  320. def global_args(self):
  321. return self._global_args if isinstance(self._global_args, list) else self._global_args.split(' ')
  322. @property
  323. def hwaccel_args(self):
  324. return self._hwaccel_args if isinstance(self._hwaccel_args, list) else self._hwaccel_args.split(' ')
  325. @property
  326. def input_args(self):
  327. return self._input_args if isinstance(self._input_args, list) else self._input_args.split(' ')
  328. class CameraFfmpegConfig():
  329. def __init__(self, global_config, config):
  330. self._inputs = [CameraInput(global_config, i) for i in config['inputs']]
  331. self._output_args = config.get('output_args', global_config['output_args'])
  332. @property
  333. def inputs(self):
  334. return self._inputs
  335. @property
  336. def output_args(self):
  337. return {k: v if isinstance(v, list) else v.split(' ') for k, v in self._output_args.items()}
  338. class SaveClipsRetainConfig():
  339. def __init__(self, global_config, config):
  340. self._default = config.get('default', global_config.get('default'))
  341. self._objects = config.get('objects', global_config.get('objects', {}))
  342. @property
  343. def default(self):
  344. return self._default
  345. @property
  346. def objects(self):
  347. return self._objects
  348. def to_dict(self):
  349. return {
  350. 'default': self.default,
  351. 'objects': self.objects
  352. }
  353. class SaveClipsConfig():
  354. def __init__(self, config):
  355. self._max_seconds = config['max_seconds']
  356. self._retain = SaveClipsRetainConfig(config['retain'], config['retain'])
  357. @property
  358. def max_seconds(self):
  359. return self._max_seconds
  360. @property
  361. def retain(self):
  362. return self._retain
  363. def to_dict(self):
  364. return {
  365. 'max_seconds': self.max_seconds,
  366. 'retain': self.retain.to_dict()
  367. }
  368. class RecordConfig():
  369. def __init__(self, global_config, config):
  370. self._enabled = config.get('enabled', global_config['enabled'])
  371. self._retain_days = config.get('retain_days', global_config['retain_days'])
  372. @property
  373. def enabled(self):
  374. return self._enabled
  375. @property
  376. def retain_days(self):
  377. return self._retain_days
  378. def to_dict(self):
  379. return {
  380. 'enabled': self.enabled,
  381. 'retain_days': self.retain_days,
  382. }
  383. class FilterConfig():
  384. def __init__(self, config):
  385. self._min_area = config['min_area']
  386. self._max_area = config['max_area']
  387. self._threshold = config['threshold']
  388. self._min_score = config.get('min_score')
  389. @property
  390. def min_area(self):
  391. return self._min_area
  392. @property
  393. def max_area(self):
  394. return self._max_area
  395. @property
  396. def threshold(self):
  397. return self._threshold
  398. @property
  399. def min_score(self):
  400. return self._min_score
  401. def to_dict(self):
  402. return {
  403. 'min_area': self.min_area,
  404. 'max_area': self.max_area,
  405. 'threshold': self.threshold,
  406. 'min_score': self.min_score
  407. }
  408. class ObjectConfig():
  409. def __init__(self, global_config, config):
  410. self._track = config.get('track', global_config['track'])
  411. if 'filters' in config:
  412. self._filters = { name: FilterConfig(c) for name, c in config['filters'].items() }
  413. else:
  414. self._filters = { name: FilterConfig(c) for name, c in global_config['filters'].items() }
  415. @property
  416. def track(self):
  417. return self._track
  418. @property
  419. def filters(self) -> Dict[str, FilterConfig]:
  420. return self._filters
  421. def to_dict(self):
  422. return {
  423. 'track': self.track,
  424. 'filters': { k: f.to_dict() for k, f in self.filters.items() }
  425. }
  426. class CameraSnapshotsConfig():
  427. def __init__(self, config):
  428. self._show_timestamp = config['show_timestamp']
  429. self._draw_zones = config['draw_zones']
  430. self._draw_bounding_boxes = config['draw_bounding_boxes']
  431. self._crop_to_region = config['crop_to_region']
  432. self._height = config.get('height')
  433. @property
  434. def show_timestamp(self):
  435. return self._show_timestamp
  436. @property
  437. def draw_zones(self):
  438. return self._draw_zones
  439. @property
  440. def draw_bounding_boxes(self):
  441. return self._draw_bounding_boxes
  442. @property
  443. def crop_to_region(self):
  444. return self._crop_to_region
  445. @property
  446. def height(self):
  447. return self._height
  448. def to_dict(self):
  449. return {
  450. 'show_timestamp': self.show_timestamp,
  451. 'draw_zones': self.draw_zones,
  452. 'draw_bounding_boxes': self.draw_bounding_boxes,
  453. 'crop_to_region': self.crop_to_region,
  454. 'height': self.height
  455. }
  456. class CameraSaveClipsConfig():
  457. def __init__(self, global_config, config):
  458. self._enabled = config['enabled']
  459. self._pre_capture = config['pre_capture']
  460. self._post_capture = config['post_capture']
  461. self._objects = config.get('objects', global_config['objects']['track'])
  462. self._retain = SaveClipsRetainConfig(global_config['save_clips']['retain'], config['retain'])
  463. @property
  464. def enabled(self):
  465. return self._enabled
  466. @property
  467. def pre_capture(self):
  468. return self._pre_capture
  469. @property
  470. def post_capture(self):
  471. return self._post_capture
  472. @property
  473. def objects(self):
  474. return self._objects
  475. @property
  476. def retain(self):
  477. return self._retain
  478. def to_dict(self):
  479. return {
  480. 'enabled': self.enabled,
  481. 'pre_capture': self.pre_capture,
  482. 'post_capture': self.post_capture,
  483. 'objects': self.objects,
  484. 'retain': self.retain.to_dict()
  485. }
  486. class CameraRtmpConfig():
  487. def __init__(self, global_config, config):
  488. self._enabled = config['enabled']
  489. @property
  490. def enabled(self):
  491. return self._enabled
  492. def to_dict(self):
  493. return {
  494. 'enabled': self.enabled,
  495. }
  496. class MotionConfig():
  497. def __init__(self, global_config, config, camera_height: int):
  498. self._threshold = config.get('threshold', global_config.get('threshold', 25))
  499. self._contour_area = config.get('contour_area', global_config.get('contour_area', 100))
  500. self._delta_alpha = config.get('delta_alpha', global_config.get('delta_alpha', 0.2))
  501. self._frame_alpha = config.get('frame_alpha', global_config.get('frame_alpha', 0.2))
  502. self._frame_height = config.get('frame_height', global_config.get('frame_height', camera_height//6))
  503. @property
  504. def threshold(self):
  505. return self._threshold
  506. @property
  507. def contour_area(self):
  508. return self._contour_area
  509. @property
  510. def delta_alpha(self):
  511. return self._delta_alpha
  512. @property
  513. def frame_alpha(self):
  514. return self._frame_alpha
  515. @property
  516. def frame_height(self):
  517. return self._frame_height
  518. def to_dict(self):
  519. return {
  520. 'threshold': self.threshold,
  521. 'contour_area': self.contour_area,
  522. 'delta_alpha': self.delta_alpha,
  523. 'frame_alpha': self.frame_alpha,
  524. 'frame_height': self.frame_height,
  525. }
  526. class DetectConfig():
  527. def __init__(self, global_config, config, camera_fps):
  528. self._max_disappeared = config.get('max_disappeared', global_config.get('max_disappeared', camera_fps*2))
  529. @property
  530. def max_disappeared(self):
  531. return self._max_disappeared
  532. def to_dict(self):
  533. return {
  534. 'max_disappeared': self._max_disappeared,
  535. }
  536. class ZoneConfig():
  537. def __init__(self, name, config):
  538. self._coordinates = config['coordinates']
  539. self._filters = { name: FilterConfig(c) for name, c in config['filters'].items() }
  540. if isinstance(self._coordinates, list):
  541. self._contour = np.array([[int(p.split(',')[0]), int(p.split(',')[1])] for p in self._coordinates])
  542. elif isinstance(self._coordinates, str):
  543. points = self._coordinates.split(',')
  544. self._contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
  545. else:
  546. print(f"Unable to parse zone coordinates for {name}")
  547. self._contour = np.array([])
  548. self._color = (0,0,0)
  549. @property
  550. def coordinates(self):
  551. return self._coordinates
  552. @property
  553. def contour(self):
  554. return self._contour
  555. @contour.setter
  556. def contour(self, val):
  557. self._contour = val
  558. @property
  559. def color(self):
  560. return self._color
  561. @color.setter
  562. def color(self, val):
  563. self._color = val
  564. @property
  565. def filters(self):
  566. return self._filters
  567. def to_dict(self):
  568. return {
  569. 'filters': {k: f.to_dict() for k, f in self.filters.items()}
  570. }
  571. class CameraConfig():
  572. def __init__(self, name, config, global_config):
  573. self._name = name
  574. self._ffmpeg = CameraFfmpegConfig(global_config['ffmpeg'], config['ffmpeg'])
  575. self._height = config.get('height')
  576. self._width = config.get('width')
  577. self._frame_shape = (self._height, self._width)
  578. self._frame_shape_yuv = (self._frame_shape[0]*3//2, self._frame_shape[1])
  579. self._fps = config.get('fps')
  580. self._mask = self._create_mask(config.get('mask'))
  581. self._best_image_timeout = config['best_image_timeout']
  582. self._zones = { name: ZoneConfig(name, z) for name, z in config['zones'].items() }
  583. self._save_clips = CameraSaveClipsConfig(global_config, config['save_clips'])
  584. self._record = RecordConfig(global_config['record'], config['record'])
  585. self._rtmp = CameraRtmpConfig(global_config, config['rtmp'])
  586. self._snapshots = CameraSnapshotsConfig(config['snapshots'])
  587. self._objects = ObjectConfig(global_config['objects'], config.get('objects', {}))
  588. self._motion = MotionConfig(global_config['motion'], config['motion'], self._height)
  589. self._detect = DetectConfig(global_config['detect'], config['detect'], config.get('fps', 5))
  590. self._ffmpeg_cmds = []
  591. for ffmpeg_input in self._ffmpeg.inputs:
  592. ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
  593. if ffmpeg_cmd is None:
  594. continue
  595. self._ffmpeg_cmds.append({
  596. 'roles': ffmpeg_input.roles,
  597. 'cmd': ffmpeg_cmd
  598. })
  599. self._set_zone_colors(self._zones)
  600. def _create_mask(self, mask):
  601. mask_img = np.zeros(self.frame_shape, np.uint8)
  602. mask_img[:] = 255
  603. if isinstance(mask, list):
  604. for m in mask:
  605. self._add_mask(m, mask_img)
  606. elif isinstance(mask, str):
  607. self._add_mask(mask, mask_img)
  608. return mask_img
  609. def _add_mask(self, mask, mask_img):
  610. if mask.startswith('poly,'):
  611. points = mask.split(',')[1:]
  612. contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
  613. cv2.fillPoly(mask_img, pts=[contour], color=(0))
  614. else:
  615. mask_file = cv2.imread(f"/config/{mask}", cv2.IMREAD_GRAYSCALE)
  616. if not mask_file.size == 0:
  617. mask_img[np.where(mask_file==[0])] = [0]
  618. def _get_ffmpeg_cmd(self, ffmpeg_input):
  619. ffmpeg_output_args = []
  620. if 'detect' in ffmpeg_input.roles:
  621. ffmpeg_output_args = self.ffmpeg.output_args['detect'] + ffmpeg_output_args + ['pipe:']
  622. if self.fps:
  623. ffmpeg_output_args = ["-r", str(self.fps)] + ffmpeg_output_args
  624. if 'rtmp' in ffmpeg_input.roles and self.rtmp.enabled:
  625. ffmpeg_output_args = self.ffmpeg.output_args['rtmp'] + [
  626. f"rtmp://127.0.0.1/live/{self.name}"
  627. ] + ffmpeg_output_args
  628. if 'clips' in ffmpeg_input.roles and self.save_clips.enabled:
  629. ffmpeg_output_args = self.ffmpeg.output_args['clips'] + [
  630. f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"
  631. ] + ffmpeg_output_args
  632. if 'record' in ffmpeg_input.roles and self.record.enabled:
  633. ffmpeg_output_args = self.ffmpeg.output_args['record'] + [
  634. f"{os.path.join(RECORD_DIR, self.name)}-%Y%m%d%H%M%S.mp4"
  635. ] + ffmpeg_output_args
  636. # if there arent any outputs enabled for this input
  637. if len(ffmpeg_output_args) == 0:
  638. return None
  639. cmd = (['ffmpeg'] +
  640. ffmpeg_input.global_args +
  641. ffmpeg_input.hwaccel_args +
  642. ffmpeg_input.input_args +
  643. ['-i', ffmpeg_input.path] +
  644. ffmpeg_output_args)
  645. return [part for part in cmd if part != '']
  646. def _set_zone_colors(self, zones: Dict[str, ZoneConfig]):
  647. # set colors for zones
  648. all_zone_names = zones.keys()
  649. zone_colors = {}
  650. colors = plt.cm.get_cmap('tab10', len(all_zone_names))
  651. for i, zone in enumerate(all_zone_names):
  652. zone_colors[zone] = tuple(int(round(255 * c)) for c in colors(i)[:3])
  653. for name, zone in zones.items():
  654. zone.color = zone_colors[name]
  655. @property
  656. def name(self):
  657. return self._name
  658. @property
  659. def ffmpeg(self):
  660. return self._ffmpeg
  661. @property
  662. def height(self):
  663. return self._height
  664. @property
  665. def width(self):
  666. return self._width
  667. @property
  668. def fps(self):
  669. return self._fps
  670. @property
  671. def mask(self):
  672. return self._mask
  673. @property
  674. def best_image_timeout(self):
  675. return self._best_image_timeout
  676. @property
  677. def zones(self)-> Dict[str, ZoneConfig]:
  678. return self._zones
  679. @property
  680. def save_clips(self):
  681. return self._save_clips
  682. @property
  683. def record(self):
  684. return self._record
  685. @property
  686. def rtmp(self):
  687. return self._rtmp
  688. @property
  689. def snapshots(self):
  690. return self._snapshots
  691. @property
  692. def objects(self):
  693. return self._objects
  694. @property
  695. def motion(self):
  696. return self._motion
  697. @property
  698. def detect(self):
  699. return self._detect
  700. @property
  701. def frame_shape(self):
  702. return self._frame_shape
  703. @property
  704. def frame_shape_yuv(self):
  705. return self._frame_shape_yuv
  706. @property
  707. def ffmpeg_cmds(self):
  708. return self._ffmpeg_cmds
  709. def to_dict(self):
  710. return {
  711. 'name': self.name,
  712. 'height': self.height,
  713. 'width': self.width,
  714. 'fps': self.fps,
  715. 'best_image_timeout': self.best_image_timeout,
  716. 'zones': {k: z.to_dict() for k, z in self.zones.items()},
  717. 'save_clips': self.save_clips.to_dict(),
  718. 'record': self.record.to_dict(),
  719. 'rtmp': self.rtmp.to_dict(),
  720. 'snapshots': self.snapshots.to_dict(),
  721. 'objects': self.objects.to_dict(),
  722. 'motion': self.motion.to_dict(),
  723. 'detect': self.detect.to_dict(),
  724. 'frame_shape': self.frame_shape,
  725. 'ffmpeg_cmds': [{'roles': c['roles'], 'cmd': ' '.join(c['cmd'])} for c in self.ffmpeg_cmds],
  726. }
  727. class FrigateConfig():
  728. def __init__(self, config_file=None, config=None):
  729. if config is None and config_file is None:
  730. raise ValueError('config or config_file must be defined')
  731. elif not config_file is None:
  732. config = self._load_file(config_file)
  733. config = FRIGATE_CONFIG_SCHEMA(config)
  734. config = self._sub_env_vars(config)
  735. self._database = DatabaseConfig(config['database'])
  736. self._model = ModelConfig(config['model'])
  737. self._detectors = { name: DetectorConfig(d) for name, d in config['detectors'].items() }
  738. self._mqtt = MqttConfig(config['mqtt'])
  739. self._save_clips = SaveClipsConfig(config['save_clips'])
  740. self._cameras = { name: CameraConfig(name, c, config) for name, c in config['cameras'].items() }
  741. self._logger = LoggerConfig(config['logger'])
  742. def _sub_env_vars(self, config):
  743. frigate_env_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')}
  744. if 'password' in config['mqtt']:
  745. config['mqtt']['password'] = config['mqtt']['password'].format(**frigate_env_vars)
  746. for camera in config['cameras'].values():
  747. for i in camera['ffmpeg']['inputs']:
  748. i['path'] = i['path'].format(**frigate_env_vars)
  749. return config
  750. def _load_file(self, config_file):
  751. with open(config_file) as f:
  752. raw_config = f.read()
  753. if config_file.endswith(".yml"):
  754. config = yaml.safe_load(raw_config)
  755. elif config_file.endswith(".json"):
  756. config = json.loads(raw_config)
  757. return config
  758. def to_dict(self):
  759. return {
  760. 'database': self.database.to_dict(),
  761. 'model': self.model.to_dict(),
  762. 'detectors': {k: d.to_dict() for k, d in self.detectors.items()},
  763. 'mqtt': self.mqtt.to_dict(),
  764. 'save_clips': self.save_clips.to_dict(),
  765. 'cameras': {k: c.to_dict() for k, c in self.cameras.items()},
  766. 'logger': self.logger.to_dict()
  767. }
  768. @property
  769. def database(self):
  770. return self._database
  771. @property
  772. def model(self):
  773. return self._model
  774. @property
  775. def detectors(self) -> Dict[str, DetectorConfig]:
  776. return self._detectors
  777. @property
  778. def logger(self):
  779. return self._logger
  780. @property
  781. def mqtt(self):
  782. return self._mqtt
  783. @property
  784. def save_clips(self):
  785. return self._save_clips
  786. @property
  787. def cameras(self) -> Dict[str, CameraConfig]:
  788. return self._cameras