Skip to content

Dataset generation #4

Merged
merged 20 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Created by venv; see https://docs.python.org/3/library/venv.html
ShipD/
ShipGen/
Scripts/
Lib/
Include/
Expand Down
6 changes: 3 additions & 3 deletions AUVSim/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .auv import (
AUV,
AUVPath,
AUVPathGen
AUV as AUV,
AUVPath as AUVPath,
AUVPathGen as AUVPathGen,
)
7 changes: 6 additions & 1 deletion AUVSim/auv.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ class AUV:
ideal_distance:float # Currently unused
start_pos:Vector3
start_facing:Vector3
def __init__(self, position:Vector3, facing:Vector3):
def __init__(self,
position:Vector3,
facing:Vector3,
ideal_distance:float = -1,
):
self.start_pos = position
self.start_facing = facing
self.ideal_distance = ideal_distance
class AUVPath:
positions:Vector3Array
facings:Vector3Array
Expand Down
2 changes: 2 additions & 0 deletions CustomTypes/custom_arrays.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class TriangleArray(CustomNP[tuple[int, Literal[3], Literal[3]], np.dtype[np.flo
def __new__(cls, content: np.ndarray[tuple[int, Literal[3], Literal[3]], np.dtype[np.float_]]) -> TriangleArray:...

@overload # type: ignore
def __getitem__(self, index:int) -> Triangle: ...
@overload
def __getitem__(self, index:tuple[int, Literal[0, 1, 2]]) -> Vector3: ...
@overload
def __getitem__(self, index:tuple[int, Literal[0, 1, 2], Literal[0, 1, 2]]) -> float: ...
Expand Down
2 changes: 1 addition & 1 deletion CustomTypes/custom_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def __new__(cls,
x, y, z = x
elif isinstance(x, Vector3):
x, y, z = x
assert isinstance(x, (float, int))
assert isinstance(x, (float, int, np.floating))
return np.array([x, y, z]).view(cls)
def __str__(self):
return f"({self[0]}, {self[1]}, {self[2]})"
Expand Down
1 change: 1 addition & 0 deletions Generate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
13 changes: 13 additions & 0 deletions Generate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .auv import(
place_auvs_over_points as place_auvs_over_points,
generate_paths as generate_paths,
scan_paths as scan_paths,
)
from .hull import (
generate_hulls as generate_hulls,
generate_points_on_hull as generate_points_on_hull,
generate_vertical_bounds as generate_vertical_bounds,
load_anomalies as load_anomalies,
pick_anomalies as pick_anomalies,
place_anomalies_at_points as place_anomalies_at_points,
)
114 changes: 114 additions & 0 deletions Generate/auv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from typing import Callable

from CustomTypes import Vector3, Vector3Array
from Raytrace import TriangleMesh
from AUVSim import AUV, AUVPathGen, AUVPath
from Raytrace.SideScan import SideScan, ScanReading, RawScanReading

from .helper import get_surface_normal, normalize

def place_auv_over_point(
point:Vector3,
mesh:TriangleMesh,
distance: float,
align_start_facing = True,
use_cw_normal:None|bool = None,
) -> AUV:
normal = get_surface_normal(mesh, point, use_cw_normal)
position = point + normal * distance
facing = Vector3(1, 0, 0)
if align_start_facing:
facing = normalize(Vector3(-normal[1], normal[0], 0)) # tangent w/ no z

return AUV(position, facing, distance)

def place_auvs_over_points(
points:Vector3Array,
meshs:TriangleMesh|list[TriangleMesh],
distance: float,
align_start_facing = True,
use_cw_normal:None|bool = None,
) -> list[AUV]:
if not isinstance(meshs, list):
meshs = [meshs] * len(points)
return [
place_auv_over_point(point, mesh, distance, align_start_facing, use_cw_normal)
for point, mesh in zip(points, meshs)
]

def generate_path(
mesh:TriangleMesh,
auv:AUV,
travel_distance:float,
samples:int,
back_steps:int|None = None,
) -> AUVPath:
# Step 1: Backstep
if not (back_steps is None):
auv_copy = AUV(auv.start_pos, auv.start_facing)
auv_copy.ideal_distance = auv.ideal_distance
back_dist = travel_distance / (samples - 1) * back_steps
auv = step_back_auv(mesh, auv_copy, back_dist, back_steps)
# Step 2: Get path
return AUVPathGen(mesh, auv).get_path(travel_distance, samples)

def generate_paths(
meshs:TriangleMesh|list[TriangleMesh],
auvs:list[AUV],
travel_distance:float,
samples:int,
back_steps:int|None|list[int|None] = None,
) -> list[AUVPath]:
if not isinstance(meshs, list):
meshs = [meshs] * len(auvs)
if not isinstance(back_steps, list):
back_steps = [back_steps] * len(auvs)
return [
generate_path(mesh, auv, travel_distance, samples, bs)
for mesh, auv, bs in zip(meshs, auvs, back_steps)
]

def step_back_auv(
mesh:TriangleMesh,
auv:AUV,
travel_distance:float,
steps:int,
) -> AUV:
auv.start_facing = auv.start_facing * -1
path_gen = AUVPathGen(mesh, auv)
back_start = path_gen.get_path(travel_distance, steps + 1).rays[-1]
auv.start_facing = auv.start_facing * -1 # undo changes

return AUV(back_start.origin, back_start.direction * -1, auv.ideal_distance)

def scan_path(
scanner:SideScan,
path: AUVPath,
min_angle:float,
max_angle:float,
angle_reselution:int,
silent:bool = False,
process:Callable[[RawScanReading], ScanReading] = ScanReading
) -> list[ScanReading]:
readings:list[ScanReading] = []
for n, orientation in enumerate(path.rays):
rays = SideScan.generate_rays(orientation, min_angle, max_angle, angle_reselution)
if not silent:
print(n + 1, len(path.rays), sep='/', end=' \r')
readings.append(process(scanner.scan_rays(rays)))
return readings
def scan_paths(
scanners:SideScan|list[SideScan],
paths: list[AUVPath],
min_angle:float,
max_angle:float,
angle_reselution:int,
silent:bool = False,
process:Callable[[RawScanReading], ScanReading] = ScanReading
) -> list[list[ScanReading]]:
if not isinstance(scanners, list):
scanners = [scanners] * len(paths)
return [
scan_path(scanner, path, min_angle, max_angle, angle_reselution, silent, process)
for scanner, path in zip(scanners, paths)
]
38 changes: 38 additions & 0 deletions Generate/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import numpy as np

from CustomTypes import Ray, Vector3
from Raytrace import TriangleMesh


def normalize(v:Vector3) -> Vector3:
return v / float(np.sqrt(v.dot(v)))

def get_surface_normal(
mesh:TriangleMesh,
point:Vector3,
use_cw_normal:None|bool = None,
):
# Step 1: Find the triangle at the point
# NOTE: Using the closest centroid is not necesarily correct
# But its a decent aproximation in most cases
min_ind = find_closest_centroid_ind(mesh, point)
tri = mesh.triangles[min_ind]
if tri is None: raise ValueError(f"sample_ray could not find surface at point {point}")
# Step 2: Find the normal at the triangle
normal = np.cross(tri[1] - tri[0], tri[2] - tri[0]).view(Vector3)
normal = normalize(normal)
if use_cw_normal:
normal = normal * -1
if use_cw_normal is None: # Guess at correct normal direction
if (normal[1] < 0) != (point[1] < 0):
normal = normal * -1 # Point normal away from xz plane
return normal

def find_closest_centroid_ind(
mesh:TriangleMesh,
point:Vector3,
) -> int:
# NOTE: Could (probably) be sped up with scipy.spatial.KDTree
rel_pos = mesh.centroids - point
sqr_dist = np.sum(rel_pos*rel_pos, axis=-1)
return int(np.argmin(sqr_dist))
122 changes: 122 additions & 0 deletions Generate/hull.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from copy import deepcopy
from typing import overload

import numpy as np
from CustomTypes import Vector3, TriangleArray, Vector3Array
from Raytrace import TriangleMesh, CompositeMesh, BVHMesh
import ShipGen as SG
import random

def generate_hulls(
find_best:int,
out_of:int,
x_length:float|None = None,
characteristics:SG.ShipCharacteristics|None = None,
silent:bool = False,
) -> list[BVHMesh]:
# Step 1: Generate the hulls
raw_hulls = SG.generate_hulls(find_best, out_of, characteristics)
# Step 2: Apply scale
if not (x_length is None):
for hull in raw_hulls:
SG.normalize_x(hull, x_length)
# Step 3: Convert them to BVHs
triangles_list = SG.mesh_list_to_triangles(raw_hulls)
hulls:list[BVHMesh] = []
for n, triangles in enumerate(triangles_list):
if not silent:
print(f'Building BVH {n} / {len(triangles_list)}')
hulls.append(BVHMesh(triangles, min_node_size = 100))
return hulls

def place_anomaly_at_point(
hull:TriangleMesh,
anomaly:TriangleMesh,
point:Vector3,
) -> CompositeMesh:
anomaly = deepcopy(anomaly)
# TODO: A proper addition method should be implemented
# HACK: Will only work fully for regular TriangleMesh
if type(anomaly) != TriangleMesh:
raise NotImplementedError('place_anomaly_at_point only works for anomalies of type TriangleMesh (no subclasses)')
anomaly.triangles = TriangleArray(anomaly.triangles + point)

return CompositeMesh([hull, anomaly])
def place_anomalies_at_points(
hull:TriangleMesh,
anomalies:list[TriangleMesh],
points:Vector3Array,
) -> list[CompositeMesh]:
return [
place_anomaly_at_point(hull, anomaly, point)
for anomaly, point in zip(anomalies, points)
]

def generate_point_on_hull_anywhere(
hull:TriangleMesh,
) -> Vector3:
# NOTE: not a perfectly even distribution
triangle_ind = random.randint(0, len(hull.triangles))
triangle = hull.triangles[triangle_ind]
rand_x = random.random()
rand_y = random.random()
if rand_y > 1 - rand_x: # outside triangle
rand_x = 1 - rand_x
rand_y = 1 - rand_y
off_x = (triangle[1] - triangle[0]) * rand_x
off_y = (triangle[2] - triangle[0]) * rand_y
return triangle[0] + off_x + off_y
def generate_point_on_hull_in(
hull:TriangleMesh,
min_bound:Vector3,
max_bound:Vector3,
attempts:int = 1000
) -> Vector3:
for _ in range(attempts):
point = generate_point_on_hull_anywhere(hull)
if np.all(min_bound <= point) and np.all(point <= max_bound):
return point
raise RuntimeError(f'Was not able to generate valid point within {attempts} attempts')

def generate_point_on_hull(
hull:TriangleMesh,
min_bound:Vector3|None = None,
max_bound:Vector3|None = None,
attempts:int = 1000
) -> Vector3:
if min_bound is None or max_bound is None:
return generate_point_on_hull_anywhere(hull)
else:
return generate_point_on_hull_in(hull, min_bound, max_bound, attempts)
def generate_points_on_hull(
count:int,
hull:TriangleMesh,
min_bound:Vector3|None = None,
max_bound:Vector3|None = None,
attempts:int = 1000
) -> Vector3Array:
return Vector3Array([
generate_point_on_hull(hull, min_bound, max_bound, attempts)
for _ in range(count)])

def generate_vertical_bounds(
hull:TriangleMesh,
cut_percentage:float,
) -> tuple[Vector3, Vector3]:
min_z = np.min(hull.triangles.nd[:,2])
max_z = np.max(hull.triangles.nd[:,2])
height = max_z - min_z
min_bound = Vector3(-np.inf, -np.inf, min_z + height * cut_percentage)
max_bound = Vector3( np.inf, np.inf, max_z - height * cut_percentage)
return min_bound, max_bound

def load_anomalies(paths:str|list[str]) -> list[TriangleMesh]:
if isinstance(paths, str): return load_anomalies([paths])
return [TriangleMesh(stl_path = path) for path in paths]
def pick_anomalies(
count:int,
anomalies:list[TriangleMesh],
weights:list[float],
) -> list[TriangleMesh]:
#TODO: Implement weights
return [random.choice(anomalies) for _ in range(count)]
Loading