From 11ff2747081dc07fd8ab2a47731380c73bafcced Mon Sep 17 00:00:00 2001 From: JoeBell Date: Sun, 2 Mar 2025 20:49:20 -0500 Subject: [PATCH 1/5] Created RawScanReading class Split off part ScanReading into RawScanReading to make modifying the raw distance values easier --- ExampleMultipleSonarReadings.ipynb | 2 +- ExampleSingleSonarReading.ipynb | 9 ++--- Raytrace/PlotRays.py | 10 +++--- Raytrace/SideScan.py | 55 +++++++++++++++++------------- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/ExampleMultipleSonarReadings.ipynb b/ExampleMultipleSonarReadings.ipynb index f45c0f0..b453a68 100644 --- a/ExampleMultipleSonarReadings.ipynb +++ b/ExampleMultipleSonarReadings.ipynb @@ -51,7 +51,7 @@ "readings:list[ScanReading] = []\n", "for n,rays in enumerate(rays_list):\n", " print(n + 1, len(rays_list), sep='/', end='\\r')\n", - " readings.append(SideScan(mesh).scan_rays(rays))\n", + " readings.append(ScanReading(SideScan(mesh).scan_rays(rays)))\n", "\n", "print()\n", "\n", diff --git a/ExampleSingleSonarReading.ipynb b/ExampleSingleSonarReading.ipynb index 5787eb4..914a918 100644 --- a/ExampleSingleSonarReading.ipynb +++ b/ExampleSingleSonarReading.ipynb @@ -8,7 +8,7 @@ "source": [ "from Raytrace.TriangleMesh import Ray\n", "from Raytrace.BVHMesh import BVHMesh\n", - "from Raytrace.SideScan import SideScan\n", + "from Raytrace.SideScan import SideScan, ScanReading\n", "import numpy as np\n", "import time" ] @@ -43,10 +43,11 @@ "\n", "rays = SideScan.generate_rays(orientation, min_angle, max_angle, sample_ray_count)\n", "\n", - "reading = SideScan(mesh).scan_rays(rays)\n", + "raw_reading = SideScan(mesh).scan_rays(rays)\n", + "reading = ScanReading(raw_reading)\n", "\n", "print('Triangles:', mesh.triangles.shape[0])\n", - "reading.print_summary()" + "raw_reading.print_summary()" ] }, { @@ -56,7 +57,7 @@ "outputs": [], "source": [ "import plotly.graph_objs as go\n", - "from PlotRays import plot_mesh, plot_rays, plot_reading" + "from Raytrace.PlotRays import plot_mesh, plot_rays, plot_reading" ] }, { diff --git a/Raytrace/PlotRays.py b/Raytrace/PlotRays.py index bb7e01a..745c271 100644 --- a/Raytrace/PlotRays.py +++ b/Raytrace/PlotRays.py @@ -13,8 +13,8 @@ def plot_mesh(mesh:TriangleMesh, **kwargs) -> go.Mesh3d: return go.Mesh3d(x=x, y=y, z=z, i=i, j=j, k=k, **kwargs) def plot_rays(reading:ScanReading, **kwargs) -> go.Scatter3d: - origin = reading.origins[reading.finite] - inters = reading.intersections[reading.finite] + origin = reading.raw.origins[reading.raw.finite] + inters = reading.raw.intersections[reading.raw.finite] x, y, z = np.stack((origin, inters, origin)).swapaxes(0,1).reshape((-1,3)).swapaxes(0,1) return go.Scatter3d(x=x, y=y, z=z, **kwargs) @@ -32,12 +32,12 @@ def plot_reading(reading:ScanReading) -> go.Figure: 'mode': 'markers', 'name': 'Raw', 'showlegend': True, - 'x': reading.distances[reading.finite], - 'y': np.zeros(reading.intersection_count), + 'x': reading.raw.distances[reading.raw.finite], + 'y': np.zeros(reading.raw.intersection_count), }) ]) def plot_intersections(reading:ScanReading, **kwargs) -> go.Scatter3d: - inters = reading.intersections[reading.finite] + inters = reading.raw.intersections[reading.raw.finite] x, y, z = inters.reshape((-1,3)).swapaxes(0,1) return go.Scatter3d(x=x, y=y, z=z, **kwargs) diff --git a/Raytrace/SideScan.py b/Raytrace/SideScan.py index 598a084..54d1df4 100644 --- a/Raytrace/SideScan.py +++ b/Raytrace/SideScan.py @@ -1,8 +1,9 @@ from Raytrace.TriangleMesh import TriangleMesh, Ray +from typing import Self import numpy as np import time -class ScanReading: +class RawScanReading: distances:np.ndarray intersections:np.ndarray origins:np.ndarray @@ -10,53 +11,61 @@ class ScanReading: finite:np.ndarray intersection_count:int + start_time:float = -1 + end_time:float = -1 + + def __init__(self, distances:np.ndarray, rays:list[Ray]): + old_error_state = np.seterr(all='ignore') + self.distances = distances + self.origins = np.array([r.origin for r in rays]) + self.directions = np.array([r.direction for r in rays]) + self.intersections = self.origins + self.directions * self.distances.reshape((-1,1)) + self.finite = np.isfinite(self.distances) + self.intersection_count = np.count_nonzero(self.finite) + np.seterr(**old_error_state) + + def print_summary(self) -> None: + print('Intersections:', self.intersection_count , '/', len(self.distances)) + if self.end_time == -1 or self.start_time == -1: return + print('Time:', self.end_time - self.start_time, 'seconds') + print('Speed:', len(self.distances)/(self.end_time - self.start_time), 'rays/seconds') + +class ScanReading: + raw:RawScanReading + smooth_dist:float result_reselution:int min_dist:float max_dist:float result:np.ndarray - - start_time:float = -1 - end_time:float = -1 def __init__(self, - distances:np.ndarray, rays:list[Ray], + raw_reading:RawScanReading, smooth_dist:float = 0.05, result_reselution:int = 1000, min_dist:float = 0, max_dist:float = 2 ): old_error_state = np.seterr(all='ignore') - self.distances = distances - self.origins = np.array([r.origin for r in rays]) - self.directions = np.array([r.direction for r in rays]) - self.intersections = self.origins + self.directions * self.distances.reshape((-1,1)) - self.finite = np.isfinite(self.distances) - self.intersection_count = np.count_nonzero(self.finite) - + self.raw = raw_reading self.smooth_dist = smooth_dist self.result_reselution = result_reselution self.min_dist = min_dist self.max_dist = max_dist - self.convert_distances() + self.process_raw() np.seterr(**old_error_state) - def convert_distances(self) -> None: + def process_raw(self) -> None: old_error_state = np.seterr(all='ignore') - norm = (self.distances[self.finite] - self.min_dist) / (self.max_dist - self.min_dist) + norm = (self.raw.distances[self.raw.finite] - self.min_dist) / (self.max_dist - self.min_dist) ldist = norm - (np.arange(0,1,1/self.result_reselution) + 0.5/self.result_reselution).reshape((-1,1)) smooth_val = self.smooth_dist / (self.max_dist - self.min_dist) - lval = np.pow(np.maximum(0, np.square(smooth_val) - np.square(ldist)),3) / (32/35*smooth_val**7) / len(self.distances) + lval = np.pow(np.maximum(0, np.square(smooth_val) - np.square(ldist)),3) / (32/35*smooth_val**7) / len(self.raw.distances) self.result = np.sum(lval, axis = 1) np.seterr(**old_error_state) - def print_summary(self) -> None: - print('Intersections:', self.intersection_count , '/', len(self.distances)) - if self.end_time == -1 or self.start_time == -1: return - print('Time:', self.end_time - self.start_time, 'seconds') - print('Speed:', len(self.distances)/(self.end_time - self.start_time), 'rays/seconds') class SideScan: mesh:TriangleMesh smooth_dist:float @@ -65,7 +74,7 @@ def __init__(self, mesh:TriangleMesh, smooth_dist:float = 0.05, result_reselutio self.mesh = mesh self.smooth_dist = smooth_dist self.result_reselution = result_reselution - def scan_rays(self, rays:list[Ray]) -> ScanReading: + def scan_rays(self, rays:list[Ray]) -> RawScanReading: distances = np.empty((len(rays),), np.float32) start_time = time.time() @@ -73,7 +82,7 @@ def scan_rays(self, rays:list[Ray]) -> ScanReading: distances[n] = self.mesh.raytrace(ray) end_time = time.time() - out = ScanReading(distances, rays) + out = RawScanReading(distances, rays) out.start_time = start_time out.end_time = end_time From b1516eeccbf92cae4afe3a16f78aa022617d4d3e Mon Sep 17 00:00:00 2001 From: JoeBell Date: Sun, 2 Mar 2025 20:52:30 -0500 Subject: [PATCH 2/5] Added combine_min function Added function to take the min of two raw readings --- Raytrace/SideScan.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Raytrace/SideScan.py b/Raytrace/SideScan.py index 54d1df4..3896246 100644 --- a/Raytrace/SideScan.py +++ b/Raytrace/SideScan.py @@ -23,6 +23,18 @@ def __init__(self, distances:np.ndarray, rays:list[Ray]): self.finite = np.isfinite(self.distances) self.intersection_count = np.count_nonzero(self.finite) np.seterr(**old_error_state) + def combine_min(self, other:Self) -> Self: + if len(self.distances) != len(other.distances): + raise ValueError("Cannot combine two readings of different sizes") + if np.any(self.origins != other.origins) or np.any(self.directions != other.directions): + pass # TODO: add warning + new_distances = self.distances.copy() + other_smaller = new_distances > other.distances + new_distances[other_smaller] = other.distances[other_smaller] + + # HACK: new rays should be generated based on which distance was closer + new_rays = [Ray(i, j) for i, j in zip(self.directions, self.origins)] + return type(self)(new_distances, new_rays) def print_summary(self) -> None: print('Intersections:', self.intersection_count , '/', len(self.distances)) From b1a383f702f9ad3bff17ddb599a247e32b06d8c7 Mon Sep 17 00:00:00 2001 From: JoeBell Date: Sun, 2 Mar 2025 21:38:47 -0500 Subject: [PATCH 3/5] Created the CompositeMesh class Created a new class that allows you to combine multiple meshes --- Raytrace/CompositeMesh.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Raytrace/CompositeMesh.py diff --git a/Raytrace/CompositeMesh.py b/Raytrace/CompositeMesh.py new file mode 100644 index 0000000..f1dc5b6 --- /dev/null +++ b/Raytrace/CompositeMesh.py @@ -0,0 +1,11 @@ +import numpy as np +from Raytrace.TriangleMesh import TriangleMesh, Ray + +class CompositeMesh(TriangleMesh): + meshes: list[TriangleMesh] + def __init__(self, meshes:list[TriangleMesh]) -> None: + self.meshes = meshes + self.triangles = np.concatenate([mesh.triangles for mesh in self.meshes]) + def raytrace(self, ray:Ray) -> float: + distances = [mesh.raytrace(ray) for mesh in self.meshes] + return min(distances, default = np.inf) \ No newline at end of file From f5f9fb1747d05e6f833a8dd6db13e47fd702403b Mon Sep 17 00:00:00 2001 From: JoeBell Date: Sun, 2 Mar 2025 22:45:11 -0500 Subject: [PATCH 4/5] Added save/load Added methods to save and load scan results and meshes using pickle --- Raytrace/SideScan.py | 25 +++++++++++++++++++++++++ Raytrace/TriangleMesh.py | 13 ++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Raytrace/SideScan.py b/Raytrace/SideScan.py index 3896246..83db3b6 100644 --- a/Raytrace/SideScan.py +++ b/Raytrace/SideScan.py @@ -2,6 +2,7 @@ from typing import Self import numpy as np import time +import pickle class RawScanReading: distances:np.ndarray @@ -42,6 +43,16 @@ def print_summary(self) -> None: print('Time:', self.end_time - self.start_time, 'seconds') print('Speed:', len(self.distances)/(self.end_time - self.start_time), 'rays/seconds') + def save(self, filename, override = False) -> None: + with open(filename, 'wb' if override else 'xb') as file: + pickle.dump(self, file) + @staticmethod + def load(filename) -> 'RawScanReading': + with open(filename, 'rb') as file: + obj = pickle.load(file) + if isinstance(obj, RawScanReading): + return obj + raise TypeError(f'The object saved in {filename} is type {type(obj)} not RawScanReading') class ScanReading: raw:RawScanReading @@ -78,6 +89,20 @@ def process_raw(self) -> None: self.result = np.sum(lval, axis = 1) np.seterr(**old_error_state) + + def save(self, filename, override = False) -> None: + with open(filename, 'wb' if override else 'xb') as file: + pickle.dump(self, file) + @staticmethod + def load(filename) -> 'ScanReading': + with open(filename, 'rb') as file: + obj = pickle.load(file) + if isinstance(obj, ScanReading): + return obj + raise TypeError(f'The object saved in {filename} is type {type(obj)} not ScanReading') + + + class SideScan: mesh:TriangleMesh smooth_dist:float diff --git a/Raytrace/TriangleMesh.py b/Raytrace/TriangleMesh.py index 950671f..d1b8898 100644 --- a/Raytrace/TriangleMesh.py +++ b/Raytrace/TriangleMesh.py @@ -1,5 +1,5 @@ import numpy as np - +import pickle class Ray: direction:np.ndarray @@ -115,3 +115,14 @@ def batch_triangle_ray_intersection(triangle_array, ray:Ray, epsilon = 1e-10) -> np.seterr(**old_error_state) return t + + def save(self, filename, override = False) -> None: + with open(filename, 'wb' if override else 'xb') as file: + pickle.dump(self, file) + @staticmethod + def load(filename) -> 'TriangleMesh': + with open(filename, 'rb') as file: + obj = pickle.load(file) + if isinstance(obj, TriangleMesh): + return obj + raise TypeError(f'The object saved in {filename} is type {type(obj)} not TriangleMesh') \ No newline at end of file From 446bc2d18f2afe2ff2f974400111392fac0ec55b Mon Sep 17 00:00:00 2001 From: JoeBell Date: Sun, 2 Mar 2025 22:53:58 -0500 Subject: [PATCH 5/5] Made typing more flexible Now plots that use a raw reading will accept either a RawScanReading or a ScanReading --- Raytrace/PlotRays.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Raytrace/PlotRays.py b/Raytrace/PlotRays.py index 745c271..6fa0dd2 100644 --- a/Raytrace/PlotRays.py +++ b/Raytrace/PlotRays.py @@ -1,5 +1,5 @@ from Raytrace.TriangleMesh import TriangleMesh -from Raytrace.SideScan import ScanReading +from Raytrace.SideScan import ScanReading, RawScanReading import numpy as np try: import plotly.graph_objs as go # type: ignore @@ -12,9 +12,12 @@ def plot_mesh(mesh:TriangleMesh, **kwargs) -> go.Mesh3d: i, j, k = [list(range(i,x.shape[0],3)) for i in range(3)] return go.Mesh3d(x=x, y=y, z=z, i=i, j=j, k=k, **kwargs) -def plot_rays(reading:ScanReading, **kwargs) -> go.Scatter3d: - origin = reading.raw.origins[reading.raw.finite] - inters = reading.raw.intersections[reading.raw.finite] +def plot_rays(reading:ScanReading|RawScanReading, **kwargs) -> go.Scatter3d: + if isinstance(reading, ScanReading): + return plot_rays(reading.raw, **kwargs) + + origin = reading.origins[reading.finite] + inters = reading.intersections[reading.finite] x, y, z = np.stack((origin, inters, origin)).swapaxes(0,1).reshape((-1,3)).swapaxes(0,1) return go.Scatter3d(x=x, y=y, z=z, **kwargs) @@ -36,13 +39,16 @@ def plot_reading(reading:ScanReading) -> go.Figure: 'y': np.zeros(reading.raw.intersection_count), }) ]) -def plot_intersections(reading:ScanReading, **kwargs) -> go.Scatter3d: - inters = reading.raw.intersections[reading.raw.finite] +def plot_intersections(reading:ScanReading|RawScanReading, **kwargs) -> go.Scatter3d: + if isinstance(reading, ScanReading): + return plot_intersections(reading.raw, **kwargs) + + inters = reading.intersections[reading.finite] x, y, z = inters.reshape((-1,3)).swapaxes(0,1) return go.Scatter3d(x=x, y=y, z=z, **kwargs) -def plot_intersections_list(readings:list[ScanReading], **kwargs) -> list[go.Scatter3d]: +def plot_intersections_list(readings:list[ScanReading|RawScanReading], **kwargs) -> list[go.Scatter3d]: return [plot_intersections(reading, **kwargs) for reading in readings] def plot_readings_heatmap(readings) -> go.Figure: