objects.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import copy
  2. import datetime
  3. import itertools
  4. import multiprocessing as mp
  5. import random
  6. import string
  7. import threading
  8. import time
  9. from collections import defaultdict
  10. import cv2
  11. import numpy as np
  12. from scipy.spatial import distance as dist
  13. from frigate.config import DetectConfig
  14. from frigate.util import intersection_over_union
  15. class ObjectTracker:
  16. def __init__(self, config: DetectConfig):
  17. self.tracked_objects = {}
  18. self.disappeared = {}
  19. self.positions = {}
  20. self.max_disappeared = config.max_disappeared
  21. self.detect_config = config
  22. def register(self, index, obj):
  23. rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
  24. id = f"{obj['frame_time']}-{rand_id}"
  25. obj["id"] = id
  26. obj["start_time"] = obj["frame_time"]
  27. obj["motionless_count"] = 0
  28. obj["position_changes"] = 0
  29. self.tracked_objects[id] = obj
  30. self.disappeared[id] = 0
  31. self.positions[id] = {
  32. "xmins": [],
  33. "ymins": [],
  34. "xmaxs": [],
  35. "ymaxs": [],
  36. "xmin": 0,
  37. "ymin": 0,
  38. "xmax": self.detect_config.width,
  39. "ymax": self.detect_config.height,
  40. }
  41. def deregister(self, id):
  42. del self.tracked_objects[id]
  43. del self.disappeared[id]
  44. # tracks the current position of the object based on the last N bounding boxes
  45. # returns False if the object has moved outside its previous position
  46. def update_position(self, id, box):
  47. position = self.positions[id]
  48. position_box = (
  49. position["xmin"],
  50. position["ymin"],
  51. position["xmax"],
  52. position["ymax"],
  53. )
  54. xmin, ymin, xmax, ymax = box
  55. iou = intersection_over_union(position_box, box)
  56. # if the iou drops below the threshold
  57. # assume the object has moved to a new position and reset the computed box
  58. if iou < 0.6:
  59. self.positions[id] = {
  60. "xmins": [xmin],
  61. "ymins": [ymin],
  62. "xmaxs": [xmax],
  63. "ymaxs": [ymax],
  64. "xmin": xmin,
  65. "ymin": ymin,
  66. "xmax": xmax,
  67. "ymax": ymax,
  68. }
  69. return False
  70. # if there are less than stationary_threshold entries for the position, add the bounding box
  71. # and recompute the position box
  72. if len(position["xmins"]) < self.detect_config.stationary_threshold:
  73. position["xmins"].append(xmin)
  74. position["ymins"].append(ymin)
  75. position["xmaxs"].append(xmax)
  76. position["ymaxs"].append(ymax)
  77. # by using percentiles here, we hopefully remove outliers
  78. position["xmin"] = np.percentile(position["xmins"], 15)
  79. position["ymin"] = np.percentile(position["ymins"], 15)
  80. position["xmax"] = np.percentile(position["xmaxs"], 85)
  81. position["ymax"] = np.percentile(position["ymaxs"], 85)
  82. return True
  83. def update(self, id, new_obj):
  84. self.disappeared[id] = 0
  85. # update the motionless count if the object has not moved to a new position
  86. if self.update_position(id, new_obj["box"]):
  87. self.tracked_objects[id]["motionless_count"] += 1
  88. else:
  89. self.tracked_objects[id]["motionless_count"] = 0
  90. self.tracked_objects[id]["position_changes"] += 1
  91. self.tracked_objects[id].update(new_obj)
  92. def update_frame_times(self, frame_time):
  93. for id in self.tracked_objects.keys():
  94. self.tracked_objects[id]["frame_time"] = frame_time
  95. self.tracked_objects[id]["motionless_count"] += 1
  96. def match_and_update(self, frame_time, new_objects):
  97. # group by name
  98. new_object_groups = defaultdict(lambda: [])
  99. for obj in new_objects:
  100. new_object_groups[obj[0]].append(
  101. {
  102. "label": obj[0],
  103. "score": obj[1],
  104. "box": obj[2],
  105. "area": obj[3],
  106. "region": obj[4],
  107. "frame_time": frame_time,
  108. }
  109. )
  110. # update any tracked objects with labels that are not
  111. # seen in the current objects and deregister if needed
  112. for obj in list(self.tracked_objects.values()):
  113. if not obj["label"] in new_object_groups:
  114. if self.disappeared[obj["id"]] >= self.max_disappeared:
  115. self.deregister(obj["id"])
  116. else:
  117. self.disappeared[obj["id"]] += 1
  118. if len(new_objects) == 0:
  119. return
  120. # track objects for each label type
  121. for label, group in new_object_groups.items():
  122. current_objects = [
  123. o for o in self.tracked_objects.values() if o["label"] == label
  124. ]
  125. current_ids = [o["id"] for o in current_objects]
  126. current_centroids = np.array([o["centroid"] for o in current_objects])
  127. # compute centroids of new objects
  128. for obj in group:
  129. centroid_x = int((obj["box"][0] + obj["box"][2]) / 2.0)
  130. centroid_y = int((obj["box"][1] + obj["box"][3]) / 2.0)
  131. obj["centroid"] = (centroid_x, centroid_y)
  132. if len(current_objects) == 0:
  133. for index, obj in enumerate(group):
  134. self.register(index, obj)
  135. continue
  136. new_centroids = np.array([o["centroid"] for o in group])
  137. # compute the distance between each pair of tracked
  138. # centroids and new centroids, respectively -- our
  139. # goal will be to match each current centroid to a new
  140. # object centroid
  141. D = dist.cdist(current_centroids, new_centroids)
  142. # in order to perform this matching we must (1) find the smallest
  143. # value in each row (i.e. the distance from each current object to
  144. # the closest new object) and then (2) sort the row indexes based
  145. # on their minimum values so that the row with the smallest
  146. # distance (the best match) is at the *front* of the index list
  147. rows = D.min(axis=1).argsort()
  148. # next, we determine which new object each existing object matched
  149. # against, and apply the same sorting as was applied previously
  150. cols = D.argmin(axis=1)[rows]
  151. # many current objects may register with each new object, so only
  152. # match the closest ones. unique returns the indices of the first
  153. # occurrences of each value, and because the rows are sorted by
  154. # distance, this will be index of the closest match
  155. _, index = np.unique(cols, return_index=True)
  156. rows = rows[index]
  157. cols = cols[index]
  158. # loop over the combination of the (row, column) index tuples
  159. for row, col in zip(rows, cols):
  160. # grab the object ID for the current row, set its new centroid,
  161. # and reset the disappeared counter
  162. objectID = current_ids[row]
  163. self.update(objectID, group[col])
  164. # compute the row and column indices we have NOT yet examined
  165. unusedRows = set(range(D.shape[0])).difference(rows)
  166. unusedCols = set(range(D.shape[1])).difference(cols)
  167. # in the event that the number of object centroids is
  168. # equal or greater than the number of input centroids
  169. # we need to check and see if some of these objects have
  170. # potentially disappeared
  171. if D.shape[0] >= D.shape[1]:
  172. for row in unusedRows:
  173. id = current_ids[row]
  174. if self.disappeared[id] >= self.max_disappeared:
  175. self.deregister(id)
  176. else:
  177. self.disappeared[id] += 1
  178. # if the number of input centroids is greater
  179. # than the number of existing object centroids we need to
  180. # register each new input centroid as a trackable object
  181. else:
  182. for col in unusedCols:
  183. self.register(col, group[col])