object_processing.py 28 KB

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