Skip to content

Commit

Permalink
Dataset generation (#4)
Browse files Browse the repository at this point in the history
* Squashed commit of the following:

commit 4acc015
Author: JoeBell <joe@onmyhonor.com>
Date:   Sun Mar 9 18:00:03 2025 -0400

    Added normalize function

    Added function to normalize the scale of meshes

commit e685ab2
Merge: 968cbac cc38025
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 21:28:20 2025 -0500

    Merge branch 'main' into shipgen-integration

commit 968cbac
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 20:58:29 2025 -0500

    Added conversion

    Added helper functions to convert stl.Mesh's to triangle arrays

commit 13a589f
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 20:47:02 2025 -0500

    Cleaned up Generate

commit 22876e9
Merge: f92334b adb4ab8
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 20:08:58 2025 -0500

    Merge branch 'main' into shipgen-integration

commit f92334b
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 19:39:31 2025 -0500

    Renamed parameters

commit f261895
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 19:37:13 2025 -0500

    Stopped warning message

commit bf90105
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 19:26:12 2025 -0500

    Fixed unused parameter

commit f79dbc1
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 19:14:46 2025 -0500

    Removed need to save stl's

commit c640f6c
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 19:05:02 2025 -0500

    Improved typing

commit 19cf09b
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 18:48:43 2025 -0500

    Added hull generator

commit e8845c8
Author: JoeBell <joe@onmyhonor.com>
Date:   Tue Mar 4 16:18:36 2025 -0500

    Uploaded streamlined ShipGen

    Uploaded a version of ShipGen modified from https://www.dropbox.com/sh/jg98r425v7ly89l/AAA49uMr7_mhaVmRDrPq0NU_a?dl=0 with all non-essential files removed

* Added Custom Types

Everything runs abut half as fast but it is much less error prone and more readable

* Changed Raytrace Imports

Change the Raytrace library so that mesh classes can be imported directly

* Restructured Raytrace Library

Moved the mesh classes into their own folder

* Fixed Typo Related Bug

Passing a Vector3 to the Vector3 constructor no longer causes an error

* Redid Custom Types

Custom types are now better integrated with numpy to get speed back to around where they were (well, 2/3rds the speed but whos counting)

* Added Triangle output to Raytrace

Raytracing functions now return a distance and a triangle. Subclasses now overwrite the raytrace_as_tri method

* Added Explicate Reexports

Added reexports to AUVSim

* Created Generate Folder

* Created Scan Utilities

Added functions to help position AUVs and scan paths

* Made CompositeMesh.triangles getter

Modified CompositeMesh to make instantiating large quantities less intensive

* Renamed AUV Utilities

* Added Parallelism to AUV Utilities

Made functions in the AUV utilities that take arrays in addition to their single value counter-parts

* Added Hull Utilities

Added utilities for generating hulls and placing anomalies

* Created Outline

Added an outline notebook that walks through how to create items for the dataset

* Fixed Point Randomizer

Fixed bug where random point had a 50% chance of not being on the triangle

* Fixed Surface Normal

* Fixed Outline
  • Loading branch information
jrb20008 authored Mar 25, 2025
1 parent fd0b115 commit 60d436b
Show file tree
Hide file tree
Showing 40 changed files with 4,994 additions and 25 deletions.
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

0 comments on commit 60d436b

Please sign in to comment.