forked from IQ.Lvbs/IQ.Pilot
187 lines
7.3 KiB
Python
187 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import shutil
|
|
import sys
|
|
import time
|
|
import importlib
|
|
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.selfdrive.test.helpers import with_processes
|
|
|
|
TEST_DIR = pathlib.Path(__file__).parent
|
|
OUTPUT_DIR = TEST_DIR / "nav_demo_report"
|
|
UI_DELAY = float(os.getenv("IQPILOT_NAV_DEMO_UI_DELAY", "0.75"))
|
|
VERSION = "0.10.1 / nav-ui-demo / 7864838 / Mar 09"
|
|
SCENE_SETTLE_S = float(os.getenv("IQPILOT_NAV_DEMO_SCENE_SETTLE_S", "0.04"))
|
|
SEED_REPEATS = int(os.getenv("IQPILOT_NAV_DEMO_SEED_REPEATS", "2"))
|
|
SEED_DELAY_S = float(os.getenv("IQPILOT_NAV_DEMO_SEED_DELAY_S", "0.02"))
|
|
SEED_REFRESH_EVERY = int(os.getenv("IQPILOT_NAV_DEMO_SEED_REFRESH_EVERY", "24"))
|
|
NAV_REPEATS = int(os.getenv("IQPILOT_NAV_DEMO_NAV_REPEATS", "2"))
|
|
NAV_DELAY_S = float(os.getenv("IQPILOT_NAV_DEMO_NAV_DELAY_S", "0.02"))
|
|
SEED_PUBLISH_DURATION_S = SEED_REPEATS * SEED_DELAY_S
|
|
NAV_SCENE_DURATION_S = NAV_REPEATS * NAV_DELAY_S
|
|
|
|
NAV_SCENES = ()
|
|
NAV_TIMELINE = ()
|
|
build_ui_pubmaster = None
|
|
publish_nav_scene = None
|
|
publish_onroad_seed = None
|
|
seed_ui_test_params = None
|
|
|
|
|
|
class NavDemoCapture:
|
|
def __init__(self, output_dir: pathlib.Path):
|
|
os.environ["SCALE"] = os.getenv("SCALE", "1")
|
|
os.environ["BIG"] = "1"
|
|
os.environ["RECORD"] = "1"
|
|
os.environ["RECORD_OUTPUT"] = str(output_dir / "nav_demo")
|
|
sys.modules["mouseinfo"] = False
|
|
self.output_dir = output_dir
|
|
self.pm = build_ui_pubmaster()
|
|
self.frames = []
|
|
self._image_lib = None
|
|
self.video_path = self.output_dir / "nav_demo.mp4"
|
|
|
|
def _load_image_lib(self):
|
|
if self._image_lib is not None:
|
|
return self._image_lib
|
|
try:
|
|
self._image_lib = importlib.import_module("PIL.Image")
|
|
except ModuleNotFoundError:
|
|
self._image_lib = None
|
|
return self._image_lib
|
|
|
|
def setup(self):
|
|
publish_onroad_seed(self.pm)
|
|
time.sleep(UI_DELAY)
|
|
|
|
@with_processes(["ui"])
|
|
def run(self):
|
|
self.setup()
|
|
for idx, scene in enumerate(NAV_TIMELINE):
|
|
if idx % SEED_REFRESH_EVERY == 0:
|
|
publish_onroad_seed(self.pm, repeats=SEED_REPEATS, delay=SEED_DELAY_S)
|
|
publish_nav_scene(self.pm, scene, repeats=NAV_REPEATS, delay=NAV_DELAY_S)
|
|
time.sleep(SCENE_SETTLE_S)
|
|
|
|
def extract_video_stills(self) -> list[pathlib.Path]:
|
|
if not self.video_path.exists():
|
|
return []
|
|
|
|
extracted = []
|
|
current_ts = UI_DELAY
|
|
for idx, scene in enumerate(NAV_TIMELINE):
|
|
if idx % SEED_REFRESH_EVERY == 0:
|
|
current_ts += SEED_PUBLISH_DURATION_S
|
|
capture_ts = current_ts + (NAV_SCENE_DURATION_S * 0.6)
|
|
capture_name = scene.get("capture_name")
|
|
if capture_name:
|
|
output_path = self.output_dir / f"{capture_name}.png"
|
|
subprocess.run(
|
|
[
|
|
"ffmpeg",
|
|
"-y",
|
|
"-loglevel", "error",
|
|
"-ss", f"{capture_ts:.2f}",
|
|
"-i", str(self.video_path),
|
|
"-frames:v", "1",
|
|
str(output_path),
|
|
],
|
|
check=True,
|
|
)
|
|
extracted.append(output_path)
|
|
current_ts += NAV_SCENE_DURATION_S + SCENE_SETTLE_S
|
|
return extracted
|
|
|
|
def write_gif(self) -> pathlib.Path | None:
|
|
if not self.frames:
|
|
for scene in NAV_SCENES:
|
|
image_path = self.output_dir / f"{scene['name']}.png"
|
|
if image_path.exists():
|
|
self.frames.append(self._load_image_lib().open(image_path).copy())
|
|
|
|
if not self.frames:
|
|
return None
|
|
if self._load_image_lib() is None:
|
|
return None
|
|
gif_path = self.output_dir / "nav_demo.gif"
|
|
self.frames[0].save(
|
|
gif_path,
|
|
save_all=True,
|
|
append_images=self.frames[1:],
|
|
duration=900,
|
|
loop=0,
|
|
)
|
|
return gif_path
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Run BIG raylib UI with a hard-coded nav route and capture screenshots/GIF.")
|
|
parser.add_argument("--output-dir", type=pathlib.Path, default=OUTPUT_DIR, help="Directory for screenshots and gif.")
|
|
parser.add_argument("--no-gif", action="store_true", help="Skip animated GIF creation.")
|
|
parser.add_argument("--mapbox-token", default="", help="Mapbox token for the demo prefix. Falls back to MAPBOX_TOKEN if omitted.")
|
|
parser.add_argument("--no-mapbox", action="store_true", help="Disable Mapbox for the demo and use only cached/offline tiles.")
|
|
parser.add_argument("--force-local-offline", action="store_true", help="Bypass both live and cached Mapbox so only the local offline provider can render.")
|
|
parser.add_argument("--offline-mbtiles", type=pathlib.Path, default=None, help="Path to a local raster MBTiles file for offline rendering.")
|
|
parser.add_argument("--offline-tile-root", type=pathlib.Path, default=None, help="Path to a local XYZ raster tile directory for offline rendering.")
|
|
parser.add_argument("--fixture", type=pathlib.Path, default=None, help="Route fixture JSON to drive the nav demo.")
|
|
parser.add_argument("--osmaps-mode", choices=("online", "offline", "both"), default=None,
|
|
help="Set the OnlineOSMaps/OfflineOSMaps params (the production source selection) for the demo prefix.")
|
|
args = parser.parse_args()
|
|
|
|
if args.output_dir.exists():
|
|
shutil.rmtree(args.output_dir)
|
|
args.output_dir.mkdir(parents=True)
|
|
|
|
mapbox_token = args.mapbox_token
|
|
if not mapbox_token and not args.no_mapbox:
|
|
existing = Params().get("MapboxToken")
|
|
if isinstance(existing, bytes):
|
|
existing = existing.decode("utf-8")
|
|
mapbox_token = existing or ""
|
|
|
|
with OpenpilotPrefix():
|
|
if args.fixture is not None:
|
|
os.environ["IQPILOT_NAV_DEMO_FIXTURE"] = str(args.fixture)
|
|
if args.force_local_offline:
|
|
os.environ["IQPILOT_DISABLE_MAPBOX_PROVIDER"] = "1"
|
|
os.environ["IQPILOT_DISABLE_MAPBOX_CACHE"] = "1"
|
|
elif args.no_mapbox:
|
|
os.environ.pop("IQPILOT_DISABLE_MAPBOX_PROVIDER", None)
|
|
os.environ.pop("IQPILOT_DISABLE_MAPBOX_CACHE", None)
|
|
if args.offline_mbtiles is not None:
|
|
os.environ["IQPILOT_OFFLINE_MBTILES"] = str(args.offline_mbtiles)
|
|
if args.offline_tile_root is not None:
|
|
os.environ["IQPILOT_OFFLINE_TILE_ROOT"] = str(args.offline_tile_root)
|
|
global NAV_SCENES, NAV_TIMELINE, build_ui_pubmaster, publish_nav_scene, publish_onroad_seed, seed_ui_test_params
|
|
nav_demo_common = importlib.import_module("openpilot.selfdrive.ui.tests.test_ui.nav_demo_common")
|
|
NAV_SCENES = nav_demo_common.NAV_SCENES
|
|
NAV_TIMELINE = nav_demo_common.NAV_TIMELINE
|
|
build_ui_pubmaster = nav_demo_common.build_ui_pubmaster
|
|
publish_nav_scene = nav_demo_common.publish_nav_scene
|
|
publish_onroad_seed = nav_demo_common.publish_onroad_seed
|
|
seed_ui_test_params = nav_demo_common.seed_ui_test_params
|
|
seed_ui_test_params(Params(), VERSION, mapbox_token=mapbox_token)
|
|
if args.osmaps_mode is not None:
|
|
demo_params = Params()
|
|
demo_params.put_bool("OnlineOSMaps", args.osmaps_mode in ("online", "both"))
|
|
demo_params.put_bool("OfflineOSMaps", args.osmaps_mode in ("offline", "both"))
|
|
demo = NavDemoCapture(args.output_dir)
|
|
demo.run()
|
|
demo.extract_video_stills()
|
|
gif_path = None if args.no_gif else demo.write_gif()
|
|
|
|
print(f"Screenshots written to: {args.output_dir}")
|
|
if demo.video_path.exists():
|
|
print(f"Recorded preview written to: {demo.video_path}")
|
|
if gif_path is not None:
|
|
print(f"Animated preview written to: {gif_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|