snapshot_capture.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. #!/usr/bin/env python3
  2. """Capture paired camera and white-line-mask screenshots from an existing ROS topic."""
  3. from pathlib import Path
  4. import threading
  5. import time
  6. import cv2
  7. import numpy as np
  8. import rospy
  9. from cv_bridge import CvBridge, CvBridgeError
  10. from sensor_msgs.msg import Image
  11. class SnapshotCapture:
  12. """A subscriber-only capture tool: it never opens /dev/video* itself."""
  13. def __init__(self):
  14. rospy.init_node("line_snapshot_capture")
  15. self.image_topic = rospy.get_param("~image_topic", "/usb_cam/image_raw")
  16. default_root = Path(__file__).resolve().parent.parent / "data"
  17. self.save_root = Path(rospy.get_param("~save_root", str(default_root))).expanduser()
  18. self.lower = self._read_hsv("~hsv_lower", [0, 0, 180])
  19. self.upper = self._read_hsv("~hsv_upper", [180, 60, 255])
  20. self.raw_dir = self.save_root / "raw"
  21. self.mask_dir = self.save_root / "mask"
  22. self.raw_dir.mkdir(parents=True, exist_ok=True)
  23. self.mask_dir.mkdir(parents=True, exist_ok=True)
  24. self.bridge = CvBridge()
  25. self.lock = threading.Lock()
  26. self.raw_image = None
  27. self.mask_image = None
  28. self.subscriber = rospy.Subscriber(
  29. self.image_topic, Image, self.image_callback, queue_size=1
  30. )
  31. rospy.loginfo(
  32. "Line snapshot capture subscribes to %s; saving pairs under %s",
  33. self.image_topic,
  34. self.save_root,
  35. )
  36. @staticmethod
  37. def _read_hsv(param_name, default):
  38. values = rospy.get_param(param_name, default)
  39. if not isinstance(values, (list, tuple)) or len(values) != 3:
  40. rospy.logwarn("%s must contain three numbers; using %s", param_name, default)
  41. values = default
  42. values = [int(max(0, min(255, value))) for value in values]
  43. values[0] = min(180, values[0])
  44. return np.array(values, dtype=np.uint8)
  45. def image_callback(self, message):
  46. try:
  47. raw = self.bridge.imgmsg_to_cv2(message, desired_encoding="bgr8")
  48. except CvBridgeError as error:
  49. rospy.logwarn_throttle(2.0, "Unable to convert camera image: %s", error)
  50. return
  51. hsv = cv2.cvtColor(raw, cv2.COLOR_BGR2HSV)
  52. mask = cv2.inRange(hsv, self.lower, self.upper)
  53. with self.lock:
  54. self.raw_image = raw.copy()
  55. self.mask_image = mask.copy()
  56. def save_pair(self):
  57. with self.lock:
  58. if self.raw_image is None or self.mask_image is None:
  59. rospy.logwarn("No camera frame received yet; nothing was saved.")
  60. return
  61. raw = self.raw_image.copy()
  62. mask = self.mask_image.copy()
  63. timestamp = time.time_ns()
  64. raw_path = self.raw_dir / ("line_%d_raw.png" % timestamp)
  65. mask_path = self.mask_dir / ("line_%d_mask.png" % timestamp)
  66. raw_ok = cv2.imwrite(str(raw_path), raw)
  67. mask_ok = cv2.imwrite(str(mask_path), mask)
  68. if raw_ok and mask_ok:
  69. rospy.loginfo("Saved snapshot pair: raw=%s mask=%s", raw_path, mask_path)
  70. else:
  71. rospy.logerr("Failed to save snapshot pair: raw=%s mask=%s", raw_path, mask_path)
  72. def run(self):
  73. window_name = "Line follower snapshot capture (S: save, Q/Esc: quit)"
  74. cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
  75. rate = rospy.Rate(30)
  76. while not rospy.is_shutdown():
  77. with self.lock:
  78. raw = None if self.raw_image is None else self.raw_image.copy()
  79. mask = None if self.mask_image is None else self.mask_image.copy()
  80. if raw is not None and mask is not None:
  81. mask_bgr = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
  82. display = np.hstack((raw, mask_bgr))
  83. width = raw.shape[1]
  84. cv2.putText(display, "RAW: %s" % self.image_topic, (12, 28),
  85. cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
  86. cv2.putText(display, "WHITE MASK HSV %s-%s" % (self.lower.tolist(), self.upper.tolist()),
  87. (width + 12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
  88. cv2.putText(display, "S: save pair Q/Esc: quit", (12, display.shape[0] - 16),
  89. cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 255), 2)
  90. cv2.imshow(window_name, display)
  91. key = cv2.waitKey(1) & 0xFF
  92. if key in (ord("s"), ord("S")):
  93. self.save_pair()
  94. elif key in (ord("q"), ord("Q"), 27):
  95. rospy.loginfo("Line snapshot capture stopped by keyboard.")
  96. break
  97. rate.sleep()
  98. cv2.destroyAllWindows()
  99. if __name__ == "__main__":
  100. try:
  101. SnapshotCapture().run()
  102. except rospy.ROSInterruptException:
  103. pass