objects.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. import time
  2. import datetime
  3. import threading
  4. import cv2
  5. import prctl
  6. import itertools
  7. import numpy as np
  8. from collections import defaultdict
  9. from scipy.spatial import distance as dist
  10. from frigate.util import draw_box_with_label, LABELS, compute_intersection_rectangle, compute_intersection_over_union, calculate_region
  11. class ObjectCleaner(threading.Thread):
  12. def __init__(self, objects_parsed, detected_objects):
  13. threading.Thread.__init__(self)
  14. self._objects_parsed = objects_parsed
  15. self._detected_objects = detected_objects
  16. def run(self):
  17. prctl.set_name("ObjectCleaner")
  18. while True:
  19. # wait a bit before checking for expired frames
  20. time.sleep(0.2)
  21. # expire the objects that are more than 1 second old
  22. now = datetime.datetime.now().timestamp()
  23. # look for the first object found within the last second
  24. # (newest objects are appended to the end)
  25. detected_objects = self._detected_objects.copy()
  26. objects_removed = False
  27. for frame_time in detected_objects.keys():
  28. if now-frame_time>2:
  29. del self._detected_objects[frame_time]
  30. objects_removed = True
  31. if objects_removed:
  32. # notify that parsed objects were changed
  33. with self._objects_parsed:
  34. self._objects_parsed.notify_all()
  35. class DetectedObjectsProcessor(threading.Thread):
  36. def __init__(self, camera):
  37. threading.Thread.__init__(self)
  38. self.camera = camera
  39. def run(self):
  40. prctl.set_name(self.__class__.__name__)
  41. while True:
  42. frame = self.camera.detected_objects_queue.get()
  43. objects = frame['detected_objects']
  44. for raw_obj in objects:
  45. name = str(LABELS[raw_obj.label_id])
  46. if not name in self.camera.objects_to_track:
  47. continue
  48. obj = {
  49. 'name': name,
  50. 'score': float(raw_obj.score),
  51. 'box': {
  52. 'xmin': int((raw_obj.bounding_box[0][0] * frame['size']) + frame['x_offset']),
  53. 'ymin': int((raw_obj.bounding_box[0][1] * frame['size']) + frame['y_offset']),
  54. 'xmax': int((raw_obj.bounding_box[1][0] * frame['size']) + frame['x_offset']),
  55. 'ymax': int((raw_obj.bounding_box[1][1] * frame['size']) + frame['y_offset'])
  56. },
  57. 'region': {
  58. 'xmin': frame['x_offset'],
  59. 'ymin': frame['y_offset'],
  60. 'xmax': frame['x_offset']+frame['size'],
  61. 'ymax': frame['y_offset']+frame['size']
  62. },
  63. 'frame_time': frame['frame_time'],
  64. 'region_id': frame['region_id']
  65. }
  66. # if the object is within 5 pixels of the region border, and the region is not on the edge
  67. # consider the object to be clipped
  68. obj['clipped'] = False
  69. if ((obj['region']['xmin'] > 5 and obj['box']['xmin']-obj['region']['xmin'] <= 5) or
  70. (obj['region']['ymin'] > 5 and obj['box']['ymin']-obj['region']['ymin'] <= 5) or
  71. (self.camera.frame_shape[1]-obj['region']['xmax'] > 5 and obj['region']['xmax']-obj['box']['xmax'] <= 5) or
  72. (self.camera.frame_shape[0]-obj['region']['ymax'] > 5 and obj['region']['ymax']-obj['box']['ymax'] <= 5)):
  73. obj['clipped'] = True
  74. # Compute the area
  75. obj['area'] = (obj['box']['xmax']-obj['box']['xmin'])*(obj['box']['ymax']-obj['box']['ymin'])
  76. self.camera.detected_objects[frame['frame_time']].append(obj)
  77. with self.camera.regions_in_process_lock:
  78. self.camera.regions_in_process[frame['frame_time']] -= 1
  79. # print(f"{frame['frame_time']} remaining regions {self.camera.regions_in_process[frame['frame_time']]}")
  80. if self.camera.regions_in_process[frame['frame_time']] == 0:
  81. del self.camera.regions_in_process[frame['frame_time']]
  82. # print(f"{frame['frame_time']} no remaining regions")
  83. self.camera.finished_frame_queue.put(frame['frame_time'])
  84. # Thread that checks finished frames for clipped objects and sends back
  85. # for processing if needed
  86. class RegionRefiner(threading.Thread):
  87. def __init__(self, camera):
  88. threading.Thread.__init__(self)
  89. self.camera = camera
  90. def run(self):
  91. prctl.set_name(self.__class__.__name__)
  92. while True:
  93. frame_time = self.camera.finished_frame_queue.get()
  94. detected_objects = self.camera.detected_objects[frame_time].copy()
  95. # print(f"{frame_time} finished")
  96. # group by name
  97. detected_object_groups = defaultdict(lambda: [])
  98. for obj in detected_objects:
  99. detected_object_groups[obj['name']].append(obj)
  100. look_again = False
  101. selected_objects = []
  102. for group in detected_object_groups.values():
  103. # apply non-maxima suppression to suppress weak, overlapping bounding boxes
  104. boxes = [(o['box']['xmin'], o['box']['ymin'], o['box']['xmax']-o['box']['xmin'], o['box']['ymax']-o['box']['ymin'])
  105. for o in group]
  106. confidences = [o['score'] for o in group]
  107. idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
  108. for index in idxs:
  109. obj = group[index[0]]
  110. selected_objects.append(obj)
  111. if obj['clipped']:
  112. box = obj['box']
  113. # calculate a new region that will hopefully get the entire object
  114. (size, x_offset, y_offset) = calculate_region(self.camera.frame_shape,
  115. box['xmin'], box['ymin'],
  116. box['xmax'], box['ymax'])
  117. # print(f"{frame_time} new region: {size} {x_offset} {y_offset}")
  118. with self.camera.regions_in_process_lock:
  119. if not frame_time in self.camera.regions_in_process:
  120. self.camera.regions_in_process[frame_time] = 1
  121. else:
  122. self.camera.regions_in_process[frame_time] += 1
  123. # add it to the queue
  124. self.camera.resize_queue.put({
  125. 'camera_name': self.camera.name,
  126. 'frame_time': frame_time,
  127. 'region_id': -1,
  128. 'size': size,
  129. 'x_offset': x_offset,
  130. 'y_offset': y_offset
  131. })
  132. self.camera.dynamic_region_fps.update()
  133. look_again = True
  134. # if we are looking again, then this frame is not ready for processing
  135. if look_again:
  136. # remove the clipped objects
  137. self.camera.detected_objects[frame_time] = [o for o in selected_objects if not o['clipped']]
  138. continue
  139. # filter objects based on camera settings
  140. selected_objects = [o for o in selected_objects if not self.filtered(o)]
  141. self.camera.detected_objects[frame_time] = selected_objects
  142. with self.camera.objects_parsed:
  143. self.camera.objects_parsed.notify_all()
  144. # print(f"{frame_time} is actually finished")
  145. # keep adding frames to the refined queue as long as they are finished
  146. with self.camera.regions_in_process_lock:
  147. while self.camera.frame_queue.qsize() > 0 and self.camera.frame_queue.queue[0] not in self.camera.regions_in_process:
  148. self.camera.last_processed_frame = self.camera.frame_queue.get()
  149. self.camera.refined_frame_queue.put(self.camera.last_processed_frame)
  150. def filtered(self, obj):
  151. object_name = obj['name']
  152. if object_name in self.camera.object_filters:
  153. obj_settings = self.camera.object_filters[object_name]
  154. # if the min area is larger than the
  155. # detected object, don't add it to detected objects
  156. if obj_settings.get('min_area',-1) > obj['area']:
  157. return True
  158. # if the detected object is larger than the
  159. # max area, don't add it to detected objects
  160. if obj_settings.get('max_area', self.camera.frame_shape[0]*self.camera.frame_shape[1]) < obj['area']:
  161. return True
  162. # if the score is lower than the threshold, skip
  163. if obj_settings.get('threshold', 0) > obj['score']:
  164. return True
  165. # compute the coordinates of the object and make sure
  166. # the location isnt outside the bounds of the image (can happen from rounding)
  167. y_location = min(int(obj['box']['ymax']), len(self.camera.mask)-1)
  168. x_location = min(int((obj['box']['xmax']-obj['box']['xmin'])/2.0)+obj['box']['xmin'], len(self.camera.mask[0])-1)
  169. # if the object is in a masked location, don't add it to detected objects
  170. if self.camera.mask[y_location][x_location] == [0]:
  171. return True
  172. return False
  173. def has_overlap(self, new_obj, obj, overlap=.7):
  174. # compute intersection rectangle with existing object and new objects region
  175. existing_obj_current_region = compute_intersection_rectangle(obj['box'], new_obj['region'])
  176. # compute intersection rectangle with new object and existing objects region
  177. new_obj_existing_region = compute_intersection_rectangle(new_obj['box'], obj['region'])
  178. # compute iou for the two intersection rectangles that were just computed
  179. iou = compute_intersection_over_union(existing_obj_current_region, new_obj_existing_region)
  180. # if intersection is greater than overlap
  181. if iou > overlap:
  182. return True
  183. else:
  184. return False
  185. def find_group(self, new_obj, groups):
  186. for index, group in enumerate(groups):
  187. for obj in group:
  188. if self.has_overlap(new_obj, obj):
  189. return index
  190. return None
  191. class ObjectTracker(threading.Thread):
  192. def __init__(self, camera, max_disappeared):
  193. threading.Thread.__init__(self)
  194. self.camera = camera
  195. self.tracked_objects = {}
  196. self.disappeared = {}
  197. self.max_disappeared = max_disappeared
  198. def run(self):
  199. prctl.set_name(self.__class__.__name__)
  200. while True:
  201. frame_time = self.camera.refined_frame_queue.get()
  202. self.match_and_update(self.camera.detected_objects[frame_time])
  203. self.camera.frame_tracked_queue.put(frame_time)
  204. def register(self, index, obj):
  205. id = f"{str(obj['frame_time'])}-{index}"
  206. obj['id'] = id
  207. self.tracked_objects[id] = obj
  208. self.disappeared[id] = 0
  209. def deregister(self, id):
  210. del self.disappeared[id]
  211. del self.tracked_objects[id]
  212. def update(self, id, new_obj):
  213. self.tracked_objects[id].update(new_obj)
  214. # TODO: am i missing anything? history?
  215. def match_and_update(self, new_objects):
  216. # check to see if the list of input bounding box rectangles
  217. # is empty
  218. if len(new_objects) == 0:
  219. # loop over any existing tracked objects and mark them
  220. # as disappeared
  221. for objectID in list(self.disappeared.keys()):
  222. self.disappeared[objectID] += 1
  223. # if we have reached a maximum number of consecutive
  224. # frames where a given object has been marked as
  225. # missing, deregister it
  226. if self.disappeared[objectID] > self.max_disappeared:
  227. self.deregister(objectID)
  228. # return early as there are no centroids or tracking info
  229. # to update
  230. return
  231. # group by name
  232. new_object_groups = defaultdict(lambda: [])
  233. for obj in new_objects:
  234. new_object_groups[obj['name']].append(obj)
  235. # track objects for each label type
  236. # TODO: this is going to miss deregistering objects that are not in the new groups
  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
  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. # val
  277. if row in usedRows or col in usedCols:
  278. continue
  279. # otherwise, grab the object ID for the current row,
  280. # set its new centroid, and reset the disappeared
  281. # counter
  282. objectID = current_ids[row]
  283. self.update(objectID, new_objects[col])
  284. self.disappeared[objectID] = 0
  285. # indicate that we have examined each of the row and
  286. # column indexes, respectively
  287. usedRows.add(row)
  288. usedCols.add(col)
  289. # compute both the row and column index we have NOT yet
  290. # examined
  291. unusedRows = set(range(0, D.shape[0])).difference(usedRows)
  292. unusedCols = set(range(0, D.shape[1])).difference(usedCols)
  293. # in the event that the number of object centroids is
  294. # equal or greater than the number of input centroids
  295. # we need to check and see if some of these objects have
  296. # potentially disappeared
  297. if D.shape[0] >= D.shape[1]:
  298. # loop over the unused row indexes
  299. for row in unusedRows:
  300. # grab the object ID for the corresponding row
  301. # index and increment the disappeared counter
  302. objectID = current_ids[row]
  303. self.disappeared[objectID] += 1
  304. # check to see if the number of consecutive
  305. # frames the object has been marked "disappeared"
  306. # for warrants deregistering the object
  307. if self.disappeared[objectID] > self.max_disappeared:
  308. self.deregister(objectID)
  309. # otherwise, if the number of input centroids is greater
  310. # than the number of existing object centroids we need to
  311. # register each new input centroid as a trackable object
  312. else:
  313. for col in unusedCols:
  314. self.register(col, new_objects[col])
  315. # Maintains the frame and object with the highest score
  316. class BestFrames(threading.Thread):
  317. def __init__(self, objects_parsed, recent_frames, detected_objects):
  318. threading.Thread.__init__(self)
  319. self.objects_parsed = objects_parsed
  320. self.recent_frames = recent_frames
  321. self.detected_objects = detected_objects
  322. self.best_objects = {}
  323. self.best_frames = {}
  324. def run(self):
  325. prctl.set_name("BestFrames")
  326. while True:
  327. # wait until objects have been parsed
  328. with self.objects_parsed:
  329. self.objects_parsed.wait()
  330. # make a copy of detected objects
  331. detected_objects = self.detected_objects.copy()
  332. for obj in itertools.chain.from_iterable(detected_objects.values()):
  333. if obj['name'] in self.best_objects:
  334. now = datetime.datetime.now().timestamp()
  335. # if the object is a higher score than the current best score
  336. # or the current object is more than 1 minute old, use the new object
  337. if obj['score'] > self.best_objects[obj['name']]['score'] or (now - self.best_objects[obj['name']]['frame_time']) > 60:
  338. self.best_objects[obj['name']] = obj
  339. else:
  340. self.best_objects[obj['name']] = obj
  341. # make a copy of the recent frames
  342. recent_frames = self.recent_frames.copy()
  343. for name, obj in self.best_objects.items():
  344. if obj['frame_time'] in recent_frames:
  345. best_frame = recent_frames[obj['frame_time']] #, np.zeros((720,1280,3), np.uint8))
  346. draw_box_with_label(best_frame, obj['box']['xmin'], obj['box']['ymin'],
  347. obj['box']['xmax'], obj['box']['ymax'], obj['name'], f"{int(obj['score']*100)}% {obj['area']}")
  348. # print a timestamp
  349. time_to_show = datetime.datetime.fromtimestamp(obj['frame_time']).strftime("%m/%d/%Y %H:%M:%S")
  350. cv2.putText(best_frame, time_to_show, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, fontScale=.8, color=(255, 255, 255), thickness=2)
  351. self.best_frames[name] = best_frame