diff --git a/backend/dataset_generator.py b/backend/dataset_generator.py index be91dc3..6251b6f 100644 --- a/backend/dataset_generator.py +++ b/backend/dataset_generator.py @@ -177,44 +177,83 @@ def _build_seafloor_meta( *, beam_count: int = 45, length_count: int | None = None, + mle_mode: bool = False, + auv_depth: float | None = None, + swath_angle_deg: float = 90.0, + relief_scale_pct: float = 100.0, ) -> dict[str, Any]: - """Build continuous seafloor heightfield parameters (no point cloud yet).""" + """Build continuous seafloor heightfield parameters (no point cloud yet). + + ``relief_scale_pct`` scales random unevenness relative to scene size + (100 = baseline, clearly visible relief; higher = larger features). + """ beams = max(1, int(beam_count)) length_points = beams if length_count is None else max(1, int(length_count)) - size_x = rng.uniform(8.0, 16.0) - size_y = rng.uniform(8.0, 16.0) - base_z = rng.uniform(-1.2, -0.2) - amplitude = rng.uniform(0.05, 0.35) - frequency = rng.uniform(0.4, 2.2) + swath_deg = max(5.0, min(170.0, float(swath_angle_deg))) + relief_pct = max(5.0, min(500.0, float(relief_scale_pct))) - hills = [ - ( - rng.uniform(-size_x * 0.4, size_x * 0.4), - rng.uniform(-size_y * 0.4, size_y * 0.4), - rng.uniform(0.15, 0.7), - rng.uniform(0.6, 2.2), + if mle_mode and auv_depth is not None: + # Swath footprint from altitude above flat datum (like МЛЭ). + depth = max(0.3, float(auv_depth)) + half_w = depth * math.tan(math.radians(swath_deg) * 0.5) + size_x = max(4.0, 2.0 * half_w * 1.08) + dx = size_x / max(beams - 1, 1) + size_y = max(4.0, dx * max(length_points - 1, 1) * rng.uniform(0.95, 1.12)) + else: + size_x = rng.uniform(8.0, 16.0) + size_y = rng.uniform(8.0, 16.0) + + base_z = rng.uniform(-1.2, -0.2) + + # Feature size tied to scene so relief stays visible in the point cloud. + # At 100%: characteristic scale ≈ 28% of the shorter scene side. + scene_ref = max(4.0, min(size_x, size_y)) + feature_scale = scene_ref * (relief_pct / 100.0) * 0.28 + feature_scale = max(0.35, feature_scale) + max_rad = scene_ref * 0.42 + + # Background undulation — several waves across the patch. + amplitude = feature_scale * rng.uniform(0.12, 0.28) + frequency = (2.0 * math.pi / scene_ref) * rng.uniform(1.8, 3.6) + + n_hills = rng.randint(2, 5) + hills = [] + for _ in range(n_hills): + hills.append( + ( + rng.uniform(-size_x * 0.38, size_x * 0.38), + rng.uniform(-size_y * 0.38, size_y * 0.38), + feature_scale * rng.uniform(0.45, 1.05), + min(max_rad, feature_scale * rng.uniform(0.75, 1.55)), + ) ) - for _ in range(rng.randint(1, 4)) - ] - valleys = [ - ( - rng.uniform(-size_x * 0.4, size_x * 0.4), - rng.uniform(-size_y * 0.4, size_y * 0.4), - rng.uniform(0.1, 0.55), - rng.uniform(0.5, 2.0), + + n_valleys = rng.randint(1, 4) + valleys = [] + for _ in range(n_valleys): + valleys.append( + ( + rng.uniform(-size_x * 0.38, size_x * 0.38), + rng.uniform(-size_y * 0.38, size_y * 0.38), + feature_scale * rng.uniform(0.3, 0.75), + min(max_rad, feature_scale * rng.uniform(0.65, 1.4)), + ) ) - for _ in range(rng.randint(1, 3)) - ] - # Relief clutter / false features as heightfield bumps (not extra points) - bumps = [ - ( - rng.uniform(-size_x * 0.45, size_x * 0.45), - rng.uniform(-size_y * 0.45, size_y * 0.45), - rng.uniform(0.03, 0.35), - rng.uniform(0.15, 0.9), + + # More medium bumps at lower scale; fewer but larger when scale is high. + density = max(0.45, min(1.8, 120.0 / max(relief_pct, 20.0))) + n_bumps = int(round(rng.uniform(10, 20) * density)) + n_bumps = max(8, min(40, n_bumps)) + bumps = [] + for _ in range(n_bumps): + bumps.append( + ( + rng.uniform(-size_x * 0.46, size_x * 0.46), + rng.uniform(-size_y * 0.46, size_y * 0.46), + feature_scale * rng.uniform(0.12, 0.45), + min(max_rad * 0.7, feature_scale * rng.uniform(0.25, 0.85)), + ) ) - for _ in range(rng.randint(4, 14)) - ] return { "sizeX": size_x, @@ -232,6 +271,11 @@ def _build_seafloor_meta( "pingCount": length_points, "swathBeams": beams, "noise": rng.uniform(0.002, 0.02), + "mleMode": bool(mle_mode), + "auvDepth": float(auv_depth) if auv_depth is not None else None, + "swathAngleDeg": swath_deg, + "reliefScalePct": relief_pct, + "featureScale": feature_scale, } @@ -574,6 +618,174 @@ def _cast_echosounder_returns( return rows, object_count, background_count +def _build_object_height_field( + world_pts: list[list[float]], + meta: dict[str, Any], +) -> dict[str, Any] | None: + """Dense XY max-Z map of object surface for angled multibeam probes.""" + if not world_pts: + return None + beams = int(meta["beamCount"]) + length_points = int(meta["lengthCount"]) + nx = max(32, min(256, beams * 3)) + ny = max(32, min(256, length_points * 3)) + size_x = float(meta["sizeX"]) + size_y = float(meta["sizeY"]) + half_x = size_x * 0.5 + half_y = size_y * 0.5 + cell_w = size_x / max(nx - 1, 1) + cell_h = size_y / max(ny - 1, 1) + splat_rx = cell_w * 1.1 + splat_ry = cell_h * 1.1 + field: list[list[float | None]] = [[None for _ in range(nx)] for _ in range(ny)] + + for px, py, pz in world_pts: + fx = 0.0 if nx == 1 else (px + half_x) / size_x * (nx - 1) + fy = 0.0 if ny == 1 else (py + half_y) / size_y * (ny - 1) + xi0 = int(math.floor(fx)) + yi0 = int(math.floor(fy)) + for dyi in (-1, 0, 1, 2): + for dxi in (-1, 0, 1, 2): + xi = xi0 + dxi + yi = yi0 + dyi + if xi < 0 or yi < 0 or xi >= nx or yi >= ny: + continue + cx = -half_x if nx == 1 else (-half_x + size_x * xi / (nx - 1)) + cy = -half_y if ny == 1 else (-half_y + size_y * yi / (ny - 1)) + if abs(px - cx) > splat_rx or abs(py - cy) > splat_ry: + continue + floor_z = _height_at(cx, cy, meta) + if pz < floor_z - 0.01: + continue + prev = field[yi][xi] + if prev is None or pz > prev: + field[yi][xi] = pz + + return { + "nx": nx, + "ny": ny, + "sizeX": size_x, + "sizeY": size_y, + "field": field, + } + + +def _sample_object_height_field( + field_info: dict[str, Any] | None, + x: float, + y: float, +) -> float | None: + if not field_info: + return None + nx = int(field_info["nx"]) + ny = int(field_info["ny"]) + size_x = float(field_info["sizeX"]) + size_y = float(field_info["sizeY"]) + half_x = size_x * 0.5 + half_y = size_y * 0.5 + u = (x + half_x) / max(size_x, 1e-6) + v = (y + half_y) / max(size_y, 1e-6) + if u < -0.02 or u > 1.02 or v < -0.02 or v > 1.02: + return None + u = min(1.0, max(0.0, u)) + v = min(1.0, max(0.0, v)) + fx = u * (nx - 1) + fy = v * (ny - 1) + i0 = int(math.floor(fx)) + j0 = int(math.floor(fy)) + i1 = min(nx - 1, i0 + 1) + j1 = min(ny - 1, j0 + 1) + vals = [ + field_info["field"][j0][i0], + field_info["field"][j0][i1], + field_info["field"][j1][i0], + field_info["field"][j1][i1], + ] + present = [z for z in vals if z is not None] + if not present: + return None + return max(present) + + +def _cast_mle_fan_returns( + rng: random.Random, + meta: dict[str, Any], + object_field: dict[str, Any] | None, + *, + auv_depth: float, + swath_angle_deg: float = 90.0, +) -> tuple[list[list[float]], int, int]: + """Multibeam fan: beamCount rays × lengthCount pings from constant altitude. + + AUV height is measured from flat seafloor datum (baseZ), not terrain-following. + """ + beams = int(meta["beamCount"]) + length_points = int(meta["lengthCount"]) + noise = float(meta.get("noise", 0.01)) + base_z = float(meta["baseZ"]) + depth = max(0.3, float(auv_depth)) + origin_z = base_z + depth + swath = math.radians(max(5.0, min(170.0, float(swath_angle_deg)))) + size_y = float(meta["sizeY"]) + half_y = size_y * 0.5 + # Reach outer beams with margin past the swath edge. + max_range = max(depth / max(math.cos(swath * 0.5), 0.12) * 1.35, depth * 2.0, 4.0) + step = max(0.08, min(0.45, max_range / 220.0)) + max_steps = int(math.ceil(max_range / step)) + 2 + + rows: list[list[float]] = [] + object_count = 0 + background_count = 0 + + for yi in range(length_points): + y = -half_y if length_points == 1 else (-half_y + size_y * yi / (length_points - 1)) + ox, oy, oz = 0.0, y, origin_z + for xi in range(beams): + t = 0.5 if beams == 1 else xi / (beams - 1) + angle = -swath * 0.5 + swath * t + dx = math.sin(angle) + dy = 0.0 + dz = -math.cos(angle) + + hit_x, hit_y, hit_z = ox, oy, base_z + is_object = False + px = ox + dx * step * 0.5 + py = oy + dy * step * 0.5 + pz = oz + dz * step * 0.5 + for _ in range(max_steps): + dist = math.hypot(px - ox, py - oy, pz - oz) + if dist > max_range: + break + floor_z = _height_at(px, py, meta) + obj_z = _sample_object_height_field(object_field, px, py) + if obj_z is not None and pz <= obj_z and obj_z >= floor_z - 0.01: + hit_x, hit_y, hit_z = px, py, obj_z + is_object = True + break + if pz <= floor_z: + hit_x, hit_y, hit_z = px, py, floor_z + break + px += dx * step + py += dy * step + pz += dz * step + else: + # No intersection within range — project to flat nadir estimate. + hit_x = ox + dx * max_range + hit_y = oy + dy * max_range + hit_z = _height_at(hit_x, hit_y, meta) + + z = hit_z + rng.uniform(-noise, noise) + if is_object: + cls = 1.0 + object_count += 1 + else: + cls = 0.0 + background_count += 1 + rows.append([float(hit_x), float(hit_y), float(z), 0.0, 0.0, 0.0, cls]) + + return rows, object_count, background_count + + # --------------------------------------------------------------------------- # Balance plan + single scene # --------------------------------------------------------------------------- @@ -657,12 +869,19 @@ def generate_sonar_scene( object_kind: str | None = None, beam_count: int = 45, length_count: int | None = None, + mle_mode: bool = False, + auv_depth: float | None = None, + swath_angle_deg: float = 90.0, + relief_scale_pct: float = 100.0, ) -> dict[str, Any]: """Build one sonar scene via beam×ping first-hit casting. visibility in absent|nearly_hidden|partial|visible. Scene size is always beam_count × length_count returns. object_kind ``pipe`` places a border-to-border pipeline with optional silt. + + When ``mle_mode`` is True, returns are cast as a multibeam fan from constant + altitude ``auv_depth`` above the flat seafloor datum (baseZ). """ rng = random.Random(int(seed)) if visibility not in ("absent",) + VISIBILITY_TIERS: @@ -671,11 +890,26 @@ def generate_sonar_scene( if visibility != "absent" and kind != "pipe" and not object_points: raise ValueError("object_points required when visibility is not absent.") - meta = _build_seafloor_meta(rng, beam_count=beam_count, length_count=length_count) + use_mle = bool(mle_mode) + depth = max(0.3, float(auv_depth)) if auv_depth is not None else None + if use_mle and depth is None: + depth = 2.5 + + meta = _build_seafloor_meta( + rng, + beam_count=beam_count, + length_count=length_count, + mle_mode=use_mle, + auv_depth=depth, + swath_angle_deg=swath_angle_deg, + relief_scale_pct=relief_scale_pct, + ) expected = int(meta["beamCount"]) * int(meta["lengthCount"]) object_info: dict[str, Any] | None = None object_hits: dict[tuple[int, int], float] | None = None + object_field: dict[str, Any] | None = None + world: list[list[float]] | None = None if visibility != "absent": if kind == "pipe": world, object_info = _place_pipeline_in_scene( @@ -692,14 +926,32 @@ def generate_sonar_scene( object_points, object_scale=object_scale, ) - object_hits = _rasterize_object_hits(world, meta) - object_info["hitCellCount"] = len(object_hits) - object_info["keptCount"] = len(object_hits) + if use_mle: + object_field = _build_object_height_field(world, meta) + hit_cells = 0 + if object_field: + for row in object_field["field"]: + hit_cells += sum(1 for z in row if z is not None) + object_info["hitCellCount"] = hit_cells + object_info["keptCount"] = hit_cells + else: + object_hits = _rasterize_object_hits(world, meta) + object_info["hitCellCount"] = len(object_hits) + object_info["keptCount"] = len(object_hits) object_info["requestedCount"] = expected - rows, object_point_count, background_point_count = _cast_echosounder_returns( - rng, meta, object_hits - ) + if use_mle: + rows, object_point_count, background_point_count = _cast_mle_fan_returns( + rng, + meta, + object_field, + auv_depth=float(depth), + swath_angle_deg=float(meta.get("swathAngleDeg", swath_angle_deg)), + ) + else: + rows, object_point_count, background_point_count = _cast_echosounder_returns( + rng, meta, object_hits + ) if len(rows) != expected: raise RuntimeError(f"Ray count mismatch: expected {expected}, got {len(rows)}") @@ -713,6 +965,8 @@ def generate_sonar_scene( "pointCount": len(rows), "objectPointCount": object_point_count, "backgroundPointCount": background_point_count, + "auvDepth": float(depth) if use_mle else None, + "mleMode": use_mle, "rows": rows, "points": xyz, "meta": { @@ -731,6 +985,10 @@ def generate_sonar_scene( "pingCount": meta.get("pingCount"), "swathBeams": meta.get("swathBeams"), "gridPointCount": expected, + "mleMode": use_mle, + "auvDepth": float(depth) if use_mle else None, + "swathAngleDeg": meta.get("swathAngleDeg"), + "reliefScalePct": meta.get("reliefScalePct"), "floorFeatures": { "hills": len(meta["hills"]), "valleys": len(meta["valleys"]), @@ -812,8 +1070,31 @@ def build_run_manifest( nearly_hidden_pct: float = 20.0, partial_pct: float = 40.0, visible_pct: float = 40.0, + mle_mode: bool = False, + auv_depth_min: float | None = None, + auv_depth_max: float | None = None, + relief_scale_pct: float = 100.0, ) -> dict[str, Any]: model_filename = _normalize_model_filename(object_name) + settings: dict[str, Any] = { + "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, + "absentPct": float(absent_pct), + "nearlyHiddenPct": float(nearly_hidden_pct), + "partialPct": float(partial_pct), + "visiblePct": float(visible_pct), + "mleMode": bool(mle_mode), + "reliefScalePct": float(relief_scale_pct), + } + if mle_mode: + settings["auvDepthMin"] = float(auv_depth_min) if auv_depth_min is not None else None + settings["auvDepthMax"] = float(auv_depth_max) if auv_depth_max is not None else None return { "version": DATASET_RUN_VERSION, "generatedAt": datetime.now(timezone.utc).isoformat(), @@ -821,20 +1102,7 @@ def build_run_manifest( "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, - "absentPct": float(absent_pct), - "nearlyHiddenPct": float(nearly_hidden_pct), - "partialPct": float(partial_pct), - "visiblePct": float(visible_pct), - }, + "settings": settings, "objectVertexCount": int(object_vertex_count), "stats": stats, "written": written, @@ -1144,6 +1412,10 @@ def iter_generate_dataset( nearly_hidden_pct: float = 20.0, partial_pct: float = 40.0, visible_pct: float = 40.0, + mle_mode: bool = False, + auv_depth_min: float = 2.0, + auv_depth_max: float = 8.0, + relief_scale_pct: float = 100.0, ): """Yield NDJSON-friendly progress events, then a final ``done`` payload. @@ -1183,6 +1455,18 @@ def iter_generate_dataset( if length_count > 1024: raise ValueError("length_count (Длина) must be <= 1024") + use_mle = bool(mle_mode) + depth_lo = max(0.3, float(auv_depth_min)) + depth_hi = max(0.3, float(auv_depth_max)) + if depth_hi < depth_lo: + depth_lo, depth_hi = depth_hi, depth_lo + if use_mle and (not math.isfinite(depth_lo) or not math.isfinite(depth_hi)): + raise ValueError("Диапазон высот must be finite numbers") + + relief_pct = max(5.0, min(500.0, float(relief_scale_pct))) + if not math.isfinite(relief_pct): + raise ValueError("relief_scale_pct (Размер неровностей) must be finite") + absent_pct = _clamp_pct(absent_pct, name="absent_pct") nearly_hidden_pct = _clamp_pct(nearly_hidden_pct, name="nearly_hidden_pct") partial_pct = _clamp_pct(partial_pct, name="partial_pct") @@ -1236,6 +1520,7 @@ def iter_generate_dataset( scene_scale = scene_rng.uniform(lo, hi) else: scene_scale = object_scale + scene_depth = scene_rng.uniform(depth_lo, depth_hi) if use_mle else None scene = generate_sonar_scene( seed=scene_seed, visibility=visibility, @@ -1244,6 +1529,9 @@ def iter_generate_dataset( object_kind=kind, beam_count=beam_count, length_count=length_count, + mle_mode=use_mle, + auv_depth=scene_depth, + relief_scale_pct=relief_pct, ) if scene.get("object") and scene["object"].get("vertexCount"): object_vertex_count = int(scene["object"]["vertexCount"]) @@ -1260,6 +1548,7 @@ def iter_generate_dataset( "pointCount": scene["pointCount"], "objectPointCount": scene["objectPointCount"], "objectScale": scene_scale, + "auvDepth": scene.get("auvDepth"), "files": paths, } written.append(entry) @@ -1302,6 +1591,10 @@ def iter_generate_dataset( "seed": int(seed), "beamCount": beam_count, "lengthCount": length_count, + "mleMode": use_mle, + "auvDepthMin": depth_lo if use_mle else None, + "auvDepthMax": depth_hi if use_mle else None, + "reliefScalePct": relief_pct, "objectName": object_name, "objectKind": kind, "objectScale": object_scale, @@ -1334,6 +1627,10 @@ def iter_generate_dataset( nearly_hidden_pct=nearly_hidden_pct, partial_pct=partial_pct, visible_pct=visible_pct, + mle_mode=use_mle, + auv_depth_min=depth_lo if use_mle else None, + auv_depth_max=depth_hi if use_mle else None, + relief_scale_pct=relief_pct, ) write_run_manifest(run_dir, manifest) result["settingsPath"] = str(run_dir / DATASET_RUN_FILENAME) @@ -1356,6 +1653,10 @@ def generate_dataset( nearly_hidden_pct: float = 20.0, partial_pct: float = 40.0, visible_pct: float = 40.0, + mle_mode: bool = False, + auv_depth_min: float = 2.0, + auv_depth_max: float = 8.0, + relief_scale_pct: float = 100.0, ) -> dict[str, Any]: """Generate `count` unique scenes into a new timestamped run folder under output_dir.""" result: dict[str, Any] | None = None @@ -1374,6 +1675,10 @@ def generate_dataset( nearly_hidden_pct=nearly_hidden_pct, partial_pct=partial_pct, visible_pct=visible_pct, + mle_mode=mle_mode, + auv_depth_min=auv_depth_min, + auv_depth_max=auv_depth_max, + relief_scale_pct=relief_scale_pct, ): if event.get("type") == "done": result = event["result"] diff --git a/backend/main.py b/backend/main.py index e68553d..5ab84f9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -503,6 +503,10 @@ async def dataset_generate( nearlyHiddenPct: float = Form(20.0), partialPct: float = Form(40.0), visiblePct: float = Form(40.0), + mleMode: bool = Form(False), + auvDepthMin: float = Form(2.0), + auvDepthMax: float = Form(8.0), + reliefScalePct: float = Form(100.0), modelPreset: str | None = Form(None), model: UploadFile | None = File(None), ) -> StreamingResponse: @@ -549,6 +553,10 @@ async def dataset_generate( nearly_hidden_pct=nearlyHiddenPct, partial_pct=partialPct, visible_pct=visiblePct, + mle_mode=mleMode, + auv_depth_min=auvDepthMin, + auv_depth_max=auvDepthMax, + relief_scale_pct=reliefScalePct, ): yield json.dumps(event, ensure_ascii=False) + "\n" except ValueError as exc: diff --git a/frontend/web/src/api/client.js b/frontend/web/src/api/client.js index d0abd8d..9214dc0 100644 --- a/frontend/web/src/api/client.js +++ b/frontend/web/src/api/client.js @@ -301,6 +301,10 @@ export const api = { nearlyHiddenPct = 20, partialPct = 40, visiblePct = 40, + mleMode = false, + auvDepthMin = 2, + auvDepthMax = 8, + reliefScalePct = 100, modelPreset = null, modelFile = null, onProgress, @@ -323,6 +327,10 @@ export const api = { formData.append("nearlyHiddenPct", String(nearlyHiddenPct ?? 20)); formData.append("partialPct", String(partialPct ?? 40)); formData.append("visiblePct", String(visiblePct ?? 40)); + formData.append("mleMode", mleMode ? "true" : "false"); + formData.append("auvDepthMin", String(auvDepthMin ?? 2)); + formData.append("auvDepthMax", String(auvDepthMax ?? 8)); + formData.append("reliefScalePct", String(reliefScalePct ?? 100)); if (preset) { formData.append("modelPreset", preset); } else { diff --git a/frontend/web/src/stores/dataset.js b/frontend/web/src/stores/dataset.js index 4508114..f0ea2bf 100644 --- a/frontend/web/src/stores/dataset.js +++ b/frontend/web/src/stores/dataset.js @@ -19,6 +19,10 @@ const PERSIST_KEYS = [ "nearlyHiddenPct", "partialPct", "visiblePct", + "mleMode", + "auvDepthMin", + "auvDepthMax", + "reliefScalePct", ]; function loadPersistedDataset() { @@ -52,6 +56,10 @@ export const useDatasetStore = defineStore("dataset", { objectScaleIsMax: !!saved.objectScaleIsMax, beamCount: saved.beamCount ?? 45, lengthCount: saved.lengthCount ?? 45, + mleMode: !!saved.mleMode, + auvDepthMin: saved.auvDepthMin ?? 2, + auvDepthMax: saved.auvDepthMax ?? 8, + reliefScalePct: saved.reliefScalePct ?? 100, absentPct: saved.absentPct ?? 30, nearlyHiddenPct: saved.nearlyHiddenPct ?? 20, partialPct: saved.partialPct ?? 40, @@ -283,7 +291,11 @@ export const useDatasetStore = defineStore("dataset", { this.statusText = "Генерация датасета…"; const modelLabel = this.selectedModelLabel || this.modelFileName || this.modelPreset; this.pushLog( - `Старт: count=${this.count}, seed=${this.seed}, beams=${this.beamCount}, length=${this.lengthCount}, scale=${this.objectScale}${this.objectScaleIsMax ? " (макс.)" : ""}, без объекта=${this.absentPct}%, видимость=${this.nearlyHiddenPct}/${this.partialPct}/${this.visiblePct}%, dir=${this.outputDir}, model=${modelLabel}`, + `Старт: count=${this.count}, seed=${this.seed}, beams=${this.beamCount}, length=${this.lengthCount}, неровности=${this.reliefScalePct}%${ + this.mleMode + ? `, МЛЭ высоты=${this.auvDepthMin}…${this.auvDepthMax} м` + : "" + }, scale=${this.objectScale}${this.objectScaleIsMax ? " (макс.)" : ""}, без объекта=${this.absentPct}%, видимость=${this.nearlyHiddenPct}/${this.partialPct}/${this.visiblePct}%, dir=${this.outputDir}, model=${modelLabel}`, ); try { const result = await api.datasetGenerate({ @@ -298,6 +310,10 @@ export const useDatasetStore = defineStore("dataset", { nearlyHiddenPct: Number(this.nearlyHiddenPct) || 0, partialPct: Number(this.partialPct) || 0, visiblePct: Number(this.visiblePct) || 0, + mleMode: !!this.mleMode, + auvDepthMin: Number(this.auvDepthMin) || 2, + auvDepthMax: Number(this.auvDepthMax) || 8, + reliefScalePct: Number(this.reliefScalePct) || 100, modelPreset: this.modelSource === "preset" ? this.modelPreset : null, modelFile: this.modelSource === "file" ? this.modelFile : null, onProgress: (event) => { @@ -321,8 +337,12 @@ export const useDatasetStore = defineStore("dataset", { entry.objectScale != null ? `, scale=${Number(entry.objectScale).toFixed(3)}` : ""; + const depthPart = + entry.auvDepth != null + ? `, h=${Number(entry.auvDepth).toFixed(2)} м` + : ""; this.pushLog( - `${entry.stem}: pts=${entry.pointCount}, object=${entry.objectPointCount}, ${entry.visibility}${scalePart}`, + `${entry.stem}: pts=${entry.pointCount}, object=${entry.objectPointCount}, ${entry.visibility}${scalePart}${depthPart}`, ); } } diff --git a/frontend/web/src/views/DatasetView.vue b/frontend/web/src/views/DatasetView.vue index 680bd47..7d36a49 100644 --- a/frontend/web/src/views/DatasetView.vue +++ b/frontend/web/src/views/DatasetView.vue @@ -79,6 +79,10 @@ watch( store.objectScaleIsMax, store.beamCount, store.lengthCount, + store.mleMode, + store.auvDepthMin, + store.auvDepthMax, + store.reliefScalePct, store.absentPct, store.nearlyHiddenPct, store.partialPct, @@ -276,7 +280,12 @@ function runOptionLabel(run) { :disabled="store.busy" /> - Поперечные лучи эхолота (X): N лучей = N возвратов по ширине галса. + + @@ -291,7 +300,72 @@ function runOptionLabel(run) { :disabled="store.busy" /> - Пинги вдоль курса (Y): L возвратов по длине. Итого точек сцены: N×L (первый отклик луча). + + + + + + + +
+ + + + Для каждой сцены высота АНПА над ровным дном выбирается случайно из этого диапазона. + +
+ + @@ -638,6 +712,12 @@ function runOptionLabel(run) { gap: 4px; font-size: 13px; } +.grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px 12px; + align-items: start; +} .field-label-row { display: flex; align-items: center;