objects.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import time
  2. import datetime
  3. import threading
  4. import cv2
  5. import prctl
  6. import itertools
  7. import copy
  8. import numpy as np
  9. import multiprocessing as mp
  10. from collections import defaultdict
  11. from scipy.spatial import distance as dist
  12. from frigate.util import draw_box_with_label, LABELS, compute_intersection_rectangle, compute_intersection_over_union, calculate_region
  13. class ObjectCleaner(threading.Thread):
  14. def __init__(self, camera):
  15. threading.Thread.__init__(self)
  16. self.camera = camera
  17. def run(self):
  18. prctl.set_name("ObjectCleaner")
  19. while True:
  20. # wait a bit before checking for expired frames
  21. time.sleep(0.2)
  22. for frame_time in list(self.camera.detected_objects.keys()).copy():
  23. if not frame_time in self.camera.frame_cache:
  24. del self.camera.detected_objects[frame_time]
  25. with self.camera.object_tracker.tracked_objects_lock:
  26. now = datetime.datetime.now().timestamp()
  27. for id, obj in list(self.camera.object_tracker.tracked_objects.items()):
  28. # if the object is more than 10 seconds old
  29. # and not in the most recent frame, deregister
  30. if (now - obj['frame_time']) > 10 and self.camera.object_tracker.most_recent_frame_time > obj['frame_time']:
  31. self.camera.object_tracker.deregister(id)
  32. class DetectedObjectsProcessor(threading.Thread):
  33. def __init__(self, camera):
  34. threading.Thread.__init__(self)
  35. self.camera = camera
  36. def run(self):
  37. prctl.set_name(self.__class__.__name__)
  38. while True:
  39. frame = self.camera.detected_objects_queue.get()
  40. objects = frame['detected_objects']
  41. for raw_obj in objects:
  42. name = str(LABELS[raw_obj.label_id])
  43. if not name in self.camera.objects_to_track:
  44. continue
  45. obj = {
  46. 'name': name,
  47. 'score': float(raw_obj.score),
  48. 'box': {
  49. 'xmin': int((raw_obj.bounding_box[0][0] * frame['size']) + frame['x_offset']),
  50. 'ymin': int((raw_obj.bounding_box[0][1] * frame['size']) + frame['y_offset']),
  51. 'xmax': int((raw_obj.bounding_box[1][0] * frame['size']) + frame['x_offset']),
  52. 'ymax': int((raw_obj.bounding_box[1][1] * frame['size']) + frame['y_offset'])
  53. },
  54. 'region': {
  55. 'xmin': frame['x_offset'],
  56. 'ymin': frame['y_offset'],
  57. 'xmax': frame['x_offset']+frame['size'],
  58. 'ymax': frame['y_offset']+frame['size']
  59. },
  60. 'frame_time': frame['frame_time'],
  61. 'region_id': frame['region_id']
  62. }
  63. # if the object is within 5 pixels of the region border, and the region is not on the edge
  64. # consider the object to be clipped
  65. obj['clipped'] = False
  66. if ((obj['region']['xmin'] > 5 and obj['box']['xmin']-obj['region']['xmin'] <= 5) or
  67. (obj['region']['ymin'] > 5 and obj['box']['ymin']-obj['region']['ymin'] <= 5) or
  68. (self.camera.frame_shape[1]-obj['region']['xmax'] > 5 and obj['region']['xmax']-obj['box']['xmax'] <= 5) or
  69. (self.camera.frame_shape[0]-obj['region']['ymax'] > 5 and obj['region']['ymax']-obj['box']['ymax'] <= 5)):
  70. obj['clipped'] = True
  71. # Compute the area
  72. obj['area'] = (obj['box']['xmax']-obj['box']['xmin'])*(obj['box']['ymax']-obj['box']['ymin'])
  73. self.camera.detected_objects[frame['frame_time']].append(obj)
  74. with self.camera.regions_in_process_lock:
  75. self.camera.regions_in_process[frame['frame_time']] -= 1
  76. # print(f"{frame['frame_time']} remaining regions {self.camera.regions_in_process[frame['frame_time']]}")
  77. if self.camera.regions_in_process[frame['frame_time']] == 0:
  78. del self.camera.regions_in_process[frame['frame_time']]
  79. # print(f"{frame['frame_time']} no remaining regions")
  80. self.camera.finished_frame_queue.put(frame['frame_time'])
  81. # Thread that checks finished frames for clipped objects and sends back
  82. # for processing if needed
  83. class RegionRefiner(threading.Thread):
  84. def __init__(self, camera):
  85. threading.Thread.__init__(self)
  86. self.camera = camera
  87. def run(self):
  88. prctl.set_name(self.__class__.__name__)
  89. while True:
  90. frame_time = self.camera.finished_frame_queue.get()
  91. detected_objects = self.camera.detected_objects[frame_time].copy()
  92. # print(f"{frame_time} finished")
  93. # group by name
  94. detected_object_groups = defaultdict(lambda: [])
  95. for obj in detected_objects:
  96. detected_object_groups[obj['name']].append(obj)
  97. look_again = False
  98. selected_objects = []
  99. for group in detected_object_groups.values():
  100. # apply non-maxima suppression to suppress weak, overlapping bounding boxes
  101. boxes = [(o['box']['xmin'], o['box']['ymin'], o['box']['xmax']-o['box']['xmin'], o['box']['ymax']-o['box']['ymin'])
  102. for o in group]
  103. confidences = [o['score'] for o in group]
  104. idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
  105. for index in idxs:
  106. obj = group[index[0]]
  107. selected_objects.append(obj)
  108. if obj['clipped']:
  109. box = obj['box']
  110. # calculate a new region that will hopefully get the entire object
  111. (size, x_offset, y_offset) = calculate_region(self.camera.frame_shape,
  112. box['xmin'], box['ymin'],
  113. box['xmax'], box['ymax'])
  114. # print(f"{frame_time} new region: {size} {x_offset} {y_offset}")
  115. with self.camera.regions_in_process_lock:
  116. if not frame_time in self.camera.regions_in_process:
  117. self.camera.regions_in_process[frame_time] = 1
  118. else:
  119. self.camera.regions_in_process[frame_time] += 1
  120. # add it to the queue
  121. self.camera.resize_queue.put({
  122. 'camera_name': self.camera.name,
  123. 'frame_time': frame_time,
  124. 'region_id': -1,
  125. 'size': size,
  126. 'x_offset': x_offset,
  127. 'y_offset': y_offset
  128. })
  129. self.camera.dynamic_region_fps.update()
  130. look_again = True
  131. # if we are looking again, then this frame is not ready for processing
  132. if look_again:
  133. # remove the clipped objects
  134. self.camera.detected_objects[frame_time] = [o for o in selected_objects if not o['clipped']]
  135. continue
  136. # filter objects based on camera settings
  137. selected_objects = [o for o in selected_objects if not self.filtered(o)]
  138. self.camera.detected_objects[frame_time] = selected_objects
  139. # print(f"{frame_time} is actually finished")
  140. # keep adding frames to the refined queue as long as they are finished
  141. with self.camera.regions_in_process_lock:
  142. while self.camera.frame_queue.qsize() > 0 and self.camera.frame_queue.queue[0] not in self.camera.regions_in_process:
  143. self.camera.last_processed_frame = self.camera.frame_queue.get()
  144. self.camera.refined_frame_queue.put(self.camera.last_processed_frame)
  145. def filtered(self, obj):
  146. object_name = obj['name']
  147. if object_name in self.camera.object_filters:
  148. obj_settings = self.camera.object_filters[object_name]
  149. # if the min area is larger than the
  150. # detected object, don't add it to detected objects
  151. if obj_settings.get('min_area',-1) > obj['area']:
  152. return True
  153. # if the detected object is larger than the
  154. # max area, don't add it to detected objects
  155. if obj_settings.get('max_area', self.camera.frame_shape[0]*self.camera.frame_shape[1]) < obj['area']:
  156. return True
  157. # if the score is lower than the threshold, skip
  158. if obj_settings.get('threshold', 0) > obj['score']:
  159. return True
  160. # compute the coordinates of the object and make sure
  161. # the location isnt outside the bounds of the image (can happen from rounding)
  162. y_location = min(int(obj['box']['ymax']), len(self.camera.mask)-1)
  163. x_location = min(int((obj['box']['xmax']-obj['box']['xmin'])/2.0)+obj['box']['xmin'], len(self.camera.mask[0])-1)
  164. # if the object is in a masked location, don't add it to detected objects
  165. if self.camera.mask[y_location][x_location] == [0]:
  166. return True
  167. return False
  168. def has_overlap(self, new_obj, obj, overlap=.7):
  169. # compute intersection rectangle with existing object and new objects region
  170. existing_obj_current_region = compute_intersection_rectangle(obj['box'], new_obj['region'])
  171. # compute intersection rectangle with new object and existing objects region
  172. new_obj_existing_region = compute_intersection_rectangle(new_obj['box'], obj['region'])
  173. # compute iou for the two intersection rectangles that were just computed
  174. iou = compute_intersection_over_union(existing_obj_current_region, new_obj_existing_region)
  175. # if intersection is greater than overlap
  176. if iou > overlap:
  177. return True
  178. else:
  179. return False
  180. def find_group(self, new_obj, groups):
  181. for index, group in enumerate(groups):
  182. for obj in group:
  183. if self.has_overlap(new_obj, obj):
  184. return index
  185. return None
  186. class ObjectTracker(threading.Thread):
  187. def __init__(self, camera, max_disappeared):
  188. threading.Thread.__init__(self)
  189. self.camera = camera
  190. self.tracked_objects = {}
  191. self.tracked_objects_lock = mp.Lock()
  192. self.most_recent_frame_time = None
  193. def run(self):
  194. prctl.set_name(self.__class__.__name__)
  195. while True:
  196. frame_time = self.camera.refined_frame_queue.get()
  197. with self.tracked_objects_lock:
  198. self.match_and_update(self.camera.detected_objects[frame_time])
  199. self.most_recent_frame_time = frame_time
  200. self.camera.frame_output_queue.put((frame_time, copy.deepcopy(self.tracked_objects)))
  201. if len(self.tracked_objects) > 0:
  202. with self.camera.objects_tracked:
  203. self.camera.objects_tracked.notify_all()
  204. def register(self, index, obj):
  205. id = f"{str(obj['frame_time'])}-{index}"
  206. obj['id'] = id
  207. obj['top_score'] = obj['score']
  208. self.add_history(obj)
  209. self.tracked_objects[id] = obj
  210. def deregister(self, id):
  211. del self.tracked_objects[id]
  212. def update(self, id, new_obj):
  213. self.tracked_objects[id].update(new_obj)
  214. self.add_history(self.tracked_objects[id])
  215. if self.tracked_objects[id]['score'] > self.tracked_objects[id]['top_score']:
  216. self.tracked_objects[id]['top_score'] = self.tracked_objects[id]['score']
  217. def add_history(self, obj):
  218. entry = {
  219. 'score': obj['score'],
  220. 'box': obj['box'],
  221. 'region': obj['region'],
  222. 'centroid': obj['centroid'],
  223. 'frame_time': obj['frame_time']
  224. }
  225. if 'history' in obj:
  226. obj['history'].append(entry)
  227. else:
  228. obj['history'] = [entry]
  229. def match_and_update(self, new_objects):
  230. if len(new_objects) == 0:
  231. return
  232. # group by name
  233. new_object_groups = defaultdict(lambda: [])
  234. for obj in new_objects:
  235. new_object_groups[obj['name']].append(obj)
  236. # track objects for each label type
  237. for label, group in new_object_groups.items():
  238. current_objects = [o for o in self.tracked_objects.values() if o['name'] == label]
  239. current_ids = [o['id'] for o in current_objects]
  240. current_centroids = np.array([o['centroid'] for o in current_objects])
  241. # compute centroids of new objects
  242. for obj in group:
  243. centroid_x = int((obj['box']['xmin']+obj['box']['xmax']) / 2.0)
  244. centroid_y = int((obj['box']['ymin']+obj['box']['ymax']) / 2.0)
  245. obj['centroid'] = (centroid_x, centroid_y)
  246. if len(current_objects) == 0:
  247. for index, obj in enumerate(group):
  248. self.register(index, obj)
  249. return
  250. new_centroids = np.array([o['centroid'] for o in group])
  251. # compute the distance between each pair of tracked
  252. # centroids and new centroids, respectively -- our
  253. # goal will be to match each new centroid to an existing
  254. # object centroid
  255. D = dist.cdist(current_centroids, new_centroids)
  256. # in order to perform this matching we must (1) find the
  257. # smallest value in each row and then (2) sort the row
  258. # indexes based on their minimum values so that the row
  259. # with the smallest value is at the *front* of the index
  260. # list
  261. rows = D.min(axis=1).argsort()
  262. # next, we perform a similar process on the columns by
  263. # finding the smallest value in each column and then
  264. # sorting using the previously computed row index list
  265. cols = D.argmin(axis=1)[rows]
  266. # in order to determine if we need to update, register,
  267. # or deregister an object we need to keep track of which
  268. # of the rows and column indexes we have already examined
  269. usedRows = set()
  270. usedCols = set()
  271. # loop over the combination of the (row, column) index
  272. # tuples
  273. for (row, col) in zip(rows, cols):
  274. # if we have already examined either the row or
  275. # column value before, ignore it
  276. if row in usedRows or col in usedCols:
  277. continue
  278. # otherwise, grab the object ID for the current row,
  279. # set its new centroid, and reset the disappeared
  280. # counter
  281. objectID = current_ids[row]
  282. self.update(objectID, group[col])
  283. # indicate that we have examined each of the row and
  284. # column indexes, respectively
  285. usedRows.add(row)
  286. usedCols.add(col)
  287. # compute the column index we have NOT yet examined
  288. unusedCols = set(range(0, D.shape[1])).difference(usedCols)
  289. # if the number of input centroids is greater
  290. # than the number of existing object centroids we need to
  291. # register each new input centroid as a trackable object
  292. # if D.shape[0] < D.shape[1]:
  293. for col in unusedCols:
  294. self.register(col, group[col])
  295. # Maintains the frame and object with the highest score
  296. class BestFrames(threading.Thread):
  297. def __init__(self, camera):
  298. threading.Thread.__init__(self)
  299. self.camera = camera
  300. self.best_objects = {}
  301. self.best_frames = {}
  302. def run(self):
  303. prctl.set_name(self.__class__.__name__)
  304. while True:
  305. # wait until objects have been tracked
  306. with self.camera.objects_tracked:
  307. self.camera.objects_tracked.wait()
  308. # make a copy of tracked objects
  309. tracked_objects = list(self.camera.object_tracker.tracked_objects.values())
  310. for obj in tracked_objects:
  311. if obj['name'] in self.best_objects:
  312. now = datetime.datetime.now().timestamp()
  313. # if the object is a higher score than the current best score
  314. # or the current object is more than 1 minute old, use the new object
  315. if obj['score'] > self.best_objects[obj['name']]['score'] or (now - self.best_objects[obj['name']]['frame_time']) > 60:
  316. self.best_objects[obj['name']] = copy.deepcopy(obj)
  317. else:
  318. self.best_objects[obj['name']] = copy.deepcopy(obj)
  319. for name, obj in self.best_objects.items():
  320. if obj['frame_time'] in self.camera.frame_cache:
  321. best_frame = self.camera.frame_cache[obj['frame_time']]
  322. draw_box_with_label(best_frame, obj['box']['xmin'], obj['box']['ymin'],
  323. obj['box']['xmax'], obj['box']['ymax'], obj['name'], f"{int(obj['score']*100)}% {obj['area']}")
  324. # print a timestamp
  325. time_to_show = datetime.datetime.fromtimestamp(obj['frame_time']).strftime("%m/%d/%Y %H:%M:%S")
  326. cv2.putText(best_frame, time_to_show, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, fontScale=.8, color=(255, 255, 255), thickness=2)
  327. self.best_frames[name] = best_frame