qr_scan_node.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """Undistort camera frames, detect QR boxes on RK3588 NPU, and decode on demand."""
  4. from pathlib import Path
  5. import cv2
  6. import numpy as np
  7. import requests
  8. import rospy
  9. import yaml
  10. from cv_bridge import CvBridge
  11. from pyzbar.pyzbar import decode
  12. from sensor_msgs.msg import Image
  13. from std_msgs.msg import Float32MultiArray, String
  14. from std_srvs.srv import SetBool, SetBoolResponse, Trigger, TriggerResponse
  15. from qr_npu_detector import RKNNQrDetector
  16. class QRCodeScanner(object):
  17. """Three explicit task modes: idle, discovery, and decode."""
  18. def __init__(self):
  19. rospy.init_node("qr_code_scanner", anonymous=True)
  20. self.bridge = CvBridge()
  21. default_calibration = Path(__file__).resolve().parent.parent / "config" / "head_camera.yaml"
  22. self.calibration_file = Path(rospy.get_param("~calibration_file", str(default_calibration)))
  23. self.camera_matrix = None
  24. self.distortion_coefficients = None
  25. self.calibration_width = None
  26. self.calibration_height = None
  27. self._load_calibration()
  28. self.scanned_results = []
  29. self.scanned_urls = set()
  30. self.decode_enabled = False
  31. default_model = Path(__file__).resolve().parent.parent / "models" / "qr_detector.rknn"
  32. self.npu_crop_expand_ratio = float(rospy.get_param("~npu_crop_expand_ratio", 0.30))
  33. self.npu_crop_scale = float(rospy.get_param("~npu_crop_scale", 2.0))
  34. self.npu_detector = RKNNQrDetector(
  35. model_file=rospy.get_param("~npu_model_file", str(default_model)),
  36. input_size=rospy.get_param("~npu_input_size", 640),
  37. confidence_threshold=rospy.get_param("~npu_confidence_threshold", 0.35),
  38. nms_threshold=rospy.get_param("~npu_nms_threshold", 0.45),
  39. min_interval_seconds=rospy.get_param("~npu_min_interval_seconds", 0.20),
  40. )
  41. # RKNNLite must initialize on this main thread, not in the SetBool
  42. # service callback that task1 invokes at center C.
  43. self.npu_detector.prepare()
  44. rospy.loginfo("二维码RKNN运行时已在主线程初始化,当前仍为idle模式")
  45. rospy.on_shutdown(self.npu_detector.close)
  46. self.image_sub = rospy.Subscriber("/usb_cam/image_raw", Image, self.image_callback, queue_size=1)
  47. self.result_pub = rospy.Publisher("/qr_scan_result", String, queue_size=10)
  48. self.detection_pub = rospy.Publisher("/qr_scan/npu_detection", Float32MultiArray, queue_size=1)
  49. self.debug_image_pub = rospy.Publisher("/qr_scan/debug_image", Image, queue_size=1)
  50. self.reset_service = rospy.Service("/qr_scan/reset", Trigger, self.reset_callback)
  51. self.npu_enable_service = rospy.Service(
  52. "/qr_scan/set_npu_enabled", SetBool, self.npu_enable_callback
  53. )
  54. self.decode_enable_service = rospy.Service(
  55. "/qr_scan/set_decode_enabled", SetBool, self.decode_enable_callback
  56. )
  57. rospy.loginfo("QR Code Scanner节点已启动(idle模式)")
  58. def _load_calibration(self):
  59. try:
  60. with self.calibration_file.open("r", encoding="utf-8") as stream:
  61. calibration = yaml.safe_load(stream)
  62. self.calibration_width = int(calibration["image_width"])
  63. self.calibration_height = int(calibration["image_height"])
  64. self.camera_matrix = np.asarray(
  65. calibration["camera_matrix"]["data"], dtype=np.float64
  66. ).reshape(3, 3)
  67. self.distortion_coefficients = np.asarray(
  68. calibration["distortion_coefficients"]["data"], dtype=np.float64
  69. ).reshape(-1, 1)
  70. except (OSError, KeyError, TypeError, ValueError, yaml.YAMLError) as error:
  71. rospy.logfatal("无法加载相机标定文件 %s: %s", self.calibration_file, error)
  72. raise
  73. rospy.loginfo("已加载相机标定: %s (%dx%d, %s)", self.calibration_file,
  74. self.calibration_width, self.calibration_height,
  75. calibration.get("distortion_model", "unknown"))
  76. def reset_callback(self, _request):
  77. self.scanned_results = []
  78. self.scanned_urls.clear()
  79. self.decode_enabled = False
  80. self.npu_detector.set_enabled(False)
  81. rospy.loginfo("二维码扫描记录已重置,已切换到idle模式")
  82. return TriggerResponse(success=True, message="qr scan state reset")
  83. def npu_enable_callback(self, request):
  84. try:
  85. self.npu_detector.set_enabled(request.data)
  86. except Exception as error:
  87. rospy.logerr("无法切换二维码NPU检测: %s", error)
  88. return SetBoolResponse(success=False, message=str(error))
  89. state = "开启" if request.data else "关闭"
  90. rospy.loginfo("二维码NPU检测已%s", state)
  91. return SetBoolResponse(success=True, message="NPU QR detection %s" % state)
  92. def decode_enable_callback(self, request):
  93. self.decode_enabled = bool(request.data)
  94. state = "decode" if self.decode_enabled else "discovery/idle"
  95. rospy.loginfo("二维码URL解码已%s(当前模式%s)", "开启" if self.decode_enabled else "关闭", state)
  96. return SetBoolResponse(success=True, message="QR decode %s" % state)
  97. def _handle_decoded_url(self, url, source):
  98. if url in self.scanned_urls:
  99. return
  100. self.scanned_urls.add(url)
  101. rospy.loginfo("%s识别到二维码URL: %s", source, url)
  102. try:
  103. response = requests.get(url, timeout=5, proxies={"http": None, "https": None})
  104. json_data = response.json()
  105. if json_data.get("code") == 200:
  106. product_name = json_data.get("result", "未知")
  107. rospy.loginfo("识别到货品: %s", product_name)
  108. self.scanned_results.append(product_name)
  109. self.result_pub.publish(String(data=product_name))
  110. else:
  111. rospy.logwarn("JSON返回错误: %s", json_data)
  112. except Exception as error:
  113. rospy.logerr("请求URL失败: %s", error)
  114. @staticmethod
  115. def _draw_pyzbar_polygon(frame, obj):
  116. points = obj.polygon
  117. if len(points) > 4:
  118. hull = cv2.convexHull(np.array(points, dtype=np.float32))
  119. hull = list(map(tuple, np.squeeze(hull)))
  120. else:
  121. hull = points
  122. for index in range(len(hull)):
  123. cv2.line(frame, hull[index], hull[(index + 1) % len(hull)], (0, 255, 0), 3)
  124. if hull:
  125. cv2.putText(frame, "QR", tuple(hull[0]), cv2.FONT_HERSHEY_SIMPLEX,
  126. 0.7, (0, 255, 0), 2)
  127. def _publish_best_detection(self, detections):
  128. if not detections:
  129. return
  130. x1, y1, x2, y2, score = max(detections, key=lambda item: item[4])
  131. message = Float32MultiArray()
  132. message.data = [
  133. (x1 + x2) / 2.0, (y1 + y2) / 2.0,
  134. x1, y1, x2, y2, score,
  135. ]
  136. self.detection_pub.publish(message)
  137. def image_callback(self, data):
  138. try:
  139. frame = self.bridge.imgmsg_to_cv2(data, "bgr8")
  140. except Exception as error:
  141. rospy.logerr("图像转换失败: %s", error)
  142. return
  143. # Idle must be genuinely lightweight: do not undistort, decode, run
  144. # inference, or publish debug frames until task1 explicitly enables
  145. # discovery or decode mode. This also keeps service callbacks
  146. # responsive when the manager turns NPU detection on at center C.
  147. if not self.decode_enabled and not self.npu_detector.enabled:
  148. return
  149. if (frame.shape[1], frame.shape[0]) != (self.calibration_width, self.calibration_height):
  150. rospy.logwarn_throttle(5.0, "图像分辨率%dx%d与标定%dx%d不一致,跳过该帧",
  151. frame.shape[1], frame.shape[0],
  152. self.calibration_width, self.calibration_height)
  153. return
  154. frame = cv2.undistort(frame, self.camera_matrix, self.distortion_coefficients)
  155. decoded_objects = decode(frame) if self.decode_enabled else []
  156. for obj in decoded_objects:
  157. self._draw_pyzbar_polygon(frame, obj)
  158. try:
  159. self._handle_decoded_url(obj.data.decode("utf-8"), "pyzbar全图")
  160. except UnicodeDecodeError:
  161. continue
  162. detections = []
  163. if self.npu_detector.enabled:
  164. try:
  165. detections = self.npu_detector.detect(frame)
  166. except Exception as error:
  167. rospy.logerr_throttle(5.0, "二维码NPU推理失败: %s", error)
  168. self._publish_best_detection(detections)
  169. for detection in detections:
  170. x1, y1, x2, y2, score = detection
  171. crop_objects = []
  172. if self.decode_enabled:
  173. crop = self.npu_detector.expanded_crop(
  174. frame, detection, self.npu_crop_expand_ratio, self.npu_crop_scale
  175. )
  176. crop_objects = decode(crop) if crop is not None else []
  177. color = (0, 255, 0) if crop_objects else (0, 165, 255)
  178. cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), color, 2)
  179. cv2.putText(frame, "QR NPU %.2f" % score,
  180. (int(x1), max(20, int(y1) - 6)),
  181. cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
  182. for obj in crop_objects:
  183. try:
  184. self._handle_decoded_url(obj.data.decode("utf-8"), "NPU裁剪pyzbar")
  185. except UnicodeDecodeError:
  186. continue
  187. self.debug_image_pub.publish(self.bridge.cv2_to_imgmsg(frame, "bgr8"))
  188. if len(self.scanned_results) >= 3:
  189. rospy.loginfo_throttle(2.0, "===== 已识别商品: %s =====", self.scanned_results)
  190. def run(self):
  191. rospy.spin()
  192. if __name__ == "__main__":
  193. try:
  194. QRCodeScanner().run()
  195. except rospy.ROSInterruptException:
  196. pass