agrotechsimapi 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.
- agrotechsimapi/__init__.py +10 -0
- agrotechsimapi/client.py +374 -0
- agrotechsimapi/high_level_client.py +695 -0
- agrotechsimapi/pid.py +59 -0
- agrotechsimapi/signal_utils.py +7 -0
- agrotechsimapi/utils/__init__.py +7 -0
- agrotechsimapi/utils/aruco_marker_recognizer.py +132 -0
- agrotechsimapi/utils/recognition_setting.py +28 -0
- agrotechsimapi/utils/utils.py +48 -0
- agrotechsimapi/utils/vision.py +168 -0
- agrotechsimapi/video_pb2.py +40 -0
- agrotechsimapi/video_pb2_grpc.py +97 -0
- agrotechsimapi-1.0.0.dist-info/METADATA +443 -0
- agrotechsimapi-1.0.0.dist-info/RECORD +19 -0
- agrotechsimapi-1.0.0.dist-info/WHEEL +5 -0
- agrotechsimapi-1.0.0.dist-info/top_level.txt +2 -0
- modules/camera_driver.py +42 -0
- modules/input_driver.py +133 -0
- modules/lidar_driver.py +57 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from agrotechsimapi.client import SimClient
|
|
2
|
+
from agrotechsimapi.client import CaptureType
|
|
3
|
+
from agrotechsimapi.video_pb2 import *
|
|
4
|
+
from agrotechsimapi.video_pb2_grpc import *
|
|
5
|
+
|
|
6
|
+
from agrotechsimapi.pid import PID
|
|
7
|
+
from agrotechsimapi.high_level_client import HighLevelSimClient
|
|
8
|
+
|
|
9
|
+
from .utils.utils import LoopingTimer, sim_to_api_distance, vel_to_rc_signal
|
|
10
|
+
from .utils.vision import process_aruco, process_blob, resolution_changes
|
agrotechsimapi/client.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import msgpackrpc
|
|
2
|
+
import cv2
|
|
3
|
+
import numpy as np
|
|
4
|
+
import random
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import threading
|
|
9
|
+
import grpc
|
|
10
|
+
from . import video_pb2
|
|
11
|
+
from . import video_pb2_grpc
|
|
12
|
+
|
|
13
|
+
class CaptureType(Enum):
|
|
14
|
+
color = 0
|
|
15
|
+
thermal = 1
|
|
16
|
+
depth = 2
|
|
17
|
+
spectrum_color = 3
|
|
18
|
+
spectrum_NIR = 4
|
|
19
|
+
spectrum_SWIR = 5
|
|
20
|
+
spectrum_RE = 6
|
|
21
|
+
spectrum_R = 7
|
|
22
|
+
spectrum_G = 8
|
|
23
|
+
spectrum_B = 9
|
|
24
|
+
|
|
25
|
+
def post_process(image, gamma=1.0, new_size=(800, 600), saturation=1.0, contrast=1.0):
|
|
26
|
+
inv_gamma = 1.0 / gamma
|
|
27
|
+
table = np.array([((i / 255.0) ** inv_gamma) * 255 for i in range(256)]).astype("uint8")
|
|
28
|
+
|
|
29
|
+
image = cv2.LUT(image, table)
|
|
30
|
+
|
|
31
|
+
if new_size is not None:
|
|
32
|
+
image = cv2.resize(image, new_size, interpolation=cv2.INTER_LINEAR)
|
|
33
|
+
|
|
34
|
+
image = cv2.convertScaleAbs(image, alpha=contrast, beta=0)
|
|
35
|
+
|
|
36
|
+
if saturation != 1.0:
|
|
37
|
+
img_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
|
|
38
|
+
img_hsv[:, :, 1] = np.clip(img_hsv[:, :, 1] * saturation, 0, 255).astype(np.uint8)
|
|
39
|
+
image = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR)
|
|
40
|
+
|
|
41
|
+
return image
|
|
42
|
+
|
|
43
|
+
class VideoStreamSender:
|
|
44
|
+
def __init__(self, camera_id=0, rate=30):
|
|
45
|
+
self.client = SimClient()
|
|
46
|
+
self.camera_id = camera_id
|
|
47
|
+
self.streaming = False
|
|
48
|
+
self.rate = rate
|
|
49
|
+
|
|
50
|
+
async def generate_frames(self):
|
|
51
|
+
while self.streaming:
|
|
52
|
+
frame = self.client.get_camera_capture(camera_id=self.camera_id)
|
|
53
|
+
if frame is not None:
|
|
54
|
+
_, buffer = cv2.imencode('.jpg', frame)
|
|
55
|
+
yield video_pb2.Frame(data=buffer.tobytes(), encoding="jpeg")
|
|
56
|
+
await asyncio.sleep(1 / self.rate)
|
|
57
|
+
|
|
58
|
+
async def stream(self, port):
|
|
59
|
+
async with grpc.aio.insecure_channel(f"localhost:{port}") as channel:
|
|
60
|
+
stub = video_pb2_grpc.VideoStreamServiceStub(channel)
|
|
61
|
+
await stub.StreamFrames(self.generate_frames())
|
|
62
|
+
|
|
63
|
+
sender_instance = None
|
|
64
|
+
thread_instance = None
|
|
65
|
+
|
|
66
|
+
class SimClient():
|
|
67
|
+
def __init__(self,
|
|
68
|
+
address : str = "127.0.0.1" ,
|
|
69
|
+
port : int = 8080):
|
|
70
|
+
self.address = address
|
|
71
|
+
self.port = port
|
|
72
|
+
self.rpc_client = msgpackrpc.Client(msgpackrpc.Address(self.address, self.port),
|
|
73
|
+
timeout = 10,
|
|
74
|
+
pack_encoding = 'utf-8',
|
|
75
|
+
unpack_encoding = 'utf-8')
|
|
76
|
+
|
|
77
|
+
self.streaming = False
|
|
78
|
+
|
|
79
|
+
def __del__(self):
|
|
80
|
+
self.close_connection()
|
|
81
|
+
|
|
82
|
+
def close_connection(self):
|
|
83
|
+
if(self.is_connected()):
|
|
84
|
+
self.rpc_client.close()
|
|
85
|
+
|
|
86
|
+
def add_noise(self,image):
|
|
87
|
+
noise = np.random.normal(0, 1, image.shape).astype(np.uint8)
|
|
88
|
+
noisy_image = cv2.add(image, noise)
|
|
89
|
+
return noisy_image
|
|
90
|
+
|
|
91
|
+
def add_artifacts(self,image):
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
h, w, _ = image.shape
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
for _ in range(random.randint(1,7)):
|
|
98
|
+
y_line = np.random.randint(0, h)
|
|
99
|
+
width = np.random.randint(3, 10)
|
|
100
|
+
line_end = min(y_line + width, h)
|
|
101
|
+
|
|
102
|
+
image[y_line:line_end, :] = np.random.randint(0, 255, size=(line_end - y_line, w, 3), dtype=np.uint8)
|
|
103
|
+
|
|
104
|
+
return image
|
|
105
|
+
|
|
106
|
+
def is_connected(self):
|
|
107
|
+
result = True
|
|
108
|
+
try:
|
|
109
|
+
result = self.rpc_client.call('ping')
|
|
110
|
+
except:
|
|
111
|
+
result = False
|
|
112
|
+
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
'''def get_camera_capture(self, camera_id: int = 0, is_clear: bool = True, is_thermal: bool = False, is_depth: bool = False):
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
This function retrieves an image from one of the drone cameras in the simulator.
|
|
119
|
+
The maximum refresh rate is 20Hz, even if you try to get an image with a higher refresh rate,
|
|
120
|
+
the camera in the simulator itself is refreshed at 20Hz.
|
|
121
|
+
The image size 640 x 480 (scaled).
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
camera_id (int): id of camera
|
|
125
|
+
is_clear(bool) : default True, if False is selected, noise will be generated
|
|
126
|
+
is_thermal(bool) : default False, this flag activate thermal vision
|
|
127
|
+
is_depth(bool) : default False, this flag activate depth vision
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
ndarray : openCV image
|
|
131
|
+
"""
|
|
132
|
+
raw_image = self.rpc_client.call('getCameraCapture', camera_id, is_thermal, is_depth)
|
|
133
|
+
|
|
134
|
+
if len(raw_image) > 0:
|
|
135
|
+
cv2_image = np.frombuffer(bytes(raw_image), dtype=np.uint8).reshape((360, 480, 4))
|
|
136
|
+
result = post_process(cv2_image,
|
|
137
|
+
gamma=1.0,
|
|
138
|
+
new_size=(640, 480),
|
|
139
|
+
saturation=1.05,
|
|
140
|
+
contrast=1)
|
|
141
|
+
|
|
142
|
+
if not is_clear:
|
|
143
|
+
result = self.add_noise(result)
|
|
144
|
+
result = self.add_artifacts(result)
|
|
145
|
+
return result'''
|
|
146
|
+
|
|
147
|
+
def get_laser_scan(self,
|
|
148
|
+
angle_min : float = -np.pi/2,
|
|
149
|
+
angle_max : float = np.pi/2,
|
|
150
|
+
range_min : float = 0.1,
|
|
151
|
+
range_max : float = 30,
|
|
152
|
+
num_ranges: int = 30,
|
|
153
|
+
is_clear : bool = False,
|
|
154
|
+
range_error: float = 0.15):
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
This function returns a data packet from the rotating lidar on the drone.
|
|
158
|
+
You can define the angle of view of the lidar (360 degrees by default) by angle_min and angle_max.
|
|
159
|
+
If the distance is closer or farther than the specified values, the distance will be equal to zero.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
angle_min (float): min angle range(degree)
|
|
163
|
+
angle_max (float): max angle range(degree)
|
|
164
|
+
range_min (float) : min range for scan distance(meters)
|
|
165
|
+
range_max (float) : max range for scan distance(meters)
|
|
166
|
+
num_ranges (int) : number of traces
|
|
167
|
+
is_clear (bool) : default True, if False is selected, noise will be generated
|
|
168
|
+
range_error (float) : maximum error variation(if is_clear is false)
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
ndarray : distances obtained from lidar scanning(meters)
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
laser_scan_data = self.rpc_client.call('getLaserScan',
|
|
175
|
+
angle_min,
|
|
176
|
+
angle_max,
|
|
177
|
+
range_min,
|
|
178
|
+
range_max,
|
|
179
|
+
num_ranges)
|
|
180
|
+
|
|
181
|
+
if(is_clear == False and len(laser_scan_data) == num_ranges):
|
|
182
|
+
noise = np.random.normal(0, range_error, num_ranges)
|
|
183
|
+
laser_scan_data += noise
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
return laser_scan_data
|
|
187
|
+
|
|
188
|
+
def get_radar_point(self,
|
|
189
|
+
radar_id : int = 0,
|
|
190
|
+
base_angle : float = 45,
|
|
191
|
+
range_min : float = 0.15,
|
|
192
|
+
range_max: float = 5,
|
|
193
|
+
is_clear : bool = True,
|
|
194
|
+
range_error: float = 0.15,
|
|
195
|
+
angle_error: float = 0.015):
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
This function returns information about the nearest point that is within the radar coverage area.
|
|
199
|
+
The coverage area has the shape of a cone sector.
|
|
200
|
+
The point information returns in the format of distance and two angles.
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
radar_id (int) : id of radar
|
|
205
|
+
base_angle (float): cone apex angle
|
|
206
|
+
range_min (float) : min range for scan distance(meters)
|
|
207
|
+
range_max (float) : max range for scan distance(meters)
|
|
208
|
+
is_clear (bool) : default True, if False is selected, noise will be generated
|
|
209
|
+
range_error (float) : maximum error variation of distance(if is_clear is false)
|
|
210
|
+
angle_error (float) : maximum error variation for angles(if is_clear is false)
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
float : point distance(meters)
|
|
214
|
+
float : angle to a point in the horizontal plane(degree)
|
|
215
|
+
float : angle to a point in the vertical plane(degree)
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
radar_point = self.rpc_client.call('getRadarData',
|
|
219
|
+
radar_id,
|
|
220
|
+
base_angle,
|
|
221
|
+
range_min,
|
|
222
|
+
range_max)
|
|
223
|
+
|
|
224
|
+
radar_point[1] = -radar_point[1]
|
|
225
|
+
|
|
226
|
+
if(is_clear == False):
|
|
227
|
+
range_noise = np.random.normal(0,range_error,1)
|
|
228
|
+
radar_point[0] += range_noise
|
|
229
|
+
|
|
230
|
+
angle_noise = np.random.normal(0,angle_error,2)
|
|
231
|
+
radar_point[1:] += angle_noise
|
|
232
|
+
|
|
233
|
+
return radar_point
|
|
234
|
+
|
|
235
|
+
def get_range_data(self,
|
|
236
|
+
rangefinder_id : int = 0,
|
|
237
|
+
range_min : float = 0.15,
|
|
238
|
+
range_max: float = 10,
|
|
239
|
+
is_clear : bool = True,
|
|
240
|
+
range_error: float = 0.15):
|
|
241
|
+
|
|
242
|
+
"""
|
|
243
|
+
This function receives information from the rangefinder
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
rangefinder_id (int) : id of rangefinder
|
|
247
|
+
range_min (float) : min range for scan distance(meters)
|
|
248
|
+
range_max (float) : max range for scan distance(meters)
|
|
249
|
+
is_clear (bool) : default True, if False is selected, noise will be generated
|
|
250
|
+
range_error (float) : maximum error variation(if is_clear is false)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
float : point distance(meters)
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
range_point = self.rpc_client.call('getRangefinderData', rangefinder_id, range_min, range_max)
|
|
257
|
+
|
|
258
|
+
if is_clear == False:
|
|
259
|
+
noise = np.random.normal(0,range_error,1)
|
|
260
|
+
range_point += noise
|
|
261
|
+
|
|
262
|
+
return range_point
|
|
263
|
+
|
|
264
|
+
def set_led_intensity(self,
|
|
265
|
+
led_id : int = 0,
|
|
266
|
+
new_intensity : float = 0.5):
|
|
267
|
+
"""
|
|
268
|
+
This feature allows you to change the intensity of the brightness of the light diodes on the drone
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
led_id (int) : id of led diode
|
|
272
|
+
new_intensity (float) : intensity in range 0..1
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
self.rpc_client.call('setLedIntensity', led_id, new_intensity)
|
|
276
|
+
|
|
277
|
+
def set_led_state(self,
|
|
278
|
+
led_id : int = 0,
|
|
279
|
+
new_state : bool = True):
|
|
280
|
+
|
|
281
|
+
"""
|
|
282
|
+
This feature allows you to enable or to disable the light diodes on the drone
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
led_id (int) : id of led diode
|
|
286
|
+
new_state (bool) : new diode state
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
self.rpc_client.call('setLedState', led_id, new_state)
|
|
290
|
+
|
|
291
|
+
def get_kinametics_data(self):
|
|
292
|
+
|
|
293
|
+
return self.rpc_client.call("getKinematicsData")
|
|
294
|
+
|
|
295
|
+
def call_event_action(self):
|
|
296
|
+
try:
|
|
297
|
+
return self.rpc_client.call("callEventAction")
|
|
298
|
+
except:
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def start_streaming(self, port: int, camera_id: int = 0, rate: int = 30):
|
|
303
|
+
global sender_instance, thread_instance
|
|
304
|
+
|
|
305
|
+
if sender_instance is not None and sender_instance.streaming:
|
|
306
|
+
print("[INFO] Streaming already running")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
sender_instance = VideoStreamSender(camera_id,rate)
|
|
310
|
+
sender_instance.streaming = True
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def run_async():
|
|
314
|
+
asyncio.run(sender_instance.stream(port))
|
|
315
|
+
|
|
316
|
+
thread_instance = threading.Thread(target=run_async, daemon=True)
|
|
317
|
+
thread_instance.start()
|
|
318
|
+
print(f"[INFO] Started streaming to port {port}")
|
|
319
|
+
|
|
320
|
+
def stop_streaming(self):
|
|
321
|
+
global sender_instance
|
|
322
|
+
if sender_instance:
|
|
323
|
+
sender_instance.streaming = False
|
|
324
|
+
print("[INFO] Stopped streaming")
|
|
325
|
+
else:
|
|
326
|
+
print("[WARN] No active streaming session")
|
|
327
|
+
|
|
328
|
+
def get_camera_capture(self, camera_id: int = 0, type: CaptureType = CaptureType.color):
|
|
329
|
+
|
|
330
|
+
pp_index = 0
|
|
331
|
+
parameter = 0
|
|
332
|
+
|
|
333
|
+
if(type == CaptureType.color):
|
|
334
|
+
pp_index = 0
|
|
335
|
+
parameter = 0
|
|
336
|
+
elif(type == CaptureType.thermal):
|
|
337
|
+
pp_index = 1
|
|
338
|
+
parameter = 0
|
|
339
|
+
elif(type == CaptureType.depth):
|
|
340
|
+
pp_index = 2
|
|
341
|
+
parameter = 0
|
|
342
|
+
elif(type == CaptureType.spectrum_color):
|
|
343
|
+
pp_index = 3
|
|
344
|
+
parameter = 0
|
|
345
|
+
elif(type == CaptureType.spectrum_NIR):
|
|
346
|
+
pp_index = 3
|
|
347
|
+
parameter = 1
|
|
348
|
+
elif(type == CaptureType.spectrum_SWIR):
|
|
349
|
+
pp_index = 3
|
|
350
|
+
parameter = 2
|
|
351
|
+
elif(type == CaptureType.spectrum_RE):
|
|
352
|
+
pp_index = 3
|
|
353
|
+
parameter = 3
|
|
354
|
+
elif(type == CaptureType.spectrum_R):
|
|
355
|
+
pp_index = 3
|
|
356
|
+
parameter = 4
|
|
357
|
+
elif(type == CaptureType.spectrum_G):
|
|
358
|
+
pp_index = 3
|
|
359
|
+
parameter = 5
|
|
360
|
+
elif(type == CaptureType.spectrum_B):
|
|
361
|
+
pp_index = 3
|
|
362
|
+
parameter = 6
|
|
363
|
+
|
|
364
|
+
raw_image = self.rpc_client.call('getCameraCapture', camera_id, pp_index, parameter)
|
|
365
|
+
|
|
366
|
+
if len(raw_image) > 1:
|
|
367
|
+
cv2_image = np.frombuffer(bytes(raw_image), dtype=np.uint8).reshape((360, 480, 4))
|
|
368
|
+
result = post_process(cv2_image,
|
|
369
|
+
gamma=1.0,
|
|
370
|
+
new_size=(640, 480),
|
|
371
|
+
saturation=1.05,
|
|
372
|
+
contrast=1)
|
|
373
|
+
|
|
374
|
+
return result
|