object_processing.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. import copy
  2. import base64
  3. import datetime
  4. import hashlib
  5. import itertools
  6. import json
  7. import logging
  8. import os
  9. import queue
  10. import threading
  11. import time
  12. from collections import Counter, defaultdict
  13. from statistics import mean, median
  14. from typing import Callable, Dict
  15. import cv2
  16. import matplotlib.pyplot as plt
  17. import numpy as np
  18. from frigate.config import FrigateConfig, CameraConfig
  19. from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
  20. from frigate.edgetpu import load_labels
  21. from frigate.util import (
  22. SharedMemoryFrameManager,
  23. draw_box_with_label,
  24. draw_timestamp,
  25. calculate_region,
  26. )
  27. logger = logging.getLogger(__name__)
  28. PATH_TO_LABELS = "/labelmap.txt"
  29. LABELS = load_labels(PATH_TO_LABELS)
  30. cmap = plt.cm.get_cmap("tab10", len(LABELS.keys()))
  31. COLOR_MAP = {}
  32. for key, val in LABELS.items():
  33. COLOR_MAP[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3])
  34. def on_edge(box, frame_shape):
  35. if (
  36. box[0] == 0
  37. or box[1] == 0
  38. or box[2] == frame_shape[1] - 1
  39. or box[3] == frame_shape[0] - 1
  40. ):
  41. return True
  42. def is_better_thumbnail(current_thumb, new_obj, frame_shape) -> bool:
  43. # larger is better
  44. # cutoff images are less ideal, but they should also be smaller?
  45. # better scores are obviously better too
  46. # if the new_thumb is on an edge, and the current thumb is not
  47. if on_edge(new_obj["box"], frame_shape) and not on_edge(
  48. current_thumb["box"], frame_shape
  49. ):
  50. return False
  51. # if the score is better by more than 5%
  52. if new_obj["score"] > current_thumb["score"] + 0.05:
  53. return True
  54. # if the area is 10% larger
  55. if new_obj["area"] > current_thumb["area"] * 1.1:
  56. return True
  57. return False
  58. class TrackedObject:
  59. def __init__(self, camera, camera_config: CameraConfig, frame_cache, obj_data):
  60. self.obj_data = obj_data
  61. self.camera = camera
  62. self.camera_config = camera_config
  63. self.frame_cache = frame_cache
  64. self.current_zones = []
  65. self.entered_zones = set()
  66. self.false_positive = True
  67. self.top_score = self.computed_score = 0.0
  68. self.thumbnail_data = None
  69. self.last_updated = 0
  70. self.last_published = 0
  71. self.frame = None
  72. self.previous = self.to_dict()
  73. # start the score history
  74. self.score_history = [self.obj_data["score"]]
  75. def _is_false_positive(self):
  76. # once a true positive, always a true positive
  77. if not self.false_positive:
  78. return False
  79. threshold = self.camera_config.objects.filters[self.obj_data["label"]].threshold
  80. return self.computed_score < threshold
  81. def compute_score(self):
  82. scores = self.score_history[:]
  83. # pad with zeros if you dont have at least 3 scores
  84. if len(scores) < 3:
  85. scores += [0.0] * (3 - len(scores))
  86. return median(scores)
  87. def update(self, current_frame_time, obj_data):
  88. significant_update = False
  89. self.obj_data.update(obj_data)
  90. # if the object is not in the current frame, add a 0.0 to the score history
  91. if self.obj_data["frame_time"] != current_frame_time:
  92. self.score_history.append(0.0)
  93. else:
  94. self.score_history.append(self.obj_data["score"])
  95. # only keep the last 10 scores
  96. if len(self.score_history) > 10:
  97. self.score_history = self.score_history[-10:]
  98. # calculate if this is a false positive
  99. self.computed_score = self.compute_score()
  100. if self.computed_score > self.top_score:
  101. self.top_score = self.computed_score
  102. self.false_positive = self._is_false_positive()
  103. if not self.false_positive:
  104. # determine if this frame is a better thumbnail
  105. if self.thumbnail_data is None or is_better_thumbnail(
  106. self.thumbnail_data, self.obj_data, self.camera_config.frame_shape
  107. ):
  108. self.thumbnail_data = {
  109. "frame_time": self.obj_data["frame_time"],
  110. "box": self.obj_data["box"],
  111. "area": self.obj_data["area"],
  112. "region": self.obj_data["region"],
  113. "score": self.obj_data["score"],
  114. }
  115. significant_update = True
  116. # check zones
  117. current_zones = []
  118. bottom_center = (self.obj_data["centroid"][0], self.obj_data["box"][3])
  119. # check each zone
  120. for name, zone in self.camera_config.zones.items():
  121. contour = zone.contour
  122. # check if the object is in the zone
  123. if cv2.pointPolygonTest(contour, bottom_center, False) >= 0:
  124. # if the object passed the filters once, dont apply again
  125. if name in self.current_zones or not zone_filtered(self, zone.filters):
  126. current_zones.append(name)
  127. self.entered_zones.add(name)
  128. # if the zones changed, signal an update
  129. if not self.false_positive and set(self.current_zones) != set(current_zones):
  130. significant_update = True
  131. self.current_zones = current_zones
  132. return significant_update
  133. def to_dict(self, include_thumbnail: bool = False):
  134. snapshot_time = (
  135. self.thumbnail_data["frame_time"]
  136. if not self.thumbnail_data is None
  137. else 0.0
  138. )
  139. event = {
  140. "id": self.obj_data["id"],
  141. "camera": self.camera,
  142. "frame_time": self.obj_data["frame_time"],
  143. "snapshot_time": snapshot_time,
  144. "label": self.obj_data["label"],
  145. "top_score": self.top_score,
  146. "false_positive": self.false_positive,
  147. "start_time": self.obj_data["start_time"],
  148. "end_time": self.obj_data.get("end_time", None),
  149. "score": self.obj_data["score"],
  150. "box": self.obj_data["box"],
  151. "area": self.obj_data["area"],
  152. "region": self.obj_data["region"],
  153. "current_zones": self.current_zones.copy(),
  154. "entered_zones": list(self.entered_zones).copy(),
  155. }
  156. if include_thumbnail:
  157. event["thumbnail"] = base64.b64encode(self.get_thumbnail()).decode("utf-8")
  158. return event
  159. def get_thumbnail(self):
  160. if (
  161. self.thumbnail_data is None
  162. or self.thumbnail_data["frame_time"] not in self.frame_cache
  163. ):
  164. ret, jpg = cv2.imencode(".jpg", np.zeros((175, 175, 3), np.uint8))
  165. jpg_bytes = self.get_jpg_bytes(
  166. timestamp=False, bounding_box=False, crop=True, height=175
  167. )
  168. if jpg_bytes:
  169. return jpg_bytes
  170. else:
  171. ret, jpg = cv2.imencode(".jpg", np.zeros((175, 175, 3), np.uint8))
  172. return jpg.tobytes()
  173. def get_clean_png(self):
  174. if self.thumbnail_data is None:
  175. return None
  176. try:
  177. best_frame = cv2.cvtColor(
  178. self.frame_cache[self.thumbnail_data["frame_time"]],
  179. cv2.COLOR_YUV2BGR_I420,
  180. )
  181. except KeyError:
  182. logger.warning(
  183. f"Unable to create clean png because frame {self.thumbnail_data['frame_time']} is not in the cache"
  184. )
  185. return None
  186. ret, png = cv2.imencode(".png", best_frame)
  187. if ret:
  188. return png.tobytes()
  189. else:
  190. return None
  191. def get_jpg_bytes(
  192. self, timestamp=False, bounding_box=False, crop=False, height=None
  193. ):
  194. if self.thumbnail_data is None:
  195. return None
  196. try:
  197. best_frame = cv2.cvtColor(
  198. self.frame_cache[self.thumbnail_data["frame_time"]],
  199. cv2.COLOR_YUV2BGR_I420,
  200. )
  201. except KeyError:
  202. logger.warning(
  203. f"Unable to create jpg because frame {self.thumbnail_data['frame_time']} is not in the cache"
  204. )
  205. return None
  206. if bounding_box:
  207. thickness = 2
  208. color = COLOR_MAP[self.obj_data["label"]]
  209. # draw the bounding boxes on the frame
  210. box = self.thumbnail_data["box"]
  211. draw_box_with_label(
  212. best_frame,
  213. box[0],
  214. box[1],
  215. box[2],
  216. box[3],
  217. self.obj_data["label"],
  218. f"{int(self.thumbnail_data['score']*100)}% {int(self.thumbnail_data['area'])}",
  219. thickness=thickness,
  220. color=color,
  221. )
  222. if crop:
  223. box = self.thumbnail_data["box"]
  224. region = calculate_region(
  225. best_frame.shape, box[0], box[1], box[2], box[3], 1.1
  226. )
  227. best_frame = best_frame[region[1] : region[3], region[0] : region[2]]
  228. if height:
  229. width = int(height * best_frame.shape[1] / best_frame.shape[0])
  230. best_frame = cv2.resize(
  231. best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA
  232. )
  233. if timestamp:
  234. logger.info("Jetzad")
  235. draw_timestamp(
  236. best_frame,
  237. self.thumbnail_data["frame_time"],
  238. self.camera_config.timestamp_style.format,
  239. font_effect=self.camera_config.timestamp_style.effect,
  240. font_scale=self.camera_config.timestamp_style.scale,
  241. font_thickness=self.camera_config.timestamp_style.thickness,
  242. font_color=self.camera_config.timestamp_style.color,
  243. position=self.camera_config.timestamp_style.position,
  244. )
  245. ret, jpg = cv2.imencode(".jpg", best_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
  246. if ret:
  247. return jpg.tobytes()
  248. else:
  249. return None
  250. def zone_filtered(obj: TrackedObject, object_config):
  251. object_name = obj.obj_data["label"]
  252. if object_name in object_config:
  253. obj_settings = object_config[object_name]
  254. # if the min area is larger than the
  255. # detected object, don't add it to detected objects
  256. if obj_settings.min_area > obj.obj_data["area"]:
  257. return True
  258. # if the detected object is larger than the
  259. # max area, don't add it to detected objects
  260. if obj_settings.max_area < obj.obj_data["area"]:
  261. return True
  262. # if the score is lower than the threshold, skip
  263. if obj_settings.threshold > obj.computed_score:
  264. return True
  265. return False
  266. # Maintains the state of a camera
  267. class CameraState:
  268. def __init__(self, name, config, frame_manager):
  269. self.name = name
  270. self.config = config
  271. self.camera_config = config.cameras[name]
  272. self.frame_manager = frame_manager
  273. self.best_objects: Dict[str, TrackedObject] = {}
  274. self.object_counts = defaultdict(int)
  275. self.tracked_objects: Dict[str, TrackedObject] = {}
  276. self.frame_cache = {}
  277. self.zone_objects = defaultdict(list)
  278. self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8)
  279. self.current_frame_lock = threading.Lock()
  280. self.current_frame_time = 0.0
  281. self.motion_boxes = []
  282. self.regions = []
  283. self.previous_frame_id = None
  284. self.callbacks = defaultdict(list)
  285. def get_current_frame(self, draw_options={}):
  286. with self.current_frame_lock:
  287. frame_copy = np.copy(self._current_frame)
  288. frame_time = self.current_frame_time
  289. tracked_objects = {k: v.to_dict() for k, v in self.tracked_objects.items()}
  290. motion_boxes = self.motion_boxes.copy()
  291. regions = self.regions.copy()
  292. frame_copy = cv2.cvtColor(frame_copy, cv2.COLOR_YUV2BGR_I420)
  293. # draw on the frame
  294. if draw_options.get("bounding_boxes"):
  295. # draw the bounding boxes on the frame
  296. for obj in tracked_objects.values():
  297. if obj["frame_time"] == frame_time:
  298. thickness = 2
  299. color = COLOR_MAP[obj["label"]]
  300. else:
  301. thickness = 1
  302. color = (255, 0, 0)
  303. # draw the bounding boxes on the frame
  304. box = obj["box"]
  305. draw_box_with_label(
  306. frame_copy,
  307. box[0],
  308. box[1],
  309. box[2],
  310. box[3],
  311. obj["label"],
  312. f"{obj['score']:.0%} {int(obj['area'])}",
  313. thickness=thickness,
  314. color=color,
  315. )
  316. if draw_options.get("regions"):
  317. for region in regions:
  318. cv2.rectangle(
  319. frame_copy,
  320. (region[0], region[1]),
  321. (region[2], region[3]),
  322. (0, 255, 0),
  323. 2,
  324. )
  325. if draw_options.get("zones"):
  326. for name, zone in self.camera_config.zones.items():
  327. thickness = (
  328. 8
  329. if any(
  330. name in obj["current_zones"] for obj in tracked_objects.values()
  331. )
  332. else 2
  333. )
  334. cv2.drawContours(frame_copy, [zone.contour], -1, zone.color, thickness)
  335. if draw_options.get("mask"):
  336. mask_overlay = np.where(self.camera_config.motion.mask == [0])
  337. frame_copy[mask_overlay] = [0, 0, 0]
  338. if draw_options.get("motion_boxes"):
  339. for m_box in motion_boxes:
  340. cv2.rectangle(
  341. frame_copy,
  342. (m_box[0], m_box[1]),
  343. (m_box[2], m_box[3]),
  344. (0, 0, 255),
  345. 2,
  346. )
  347. if draw_options.get("timestamp"):
  348. draw_timestamp(
  349. frame_copy,
  350. frame_time,
  351. self.camera_config.timestamp_style.format,
  352. font_effect=self.camera_config.timestamp_style.effect,
  353. font_scale=self.camera_config.timestamp_style.scale,
  354. font_thickness=self.camera_config.timestamp_style.thickness,
  355. font_color=self.camera_config.timestamp_style.color,
  356. position=self.camera_config.timestamp_style.position,
  357. )
  358. return frame_copy
  359. def finished(self, obj_id):
  360. del self.tracked_objects[obj_id]
  361. def on(self, event_type: str, callback: Callable[[Dict], None]):
  362. self.callbacks[event_type].append(callback)
  363. def update(self, frame_time, current_detections, motion_boxes, regions):
  364. # get the new frame
  365. frame_id = f"{self.name}{frame_time}"
  366. current_frame = self.frame_manager.get(
  367. frame_id, self.camera_config.frame_shape_yuv
  368. )
  369. tracked_objects = self.tracked_objects.copy()
  370. current_ids = set(current_detections.keys())
  371. previous_ids = set(tracked_objects.keys())
  372. removed_ids = previous_ids.difference(current_ids)
  373. new_ids = current_ids.difference(previous_ids)
  374. updated_ids = current_ids.intersection(previous_ids)
  375. for id in new_ids:
  376. new_obj = tracked_objects[id] = TrackedObject(
  377. self.name, self.camera_config, self.frame_cache, current_detections[id]
  378. )
  379. # call event handlers
  380. for c in self.callbacks["start"]:
  381. c(self.name, new_obj, frame_time)
  382. for id in updated_ids:
  383. updated_obj = tracked_objects[id]
  384. significant_update = updated_obj.update(frame_time, current_detections[id])
  385. if significant_update:
  386. # ensure this frame is stored in the cache
  387. if (
  388. updated_obj.thumbnail_data["frame_time"] == frame_time
  389. and frame_time not in self.frame_cache
  390. ):
  391. self.frame_cache[frame_time] = np.copy(current_frame)
  392. updated_obj.last_updated = frame_time
  393. # if it has been more than 5 seconds since the last publish
  394. # and the last update is greater than the last publish
  395. if (
  396. frame_time - updated_obj.last_published > 5
  397. and updated_obj.last_updated > updated_obj.last_published
  398. ):
  399. # call event handlers
  400. for c in self.callbacks["update"]:
  401. c(self.name, updated_obj, frame_time)
  402. updated_obj.last_published = frame_time
  403. for id in removed_ids:
  404. # publish events to mqtt
  405. removed_obj = tracked_objects[id]
  406. if not "end_time" in removed_obj.obj_data:
  407. removed_obj.obj_data["end_time"] = frame_time
  408. for c in self.callbacks["end"]:
  409. c(self.name, removed_obj, frame_time)
  410. # TODO: can i switch to looking this up and only changing when an event ends?
  411. # maintain best objects
  412. for obj in tracked_objects.values():
  413. object_type = obj.obj_data["label"]
  414. # if the object's thumbnail is not from the current frame
  415. if obj.false_positive or obj.thumbnail_data["frame_time"] != frame_time:
  416. continue
  417. if object_type in self.best_objects:
  418. current_best = self.best_objects[object_type]
  419. now = datetime.datetime.now().timestamp()
  420. # if the object is a higher score than the current best score
  421. # or the current object is older than desired, use the new object
  422. if (
  423. is_better_thumbnail(
  424. current_best.thumbnail_data,
  425. obj.thumbnail_data,
  426. self.camera_config.frame_shape,
  427. )
  428. or (now - current_best.thumbnail_data["frame_time"])
  429. > self.camera_config.best_image_timeout
  430. ):
  431. self.best_objects[object_type] = obj
  432. for c in self.callbacks["snapshot"]:
  433. c(self.name, self.best_objects[object_type], frame_time)
  434. else:
  435. self.best_objects[object_type] = obj
  436. for c in self.callbacks["snapshot"]:
  437. c(self.name, self.best_objects[object_type], frame_time)
  438. # update overall camera state for each object type
  439. obj_counter = Counter(
  440. obj.obj_data["label"]
  441. for obj in tracked_objects.values()
  442. if not obj.false_positive
  443. )
  444. # report on detected objects
  445. for obj_name, count in obj_counter.items():
  446. if count != self.object_counts[obj_name]:
  447. self.object_counts[obj_name] = count
  448. for c in self.callbacks["object_status"]:
  449. c(self.name, obj_name, count)
  450. # expire any objects that are >0 and no longer detected
  451. expired_objects = [
  452. obj_name
  453. for obj_name, count in self.object_counts.items()
  454. if count > 0 and obj_name not in obj_counter
  455. ]
  456. for obj_name in expired_objects:
  457. self.object_counts[obj_name] = 0
  458. for c in self.callbacks["object_status"]:
  459. c(self.name, obj_name, 0)
  460. for c in self.callbacks["snapshot"]:
  461. c(self.name, self.best_objects[obj_name], frame_time)
  462. # cleanup thumbnail frame cache
  463. current_thumb_frames = {
  464. obj.thumbnail_data["frame_time"]
  465. for obj in tracked_objects.values()
  466. if not obj.false_positive
  467. }
  468. current_best_frames = {
  469. obj.thumbnail_data["frame_time"] for obj in self.best_objects.values()
  470. }
  471. thumb_frames_to_delete = [
  472. t
  473. for t in self.frame_cache.keys()
  474. if t not in current_thumb_frames and t not in current_best_frames
  475. ]
  476. for t in thumb_frames_to_delete:
  477. del self.frame_cache[t]
  478. with self.current_frame_lock:
  479. self.tracked_objects = tracked_objects
  480. self.current_frame_time = frame_time
  481. self.motion_boxes = motion_boxes
  482. self.regions = regions
  483. self._current_frame = current_frame
  484. if self.previous_frame_id is not None:
  485. self.frame_manager.close(self.previous_frame_id)
  486. self.previous_frame_id = frame_id
  487. class TrackedObjectProcessor(threading.Thread):
  488. def __init__(
  489. self,
  490. config: FrigateConfig,
  491. client,
  492. topic_prefix,
  493. tracked_objects_queue,
  494. event_queue,
  495. event_processed_queue,
  496. video_output_queue,
  497. stop_event,
  498. ):
  499. threading.Thread.__init__(self)
  500. self.name = "detected_frames_processor"
  501. self.config = config
  502. self.client = client
  503. self.topic_prefix = topic_prefix
  504. self.tracked_objects_queue = tracked_objects_queue
  505. self.event_queue = event_queue
  506. self.event_processed_queue = event_processed_queue
  507. self.video_output_queue = video_output_queue
  508. self.stop_event = stop_event
  509. self.camera_states: Dict[str, CameraState] = {}
  510. self.frame_manager = SharedMemoryFrameManager()
  511. def start(camera, obj: TrackedObject, current_frame_time):
  512. self.event_queue.put(("start", camera, obj.to_dict()))
  513. def update(camera, obj: TrackedObject, current_frame_time):
  514. after = obj.to_dict()
  515. message = {
  516. "before": obj.previous,
  517. "after": after,
  518. "type": "new" if obj.previous["false_positive"] else "update",
  519. }
  520. self.client.publish(
  521. f"{self.topic_prefix}/events", json.dumps(message), retain=False
  522. )
  523. obj.previous = after
  524. def end(camera, obj: TrackedObject, current_frame_time):
  525. snapshot_config = self.config.cameras[camera].snapshots
  526. event_data = obj.to_dict(include_thumbnail=True)
  527. event_data["has_snapshot"] = False
  528. if not obj.false_positive:
  529. message = {
  530. "before": obj.previous,
  531. "after": obj.to_dict(),
  532. "type": "end",
  533. }
  534. self.client.publish(
  535. f"{self.topic_prefix}/events", json.dumps(message), retain=False
  536. )
  537. # write snapshot to disk if enabled
  538. if snapshot_config.enabled and self.should_save_snapshot(camera, obj):
  539. jpg_bytes = obj.get_jpg_bytes(
  540. timestamp=snapshot_config.timestamp,
  541. bounding_box=snapshot_config.bounding_box,
  542. crop=snapshot_config.crop,
  543. height=snapshot_config.height,
  544. )
  545. if jpg_bytes is None:
  546. logger.warning(
  547. f"Unable to save snapshot for {obj.obj_data['id']}."
  548. )
  549. else:
  550. with open(
  551. os.path.join(
  552. CLIPS_DIR, f"{camera}-{obj.obj_data['id']}.jpg"
  553. ),
  554. "wb",
  555. ) as j:
  556. j.write(jpg_bytes)
  557. event_data["has_snapshot"] = True
  558. # write clean snapshot if enabled
  559. if snapshot_config.clean_copy:
  560. png_bytes = obj.get_clean_png()
  561. if png_bytes is None:
  562. logger.warning(
  563. f"Unable to save clean snapshot for {obj.obj_data['id']}."
  564. )
  565. else:
  566. with open(
  567. os.path.join(
  568. CLIPS_DIR,
  569. f"{camera}-{obj.obj_data['id']}-clean.png",
  570. ),
  571. "wb",
  572. ) as p:
  573. p.write(png_bytes)
  574. self.event_queue.put(("end", camera, event_data))
  575. def snapshot(camera, obj: TrackedObject, current_frame_time):
  576. mqtt_config = self.config.cameras[camera].mqtt
  577. if mqtt_config.enabled and self.should_mqtt_snapshot(camera, obj):
  578. jpg_bytes = obj.get_jpg_bytes(
  579. timestamp=mqtt_config.timestamp,
  580. bounding_box=mqtt_config.bounding_box,
  581. crop=mqtt_config.crop,
  582. height=mqtt_config.height,
  583. )
  584. if jpg_bytes is None:
  585. logger.warning(
  586. f"Unable to send mqtt snapshot for {obj.obj_data['id']}."
  587. )
  588. else:
  589. self.client.publish(
  590. f"{self.topic_prefix}/{camera}/{obj.obj_data['label']}/snapshot",
  591. jpg_bytes,
  592. retain=True,
  593. )
  594. def object_status(camera, object_name, status):
  595. self.client.publish(
  596. f"{self.topic_prefix}/{camera}/{object_name}", status, retain=False
  597. )
  598. for camera in self.config.cameras.keys():
  599. camera_state = CameraState(camera, self.config, self.frame_manager)
  600. camera_state.on("start", start)
  601. camera_state.on("update", update)
  602. camera_state.on("end", end)
  603. camera_state.on("snapshot", snapshot)
  604. camera_state.on("object_status", object_status)
  605. self.camera_states[camera] = camera_state
  606. # {
  607. # 'zone_name': {
  608. # 'person': {
  609. # 'camera_1': 2,
  610. # 'camera_2': 1
  611. # }
  612. # }
  613. # }
  614. self.zone_data = defaultdict(lambda: defaultdict(dict))
  615. def should_save_snapshot(self, camera, obj: TrackedObject):
  616. # if there are required zones and there is no overlap
  617. required_zones = self.config.cameras[camera].snapshots.required_zones
  618. if len(required_zones) > 0 and not obj.entered_zones & set(required_zones):
  619. logger.debug(
  620. f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones"
  621. )
  622. return False
  623. return True
  624. def should_mqtt_snapshot(self, camera, obj: TrackedObject):
  625. # if there are required zones and there is no overlap
  626. required_zones = self.config.cameras[camera].mqtt.required_zones
  627. if len(required_zones) > 0 and not obj.entered_zones & set(required_zones):
  628. logger.debug(
  629. f"Not sending mqtt for {obj.obj_data['id']} because it did not enter required zones"
  630. )
  631. return False
  632. return True
  633. def get_best(self, camera, label):
  634. # TODO: need a lock here
  635. camera_state = self.camera_states[camera]
  636. if label in camera_state.best_objects:
  637. best_obj = camera_state.best_objects[label]
  638. best = best_obj.thumbnail_data.copy()
  639. best["frame"] = camera_state.frame_cache.get(
  640. best_obj.thumbnail_data["frame_time"]
  641. )
  642. return best
  643. else:
  644. return {}
  645. def get_current_frame(self, camera, draw_options={}):
  646. return self.camera_states[camera].get_current_frame(draw_options)
  647. def run(self):
  648. while not self.stop_event.is_set():
  649. try:
  650. (
  651. camera,
  652. frame_time,
  653. current_tracked_objects,
  654. motion_boxes,
  655. regions,
  656. ) = self.tracked_objects_queue.get(True, 10)
  657. except queue.Empty:
  658. continue
  659. camera_state = self.camera_states[camera]
  660. camera_state.update(
  661. frame_time, current_tracked_objects, motion_boxes, regions
  662. )
  663. self.video_output_queue.put(
  664. (
  665. camera,
  666. frame_time,
  667. current_tracked_objects,
  668. motion_boxes,
  669. regions,
  670. )
  671. )
  672. # update zone counts for each label
  673. # for each zone in the current camera
  674. for zone in self.config.cameras[camera].zones.keys():
  675. # count labels for the camera in the zone
  676. obj_counter = Counter(
  677. obj.obj_data["label"]
  678. for obj in camera_state.tracked_objects.values()
  679. if zone in obj.current_zones and not obj.false_positive
  680. )
  681. # update counts and publish status
  682. for label in set(self.zone_data[zone].keys()) | set(obj_counter.keys()):
  683. # if we have previously published a count for this zone/label
  684. zone_label = self.zone_data[zone][label]
  685. if camera in zone_label:
  686. current_count = sum(zone_label.values())
  687. zone_label[camera] = (
  688. obj_counter[label] if label in obj_counter else 0
  689. )
  690. new_count = sum(zone_label.values())
  691. if new_count != current_count:
  692. self.client.publish(
  693. f"{self.topic_prefix}/{zone}/{label}",
  694. new_count,
  695. retain=False,
  696. )
  697. # if this is a new zone/label combo for this camera
  698. else:
  699. if label in obj_counter:
  700. zone_label[camera] = obj_counter[label]
  701. self.client.publish(
  702. f"{self.topic_prefix}/{zone}/{label}",
  703. obj_counter[label],
  704. retain=False,
  705. )
  706. # cleanup event finished queue
  707. while not self.event_processed_queue.empty():
  708. event_id, camera, clip_created = self.event_processed_queue.get()
  709. if clip_created:
  710. obj = self.camera_states[camera].tracked_objects[event_id]
  711. message = {
  712. "before": obj.previous,
  713. "after": obj.to_dict(),
  714. "type": "clip_ready",
  715. }
  716. self.client.publish(
  717. f"{self.topic_prefix}/events", json.dumps(message), retain=False
  718. )
  719. self.camera_states[camera].finished(event_id)
  720. logger.info(f"Exiting object processor...")