From 46c35925630883f5ea3d9753c7e026b094d1aaf2 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:09:58 -0500 Subject: [PATCH] ftm --- common/params_keys.h | 1 + .../import_manual_review_queue.py | 60 +++++++- .../serve_manual_review_queue.py | 4 +- .../test_review_pipeline.py | 28 ++++ starpilot/common/starpilot_variables.py | 1 + .../assets/components/tools/tuning.js | 40 ++++- starpilot/system/the_galaxy/ftm_workspace.py | 138 ++++++++++++++++-- .../the_galaxy/tests/test_ftm_workspace.py | 106 ++++++++++++++ starpilot/system/the_galaxy/the_galaxy.py | 9 ++ tools/StarPilot/derive_feasible_params.py | 1 + 10 files changed, 365 insertions(+), 23 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index 29ad6fefe..d693f4790 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -321,6 +321,7 @@ inline static std::unordered_map keys = { {"ForceTorqueController", {PERSISTENT, BOOL, "0", "0", 3}}, {"FTMActiveOverrides", {PERSISTENT, JSON, "{}", "{}", 2}}, {"FTMActiveProfileId", {PERSISTENT, STRING, "", "", 2}}, + {"FTMTrialBaseline", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}}, {"FTMTrialApplied", {PERSISTENT, BOOL, "0", "0", 2}}, {"FPSCounter", {PERSISTENT, BOOL, "1", "0", 3}}, {"GalaxyDashboardStats", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}}, diff --git a/scripts/speed_limit_vision/import_manual_review_queue.py b/scripts/speed_limit_vision/import_manual_review_queue.py index 75b6c3ae0..7edaf904b 100644 --- a/scripts/speed_limit_vision/import_manual_review_queue.py +++ b/scripts/speed_limit_vision/import_manual_review_queue.py @@ -305,7 +305,53 @@ def classifier_reject_row(row: dict[str, str], split: str) -> dict[str, object]: } -def positive_classifier_row(row: dict[str, str], split: str) -> dict[str, object]: +def corrected_classifier_crop( + row: dict[str, str], + output_dir: Path, + overwrite: bool, +) -> tuple[str, str, bool]: + original_crop = Path(row.get("crop_path", "")).expanduser() + original_bbox = parse_bbox(row.get("bbox", "")) + review_bbox = parse_bbox(row.get("review_bbox", "")) + if review_bbox is None or review_bbox == original_bbox: + return str(original_crop), row.get("crop_bbox", ""), False + + frame_path = Path(row.get("frame_path", "")).expanduser() + frame = cv2.imread(str(frame_path)) + if frame is None: + raise RuntimeError(f"Cannot regenerate corrected crop for {row['record_key']}: unreadable frame {frame_path}") + + image_h, image_w = frame.shape[:2] + x1, y1, x2, y2 = review_bbox + pad_x = max(round((x2 - x1) * 0.10), 2) + pad_y = max(round((y2 - y1) * 0.10), 2) + crop_bbox = ( + max(x1 - pad_x, 0), + max(y1 - pad_y, 0), + min(x2 + pad_x, image_w), + min(y2 + pad_y, image_h), + ) + crop_x1, crop_y1, crop_x2, crop_y2 = crop_bbox + crop = frame[crop_y1:crop_y2, crop_x1:crop_x2] + if crop.size == 0: + raise RuntimeError(f"Cannot regenerate corrected crop for {row['record_key']}: empty review bbox {review_bbox}") + + corrected_dir = output_dir / "corrected_classifier_crops" + corrected_path = corrected_dir / f"{safe_stem(row['record_key'])}_crop.jpg" + if overwrite or not corrected_path.is_file(): + ensure_dir(corrected_dir) + if not cv2.imwrite(str(corrected_path), crop, [cv2.IMWRITE_JPEG_QUALITY, 94]): + raise RuntimeError(f"Cannot write corrected crop for {row['record_key']}: {corrected_path}") + crop_bbox_text = ",".join(str(value) for value in crop_bbox) + return str(corrected_path), crop_bbox_text, True + + +def positive_classifier_row( + row: dict[str, str], + split: str, + crop_path: str | None = None, + crop_bbox: str | None = None, +) -> dict[str, object]: speed = parse_speed(row.get("review_speed_limit_mph", "")) return { "record_key": row["record_key"], @@ -315,10 +361,10 @@ def positive_classifier_row(row: dict[str, str], split: str) -> dict[str, object "split": split, "speed_limit_mph": speed, "review_sign_type": effective_sign_type(row), - "crop_path": row.get("crop_path", ""), + "crop_path": crop_path if crop_path is not None else row.get("crop_path", ""), "frame_path": row.get("frame_path", ""), - "bbox": row.get("bbox", ""), - "crop_bbox": row.get("crop_bbox", ""), + "bbox": row.get("review_bbox") or row.get("bbox", ""), + "crop_bbox": crop_bbox if crop_bbox is not None else row.get("crop_bbox", ""), "review_status": row.get("review_status", ""), "candidate_speed_limit_mph": row.get("candidate_speed_limit_mph", ""), "candidate_confidence": row.get("candidate_confidence", ""), @@ -436,10 +482,13 @@ def main() -> int: runtime_rows: list[dict[str, object]] = [] detector_rows: list[dict[str, object]] = [] reject_rows: list[dict[str, object]] = [] + corrected_classifier_crops = 0 for row in positive_rows: split = split_for_key(split_group_key(row), args.val_modulo, args.val_remainder) - classifier_rows.append(positive_classifier_row(row, split)) + classifier_crop_path, classifier_crop_bbox, corrected = corrected_classifier_crop(row, output_dir, args.overwrite) + classifier_rows.append(positive_classifier_row(row, split, classifier_crop_path, classifier_crop_bbox)) + corrected_classifier_crops += int(corrected) sample_type = "advisory_negative" if is_advisory_positive(row) else "positive" runtime_rows.append(runtime_row(row, split, sample_type)) detector_row = import_detector_example(workspace, row, split, args.source_name, "positive", args.mode, args.overwrite) @@ -475,6 +524,7 @@ def main() -> int: "uncertain_positive_rows": len(uncertain_positive_rows), "true_negative_rows": len(true_negative_rows), "classifier_reject_rows": len(reject_rows), + "corrected_classifier_crops": corrected_classifier_crops, "classifier_manifest": str(classifier_manifest), "runtime_manifest": str(runtime_manifest), "detector_manifest": str(detector_manifest), diff --git a/scripts/speed_limit_vision/serve_manual_review_queue.py b/scripts/speed_limit_vision/serve_manual_review_queue.py index 67c81f80f..bb882c0ea 100644 --- a/scripts/speed_limit_vision/serve_manual_review_queue.py +++ b/scripts/speed_limit_vision/serve_manual_review_queue.py @@ -114,8 +114,8 @@ HTML = r""" - - + +
diff --git a/scripts/speed_limit_vision/test_review_pipeline.py b/scripts/speed_limit_vision/test_review_pipeline.py index 0e03982d9..efa8dfd6e 100644 --- a/scripts/speed_limit_vision/test_review_pipeline.py +++ b/scripts/speed_limit_vision/test_review_pipeline.py @@ -191,3 +191,31 @@ def test_rescore_row_preserves_before_values_and_marks_gained_read(tmp_path): assert rescored["before_speed_limit_mph"] == "" assert rescored["candidate_speed_limit_mph"] == "20" assert rescored["comparison_change"] == "gained_read" + + +def test_corrected_bbox_regenerates_classifier_crop(tmp_path): + import cv2 + import numpy as np + + frame_path = tmp_path / "frame.jpg" + original_crop_path = tmp_path / "original_crop.jpg" + frame = np.zeros((100, 200, 3), dtype=np.uint8) + frame[20:80, 60:140] = 255 + cv2.imwrite(str(frame_path), frame) + cv2.imwrite(str(original_crop_path), np.zeros((20, 20, 3), dtype=np.uint8)) + row = { + "record_key": "corrected-box", + "frame_path": str(frame_path), + "crop_path": str(original_crop_path), + "bbox": "0,0,20,20", + "crop_bbox": "0,0,24,24", + "review_bbox": "60,20,140,80", + } + + crop_path, crop_bbox, corrected = import_queue.corrected_classifier_crop(row, tmp_path, overwrite=False) + crop = cv2.imread(crop_path) + + assert corrected + assert crop is not None and crop.shape[:2] == (72, 96) + assert crop.mean() > 150 + assert crop_bbox == "52,14,148,86" diff --git a/starpilot/common/starpilot_variables.py b/starpilot/common/starpilot_variables.py index 21bdf5ac2..5f0250e26 100644 --- a/starpilot/common/starpilot_variables.py +++ b/starpilot/common/starpilot_variables.py @@ -234,6 +234,7 @@ EXCLUDED_KEYS = { "ExperimentalLongitudinalEnabled", "FTMActiveOverrides", "FTMActiveProfileId", + "FTMTrialBaseline", "FTMTrialApplied", "InstallDate", "StarPilotCarParamsPersistent", diff --git a/starpilot/system/the_galaxy/assets/components/tools/tuning.js b/starpilot/system/the_galaxy/assets/components/tools/tuning.js index 219026a3d..5254aa2ed 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/tuning.js +++ b/starpilot/system/the_galaxy/assets/components/tools/tuning.js @@ -368,6 +368,7 @@ async function applyProfile(profileId) { }) const payload = await response.json() if (!response.ok) throw new Error(payload.error || "Failed to apply trial profile.") + state.error = "" await fetchWorkspace() showSnackbar(payload.message || "Trial profile applied.") } catch (error) { @@ -409,6 +410,7 @@ async function revertProfile() { const response = await fetch("/api/ftm/trials/revert", { method: "POST" }) const payload = await response.json() if (!response.ok) throw new Error(payload.error || "Failed to revert trial profile.") + state.error = "" await fetchWorkspace() showSnackbar(payload.message || "Trial profile reverted.") } catch (error) { @@ -419,6 +421,26 @@ async function revertProfile() { } } +async function acceptCurrentAsBaseline() { + if (state.runningAction || !state.workspace?.activeTrial) return + if (!window.confirm("Keep the currently applied tuning values and end this trial? This does not restore the previous tune.")) return + + state.runningAction = true + try { + const response = await fetch("/api/ftm/trials/accept", { method: "POST" }) + const payload = await response.json() + if (!response.ok) throw new Error(payload.error || "Failed to keep the current tune.") + state.error = "" + state.workspace = payload.workspace || state.workspace + showSnackbar(payload.message || "Current tune kept as the new baseline.") + } catch (error) { + state.error = error?.message || "Failed to keep the current tune." + showSnackbar(state.error, "error") + } finally { + state.runningAction = false + } +} + function setDimensionFeedback(dimensionId, mode) { const accepted = new Set(state.feedbackAccepted) const ignored = new Set(state.feedbackIgnored) @@ -785,7 +807,7 @@ function renderProfile(profile) { @@ -926,10 +948,18 @@ export function Tuning() { + ${() => state.workspace?.activeTrial?.rollbackAvailable === false ? html` + + ` : ""}