qr_npu_detector.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. #!/usr/bin/env python3
  2. """RK3588 NPU QR-code box detector used by qr_scan_node.py.
  3. The model is a single-class YOLOv5 export with a 640x640 RGB input and a
  4. 25200x6 output. Detection is explicitly disabled by default so navigation
  5. never spends NPU time on QR detection.
  6. """
  7. from __future__ import print_function
  8. import logging
  9. import time
  10. import cv2
  11. import numpy as np
  12. class RKNNQrDetector(object):
  13. """Run a single-class YOLOv5 QR detector through RKNNLite."""
  14. def __init__(
  15. self,
  16. model_file,
  17. input_size=640,
  18. confidence_threshold=0.35,
  19. nms_threshold=0.45,
  20. min_interval_seconds=0.20,
  21. ):
  22. self.model_file = str(model_file)
  23. self.input_size = int(input_size)
  24. self.confidence_threshold = float(confidence_threshold)
  25. self.nms_threshold = float(nms_threshold)
  26. self.min_interval_seconds = float(min_interval_seconds)
  27. self.enabled = False
  28. self._rknn = None
  29. self._last_inference_time = float("-inf")
  30. @property
  31. def loaded(self):
  32. return self._rknn is not None
  33. def prepare(self):
  34. """Initialize RKNN from the node's main thread before rospy.spin()."""
  35. self._ensure_loaded()
  36. def set_enabled(self, enabled):
  37. # Runtime creation is deliberately not done from a rospy service
  38. # callback thread: RKNNLite.init_runtime can block there on RK3588.
  39. if enabled and not self.loaded:
  40. raise RuntimeError("二维码NPU运行时尚未在主线程初始化")
  41. if enabled:
  42. self._last_inference_time = float("-inf")
  43. self.enabled = bool(enabled)
  44. def close(self):
  45. self.enabled = False
  46. if self._rknn is not None:
  47. self._rknn.release()
  48. self._rknn = None
  49. def _ensure_loaded(self):
  50. if self._rknn is not None:
  51. return
  52. try:
  53. # RKNNLite 1.5 changes logging level names during import.
  54. # Delay it until rospy has finished configuring its log handlers.
  55. from rknnlite.api import RKNNLite
  56. # RKNNLite 1.5 renames standard logging levels (for example
  57. # INFO -> I). rospy's roslogging only accepts standard names,
  58. # therefore restore both name-to-level and level-to-name maps.
  59. for level, name in (
  60. (logging.CRITICAL, "CRITICAL"),
  61. (logging.ERROR, "ERROR"),
  62. (logging.WARNING, "WARNING"),
  63. (logging.INFO, "INFO"),
  64. (logging.DEBUG, "DEBUG"),
  65. (logging.NOTSET, "NOTSET"),
  66. ):
  67. logging.addLevelName(level, name)
  68. logging._nameToLevel.update({
  69. "FATAL": logging.FATAL,
  70. "WARN": logging.WARNING,
  71. })
  72. except ImportError:
  73. raise RuntimeError(
  74. "未找到rknnlite;请使用venv3.9启动二维码NPU节点"
  75. )
  76. rknn = RKNNLite()
  77. result = rknn.load_rknn(self.model_file)
  78. if result != 0:
  79. raise RuntimeError("加载RKNN二维码模型失败,错误码%d" % result)
  80. result = rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
  81. if result != 0:
  82. rknn.release()
  83. raise RuntimeError("初始化RK3588 NPU失败,错误码%d" % result)
  84. self._rknn = rknn
  85. def _letterbox(self, image):
  86. height, width = image.shape[:2]
  87. size = self.input_size
  88. scale = min(float(size) / float(width), float(size) / float(height))
  89. resized_width = int(round(width * scale))
  90. resized_height = int(round(height * scale))
  91. resized = cv2.resize(
  92. image, (resized_width, resized_height), interpolation=cv2.INTER_LINEAR
  93. )
  94. pad_x = (size - resized_width) // 2
  95. pad_y = (size - resized_height) // 2
  96. padded = cv2.copyMakeBorder(
  97. resized,
  98. pad_y,
  99. size - resized_height - pad_y,
  100. pad_x,
  101. size - resized_width - pad_x,
  102. cv2.BORDER_CONSTANT,
  103. value=(114, 114, 114),
  104. )
  105. return cv2.cvtColor(padded, cv2.COLOR_BGR2RGB), scale, pad_x, pad_y
  106. def _nms_indices(self, boxes, scores):
  107. if not boxes:
  108. return []
  109. xywh_boxes = []
  110. for x1, y1, x2, y2 in boxes:
  111. xywh_boxes.append([
  112. int(round(x1)), int(round(y1)),
  113. int(round(max(0.0, x2 - x1))),
  114. int(round(max(0.0, y2 - y1))),
  115. ])
  116. indices = cv2.dnn.NMSBoxes(
  117. xywh_boxes,
  118. scores,
  119. self.confidence_threshold,
  120. self.nms_threshold,
  121. )
  122. if len(indices) == 0:
  123. return []
  124. return np.asarray(indices).reshape(-1).tolist()
  125. def detect(self, image):
  126. """Return (x1, y1, x2, y2, score) boxes in original-image pixels."""
  127. if not self.enabled or self._rknn is None:
  128. return []
  129. now = time.monotonic()
  130. if now - self._last_inference_time < self.min_interval_seconds:
  131. return []
  132. self._last_inference_time = now
  133. model_image, scale, pad_x, pad_y = self._letterbox(image)
  134. outputs = self._rknn.inference(
  135. inputs=[model_image],
  136. data_format="nhwc",
  137. )
  138. if len(outputs) != 1:
  139. raise RuntimeError("二维码NPU输出数量异常: %d" % len(outputs))
  140. prediction = np.asarray(outputs[0]).squeeze()
  141. if prediction.ndim != 2 or prediction.shape[1] != 6:
  142. raise RuntimeError(
  143. "二维码NPU输出形状异常: %s" % (np.asarray(outputs[0]).shape,)
  144. )
  145. scores = prediction[:, 4] * prediction[:, 5]
  146. selected = np.where(scores >= self.confidence_threshold)[0]
  147. boxes = []
  148. candidate_scores = []
  149. for index in selected:
  150. center_x, center_y, box_width, box_height = prediction[index, :4]
  151. boxes.append((
  152. center_x - box_width / 2.0,
  153. center_y - box_height / 2.0,
  154. center_x + box_width / 2.0,
  155. center_y + box_height / 2.0,
  156. ))
  157. candidate_scores.append(float(scores[index]))
  158. image_height, image_width = image.shape[:2]
  159. detections = []
  160. for index in self._nms_indices(boxes, candidate_scores):
  161. x1, y1, x2, y2 = boxes[index]
  162. x1 = max(0.0, min(float(image_width), (x1 - pad_x) / scale))
  163. y1 = max(0.0, min(float(image_height), (y1 - pad_y) / scale))
  164. x2 = max(0.0, min(float(image_width), (x2 - pad_x) / scale))
  165. y2 = max(0.0, min(float(image_height), (y2 - pad_y) / scale))
  166. if x2 - x1 >= 8.0 and y2 - y1 >= 8.0:
  167. detections.append((x1, y1, x2, y2, candidate_scores[index]))
  168. return detections
  169. @staticmethod
  170. def expanded_crop(image, detection, expand_ratio, scale):
  171. """Return an expanded, optionally enlarged crop for pyzbar decoding."""
  172. x1, y1, x2, y2 = detection[:4]
  173. image_height, image_width = image.shape[:2]
  174. expand_x = (x2 - x1) * float(expand_ratio)
  175. expand_y = (y2 - y1) * float(expand_ratio)
  176. left = max(0, int(round(x1 - expand_x)))
  177. top = max(0, int(round(y1 - expand_y)))
  178. right = min(image_width, int(round(x2 + expand_x)))
  179. bottom = min(image_height, int(round(y2 + expand_y)))
  180. if right <= left or bottom <= top:
  181. return None
  182. crop = image[top:bottom, left:right]
  183. if float(scale) > 1.0:
  184. crop = cv2.resize(
  185. crop,
  186. None,
  187. fx=float(scale),
  188. fy=float(scale),
  189. interpolation=cv2.INTER_CUBIC,
  190. )
  191. return crop