168 lines
5.1 KiB
Python
168 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Camera Processor - Handles Gazebo camera feeds for GPS-denied navigation.
|
|
Subscribes to gz-transport topics, processes frames, and displays via OpenCV.
|
|
Downstream modules (ArUco detector, optical flow) register as callbacks.
|
|
"""
|
|
|
|
import sys
|
|
import signal
|
|
import time
|
|
import numpy as np
|
|
import cv2
|
|
from gz.transport13 import Node
|
|
from gz.msgs10.image_pb2 import Image
|
|
|
|
# PixelFormatType enum values (from gz.msgs10 Image proto)
|
|
PF_RGB_INT8 = 3
|
|
PF_BGR_INT8 = 8
|
|
PF_R_FLOAT32 = 13
|
|
|
|
|
|
class CameraProcessor:
|
|
def __init__(self, topics=None, show_gui=True):
|
|
self.node = Node()
|
|
self.show_gui = show_gui
|
|
self.frames = {}
|
|
self.callbacks = {}
|
|
self.running = True
|
|
|
|
if topics is None:
|
|
topics = {
|
|
"downward": "/uav/camera/downward",
|
|
"gimbal": "/world/uav_ugv_search/model/iris_with_gimbal/model/gimbal/link/pitch_link/sensor/camera/image",
|
|
}
|
|
|
|
self.topics = topics
|
|
|
|
for name, topic in self.topics.items():
|
|
self.callbacks[name] = []
|
|
ok = self.node.subscribe(Image, topic, self._make_gz_callback(name))
|
|
if ok:
|
|
print(f"[CAM] Subscribed: {name} -> {topic}")
|
|
else:
|
|
print(f"[CAM] Failed: {topic}")
|
|
|
|
signal.signal(signal.SIGINT, lambda s, f: self.stop())
|
|
|
|
def _make_gz_callback(self, name):
|
|
def cb(msg):
|
|
fmt = msg.pixel_format_type
|
|
if fmt == PF_RGB_INT8:
|
|
arr = np.frombuffer(msg.data, dtype=np.uint8).reshape(
|
|
msg.height, msg.width, 3
|
|
)
|
|
bgr = cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
|
|
elif fmt == PF_BGR_INT8:
|
|
arr = np.frombuffer(msg.data, dtype=np.uint8).reshape(
|
|
msg.height, msg.width, 3
|
|
)
|
|
bgr = arr
|
|
elif fmt == PF_R_FLOAT32:
|
|
arr = np.frombuffer(msg.data, dtype=np.float32).reshape(
|
|
msg.height, msg.width
|
|
)
|
|
normalized = cv2.normalize(arr, None, 0, 255, cv2.NORM_MINMAX)
|
|
bgr = cv2.cvtColor(normalized.astype(np.uint8), cv2.COLOR_GRAY2BGR)
|
|
else:
|
|
return
|
|
|
|
processed = self.process_image(bgr)
|
|
self.frames[name] = processed
|
|
|
|
for fn in self.callbacks.get(name, []):
|
|
fn(name, processed)
|
|
|
|
return cb
|
|
|
|
def process_image(self, image):
|
|
processed = image.copy()
|
|
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
|
lab = cv2.cvtColor(processed, cv2.COLOR_BGR2LAB)
|
|
lab[:, :, 0] = clahe.apply(lab[:, :, 0])
|
|
processed = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
|
|
return processed
|
|
|
|
def register_callback(self, camera_name, fn):
|
|
if camera_name in self.callbacks:
|
|
self.callbacks[camera_name].append(fn)
|
|
print(f"[CAM] Registered callback for {camera_name}: {fn.__name__}")
|
|
|
|
def stop(self):
|
|
self.running = False
|
|
|
|
def spin(self):
|
|
print("[CAM] Running. Press 'q' to quit, 's' to screenshot.")
|
|
while self.running:
|
|
for name, frame in list(self.frames.items()):
|
|
cv2.imshow(name, frame)
|
|
|
|
key = cv2.waitKey(33) & 0xFF
|
|
if key == ord("q"):
|
|
break
|
|
elif key == ord("s"):
|
|
for name, frame in self.frames.items():
|
|
fname = f"{name}_{int(time.time())}.png"
|
|
cv2.imwrite(fname, frame)
|
|
print(f"[CAM] Saved {fname}")
|
|
|
|
cv2.destroyAllWindows()
|
|
print("[CAM] Stopped.")
|
|
|
|
def spin_headless(self):
|
|
print("[CAM] Running headless (no GUI).")
|
|
while self.running:
|
|
time.sleep(0.1)
|
|
print("[CAM] Stopped.")
|
|
|
|
def get_frame(self, camera_name):
|
|
return self.frames.get(camera_name)
|
|
|
|
|
|
def main():
|
|
cameras = "both"
|
|
show_gui = True
|
|
|
|
if len(sys.argv) > 1:
|
|
cameras = sys.argv[1].lower()
|
|
if "--headless" in sys.argv:
|
|
show_gui = False
|
|
|
|
all_topics = {
|
|
"downward": "/uav/camera/downward",
|
|
"gimbal": "/world/uav_ugv_search/model/iris_with_gimbal/model/gimbal/link/pitch_link/sensor/camera/image",
|
|
}
|
|
|
|
if cameras == "down":
|
|
topics = {"downward": all_topics["downward"]}
|
|
elif cameras == "gimbal":
|
|
topics = {"gimbal": all_topics["gimbal"]}
|
|
else:
|
|
topics = all_topics
|
|
|
|
proc = CameraProcessor(topics=topics, show_gui=show_gui)
|
|
|
|
try:
|
|
from vision.object_detector import ObjectDetector
|
|
detector = ObjectDetector(aruco_dict_name="DICT_4X4_50")
|
|
|
|
def detection_overlay(camera_name, frame):
|
|
detections = detector.detect(frame)
|
|
if detections:
|
|
annotated = detector.annotate_frame(frame, detections)
|
|
proc.frames[camera_name] = annotated
|
|
|
|
for cam_name in topics:
|
|
proc.register_callback(cam_name, detection_overlay)
|
|
except Exception as e:
|
|
print(f"[CAM] ArUco detection unavailable: {e}")
|
|
|
|
if show_gui:
|
|
proc.spin()
|
|
else:
|
|
proc.spin_headless()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|