精简仓库结构并增强任务链路:归档双目可选后端、新增LOS判据与自定义返航
归档 legacy/:stereo_yolo/stereo_ranger/target_tracker 三包 + 175 条测试 + demo/配置/权重/三份规格;运行链路不再依赖,默认 pytest 446 条,归档测试按需运行。 目标有效性第二判据改为 LOS 运动学一致性(pod_bridge/los_consistency.py),删除 require_vision;pod_locked() 叠加该判据,状态机随之后退重搜。 自定义返航(task_runner/return_home.py):保持转场高度回起飞点 → 恢复初始航向 → 垂直降落,不再爬 RTL_RETURN_ALT;未起飞/无起飞点/超时/异常仍退回 PX4 原生 RTL。 任务链安全兜底:主循环异常→紧急返航、stop 幂等、landing_stop 保留遥测、in_air 流复用+超时、解锁/起飞与吊舱识别仅执行一次、rtl_timeout 上限。 入口共享件拆到 utils/sitl_support.py;pod_sitl_test.py→l1_sitl_test.py;删除旧架构 test.py;相机脚本移入 tools/。 新增 .gitignore 并剔除历史误跟踪的 59 个 .pyc;requirements 按实际依赖重写;文档全面对齐,新增 legacy/README.md 与 tools/README.md。
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""video_source_test.py —— RTSP→MJPEG 视频源单元测试(注入假采集器,不依赖真流)。
|
||||
|
||||
覆盖:拉帧→JPEG 编码缓存、宽度限制(等比缩放)、断线重连(重新调用工厂)、
|
||||
打开失败退避重试、online 判定、MJPEG 分块格式。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
_ROOT = Path(__file__).resolve().parents[3] # utils/tests/ground_station → 仓库根
|
||||
if str(_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_ROOT))
|
||||
|
||||
from ground_station.video_source import RtspVideoSource # noqa: E402
|
||||
|
||||
|
||||
class FakeCapture:
|
||||
"""假采集器:可注入读失败次数;帧为纯色小图。"""
|
||||
|
||||
def __init__(self, fail_reads=0, height=60, width=120):
|
||||
self.fail_reads = fail_reads
|
||||
self._frame = np.full((height, width, 3), 128, dtype=np.uint8)
|
||||
self.released = False
|
||||
|
||||
def isOpened(self):
|
||||
return True
|
||||
|
||||
def read(self):
|
||||
if self.fail_reads > 0:
|
||||
self.fail_reads -= 1
|
||||
return False, None
|
||||
return True, self._frame
|
||||
|
||||
def release(self):
|
||||
self.released = True
|
||||
|
||||
|
||||
def _wait(pred, timeout_s=4.0, step=0.02):
|
||||
t0 = time.monotonic()
|
||||
while time.monotonic() - t0 < timeout_s:
|
||||
if pred():
|
||||
return True
|
||||
time.sleep(step)
|
||||
return False
|
||||
|
||||
|
||||
def _decode(jpeg):
|
||||
import cv2
|
||||
return cv2.imdecode(np.frombuffer(jpeg, dtype=np.uint8), cv2.IMREAD_COLOR)
|
||||
|
||||
|
||||
def test_frames_encoded_and_online():
|
||||
src = RtspVideoSource("rtsp://fake", width=120, fps=50,
|
||||
capture_factory=lambda: FakeCapture())
|
||||
assert src.online is False # 尚无帧 → 离线
|
||||
src.start()
|
||||
try:
|
||||
assert _wait(lambda: src.latest()[1] is not None)
|
||||
seq, jpeg = src.latest()
|
||||
assert seq >= 1
|
||||
assert jpeg[:2] == b"\xff\xd8" # JPEG SOI 头
|
||||
assert _decode(jpeg).shape[1] == 120 # 宽度不超限
|
||||
assert src.online is True
|
||||
finally:
|
||||
src.stop()
|
||||
|
||||
|
||||
def test_downscale_to_max_width():
|
||||
src = RtspVideoSource("rtsp://fake", width=60, fps=50,
|
||||
capture_factory=lambda: FakeCapture(height=60, width=120))
|
||||
src.start()
|
||||
try:
|
||||
assert _wait(lambda: src.latest()[1] is not None)
|
||||
img = _decode(src.latest()[1])
|
||||
assert img.shape[1] == 60 and img.shape[0] == 30 # 等比缩到 60 宽
|
||||
finally:
|
||||
src.stop()
|
||||
|
||||
|
||||
def test_reconnect_after_read_failure():
|
||||
opened = []
|
||||
|
||||
def factory():
|
||||
cap = FakeCapture(fail_reads=(10 ** 6 if not opened else 0))
|
||||
opened.append(cap)
|
||||
return cap
|
||||
|
||||
src = RtspVideoSource("rtsp://fake", width=120, fps=50,
|
||||
capture_factory=factory)
|
||||
src.start()
|
||||
try:
|
||||
assert _wait(lambda: src.latest()[1] is not None, timeout_s=5.0)
|
||||
assert len(opened) >= 2 # 首采集器读失败 → 重新打开
|
||||
assert opened[0].released is True
|
||||
finally:
|
||||
src.stop()
|
||||
|
||||
|
||||
def test_open_failure_keeps_retrying():
|
||||
calls = []
|
||||
|
||||
class ClosedCapture:
|
||||
def isOpened(self):
|
||||
return False
|
||||
|
||||
def release(self):
|
||||
pass
|
||||
|
||||
def factory():
|
||||
calls.append(1)
|
||||
return ClosedCapture()
|
||||
|
||||
src = RtspVideoSource("rtsp://fake", capture_factory=factory)
|
||||
src.start()
|
||||
try:
|
||||
assert _wait(lambda: len(calls) >= 2, timeout_s=4.0) # 1s 退避后重试
|
||||
assert src.latest()[1] is None
|
||||
finally:
|
||||
src.stop()
|
||||
|
||||
|
||||
def test_mjpeg_stream_chunks():
|
||||
src = RtspVideoSource("rtsp://fake", width=120, fps=50,
|
||||
capture_factory=lambda: FakeCapture())
|
||||
src.start()
|
||||
try:
|
||||
assert _wait(lambda: src.latest()[1] is not None)
|
||||
gen = src.mjpeg_stream()
|
||||
chunk = next(gen)
|
||||
assert chunk.startswith(b"--frame\r\nContent-Type: image/jpeg\r\n")
|
||||
assert b"Content-Length: " in chunk
|
||||
assert chunk.endswith(b"\r\n")
|
||||
gen.close()
|
||||
finally:
|
||||
src.stop()
|
||||
Reference in New Issue
Block a user