cargo_service.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import os
  2. import threading
  3. import time
  4. from typing import Any, Dict, Optional
  5. import cv2
  6. import numpy as np
  7. from api_config import ApiConfig
  8. from depth_common import (
  9. TemporalFilter,
  10. compute_roi_bounds,
  11. extract_depth_data,
  12. find_nearest_point,
  13. init_depth_pipeline,
  14. nearest_distance_in_roi,
  15. )
  16. from utils import frame_to_bgr_image
  17. class CargoHeightService:
  18. def __init__(self, config: ApiConfig) -> None:
  19. self.config = config
  20. self._pipeline = None
  21. self._depth_intrinsics = None
  22. self._temporal_filter = None
  23. self._lock = threading.Lock()
  24. def startup(self) -> None:
  25. if self._pipeline is not None:
  26. return
  27. try:
  28. pipeline, depth_intrinsics, _ = init_depth_pipeline()
  29. except Exception as exc:
  30. raise RuntimeError(f"Failed to init depth camera: {exc}") from exc
  31. self._pipeline = pipeline
  32. self._depth_intrinsics = depth_intrinsics
  33. self._temporal_filter = TemporalFilter(alpha=0.5)
  34. def shutdown(self) -> None:
  35. if self._pipeline is None:
  36. return
  37. self._pipeline.stop()
  38. self._pipeline = None
  39. def measure_height(self) -> Optional[Dict[str, Any]]:
  40. start_time = time.time()
  41. samples = []
  42. first_valid_sample = None
  43. first_color_frame = None
  44. with self._lock:
  45. while len(samples) < self.config.sample_count and (time.time() - start_time) < self.config.sample_timeout_sec:
  46. sample = self._measure_once()
  47. if sample is None:
  48. continue
  49. samples.append(sample["nearest_distance"])
  50. if first_valid_sample is None:
  51. first_valid_sample = sample
  52. if first_color_frame is None and sample.get("color_frame") is not None:
  53. first_color_frame = sample.get("color_frame")
  54. if first_color_frame is None:
  55. for _ in range(5):
  56. frames = self._pipeline.wait_for_frames(self.config.frame_timeout_ms)
  57. if frames is None:
  58. continue
  59. color_frame = frames.get_color_frame()
  60. if color_frame is not None:
  61. first_color_frame = color_frame
  62. break
  63. if first_valid_sample is not None:
  64. if first_color_frame is not None:
  65. first_valid_sample["color_frame"] = first_color_frame
  66. self._save_current_sample_images(first_valid_sample)
  67. if len(samples) < self.config.sample_count:
  68. return None
  69. median_value = int(np.median(np.array(samples, dtype=np.int32)))
  70. return {
  71. "height_mm": median_value,
  72. "samples": samples,
  73. "unit": "mm",
  74. "sample_count": self.config.sample_count,
  75. }
  76. def _measure_once(self) -> Optional[Dict[str, Any]]:
  77. frames = self._pipeline.wait_for_frames(self.config.frame_timeout_ms)
  78. if frames is None:
  79. return None
  80. color_frame = frames.get_color_frame()
  81. depth_frame = frames.get_depth_frame()
  82. depth_data = extract_depth_data(depth_frame, self.config.settings, self._temporal_filter)
  83. if depth_data is None:
  84. return None
  85. bounds = compute_roi_bounds(depth_data, self._depth_intrinsics, self.config.settings)
  86. if bounds is None:
  87. return None
  88. x_start, x_end, y_start, y_end, center_distance = bounds
  89. roi = depth_data[y_start:y_end, x_start:x_end]
  90. nearest_distance = nearest_distance_in_roi(roi, self.config.settings)
  91. if nearest_distance is None:
  92. return None
  93. return {
  94. "nearest_distance": nearest_distance,
  95. "color_frame": color_frame,
  96. "depth_data": depth_data,
  97. "bounds": bounds,
  98. "center_distance": center_distance,
  99. }
  100. def _save_current_sample_images(self, sample: Dict[str, Any]) -> None:
  101. save_image_dir = os.path.join(os.getcwd(), "sample_images")
  102. os.makedirs(save_image_dir, exist_ok=True)
  103. now = time.localtime()
  104. time_str = time.strftime("%Y%m%d_%H%M%S", now)
  105. millis = int((time.time() % 1) * 1000)
  106. timestamp = f"{time_str}_{millis:03d}"
  107. color_frame = sample.get("color_frame")
  108. if color_frame is not None:
  109. color_image = frame_to_bgr_image(color_frame)
  110. if color_image is not None:
  111. color_height, color_width = color_image.shape[:2]
  112. color_file = os.path.join(
  113. save_image_dir,
  114. f"color_{color_width}x{color_height}_{timestamp}.png",
  115. )
  116. cv2.imwrite(color_file, color_image)
  117. depth_data = sample["depth_data"]
  118. x_start, x_end, y_start, y_end, center_distance = sample["bounds"]
  119. nearest_distance = sample["nearest_distance"]
  120. roi = depth_data[y_start:y_end, x_start:x_end]
  121. nearest_point = find_nearest_point(roi, x_start, y_start, self.config.settings, nearest_distance)
  122. depth_image = cv2.normalize(depth_data, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
  123. depth_image = cv2.applyColorMap(depth_image, cv2.COLORMAP_JET)
  124. cv2.rectangle(
  125. depth_image,
  126. (x_start, y_start),
  127. (x_end - 1, y_end - 1),
  128. (0, 255, 0),
  129. 2,
  130. )
  131. if nearest_point is not None:
  132. cv2.circle(depth_image, nearest_point, 4, (0, 0, 0), -1)
  133. cv2.circle(depth_image, nearest_point, 6, (0, 255, 255), 2)
  134. cv2.putText(
  135. depth_image,
  136. f"nearest: {nearest_distance} mm",
  137. (10, 30),
  138. cv2.FONT_HERSHEY_SIMPLEX,
  139. 0.8,
  140. (255, 255, 255),
  141. 2,
  142. cv2.LINE_AA,
  143. )
  144. cv2.putText(
  145. depth_image,
  146. f"center: {int(center_distance)} mm",
  147. (10, 60),
  148. cv2.FONT_HERSHEY_SIMPLEX,
  149. 0.8,
  150. (255, 255, 255),
  151. 2,
  152. cv2.LINE_AA,
  153. )
  154. depth_h, depth_w = depth_image.shape[:2]
  155. depth_file = os.path.join(
  156. save_image_dir,
  157. f"depth_annotated_{depth_w}x{depth_h}_{timestamp}.png",
  158. )
  159. cv2.imwrite(depth_file, depth_image)
  160. self._prune_saved_images(save_image_dir, self.config.max_saved_images)
  161. @staticmethod
  162. def _prune_saved_images(save_dir: str, max_images: int) -> None:
  163. png_files = [
  164. os.path.join(save_dir, name)
  165. for name in os.listdir(save_dir)
  166. if name.lower().endswith(".png")
  167. ]
  168. if len(png_files) <= max_images:
  169. return
  170. png_files.sort(key=os.path.getmtime)
  171. for file_path in png_files[: len(png_files) - max_images]:
  172. try:
  173. os.remove(file_path)
  174. except OSError:
  175. pass