api.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import time
  2. import threading
  3. import numpy as np
  4. import cv2
  5. from fastapi import FastAPI, HTTPException
  6. from pyorbbecsdk import *
  7. ESC_KEY = 27
  8. PRINT_INTERVAL = 1 # seconds
  9. MIN_DEPTH = 500 # mm
  10. MAX_DEPTH = 4000 # mm
  11. ROI_WIDTH_CM = 10.0 # cm
  12. ROI_HEIGHT_CM = 12.0 # cm
  13. MEDIAN_BLUR_KSIZE = 5 # odd number, 0 to disable
  14. MORPH_OPEN_KSIZE = 3 # odd number, 0 to disable
  15. NEAREST_PERCENTILE = 5 # use low percentile to suppress isolated noise (0 for raw min)
  16. SAMPLE_COUNT = 10
  17. FRAME_TIMEOUT_MS = 200
  18. SAMPLE_TIMEOUT_SEC = 8
  19. app = FastAPI(title="Cargo Height API")
  20. class TemporalFilter:
  21. def __init__(self, alpha):
  22. self.alpha = alpha
  23. self.previous_frame = None
  24. def process(self, frame):
  25. if self.previous_frame is None:
  26. result = frame
  27. else:
  28. result = cv2.addWeighted(frame, self.alpha, self.previous_frame, 1 - self.alpha, 0)
  29. self.previous_frame = result
  30. return result
  31. _pipeline = None
  32. _depth_intrinsics = None
  33. _temporal_filter = None
  34. _lock = threading.Lock()
  35. def _init_camera():
  36. global _pipeline, _depth_intrinsics, _temporal_filter
  37. if _pipeline is not None:
  38. return
  39. config = Config()
  40. pipeline = Pipeline()
  41. try:
  42. profile_list = pipeline.get_stream_profile_list(OBSensorType.DEPTH_SENSOR)
  43. if profile_list is None:
  44. raise RuntimeError("depth profile list is empty")
  45. depth_profile = profile_list.get_default_video_stream_profile()
  46. if depth_profile is None:
  47. raise RuntimeError("default depth profile is empty")
  48. _depth_intrinsics = depth_profile.get_intrinsic()
  49. config.enable_stream(depth_profile)
  50. except Exception as exc:
  51. raise RuntimeError(f"Failed to init depth camera: {exc}") from exc
  52. pipeline.start(config)
  53. _pipeline = pipeline
  54. _temporal_filter = TemporalFilter(alpha=0.5)
  55. def _shutdown_camera():
  56. global _pipeline
  57. if _pipeline is None:
  58. return
  59. _pipeline.stop()
  60. _pipeline = None
  61. def _measure_once():
  62. frames = _pipeline.wait_for_frames(FRAME_TIMEOUT_MS)
  63. if frames is None:
  64. return None
  65. depth_frame = frames.get_depth_frame()
  66. if depth_frame is None:
  67. return None
  68. depth_format = depth_frame.get_format()
  69. if depth_format != OBFormat.Y16:
  70. return None
  71. width = depth_frame.get_width()
  72. height = depth_frame.get_height()
  73. scale = depth_frame.get_depth_scale()
  74. depth_data = np.frombuffer(depth_frame.get_data(), dtype=np.uint16)
  75. depth_data = depth_data.reshape((height, width))
  76. depth_data = depth_data.astype(np.float32) * scale
  77. depth_data = np.where((depth_data > MIN_DEPTH) & (depth_data < MAX_DEPTH), depth_data, 0)
  78. depth_data = depth_data.astype(np.uint16)
  79. if MEDIAN_BLUR_KSIZE and MEDIAN_BLUR_KSIZE % 2 == 1:
  80. depth_data = cv2.medianBlur(depth_data, MEDIAN_BLUR_KSIZE)
  81. if MORPH_OPEN_KSIZE and MORPH_OPEN_KSIZE % 2 == 1:
  82. kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (MORPH_OPEN_KSIZE, MORPH_OPEN_KSIZE))
  83. valid_mask = (depth_data > 0).astype(np.uint8)
  84. valid_mask = cv2.morphologyEx(valid_mask, cv2.MORPH_OPEN, kernel)
  85. depth_data = np.where(valid_mask > 0, depth_data, 0).astype(np.uint16)
  86. depth_data = _temporal_filter.process(depth_data)
  87. center_y = height // 2
  88. center_x = width // 2
  89. center_distance = depth_data[center_y, center_x]
  90. if center_distance == 0:
  91. return None
  92. center_distance_m = center_distance / 1000.0
  93. half_width_m = (ROI_WIDTH_CM / 100.0) / 2.0
  94. half_height_m = (ROI_HEIGHT_CM / 100.0) / 2.0
  95. half_width_px = int(_depth_intrinsics.fx * half_width_m / center_distance_m)
  96. half_height_px = int(_depth_intrinsics.fy * half_height_m / center_distance_m)
  97. if half_width_px <= 0 or half_height_px <= 0:
  98. return None
  99. half_width_px = min(half_width_px, center_x, width - center_x - 1)
  100. half_height_px = min(half_height_px, center_y, height - center_y - 1)
  101. if half_width_px <= 0 or half_height_px <= 0:
  102. return None
  103. x_start = center_x - half_width_px
  104. x_end = center_x + half_width_px + 1
  105. y_start = center_y - half_height_px
  106. y_end = center_y + half_height_px + 1
  107. roi = depth_data[y_start:y_end, x_start:x_end]
  108. valid_values = roi[(roi >= MIN_DEPTH) & (roi <= MAX_DEPTH)]
  109. if valid_values.size == 0:
  110. return None
  111. if NEAREST_PERCENTILE and 0 < NEAREST_PERCENTILE < 100:
  112. return int(np.percentile(valid_values, NEAREST_PERCENTILE))
  113. return int(valid_values.min())
  114. @app.on_event("startup")
  115. def on_startup():
  116. _init_camera()
  117. @app.on_event("shutdown")
  118. def on_shutdown():
  119. _shutdown_camera()
  120. @app.get("/height")
  121. def get_height():
  122. start_time = time.time()
  123. samples = []
  124. with _lock:
  125. while len(samples) < SAMPLE_COUNT and (time.time() - start_time) < SAMPLE_TIMEOUT_SEC:
  126. value = _measure_once()
  127. if value is not None:
  128. samples.append(value)
  129. if len(samples) < SAMPLE_COUNT:
  130. raise HTTPException(status_code=503, detail="Insufficient valid samples from depth camera")
  131. median_value = int(np.median(np.array(samples, dtype=np.int32)))
  132. return {
  133. "height_mm": median_value,
  134. "samples": samples,
  135. "unit": "mm",
  136. "sample_count": SAMPLE_COUNT,
  137. }
  138. @app.get("/health")
  139. def health():
  140. return {"status": "ok"}