foodforthought-cli 0.2.7__py3-none-any.whl → 0.2.8__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,362 @@
1
+ """
2
+ Perception interfaces for robot sensors.
3
+
4
+ Supports:
5
+ - RGB cameras
6
+ - Depth cameras (stereo, ToF, structured light)
7
+ - LiDAR (2D and 3D)
8
+ - IMU
9
+ - Force/Torque sensors
10
+
11
+ Design principle: Return raw sensor data in standard formats.
12
+ Processing (object detection, SLAM, etc.) happens at higher levels.
13
+ """
14
+
15
+ from abc import ABC, abstractmethod
16
+ from typing import Optional, List, Callable, Tuple
17
+ from enum import Enum, auto
18
+
19
+ from .types import (
20
+ Vector3,
21
+ Quaternion,
22
+ Pose,
23
+ Image,
24
+ DepthImage,
25
+ PointCloud,
26
+ IMUReading,
27
+ ForceTorqueReading,
28
+ ActionResult,
29
+ )
30
+
31
+
32
+ class CameraInterface(ABC):
33
+ """
34
+ Interface for RGB cameras.
35
+
36
+ Can be:
37
+ - Fixed mount (e.g., head camera)
38
+ - On end-effector (e.g., wrist camera)
39
+ - External (e.g., Intel RealSense)
40
+ """
41
+
42
+ @abstractmethod
43
+ def get_image(self) -> Image:
44
+ """
45
+ Capture current frame.
46
+
47
+ Returns:
48
+ Image with RGB data
49
+ """
50
+ pass
51
+
52
+ @abstractmethod
53
+ def get_resolution(self) -> Tuple[int, int]:
54
+ """
55
+ Get camera resolution.
56
+
57
+ Returns:
58
+ (width, height) in pixels
59
+ """
60
+ pass
61
+
62
+ @abstractmethod
63
+ def get_intrinsics(self) -> dict:
64
+ """
65
+ Get camera intrinsic parameters.
66
+
67
+ Returns:
68
+ Dict with:
69
+ - fx, fy: focal lengths
70
+ - cx, cy: principal point
71
+ - distortion: distortion coefficients
72
+ """
73
+ pass
74
+
75
+ def get_frame_id(self) -> str:
76
+ """Get coordinate frame ID for this camera."""
77
+ return "camera"
78
+
79
+ def set_resolution(self, width: int, height: int) -> ActionResult:
80
+ """Set camera resolution."""
81
+ return ActionResult.error("Resolution change not supported")
82
+
83
+ def set_exposure(self, exposure_ms: float) -> ActionResult:
84
+ """Set exposure time in milliseconds."""
85
+ return ActionResult.error("Exposure control not supported")
86
+
87
+ # =========================================================================
88
+ # Streaming (optional)
89
+ # =========================================================================
90
+
91
+ def start_streaming(self, callback: Callable[[Image], None]) -> ActionResult:
92
+ """
93
+ Start continuous image streaming.
94
+
95
+ Args:
96
+ callback: Function called with each new frame
97
+ """
98
+ return ActionResult.error("Streaming not supported")
99
+
100
+ def stop_streaming(self) -> ActionResult:
101
+ """Stop image streaming."""
102
+ return ActionResult.error("Streaming not supported")
103
+
104
+ def get_fps(self) -> float:
105
+ """Get current frame rate."""
106
+ return 0.0
107
+
108
+
109
+ class DepthCameraInterface(CameraInterface):
110
+ """
111
+ Interface for depth cameras.
112
+
113
+ Extends CameraInterface with depth-specific methods.
114
+ Implemented by: RealSense, Kinect, stereo cameras, ToF sensors.
115
+ """
116
+
117
+ @abstractmethod
118
+ def get_depth_image(self) -> DepthImage:
119
+ """
120
+ Capture current depth frame.
121
+
122
+ Returns:
123
+ DepthImage with depth values
124
+ """
125
+ pass
126
+
127
+ @abstractmethod
128
+ def get_point_cloud(self) -> PointCloud:
129
+ """
130
+ Get 3D point cloud from depth data.
131
+
132
+ Returns:
133
+ PointCloud in camera frame
134
+ """
135
+ pass
136
+
137
+ def get_rgbd(self) -> Tuple[Image, DepthImage]:
138
+ """
139
+ Get aligned RGB and depth images.
140
+
141
+ Returns:
142
+ (rgb_image, depth_image) tuple
143
+ """
144
+ return (self.get_image(), self.get_depth_image())
145
+
146
+ def get_depth_range(self) -> Tuple[float, float]:
147
+ """
148
+ Get valid depth range.
149
+
150
+ Returns:
151
+ (min_depth, max_depth) in meters
152
+ """
153
+ return (0.1, 10.0)
154
+
155
+ def point_to_3d(self, x: int, y: int) -> Optional[Vector3]:
156
+ """
157
+ Convert pixel coordinates to 3D point.
158
+
159
+ Args:
160
+ x, y: Pixel coordinates
161
+
162
+ Returns:
163
+ Vector3 in camera frame, or None if invalid depth
164
+ """
165
+ depth = self.get_depth_image()
166
+ # Implementation depends on depth format
167
+ return None
168
+
169
+
170
+ class LidarInterface(ABC):
171
+ """
172
+ Interface for LiDAR sensors.
173
+
174
+ Supports:
175
+ - 2D LiDAR (RPLIDAR, Hokuyo)
176
+ - 3D LiDAR (Velodyne, Ouster, Livox)
177
+ """
178
+
179
+ @abstractmethod
180
+ def get_scan(self) -> PointCloud:
181
+ """
182
+ Get current LiDAR scan.
183
+
184
+ Returns:
185
+ PointCloud in sensor frame
186
+ """
187
+ pass
188
+
189
+ @abstractmethod
190
+ def get_range(self) -> Tuple[float, float]:
191
+ """
192
+ Get measurement range.
193
+
194
+ Returns:
195
+ (min_range, max_range) in meters
196
+ """
197
+ pass
198
+
199
+ def get_frame_id(self) -> str:
200
+ """Get coordinate frame ID for this LiDAR."""
201
+ return "lidar"
202
+
203
+ def is_3d(self) -> bool:
204
+ """Check if this is a 3D LiDAR."""
205
+ return True
206
+
207
+ def get_angular_resolution(self) -> float:
208
+ """Get angular resolution in radians."""
209
+ return 0.01 # ~0.5 degrees default
210
+
211
+ # =========================================================================
212
+ # 2D LiDAR specific
213
+ # =========================================================================
214
+
215
+ def get_scan_2d(self) -> List[Tuple[float, float]]:
216
+ """
217
+ Get 2D scan as list of (angle, distance) pairs.
218
+
219
+ Returns:
220
+ List of (angle_rad, distance_m) tuples
221
+ """
222
+ # Default: project 3D to 2D
223
+ cloud = self.get_scan()
224
+ scan_2d = []
225
+ import math
226
+ for point in cloud.points:
227
+ angle = math.atan2(point.y, point.x)
228
+ distance = math.sqrt(point.x**2 + point.y**2)
229
+ scan_2d.append((angle, distance))
230
+ return scan_2d
231
+
232
+
233
+ class IMUInterface(ABC):
234
+ """
235
+ Interface for Inertial Measurement Units.
236
+
237
+ Provides:
238
+ - Orientation (from gyro integration or sensor fusion)
239
+ - Angular velocity (from gyroscope)
240
+ - Linear acceleration (from accelerometer)
241
+ """
242
+
243
+ @abstractmethod
244
+ def get_reading(self) -> IMUReading:
245
+ """
246
+ Get current IMU reading.
247
+
248
+ Returns:
249
+ IMUReading with orientation, angular velocity, acceleration
250
+ """
251
+ pass
252
+
253
+ @abstractmethod
254
+ def get_orientation(self) -> Quaternion:
255
+ """
256
+ Get current orientation.
257
+
258
+ Returns:
259
+ Quaternion representing orientation
260
+ """
261
+ pass
262
+
263
+ def get_euler(self) -> Tuple[float, float, float]:
264
+ """
265
+ Get orientation as Euler angles.
266
+
267
+ Returns:
268
+ (roll, pitch, yaw) in radians
269
+ """
270
+ return self.get_orientation().to_euler()
271
+
272
+ def get_angular_velocity(self) -> Vector3:
273
+ """
274
+ Get angular velocity.
275
+
276
+ Returns:
277
+ Vector3 in rad/s
278
+ """
279
+ return self.get_reading().angular_velocity
280
+
281
+ def get_linear_acceleration(self) -> Vector3:
282
+ """
283
+ Get linear acceleration.
284
+
285
+ Returns:
286
+ Vector3 in m/s² (includes gravity)
287
+ """
288
+ return self.get_reading().linear_acceleration
289
+
290
+ def calibrate(self) -> ActionResult:
291
+ """
292
+ Calibrate IMU (set current orientation as reference).
293
+
294
+ Robot should be stationary and level.
295
+ """
296
+ return ActionResult.error("Calibration not supported")
297
+
298
+ def get_frame_id(self) -> str:
299
+ """Get coordinate frame ID for this IMU."""
300
+ return "imu"
301
+
302
+
303
+ class ForceTorqueInterface(ABC):
304
+ """
305
+ Interface for force-torque sensors.
306
+
307
+ Typically mounted at wrist (between arm and gripper).
308
+ """
309
+
310
+ @abstractmethod
311
+ def get_reading(self) -> ForceTorqueReading:
312
+ """
313
+ Get current force-torque reading.
314
+
315
+ Returns:
316
+ ForceTorqueReading with force and torque vectors
317
+ """
318
+ pass
319
+
320
+ @abstractmethod
321
+ def get_force(self) -> Vector3:
322
+ """
323
+ Get force vector.
324
+
325
+ Returns:
326
+ Vector3 in Newtons
327
+ """
328
+ pass
329
+
330
+ @abstractmethod
331
+ def get_torque(self) -> Vector3:
332
+ """
333
+ Get torque vector.
334
+
335
+ Returns:
336
+ Vector3 in Nm
337
+ """
338
+ pass
339
+
340
+ def zero(self) -> ActionResult:
341
+ """
342
+ Zero the sensor (set current reading as offset).
343
+
344
+ Call when no external load is applied.
345
+ """
346
+ return ActionResult.error("Zeroing not supported")
347
+
348
+ def get_frame_id(self) -> str:
349
+ """Get coordinate frame ID for this sensor."""
350
+ return "ft_sensor"
351
+
352
+ def is_in_contact(self, threshold: float = 1.0) -> bool:
353
+ """
354
+ Check if sensor detects contact.
355
+
356
+ Args:
357
+ threshold: Force threshold in Newtons
358
+
359
+ Returns:
360
+ True if force magnitude exceeds threshold
361
+ """
362
+ return self.get_force().magnitude() > threshold