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.
@@ -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
@@ -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