Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from flask import Flask

from lib.aruco_logger import ArucoPipelineLogger
from lib.axis_config_sender import send_axis_config
from lib.bitmask import init_bitmask
from lib.camera import init_camera, init_ip_camera, init_rpi_camera
Expand Down Expand Up @@ -31,6 +32,9 @@
# Start background IMU receiver (UDP port 5002)
app.config["IMU"] = init_imu_receiver(port=5002)

# Tracks ordered ARUCO markers for the pipeline challenge.
app.config["ARUCO_LOGGER"] = ArucoPipelineLogger()

# Load saved IMU axis mapping from config
_config = JSONDataHandler(file_path=data_path("config.json"))
_saved_axes = _config.get_section("imu_axes")
Expand Down Expand Up @@ -68,6 +72,7 @@
out_height=rpi_cam_out_height,
jpeg_quality=rpi_cam_jpeg_quality,
flip_180=rpi_cam_flip_180,
marker_logger=app.config["ARUCO_LOGGER"],
)

# Initialize IP camera (SMTSEC SIP-K327GS) via RTSP
Expand All @@ -82,6 +87,7 @@
out_height=ip_cam_out_height,
jpeg_quality=ip_cam_jpeg_quality,
flip_180=ip_cam_flip_180,
marker_logger=app.config["ARUCO_LOGGER"],
)

# Initialize default local camera for the legacy Camera 1 feed.
Expand All @@ -91,6 +97,7 @@
app.config["DEFAULT_CAMERA"] = init_camera(
device_index=default_cam_device,
jpeg_quality=default_cam_jpeg_quality,
marker_logger=app.config["ARUCO_LOGGER"],
)

# Start background resource monitor receiver (UDP port 12346)
Expand Down
79 changes: 79 additions & 0 deletions lib/aruco_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import threading
import time
from datetime import UTC, datetime


class ArucoPipelineLogger:
"""Tracks ordered ARUCO sightings for the pipeline challenge."""

def __init__(self):
self._lock = threading.Lock()
self._enabled = False
self._entries = []
self._logged_ids = set()
self._visible_ids = []
self._duplicate_count = 0

def start(self):
with self._lock:
self._enabled = True
return self._snapshot_locked()

def stop(self):
with self._lock:
self._enabled = False
return self._snapshot_locked()

def clear(self):
with self._lock:
self._entries = []
self._logged_ids = set()
self._visible_ids = []
self._duplicate_count = 0
return self._snapshot_locked()

def record_visible(self, detections):
ordered = sorted(
detections,
key=lambda marker: (
marker.get("center", (float("inf"), float("inf")))[0],
marker.get("id", 0),
),
)
now = time.time()

with self._lock:
self._visible_ids = [marker["id"] for marker in ordered]
if not self._enabled:
return self._snapshot_locked()

for marker in ordered:
marker_id = marker["id"]
if marker_id in self._logged_ids:
self._duplicate_count += 1
continue
self._logged_ids.add(marker_id)
self._entries.append(
{
"order": len(self._entries) + 1,
"id": marker_id,
"seen_at": _format_timestamp(now),
}
)
return self._snapshot_locked()

def snapshot(self):
with self._lock:
return self._snapshot_locked()

def _snapshot_locked(self):
return {
"enabled": self._enabled,
"entries": list(self._entries),
"visible_ids": list(self._visible_ids),
"duplicate_count": self._duplicate_count,
}


def _format_timestamp(timestamp):
return datetime.fromtimestamp(timestamp, UTC).isoformat().replace("+00:00", "Z")
61 changes: 50 additions & 11 deletions lib/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,57 @@
import numpy as np


# Dummy ArUco detector to keep code functional after removing autonomous
class DummyArUcoMarkerDetector:
def __init__(self, camera_matrix=None, dist_coeffs=None):
pass
class ArUcoMarkerDetector:
def __init__(self, dictionary_name="DICT_4X4_50", camera_matrix=None, dist_coeffs=None):
aruco = cv2.aruco
self.dictionary_name = dictionary_name
self.camera_matrix = camera_matrix
self.dist_coeffs = dist_coeffs
self._dictionary = aruco.getPredefinedDictionary(getattr(aruco, dictionary_name))
self._detector = aruco.ArucoDetector(self._dictionary, aruco.DetectorParameters())

