objects.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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 10 entries for the position, add the bounding box
  71. # and recompute the position box
  72. if len(position["xmins"]) < 10:
  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. # register the first position change and then only increment if
  90. # the object was previously stationary
  91. if (
  92. self.tracked_objects[id]["position_changes"] == 0
  93. or self.tracked_objects[id]["motionless_count"]
  94. >= self.detect_config.stationary.threshold
  95. ):
  96. self.tracked_objects[id]["position_changes"] += 1
  97. self.tracked_objects[id]["motionless_count"] = 0
  98. self.tracked_objects[id].update(new_obj)
  99. def update_frame_times(self, frame_time):
  100. for id in self.tracked_objects.keys():
  101. self.tracked_objects[id]["frame_time"] = frame_time
  102. self.tracked_objects[id]["motionless_count"] += 1
  103. def match_and_update(self, frame_time, new_objects):
  104. # group by name
  105. new_object_groups = defaultdict(lambda: [])
  106. for obj in new_objects:
  107. new_object_groups[obj[0]].append(
  108. {
  109. "label": obj[0],
  110. "score": obj[1],
  111. "box": obj[2],
  112. "area": obj[3],
  113. "region": obj[4],
  114. "frame_time": frame_time,
  115. }
  116. )
  117. # update any tracked objects with labels that are not
  118. # seen in the current objects and deregister if needed
  119. for obj in list(self.tracked_objects.values()):
  120. if not obj["label"] in new_object_groups:
  121. if self.disappeared[obj["id"]] >= self.max_disappeared:
  122. self.deregister(obj["id"])
  123. else:
  124. self.disappeared[obj["id"]] += 1
  125. if len(new_objects) == 0:
  126. return
  127. # track objects for each label type
  128. for label, group in new_object_groups.items():
  129. current_objects = [
  130. o for o in self.tracked_objects.values() if o["label"] == label
  131. ]
  132. current_ids = [o["id"] for o in current_objects]
  133. current_centroids = np.array([o["centroid"] for o in current_objects])
  134. # compute centroids of new objects
  135. for obj in group:
  136. centroid_x = int((obj["box"][0] + obj["box"][2]) / 2.0)
  137. centroid_y = int((obj["box"][1] + obj["box"][3]) / 2.0)
  138. obj["centroid"] = (centroid_x, centroid_y)
  139. if len(current_objects) == 0:
  140. for index, obj in enumerate(group):
  141. self.register(index, obj)
  142. continue
  143. new_centroids = np.array([o["centroid"] for o in group])
  144. # compute the distance between each pair of tracked
  145. # centroids and new centroids, respectively -- our
  146. # goal will be to match each current centroid to a new
  147. # object centroid
  148. D = dist.cdist(current_centroids, new_centroids)
  149. # in order to perform this matching we must (1) find the smallest
  150. # value in each row (i.e. the distance from each current object to
  151. # the closest new object) and then (2) sort the row indexes based
  152. # on their minimum values so that the row with the smallest
  153. # distance (the best match) is at the *front* of the index list
  154. rows = D.min(axis=1).argsort()
  155. # next, we determine which new object each existing object matched
  156. # against, and apply the same sorting as was applied previously
  157. cols = D.argmin(axis=1)[rows]
  158. # many current objects may register with each new object, so only
  159. # match the closest ones. unique returns the indices of the first
  160. # occurrences of each value, and because the rows are sorted by
  161. # distance, this will be index of the closest match
  162. _, index = np.unique(cols, return_index=True)
  163. rows = rows[index]
  164. cols = cols[index]
  165. # loop over the combination of the (row, column) index tuples
  166. for row, col in zip(rows, cols):
  167. # grab the object ID for the current row, set its new centroid,
  168. # and reset the disappeared counter
  169. objectID = current_ids[row]
  170. self.update(objectID, group[col])
  171. # compute the row and column indices we have NOT yet examined
  172. unusedRows = set(range(D.shape[0])).difference(rows)
  173. unusedCols = set(range(D.shape[1])).difference(cols)
  174. # in the event that the number of object centroids is
  175. # equal or greater than the number of input centroids
  176. # we need to check and see if some of these objects have
  177. # potentially disappeared
  178. if D.shape[0] >= D.shape[1]:
  179. for row in unusedRows:
  180. id = current_ids[row]
  181. if self.disappeared[id] >= self.max_disappeared:
  182. self.deregister(id)
  183. else:
  184. self.disappeared[id] += 1
  185. # if the number of input centroids is greater
  186. # than the number of existing object centroids we need to
  187. # register each new input centroid as a trackable object
  188. else:
  189. for col in unusedCols:
  190. self.register(col, group[col])