186 lines
6.3 KiB
Python
186 lines
6.3 KiB
Python
"""End-to-end off-robot smoke test for GoWelcome (real YOLO + cv2 + threads).
|
|
|
|
Run AFTER ``./requirement.sh`` (needs ultralytics + opencv installed), with NO
|
|
robot attached. It exercises the genuine perception + control stack on real
|
|
images so you catch any dependency/logic problem on the bench, not on the dog:
|
|
|
|
conda activate welcome # or: source the env however you like
|
|
python scripts/smoke_test.py
|
|
|
|
Checks:
|
|
1. YoloDetector on a people image and a vehicle image (bundled with ultralytics).
|
|
2. RoadDetector HSV mask: flags asphalt-grey, ignores saturated grass.
|
|
3. PerceptionThread + GoWelcomeStateMachine: a detected person drives
|
|
WANDER -> APPROACH/GREET; a detected vehicle forces AVOID_DANGER.
|
|
4. build_robot factory builds MockRobot; NullAudio + shutdown are clean.
|
|
|
|
Exits non-zero on the first failed assertion.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
# Dynamic: project root is this file's parent dir's parent (scripts/..).
|
|
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
if _ROOT not in sys.path:
|
|
sys.path.insert(0, _ROOT)
|
|
|
|
logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(name)s: %(message)s")
|
|
|
|
import numpy as np
|
|
|
|
try:
|
|
import cv2
|
|
from ultralytics.utils import ASSETS
|
|
except ImportError as exc: # pragma: no cover
|
|
print(f"ERROR: smoke test needs opencv + ultralytics installed ({exc}).")
|
|
print("Run ./requirement.sh first, then activate the 'welcome' env.")
|
|
sys.exit(2)
|
|
|
|
from config import default_config
|
|
from gowelcome.perception.detector import YoloDetector
|
|
from gowelcome.perception.road_mask import RoadDetector
|
|
from gowelcome.perception.vision_thread import PerceptionThread
|
|
from gowelcome.statemachine import GoWelcomeStateMachine
|
|
from gowelcome.robot.interface import RobotInterface
|
|
from gowelcome.robot.factory import build_robot
|
|
|
|
|
|
def banner(t: str) -> None:
|
|
print(f"\n===== {t} =====")
|
|
|
|
|
|
class _StubRobot(RobotInterface):
|
|
"""Serves one fixed frame; records commands. No hardware."""
|
|
|
|
def __init__(self, frame):
|
|
self._f = frame
|
|
self.drives, self.gestures, self.greets = [], [], 0
|
|
|
|
def get_frame(self):
|
|
return self._f
|
|
|
|
def frame_size(self):
|
|
return (self._f.shape[1], self._f.shape[0])
|
|
|
|
def drive(self, vx, vy, vyaw):
|
|
self.drives.append((vx, vy, vyaw))
|
|
|
|
def stop(self):
|
|
pass
|
|
|
|
def set_avoidance(self, on):
|
|
pass
|
|
|
|
def balance_stand(self):
|
|
pass
|
|
|
|
def stand_up(self):
|
|
pass
|
|
|
|
def damp(self):
|
|
pass
|
|
|
|
def gesture(self, name):
|
|
self.gestures.append(name)
|
|
|
|
def play_greeting(self):
|
|
self.greets += 1
|
|
|
|
def shutdown(self):
|
|
pass
|
|
|
|
|
|
def main() -> int:
|
|
cfg = default_config()
|
|
cfg.mock = True
|
|
cfg.audio.backend = "null"
|
|
cfg.perception.device = "cpu"
|
|
w, h = cfg.camera.width, cfg.camera.height
|
|
|
|
# ---------------------------------------------------------- 1. YOLO
|
|
banner("1. YoloDetector on real images")
|
|
det = YoloDetector(cfg.perception)
|
|
|
|
zid = cv2.resize(cv2.imread(str(ASSETS / "zidane.jpg")), (w, h))
|
|
zdets = det.detect(zid)
|
|
print("zidane.jpg ->", [(d.label, round(d.conf, 2)) for d in zdets])
|
|
assert any(d.label == "person" for d in zdets), "expected person(s) in zidane.jpg"
|
|
|
|
bus = cv2.resize(cv2.imread(str(ASSETS / "bus.jpg")), (w, h))
|
|
bdets = det.detect(bus)
|
|
blabels = {d.label for d in bdets}
|
|
print("bus.jpg ->", [(d.label, round(d.conf, 2)) for d in bdets])
|
|
assert "person" in blabels, "expected person in bus.jpg"
|
|
assert "bus" in blabels, "expected bus (danger class) in bus.jpg"
|
|
print("OK: real YOLO detects persons and a vehicle")
|
|
|
|
# ---------------------------------------------------------- 2. ROAD
|
|
banner("2. RoadDetector HSV mask")
|
|
grey = np.full((h, w, 3), 95, np.uint8)
|
|
ri = RoadDetector(cfg.perception).analyze(grey)
|
|
print(f"grey -> coverage={ri.coverage:.2f} L/C/R={ri.left:.2f}/{ri.center:.2f}/{ri.right:.2f}")
|
|
assert ri.coverage > 0.5, "asphalt-grey frame should read as mostly road"
|
|
green = np.zeros((h, w, 3), np.uint8)
|
|
green[:] = (40, 200, 40)
|
|
rg = RoadDetector(cfg.perception).analyze(green)
|
|
print(f"green -> coverage={rg.coverage:.2f}")
|
|
assert rg.coverage < 0.2, "saturated green should not read as road"
|
|
print("OK: road mask flags asphalt, ignores grass")
|
|
|
|
# ---------------------------------------------------------- 3. PIPELINE
|
|
banner("3. PerceptionThread + StateMachine (person -> APPROACH/GREET)")
|
|
cfg.perception.road_enabled = False
|
|
cfg.perception.person_conf = 0.5
|
|
robot = _StubRobot(zid)
|
|
pt = PerceptionThread(robot, cfg)
|
|
pt.start()
|
|
time.sleep(2.0)
|
|
res = pt.latest()
|
|
print("perception ->", "persons:", len(res.persons), "dangers:", len(res.dangers))
|
|
assert res and res.persons, "perception thread should report a person"
|
|
sm = GoWelcomeStateMachine(robot, pt, cfg)
|
|
seq = [sm.step(1 / 15).value for _ in range(6)]
|
|
pt.stop()
|
|
print("states:", seq)
|
|
assert any(s in ("APPROACH", "GREET") for s in seq), "should approach/greet a person"
|
|
print("OK: perception->decision reacts to a real detection")
|
|
|
|
# ---------------------------------------------------------- 3b. DANGER
|
|
banner("3b. Danger override (vehicle -> AVOID_DANGER)")
|
|
robot2 = _StubRobot(bus)
|
|
pt2 = PerceptionThread(robot2, cfg)
|
|
pt2.start()
|
|
time.sleep(2.0)
|
|
res2 = pt2.latest()
|
|
print("perception ->", "persons:", len(res2.persons), "dangers:", len(res2.dangers))
|
|
sm2 = GoWelcomeStateMachine(robot2, pt2, cfg)
|
|
seq2 = [sm2.step(1 / 15).value for _ in range(4)]
|
|
pt2.stop()
|
|
print("states:", seq2)
|
|
assert res2.dangers, "the bus should be classified as a danger"
|
|
assert "AVOID_DANGER" in seq2, "a nearby vehicle must force AVOID_DANGER"
|
|
print("OK: vehicle triggers the safety escape state")
|
|
|
|
# ---------------------------------------------------------- 4. FACTORY
|
|
banner("4. build_robot factory + NullAudio")
|
|
bot = build_robot(cfg)
|
|
print("build_robot ->", type(bot).__name__)
|
|
assert type(bot).__name__ == "MockRobot"
|
|
bot.play_greeting()
|
|
bot.shutdown()
|
|
print("OK: factory builds MockRobot; null audio + shutdown clean")
|
|
|
|
print("\n" + "=" * 40)
|
|
print("ALL SMOKE TESTS PASSED")
|
|
print("=" * 40)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|