From 8451cae9b3b19aebfc4525456a058d21da4ddb20 Mon Sep 17 00:00:00 2001 From: Louis Freeland-Haynes <66101835+louisfh@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:17:22 -0400 Subject: [PATCH 1/4] add SpatialGrid class --- opensoundscape/localization.py | 71 ++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/opensoundscape/localization.py b/opensoundscape/localization.py index fca57cad..5e8b1d06 100644 --- a/opensoundscape/localization.py +++ b/opensoundscape/localization.py @@ -282,6 +282,77 @@ def _localize_after_cross_correlation(self, localization_algorithm): return self.location_estimate +from scipy.spatial import ConvexHull + + +class SpatialGrid: + """ + Class for creating a grid of points for localizing sound events with methods that use grid search. + """ + + def __init__(self, recorder_positions, resolution=1, margin=0): + """ + Initialize a SpatialGrid object + + Args: + recorder_positions: list of [x,y] or [x,y,z] positions of each recorder in meters + resolution: resolution of the grid in meters. Default is 1. + margin: margin around the convex hull of the grid in meters. Will only attempt to localize events that are inside the grid + margin. + A negative margin will shrink the grid. Default is 0. + + """ + self.recorder_positions = np.array(recorder_positions) + self.resolution = resolution + self.margin = margin + self.dimensions = self.recorder_positions.shape[1] + self.convex_hull = ConvexHull(self.recorder_positions) + self.grid = self._make_grid() + + def _make_grid(self): + """ + Create a grid of points for localizing sound events + + Returns: + grid: a list of [x,y] or [x,y,z] positions of each point in the grid + """ + + # make a grid of all the points between the min and max possible coordinates of the recorder positions + x = np.arange( + np.floor(np.min(self.recorder_positions[:, 0])) - self.margin, + np.ceil(np.max(self.recorder_positions[:, 0])) + self.margin, + self.resolution, + ) + y = np.arange( + np.floor(np.min(self.recorder_positions[:, 1])) - self.margin, + np.ceil(np.max(self.recorder_positions[:, 1])) + self.margin, + self.resolution, + ) + + if self.dimensions == 2: + grid = np.array(np.meshgrid(x, y)).T.reshape(-1, 2) + else: + z = np.arange( + np.floor(np.min(self.recorder_positions[:, 2])) - self.margin, + np.ceil(np.max(self.recorder_positions[:, 2])) + self.margin, + self.resolution, + ) + grid = np.array(np.meshgrid(x, y, z)).T.reshape(-1, 3) + + # only keep the points that are inside the convex hull of the recorder positions + # self.hull is the ConvexHull object of the recorder positions + # self.hull.equations is the equation of the hyperplane of each face of the convex hull + # apply the equation of each face to the grid points to check if they are inside the convex hull + mask = np.all( + self.convex_hull.equations[:, :-1].dot(grid.T) + + self.convex_hull.equations[:, -1][:, None] + <= self.margin, + axis=0, + ) + grid = grid[mask] + + return grid + + class SynchronizedRecorderArray: """ Class with utilities for localizing sound events from array of recorders From 4ecf2cdbe3f26bfd41d121b97aa8894b94a02e5a Mon Sep 17 00:00:00 2001 From: Louis Freeland-Haynes <66101835+louisfh@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:27:01 -0400 Subject: [PATCH 2/4] spatialgrid convex hull implementation --- opensoundscape/localization.py | 15 +++++++-------- tests/test_localization.py | 8 ++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/opensoundscape/localization.py b/opensoundscape/localization.py index 5e8b1d06..ec4d8b37 100644 --- a/opensoundscape/localization.py +++ b/opensoundscape/localization.py @@ -8,7 +8,7 @@ from opensoundscape import audio from opensoundscape.utils import cast_np_to_native from scipy.optimize import least_squares - +from scipy.spatial import ConvexHull # define defaults for physical constants SPEED_OF_SOUND = 343 # default value in meters per second @@ -282,9 +282,6 @@ def _localize_after_cross_correlation(self, localization_algorithm): return self.location_estimate -from scipy.spatial import ConvexHull - - class SpatialGrid: """ Class for creating a grid of points for localizing sound events with methods that use grid search. @@ -305,7 +302,6 @@ def __init__(self, recorder_positions, resolution=1, margin=0): self.resolution = resolution self.margin = margin self.dimensions = self.recorder_positions.shape[1] - self.convex_hull = ConvexHull(self.recorder_positions) self.grid = self._make_grid() def _make_grid(self): @@ -338,14 +334,17 @@ def _make_grid(self): ) grid = np.array(np.meshgrid(x, y, z)).T.reshape(-1, 3) + hull = ConvexHull(self.recorder_positions) # only keep the points that are inside the convex hull of the recorder positions # self.hull is the ConvexHull object of the recorder positions # self.hull.equations is the equation of the hyperplane of each face of the convex hull # apply the equation of each face to the grid points to check if they are inside the convex hull + eps = ( + margin / 100 + ) # add a small epsilon to the margin to ensure that points on the edge of the convex hull are included mask = np.all( - self.convex_hull.equations[:, :-1].dot(grid.T) - + self.convex_hull.equations[:, -1][:, None] - <= self.margin, + hull.equations[:, :-1].dot(grid.T) + hull.equations[:, -1][:, None] + <= self.margin + eps, axis=0, ) grid = grid[mask] diff --git a/tests/test_localization.py b/tests/test_localization.py index 76a649bb..ffd13da5 100644 --- a/tests/test_localization.py +++ b/tests/test_localization.py @@ -414,3 +414,11 @@ def test_localize_too_few_receivers(LOCA_2021_aru_coords, LOCA_2021_detections): ) assert len(localized_events) == 0 assert len(unlocalized_events) == 6 + + +def test_SpatialGrid(): + # Verify that the SpatialGrid class creates the correct grid + receivers = np.array([[0, 0], [0, 10], [10, 0], [10, 10]]) + grid = localization.SpatialGrid(receivers, resolution=1, margin=0) + # verify there are the right number of grid points + assert len(grid.grid) == 100 From 550cd7a318d6dd69a7fa0dcbfbb2672c0ab90821 Mon Sep 17 00:00:00 2001 From: Louis Freeland-Haynes <66101835+louisfh@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:28:53 -0400 Subject: [PATCH 3/4] spatialGrid eps change --- opensoundscape/localization.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/opensoundscape/localization.py b/opensoundscape/localization.py index ec4d8b37..4b1e19d6 100644 --- a/opensoundscape/localization.py +++ b/opensoundscape/localization.py @@ -339,9 +339,8 @@ def _make_grid(self): # self.hull is the ConvexHull object of the recorder positions # self.hull.equations is the equation of the hyperplane of each face of the convex hull # apply the equation of each face to the grid points to check if they are inside the convex hull - eps = ( - margin / 100 - ) # add a small epsilon to the margin to ensure that points on the edge of the convex hull are included + eps = 1e-6 + # add a small epsilon to the margin to ensure that points on the edge of the convex hull are included mask = np.all( hull.equations[:, :-1].dot(grid.T) + hull.equations[:, -1][:, None] <= self.margin + eps, From dbd8c7e0d89b3d1dcc4809bf223d07518e0e5737 Mon Sep 17 00:00:00 2001 From: Louis Freeland-Haynes <66101835+louisfh@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:36:08 -0400 Subject: [PATCH 4/4] fiddle --- opensoundscape/localization.py | 42 ++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/opensoundscape/localization.py b/opensoundscape/localization.py index f2de1952..fc4c36ac 100644 --- a/opensoundscape/localization.py +++ b/opensoundscape/localization.py @@ -87,6 +87,7 @@ def __init__( # Initialize attributes self.tdoas = None # time delay at each receiver + self.cc_matrix = None # cross correlation matrix self.cc_maxs = None # max of cross correlation for each time delay self.location_estimate = None # cartesian location estimate in meters self.distance_residuals = None # distance residuals in meters @@ -341,6 +342,10 @@ def _localize_after_cross_correlation(self, localization_algorithm): return self.location_estimate + @property + def cc_maxs(self): + return self._cc_maxs + @classmethod def from_dict(cls, dictionary): """Recover SpatialEvent from dictionary, eg loaded from json""" @@ -445,25 +450,38 @@ def __call__(self, file, start_time): class SpatialGrid: """ - Class for creating a grid of points for localizing sound events with methods that use grid search. + Class for creating a grid of points for localizing sound events with methods that use a grid search approach. """ - def __init__(self, recorder_positions, resolution=1, margin=0): + def __init__( + self, + recorder_positions, + sample_rate, + resolution=1, + margin=0, + speed_of_sound=SPEED_OF_SOUND, + ): """ Initialize a SpatialGrid object Args: recorder_positions: list of [x,y] or [x,y,z] positions of each recorder in meters + sample_rate: sample rate of the audio in Hz. resolution: resolution of the grid in meters. Default is 1. margin: margin around the convex hull of the grid in meters. Will only attempt to localize events that are inside the grid + margin. A negative margin will shrink the grid. Default is 0. + speed_of_sound: speed of sound in meters per second. Default is 343 m/s. + """ self.recorder_positions = np.array(recorder_positions) + self.sample_rate = sample_rate self.resolution = resolution self.margin = margin self.dimensions = self.recorder_positions.shape[1] + self.speed_of_sound = speed_of_sound self.grid = self._make_grid() + self.tdoa_grid = self._make_tdoa_grid() def _make_grid(self): """ @@ -511,6 +529,26 @@ def _make_grid(self): return grid + def _make_tdoa_grid(self): + """ + Create an array containing the expected TDOAs for every recorder, for every recorder position in the grid. + + Returns: + delays_grid: a grid, where each point has the matrix of TDOAs for each recorder position + """ + # calculate the time delays for each point in the grid + delays_grid = [] + for point in self.grid: + delays = ( + np.linalg.norm(self.recorder_positions - point, axis=1) + / self.speed_of_sound + ) + # now make a matrix of all the relative delays between each pair of recorders + # this is a matrix where the i,j-th element is the delay between recorder i and recorder j. With recorder i as the reference. + delays = delays - delays[:, None] + delays_grid.append(delays) + return np.array(delays_grid) + class SynchronizedRecorderArray: """