Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
215b2108be | ||
|
|
a6e8ba31bd |
+357
-52
@@ -177,44 +177,83 @@ def _build_seafloor_meta(
|
|||||||
*,
|
*,
|
||||||
beam_count: int = 45,
|
beam_count: int = 45,
|
||||||
length_count: int | None = None,
|
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]:
|
) -> 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))
|
beams = max(1, int(beam_count))
|
||||||
length_points = beams if length_count is None else max(1, int(length_count))
|
length_points = beams if length_count is None else max(1, int(length_count))
|
||||||
size_x = rng.uniform(8.0, 16.0)
|
swath_deg = max(5.0, min(170.0, float(swath_angle_deg)))
|
||||||
size_y = rng.uniform(8.0, 16.0)
|
relief_pct = max(5.0, min(500.0, float(relief_scale_pct)))
|
||||||
base_z = rng.uniform(-1.2, -0.2)
|
|
||||||
amplitude = rng.uniform(0.05, 0.35)
|
|
||||||
frequency = rng.uniform(0.4, 2.2)
|
|
||||||
|
|
||||||
hills = [
|
if mle_mode and auv_depth is not None:
|
||||||
(
|
# Swath footprint from altitude above flat datum (like МЛЭ).
|
||||||
rng.uniform(-size_x * 0.4, size_x * 0.4),
|
depth = max(0.3, float(auv_depth))
|
||||||
rng.uniform(-size_y * 0.4, size_y * 0.4),
|
half_w = depth * math.tan(math.radians(swath_deg) * 0.5)
|
||||||
rng.uniform(0.15, 0.7),
|
size_x = max(4.0, 2.0 * half_w * 1.08)
|
||||||
rng.uniform(0.6, 2.2),
|
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))
|
|
||||||
]
|
n_valleys = rng.randint(1, 4)
|
||||||
valleys = [
|
valleys = []
|
||||||
(
|
for _ in range(n_valleys):
|
||||||
rng.uniform(-size_x * 0.4, size_x * 0.4),
|
valleys.append(
|
||||||
rng.uniform(-size_y * 0.4, size_y * 0.4),
|
(
|
||||||
rng.uniform(0.1, 0.55),
|
rng.uniform(-size_x * 0.38, size_x * 0.38),
|
||||||
rng.uniform(0.5, 2.0),
|
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))
|
|
||||||
]
|
# More medium bumps at lower scale; fewer but larger when scale is high.
|
||||||
# Relief clutter / false features as heightfield bumps (not extra points)
|
density = max(0.45, min(1.8, 120.0 / max(relief_pct, 20.0)))
|
||||||
bumps = [
|
n_bumps = int(round(rng.uniform(10, 20) * density))
|
||||||
(
|
n_bumps = max(8, min(40, n_bumps))
|
||||||
rng.uniform(-size_x * 0.45, size_x * 0.45),
|
bumps = []
|
||||||
rng.uniform(-size_y * 0.45, size_y * 0.45),
|
for _ in range(n_bumps):
|
||||||
rng.uniform(0.03, 0.35),
|
bumps.append(
|
||||||
rng.uniform(0.15, 0.9),
|
(
|
||||||
|
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 {
|
return {
|
||||||
"sizeX": size_x,
|
"sizeX": size_x,
|
||||||
@@ -232,6 +271,11 @@ def _build_seafloor_meta(
|
|||||||
"pingCount": length_points,
|
"pingCount": length_points,
|
||||||
"swathBeams": beams,
|
"swathBeams": beams,
|
||||||
"noise": rng.uniform(0.002, 0.02),
|
"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
|
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
|
# Balance plan + single scene
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -657,12 +869,19 @@ def generate_sonar_scene(
|
|||||||
object_kind: str | None = None,
|
object_kind: str | None = None,
|
||||||
beam_count: int = 45,
|
beam_count: int = 45,
|
||||||
length_count: int | None = None,
|
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]:
|
) -> dict[str, Any]:
|
||||||
"""Build one sonar scene via beam×ping first-hit casting.
|
"""Build one sonar scene via beam×ping first-hit casting.
|
||||||
|
|
||||||
visibility in absent|nearly_hidden|partial|visible.
|
visibility in absent|nearly_hidden|partial|visible.
|
||||||
Scene size is always beam_count × length_count returns.
|
Scene size is always beam_count × length_count returns.
|
||||||
object_kind ``pipe`` places a border-to-border pipeline with optional silt.
|
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))
|
rng = random.Random(int(seed))
|
||||||
if visibility not in ("absent",) + VISIBILITY_TIERS:
|
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:
|
if visibility != "absent" and kind != "pipe" and not object_points:
|
||||||
raise ValueError("object_points required when visibility is not absent.")
|
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"])
|
expected = int(meta["beamCount"]) * int(meta["lengthCount"])
|
||||||
|
|
||||||
object_info: dict[str, Any] | None = None
|
object_info: dict[str, Any] | None = None
|
||||||
object_hits: dict[tuple[int, int], float] | 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 visibility != "absent":
|
||||||
if kind == "pipe":
|
if kind == "pipe":
|
||||||
world, object_info = _place_pipeline_in_scene(
|
world, object_info = _place_pipeline_in_scene(
|
||||||
@@ -692,14 +926,32 @@ def generate_sonar_scene(
|
|||||||
object_points,
|
object_points,
|
||||||
object_scale=object_scale,
|
object_scale=object_scale,
|
||||||
)
|
)
|
||||||
object_hits = _rasterize_object_hits(world, meta)
|
if use_mle:
|
||||||
object_info["hitCellCount"] = len(object_hits)
|
object_field = _build_object_height_field(world, meta)
|
||||||
object_info["keptCount"] = len(object_hits)
|
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
|
object_info["requestedCount"] = expected
|
||||||
|
|
||||||
rows, object_point_count, background_point_count = _cast_echosounder_returns(
|
if use_mle:
|
||||||
rng, meta, object_hits
|
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:
|
if len(rows) != expected:
|
||||||
raise RuntimeError(f"Ray count mismatch: expected {expected}, got {len(rows)}")
|
raise RuntimeError(f"Ray count mismatch: expected {expected}, got {len(rows)}")
|
||||||
|
|
||||||
@@ -713,6 +965,8 @@ def generate_sonar_scene(
|
|||||||
"pointCount": len(rows),
|
"pointCount": len(rows),
|
||||||
"objectPointCount": object_point_count,
|
"objectPointCount": object_point_count,
|
||||||
"backgroundPointCount": background_point_count,
|
"backgroundPointCount": background_point_count,
|
||||||
|
"auvDepth": float(depth) if use_mle else None,
|
||||||
|
"mleMode": use_mle,
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
"points": xyz,
|
"points": xyz,
|
||||||
"meta": {
|
"meta": {
|
||||||
@@ -731,6 +985,10 @@ def generate_sonar_scene(
|
|||||||
"pingCount": meta.get("pingCount"),
|
"pingCount": meta.get("pingCount"),
|
||||||
"swathBeams": meta.get("swathBeams"),
|
"swathBeams": meta.get("swathBeams"),
|
||||||
"gridPointCount": expected,
|
"gridPointCount": expected,
|
||||||
|
"mleMode": use_mle,
|
||||||
|
"auvDepth": float(depth) if use_mle else None,
|
||||||
|
"swathAngleDeg": meta.get("swathAngleDeg"),
|
||||||
|
"reliefScalePct": meta.get("reliefScalePct"),
|
||||||
"floorFeatures": {
|
"floorFeatures": {
|
||||||
"hills": len(meta["hills"]),
|
"hills": len(meta["hills"]),
|
||||||
"valleys": len(meta["valleys"]),
|
"valleys": len(meta["valleys"]),
|
||||||
@@ -812,8 +1070,31 @@ def build_run_manifest(
|
|||||||
nearly_hidden_pct: float = 20.0,
|
nearly_hidden_pct: float = 20.0,
|
||||||
partial_pct: float = 40.0,
|
partial_pct: float = 40.0,
|
||||||
visible_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]:
|
) -> dict[str, Any]:
|
||||||
model_filename = _normalize_model_filename(object_name)
|
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 {
|
return {
|
||||||
"version": DATASET_RUN_VERSION,
|
"version": DATASET_RUN_VERSION,
|
||||||
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||||
@@ -821,20 +1102,7 @@ def build_run_manifest(
|
|||||||
"outputDir": str(run_dir),
|
"outputDir": str(run_dir),
|
||||||
"baseDir": str(base_dir),
|
"baseDir": str(base_dir),
|
||||||
"objectName": model_filename,
|
"objectName": model_filename,
|
||||||
"settings": {
|
"settings": 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),
|
|
||||||
},
|
|
||||||
"objectVertexCount": int(object_vertex_count),
|
"objectVertexCount": int(object_vertex_count),
|
||||||
"stats": stats,
|
"stats": stats,
|
||||||
"written": written,
|
"written": written,
|
||||||
@@ -1144,6 +1412,10 @@ def iter_generate_dataset(
|
|||||||
nearly_hidden_pct: float = 20.0,
|
nearly_hidden_pct: float = 20.0,
|
||||||
partial_pct: float = 40.0,
|
partial_pct: float = 40.0,
|
||||||
visible_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.
|
"""Yield NDJSON-friendly progress events, then a final ``done`` payload.
|
||||||
|
|
||||||
@@ -1183,6 +1455,18 @@ def iter_generate_dataset(
|
|||||||
if length_count > 1024:
|
if length_count > 1024:
|
||||||
raise ValueError("length_count (Длина) must be <= 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")
|
absent_pct = _clamp_pct(absent_pct, name="absent_pct")
|
||||||
nearly_hidden_pct = _clamp_pct(nearly_hidden_pct, name="nearly_hidden_pct")
|
nearly_hidden_pct = _clamp_pct(nearly_hidden_pct, name="nearly_hidden_pct")
|
||||||
partial_pct = _clamp_pct(partial_pct, name="partial_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)
|
scene_scale = scene_rng.uniform(lo, hi)
|
||||||
else:
|
else:
|
||||||
scene_scale = object_scale
|
scene_scale = object_scale
|
||||||
|
scene_depth = scene_rng.uniform(depth_lo, depth_hi) if use_mle else None
|
||||||
scene = generate_sonar_scene(
|
scene = generate_sonar_scene(
|
||||||
seed=scene_seed,
|
seed=scene_seed,
|
||||||
visibility=visibility,
|
visibility=visibility,
|
||||||
@@ -1244,6 +1529,9 @@ def iter_generate_dataset(
|
|||||||
object_kind=kind,
|
object_kind=kind,
|
||||||
beam_count=beam_count,
|
beam_count=beam_count,
|
||||||
length_count=length_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"):
|
if scene.get("object") and scene["object"].get("vertexCount"):
|
||||||
object_vertex_count = int(scene["object"]["vertexCount"])
|
object_vertex_count = int(scene["object"]["vertexCount"])
|
||||||
@@ -1260,6 +1548,7 @@ def iter_generate_dataset(
|
|||||||
"pointCount": scene["pointCount"],
|
"pointCount": scene["pointCount"],
|
||||||
"objectPointCount": scene["objectPointCount"],
|
"objectPointCount": scene["objectPointCount"],
|
||||||
"objectScale": scene_scale,
|
"objectScale": scene_scale,
|
||||||
|
"auvDepth": scene.get("auvDepth"),
|
||||||
"files": paths,
|
"files": paths,
|
||||||
}
|
}
|
||||||
written.append(entry)
|
written.append(entry)
|
||||||
@@ -1302,6 +1591,10 @@ def iter_generate_dataset(
|
|||||||
"seed": int(seed),
|
"seed": int(seed),
|
||||||
"beamCount": beam_count,
|
"beamCount": beam_count,
|
||||||
"lengthCount": length_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,
|
"objectName": object_name,
|
||||||
"objectKind": kind,
|
"objectKind": kind,
|
||||||
"objectScale": object_scale,
|
"objectScale": object_scale,
|
||||||
@@ -1334,6 +1627,10 @@ def iter_generate_dataset(
|
|||||||
nearly_hidden_pct=nearly_hidden_pct,
|
nearly_hidden_pct=nearly_hidden_pct,
|
||||||
partial_pct=partial_pct,
|
partial_pct=partial_pct,
|
||||||
visible_pct=visible_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)
|
write_run_manifest(run_dir, manifest)
|
||||||
result["settingsPath"] = str(run_dir / DATASET_RUN_FILENAME)
|
result["settingsPath"] = str(run_dir / DATASET_RUN_FILENAME)
|
||||||
@@ -1356,6 +1653,10 @@ def generate_dataset(
|
|||||||
nearly_hidden_pct: float = 20.0,
|
nearly_hidden_pct: float = 20.0,
|
||||||
partial_pct: float = 40.0,
|
partial_pct: float = 40.0,
|
||||||
visible_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]:
|
) -> dict[str, Any]:
|
||||||
"""Generate `count` unique scenes into a new timestamped run folder under output_dir."""
|
"""Generate `count` unique scenes into a new timestamped run folder under output_dir."""
|
||||||
result: dict[str, Any] | None = None
|
result: dict[str, Any] | None = None
|
||||||
@@ -1374,6 +1675,10 @@ def generate_dataset(
|
|||||||
nearly_hidden_pct=nearly_hidden_pct,
|
nearly_hidden_pct=nearly_hidden_pct,
|
||||||
partial_pct=partial_pct,
|
partial_pct=partial_pct,
|
||||||
visible_pct=visible_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":
|
if event.get("type") == "done":
|
||||||
result = event["result"]
|
result = event["result"]
|
||||||
|
|||||||
@@ -503,6 +503,10 @@ async def dataset_generate(
|
|||||||
nearlyHiddenPct: float = Form(20.0),
|
nearlyHiddenPct: float = Form(20.0),
|
||||||
partialPct: float = Form(40.0),
|
partialPct: float = Form(40.0),
|
||||||
visiblePct: 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),
|
modelPreset: str | None = Form(None),
|
||||||
model: UploadFile | None = File(None),
|
model: UploadFile | None = File(None),
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
@@ -549,6 +553,10 @@ async def dataset_generate(
|
|||||||
nearly_hidden_pct=nearlyHiddenPct,
|
nearly_hidden_pct=nearlyHiddenPct,
|
||||||
partial_pct=partialPct,
|
partial_pct=partialPct,
|
||||||
visible_pct=visiblePct,
|
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"
|
yield json.dumps(event, ensure_ascii=False) + "\n"
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
|
|||||||
@@ -301,6 +301,10 @@ export const api = {
|
|||||||
nearlyHiddenPct = 20,
|
nearlyHiddenPct = 20,
|
||||||
partialPct = 40,
|
partialPct = 40,
|
||||||
visiblePct = 40,
|
visiblePct = 40,
|
||||||
|
mleMode = false,
|
||||||
|
auvDepthMin = 2,
|
||||||
|
auvDepthMax = 8,
|
||||||
|
reliefScalePct = 100,
|
||||||
modelPreset = null,
|
modelPreset = null,
|
||||||
modelFile = null,
|
modelFile = null,
|
||||||
onProgress,
|
onProgress,
|
||||||
@@ -323,6 +327,10 @@ export const api = {
|
|||||||
formData.append("nearlyHiddenPct", String(nearlyHiddenPct ?? 20));
|
formData.append("nearlyHiddenPct", String(nearlyHiddenPct ?? 20));
|
||||||
formData.append("partialPct", String(partialPct ?? 40));
|
formData.append("partialPct", String(partialPct ?? 40));
|
||||||
formData.append("visiblePct", String(visiblePct ?? 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) {
|
if (preset) {
|
||||||
formData.append("modelPreset", preset);
|
formData.append("modelPreset", preset);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ function sampleHeight(meshInfo, x, y) {
|
|||||||
return (h00 * (1 - tx) + h10 * tx) * (1 - ty) + (h01 * (1 - tx) + h11 * tx) * ty;
|
return (h00 * (1 - tx) + h10 * tx) * (1 - ty) + (h01 * (1 - tx) + h11 * tx) * ty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Flat seafloor datum (without corridor relief). AUV altitude is measured from this. */
|
||||||
|
function referenceSeafloorZ(meshInfo) {
|
||||||
|
if (!meshInfo) return -5;
|
||||||
|
const base = Number(meshInfo.baseZ);
|
||||||
|
if (Number.isFinite(base)) return base;
|
||||||
|
return sampleHeight(meshInfo, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('vue').Ref} containerRef main 3D view
|
* @param {import('vue').Ref} containerRef main 3D view
|
||||||
* @param {import('vue').Ref} surveyRef bottom-right survey surface panel
|
* @param {import('vue').Ref} surveyRef bottom-right survey surface panel
|
||||||
@@ -959,7 +967,7 @@ export function useGboSimulator(containerRef, surveyRef) {
|
|||||||
|
|
||||||
function placeAuv(x, y, depth, headingDeg) {
|
function placeAuv(x, y, depth, headingDeg) {
|
||||||
if (!auvPivot) return;
|
if (!auvPivot) return;
|
||||||
const floorZ = sampleHeight(meshInfo, x, y);
|
const floorZ = referenceSeafloorZ(meshInfo);
|
||||||
auvPivot.position.set(x, y, floorZ + Math.max(0.3, Number(depth) || 2.5));
|
auvPivot.position.set(x, y, floorZ + Math.max(0.3, Number(depth) || 2.5));
|
||||||
headingRad = degToRad(headingDeg);
|
headingRad = degToRad(headingDeg);
|
||||||
auvPivot.rotation.set(0, 0, headingRad);
|
auvPivot.rotation.set(0, 0, headingRad);
|
||||||
@@ -1016,8 +1024,8 @@ export function useGboSimulator(containerRef, surveyRef) {
|
|||||||
traveled += step;
|
traveled += step;
|
||||||
const nx = auvPivot.position.x + Math.cos(headingRad) * step;
|
const nx = auvPivot.position.x + Math.cos(headingRad) * step;
|
||||||
const ny = auvPivot.position.y + Math.sin(headingRad) * step;
|
const ny = auvPivot.position.y + Math.sin(headingRad) * step;
|
||||||
const floorZ = sampleHeight(meshInfo, nx, ny);
|
// Constant altitude vs flat datum — do not follow seafloor relief.
|
||||||
auvPivot.position.set(nx, ny, floorZ + auvDepth);
|
auvPivot.position.set(nx, ny, referenceSeafloorZ(meshInfo) + auvDepth);
|
||||||
updateRays(true);
|
updateRays(true);
|
||||||
updateTrail();
|
updateTrail();
|
||||||
onStatus({
|
onStatus({
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ function sampleHeight(meshInfo, x, y) {
|
|||||||
return (h00 * (1 - tx) + h10 * tx) * (1 - ty) + (h01 * (1 - tx) + h11 * tx) * ty;
|
return (h00 * (1 - tx) + h10 * tx) * (1 - ty) + (h01 * (1 - tx) + h11 * tx) * ty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Flat seafloor datum (without corridor relief). AUV altitude is measured from this. */
|
||||||
|
function referenceSeafloorZ(meshInfo) {
|
||||||
|
if (!meshInfo) return -5;
|
||||||
|
const base = Number(meshInfo.baseZ);
|
||||||
|
if (Number.isFinite(base)) return base;
|
||||||
|
return sampleHeight(meshInfo, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('vue').Ref} containerRef main 3D view
|
* @param {import('vue').Ref} containerRef main 3D view
|
||||||
* @param {import('vue').Ref} surveyRef bottom-right survey surface panel
|
* @param {import('vue').Ref} surveyRef bottom-right survey surface panel
|
||||||
@@ -257,8 +265,8 @@ export function useMleSimulator(containerRef, surveyRef) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preview of seafloor/object contacts the AUV will meet along the survey track.
|
* Preview of the future survey swath on the seafloor only (relief / unevenness).
|
||||||
* Starts at current echosounder footprint and extends forward by surveyLength.
|
* Does not wrap or outline the bottom object — object contacts appear only during motion.
|
||||||
*/
|
*/
|
||||||
function buildPathReliefPreview() {
|
function buildPathReliefPreview() {
|
||||||
clearPathRelief();
|
clearPathRelief();
|
||||||
@@ -277,122 +285,65 @@ export function useMleSimulator(containerRef, surveyRef) {
|
|||||||
const startY = auvPivot.position.y;
|
const startY = auvPivot.position.y;
|
||||||
const depth = Math.max(0.3, Number(auvDepth) || 2.5);
|
const depth = Math.max(0.3, Number(auvDepth) || 2.5);
|
||||||
|
|
||||||
// Station 0: real beam hits (where rays touch seafloor/object now).
|
|
||||||
const currentHits = castBeamHits();
|
|
||||||
const positions = new Float32Array(nAlong * nAcross * 3);
|
const positions = new Float32Array(nAlong * nAcross * 3);
|
||||||
const colors = new Float32Array(nAlong * nAcross * 3);
|
const colors = new Float32Array(nAlong * nAcross * 3);
|
||||||
const flat = new THREE.Color(0x2f6b52);
|
const flat = new THREE.Color(0x2f6b52);
|
||||||
const mid = new THREE.Color(0xe6a820);
|
const mid = new THREE.Color(0xe6a820);
|
||||||
const hot = new THREE.Color(0xfff176);
|
const hot = new THREE.Color(0xfff176);
|
||||||
const objTint = new THREE.Color(0xff6b2d);
|
|
||||||
|
|
||||||
const writeVertex = (row, col, x, y, z, intensity, isObject) => {
|
const writeVertex = (row, col, x, y, z, intensity) => {
|
||||||
const o = (row * nAcross + col) * 3;
|
const o = (row * nAcross + col) * 3;
|
||||||
positions[o] = x;
|
positions[o] = x;
|
||||||
positions[o + 1] = y;
|
positions[o + 1] = y;
|
||||||
positions[o + 2] = z + 0.08; // slightly above so it reads over the wireframe
|
positions[o + 2] = z + 0.08; // slightly above so it reads over the wireframe
|
||||||
let c;
|
const c =
|
||||||
if (isObject) {
|
intensity < 0.35
|
||||||
c = objTint;
|
? flat.clone().lerp(mid, intensity / 0.35)
|
||||||
} else if (intensity < 0.35) {
|
: mid.clone().lerp(hot, Math.min(1, (intensity - 0.35) / 0.65));
|
||||||
c = flat.clone().lerp(mid, intensity / 0.35);
|
|
||||||
} else {
|
|
||||||
c = mid.clone().lerp(hot, Math.min(1, (intensity - 0.35) / 0.65));
|
|
||||||
}
|
|
||||||
// Keep the whole swath visible; boost alpha via brightness on relief.
|
|
||||||
colors[o] = c.r;
|
colors[o] = c.r;
|
||||||
colors[o + 1] = c.g;
|
colors[o + 1] = c.g;
|
||||||
colors[o + 2] = c.b;
|
colors[o + 2] = c.b;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let col = 0; col < nAcross; col += 1) {
|
const probeSeafloorHit = (ax, ay, originZ, dirX, dirY, dirZ) => {
|
||||||
const u = nAcross === 1 ? 0.5 : col / (nAcross - 1);
|
let hitX = ax;
|
||||||
if (currentHits.length) {
|
let hitY = ay;
|
||||||
const src = currentHits[Math.min(currentHits.length - 1, Math.round(u * (currentHits.length - 1)))];
|
let hitZ = sampleHeight(meshInfo, ax, ay);
|
||||||
const intensity = reliefIntensity(src.x, src.y);
|
const step = Math.max(0.3, maxRange / 180);
|
||||||
writeVertex(0, col, src.x, src.y, src.z, Math.max(intensity, src.isObject ? 1 : 0.2), !!src.isObject);
|
let px = ax;
|
||||||
} else {
|
let py = ay;
|
||||||
const angle = -swath * 0.5 + swath * u;
|
let pz = originZ;
|
||||||
const dirX = Math.sin(angle) * acrossX;
|
for (let s = 0; s < 220; s += 1) {
|
||||||
const dirY = Math.sin(angle) * acrossY;
|
px += dirX * step;
|
||||||
const dirZ = -Math.cos(angle);
|
py += dirY * step;
|
||||||
// fallback probe from AUV
|
pz += dirZ * step;
|
||||||
let hitX = startX;
|
if (Math.hypot(px - ax, py - ay) + Math.abs(pz - originZ) > maxRange * 1.15) break;
|
||||||
let hitY = startY;
|
const floorZ = sampleHeight(meshInfo, px, py);
|
||||||
let hitZ = sampleHeight(meshInfo, startX, startY);
|
if (pz <= floorZ) {
|
||||||
const originZ = hitZ + depth;
|
hitX = px;
|
||||||
const step = Math.max(0.3, maxRange / 180);
|
hitY = py;
|
||||||
let px = startX;
|
hitZ = floorZ;
|
||||||
let py = startY;
|
break;
|
||||||
let pz = originZ;
|
|
||||||
for (let s = 0; s < 220; s += 1) {
|
|
||||||
px += dirX * step;
|
|
||||||
py += dirY * step;
|
|
||||||
pz += dirZ * step;
|
|
||||||
const floorZ = sampleHeight(meshInfo, px, py);
|
|
||||||
if (pz <= floorZ) {
|
|
||||||
hitX = px;
|
|
||||||
hitY = py;
|
|
||||||
hitZ = floorZ;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
writeVertex(0, col, hitX, hitY, hitZ, Math.max(0.15, reliefIntensity(hitX, hitY)), false);
|
|
||||||
}
|
}
|
||||||
}
|
return { hitX, hitY, hitZ };
|
||||||
|
};
|
||||||
|
|
||||||
// Forward stations: predicted beam footprint along the future track.
|
// Stations along track: seafloor footprint only (ignore object mesh until simulation).
|
||||||
for (let row = 1; row < nAlong; row += 1) {
|
// Beam origins stay at constant altitude above the flat datum (not terrain-following).
|
||||||
const along = (row / (nAlong - 1)) * length;
|
const originZ = referenceSeafloorZ(meshInfo) + depth;
|
||||||
|
for (let row = 0; row < nAlong; row += 1) {
|
||||||
|
const along = nAlong === 1 ? 0 : (row / (nAlong - 1)) * length;
|
||||||
const ax = startX + hx * along;
|
const ax = startX + hx * along;
|
||||||
const ay = startY + hy * along;
|
const ay = startY + hy * along;
|
||||||
const floorHere = sampleHeight(meshInfo, ax, ay);
|
|
||||||
const originZ = floorHere + depth;
|
|
||||||
for (let col = 0; col < nAcross; col += 1) {
|
for (let col = 0; col < nAcross; col += 1) {
|
||||||
const u = nAcross === 1 ? 0.5 : col / (nAcross - 1);
|
const u = nAcross === 1 ? 0.5 : col / (nAcross - 1);
|
||||||
const angle = -swath * 0.5 + swath * u;
|
const angle = -swath * 0.5 + swath * u;
|
||||||
const dirX = Math.sin(angle) * acrossX;
|
const dirX = Math.sin(angle) * acrossX;
|
||||||
const dirY = Math.sin(angle) * acrossY;
|
const dirY = Math.sin(angle) * acrossY;
|
||||||
const dirZ = -Math.cos(angle);
|
const dirZ = -Math.cos(angle);
|
||||||
let hitX = ax;
|
const { hitX, hitY, hitZ } = probeSeafloorHit(ax, ay, originZ, dirX, dirY, dirZ);
|
||||||
let hitY = ay;
|
writeVertex(row, col, hitX, hitY, hitZ, Math.max(0.12, reliefIntensity(hitX, hitY)));
|
||||||
let hitZ = floorHere;
|
|
||||||
let isObject = false;
|
|
||||||
const step = Math.max(0.3, maxRange / 180);
|
|
||||||
let px = ax;
|
|
||||||
let py = ay;
|
|
||||||
let pz = originZ;
|
|
||||||
for (let s = 0; s < 220; s += 1) {
|
|
||||||
px += dirX * step;
|
|
||||||
py += dirY * step;
|
|
||||||
pz += dirZ * step;
|
|
||||||
if (Math.hypot(px - ax, py - ay) + Math.abs(pz - originZ) > maxRange * 1.15) break;
|
|
||||||
const floorZ = sampleHeight(meshInfo, px, py);
|
|
||||||
if (pz <= floorZ) {
|
|
||||||
hitX = px;
|
|
||||||
hitY = py;
|
|
||||||
hitZ = floorZ;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Object occlusion preview: if object mesh is above floor along beam, tint as object.
|
|
||||||
if (objectPivot) {
|
|
||||||
const origin = new THREE.Vector3(ax, ay, originZ);
|
|
||||||
const dir = new THREE.Vector3(dirX, dirY, dirZ).normalize();
|
|
||||||
raycaster.set(origin, dir);
|
|
||||||
raycaster.far = maxRange;
|
|
||||||
const intersects = raycaster.intersectObject(objectPivot, true);
|
|
||||||
if (intersects.length) {
|
|
||||||
const dFloor = Math.hypot(hitX - ax, hitY - ay, hitZ - originZ);
|
|
||||||
if (intersects[0].distance < dFloor) {
|
|
||||||
hitX = intersects[0].point.x;
|
|
||||||
hitY = intersects[0].point.y;
|
|
||||||
hitZ = intersects[0].point.z;
|
|
||||||
isObject = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writeVertex(row, col, hitX, hitY, hitZ, Math.max(0.12, reliefIntensity(hitX, hitY)), isObject);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,7 +844,7 @@ export function useMleSimulator(containerRef, surveyRef) {
|
|||||||
|
|
||||||
function placeAuv(x, y, depth, headingDeg) {
|
function placeAuv(x, y, depth, headingDeg) {
|
||||||
if (!auvPivot) return;
|
if (!auvPivot) return;
|
||||||
const floorZ = sampleHeight(meshInfo, x, y);
|
const floorZ = referenceSeafloorZ(meshInfo);
|
||||||
auvPivot.position.set(x, y, floorZ + Math.max(0.3, Number(depth) || 2.5));
|
auvPivot.position.set(x, y, floorZ + Math.max(0.3, Number(depth) || 2.5));
|
||||||
headingRad = degToRad(headingDeg);
|
headingRad = degToRad(headingDeg);
|
||||||
auvPivot.rotation.set(0, 0, headingRad);
|
auvPivot.rotation.set(0, 0, headingRad);
|
||||||
@@ -915,7 +866,7 @@ export function useMleSimulator(containerRef, surveyRef) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function castBeamHits() {
|
function castBeamHits({ includeObject = true } = {}) {
|
||||||
if (!auvPivot || !meshInfo) return [];
|
if (!auvPivot || !meshInfo) return [];
|
||||||
const origin = auvPivot.position.clone();
|
const origin = auvPivot.position.clone();
|
||||||
const count = Math.max(1, Math.min(256, Number(beamCount) || 45));
|
const count = Math.max(1, Math.min(256, Number(beamCount) || 45));
|
||||||
@@ -923,7 +874,7 @@ export function useMleSimulator(containerRef, surveyRef) {
|
|||||||
const maxRange = Math.max(1, Number(detectionRangeM) || DETECTION_RANGE_DEFAULT);
|
const maxRange = Math.max(1, Number(detectionRangeM) || DETECTION_RANGE_DEFAULT);
|
||||||
raycaster.far = maxRange;
|
raycaster.far = maxRange;
|
||||||
const across = new THREE.Vector3(-Math.sin(headingRad), Math.cos(headingRad), 0);
|
const across = new THREE.Vector3(-Math.sin(headingRad), Math.cos(headingRad), 0);
|
||||||
if (objectPivot) objectPivot.updateMatrixWorld(true);
|
if (includeObject && objectPivot) objectPivot.updateMatrixWorld(true);
|
||||||
|
|
||||||
const hits = [];
|
const hits = [];
|
||||||
for (let i = 0; i < count; i += 1) {
|
for (let i = 0; i < count; i += 1) {
|
||||||
@@ -952,10 +903,10 @@ export function useMleSimulator(containerRef, surveyRef) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object first-hit (mesh raycast) — echosounder return if closer than seafloor
|
// Object first-hit (mesh raycast) — only during simulation / survey accumulation
|
||||||
let objPoint = null;
|
let objPoint = null;
|
||||||
let objDist = Infinity;
|
let objDist = Infinity;
|
||||||
if (objectPivot) {
|
if (includeObject && objectPivot) {
|
||||||
raycaster.set(origin, beamDir);
|
raycaster.set(origin, beamDir);
|
||||||
const intersects = raycaster.intersectObject(objectPivot, true);
|
const intersects = raycaster.intersectObject(objectPivot, true);
|
||||||
if (intersects.length && intersects[0].distance <= maxRange) {
|
if (intersects.length && intersects[0].distance <= maxRange) {
|
||||||
@@ -996,7 +947,8 @@ export function useMleSimulator(containerRef, surveyRef) {
|
|||||||
}
|
}
|
||||||
if (!auvPivot || !meshInfo) return;
|
if (!auvPivot || !meshInfo) return;
|
||||||
const origin = auvPivot.position.clone();
|
const origin = auvPivot.position.clone();
|
||||||
const hits = castBeamHits();
|
// Before motion starts, rays and path preview ignore the object (seafloor only).
|
||||||
|
const hits = castBeamHits({ includeObject: !!accumulateSurvey || running });
|
||||||
const positions = new Float32Array(hits.length * 2 * 3);
|
const positions = new Float32Array(hits.length * 2 * 3);
|
||||||
for (let i = 0; i < hits.length; i += 1) {
|
for (let i = 0; i < hits.length; i += 1) {
|
||||||
const o = i * 6;
|
const o = i * 6;
|
||||||
@@ -1060,8 +1012,8 @@ export function useMleSimulator(containerRef, surveyRef) {
|
|||||||
traveled += step;
|
traveled += step;
|
||||||
const nx = auvPivot.position.x + Math.cos(headingRad) * step;
|
const nx = auvPivot.position.x + Math.cos(headingRad) * step;
|
||||||
const ny = auvPivot.position.y + Math.sin(headingRad) * step;
|
const ny = auvPivot.position.y + Math.sin(headingRad) * step;
|
||||||
const floorZ = sampleHeight(meshInfo, nx, ny);
|
// Constant altitude vs flat datum — do not follow seafloor relief.
|
||||||
auvPivot.position.set(nx, ny, floorZ + auvDepth);
|
auvPivot.position.set(nx, ny, referenceSeafloorZ(meshInfo) + auvDepth);
|
||||||
updateRays(true);
|
updateRays(true);
|
||||||
updateTrail();
|
updateTrail();
|
||||||
onStatus({
|
onStatus({
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ const PERSIST_KEYS = [
|
|||||||
"nearlyHiddenPct",
|
"nearlyHiddenPct",
|
||||||
"partialPct",
|
"partialPct",
|
||||||
"visiblePct",
|
"visiblePct",
|
||||||
|
"mleMode",
|
||||||
|
"auvDepthMin",
|
||||||
|
"auvDepthMax",
|
||||||
|
"reliefScalePct",
|
||||||
];
|
];
|
||||||
|
|
||||||
function loadPersistedDataset() {
|
function loadPersistedDataset() {
|
||||||
@@ -52,6 +56,10 @@ export const useDatasetStore = defineStore("dataset", {
|
|||||||
objectScaleIsMax: !!saved.objectScaleIsMax,
|
objectScaleIsMax: !!saved.objectScaleIsMax,
|
||||||
beamCount: saved.beamCount ?? 45,
|
beamCount: saved.beamCount ?? 45,
|
||||||
lengthCount: saved.lengthCount ?? 45,
|
lengthCount: saved.lengthCount ?? 45,
|
||||||
|
mleMode: !!saved.mleMode,
|
||||||
|
auvDepthMin: saved.auvDepthMin ?? 2,
|
||||||
|
auvDepthMax: saved.auvDepthMax ?? 8,
|
||||||
|
reliefScalePct: saved.reliefScalePct ?? 100,
|
||||||
absentPct: saved.absentPct ?? 30,
|
absentPct: saved.absentPct ?? 30,
|
||||||
nearlyHiddenPct: saved.nearlyHiddenPct ?? 20,
|
nearlyHiddenPct: saved.nearlyHiddenPct ?? 20,
|
||||||
partialPct: saved.partialPct ?? 40,
|
partialPct: saved.partialPct ?? 40,
|
||||||
@@ -283,7 +291,11 @@ export const useDatasetStore = defineStore("dataset", {
|
|||||||
this.statusText = "Генерация датасета…";
|
this.statusText = "Генерация датасета…";
|
||||||
const modelLabel = this.selectedModelLabel || this.modelFileName || this.modelPreset;
|
const modelLabel = this.selectedModelLabel || this.modelFileName || this.modelPreset;
|
||||||
this.pushLog(
|
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 {
|
try {
|
||||||
const result = await api.datasetGenerate({
|
const result = await api.datasetGenerate({
|
||||||
@@ -298,6 +310,10 @@ export const useDatasetStore = defineStore("dataset", {
|
|||||||
nearlyHiddenPct: Number(this.nearlyHiddenPct) || 0,
|
nearlyHiddenPct: Number(this.nearlyHiddenPct) || 0,
|
||||||
partialPct: Number(this.partialPct) || 0,
|
partialPct: Number(this.partialPct) || 0,
|
||||||
visiblePct: Number(this.visiblePct) || 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,
|
modelPreset: this.modelSource === "preset" ? this.modelPreset : null,
|
||||||
modelFile: this.modelSource === "file" ? this.modelFile : null,
|
modelFile: this.modelSource === "file" ? this.modelFile : null,
|
||||||
onProgress: (event) => {
|
onProgress: (event) => {
|
||||||
@@ -321,8 +337,12 @@ export const useDatasetStore = defineStore("dataset", {
|
|||||||
entry.objectScale != null
|
entry.objectScale != null
|
||||||
? `, scale=${Number(entry.objectScale).toFixed(3)}`
|
? `, scale=${Number(entry.objectScale).toFixed(3)}`
|
||||||
: "";
|
: "";
|
||||||
|
const depthPart =
|
||||||
|
entry.auvDepth != null
|
||||||
|
? `, h=${Number(entry.auvDepth).toFixed(2)} м`
|
||||||
|
: "";
|
||||||
this.pushLog(
|
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}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ watch(
|
|||||||
store.objectScaleIsMax,
|
store.objectScaleIsMax,
|
||||||
store.beamCount,
|
store.beamCount,
|
||||||
store.lengthCount,
|
store.lengthCount,
|
||||||
|
store.mleMode,
|
||||||
|
store.auvDepthMin,
|
||||||
|
store.auvDepthMax,
|
||||||
|
store.reliefScalePct,
|
||||||
store.absentPct,
|
store.absentPct,
|
||||||
store.nearlyHiddenPct,
|
store.nearlyHiddenPct,
|
||||||
store.partialPct,
|
store.partialPct,
|
||||||
@@ -276,7 +280,12 @@ function runOptionLabel(run) {
|
|||||||
:disabled="store.busy"
|
:disabled="store.busy"
|
||||||
/>
|
/>
|
||||||
<span class="ref-caption">
|
<span class="ref-caption">
|
||||||
Поперечные лучи эхолота (X): N лучей = N возвратов по ширине галса.
|
<template v-if="store.mleMode">
|
||||||
|
Число лучей веера МЛЭ в одном пинге (поперек курса).
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Поперечные лучи эхолота (X): N лучей = N возвратов по ширине галса.
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -291,7 +300,72 @@ function runOptionLabel(run) {
|
|||||||
:disabled="store.busy"
|
:disabled="store.busy"
|
||||||
/>
|
/>
|
||||||
<span class="ref-caption">
|
<span class="ref-caption">
|
||||||
Пинги вдоль курса (Y): L возвратов по длине. Итого точек сцены: N×L (первый отклик луча).
|
<template v-if="store.mleMode">
|
||||||
|
Число пингов вдоль курса. Итого точек сцены: лучи × пинги.
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Пинги вдоль курса (Y): L возвратов по длине. Итого точек сцены: N×L (первый отклик луча).
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field check-block">
|
||||||
|
<span class="field-label-row">
|
||||||
|
<label class="check-inline">
|
||||||
|
<input
|
||||||
|
v-model="store.mleMode"
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="store.busy"
|
||||||
|
/>
|
||||||
|
Съёмка как многолучевой эхолот
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
<span class="ref-caption">
|
||||||
|
Рельеф и объекты — как первый отклик веера МЛЭ (наклонными лучами), а не ортогональной сеткой.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div v-if="store.mleMode" class="grid-2">
|
||||||
|
<label class="field">
|
||||||
|
<span>Диапазон высот, м (от)</span>
|
||||||
|
<input
|
||||||
|
v-model.number="store.auvDepthMin"
|
||||||
|
type="number"
|
||||||
|
min="0.3"
|
||||||
|
max="200"
|
||||||
|
step="0.1"
|
||||||
|
:disabled="store.busy"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Диапазон высот, м (до)</span>
|
||||||
|
<input
|
||||||
|
v-model.number="store.auvDepthMax"
|
||||||
|
type="number"
|
||||||
|
min="0.3"
|
||||||
|
max="200"
|
||||||
|
step="0.1"
|
||||||
|
:disabled="store.busy"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<span class="ref-caption" style="grid-column: 1 / -1">
|
||||||
|
Для каждой сцены высота АНПА над ровным дном выбирается случайно из этого диапазона.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="field">
|
||||||
|
<span>Размер неровностей, %</span>
|
||||||
|
<input
|
||||||
|
v-model.number="store.reliefScalePct"
|
||||||
|
type="number"
|
||||||
|
min="5"
|
||||||
|
max="500"
|
||||||
|
step="5"
|
||||||
|
:disabled="store.busy"
|
||||||
|
/>
|
||||||
|
<span class="ref-caption">
|
||||||
|
Масштаб случайного рельефа дна относительно размера сцены.
|
||||||
|
100% — заметные холмы/впадины; 200% — примерно вдвое крупнее.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -638,6 +712,12 @@ function runOptionLabel(run) {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
.grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px 12px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
.field-label-row {
|
.field-label-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -221,11 +221,11 @@ function onShotSurvey() {
|
|||||||
<input v-model.number="store.auvX" type="number" step="0.5" :disabled="store.running" />
|
<input v-model.number="store.auvX" type="number" step="0.5" :disabled="store.running" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Y (старт)</span>
|
<span>Y</span>
|
||||||
<input v-model.number="store.auvY" type="number" step="0.5" :disabled="store.running" />
|
<input v-model.number="store.auvY" type="number" step="0.5" :disabled="store.running" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Высота над дном, м</span>
|
<span>Высота над дном (без неровностей), м</span>
|
||||||
<input v-model.number="store.auvDepth" type="number" min="0.3" step="0.1" :disabled="store.running" />
|
<input v-model.number="store.auvDepth" type="number" min="0.3" step="0.1" :disabled="store.running" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
|
|||||||
@@ -241,11 +241,11 @@ function onShotSurvey() {
|
|||||||
<input v-model.number="store.auvX" type="number" step="0.5" :disabled="store.running" />
|
<input v-model.number="store.auvX" type="number" step="0.5" :disabled="store.running" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Y (старт)</span>
|
<span>Y</span>
|
||||||
<input v-model.number="store.auvY" type="number" step="0.5" :disabled="store.running" />
|
<input v-model.number="store.auvY" type="number" step="0.5" :disabled="store.running" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Высота над дном, м</span>
|
<span>Высота над дном (без неровностей), м</span>
|
||||||
<input v-model.number="store.auvDepth" type="number" min="0.3" step="0.1" :disabled="store.running" />
|
<input v-model.number="store.auvDepth" type="number" min="0.3" step="0.1" :disabled="store.running" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
|
|||||||
Reference in New Issue
Block a user