Enhance VASM annotations: clarify button labels and add retry snapshot functionality

This commit is contained in:
Prabhaav Pillai
2026-07-25 00:00:26 -04:00
parent 8f945fb6fe
commit 4e711173e0
4 changed files with 68 additions and 32 deletions
@@ -23,7 +23,7 @@ const MENU_ITEMS = {
{ name: "Plots", link: "/plots", icon: "bi-graph-up-arrow" },
{ name: "Testing Ground", link: "/testing_ground", icon: "bi-bezier2" },
{ name: "Troubleshoot", link: "/troubleshoot", icon: "bi-tools" },
{ name: "V-ASM Annotations", link: "/manage_v_asm", icon: "bi-eye" },
{ name: "V-Adj Spot Monitor", link: "/manage_v_asm", icon: "bi-eye" },
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" },
{ name: "Tmux Log", link: "/manage_tmux", icon: "bi-terminal" },
{ name: "Backup and Restore", link: "/manage_toggles", icon: "bi-arrow-repeat" },
@@ -323,7 +323,7 @@
"ui_type": "toggle",
"is_parent_toggle": true,
"requires_nonempty_key": "VASMAnnotationConfig",
"disabled_reason": "Configure window annotations first in Galaxy > V-ASM Annotations",
"disabled_reason": "Configure window annotations first in Galaxy > V-Adj Spot Monitor",
"settings_tier": "advanced"
},
{
@@ -67,8 +67,8 @@ function redraw() {
}
const sides = [
{ key: "left", points: state.leftPoints, fillColor: "rgba(13, 110, 253, 0.25)", borderColor: "#0d6efd", label: "LEFT" },
{ key: "right", points: state.rightPoints, fillColor: "rgba(253, 126, 20, 0.25)", borderColor: "#fd7e14", label: "RIGHT" },
{ key: "left", points: state.leftPoints, fillColor: "rgba(13, 110, 253, 0.25)", borderColor: "#0d6efd", label: "LEFT OF IMAGE" },
{ key: "right", points: state.rightPoints, fillColor: "rgba(253, 126, 20, 0.25)", borderColor: "#fd7e14", label: "RIGHT OF IMAGE" },
];
for (const side of sides) {
@@ -429,7 +429,7 @@ export function VASMAnnotations() {
<ul class="v-asm-card-list">
<li>Trace the visible glass (front and rear side windows on each side, as seen by the driver camera). Mask as much of the window area as possible.</li>
<li>Exclude A-pillars, door frames, and interior. Include the side mirror if visible through the glass.</li>
<li>The B-pillar is fine to include if needed for a continuous mask. Your head or body being in frame is fine (that is part of the training data). Be consistent left vs right.</li>
<li>The B-pillar is fine to include as needed for a continuous mask. Your head or body being in frame is fine (that is part of the training data). Be consistent left vs right.</li>
</ul>
</div>
@@ -451,11 +451,11 @@ export function VASMAnnotations() {
<div class="v-asm-btn-group">
<button class="${state.annotating === "left" ? "v-asm-btn v-asm-btn-left-active" : "v-asm-btn v-asm-btn-outline-left"}"
@click="${startAnnotate}" value="left">
${state.annotating === "left" ? "Annotating Left..." : "Annotate Left"}
${state.annotating === "left" ? "Annotating Left (perspective of image)..." : "Annotate Left (as seen)"}
</button>
<button class="${state.annotating === "right" ? "v-asm-btn v-asm-btn-right-active" : "v-asm-btn v-asm-btn-outline-right"}"
@click="${startAnnotate}" value="right">
${state.annotating === "right" ? "Annotating Right..." : "Annotate Right"}
${state.annotating === "right" ? "Annotating Right (perspective of image)..." : "Annotate Right (as seen)"}
</button>
${state.annotating ? html`<button class="v-asm-btn v-asm-btn-primary" @click="${finishSide}">Finish ${state.annotating === "left" ? "Left" : "Right"}</button>` : ""}
@@ -464,6 +464,9 @@ export function VASMAnnotations() {
</button>
${state.configExists ? html`<button class="v-asm-btn v-asm-btn-danger" @click="${deleteConfig}" .disabled="${state.loading}">Delete Config</button>` : ""}
<button class="v-asm-btn v-asm-btn-secondary" @click="${clearAll}">Clear All</button>
<button class="v-asm-btn v-asm-btn-secondary" @click="${retrySnapshot}" .disabled="${state.loading}">
${state.loading ? "Loading..." : "Get a new Snapshot"}
</button>
</div>
<div class="v-asm-points-summary">
+58 -25
View File
@@ -23,6 +23,9 @@ import selectors
import shutil
import signal
import subprocess
import numpy as np
from msgq.visionipc import VisionIpcClient, VisionStreamType
from PIL import Image
import threading
import time
import traceback
@@ -6381,10 +6384,10 @@ def setup(app):
pw_hash = hashlib.sha256(password.encode()).hexdigest()
GALAXY_DIR.mkdir(parents=True, exist_ok=True)
GALAXY_AUTH_FILE.write_text(pw_hash)
# Generate 256-bit secure session token
GALAXY_SESSION_FILE.write_text(secrets.token_hex(32))
# Generate 16-character alphanumeric routing slug
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
slug = ''.join(secrets.choice(charset) for _ in range(16))
@@ -7446,31 +7449,61 @@ def setup(app):
@app.route("/api/v_asm/snapshot", methods=["GET"])
def v_asm_snapshot():
if params.get_bool("IsOnroad"):
return jsonify({"error": "Snapshot only available while offroad."}), 409
jpeg = _get_live_driver_jpeg()
if jpeg is not None:
return Response(jpeg, mimetype="image/jpeg")
return jsonify({"error": "Unable to capture live frame from driver camera."}), 503
for footage_path in FOOTAGE_PATHS:
if not os.path.isdir(footage_path):
continue
def _get_live_driver_jpeg():
from openpilot.system.manager.process_config import managed_processes
started = False
try:
try:
entries = sorted((e for e in os.listdir(footage_path) if utilities.SEGMENT_RE.fullmatch(e)), reverse=True)
except OSError:
continue
for entry in entries[:3]:
camera_file = os.path.join(footage_path, entry, "dcamera.hevc")
if not os.path.isfile(camera_file):
continue
for seek_time in ("5", "2", None):
command = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-nostdin", "-i", camera_file]
if seek_time is not None:
command.extend(["-ss", seek_time])
command.extend(["-frames:v", "1", "-q:v", "2", "-f", "image2pipe", "-vcodec", "mjpeg", "-"])
try:
result = subprocess.run(command, capture_output=True, check=True, timeout=5, stdin=subprocess.DEVNULL)
return Response(result.stdout, mimetype="image/jpeg")
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
continue
return jsonify({"error": "No driver camera footage available."}), 404
subprocess.check_call(["pgrep", "camerad"])
except subprocess.CalledProcessError:
managed_processes['camerad'].start()
started = True
client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True)
if not client.connect(True):
return None
if started:
settle_deadline = time.monotonic() + 4.0
while time.monotonic() < settle_deadline:
client.recv(timeout_ms=100)
buf = client.recv(timeout_ms=5000)
if buf is None:
return None
y = np.array(buf.data[:buf.uv_offset], dtype=np.uint8).reshape((-1, buf.stride))[:buf.height, :buf.width]
u = np.array(buf.data[buf.uv_offset::2], dtype=np.uint8).reshape((-1, buf.stride // 2))[:buf.height // 2, :buf.width // 2]
v = np.array(buf.data[buf.uv_offset + 1::2], dtype=np.uint8).reshape((-1, buf.stride // 2))[:buf.height // 2, :buf.width // 2]
ul = np.repeat(np.repeat(u, 2).reshape(u.shape[0], y.shape[1]), 2, axis=0).reshape(y.shape)
vl = np.repeat(np.repeat(v, 2).reshape(v.shape[0], y.shape[1]), 2, axis=0).reshape(y.shape)
yuv = np.dstack((y, ul, vl)).astype(np.int16)
yuv[:, :, 1:] -= 128
m = np.array([
[1.00000, 1.00000, 1.00000],
[0.00000, -0.39465, 2.03211],
[1.13983, -0.58060, 0.00000],
])
rgb = np.dot(yuv, m).clip(0, 255).astype(np.uint8)
img = Image.fromarray(rgb)
buf_io = BytesIO()
img.save(buf_io, format="JPEG", quality=85)
return buf_io.getvalue()
except Exception:
return None
finally:
if started:
managed_processes['camerad'].stop()
@app.route("/api/v_asm/config", methods=["GET"])
def v_asm_get_config():