detector.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import cv2
  2. import numpy as np
  3. import typing as T
  4. from munch import munchify
  5. from collections import namedtuple
  6. from skimage import filters
  7. # the coordinates are relative!
  8. BBox = namedtuple("BBox", "x0 y0 x1 y1")
  9. BBoxInfo = namedtuple("BBoxInfo", "area ratio mean std selected", defaults=[-1, -1, -1, -1, False])
  10. Detection = namedtuple("Detection", "bbox info")
  11. class Detector(object):
  12. def __init__(self, configuration: T.Dict[str, T.Dict]) -> None:
  13. super().__init__()
  14. config = munchify(configuration)
  15. self.scale: float = config.preprocess.scale
  16. self.min_size: int = config.preprocess.min_size
  17. self.sigma: float = config.preprocess.sigma
  18. self.block_size_scale: float = config.threshold.block_size_scale
  19. self.dilate_iterations: int = config.postprocess.dilate_iterations
  20. self.kernel_size: int = config.postprocess.kernel_size
  21. def __call__(self, im: np.ndarray) -> T.List[Detection]:
  22. _im = self.rescale(im)
  23. im0 = self.preprocess(_im)
  24. im1 = self.threshold(im0)
  25. im2 = self.postprocess(im1)
  26. bboxes = self.detect(im2)
  27. return self.postprocess_boxes(_im, bboxes)
  28. def detect(self, im: np.ndarray) -> T.List[BBox]:
  29. contours, hierarchy = cv2.findContours(im, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
  30. contours = sorted(contours, key=cv2.contourArea, reverse=True)
  31. return [_contour2bbox(c, im.shape) for c in contours]
  32. def rescale(self, im: np.ndarray) -> np.ndarray:
  33. H, W = im.shape
  34. _scale = self.min_size / min(H, W)
  35. scale = max(self.scale, min(1, _scale))
  36. size = int(W * scale), int(H * scale)
  37. return cv2.resize(im, dsize=size)
  38. def preprocess(self, im: np.ndarray) -> np.ndarray:
  39. res = filters.gaussian(im, sigma=self.sigma, preserve_range=True)
  40. return res.astype(im.dtype)
  41. def threshold(self, im: np.ndarray) -> np.ndarray:
  42. block_size_scale = self.block_size_scale
  43. # make block size an odd number
  44. block_size = min(im.shape) * block_size_scale // 2 * 2 + 1
  45. thresh = filters.threshold_local(im,
  46. block_size=block_size,
  47. mode="constant",
  48. )
  49. max_value = 255
  50. bin_im = ((im > thresh) * max_value).astype(np.uint8)
  51. return max_value - bin_im
  52. def postprocess(self, im: np.ndarray) -> np.ndarray:
  53. kernel_size = self.kernel_size
  54. iterations = self.dilate_iterations
  55. kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
  56. im = cv2.morphologyEx(im, cv2.MORPH_OPEN, kernel)
  57. im = cv2.morphologyEx(im, cv2.MORPH_CLOSE, kernel)
  58. if iterations >= 1:
  59. im = cv2.erode(im, kernel, iterations=iterations)
  60. im = cv2.dilate(im, kernel, iterations=iterations)
  61. return im
  62. def postprocess_boxes(self, im: np.ndarray, bboxes: T.List[BBox]):
  63. detections = [Detection(bbox, BBoxInfo()) for bbox in bboxes]
  64. _im = im.astype(np.float64) / 255.
  65. integral, integral_sq = cv2.integral2(_im)
  66. # im_mean, im_std, im_n = _im_mean_std(integral, integral_sq)
  67. inds = cv2.dnn.NMSBoxes([[x0, y0, x1-x0, y1-y0] for (x0, y0, x1, y1) in bboxes],
  68. np.ones(len(bboxes), dtype=np.float32),
  69. score_threshold=0.99,
  70. nms_threshold=0.1,
  71. )
  72. # calculate the BBoxInfos only for the selected and update the detections
  73. for i in inds.squeeze():
  74. bbox, _ = detections[i]
  75. mean, std, n = _im_mean_std(integral, integral_sq, bbox)
  76. area, ratio = _area(bbox), _ratio(bbox)
  77. selected = self.is_selected(mean, std, ratio, area)
  78. info = BBoxInfo(mean, std, area, ratio, selected)
  79. detections[i] = Detection(bbox, info)
  80. return detections
  81. def is_selected(self, mean: float, std: float, ratio: float, area: float) -> bool:
  82. # Caution, here are some magic numbers!
  83. return \
  84. std >= 5e-2 and \
  85. ratio >= 2.5e-1 and \
  86. 4e-4 <= area <= 1/9
  87. def _contour2bbox(contour: np.ndarray, shape: T.Tuple[int, int]) -> BBox:
  88. """ Gets the maximal extent of a contour and translates it to a bounding box. """
  89. x0, y0 = contour.min(axis=0)[0].astype(np.int32)
  90. x1, y1 = contour.max(axis=0)[0].astype(np.int32)
  91. h, w = shape
  92. return BBox(x0/w, y0/h, x1/w, y1/h)
  93. def _ratio(bbox: BBox) -> float:
  94. x0, y0, x1, y1 = bbox
  95. h, w = y1-y0, x1-x0
  96. return min(h, w) / max(h, w)
  97. def _area(bbox: BBox) -> float:
  98. x0, y0, x1, y1 = bbox
  99. h, w = y1-y0, x1-x0
  100. return h * w
  101. def _im_mean_std(integral: np.ndarray,
  102. integral_sq: np.ndarray,
  103. bbox: T.Optional[BBox] = None
  104. ) -> T.Tuple[float, float, int]:
  105. h, w = integral.shape[0] - 1, integral.shape[1] - 1
  106. if bbox is None:
  107. arr_sum = integral[-1, -1]
  108. arr_sum_sq = integral_sq[-1, -1]
  109. N = h * w
  110. else:
  111. x0, y0, x1, y1 = bbox
  112. x0, x1 = int(x0 * w), int(x1 * w)
  113. y0, y1 = int(y0 * h), int(y1 * h)
  114. A, B, C, D = (y0,x0), (y1,x0), (y0,x1), (y1,x1)
  115. arr_sum = integral[D] + integral[A] - integral[B] - integral[C]
  116. arr_sum_sq = integral_sq[D] + integral_sq[A] - integral_sq[B] - integral_sq[C]
  117. N = (x1-x0) * (y1-y0)
  118. arr_mean = arr_sum / N
  119. arr_std = np.sqrt((arr_sum_sq - (arr_sum**2) / N) / N)
  120. return arr_mean, arr_std, N