objects.py 9.2 KB

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