Spaces:
Paused
Paused
Deploy Sensorflow Studio
Browse files- app_backend.py +9 -3
- data/README.md +8 -0
- sensorflow/adapters/local_adapter.py +32 -12
- sensorflow/av_media_filter.py +286 -0
- sensorflow/execution_ops.py +40 -8
- sensorflow/pipeline_artifacts.py +52 -19
- static/app.js +47 -5
- static/index.html +1 -0
- tests/test_pipeline/test_av_media_filter.py +140 -0
- tests/test_pipeline/test_pipeline_browse.py +15 -16
app_backend.py
CHANGED
|
@@ -2447,10 +2447,16 @@ def pipeline_file(path: str):
|
|
| 2447 |
|
| 2448 |
|
| 2449 |
@app.get("/api/dataset/browse")
|
| 2450 |
-
def dataset_browse(
|
| 2451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2452 |
from sensorflow import pipeline_artifacts as artifacts
|
| 2453 |
-
|
|
|
|
|
|
|
| 2454 |
|
| 2455 |
|
| 2456 |
@app.get("/api/mitl/queue")
|
|
|
|
| 2447 |
|
| 2448 |
|
| 2449 |
@app.get("/api/dataset/browse")
|
| 2450 |
+
def dataset_browse(
|
| 2451 |
+
source_path: str = "data",
|
| 2452 |
+
limit: int = 48,
|
| 2453 |
+
dataset_type: Optional[str] = None,
|
| 2454 |
+
):
|
| 2455 |
+
"""Validate Images Path by listing browsable local driving-relevant frames."""
|
| 2456 |
from sensorflow import pipeline_artifacts as artifacts
|
| 2457 |
+
|
| 2458 |
+
dtype = dataset_type or load_config().dataset_type
|
| 2459 |
+
return artifacts.scan_source_images(source_path, limit=limit, dataset_type=dtype)
|
| 2460 |
|
| 2461 |
|
| 2462 |
@app.get("/api/mitl/queue")
|
data/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Local data folder
|
| 2 |
+
|
| 3 |
+
This folder may contain ad-hoc test assets. **Dataset browse and load filters** exclude
|
| 4 |
+
non-driving media (README assets, logos, generic `test.jpg` files, docs paths, and files
|
| 5 |
+
that do not match AV camera-frame layout when an official dataset type is selected).
|
| 6 |
+
|
| 7 |
+
Place real camera frames under a nested path such as `data/images/train/` or
|
| 8 |
+
`data/camera/front/` for Validate & Browse and Local ingest to pick them up.
|
sensorflow/adapters/local_adapter.py
CHANGED
|
@@ -23,29 +23,49 @@ VIDEO_EXTS = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".m4v"}
|
|
| 23 |
DEFAULT_MAX_FRAMES = 10_000
|
| 24 |
|
| 25 |
|
| 26 |
-
def discover_media(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
"""Return (image_files, video_files) under path (file or directory)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
if path.is_file():
|
| 29 |
ext = path.suffix.lower()
|
| 30 |
-
if ext in
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
| 34 |
return [], []
|
| 35 |
|
| 36 |
if not path.is_dir():
|
| 37 |
return [], []
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
for p in sorted(path.rglob("*")):
|
| 42 |
if not p.is_file():
|
| 43 |
continue
|
| 44 |
ext = p.suffix.lower()
|
| 45 |
-
if ext in
|
| 46 |
-
|
| 47 |
-
elif ext in
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
return images, videos
|
| 50 |
|
| 51 |
|
|
@@ -111,7 +131,7 @@ class LocalSequenceAdapter(VendorAdapter):
|
|
| 111 |
"or a video file, then re-run ingest with Local enabled."
|
| 112 |
)
|
| 113 |
|
| 114 |
-
images, videos = discover_media(path)
|
| 115 |
frame_images = list(images)
|
| 116 |
|
| 117 |
decode_dir = Path("runs/pipeline") / sequence_id / "decoded_frames"
|
|
|
|
| 23 |
DEFAULT_MAX_FRAMES = 10_000
|
| 24 |
|
| 25 |
|
| 26 |
+
def discover_media(
|
| 27 |
+
path: Path,
|
| 28 |
+
*,
|
| 29 |
+
dataset_type: Optional[str] = None,
|
| 30 |
+
) -> Tuple[List[Path], List[Path]]:
|
| 31 |
"""Return (image_files, video_files) under path (file or directory)."""
|
| 32 |
+
from sensorflow.av_media_filter import (
|
| 33 |
+
IMAGE_EXTS as AV_IMAGE_EXTS,
|
| 34 |
+
VIDEO_EXTS as AV_VIDEO_EXTS,
|
| 35 |
+
partition_media_files,
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
if path.is_file():
|
| 39 |
ext = path.suffix.lower()
|
| 40 |
+
if ext in AV_IMAGE_EXTS:
|
| 41 |
+
images, _ = partition_media_files([path], root=path.parent, dataset_type=dataset_type)
|
| 42 |
+
return images, []
|
| 43 |
+
if ext in AV_VIDEO_EXTS:
|
| 44 |
+
_, excluded = partition_media_files([path], root=path.parent, dataset_type=dataset_type, allow_videos=True)
|
| 45 |
+
return [], [] if excluded else [path]
|
| 46 |
return [], []
|
| 47 |
|
| 48 |
if not path.is_dir():
|
| 49 |
return [], []
|
| 50 |
|
| 51 |
+
raw_images: List[Path] = []
|
| 52 |
+
raw_videos: List[Path] = []
|
| 53 |
for p in sorted(path.rglob("*")):
|
| 54 |
if not p.is_file():
|
| 55 |
continue
|
| 56 |
ext = p.suffix.lower()
|
| 57 |
+
if ext in AV_IMAGE_EXTS:
|
| 58 |
+
raw_images.append(p)
|
| 59 |
+
elif ext in AV_VIDEO_EXTS:
|
| 60 |
+
raw_videos.append(p)
|
| 61 |
+
|
| 62 |
+
images, _ = partition_media_files(raw_images, root=path, dataset_type=dataset_type)
|
| 63 |
+
videos, _ = partition_media_files(
|
| 64 |
+
raw_videos,
|
| 65 |
+
root=path,
|
| 66 |
+
dataset_type=dataset_type,
|
| 67 |
+
allow_videos=True,
|
| 68 |
+
)
|
| 69 |
return images, videos
|
| 70 |
|
| 71 |
|
|
|
|
| 131 |
"or a video file, then re-run ingest with Local enabled."
|
| 132 |
)
|
| 133 |
|
| 134 |
+
images, videos = discover_media(path, dataset_type=source.get("dataset_type"))
|
| 135 |
frame_images = list(images)
|
| 136 |
|
| 137 |
decode_dir = Path("runs/pipeline") / sequence_id / "decoded_frames"
|
sensorflow/av_media_filter.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Heuristic filters for driving / AV pipeline image and video discovery."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
| 8 |
+
|
| 9 |
+
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"}
|
| 10 |
+
VIDEO_EXTS = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".m4v"}
|
| 11 |
+
|
| 12 |
+
# Path segments that almost never contain drivable-scene media.
|
| 13 |
+
EXCLUDED_DIR_SEGMENTS = {
|
| 14 |
+
".git",
|
| 15 |
+
".github",
|
| 16 |
+
".venv",
|
| 17 |
+
"venv",
|
| 18 |
+
"__pycache__",
|
| 19 |
+
"node_modules",
|
| 20 |
+
"dist",
|
| 21 |
+
"build",
|
| 22 |
+
"docs",
|
| 23 |
+
"doc",
|
| 24 |
+
"documentation",
|
| 25 |
+
"marketing",
|
| 26 |
+
"assets",
|
| 27 |
+
"static/icons",
|
| 28 |
+
"icons",
|
| 29 |
+
"badges",
|
| 30 |
+
"screenshots",
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
# Filename stems that are commonly README / branding / test junk.
|
| 34 |
+
JUNK_NAME_STEMS = {
|
| 35 |
+
"logo",
|
| 36 |
+
"icon",
|
| 37 |
+
"favicon",
|
| 38 |
+
"avatar",
|
| 39 |
+
"profile",
|
| 40 |
+
"readme",
|
| 41 |
+
"banner",
|
| 42 |
+
"badge",
|
| 43 |
+
"placeholder",
|
| 44 |
+
"sample",
|
| 45 |
+
"example",
|
| 46 |
+
"demo",
|
| 47 |
+
"test",
|
| 48 |
+
"screenshot",
|
| 49 |
+
"thumbnail",
|
| 50 |
+
"thumb",
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# Substrings in relative paths that suggest AV camera frames.
|
| 54 |
+
AV_PATH_MARKERS = (
|
| 55 |
+
"camera",
|
| 56 |
+
"cameras",
|
| 57 |
+
"images",
|
| 58 |
+
"frames",
|
| 59 |
+
"frame",
|
| 60 |
+
"front",
|
| 61 |
+
"rear",
|
| 62 |
+
"left",
|
| 63 |
+
"right",
|
| 64 |
+
"rgb",
|
| 65 |
+
"video",
|
| 66 |
+
"sequence",
|
| 67 |
+
"segment",
|
| 68 |
+
"train",
|
| 69 |
+
"val",
|
| 70 |
+
"training",
|
| 71 |
+
"validation",
|
| 72 |
+
"lidar",
|
| 73 |
+
"sensor",
|
| 74 |
+
"recording",
|
| 75 |
+
"drive",
|
| 76 |
+
"driving",
|
| 77 |
+
"scene",
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
OFFICIAL_AV_DATASETS = frozenset({"alpamayo", "waymo", "a2d2"})
|
| 81 |
+
|
| 82 |
+
MIN_DRIVING_IMAGE_BYTES = 10 * 1024 # icons / tiny README assets
|
| 83 |
+
|
| 84 |
+
EXCLUSION_CATEGORIES = (
|
| 85 |
+
"wrong_extension",
|
| 86 |
+
"excluded_path",
|
| 87 |
+
"junk_filename",
|
| 88 |
+
"too_small",
|
| 89 |
+
"non_av_layout",
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _normalize_parts(path: Path) -> Tuple[str, ...]:
|
| 94 |
+
return tuple(part.lower() for part in path.parts)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _relative_to_root(path: Path, root: Optional[Path]) -> Path:
|
| 98 |
+
if root is None:
|
| 99 |
+
return path
|
| 100 |
+
try:
|
| 101 |
+
return path.resolve().relative_to(root.resolve())
|
| 102 |
+
except ValueError:
|
| 103 |
+
return path
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _has_excluded_dir(path: Path) -> bool:
|
| 107 |
+
parts = _normalize_parts(path)
|
| 108 |
+
for i, part in enumerate(parts):
|
| 109 |
+
if part in EXCLUDED_DIR_SEGMENTS:
|
| 110 |
+
return True
|
| 111 |
+
if part == "assets" and i + 1 < len(parts) and parts[i + 1] in {
|
| 112 |
+
"marketing",
|
| 113 |
+
"branding",
|
| 114 |
+
"icons",
|
| 115 |
+
"logos",
|
| 116 |
+
}:
|
| 117 |
+
return True
|
| 118 |
+
return False
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _junk_filename(path: Path) -> bool:
|
| 122 |
+
stem = path.stem.lower()
|
| 123 |
+
if stem in JUNK_NAME_STEMS:
|
| 124 |
+
return True
|
| 125 |
+
if re.match(r"^(logo|icon|favicon|readme|banner|badge)[-_]?\d*$", stem):
|
| 126 |
+
return True
|
| 127 |
+
return False
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _looks_like_av_layout(relative: Path) -> bool:
|
| 131 |
+
rel_str = str(relative).lower().replace("\\", "/")
|
| 132 |
+
if any(marker in rel_str for marker in AV_PATH_MARKERS):
|
| 133 |
+
return True
|
| 134 |
+
# Nested structure (e.g. data/sequences/001/front.jpg) — not a loose root file.
|
| 135 |
+
return len(relative.parts) >= 2
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def classify_media_file(
|
| 139 |
+
path: Path,
|
| 140 |
+
*,
|
| 141 |
+
root: Optional[Path] = None,
|
| 142 |
+
dataset_type: Optional[str] = None,
|
| 143 |
+
allow_videos: bool = True,
|
| 144 |
+
) -> Tuple[bool, Optional[str]]:
|
| 145 |
+
"""Return (accepted, exclusion_category)."""
|
| 146 |
+
if not path.is_file():
|
| 147 |
+
return False, "non_media"
|
| 148 |
+
|
| 149 |
+
ext = path.suffix.lower()
|
| 150 |
+
allowed = IMAGE_EXTS | (VIDEO_EXTS if allow_videos else set())
|
| 151 |
+
if ext not in allowed:
|
| 152 |
+
return False, "wrong_extension"
|
| 153 |
+
|
| 154 |
+
if _has_excluded_dir(path):
|
| 155 |
+
return False, "excluded_path"
|
| 156 |
+
|
| 157 |
+
if _junk_filename(path):
|
| 158 |
+
return False, "junk_filename"
|
| 159 |
+
|
| 160 |
+
if ext in IMAGE_EXTS:
|
| 161 |
+
try:
|
| 162 |
+
size = path.stat().st_size
|
| 163 |
+
except OSError:
|
| 164 |
+
return False, "non_media"
|
| 165 |
+
if size < MIN_DRIVING_IMAGE_BYTES:
|
| 166 |
+
return False, "too_small"
|
| 167 |
+
|
| 168 |
+
relative = _relative_to_root(path, root)
|
| 169 |
+
dtype = (dataset_type or "local").lower()
|
| 170 |
+
if dtype in OFFICIAL_AV_DATASETS and not _looks_like_av_layout(relative):
|
| 171 |
+
return False, "non_av_layout"
|
| 172 |
+
|
| 173 |
+
return True, None
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def partition_media_files(
|
| 177 |
+
paths: Iterable[Path],
|
| 178 |
+
*,
|
| 179 |
+
root: Optional[Path] = None,
|
| 180 |
+
dataset_type: Optional[str] = None,
|
| 181 |
+
allow_videos: bool = True,
|
| 182 |
+
) -> Tuple[List[Path], List[Dict[str, Any]]]:
|
| 183 |
+
"""Split paths into accepted driving media and excluded entries with reasons."""
|
| 184 |
+
accepted: List[Path] = []
|
| 185 |
+
excluded: List[Dict[str, Any]] = []
|
| 186 |
+
for path in paths:
|
| 187 |
+
ok, reason = classify_media_file(
|
| 188 |
+
path,
|
| 189 |
+
root=root,
|
| 190 |
+
dataset_type=dataset_type,
|
| 191 |
+
allow_videos=allow_videos,
|
| 192 |
+
)
|
| 193 |
+
if ok:
|
| 194 |
+
accepted.append(path)
|
| 195 |
+
else:
|
| 196 |
+
excluded.append(
|
| 197 |
+
{
|
| 198 |
+
"path": str(path),
|
| 199 |
+
"name": path.name,
|
| 200 |
+
"reason": reason or "excluded_path",
|
| 201 |
+
}
|
| 202 |
+
)
|
| 203 |
+
accepted.sort(key=lambda p: str(p).lower())
|
| 204 |
+
return accepted, excluded
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def iter_candidate_files(
|
| 208 |
+
root: Path,
|
| 209 |
+
*,
|
| 210 |
+
allow_videos: bool = True,
|
| 211 |
+
) -> List[Path]:
|
| 212 |
+
"""Collect image/video candidates under root before AV heuristics."""
|
| 213 |
+
exts = set(IMAGE_EXTS)
|
| 214 |
+
if allow_videos:
|
| 215 |
+
exts |= VIDEO_EXTS
|
| 216 |
+
|
| 217 |
+
if root.is_file():
|
| 218 |
+
return [root] if root.suffix.lower() in exts else []
|
| 219 |
+
|
| 220 |
+
if not root.is_dir():
|
| 221 |
+
return []
|
| 222 |
+
|
| 223 |
+
out: List[Path] = []
|
| 224 |
+
for p in root.rglob("*"):
|
| 225 |
+
if p.is_file() and p.suffix.lower() in exts:
|
| 226 |
+
out.append(p)
|
| 227 |
+
return out
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def scan_driving_media(
|
| 231 |
+
source_path: str | Path,
|
| 232 |
+
*,
|
| 233 |
+
dataset_type: Optional[str] = None,
|
| 234 |
+
allow_videos: bool = True,
|
| 235 |
+
limit: Optional[int] = None,
|
| 236 |
+
) -> Dict[str, Any]:
|
| 237 |
+
"""Scan a path and return accepted media plus exclusion breakdown."""
|
| 238 |
+
root = Path(source_path)
|
| 239 |
+
if not root.is_absolute():
|
| 240 |
+
root = Path.cwd() / root
|
| 241 |
+
|
| 242 |
+
if not root.exists():
|
| 243 |
+
return {
|
| 244 |
+
"source_path": str(source_path),
|
| 245 |
+
"exists": False,
|
| 246 |
+
"accepted": [],
|
| 247 |
+
"excluded": [],
|
| 248 |
+
"count": 0,
|
| 249 |
+
"excluded_count": 0,
|
| 250 |
+
"excluded_by_reason": {},
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
candidates = iter_candidate_files(root, allow_videos=allow_videos)
|
| 254 |
+
scan_root = root if root.is_dir() else root.parent
|
| 255 |
+
accepted, excluded = partition_media_files(
|
| 256 |
+
candidates,
|
| 257 |
+
root=scan_root,
|
| 258 |
+
dataset_type=dataset_type,
|
| 259 |
+
allow_videos=allow_videos,
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
if limit is not None:
|
| 263 |
+
accepted = accepted[:limit]
|
| 264 |
+
|
| 265 |
+
by_reason: Dict[str, int] = {}
|
| 266 |
+
for item in excluded:
|
| 267 |
+
reason = item.get("reason") or "excluded_path"
|
| 268 |
+
by_reason[reason] = by_reason.get(reason, 0) + 1
|
| 269 |
+
|
| 270 |
+
return {
|
| 271 |
+
"source_path": str(source_path),
|
| 272 |
+
"exists": True,
|
| 273 |
+
"accepted": accepted,
|
| 274 |
+
"excluded": excluded,
|
| 275 |
+
"count": len(accepted),
|
| 276 |
+
"excluded_count": len(excluded),
|
| 277 |
+
"excluded_by_reason": by_reason,
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def summarize_exclusions(excluded: Sequence[Dict[str, Any]]) -> Dict[str, int]:
|
| 282 |
+
counts: Dict[str, int] = {}
|
| 283 |
+
for item in excluded:
|
| 284 |
+
reason = item.get("reason") or "excluded_path"
|
| 285 |
+
counts[reason] = counts.get(reason, 0) + 1
|
| 286 |
+
return counts
|
sensorflow/execution_ops.py
CHANGED
|
@@ -22,8 +22,15 @@ def _resolve(path: str | Path) -> Path:
|
|
| 22 |
return p
|
| 23 |
|
| 24 |
|
| 25 |
-
def discover_images(
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
root = _resolve(source_path)
|
| 28 |
result: Dict[str, Any] = {
|
| 29 |
"source_path": str(root),
|
|
@@ -32,6 +39,9 @@ def discover_images(source_path: str, *, max_scan: int = 100_000) -> Dict[str, A
|
|
| 32 |
"images_discovered": 0,
|
| 33 |
"images_readable": 0,
|
| 34 |
"images_corrupt": 0,
|
|
|
|
|
|
|
|
|
|
| 35 |
"annotations_discovered": 0,
|
| 36 |
"annotation_objects": 0,
|
| 37 |
"class_ids": [],
|
|
@@ -45,11 +55,11 @@ def discover_images(source_path: str, *, max_scan: int = 100_000) -> Dict[str, A
|
|
| 45 |
result["errors"].append(f"Path does not exist: {source_path}")
|
| 46 |
return result
|
| 47 |
|
| 48 |
-
|
| 49 |
labels: List[Path] = []
|
| 50 |
if root.is_file():
|
| 51 |
if root.suffix.lower() in IMAGE_EXTS:
|
| 52 |
-
|
| 53 |
elif root.suffix.lower() in LABEL_EXTS:
|
| 54 |
labels = [root]
|
| 55 |
else:
|
|
@@ -61,14 +71,31 @@ def discover_images(source_path: str, *, max_scan: int = 100_000) -> Dict[str, A
|
|
| 61 |
continue
|
| 62 |
suf = p.suffix.lower()
|
| 63 |
if suf in IMAGE_EXTS:
|
| 64 |
-
|
| 65 |
elif suf in LABEL_EXTS:
|
| 66 |
labels.append(p)
|
| 67 |
-
if len(
|
| 68 |
result["warnings"].append(f"Scan capped at {max_scan} files")
|
| 69 |
break
|
| 70 |
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
labels = sorted(labels)
|
| 73 |
result["images_discovered"] = len(images)
|
| 74 |
result["annotations_discovered"] = len(labels)
|
|
@@ -158,6 +185,9 @@ def write_load_manifest(discovery: Dict[str, Any], dest_dir: Path) -> Path:
|
|
| 158 |
"images_discovered": discovery.get("images_discovered"),
|
| 159 |
"images_readable": discovery.get("images_readable"),
|
| 160 |
"images_corrupt": discovery.get("images_corrupt"),
|
|
|
|
|
|
|
|
|
|
| 161 |
"annotations_discovered": discovery.get("annotations_discovered"),
|
| 162 |
"annotation_objects": discovery.get("annotation_objects"),
|
| 163 |
"class_ids": discovery.get("class_ids"),
|
|
@@ -175,7 +205,7 @@ def load_and_preprocess(
|
|
| 175 |
dataset_type: str = "local",
|
| 176 |
) -> Dict[str, Any]:
|
| 177 |
"""Real Load & Preprocess: discovery + optional YAML semantic checks."""
|
| 178 |
-
discovery = discover_images(source_path)
|
| 179 |
yaml_report = None
|
| 180 |
if yaml_path:
|
| 181 |
ypath = Path(yaml_path)
|
|
@@ -230,6 +260,8 @@ def load_and_preprocess(
|
|
| 230 |
"images_discovered": discovery["images_discovered"],
|
| 231 |
"images_readable": discovery["images_readable"],
|
| 232 |
"images_corrupt": discovery["images_corrupt"],
|
|
|
|
|
|
|
| 233 |
"annotations_discovered": discovery["annotations_discovered"],
|
| 234 |
"annotation_objects": discovery["annotation_objects"],
|
| 235 |
"loaded_pct_of_discovered": loaded_pct,
|
|
|
|
| 22 |
return p
|
| 23 |
|
| 24 |
|
| 25 |
+
def discover_images(
|
| 26 |
+
source_path: str,
|
| 27 |
+
*,
|
| 28 |
+
max_scan: int = 100_000,
|
| 29 |
+
dataset_type: Optional[str] = None,
|
| 30 |
+
) -> Dict[str, Any]:
|
| 31 |
+
"""Discover driving-relevant images under a path; report corrupt/unreadable files."""
|
| 32 |
+
from sensorflow.av_media_filter import partition_media_files
|
| 33 |
+
|
| 34 |
root = _resolve(source_path)
|
| 35 |
result: Dict[str, Any] = {
|
| 36 |
"source_path": str(root),
|
|
|
|
| 39 |
"images_discovered": 0,
|
| 40 |
"images_readable": 0,
|
| 41 |
"images_corrupt": 0,
|
| 42 |
+
"images_excluded": 0,
|
| 43 |
+
"excluded_by_reason": {},
|
| 44 |
+
"excluded_samples": [],
|
| 45 |
"annotations_discovered": 0,
|
| 46 |
"annotation_objects": 0,
|
| 47 |
"class_ids": [],
|
|
|
|
| 55 |
result["errors"].append(f"Path does not exist: {source_path}")
|
| 56 |
return result
|
| 57 |
|
| 58 |
+
raw_images: List[Path] = []
|
| 59 |
labels: List[Path] = []
|
| 60 |
if root.is_file():
|
| 61 |
if root.suffix.lower() in IMAGE_EXTS:
|
| 62 |
+
raw_images = [root]
|
| 63 |
elif root.suffix.lower() in LABEL_EXTS:
|
| 64 |
labels = [root]
|
| 65 |
else:
|
|
|
|
| 71 |
continue
|
| 72 |
suf = p.suffix.lower()
|
| 73 |
if suf in IMAGE_EXTS:
|
| 74 |
+
raw_images.append(p)
|
| 75 |
elif suf in LABEL_EXTS:
|
| 76 |
labels.append(p)
|
| 77 |
+
if len(raw_images) + len(labels) >= max_scan:
|
| 78 |
result["warnings"].append(f"Scan capped at {max_scan} files")
|
| 79 |
break
|
| 80 |
|
| 81 |
+
scan_root = root if root.is_dir() else root.parent
|
| 82 |
+
images, excluded = partition_media_files(
|
| 83 |
+
raw_images,
|
| 84 |
+
root=scan_root,
|
| 85 |
+
dataset_type=dataset_type,
|
| 86 |
+
allow_videos=False,
|
| 87 |
+
)
|
| 88 |
+
result["images_excluded"] = len(excluded)
|
| 89 |
+
result["excluded_by_reason"] = {
|
| 90 |
+
reason: sum(1 for e in excluded if e.get("reason") == reason)
|
| 91 |
+
for reason in sorted({e.get("reason", "excluded_path") for e in excluded})
|
| 92 |
+
}
|
| 93 |
+
result["excluded_samples"] = excluded[:50]
|
| 94 |
+
if excluded:
|
| 95 |
+
result["warnings"].append(
|
| 96 |
+
f"{len(excluded)} file(s) excluded as non-driving media "
|
| 97 |
+
f"({', '.join(f'{k}: {v}' for k, v in result['excluded_by_reason'].items())})"
|
| 98 |
+
)
|
| 99 |
labels = sorted(labels)
|
| 100 |
result["images_discovered"] = len(images)
|
| 101 |
result["annotations_discovered"] = len(labels)
|
|
|
|
| 185 |
"images_discovered": discovery.get("images_discovered"),
|
| 186 |
"images_readable": discovery.get("images_readable"),
|
| 187 |
"images_corrupt": discovery.get("images_corrupt"),
|
| 188 |
+
"images_excluded": discovery.get("images_excluded"),
|
| 189 |
+
"excluded_by_reason": discovery.get("excluded_by_reason"),
|
| 190 |
+
"excluded_samples": discovery.get("excluded_samples"),
|
| 191 |
"annotations_discovered": discovery.get("annotations_discovered"),
|
| 192 |
"annotation_objects": discovery.get("annotation_objects"),
|
| 193 |
"class_ids": discovery.get("class_ids"),
|
|
|
|
| 205 |
dataset_type: str = "local",
|
| 206 |
) -> Dict[str, Any]:
|
| 207 |
"""Real Load & Preprocess: discovery + optional YAML semantic checks."""
|
| 208 |
+
discovery = discover_images(source_path, dataset_type=dataset_type)
|
| 209 |
yaml_report = None
|
| 210 |
if yaml_path:
|
| 211 |
ypath = Path(yaml_path)
|
|
|
|
| 260 |
"images_discovered": discovery["images_discovered"],
|
| 261 |
"images_readable": discovery["images_readable"],
|
| 262 |
"images_corrupt": discovery["images_corrupt"],
|
| 263 |
+
"images_excluded": discovery.get("images_excluded", 0),
|
| 264 |
+
"excluded_by_reason": discovery.get("excluded_by_reason", {}),
|
| 265 |
"annotations_discovered": discovery["annotations_discovered"],
|
| 266 |
"annotation_objects": discovery["annotation_objects"],
|
| 267 |
"loaded_pct_of_discovered": loaded_pct,
|
sensorflow/pipeline_artifacts.py
CHANGED
|
@@ -253,9 +253,19 @@ def resolve_file(path_str: str) -> Tuple[Path, str]:
|
|
| 253 |
return resolved, media_type or "application/octet-stream"
|
| 254 |
|
| 255 |
|
| 256 |
-
def scan_source_images(
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
path = Path(source_path)
|
|
|
|
|
|
|
|
|
|
| 259 |
if not path.exists():
|
| 260 |
return {
|
| 261 |
"status": "ok",
|
|
@@ -264,19 +274,13 @@ def scan_source_images(source_path: str, limit: int = 48) -> Dict[str, Any]:
|
|
| 264 |
"browsable": False,
|
| 265 |
"count": 0,
|
| 266 |
"images": [],
|
|
|
|
|
|
|
|
|
|
| 267 |
"empty_reason": f"Path does not exist: {source_path}",
|
| 268 |
}
|
| 269 |
|
| 270 |
-
|
| 271 |
-
if path.is_file() and path.suffix.lower() in IMAGE_EXTS:
|
| 272 |
-
images = [path]
|
| 273 |
-
elif path.is_dir():
|
| 274 |
-
for p in sorted(path.rglob("*")):
|
| 275 |
-
if p.is_file() and p.suffix.lower() in IMAGE_EXTS:
|
| 276 |
-
images.append(p)
|
| 277 |
-
if len(images) >= max(limit, 1):
|
| 278 |
-
break
|
| 279 |
-
else:
|
| 280 |
return {
|
| 281 |
"status": "ok",
|
| 282 |
"source_path": source_path,
|
|
@@ -284,16 +288,24 @@ def scan_source_images(source_path: str, limit: int = 48) -> Dict[str, Any]:
|
|
| 284 |
"browsable": False,
|
| 285 |
"count": 0,
|
| 286 |
"images": [],
|
| 287 |
-
"
|
|
|
|
|
|
|
|
|
|
| 288 |
}
|
| 289 |
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
|
| 295 |
previews = []
|
| 296 |
-
for img in
|
| 297 |
previews.append(
|
| 298 |
{
|
| 299 |
"name": img.name,
|
|
@@ -302,6 +314,23 @@ def scan_source_images(source_path: str, limit: int = 48) -> Dict[str, Any]:
|
|
| 302 |
}
|
| 303 |
)
|
| 304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
return {
|
| 306 |
"status": "ok",
|
| 307 |
"source_path": source_path,
|
|
@@ -309,5 +338,9 @@ def scan_source_images(source_path: str, limit: int = 48) -> Dict[str, Any]:
|
|
| 309 |
"browsable": total > 0,
|
| 310 |
"count": total,
|
| 311 |
"images": previews,
|
| 312 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
}
|
|
|
|
| 253 |
return resolved, media_type or "application/octet-stream"
|
| 254 |
|
| 255 |
|
| 256 |
+
def scan_source_images(
|
| 257 |
+
source_path: str,
|
| 258 |
+
limit: int = 48,
|
| 259 |
+
*,
|
| 260 |
+
dataset_type: Optional[str] = None,
|
| 261 |
+
) -> Dict[str, Any]:
|
| 262 |
+
"""List local driving-relevant images under Dataset Configuration Images Path."""
|
| 263 |
+
from sensorflow.av_media_filter import OFFICIAL_AV_DATASETS, scan_driving_media
|
| 264 |
+
|
| 265 |
path = Path(source_path)
|
| 266 |
+
if not path.is_absolute():
|
| 267 |
+
path = Path.cwd() / path
|
| 268 |
+
|
| 269 |
if not path.exists():
|
| 270 |
return {
|
| 271 |
"status": "ok",
|
|
|
|
| 274 |
"browsable": False,
|
| 275 |
"count": 0,
|
| 276 |
"images": [],
|
| 277 |
+
"excluded_count": 0,
|
| 278 |
+
"excluded": [],
|
| 279 |
+
"excluded_by_reason": {},
|
| 280 |
"empty_reason": f"Path does not exist: {source_path}",
|
| 281 |
}
|
| 282 |
|
| 283 |
+
if path.is_file() and path.suffix.lower() not in IMAGE_EXTS:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
return {
|
| 285 |
"status": "ok",
|
| 286 |
"source_path": source_path,
|
|
|
|
| 288 |
"browsable": False,
|
| 289 |
"count": 0,
|
| 290 |
"images": [],
|
| 291 |
+
"excluded_count": 0,
|
| 292 |
+
"excluded": [],
|
| 293 |
+
"excluded_by_reason": {},
|
| 294 |
+
"empty_reason": "Not an image file or directory of driving media.",
|
| 295 |
}
|
| 296 |
|
| 297 |
+
scan = scan_driving_media(
|
| 298 |
+
path,
|
| 299 |
+
dataset_type=dataset_type,
|
| 300 |
+
allow_videos=False,
|
| 301 |
+
)
|
| 302 |
+
accepted = scan["accepted"]
|
| 303 |
+
excluded = scan["excluded"]
|
| 304 |
+
total = scan["count"]
|
| 305 |
+
dtype = (dataset_type or "local").lower()
|
| 306 |
|
| 307 |
previews = []
|
| 308 |
+
for img in accepted[:limit]:
|
| 309 |
previews.append(
|
| 310 |
{
|
| 311 |
"name": img.name,
|
|
|
|
| 314 |
}
|
| 315 |
)
|
| 316 |
|
| 317 |
+
empty_reason = None
|
| 318 |
+
if total == 0:
|
| 319 |
+
if excluded:
|
| 320 |
+
empty_reason = (
|
| 321 |
+
f"No driving-relevant images found under {source_path} "
|
| 322 |
+
f"({len(excluded)} file(s) excluded as non-AV media)."
|
| 323 |
+
)
|
| 324 |
+
else:
|
| 325 |
+
empty_reason = "No driving-relevant images found under this path."
|
| 326 |
+
|
| 327 |
+
warnings: List[str] = []
|
| 328 |
+
if dtype in OFFICIAL_AV_DATASETS and excluded:
|
| 329 |
+
warnings.append(
|
| 330 |
+
f"Browsed path does not match typical {dtype} layout; "
|
| 331 |
+
"point Images Path at downloaded camera frames or enable Local ingest."
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
return {
|
| 335 |
"status": "ok",
|
| 336 |
"source_path": source_path,
|
|
|
|
| 338 |
"browsable": total > 0,
|
| 339 |
"count": total,
|
| 340 |
"images": previews,
|
| 341 |
+
"excluded_count": scan["excluded_count"],
|
| 342 |
+
"excluded": excluded[:200],
|
| 343 |
+
"excluded_by_reason": scan["excluded_by_reason"],
|
| 344 |
+
"warnings": warnings,
|
| 345 |
+
"empty_reason": empty_reason,
|
| 346 |
}
|
static/app.js
CHANGED
|
@@ -922,13 +922,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 922 |
if (btnBrowseSource) {
|
| 923 |
btnBrowseSource.addEventListener('click', async () => {
|
| 924 |
const sourcePath = datasetSourcePath?.value || document.getElementById('input-source')?.value || 'data';
|
|
|
|
| 925 |
const statusEl = document.getElementById('source-browse-status');
|
|
|
|
| 926 |
const gallery = document.getElementById('source-browse-gallery');
|
| 927 |
btnBrowseSource.disabled = true;
|
| 928 |
btnBrowseSource.textContent = 'Scanning…';
|
| 929 |
if (statusEl) statusEl.textContent = `Validating ${sourcePath}…`;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
try {
|
| 931 |
-
const res = await fetch(
|
|
|
|
|
|
|
|
|
|
| 932 |
const data = await res.json();
|
| 933 |
if (gallery) {
|
| 934 |
gallery.innerHTML = '';
|
|
@@ -942,20 +951,53 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 942 |
});
|
| 943 |
}
|
| 944 |
if (statusEl) {
|
| 945 |
-
|
| 946 |
-
? `Browsable: ${data.count} image(s) under ${data.source_path}`
|
| 947 |
: (data.empty_reason || 'Nothing browsable at this path.');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 948 |
}
|
| 949 |
const badge = document.getElementById('ds-meta-badge');
|
| 950 |
if (badge) {
|
| 951 |
if (data.browsable) {
|
| 952 |
-
badge.textContent = `${data.count} images browsable on disk`;
|
| 953 |
badge.style.background = 'rgba(0, 255, 170, 0.15)';
|
| 954 |
badge.style.color = '#00ffaa';
|
| 955 |
badge.style.border = '1px solid rgba(0, 255, 170, 0.3)';
|
| 956 |
document.getElementById('ds-kpi-loaded-rows').textContent = data.count;
|
| 957 |
} else {
|
| 958 |
-
badge.textContent = 'Not browsable —
|
| 959 |
badge.style.background = 'rgba(255, 68, 68, 0.12)';
|
| 960 |
badge.style.color = '#ff8888';
|
| 961 |
badge.style.border = '1px solid rgba(255, 68, 68, 0.35)';
|
|
|
|
| 922 |
if (btnBrowseSource) {
|
| 923 |
btnBrowseSource.addEventListener('click', async () => {
|
| 924 |
const sourcePath = datasetSourcePath?.value || document.getElementById('input-source')?.value || 'data';
|
| 925 |
+
const datasetType = selectDatasetType?.value || 'local';
|
| 926 |
const statusEl = document.getElementById('source-browse-status');
|
| 927 |
+
const excludedEl = document.getElementById('source-browse-excluded');
|
| 928 |
const gallery = document.getElementById('source-browse-gallery');
|
| 929 |
btnBrowseSource.disabled = true;
|
| 930 |
btnBrowseSource.textContent = 'Scanning…';
|
| 931 |
if (statusEl) statusEl.textContent = `Validating ${sourcePath}…`;
|
| 932 |
+
if (excludedEl) {
|
| 933 |
+
excludedEl.style.display = 'none';
|
| 934 |
+
excludedEl.innerHTML = '';
|
| 935 |
+
}
|
| 936 |
try {
|
| 937 |
+
const res = await fetch(
|
| 938 |
+
`${API_BASE}/api/dataset/browse?source_path=${encodeURIComponent(sourcePath)}`
|
| 939 |
+
+ `&limit=48&dataset_type=${encodeURIComponent(datasetType)}`
|
| 940 |
+
);
|
| 941 |
const data = await res.json();
|
| 942 |
if (gallery) {
|
| 943 |
gallery.innerHTML = '';
|
|
|
|
| 951 |
});
|
| 952 |
}
|
| 953 |
if (statusEl) {
|
| 954 |
+
let msg = data.browsable
|
| 955 |
+
? `Browsable: ${data.count} driving-relevant image(s) under ${data.source_path}`
|
| 956 |
: (data.empty_reason || 'Nothing browsable at this path.');
|
| 957 |
+
if (Array.isArray(data.warnings) && data.warnings.length) {
|
| 958 |
+
msg += ` — ${data.warnings[0]}`;
|
| 959 |
+
}
|
| 960 |
+
statusEl.textContent = msg;
|
| 961 |
+
}
|
| 962 |
+
if (excludedEl && data.excluded_count > 0) {
|
| 963 |
+
excludedEl.style.display = 'block';
|
| 964 |
+
const summary = document.createElement('div');
|
| 965 |
+
summary.textContent = `${data.excluded_count} file(s) excluded (not driving media)`;
|
| 966 |
+
const toggle = document.createElement('button');
|
| 967 |
+
toggle.type = 'button';
|
| 968 |
+
toggle.className = 'btn btn-secondary';
|
| 969 |
+
toggle.style.marginTop = '8px';
|
| 970 |
+
toggle.style.fontSize = '12px';
|
| 971 |
+
toggle.textContent = 'Show excluded files';
|
| 972 |
+
const list = document.createElement('ul');
|
| 973 |
+
list.style.display = 'none';
|
| 974 |
+
list.style.marginTop = '8px';
|
| 975 |
+
list.style.fontSize = '12px';
|
| 976 |
+
list.style.color = 'var(--text-muted)';
|
| 977 |
+
(data.excluded || []).forEach((item) => {
|
| 978 |
+
const li = document.createElement('li');
|
| 979 |
+
li.textContent = `${item.name || item.path} (${item.reason || 'excluded'})`;
|
| 980 |
+
list.appendChild(li);
|
| 981 |
+
});
|
| 982 |
+
toggle.addEventListener('click', () => {
|
| 983 |
+
const open = list.style.display === 'none';
|
| 984 |
+
list.style.display = open ? 'block' : 'none';
|
| 985 |
+
toggle.textContent = open ? 'Hide excluded files' : 'Show excluded files';
|
| 986 |
+
});
|
| 987 |
+
excludedEl.appendChild(summary);
|
| 988 |
+
excludedEl.appendChild(toggle);
|
| 989 |
+
excludedEl.appendChild(list);
|
| 990 |
}
|
| 991 |
const badge = document.getElementById('ds-meta-badge');
|
| 992 |
if (badge) {
|
| 993 |
if (data.browsable) {
|
| 994 |
+
badge.textContent = `${data.count} driving images browsable on disk`;
|
| 995 |
badge.style.background = 'rgba(0, 255, 170, 0.15)';
|
| 996 |
badge.style.color = '#00ffaa';
|
| 997 |
badge.style.border = '1px solid rgba(0, 255, 170, 0.3)';
|
| 998 |
document.getElementById('ds-kpi-loaded-rows').textContent = data.count;
|
| 999 |
} else {
|
| 1000 |
+
badge.textContent = 'Not browsable — no driving media at path';
|
| 1001 |
badge.style.background = 'rgba(255, 68, 68, 0.12)';
|
| 1002 |
badge.style.color = '#ff8888';
|
| 1003 |
badge.style.border = '1px solid rgba(255, 68, 68, 0.35)';
|
static/index.html
CHANGED
|
@@ -153,6 +153,7 @@
|
|
| 153 |
</div>
|
| 154 |
<div id="config-exec-status" class="config-exec-status" style="margin-top:10px;font-size:12px;color:var(--text-muted);"></div>
|
| 155 |
<div id="source-browse-status" class="status-box" style="margin-top:12px;"></div>
|
|
|
|
| 156 |
<div id="source-browse-gallery" class="output-browse-gallery" style="margin-top:12px;"></div>
|
| 157 |
<div class="evidence-card" id="evidence-dataset" data-stage="dataset">
|
| 158 |
<h4>What actually happened?</h4>
|
|
|
|
| 153 |
</div>
|
| 154 |
<div id="config-exec-status" class="config-exec-status" style="margin-top:10px;font-size:12px;color:var(--text-muted);"></div>
|
| 155 |
<div id="source-browse-status" class="status-box" style="margin-top:12px;"></div>
|
| 156 |
+
<div id="source-browse-excluded" class="status-box" style="margin-top:8px;display:none;"></div>
|
| 157 |
<div id="source-browse-gallery" class="output-browse-gallery" style="margin-top:12px;"></div>
|
| 158 |
<div class="evidence-card" id="evidence-dataset" data-stage="dataset">
|
| 159 |
<h4>What actually happened?</h4>
|
tests/test_pipeline/test_av_media_filter.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for AV driving-media browse filtering."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import importlib
|
| 6 |
+
import shutil
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
from fastapi.testclient import TestClient
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _png_bytes() -> bytes:
|
| 14 |
+
return bytes.fromhex(
|
| 15 |
+
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
|
| 16 |
+
"0000000a49444154789c63000100000500010d0a2db40000000049454e44ae426082"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _large_png_bytes(min_bytes: int = 11 * 1024) -> bytes:
|
| 21 |
+
"""Valid PNG header with zero padding so size heuristics accept it in tests."""
|
| 22 |
+
base = _png_bytes()
|
| 23 |
+
if len(base) >= min_bytes:
|
| 24 |
+
return base
|
| 25 |
+
return base + b"\x00" * (min_bytes - len(base))
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _write_driving_frame(root: Path, rel: str = "images/train/frame.png") -> Path:
|
| 29 |
+
path = root / rel
|
| 30 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
path.write_bytes(_large_png_bytes())
|
| 32 |
+
return path
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@pytest.fixture
|
| 36 |
+
def client(tmp_path, monkeypatch):
|
| 37 |
+
monkeypatch.chdir(tmp_path)
|
| 38 |
+
import app_backend
|
| 39 |
+
|
| 40 |
+
importlib.reload(app_backend)
|
| 41 |
+
return TestClient(app_backend.app)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_browse_excludes_junk_test_file_for_alpamayo(client, tmp_path):
|
| 45 |
+
data_dir = tmp_path / "data"
|
| 46 |
+
data_dir.mkdir()
|
| 47 |
+
(data_dir / "test.jpg").write_bytes(_large_png_bytes()) # wrong ext still image-like size
|
| 48 |
+
_write_driving_frame(data_dir, "camera/front/scene_001.jpg").write_bytes(_large_png_bytes())
|
| 49 |
+
|
| 50 |
+
res = client.get(
|
| 51 |
+
"/api/dataset/browse?source_path=data&dataset_type=alpamayo"
|
| 52 |
+
)
|
| 53 |
+
assert res.status_code == 200
|
| 54 |
+
body = res.json()
|
| 55 |
+
assert body["count"] == 1
|
| 56 |
+
assert body["browsable"] is True
|
| 57 |
+
assert body["excluded_count"] >= 1
|
| 58 |
+
names = {img["name"] for img in body["images"]}
|
| 59 |
+
assert "scene_001.jpg" in names
|
| 60 |
+
assert "test.jpg" not in names
|
| 61 |
+
reasons = {e["reason"] for e in body["excluded"]}
|
| 62 |
+
assert "junk_filename" in reasons or "non_av_layout" in reasons
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_browse_excludes_docs_and_logo_paths(client, tmp_path):
|
| 66 |
+
root = tmp_path / "data"
|
| 67 |
+
_write_driving_frame(root, "frames/road.png")
|
| 68 |
+
logo = root / "docs" / "logo.png"
|
| 69 |
+
logo.parent.mkdir(parents=True)
|
| 70 |
+
logo.write_bytes(_large_png_bytes())
|
| 71 |
+
icon = root / "assets" / "icon.png"
|
| 72 |
+
icon.parent.mkdir(parents=True)
|
| 73 |
+
icon.write_bytes(_large_png_bytes())
|
| 74 |
+
|
| 75 |
+
res = client.get("/api/dataset/browse?source_path=data&dataset_type=local")
|
| 76 |
+
body = res.json()
|
| 77 |
+
assert body["count"] == 1
|
| 78 |
+
assert body["excluded_count"] == 2
|
| 79 |
+
assert all(e["reason"] == "excluded_path" for e in body["excluded"])
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_browse_excludes_tiny_icons(client, tmp_path):
|
| 83 |
+
root = tmp_path / "data"
|
| 84 |
+
_write_driving_frame(root, "images/train/big.png")
|
| 85 |
+
tiny = root / "images" / "train" / "favicon.png"
|
| 86 |
+
tiny.write_bytes(_png_bytes()) # < 10KB and junk name
|
| 87 |
+
|
| 88 |
+
res = client.get("/api/dataset/browse?source_path=data&dataset_type=local")
|
| 89 |
+
body = res.json()
|
| 90 |
+
assert body["count"] == 1
|
| 91 |
+
assert body["excluded_count"] == 1
|
| 92 |
+
assert body["excluded"][0]["reason"] in {"too_small", "junk_filename"}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def test_browse_lists_valid_driving_media(client, tmp_path):
|
| 96 |
+
root = tmp_path / "data"
|
| 97 |
+
_write_driving_frame(root, "camera/front/a.png")
|
| 98 |
+
|
| 99 |
+
res = client.get("/api/dataset/browse?source_path=data&dataset_type=local")
|
| 100 |
+
assert res.status_code == 200
|
| 101 |
+
body = res.json()
|
| 102 |
+
assert body["browsable"] is True
|
| 103 |
+
assert body["count"] == 1
|
| 104 |
+
assert body["images"][0]["preview_url"].startswith("/api/pipeline/file?")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def test_load_reports_images_excluded(client, tmp_path):
|
| 108 |
+
root = tmp_path / "data"
|
| 109 |
+
_write_driving_frame(root, "images/train/good.png")
|
| 110 |
+
(root / "sample.jpg").write_bytes(_large_png_bytes())
|
| 111 |
+
|
| 112 |
+
res = client.post(
|
| 113 |
+
"/api/dataset/load",
|
| 114 |
+
json={"source_path": str(root), "dataset_type": "local"},
|
| 115 |
+
)
|
| 116 |
+
data = res.json()
|
| 117 |
+
assert data["metrics"]["images_discovered"] == 1
|
| 118 |
+
assert data["metrics"]["images_excluded"] == 1
|
| 119 |
+
assert data["discovery"]["excluded_by_reason"]
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def test_repo_data_test_jpg_excluded_when_alpamayo_selected(client, tmp_path, monkeypatch):
|
| 123 |
+
"""Regression: loose data/test.jpg must not appear for official AV dataset types."""
|
| 124 |
+
repo_data = Path(__file__).resolve().parents[2] / "data"
|
| 125 |
+
if not (repo_data / "test.jpg").exists():
|
| 126 |
+
pytest.skip("repo data/test.jpg not present")
|
| 127 |
+
|
| 128 |
+
monkeypatch.chdir(tmp_path)
|
| 129 |
+
shutil.copytree(repo_data, tmp_path / "data")
|
| 130 |
+
|
| 131 |
+
import app_backend
|
| 132 |
+
|
| 133 |
+
importlib.reload(app_backend)
|
| 134 |
+
c = TestClient(app_backend.app)
|
| 135 |
+
res = c.get("/api/dataset/browse?source_path=data&dataset_type=alpamayo")
|
| 136 |
+
body = res.json()
|
| 137 |
+
assert body["count"] == 0
|
| 138 |
+
assert body["browsable"] is False
|
| 139 |
+
assert body["excluded_count"] >= 1
|
| 140 |
+
assert not body["images"]
|
tests/test_pipeline/test_pipeline_browse.py
CHANGED
|
@@ -6,6 +6,14 @@ import pytest
|
|
| 6 |
from fastapi.testclient import TestClient
|
| 7 |
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
@pytest.fixture
|
| 10 |
def client(tmp_path, monkeypatch):
|
| 11 |
monkeypatch.chdir(tmp_path)
|
|
@@ -118,14 +126,10 @@ def test_load_all_not_executed_without_stub(client, tmp_path):
|
|
| 118 |
|
| 119 |
|
| 120 |
def test_load_all_local_real_when_path_has_images(client, tmp_path):
|
| 121 |
-
data_dir = tmp_path / "data"
|
| 122 |
-
data_dir.mkdir()
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
|
| 126 |
-
"0000000a49444154789c63000100000500010d0a2db40000000049454e44ae426082"
|
| 127 |
-
)
|
| 128 |
-
)
|
| 129 |
res = client.post(
|
| 130 |
"/api/dataset/load-all",
|
| 131 |
json={
|
|
@@ -174,14 +178,9 @@ def test_pipeline_file_blocks_path_traversal(client, tmp_path):
|
|
| 174 |
|
| 175 |
|
| 176 |
def test_dataset_browse_lists_images(client, tmp_path):
|
| 177 |
-
data_dir = tmp_path / "data"
|
| 178 |
-
data_dir.mkdir()
|
| 179 |
-
(data_dir / "a.png").write_bytes(
|
| 180 |
-
bytes.fromhex(
|
| 181 |
-
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
|
| 182 |
-
"0000000a49444154789c63000100000500010d0a2db40000000049454e44ae426082"
|
| 183 |
-
)
|
| 184 |
-
)
|
| 185 |
res = client.get("/api/dataset/browse?source_path=data")
|
| 186 |
assert res.status_code == 200
|
| 187 |
body = res.json()
|
|
|
|
| 6 |
from fastapi.testclient import TestClient
|
| 7 |
|
| 8 |
|
| 9 |
+
def _large_png_bytes(min_bytes: int = 11 * 1024) -> bytes:
|
| 10 |
+
base = bytes.fromhex(
|
| 11 |
+
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
|
| 12 |
+
"0000000a49444154789c63000100000500010d0a2db40000000049454e44ae426082"
|
| 13 |
+
)
|
| 14 |
+
return base if len(base) >= min_bytes else base + b"\x00" * (min_bytes - len(base))
|
| 15 |
+
|
| 16 |
+
|
| 17 |
@pytest.fixture
|
| 18 |
def client(tmp_path, monkeypatch):
|
| 19 |
monkeypatch.chdir(tmp_path)
|
|
|
|
| 126 |
|
| 127 |
|
| 128 |
def test_load_all_local_real_when_path_has_images(client, tmp_path):
|
| 129 |
+
data_dir = tmp_path / "data" / "images" / "train"
|
| 130 |
+
data_dir.mkdir(parents=True)
|
| 131 |
+
png = _large_png_bytes()
|
| 132 |
+
(data_dir / "a.png").write_bytes(png)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
res = client.post(
|
| 134 |
"/api/dataset/load-all",
|
| 135 |
json={
|
|
|
|
| 178 |
|
| 179 |
|
| 180 |
def test_dataset_browse_lists_images(client, tmp_path):
|
| 181 |
+
data_dir = tmp_path / "data" / "camera" / "front"
|
| 182 |
+
data_dir.mkdir(parents=True)
|
| 183 |
+
(data_dir / "a.png").write_bytes(_large_png_bytes())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
res = client.get("/api/dataset/browse?source_path=data")
|
| 185 |
assert res.status_code == 200
|
| 186 |
body = res.json()
|