objects.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  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 draw_box_with_label
  15. class ObjectTracker:
  16. def __init__(self, config: DetectConfig):
  17. self.tracked_objects = {}
  18. self.disappeared = {}
  19. self.max_disappeared = config.max_disappeared
  20. def register(self, index, obj):
  21. rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
  22. id = f"{obj['frame_time']}-{rand_id}"
  23. obj["id"] = id
  24. obj["start_time"] = obj["frame_time"]
  25. self.tracked_objects[id] = obj
  26. self.disappeared[id] = 0
  27. def deregister(self, id):
  28. del self.tracked_objects[id]
  29. del self.disappeared[id]
  30. def update(self, id, new_obj):
  31. self.disappeared[id] = 0
  32. self.tracked_objects[id].update(new_obj)
  33. def match_and_update(self, frame_time, new_objects):
  34. # group by name
  35. new_object_groups = defaultdict(lambda: [])
  36. for obj in new_objects:
  37. new_object_groups[obj[0]].append(
  38. {
  39. "label": obj[0],
  40. "score": obj[1],
  41. "box": obj[2],
  42. "area": obj[3],
  43. "region": obj[4],
  44. "frame_time": frame_time,
  45. }
  46. )
  47. # update any tracked objects with labels that are not
  48. # seen in the current objects and deregister if needed
  49. for obj in list(self.tracked_objects.values()):
  50. if not obj["label"] in new_object_groups:
  51. if self.disappeared[obj["id"]] >= self.max_disappeared:
  52. self.deregister(obj["id"])
  53. else:
  54. self.disappeared[obj["id"]] += 1
  55. if len(new_objects) == 0:
  56. return
  57. # track objects for each label type
  58. for label, group in new_object_groups.items():
  59. current_objects = [
  60. o for o in self.tracked_objects.values() if o["label"] == label
  61. ]
  62. current_ids = [o["id"] for o in current_objects]
  63. current_centroids = np.array([o["centroid"] for o in current_objects])
  64. # compute centroids of new objects
  65. for obj in group:
  66. centroid_x = int((obj["box"][0] + obj["box"][2]) / 2.0)
  67. centroid_y = int((obj["box"][1] + obj["box"][3]) / 2.0)
  68. obj["centroid"] = (centroid_x, centroid_y)
  69. if len(current_objects) == 0:
  70. for index, obj in enumerate(group):
  71. self.register(index, obj)
  72. continue
  73. new_centroids = np.array([o["centroid"] for o in group])
  74. # compute the distance between each pair of tracked
  75. # centroids and new centroids, respectively -- our
  76. # goal will be to match each current centroid to a new
  77. # object centroid
  78. D = dist.cdist(current_centroids, new_centroids)
  79. # in order to perform this matching we must (1) find the smallest
  80. # value in each row (i.e. the distance from each current object to
  81. # the closest new object) and then (2) sort the row indexes based
  82. # on their minimum values so that the row with the smallest
  83. # distance (the best match) is at the *front* of the index list
  84. rows = D.min(axis=1).argsort()
  85. # next, we determine which new object each existing object matched
  86. # against, and apply the same sorting as was applied previously
  87. cols = D.argmin(axis=1)[rows]
  88. # many current objects may register with each new object, so only
  89. # match the closest ones. unique returns the indices of the first
  90. # occurrences of each value, and because the rows are sorted by
  91. # distance, this will be index of the closest match
  92. _, index = np.unique(cols, return_index=True)
  93. rows = rows[index]
  94. cols = cols[index]
  95. # loop over the combination of the (row, column) index tuples
  96. for row, col in zip(rows, cols):
  97. # grab the object ID for the current row, set its new centroid,
  98. # and reset the disappeared counter
  99. objectID = current_ids[row]
  100. self.update(objectID, group[col])
  101. # compute the row and column indices we have NOT yet examined
  102. unusedRows = set(range(D.shape[0])).difference(rows)
  103. unusedCols = set(range(D.shape[1])).difference(cols)
  104. # in the event that the number of object centroids is
  105. # equal or greater than the number of input centroids
  106. # we need to check and see if some of these objects have
  107. # potentially disappeared
  108. if D.shape[0] >= D.shape[1]:
  109. for row in unusedRows:
  110. id = current_ids[row]
  111. if self.disappeared[id] >= self.max_disappeared:
  112. self.deregister(id)
  113. else:
  114. self.disappeared[id] += 1
  115. # if the number of input centroids is greater
  116. # than the number of existing object centroids we need to
  117. # register each new input centroid as a trackable object
  118. else:
  119. for col in unusedCols:
  120. self.register(col, group[col])