config.py 32 KB

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