util.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. import collections
  2. import datetime
  3. import hashlib
  4. import json
  5. import logging
  6. import math
  7. import signal
  8. import subprocess as sp
  9. import threading
  10. import time
  11. import traceback
  12. from abc import ABC, abstractmethod
  13. from multiprocessing import shared_memory
  14. from typing import AnyStr
  15. import cv2
  16. import matplotlib.pyplot as plt
  17. import numpy as np
  18. logger = logging.getLogger(__name__)
  19. def draw_box_with_label(
  20. frame,
  21. x_min,
  22. y_min,
  23. x_max,
  24. y_max,
  25. label,
  26. info,
  27. thickness=2,
  28. color=None,
  29. position="ul",
  30. ):
  31. if color is None:
  32. color = (0, 0, 255)
  33. display_text = "{}: {}".format(label, info)
  34. cv2.rectangle(frame, (x_min, y_min), (x_max, y_max), color, thickness)
  35. font_scale = 0.5
  36. font = cv2.FONT_HERSHEY_SIMPLEX
  37. # get the width and height of the text box
  38. size = cv2.getTextSize(display_text, font, fontScale=font_scale, thickness=2)
  39. text_width = size[0][0]
  40. text_height = size[0][1]
  41. line_height = text_height + size[1]
  42. # set the text start position
  43. if position == "ul":
  44. text_offset_x = x_min
  45. text_offset_y = 0 if y_min < line_height else y_min - (line_height + 8)
  46. elif position == "ur":
  47. text_offset_x = x_max - (text_width + 8)
  48. text_offset_y = 0 if y_min < line_height else y_min - (line_height + 8)
  49. elif position == "bl":
  50. text_offset_x = x_min
  51. text_offset_y = y_max
  52. elif position == "br":
  53. text_offset_x = x_max - (text_width + 8)
  54. text_offset_y = y_max
  55. # make the coords of the box with a small padding of two pixels
  56. textbox_coords = (
  57. (text_offset_x, text_offset_y),
  58. (text_offset_x + text_width + 2, text_offset_y + line_height),
  59. )
  60. cv2.rectangle(frame, textbox_coords[0], textbox_coords[1], color, cv2.FILLED)
  61. cv2.putText(
  62. frame,
  63. display_text,
  64. (text_offset_x, text_offset_y + line_height - 3),
  65. font,
  66. fontScale=font_scale,
  67. color=(0, 0, 0),
  68. thickness=2,
  69. )
  70. def calculate_region(frame_shape, xmin, ymin, xmax, ymax, multiplier=2):
  71. # size is the longest edge and divisible by 4
  72. size = int(max(xmax - xmin, ymax - ymin) // 4 * 4 * multiplier)
  73. # dont go any smaller than 300
  74. if size < 300:
  75. size = 300
  76. # x_offset is midpoint of bounding box minus half the size
  77. x_offset = int((xmax - xmin) / 2.0 + xmin - size / 2.0)
  78. # if outside the image
  79. if x_offset < 0:
  80. x_offset = 0
  81. elif x_offset > (frame_shape[1] - size):
  82. x_offset = max(0, (frame_shape[1] - size))
  83. # y_offset is midpoint of bounding box minus half the size
  84. y_offset = int((ymax - ymin) / 2.0 + ymin - size / 2.0)
  85. # # if outside the image
  86. if y_offset < 0:
  87. y_offset = 0
  88. elif y_offset > (frame_shape[0] - size):
  89. y_offset = max(0, (frame_shape[0] - size))
  90. return (x_offset, y_offset, x_offset + size, y_offset + size)
  91. def get_yuv_crop(frame_shape, crop):
  92. # crop should be (x1,y1,x2,y2)
  93. frame_height = frame_shape[0] // 3 * 2
  94. frame_width = frame_shape[1]
  95. # compute the width/height of the uv channels
  96. uv_width = frame_width // 2 # width of the uv channels
  97. uv_height = frame_height // 4 # height of the uv channels
  98. # compute the offset for upper left corner of the uv channels
  99. uv_x_offset = crop[0] // 2 # x offset of the uv channels
  100. uv_y_offset = crop[1] // 4 # y offset of the uv channels
  101. # compute the width/height of the uv crops
  102. uv_crop_width = (crop[2] - crop[0]) // 2 # width of the cropped uv channels
  103. uv_crop_height = (crop[3] - crop[1]) // 4 # height of the cropped uv channels
  104. # ensure crop dimensions are multiples of 2 and 4
  105. y = (crop[0], crop[1], crop[0] + uv_crop_width * 2, crop[1] + uv_crop_height * 4)
  106. u1 = (
  107. 0 + uv_x_offset,
  108. frame_height + uv_y_offset,
  109. 0 + uv_x_offset + uv_crop_width,
  110. frame_height + uv_y_offset + uv_crop_height,
  111. )
  112. u2 = (
  113. uv_width + uv_x_offset,
  114. frame_height + uv_y_offset,
  115. uv_width + uv_x_offset + uv_crop_width,
  116. frame_height + uv_y_offset + uv_crop_height,
  117. )
  118. v1 = (
  119. 0 + uv_x_offset,
  120. frame_height + uv_height + uv_y_offset,
  121. 0 + uv_x_offset + uv_crop_width,
  122. frame_height + uv_height + uv_y_offset + uv_crop_height,
  123. )
  124. v2 = (
  125. uv_width + uv_x_offset,
  126. frame_height + uv_height + uv_y_offset,
  127. uv_width + uv_x_offset + uv_crop_width,
  128. frame_height + uv_height + uv_y_offset + uv_crop_height,
  129. )
  130. return y, u1, u2, v1, v2
  131. def yuv_crop_and_resize(frame, region, height=None):
  132. # Crops and resizes a YUV frame while maintaining aspect ratio
  133. # https://stackoverflow.com/a/57022634
  134. height = frame.shape[0] // 3 * 2
  135. width = frame.shape[1]
  136. # get the crop box if the region extends beyond the frame
  137. crop_x1 = max(0, region[0])
  138. crop_y1 = max(0, region[1])
  139. # ensure these are a multiple of 4
  140. crop_x2 = min(width, region[2])
  141. crop_y2 = min(height, region[3])
  142. crop_box = (crop_x1, crop_y1, crop_x2, crop_y2)
  143. y, u1, u2, v1, v2 = get_yuv_crop(frame.shape, crop_box)
  144. # if the region starts outside the frame, indent the start point in the cropped frame
  145. y_channel_x_offset = abs(min(0, region[0]))
  146. y_channel_y_offset = abs(min(0, region[1]))
  147. uv_channel_x_offset = y_channel_x_offset // 2
  148. uv_channel_y_offset = y_channel_y_offset // 4
  149. # create the yuv region frame
  150. # make sure the size is a multiple of 4
  151. # TODO: this should be based on the size after resize now
  152. size = (region[3] - region[1]) // 4 * 4
  153. yuv_cropped_frame = np.zeros((size + size // 2, size), np.uint8)
  154. # fill in black
  155. yuv_cropped_frame[:] = 128
  156. yuv_cropped_frame[0:size, 0:size] = 16
  157. # copy the y channel
  158. yuv_cropped_frame[
  159. y_channel_y_offset : y_channel_y_offset + y[3] - y[1],
  160. y_channel_x_offset : y_channel_x_offset + y[2] - y[0],
  161. ] = frame[y[1] : y[3], y[0] : y[2]]
  162. uv_crop_width = u1[2] - u1[0]
  163. uv_crop_height = u1[3] - u1[1]
  164. # copy u1
  165. yuv_cropped_frame[
  166. size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height,
  167. 0 + uv_channel_x_offset : 0 + uv_channel_x_offset + uv_crop_width,
  168. ] = frame[u1[1] : u1[3], u1[0] : u1[2]]
  169. # copy u2
  170. yuv_cropped_frame[
  171. size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height,
  172. size // 2
  173. + uv_channel_x_offset : size // 2
  174. + uv_channel_x_offset
  175. + uv_crop_width,
  176. ] = frame[u2[1] : u2[3], u2[0] : u2[2]]
  177. # copy v1
  178. yuv_cropped_frame[
  179. size
  180. + size // 4
  181. + uv_channel_y_offset : size
  182. + size // 4
  183. + uv_channel_y_offset
  184. + uv_crop_height,
  185. 0 + uv_channel_x_offset : 0 + uv_channel_x_offset + uv_crop_width,
  186. ] = frame[v1[1] : v1[3], v1[0] : v1[2]]
  187. # copy v2
  188. yuv_cropped_frame[
  189. size
  190. + size // 4
  191. + uv_channel_y_offset : size
  192. + size // 4
  193. + uv_channel_y_offset
  194. + uv_crop_height,
  195. size // 2
  196. + uv_channel_x_offset : size // 2
  197. + uv_channel_x_offset
  198. + uv_crop_width,
  199. ] = frame[v2[1] : v2[3], v2[0] : v2[2]]
  200. return yuv_cropped_frame
  201. def copy_yuv_to_position(
  202. position,
  203. destination_frame,
  204. destination_dim,
  205. source_frame=None,
  206. source_channel_dim=None,
  207. ):
  208. # TODO: consider calculating this on layout reflow instead of all the time
  209. layout_shape = (
  210. (destination_frame.shape[0] // 3 * 2) // destination_dim,
  211. destination_frame.shape[1] // destination_dim,
  212. )
  213. # calculate the x and y offset for the frame in the layout
  214. y_offset = layout_shape[0] * math.floor(position / destination_dim)
  215. x_offset = layout_shape[1] * (position % destination_dim)
  216. # get the coordinates of the channels for this position in the layout
  217. y, u1, u2, v1, v2 = get_yuv_crop(
  218. destination_frame.shape,
  219. (
  220. x_offset,
  221. y_offset,
  222. x_offset + layout_shape[1],
  223. y_offset + layout_shape[0],
  224. ),
  225. )
  226. if source_frame is None:
  227. # clear y
  228. destination_frame[
  229. y[1] : y[3],
  230. y[0] : y[2],
  231. ] = 16
  232. # clear u1
  233. destination_frame[u1[1] : u1[3], u1[0] : u1[2]] = 128
  234. # clear u2
  235. destination_frame[u2[1] : u2[3], u2[0] : u2[2]] = 128
  236. # clear v1
  237. destination_frame[v1[1] : v1[3], v1[0] : v1[2]] = 128
  238. # clear v2
  239. destination_frame[v2[1] : v2[3], v2[0] : v2[2]] = 128
  240. else:
  241. interpolation = cv2.INTER_AREA
  242. # resize/copy y channel
  243. destination_frame[y[1] : y[3], y[0] : y[2]] = cv2.resize(
  244. source_frame[
  245. source_channel_dim["y"][1] : source_channel_dim["y"][3],
  246. source_channel_dim["y"][0] : source_channel_dim["y"][2],
  247. ],
  248. dsize=(y[2] - y[0], y[3] - y[1]),
  249. interpolation=interpolation,
  250. )
  251. # resize/copy u1
  252. destination_frame[u1[1] : u1[3], u1[0] : u1[2]] = cv2.resize(
  253. source_frame[
  254. source_channel_dim["u1"][1] : source_channel_dim["u1"][3],
  255. source_channel_dim["u1"][0] : source_channel_dim["u1"][2],
  256. ],
  257. dsize=(u1[2] - u1[0], u1[3] - u1[1]),
  258. interpolation=interpolation,
  259. )
  260. # resize/copy u2
  261. destination_frame[u2[1] : u2[3], u2[0] : u2[2]] = cv2.resize(
  262. source_frame[
  263. source_channel_dim["u2"][1] : source_channel_dim["u2"][3],
  264. source_channel_dim["u2"][0] : source_channel_dim["u2"][2],
  265. ],
  266. dsize=(u2[2] - u2[0], u2[3] - u2[1]),
  267. interpolation=interpolation,
  268. )
  269. # resize/copy v1
  270. destination_frame[v1[1] : v1[3], v1[0] : v1[2]] = cv2.resize(
  271. source_frame[
  272. source_channel_dim["v1"][1] : source_channel_dim["v1"][3],
  273. source_channel_dim["v1"][0] : source_channel_dim["v1"][2],
  274. ],
  275. dsize=(v1[2] - v1[0], v1[3] - v1[1]),
  276. interpolation=interpolation,
  277. )
  278. # resize/copy v2
  279. destination_frame[v2[1] : v2[3], v2[0] : v2[2]] = cv2.resize(
  280. source_frame[
  281. source_channel_dim["v2"][1] : source_channel_dim["v2"][3],
  282. source_channel_dim["v2"][0] : source_channel_dim["v2"][2],
  283. ],
  284. dsize=(v2[2] - v2[0], v2[3] - v2[1]),
  285. interpolation=interpolation,
  286. )
  287. def yuv_region_2_rgb(frame, region):
  288. try:
  289. # TODO: does this copy the numpy array?
  290. yuv_cropped_frame = yuv_crop_and_resize(frame, region)
  291. return cv2.cvtColor(yuv_cropped_frame, cv2.COLOR_YUV2RGB_I420)
  292. except:
  293. print(f"frame.shape: {frame.shape}")
  294. print(f"region: {region}")
  295. raise
  296. def intersection(box_a, box_b):
  297. return (
  298. max(box_a[0], box_b[0]),
  299. max(box_a[1], box_b[1]),
  300. min(box_a[2], box_b[2]),
  301. min(box_a[3], box_b[3]),
  302. )
  303. def area(box):
  304. return (box[2] - box[0] + 1) * (box[3] - box[1] + 1)
  305. def intersection_over_union(box_a, box_b):
  306. # determine the (x, y)-coordinates of the intersection rectangle
  307. intersect = intersection(box_a, box_b)
  308. # compute the area of intersection rectangle
  309. inter_area = max(0, intersect[2] - intersect[0] + 1) * max(
  310. 0, intersect[3] - intersect[1] + 1
  311. )
  312. if inter_area == 0:
  313. return 0.0
  314. # compute the area of both the prediction and ground-truth
  315. # rectangles
  316. box_a_area = (box_a[2] - box_a[0] + 1) * (box_a[3] - box_a[1] + 1)
  317. box_b_area = (box_b[2] - box_b[0] + 1) * (box_b[3] - box_b[1] + 1)
  318. # compute the intersection over union by taking the intersection
  319. # area and dividing it by the sum of prediction + ground-truth
  320. # areas - the interesection area
  321. iou = inter_area / float(box_a_area + box_b_area - inter_area)
  322. # return the intersection over union value
  323. return iou
  324. def clipped(obj, frame_shape):
  325. # if the object is within 5 pixels of the region border, and the region is not on the edge
  326. # consider the object to be clipped
  327. box = obj[2]
  328. region = obj[4]
  329. if (
  330. (region[0] > 5 and box[0] - region[0] <= 5)
  331. or (region[1] > 5 and box[1] - region[1] <= 5)
  332. or (frame_shape[1] - region[2] > 5 and region[2] - box[2] <= 5)
  333. or (frame_shape[0] - region[3] > 5 and region[3] - box[3] <= 5)
  334. ):
  335. return True
  336. else:
  337. return False
  338. class EventsPerSecond:
  339. def __init__(self, max_events=1000):
  340. self._start = None
  341. self._max_events = max_events
  342. self._timestamps = []
  343. def start(self):
  344. self._start = datetime.datetime.now().timestamp()
  345. def update(self):
  346. if self._start is None:
  347. self.start()
  348. self._timestamps.append(datetime.datetime.now().timestamp())
  349. # truncate the list when it goes 100 over the max_size
  350. if len(self._timestamps) > self._max_events + 100:
  351. self._timestamps = self._timestamps[(1 - self._max_events) :]
  352. def eps(self, last_n_seconds=10):
  353. if self._start is None:
  354. self.start()
  355. # compute the (approximate) events in the last n seconds
  356. now = datetime.datetime.now().timestamp()
  357. seconds = min(now - self._start, last_n_seconds)
  358. return (
  359. len([t for t in self._timestamps if t > (now - last_n_seconds)]) / seconds
  360. )
  361. def print_stack(sig, frame):
  362. traceback.print_stack(frame)
  363. def listen():
  364. signal.signal(signal.SIGUSR1, print_stack)
  365. def create_mask(frame_shape, mask):
  366. mask_img = np.zeros(frame_shape, np.uint8)
  367. mask_img[:] = 255
  368. if isinstance(mask, list):
  369. for m in mask:
  370. add_mask(m, mask_img)
  371. elif isinstance(mask, str):
  372. add_mask(mask, mask_img)
  373. return mask_img
  374. def add_mask(mask, mask_img):
  375. points = mask.split(",")
  376. contour = np.array(
  377. [[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)]
  378. )
  379. cv2.fillPoly(mask_img, pts=[contour], color=(0))
  380. class FrameManager(ABC):
  381. @abstractmethod
  382. def create(self, name, size) -> AnyStr:
  383. pass
  384. @abstractmethod
  385. def get(self, name, timeout_ms=0):
  386. pass
  387. @abstractmethod
  388. def close(self, name):
  389. pass
  390. @abstractmethod
  391. def delete(self, name):
  392. pass
  393. class DictFrameManager(FrameManager):
  394. def __init__(self):
  395. self.frames = {}
  396. def create(self, name, size) -> AnyStr:
  397. mem = bytearray(size)
  398. self.frames[name] = mem
  399. return mem
  400. def get(self, name, shape):
  401. mem = self.frames[name]
  402. return np.ndarray(shape, dtype=np.uint8, buffer=mem)
  403. def close(self, name):
  404. pass
  405. def delete(self, name):
  406. del self.frames[name]
  407. class SharedMemoryFrameManager(FrameManager):
  408. def __init__(self):
  409. self.shm_store = {}
  410. def create(self, name, size) -> AnyStr:
  411. shm = shared_memory.SharedMemory(name=name, create=True, size=size)
  412. self.shm_store[name] = shm
  413. return shm.buf
  414. def get(self, name, shape):
  415. if name in self.shm_store:
  416. shm = self.shm_store[name]
  417. else:
  418. shm = shared_memory.SharedMemory(name=name)
  419. self.shm_store[name] = shm
  420. return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf)
  421. def close(self, name):
  422. if name in self.shm_store:
  423. self.shm_store[name].close()
  424. del self.shm_store[name]
  425. def delete(self, name):
  426. if name in self.shm_store:
  427. self.shm_store[name].close()
  428. self.shm_store[name].unlink()
  429. del self.shm_store[name]