def detect_markers(self, frame):
return [], [], []
return self._detector.detectMarkers(frame)

def draw_detected_markers(self, frame, corners, ids):
if ids is not None and len(ids) > 0:
cv2.aruco.drawDetectedMarkers(frame, corners, ids)
return frame

def marker_detections(self, corners, ids):
if ids is None:
return []

detections = []
for marker_id, marker_corners in zip(ids.flatten(), corners, strict=False):
points = np.asarray(marker_corners, dtype=np.float32).reshape(-1, 2)
center = points.mean(axis=0)
detections.append({"id": int(marker_id), "center": (float(center[0]), float(center[1]))})
return detections


def _process_aruco_frame(frame, detector, marker_logger):
if detector is None:
return frame
corners, ids, _rejected = detector.detect_markers(frame)
detections = detector.marker_detections(corners, ids)
if marker_logger is not None:
marker_logger.record_visible(detections)
return detector.draw_detected_markers(frame, corners, ids)


class DefaultCameraReceiver:
"""Reads the default local camera in the background and exposes latest JPEG frame."""

RECONNECT_DELAY = 3.0

def __init__(self, device_index=0, jpeg_quality=70):
def __init__(self, device_index=0, jpeg_quality=70, marker_logger=None):
self.device_index = int(device_index)
self.jpeg_quality = min(95, max(40, int(jpeg_quality)))
self.is_connected = False
self.is_listening = False
self.last_error = None
self.marker_logger = marker_logger

self._stop_event = threading.Event()
self._thread = None
Expand All @@ -43,7 +71,7 @@ def __init__(self, device_index=0, jpeg_quality=70):
self._placeholder_jpeg = self._build_placeholder_jpeg()
camera_matrix = np.array([[900, 0, 640], [0, 900, 360], [0, 0, 1]], dtype=np.float32)
dist_coeffs = np.zeros((5, 1), dtype=np.float32)
self._detector = DummyArUcoMarkerDetector(camera_matrix=camera_matrix, dist_coeffs=dist_coeffs)
self._detector = ArUcoMarkerDetector(camera_matrix=camera_matrix, dist_coeffs=dist_coeffs)

def start(self):
if self._thread and self._thread.is_alive():
Expand Down Expand Up @@ -89,8 +117,7 @@ def get_status(self):
}

def _set_frame(self, frame):
corners, ids, _rejected = self._detector.detect_markers(frame)
frame = self._detector.draw_detected_markers(frame, corners, ids)
frame = _process_aruco_frame(frame, self._detector, self.marker_logger)
ok, buf = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), self.jpeg_quality])
if not ok:
return
Expand Down Expand Up @@ -183,9 +210,9 @@ def generate_frames(camera):
yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame + b"\r\n")


def init_camera(device_index=0, jpeg_quality=70):
def init_camera(device_index=0, jpeg_quality=70, marker_logger=None):
"""Initialize and start the default local camera receiver."""
receiver = DefaultCameraReceiver(device_index=device_index, jpeg_quality=jpeg_quality)
receiver = DefaultCameraReceiver(device_index=device_index, jpeg_quality=jpeg_quality, marker_logger=marker_logger)
receiver.start()
return receiver

