the-bumblebee 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bumblebee/__init__.py +2 -0
- bumblebee/__pycache__/__init__.cpython-312.pyc +0 -0
- bumblebee/__pycache__/__init__.cpython-313.pyc +0 -0
- bumblebee/__pycache__/keyboard.cpython-312.pyc +0 -0
- bumblebee/__pycache__/keyboard.cpython-313.pyc +0 -0
- bumblebee/__pycache__/mouse.cpython-312.pyc +0 -0
- bumblebee/__pycache__/mouse.cpython-313.pyc +0 -0
- bumblebee/ai/__init__.py +2 -0
- bumblebee/ai/__pycache__/__init__.cpython-312.pyc +0 -0
- bumblebee/ai/__pycache__/__init__.cpython-313.pyc +0 -0
- bumblebee/ai/__pycache__/predict.cpython-312.pyc +0 -0
- bumblebee/ai/__pycache__/predict.cpython-313.pyc +0 -0
- bumblebee/ai/__pycache__/rnn.cpython-312.pyc +0 -0
- bumblebee/ai/__pycache__/rnn.cpython-313.pyc +0 -0
- bumblebee/ai/models/README.md +3 -0
- bumblebee/ai/models/bumblebee-c-v1.pth +0 -0
- bumblebee/ai/predict.py +394 -0
- bumblebee/ai/rnn.py +71 -0
- bumblebee/keyboard.py +471 -0
- bumblebee/mouse.py +219 -0
- the_bumblebee-1.0.0.dist-info/LICENSE.md +21 -0
- the_bumblebee-1.0.0.dist-info/METADATA +156 -0
- the_bumblebee-1.0.0.dist-info/RECORD +25 -0
- the_bumblebee-1.0.0.dist-info/WHEEL +5 -0
- the_bumblebee-1.0.0.dist-info/top_level.txt +1 -0
bumblebee/__init__.py
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
bumblebee/ai/__init__.py
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
bumblebee/ai/predict.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch
|
|
5
|
+
from scipy.interpolate import interp1d
|
|
6
|
+
from scipy.ndimage import gaussian_filter1d
|
|
7
|
+
from scipy.special import gamma
|
|
8
|
+
|
|
9
|
+
from bumblebee.ai.rnn import CursorRNN as RNN
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Predictor:
|
|
13
|
+
def __init__(self, model_name: str = "bumblebee-c-v1.pth"):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the prediction model with a pre-trained RNN for generating intermediate path steps.
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
model_name (str): The filename of the pre-trained model to load. Defaults to "bumblebee-c-v1.pth".
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
MAX_COORDINATE (int): Maximum coordinate value supported (e.g., 4096 for 4K resolution).
|
|
22
|
+
MIN_COORDINATE (int): Minimum coordinate value.
|
|
23
|
+
INPUT_DIM (int): Number of input features (start and destination x, y coordinates).
|
|
24
|
+
HIDDEN_DIM (int): Size of the hidden state in the RNN.
|
|
25
|
+
STEPS (int): Number of intermediate steps predicted between the start and destination.
|
|
26
|
+
OUTPUT_DIM (int): Number of output features (2 coordinates for each intermediate step).
|
|
27
|
+
LSTM_LAYERS (int): The number of LSTM layers in the RNN.
|
|
28
|
+
device (torch.device): The device (CUDA, MPS, or CPU) on which the model computations are performed.
|
|
29
|
+
model (RNN): The RNN model instance used for predicting paths.
|
|
30
|
+
model_path (str): The full file path to the pre-trained model file.
|
|
31
|
+
|
|
32
|
+
The constructor loads the pre-trained model, maps it to the appropriate computation device,
|
|
33
|
+
and sets the model to evaluation mode.
|
|
34
|
+
"""
|
|
35
|
+
# Constants for normalization
|
|
36
|
+
self.MAX_COORDINATE = 4096 # allows to use 4k display,taken from training notebook: `rnn-train.ipynb`
|
|
37
|
+
self.MIN_COORDINATE = 0
|
|
38
|
+
|
|
39
|
+
# Constant for RNN model
|
|
40
|
+
self.INPUT_DIM = 4 # Start (x, y) and Destination (x, y) coordinates
|
|
41
|
+
self.HIDDEN_DIM = 512
|
|
42
|
+
self.STEPS = 39 # Number of intemediate steps between start and destination
|
|
43
|
+
self.OUTPUT_DIM = self.STEPS * 2 # 2 coordinates (x, y) for each steps
|
|
44
|
+
self.LSTM_LAYERS = 2
|
|
45
|
+
|
|
46
|
+
self.device = torch.device(
|
|
47
|
+
"cuda"
|
|
48
|
+
if torch.cuda.is_available()
|
|
49
|
+
else "mps" if torch.backends.mps.is_available() else "cpu"
|
|
50
|
+
) # Check for best hardware to run the model on
|
|
51
|
+
|
|
52
|
+
self.model = RNN(
|
|
53
|
+
self.INPUT_DIM,
|
|
54
|
+
self.HIDDEN_DIM,
|
|
55
|
+
self.OUTPUT_DIM,
|
|
56
|
+
num_layers=self.LSTM_LAYERS,
|
|
57
|
+
).to(self.device)
|
|
58
|
+
|
|
59
|
+
self.model_path = os.path.join(os.path.dirname(__file__), "models", model_name)
|
|
60
|
+
if self.device == "cuda":
|
|
61
|
+
self.model.load_state_dict(torch.load(self.model_path))
|
|
62
|
+
else:
|
|
63
|
+
self.model.load_state_dict(
|
|
64
|
+
torch.load(self.model_path, map_location=torch.device("cpu"))
|
|
65
|
+
)
|
|
66
|
+
self.model.eval()
|
|
67
|
+
|
|
68
|
+
def __normalize(self, data: np.ndarray) -> np.ndarray:
|
|
69
|
+
"""
|
|
70
|
+
Normalize coordinates to [0,1] range for model input
|
|
71
|
+
"""
|
|
72
|
+
return (data - self.MIN_COORDINATE) / (
|
|
73
|
+
self.MAX_COORDINATE - self.MIN_COORDINATE
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __denormalize(self, data: np.ndarray) -> np.ndarray:
|
|
77
|
+
"""
|
|
78
|
+
Denormalize coordinates from [0,1] range to original range
|
|
79
|
+
"""
|
|
80
|
+
return data * (self.MAX_COORDINATE - self.MIN_COORDINATE) + self.MIN_COORDINATE
|
|
81
|
+
|
|
82
|
+
def _calculate_distance(self, point1: np.ndarray, point2: np.ndarray) -> float:
|
|
83
|
+
"""
|
|
84
|
+
Calculate the Euclidean distance between two points in 2D space.
|
|
85
|
+
Parameters:
|
|
86
|
+
point1 (np.ndarray): The first point as [x1, y1].
|
|
87
|
+
point2 (np.ndarray): The second point as [x2, y2].
|
|
88
|
+
Returns:
|
|
89
|
+
float: The Euclidean distance between point1 and point2.
|
|
90
|
+
"""
|
|
91
|
+
return float(np.linalg.norm(point1 - point2))
|
|
92
|
+
|
|
93
|
+
def __calculate_angle(
|
|
94
|
+
self, point1: np.ndarray, point2: np.ndarray, point3: np.ndarray
|
|
95
|
+
) -> float:
|
|
96
|
+
"""
|
|
97
|
+
Calculate the angle between three points in 2D space.
|
|
98
|
+
Parameters:
|
|
99
|
+
point1 (np.ndarray): The first point as [x1, y1].
|
|
100
|
+
point2 (np.ndarray): The second point as [x2, y2].
|
|
101
|
+
point3 (np.ndarray): The third point as [x3, y3].
|
|
102
|
+
Returns:
|
|
103
|
+
float: The angle in radians between the line segments formed by point1-point2 and point2-point3.
|
|
104
|
+
"""
|
|
105
|
+
vector1 = point2 - point1
|
|
106
|
+
vector2 = point3 - point2
|
|
107
|
+
|
|
108
|
+
dot_product = np.dot(vector1, vector2)
|
|
109
|
+
magnitude1 = np.linalg.norm(vector1)
|
|
110
|
+
magnitude2 = np.linalg.norm(vector2)
|
|
111
|
+
|
|
112
|
+
if magnitude1 == 0 or magnitude2 == 0:
|
|
113
|
+
return 0.0
|
|
114
|
+
cos_angle = dot_product / (magnitude1 * magnitude2)
|
|
115
|
+
cos_angle = np.clip(
|
|
116
|
+
cos_angle, -1.0, 1.0
|
|
117
|
+
) # Ensure the value is within the valid range for arccos
|
|
118
|
+
angle = np.arccos(cos_angle)
|
|
119
|
+
return angle
|
|
120
|
+
|
|
121
|
+
def __clean_path(
|
|
122
|
+
self, path: np.ndarray, min_distance=10.0, deviation_threshold=50.0
|
|
123
|
+
) -> np.ndarray:
|
|
124
|
+
"""
|
|
125
|
+
Clean the predicted path by removing points that are too close to each other.
|
|
126
|
+
Parameters:
|
|
127
|
+
path (np.ndarray): The predicted path as an array of shape (STEPS, 2).
|
|
128
|
+
min_distance (float): The minimum distance between consecutive points to keep them.
|
|
129
|
+
devaition_threshold (float): The threshold for deviation to consider a point as part of the path.
|
|
130
|
+
Returns:
|
|
131
|
+
np.ndarray: The cleaned path with points that are sufficiently spaced apart.
|
|
132
|
+
"""
|
|
133
|
+
destination = path[-1]
|
|
134
|
+
start = path[0]
|
|
135
|
+
|
|
136
|
+
cleaned_path = [start] # Start with the first point
|
|
137
|
+
|
|
138
|
+
for i in range(1, len(path) - 1):
|
|
139
|
+
if self._calculate_distance(cleaned_path[-1], path[i]) >= min_distance:
|
|
140
|
+
cleaned_path.append(path[i])
|
|
141
|
+
cleaned_path.append(destination) # Always add the last point
|
|
142
|
+
|
|
143
|
+
undeviated_path = np.array([], dtype=np.float32)
|
|
144
|
+
cleaned_path = np.array(cleaned_path)
|
|
145
|
+
if len(cleaned_path) <= 2:
|
|
146
|
+
return cleaned_path
|
|
147
|
+
previous_point_distance = float("inf")
|
|
148
|
+
for i in range(len(cleaned_path)):
|
|
149
|
+
distance_to_dest = self._calculate_distance(cleaned_path[i], destination)
|
|
150
|
+
if distance_to_dest > previous_point_distance:
|
|
151
|
+
if distance_to_dest <= deviation_threshold:
|
|
152
|
+
undeviated_path = np.append(undeviated_path, cleaned_path[i])
|
|
153
|
+
previous_point_distance = distance_to_dest
|
|
154
|
+
else:
|
|
155
|
+
undeviated_path = np.append(undeviated_path, cleaned_path[i])
|
|
156
|
+
previous_point_distance = distance_to_dest
|
|
157
|
+
|
|
158
|
+
undeviated_path = undeviated_path.reshape(-1, 2)
|
|
159
|
+
|
|
160
|
+
return undeviated_path
|
|
161
|
+
|
|
162
|
+
def __calculate_speed_factor(self, progress: float) -> float:
|
|
163
|
+
"""
|
|
164
|
+
Calculate the speed factor based on the progress of the path; uses gamma distribution.
|
|
165
|
+
Parameters:
|
|
166
|
+
progress (float): The progress of the path as a value between 0 and 1.
|
|
167
|
+
Returns:
|
|
168
|
+
float: The speed factor, which increases gradually and tapers off.
|
|
169
|
+
"""
|
|
170
|
+
# Parameters for gamma distribution
|
|
171
|
+
k = 1.8 # Shape parameter
|
|
172
|
+
theta = 1.0 # Scale parameter
|
|
173
|
+
|
|
174
|
+
# Precompute normalization factor over the scaled range [0, 10]
|
|
175
|
+
x = np.linspace(0, 10, 1000) # Scaled progress range
|
|
176
|
+
y = (x ** (k - 1) * np.exp(-x / theta)) / (theta**k * gamma(k))
|
|
177
|
+
max_val = np.max(y) # Maximum value for normalization
|
|
178
|
+
|
|
179
|
+
# Scale progress to fit gamma's natural range
|
|
180
|
+
scaled_progress = progress * 10
|
|
181
|
+
|
|
182
|
+
# Base gamma distribution
|
|
183
|
+
speed_factor = (
|
|
184
|
+
scaled_progress ** (k - 1) * np.exp(-scaled_progress / theta)
|
|
185
|
+
) / (theta**k * gamma(k))
|
|
186
|
+
speed_factor = speed_factor / max_val # Normalize
|
|
187
|
+
|
|
188
|
+
# Define flat region and transitions
|
|
189
|
+
flat_value = 1.0 # Base value for flat region
|
|
190
|
+
smooth_width = 0.15 # Transition width
|
|
191
|
+
|
|
192
|
+
# Precompute gamma curve values at key points for transitions
|
|
193
|
+
base_y = y / max_val # Normalized base curve
|
|
194
|
+
x_0_05 = 0.05 # Start of lower transition
|
|
195
|
+
y_0_05 = base_y[np.searchsorted(x / 5, x_0_05)] # Value at x = 0.05
|
|
196
|
+
y_1_0 = base_y[-1] # Value at x = 1.0
|
|
197
|
+
|
|
198
|
+
# Apply flat region and transitions
|
|
199
|
+
if 0.20 <= progress <= 0.90: # Flat region with noise
|
|
200
|
+
noise = np.random.uniform(-0.05, 0.0) # Noise between -0.05 and 0
|
|
201
|
+
speed_factor = flat_value + noise
|
|
202
|
+
|
|
203
|
+
elif (0.20 - smooth_width) < progress < 0.20: # Lower transition (0.05 to 0.20)
|
|
204
|
+
speed_factor = np.interp(
|
|
205
|
+
progress, [0.20 - smooth_width, 0.20], [y_0_05, flat_value]
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
elif 0.90 < progress < (0.90 + smooth_width): # Upper transition (0.90 to 1.0)
|
|
209
|
+
speed_factor = np.interp(progress, [0.90, 1.0], [flat_value, y_1_0])
|
|
210
|
+
|
|
211
|
+
return speed_factor
|
|
212
|
+
|
|
213
|
+
def __smooth_path(self, path: np.ndarray, noise_factor=1.5) -> np.ndarray:
|
|
214
|
+
"""
|
|
215
|
+
Smooth the predicted path by applying Gaussian noise to the coordinates.
|
|
216
|
+
Parameters:
|
|
217
|
+
path (np.ndarray): The predicted path as an array of shape (STEPS, 2).
|
|
218
|
+
noise_factor (float): The standard deviation of the Gaussian noise to be added.
|
|
219
|
+
Returns:
|
|
220
|
+
np.ndarray: The smoothed path with Gaussian noise applied to the coordinates.
|
|
221
|
+
"""
|
|
222
|
+
smoothed_x = gaussian_filter1d(
|
|
223
|
+
path[:, 0], sigma=0.5
|
|
224
|
+
) # apply gaussian filter to x coordinates
|
|
225
|
+
smoothed_y = gaussian_filter1d(
|
|
226
|
+
path[:, 1], sigma=0.5
|
|
227
|
+
) # apply gaussian filter to y coordinates
|
|
228
|
+
|
|
229
|
+
noise_x = np.random.normal(
|
|
230
|
+
0, noise_factor, size=smoothed_x.shape
|
|
231
|
+
) # generate noise for x coordinates
|
|
232
|
+
noise_y = np.random.normal(
|
|
233
|
+
0, noise_factor, size=smoothed_y.shape
|
|
234
|
+
) # generate noise for y coordinates
|
|
235
|
+
|
|
236
|
+
t = np.linspace(0, 1, len(smoothed_x)) # Normalized time variable
|
|
237
|
+
noise_decay = np.exp(-4 * t) # Exponential decay function
|
|
238
|
+
|
|
239
|
+
# Here we are adding noise decay so that noise reduces as we move from start to end
|
|
240
|
+
|
|
241
|
+
smoothed_x += noise_x * noise_decay
|
|
242
|
+
smoothed_y += noise_y * noise_decay
|
|
243
|
+
|
|
244
|
+
# Apply final smoothing to prevent abrupt jumps
|
|
245
|
+
smoothed_x = gaussian_filter1d(smoothed_x, sigma=0.5)
|
|
246
|
+
smoothed_y = gaussian_filter1d(smoothed_y, sigma=0.5)
|
|
247
|
+
return np.column_stack((smoothed_x, smoothed_y))
|
|
248
|
+
|
|
249
|
+
def __interpolate_path(self, path: np.ndarray, num_points=50) -> np.ndarray:
|
|
250
|
+
"""
|
|
251
|
+
Interpolate the predicted path to have a fixed number of intermediate points for smoother transitions.
|
|
252
|
+
Parameters:
|
|
253
|
+
path (np.ndarray): The predicted path as an array of shape (STEPS, 2).
|
|
254
|
+
num_points (int): The number of points to interpolate to.
|
|
255
|
+
Returns:
|
|
256
|
+
np.ndarray: The interpolated path with a fixed number of points.
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
x = path[:, 0]
|
|
260
|
+
y = path[:, 1]
|
|
261
|
+
t = np.linspace(0, 1, len(x))
|
|
262
|
+
|
|
263
|
+
interp_func_x = interp1d(t, x, kind="linear")
|
|
264
|
+
interp_func_y = interp1d(t, y, kind="linear")
|
|
265
|
+
|
|
266
|
+
t_new = np.linspace(0, 1, num_points)
|
|
267
|
+
x_new = interp_func_x(t_new)
|
|
268
|
+
y_new = interp_func_y(t_new)
|
|
269
|
+
|
|
270
|
+
return np.column_stack((x_new, y_new))
|
|
271
|
+
except Exception as e:
|
|
272
|
+
print(f"Interpolation failed: {e}")
|
|
273
|
+
return path
|
|
274
|
+
|
|
275
|
+
def __predict_path(self, input_arr: np.ndarray) -> np.ndarray:
|
|
276
|
+
"""
|
|
277
|
+
Predict the intermediate path steps between the start and destination coordinates.
|
|
278
|
+
|
|
279
|
+
Parameters:
|
|
280
|
+
input_arr (np.ndarray): The input data containing start and destination coordinates. e.g [[start_x, start_y, dest_x, dest_y]]
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
np.ndarray: The predicted intermediate steps of the path.
|
|
284
|
+
|
|
285
|
+
This method uses the pre-trained RNN model to generate intermediate steps between the start
|
|
286
|
+
and destination. The input coordinates are normalized before being fed into the model.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
# Normalize the input array to be within the [0, 1] range for model compatibility
|
|
290
|
+
input_normalized_arr = self.__normalize(input_arr)
|
|
291
|
+
|
|
292
|
+
# Convert the normalized array to a PyTorch tensor, send it to the appropriate device, and add a dimension for batch processing (unsqueeze simulates a batch of size 1)
|
|
293
|
+
input_tensor = torch.tensor(input_normalized_arr).to(self.device).unsqueeze(1)
|
|
294
|
+
|
|
295
|
+
# Clean up intermediate arrays to free memory
|
|
296
|
+
del input_arr
|
|
297
|
+
del input_normalized_arr
|
|
298
|
+
|
|
299
|
+
with torch.no_grad():
|
|
300
|
+
# Pass the input tensor through the model to get the output
|
|
301
|
+
output = self.model(input_tensor)
|
|
302
|
+
|
|
303
|
+
# Clean up the input tensor to free memory
|
|
304
|
+
del input_tensor
|
|
305
|
+
|
|
306
|
+
# Reshape the output to match the expected dimensions and move it to CPU
|
|
307
|
+
output = output[0].view([self.STEPS, 2]).cpu().numpy()
|
|
308
|
+
|
|
309
|
+
# Denormalize the output to convert it back to the original coordinate range
|
|
310
|
+
output = self.__denormalize(output)
|
|
311
|
+
|
|
312
|
+
return output
|
|
313
|
+
|
|
314
|
+
def __add_speed_factor(self, path: np.ndarray) -> np.ndarray:
|
|
315
|
+
"""
|
|
316
|
+
Add speed factor to the predicted path to simulate variable speed along the path.
|
|
317
|
+
Parameters:
|
|
318
|
+
path (np.ndarray): The predicted path as an array of shape (STEPS, 2).
|
|
319
|
+
Returns:
|
|
320
|
+
np.ndarray: The path with speed factor applied to simulate variable speed, with shape (STEPS, 3).
|
|
321
|
+
"""
|
|
322
|
+
path_with_speed = np.zeros((len(path), 3), dtype=np.float64)
|
|
323
|
+
for i in range(len(path)):
|
|
324
|
+
progress = i / (len(path) - 1)
|
|
325
|
+
angle = (
|
|
326
|
+
self.__calculate_angle(path[i - 1], path[i], path[i + 1])
|
|
327
|
+
if i > 0 and i < len(path) - 1
|
|
328
|
+
else 0
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
angle_speed_factor = 1 - (angle / np.pi)
|
|
332
|
+
progress_speed_factor = self.__calculate_speed_factor(progress)
|
|
333
|
+
speed_factor = (0.60 * angle_speed_factor) + (
|
|
334
|
+
0.4 * progress_speed_factor
|
|
335
|
+
) # 60% weight to angle and 40% to progress
|
|
336
|
+
path_with_speed[i] = np.array([path[i][0], path[i][1], speed_factor])
|
|
337
|
+
|
|
338
|
+
return path_with_speed
|
|
339
|
+
|
|
340
|
+
def predict(self, start: np.ndarray, dest: np.ndarray) -> np.ndarray:
|
|
341
|
+
"""
|
|
342
|
+
Predict the path from start to destination coordinates.
|
|
343
|
+
|
|
344
|
+
Parameters:
|
|
345
|
+
start (np.ndarray): The starting coordinates as [x, y].
|
|
346
|
+
dest (np.ndarray): The destination coordinates as [x, y].
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
np.ndarray: The predicted path with speed factor as an array of shape (STEPS, 3).
|
|
350
|
+
"""
|
|
351
|
+
# Combine start and destination coordinates into a single input array
|
|
352
|
+
input_arr = np.concatenate([start, dest]).reshape(
|
|
353
|
+
1, 4
|
|
354
|
+
) # the input must be in [[x1,y1, x2, y2]] format, required by model
|
|
355
|
+
|
|
356
|
+
# Predict the intermediate path using the model
|
|
357
|
+
path = self.__predict_path(input_arr)
|
|
358
|
+
path = np.vstack([start, path, dest])
|
|
359
|
+
path = self.__clean_path(path)
|
|
360
|
+
|
|
361
|
+
path = self.__smooth_path(path)
|
|
362
|
+
interpolated_path = np.array([], dtype=np.float32)
|
|
363
|
+
|
|
364
|
+
# Interpolate the path to ensure smooth transitions, make sure that there are no jumps, if distance between two points is more than 20 pixels, we interpolate
|
|
365
|
+
for i in range(len(path) - 1):
|
|
366
|
+
distance_with_previous_point = int(
|
|
367
|
+
self._calculate_distance(path[i], path[i + 1]) if i > 0 else 0
|
|
368
|
+
)
|
|
369
|
+
if distance_with_previous_point >= 40:
|
|
370
|
+
current_num_points = int(distance_with_previous_point / 20)
|
|
371
|
+
current_interpolated_path = self.__interpolate_path(
|
|
372
|
+
path[i : i + 2], num_points=current_num_points
|
|
373
|
+
)
|
|
374
|
+
interpolated_path = (
|
|
375
|
+
np.vstack([interpolated_path, current_interpolated_path])
|
|
376
|
+
if interpolated_path.size
|
|
377
|
+
else current_interpolated_path
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
interpolated_path = (
|
|
381
|
+
np.vstack([interpolated_path, path[i]])
|
|
382
|
+
if interpolated_path.size
|
|
383
|
+
else path[i]
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
path = interpolated_path
|
|
387
|
+
if not np.array_equal(path[-1], dest):
|
|
388
|
+
path = np.vstack([path, dest])
|
|
389
|
+
if not np.array_equal(path[0], start):
|
|
390
|
+
path = np.vstack([start, path])
|
|
391
|
+
|
|
392
|
+
path = self.__add_speed_factor(path)
|
|
393
|
+
|
|
394
|
+
return path
|
bumblebee/ai/rnn.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file contains the code for neural network models used in the project. They are copied from the training notebooks.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
import torch.nn as nn
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
Attention and CursorRNN are copied from 'rnn-train.ipynb' notebook
|
|
10
|
+
Attention class is used to compute the attention weights and context vector
|
|
11
|
+
CursorRNN class is the main model that combines LSTM, attention, and residual connections to predict the path
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Attention(nn.Module):
|
|
16
|
+
def __init__(self, hidden_dim):
|
|
17
|
+
super(Attention, self).__init__()
|
|
18
|
+
self.attn = nn.Linear(
|
|
19
|
+
hidden_dim, 1, bias=False
|
|
20
|
+
) # Attention layer to assign weights to different time steps
|
|
21
|
+
|
|
22
|
+
def forward(self, lstm_out):
|
|
23
|
+
scores = self.attn(lstm_out) # Compute attention scores for each time step
|
|
24
|
+
attn_weights = torch.softmax(
|
|
25
|
+
scores, dim=1
|
|
26
|
+
) # Apply softmax to normalize attention weights
|
|
27
|
+
context = torch.sum(
|
|
28
|
+
attn_weights * lstm_out, dim=1
|
|
29
|
+
) # Create context vector by weighted sum of LSTM outputs
|
|
30
|
+
|
|
31
|
+
return context, attn_weights
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CursorRNN(nn.Module):
|
|
35
|
+
def __init__(self, input_dim, hidden_dim, output_dim, num_layers=1, dropout=0.2):
|
|
36
|
+
super(CursorRNN, self).__init__()
|
|
37
|
+
self.lstm = nn.LSTM(
|
|
38
|
+
input_dim,
|
|
39
|
+
hidden_dim,
|
|
40
|
+
num_layers=num_layers,
|
|
41
|
+
batch_first=True,
|
|
42
|
+
bidirectional=False,
|
|
43
|
+
dropout=dropout,
|
|
44
|
+
) # LSTM layer for sequence processing
|
|
45
|
+
self.attention = Attention(
|
|
46
|
+
hidden_dim
|
|
47
|
+
) # Attention mechanism to focus on important time steps
|
|
48
|
+
self.residual_fc = nn.Linear(
|
|
49
|
+
input_dim, hidden_dim
|
|
50
|
+
) # Residual connection to help with gradient flow
|
|
51
|
+
self.layer_norm = nn.LayerNorm(
|
|
52
|
+
hidden_dim
|
|
53
|
+
) # Layer normalization for training stability
|
|
54
|
+
self.fc = nn.Linear(
|
|
55
|
+
hidden_dim, output_dim
|
|
56
|
+
) # Output projection layer to generate final predictions
|
|
57
|
+
|
|
58
|
+
def forward(self, x):
|
|
59
|
+
lstm_out, _ = self.lstm(x) # Process sequence through LSTM
|
|
60
|
+
context, attn_weights = self.attention(
|
|
61
|
+
lstm_out
|
|
62
|
+
) # Apply attention to focus on relevant parts
|
|
63
|
+
residual = self.residual_fc(
|
|
64
|
+
x[:, -1, :]
|
|
65
|
+
) # Create residual connection from last input
|
|
66
|
+
combined = self.layer_norm(
|
|
67
|
+
context + residual
|
|
68
|
+
) # Combine attention output with residual and normalize
|
|
69
|
+
output = self.fc(combined) # Generate final trajectory prediction
|
|
70
|
+
|
|
71
|
+
return output, attn_weights
|