Добавить имитаторы МЛЭ и ГБО с 3D-сценой, съёмкой рельефа и пресетами.
Вкладки позволяют готовить рельеф, двигать АНПА, накапливать поверхность по лучам и сохранять скриншоты окон; ГБО использует бортовые секторы 12–75° и чёрные зоны вне обзора. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,8 +9,10 @@ Target class 1 = user-provided object; class 0 = seafloor.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -18,6 +20,7 @@ from scene_generator import (
|
||||
apply_transform,
|
||||
export_npy_float64,
|
||||
export_obj,
|
||||
parse_obj_labeled_points,
|
||||
parse_obj_points,
|
||||
)
|
||||
|
||||
@@ -474,6 +477,10 @@ def resolve_output_dir(output_dir: str | Path = "sonar_dataset") -> Path:
|
||||
return out
|
||||
|
||||
|
||||
DATASET_RUN_FILENAME = "dataset_run.json"
|
||||
DATASET_RUN_VERSION = 1
|
||||
|
||||
|
||||
def _safe_object_stem(object_name: str | None) -> str:
|
||||
stem = Path(object_name or "object").stem
|
||||
safe = "".join(ch if ch.isalnum() or ch in "_-" else "_" for ch in stem).strip("._-")
|
||||
@@ -503,6 +510,221 @@ def make_generation_run_dir(base_dir: Path, object_name: str | None = None) -> P
|
||||
return path
|
||||
|
||||
|
||||
def _normalize_model_filename(name: str | None) -> str | None:
|
||||
"""Keep the full uploaded basename, e.g. ``airplane2.obj``."""
|
||||
if not name:
|
||||
return None
|
||||
base = Path(str(name).strip()).name
|
||||
return base or None
|
||||
|
||||
|
||||
def build_run_manifest(
|
||||
*,
|
||||
run_dir: Path,
|
||||
base_dir: Path,
|
||||
count: int,
|
||||
seed: int,
|
||||
output_dir: str,
|
||||
object_name: str | None,
|
||||
object_scale: float,
|
||||
object_scale_is_max: bool,
|
||||
beam_count: int,
|
||||
length_count: int,
|
||||
object_vertex_count: int,
|
||||
stats: dict[str, Any],
|
||||
written: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
model_filename = _normalize_model_filename(object_name)
|
||||
return {
|
||||
"version": DATASET_RUN_VERSION,
|
||||
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||
"runName": run_dir.name,
|
||||
"outputDir": str(run_dir),
|
||||
"baseDir": str(base_dir),
|
||||
"objectName": model_filename,
|
||||
"settings": {
|
||||
"count": int(count),
|
||||
"seed": int(seed),
|
||||
"outputDir": str(output_dir),
|
||||
"objectScale": float(object_scale),
|
||||
"objectScaleIsMax": bool(object_scale_is_max),
|
||||
"beamCount": int(beam_count),
|
||||
"lengthCount": int(length_count),
|
||||
"objectName": model_filename,
|
||||
},
|
||||
"objectVertexCount": int(object_vertex_count),
|
||||
"stats": stats,
|
||||
"written": written,
|
||||
}
|
||||
|
||||
|
||||
def write_run_manifest(run_dir: Path, manifest: dict[str, Any]) -> Path:
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = run_dir / DATASET_RUN_FILENAME
|
||||
path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def read_run_manifest(run_dir: Path) -> dict[str, Any] | None:
|
||||
path = run_dir / DATASET_RUN_FILENAME
|
||||
if not path.is_file():
|
||||
return None
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
return data if isinstance(data, dict) else None
|
||||
|
||||
|
||||
def _scene_entry_from_files(run_dir: Path, stem: str) -> dict[str, Any]:
|
||||
npy_path = run_dir / f"{stem}.npy"
|
||||
obj_path = run_dir / f"{stem}.obj"
|
||||
entry: dict[str, Any] = {
|
||||
"stem": stem,
|
||||
"visibility": "unknown",
|
||||
"hasObject": None,
|
||||
"pointCount": None,
|
||||
"objectPointCount": None,
|
||||
}
|
||||
if npy_path.is_file():
|
||||
try:
|
||||
rows = load_npy_float64_rows(npy_path)
|
||||
object_point_count = sum(1 for row in rows if int(row[6]) == 1)
|
||||
entry["pointCount"] = len(rows)
|
||||
entry["objectPointCount"] = object_point_count
|
||||
entry["hasObject"] = object_point_count > 0
|
||||
except (OSError, ValueError, IndexError):
|
||||
pass
|
||||
elif obj_path.is_file():
|
||||
try:
|
||||
text = obj_path.read_text(encoding="utf-8", errors="ignore")
|
||||
labeled = parse_obj_labeled_points(text)
|
||||
if labeled:
|
||||
object_point_count = sum(1 for row in labeled if int(row[3]) == 1)
|
||||
entry["pointCount"] = len(labeled)
|
||||
entry["objectPointCount"] = object_point_count
|
||||
entry["hasObject"] = object_point_count > 0
|
||||
else:
|
||||
points = parse_obj_points(text)
|
||||
entry["pointCount"] = len(points)
|
||||
except OSError:
|
||||
pass
|
||||
return entry
|
||||
|
||||
|
||||
def scan_run_scenes(run_dir: Path) -> list[dict[str, Any]]:
|
||||
stems: set[str] = set()
|
||||
for pattern in ("*.obj", "*.npy"):
|
||||
for path in run_dir.glob(pattern):
|
||||
if path.is_file():
|
||||
stems.add(path.stem)
|
||||
return [_scene_entry_from_files(run_dir, stem) for stem in sorted(stems)]
|
||||
|
||||
|
||||
def _run_has_scene_files(path: Path) -> bool:
|
||||
return any(path.glob("*.obj")) or any(path.glob("*.npy"))
|
||||
|
||||
|
||||
def _relative_to_project(path: Path) -> str:
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
try:
|
||||
return str(path.relative_to(project_root))
|
||||
except ValueError:
|
||||
return str(path)
|
||||
|
||||
|
||||
def list_dataset_runs(output_dir: str | Path = "sonar_dataset") -> list[dict[str, Any]]:
|
||||
"""List dataset run folders under the base output directory."""
|
||||
base = resolve_output_dir(output_dir)
|
||||
runs: list[dict[str, Any]] = []
|
||||
|
||||
if base.is_dir() and _run_has_scene_files(base):
|
||||
manifest = read_run_manifest(base)
|
||||
written = manifest.get("written") if manifest else scan_run_scenes(base)
|
||||
runs.append(
|
||||
{
|
||||
"runName": "(корень)",
|
||||
"outputDir": str(base),
|
||||
"loadPath": _relative_to_project(base),
|
||||
"sceneCount": len(written),
|
||||
"hasSettings": manifest is not None,
|
||||
"generatedAt": manifest.get("generatedAt") if manifest else None,
|
||||
"objectName": (manifest or {}).get("objectName")
|
||||
or (manifest or {}).get("settings", {}).get("objectName"),
|
||||
}
|
||||
)
|
||||
|
||||
if not base.is_dir():
|
||||
return runs
|
||||
|
||||
for child in sorted(base.iterdir(), key=lambda p: p.name, reverse=True):
|
||||
if not child.is_dir() or not _run_has_scene_files(child):
|
||||
continue
|
||||
manifest = read_run_manifest(child)
|
||||
written = manifest.get("written") if manifest else scan_run_scenes(child)
|
||||
runs.append(
|
||||
{
|
||||
"runName": child.name,
|
||||
"outputDir": str(child),
|
||||
"loadPath": _relative_to_project(child),
|
||||
"sceneCount": len(written),
|
||||
"hasSettings": manifest is not None,
|
||||
"generatedAt": manifest.get("generatedAt") if manifest else None,
|
||||
"objectName": (manifest or {}).get("objectName")
|
||||
or (manifest or {}).get("settings", {}).get("objectName"),
|
||||
}
|
||||
)
|
||||
return runs
|
||||
|
||||
|
||||
def load_dataset_run(output_dir: str | Path) -> dict[str, Any]:
|
||||
"""Load a dataset run folder: settings, stats and scene list."""
|
||||
run_dir = resolve_output_dir(output_dir)
|
||||
if not run_dir.is_dir():
|
||||
raise FileNotFoundError(f"Dataset folder not found: {run_dir}")
|
||||
if not _run_has_scene_files(run_dir):
|
||||
raise FileNotFoundError(f"No scene files in dataset folder: {run_dir}")
|
||||
|
||||
manifest = read_run_manifest(run_dir)
|
||||
written = manifest.get("written") if manifest else scan_run_scenes(run_dir)
|
||||
if not written:
|
||||
raise FileNotFoundError(f"No scenes found in dataset folder: {run_dir}")
|
||||
|
||||
settings = dict((manifest or {}).get("settings") or {})
|
||||
model_filename = (
|
||||
settings.get("objectName")
|
||||
or (manifest.get("objectName") if manifest else None)
|
||||
)
|
||||
stats = dict((manifest or {}).get("stats") or {})
|
||||
if not stats:
|
||||
stats = {
|
||||
"total": len(written),
|
||||
"withObject": sum(1 for item in written if item.get("hasObject")),
|
||||
"withoutObject": sum(1 for item in written if item.get("hasObject") is False),
|
||||
}
|
||||
|
||||
base_dir = Path(manifest["baseDir"]) if manifest and manifest.get("baseDir") else run_dir.parent
|
||||
return {
|
||||
"outputDir": str(run_dir),
|
||||
"baseDir": str(base_dir),
|
||||
"runName": manifest.get("runName") if manifest else run_dir.name,
|
||||
"generatedAt": manifest.get("generatedAt") if manifest else None,
|
||||
"hasSettings": manifest is not None,
|
||||
"settings": settings,
|
||||
"objectVertexCount": (manifest or {}).get("objectVertexCount"),
|
||||
"stats": stats,
|
||||
"written": written,
|
||||
"count": settings.get("count") or len(written),
|
||||
"seed": settings.get("seed"),
|
||||
"beamCount": settings.get("beamCount"),
|
||||
"lengthCount": settings.get("lengthCount"),
|
||||
"objectScale": settings.get("objectScale"),
|
||||
"objectScaleIsMax": settings.get("objectScaleIsMax"),
|
||||
"objectName": model_filename,
|
||||
"classLabels": {"0": "background", "1": "object"},
|
||||
}
|
||||
|
||||
|
||||
def _downsample_points(points: list[list[float]], max_points: int) -> list[list[float]]:
|
||||
max_points = max(100, int(max_points))
|
||||
if len(points) <= max_points:
|
||||
@@ -584,8 +806,10 @@ def load_scene_preview(
|
||||
labeled = [[float(r[0]), float(r[1]), float(r[2]), float(r[6])] for r in rows]
|
||||
elif obj_path.is_file():
|
||||
text = obj_path.read_text(encoding="utf-8", errors="ignore")
|
||||
points = parse_obj_points(text)
|
||||
labeled = [[p[0], p[1], p[2], 0.0] for p in points]
|
||||
labeled = parse_obj_labeled_points(text)
|
||||
if not labeled:
|
||||
points = parse_obj_points(text)
|
||||
labeled = [[p[0], p[1], p[2], 0.0] for p in points]
|
||||
else:
|
||||
raise FileNotFoundError(f"Scene not found: {safe}.npy / {safe}.obj")
|
||||
|
||||
@@ -613,7 +837,11 @@ def write_scene_files(
|
||||
npy_path = output_dir / f"{stem}.npy"
|
||||
obj_path = output_dir / f"{stem}.obj"
|
||||
npy_path.write_bytes(export_npy_float64(scene["rows"]))
|
||||
obj_path.write_text(export_obj(scene["points"], object_name=stem), encoding="utf-8")
|
||||
classes = [int(r[6]) for r in scene["rows"]]
|
||||
obj_path.write_text(
|
||||
export_obj(scene["points"], object_name=stem, classes=classes),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return {"npy": str(npy_path), "obj": str(obj_path), "stem": stem}
|
||||
|
||||
|
||||
@@ -662,6 +890,8 @@ def iter_generate_dataset(
|
||||
if length_count > 1024:
|
||||
raise ValueError("length_count (Длина) must be <= 1024")
|
||||
|
||||
object_name = _normalize_model_filename(object_name)
|
||||
|
||||
base = resolve_output_dir(output_dir)
|
||||
run_dir = make_generation_run_dir(base, object_name)
|
||||
template = normalize_object_points(object_points)
|
||||
@@ -773,6 +1003,23 @@ def iter_generate_dataset(
|
||||
"written": written,
|
||||
"preview": preview,
|
||||
}
|
||||
manifest = build_run_manifest(
|
||||
run_dir=run_dir,
|
||||
base_dir=base,
|
||||
count=count,
|
||||
seed=int(seed),
|
||||
output_dir=str(output_dir),
|
||||
object_name=object_name,
|
||||
object_scale=object_scale,
|
||||
object_scale_is_max=object_scale_is_max,
|
||||
beam_count=beam_count,
|
||||
length_count=length_count,
|
||||
object_vertex_count=len(template),
|
||||
stats=stats,
|
||||
written=written,
|
||||
)
|
||||
write_run_manifest(run_dir, manifest)
|
||||
result["settingsPath"] = str(run_dir / DATASET_RUN_FILENAME)
|
||||
yield {"type": "done", "result": result}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user