Expand All @@ -202,6 +229,7 @@ def __init__(
out_height=540,
jpeg_quality=70,
flip_180=True,
marker_logger=None,
):
self.host = host
self.port = int(port)
Expand All @@ -214,6 +242,7 @@ def __init__(
self.is_listening = False
self.backend = "none"
self.last_error = None
self.marker_logger = marker_logger

self._stop_event = threading.Event()
self._thread = None
Expand All @@ -227,6 +256,7 @@ def __init__(
self._frame_seq = 0
self._last_frame_ts = 0.0
self._placeholder_jpeg = self._build_placeholder_jpeg()
self._detector = ArUcoMarkerDetector()

def start(self):
if self._thread and self._thread.is_alive():
Expand Down Expand Up @@ -278,6 +308,7 @@ def get_status(self):
}

def _set_frame(self, frame):
frame = _process_aruco_frame(frame, self._detector, self.marker_logger)
ok, buf = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 80])
if not ok:
return
Expand Down Expand Up @@ -543,6 +574,7 @@ def init_rpi_camera(
out_height=540,
jpeg_quality=70,
flip_180=False,
marker_logger=None,
):
receiver = RPiCameraReceiver(
host=host,
Expand All @@ -552,6 +584,7 @@ def init_rpi_camera(
out_height=out_height,
jpeg_quality=jpeg_quality,
flip_180=flip_180,
marker_logger=marker_logger,
)
receiver.start()
return receiver
Expand Down Expand Up @@ -589,6 +622,7 @@ def __init__(
out_height=540,
jpeg_quality=70,
flip_180=False,
marker_logger=None,
):
self.url = url
self.out_width = max(160, int(out_width))
Expand All @@ -597,6 +631,7 @@ def __init__(
self.flip_180 = bool(flip_180)
self.is_connected = False
self.last_error = None
self.marker_logger = marker_logger

self._stop_event = threading.Event()
self._thread = None
Expand All @@ -608,6 +643,7 @@ def __init__(
self._frame_seq = 0
self._last_frame_ts = 0.0
self._placeholder_jpeg = self._build_placeholder_jpeg()
self._detector = ArUcoMarkerDetector()

def start(self):
if self._thread and self._thread.is_alive():
Expand Down Expand Up @@ -655,6 +691,7 @@ def get_status(self):
}

def _set_frame(self, frame):
frame = _process_aruco_frame(frame, self._detector, self.marker_logger)
ok, buf = cv2.imencode(
".jpg",
frame,
Expand Down Expand Up @@ -751,6 +788,7 @@ def init_ip_camera(
out_height=540,
jpeg_quality=70,
flip_180=False,
marker_logger=None,
):
"""Initialize and start an IP camera receiver (RTSP/HTTP)."""
receiver = IPCameraReceiver(
Expand All @@ -759,6 +797,7 @@ def init_ip_camera(
out_height=out_height,
jpeg_quality=jpeg_quality,
flip_180=flip_180,
marker_logger=marker_logger,
)
receiver.start()
return receiver
Expand Down
32 changes: 32 additions & 0 deletions routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,38 @@ def ip_camera_status():
return jsonify(ip_cam.get_status())
return jsonify({"connected": False})

@app.route("/api/aruco-log", methods=["GET"])
def aruco_log_status():
"""Return ordered ARUCO marker sightings for the pipeline challenge."""
logger = current_app.config.get("ARUCO_LOGGER")
if not logger:
return jsonify({"ok": False, "error": "ARUCO logger unavailable"}), 503
return jsonify({"ok": True, "log": logger.snapshot()})

@app.route("/api/aruco-log/start", methods=["POST"])
def start_aruco_log():
"""Start logging new ARUCO marker IDs."""
logger = current_app.config.get("ARUCO_LOGGER")
if not logger:
return jsonify({"ok": False, "error": "ARUCO logger unavailable"}), 503
return jsonify({"ok": True, "log": logger.start()})

@app.route("/api/aruco-log/stop", methods=["POST"])
def stop_aruco_log():
"""Stop logging new ARUCO marker IDs."""
logger = current_app.config.get("ARUCO_LOGGER")
if not logger:
return jsonify({"ok": False, "error": "ARUCO logger unavailable"}), 503
return jsonify({"ok": True, "log": logger.stop()})

@app.route("/api/aruco-log/clear", methods=["POST"])
def clear_aruco_log():
"""Clear the ordered ARUCO marker log."""
logger = current_app.config.get("ARUCO_LOGGER")
if not logger:
return jsonify({"ok": False, "error": "ARUCO logger unavailable"}), 503
return jsonify({"ok": True, "log": logger.clear()})

@app.route("/api/thrusters", methods=["GET"])
def get_thrusters():
"""API route for thrusters data."""
Expand Down
Loading
Loading