foodforthought-cli 0.2.8__py3-none-any.whl → 0.3.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.
- ate/__init__.py +6 -0
- ate/__main__.py +16 -0
- ate/auth/__init__.py +1 -0
- ate/auth/device_flow.py +141 -0
- ate/auth/token_store.py +96 -0
- ate/behaviors/__init__.py +12 -0
- ate/behaviors/approach.py +399 -0
- ate/cli.py +855 -4551
- ate/client.py +90 -0
- ate/commands/__init__.py +168 -0
- ate/commands/auth.py +389 -0
- ate/commands/bridge.py +448 -0
- ate/commands/data.py +185 -0
- ate/commands/deps.py +111 -0
- ate/commands/generate.py +384 -0
- ate/commands/memory.py +907 -0
- ate/commands/parts.py +166 -0
- ate/commands/primitive.py +399 -0
- ate/commands/protocol.py +288 -0
- ate/commands/recording.py +524 -0
- ate/commands/repo.py +154 -0
- ate/commands/simulation.py +291 -0
- ate/commands/skill.py +303 -0
- ate/commands/skills.py +487 -0
- ate/commands/team.py +147 -0
- ate/commands/workflow.py +271 -0
- ate/detection/__init__.py +38 -0
- ate/detection/base.py +142 -0
- ate/detection/color_detector.py +399 -0
- ate/detection/trash_detector.py +322 -0
- ate/drivers/__init__.py +18 -6
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +360 -24
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +16 -0
- ate/interfaces/base.py +2 -0
- ate/interfaces/sensors.py +247 -0
- ate/llm_proxy.py +239 -0
- ate/memory/__init__.py +35 -0
- ate/memory/cloud.py +244 -0
- ate/memory/context.py +269 -0
- ate/memory/embeddings.py +184 -0
- ate/memory/export.py +26 -0
- ate/memory/merge.py +146 -0
- ate/memory/migrate/__init__.py +34 -0
- ate/memory/migrate/base.py +89 -0
- ate/memory/migrate/pipeline.py +189 -0
- ate/memory/migrate/sources/__init__.py +13 -0
- ate/memory/migrate/sources/chroma.py +170 -0
- ate/memory/migrate/sources/pinecone.py +120 -0
- ate/memory/migrate/sources/qdrant.py +110 -0
- ate/memory/migrate/sources/weaviate.py +160 -0
- ate/memory/reranker.py +353 -0
- ate/memory/search.py +26 -0
- ate/memory/store.py +548 -0
- ate/recording/__init__.py +42 -3
- ate/recording/session.py +12 -2
- ate/recording/visual.py +416 -0
- ate/robot/__init__.py +142 -0
- ate/robot/agentic_servo.py +856 -0
- ate/robot/behaviors.py +493 -0
- ate/robot/ble_capture.py +1000 -0
- ate/robot/ble_enumerate.py +506 -0
- ate/robot/calibration.py +88 -3
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +143 -11
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +104 -2
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +6 -0
- ate/robot/registry.py +5 -2
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +285 -3
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +9 -0
- ate/robot/visual_servo_loop.py +494 -0
- ate/robot/visual_servoing.py +570 -0
- ate/robot/visual_system_id.py +906 -0
- ate/transports/__init__.py +121 -0
- ate/transports/base.py +394 -0
- ate/transports/ble.py +405 -0
- ate/transports/hybrid.py +444 -0
- ate/transports/serial.py +345 -0
- ate/urdf/__init__.py +30 -0
- ate/urdf/capture.py +582 -0
- ate/urdf/cloud.py +491 -0
- ate/urdf/collision.py +271 -0
- ate/urdf/commands.py +708 -0
- ate/urdf/depth.py +360 -0
- ate/urdf/inertial.py +312 -0
- ate/urdf/kinematics.py +330 -0
- ate/urdf/lifting.py +415 -0
- ate/urdf/meshing.py +300 -0
- ate/urdf/models/__init__.py +110 -0
- ate/urdf/models/depth_anything.py +253 -0
- ate/urdf/models/sam2.py +324 -0
- ate/urdf/motion_analysis.py +396 -0
- ate/urdf/pipeline.py +468 -0
- ate/urdf/scale.py +256 -0
- ate/urdf/scan_session.py +411 -0
- ate/urdf/segmentation.py +299 -0
- ate/urdf/synthesis.py +319 -0
- ate/urdf/topology.py +336 -0
- ate/urdf/validation.py +371 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +1 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ArUco Marker PDF Generator
|
|
4
|
+
===========================
|
|
5
|
+
|
|
6
|
+
Generates printable ArUco markers for robot calibration.
|
|
7
|
+
Solves the pain point of users needing to figure out marker sizes,
|
|
8
|
+
print settings, and cutting guides.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
ate robot generate-markers --output markers.pdf
|
|
12
|
+
ate robot generate-markers --count 10 --size 30 --output markers.pdf
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import io
|
|
16
|
+
import math
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional, List, Tuple
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import cv2
|
|
23
|
+
import numpy as np
|
|
24
|
+
HAS_OPENCV = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
HAS_OPENCV = False
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from reportlab.lib.pagesizes import LETTER, A4
|
|
30
|
+
from reportlab.lib.units import mm, inch
|
|
31
|
+
from reportlab.pdfgen import canvas
|
|
32
|
+
from reportlab.lib.colors import black, gray, white, lightgrey
|
|
33
|
+
HAS_REPORTLAB = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
HAS_REPORTLAB = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class MarkerSpec:
|
|
40
|
+
"""Specification for a single marker."""
|
|
41
|
+
id: int
|
|
42
|
+
label: str
|
|
43
|
+
size_mm: float
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_dependencies():
|
|
47
|
+
"""Check if required dependencies are installed."""
|
|
48
|
+
missing = []
|
|
49
|
+
if not HAS_OPENCV:
|
|
50
|
+
missing.append("opencv-contrib-python")
|
|
51
|
+
if not HAS_REPORTLAB:
|
|
52
|
+
missing.append("reportlab")
|
|
53
|
+
|
|
54
|
+
if missing:
|
|
55
|
+
raise ImportError(
|
|
56
|
+
f"Missing required packages: {', '.join(missing)}. "
|
|
57
|
+
f"Install with: pip install {' '.join(missing)}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def generate_aruco_image(
|
|
62
|
+
marker_id: int,
|
|
63
|
+
size_pixels: int = 200,
|
|
64
|
+
dictionary_id: int = cv2.aruco.DICT_4X4_50
|
|
65
|
+
) -> np.ndarray:
|
|
66
|
+
"""Generate an ArUco marker image.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
marker_id: The marker ID (0-49 for DICT_4X4_50)
|
|
70
|
+
size_pixels: Output image size in pixels
|
|
71
|
+
dictionary_id: ArUco dictionary to use
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Numpy array with the marker image (grayscale)
|
|
75
|
+
"""
|
|
76
|
+
dictionary = cv2.aruco.getPredefinedDictionary(dictionary_id)
|
|
77
|
+
marker_image = cv2.aruco.generateImageMarker(dictionary, marker_id, size_pixels)
|
|
78
|
+
return marker_image
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def generate_marker_pdf(
|
|
82
|
+
output_path: str,
|
|
83
|
+
marker_specs: Optional[List[MarkerSpec]] = None,
|
|
84
|
+
count: int = 12,
|
|
85
|
+
size_mm: float = 30.0,
|
|
86
|
+
page_size: Tuple[float, float] = LETTER,
|
|
87
|
+
include_instructions: bool = True,
|
|
88
|
+
robot_name: Optional[str] = None,
|
|
89
|
+
) -> str:
|
|
90
|
+
"""Generate a PDF with printable ArUco markers.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
output_path: Path to save the PDF
|
|
94
|
+
marker_specs: Optional list of marker specifications. If None, generates
|
|
95
|
+
generic markers with IDs 0 to count-1.
|
|
96
|
+
count: Number of markers to generate (ignored if marker_specs provided)
|
|
97
|
+
size_mm: Marker size in millimeters (ignored if marker_specs provided)
|
|
98
|
+
page_size: Page size tuple (width, height) in points
|
|
99
|
+
include_instructions: Include usage instructions on first page
|
|
100
|
+
robot_name: Optional robot name for labeling
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Path to the generated PDF
|
|
104
|
+
"""
|
|
105
|
+
check_dependencies()
|
|
106
|
+
|
|
107
|
+
# Default marker specs if not provided
|
|
108
|
+
if marker_specs is None:
|
|
109
|
+
marker_specs = [
|
|
110
|
+
MarkerSpec(id=i, label=f"Joint {i}", size_mm=size_mm)
|
|
111
|
+
for i in range(count)
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# Create PDF
|
|
115
|
+
c = canvas.Canvas(output_path, pagesize=page_size)
|
|
116
|
+
page_width, page_height = page_size
|
|
117
|
+
|
|
118
|
+
# Margins
|
|
119
|
+
margin = 0.5 * inch
|
|
120
|
+
usable_width = page_width - 2 * margin
|
|
121
|
+
usable_height = page_height - 2 * margin
|
|
122
|
+
|
|
123
|
+
# Instructions page
|
|
124
|
+
if include_instructions:
|
|
125
|
+
_draw_instructions_page(c, page_width, page_height, robot_name)
|
|
126
|
+
c.showPage()
|
|
127
|
+
|
|
128
|
+
# Calculate grid layout
|
|
129
|
+
# Add 10mm margin around each marker for cutting
|
|
130
|
+
marker_with_margin = size_mm + 20 # 10mm on each side
|
|
131
|
+
|
|
132
|
+
cols = int(usable_width / (marker_with_margin * mm))
|
|
133
|
+
rows = int(usable_height / (marker_with_margin * mm))
|
|
134
|
+
|
|
135
|
+
if cols < 1:
|
|
136
|
+
cols = 1
|
|
137
|
+
if rows < 1:
|
|
138
|
+
rows = 1
|
|
139
|
+
|
|
140
|
+
markers_per_page = cols * rows
|
|
141
|
+
|
|
142
|
+
# Center the grid
|
|
143
|
+
grid_width = cols * marker_with_margin * mm
|
|
144
|
+
grid_height = rows * marker_with_margin * mm
|
|
145
|
+
start_x = (page_width - grid_width) / 2 + 10 * mm
|
|
146
|
+
start_y = page_height - (page_height - grid_height) / 2 - 10 * mm - size_mm * mm
|
|
147
|
+
|
|
148
|
+
current_marker = 0
|
|
149
|
+
|
|
150
|
+
while current_marker < len(marker_specs):
|
|
151
|
+
# Draw markers on this page
|
|
152
|
+
for row in range(rows):
|
|
153
|
+
if current_marker >= len(marker_specs):
|
|
154
|
+
break
|
|
155
|
+
for col in range(cols):
|
|
156
|
+
if current_marker >= len(marker_specs):
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
spec = marker_specs[current_marker]
|
|
160
|
+
x = start_x + col * marker_with_margin * mm
|
|
161
|
+
y = start_y - row * marker_with_margin * mm
|
|
162
|
+
|
|
163
|
+
_draw_marker_cell(c, spec, x, y, spec.size_mm * mm)
|
|
164
|
+
current_marker += 1
|
|
165
|
+
|
|
166
|
+
# Page number
|
|
167
|
+
c.setFont("Helvetica", 9)
|
|
168
|
+
c.setFillColor(gray)
|
|
169
|
+
page_num = (current_marker - 1) // markers_per_page + 1
|
|
170
|
+
if include_instructions:
|
|
171
|
+
page_num += 1
|
|
172
|
+
c.drawCentredString(page_width / 2, 0.3 * inch, f"Page {page_num}")
|
|
173
|
+
|
|
174
|
+
if current_marker < len(marker_specs):
|
|
175
|
+
c.showPage()
|
|
176
|
+
|
|
177
|
+
c.save()
|
|
178
|
+
return output_path
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _draw_instructions_page(
|
|
182
|
+
c: canvas.Canvas,
|
|
183
|
+
page_width: float,
|
|
184
|
+
page_height: float,
|
|
185
|
+
robot_name: Optional[str] = None,
|
|
186
|
+
):
|
|
187
|
+
"""Draw the instructions page."""
|
|
188
|
+
# Title
|
|
189
|
+
c.setFont("Helvetica-Bold", 24)
|
|
190
|
+
c.setFillColor(black)
|
|
191
|
+
title = "ArUco Calibration Markers"
|
|
192
|
+
if robot_name:
|
|
193
|
+
title = f"ArUco Markers for {robot_name}"
|
|
194
|
+
c.drawCentredString(page_width / 2, page_height - 1 * inch, title)
|
|
195
|
+
|
|
196
|
+
# Subtitle
|
|
197
|
+
c.setFont("Helvetica", 14)
|
|
198
|
+
c.setFillColor(gray)
|
|
199
|
+
c.drawCentredString(
|
|
200
|
+
page_width / 2,
|
|
201
|
+
page_height - 1.4 * inch,
|
|
202
|
+
"FoodforThought Robot Calibration System"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Instructions box
|
|
206
|
+
box_x = 1 * inch
|
|
207
|
+
box_y = page_height - 6 * inch
|
|
208
|
+
box_width = page_width - 2 * inch
|
|
209
|
+
box_height = 4 * inch
|
|
210
|
+
|
|
211
|
+
c.setFillColor(lightgrey)
|
|
212
|
+
c.rect(box_x, box_y, box_width, box_height, fill=True, stroke=False)
|
|
213
|
+
|
|
214
|
+
c.setFillColor(black)
|
|
215
|
+
c.setFont("Helvetica-Bold", 14)
|
|
216
|
+
c.drawString(box_x + 0.2 * inch, box_y + box_height - 0.4 * inch, "Instructions")
|
|
217
|
+
|
|
218
|
+
instructions = [
|
|
219
|
+
"1. Print this PDF at 100% scale (do not fit to page)",
|
|
220
|
+
"2. Cut out each marker along the dashed lines",
|
|
221
|
+
"3. Attach markers to each moving joint of your robot:",
|
|
222
|
+
" - Use tape or adhesive putty for temporary mounting",
|
|
223
|
+
" - Place marker on rigid part of each joint segment",
|
|
224
|
+
" - Ensure markers face the camera and are visible",
|
|
225
|
+
"4. Run the calibration command:",
|
|
226
|
+
" ate robot calibrate --method aruco",
|
|
227
|
+
"5. Follow the on-screen prompts to wiggle each joint",
|
|
228
|
+
"6. Upload your calibration to share with the community!",
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
c.setFont("Helvetica", 11)
|
|
232
|
+
y = box_y + box_height - 0.8 * inch
|
|
233
|
+
for line in instructions:
|
|
234
|
+
c.drawString(box_x + 0.3 * inch, y, line)
|
|
235
|
+
y -= 0.35 * inch
|
|
236
|
+
|
|
237
|
+
# Tips box
|
|
238
|
+
tips_y = box_y - 0.5 * inch - 2.5 * inch
|
|
239
|
+
tips_height = 2.5 * inch
|
|
240
|
+
|
|
241
|
+
c.setFillColor(white)
|
|
242
|
+
c.setStrokeColor(gray)
|
|
243
|
+
c.rect(box_x, tips_y, box_width, tips_height, fill=True, stroke=True)
|
|
244
|
+
|
|
245
|
+
c.setFillColor(black)
|
|
246
|
+
c.setFont("Helvetica-Bold", 12)
|
|
247
|
+
c.drawString(box_x + 0.2 * inch, tips_y + tips_height - 0.4 * inch, "Tips for Best Results")
|
|
248
|
+
|
|
249
|
+
tips = [
|
|
250
|
+
"- Ensure good, even lighting (avoid shadows and glare)",
|
|
251
|
+
"- Keep markers flat and perpendicular to camera when possible",
|
|
252
|
+
"- Use the largest markers that fit on your robot's joints",
|
|
253
|
+
"- Each marker ID should only appear once on the robot",
|
|
254
|
+
"- Test detection before running full calibration",
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
c.setFont("Helvetica", 10)
|
|
258
|
+
y = tips_y + tips_height - 0.7 * inch
|
|
259
|
+
for tip in tips:
|
|
260
|
+
c.drawString(box_x + 0.3 * inch, y, tip)
|
|
261
|
+
y -= 0.3 * inch
|
|
262
|
+
|
|
263
|
+
# Footer
|
|
264
|
+
c.setFont("Helvetica", 9)
|
|
265
|
+
c.setFillColor(gray)
|
|
266
|
+
c.drawCentredString(
|
|
267
|
+
page_width / 2,
|
|
268
|
+
0.5 * inch,
|
|
269
|
+
"Generated by FoodforThought CLI | kindlyrobotics.com"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _draw_marker_cell(
|
|
274
|
+
c: canvas.Canvas,
|
|
275
|
+
spec: MarkerSpec,
|
|
276
|
+
x: float,
|
|
277
|
+
y: float,
|
|
278
|
+
size: float,
|
|
279
|
+
):
|
|
280
|
+
"""Draw a single marker with cutting guides and label."""
|
|
281
|
+
# Generate marker image
|
|
282
|
+
marker_img = generate_aruco_image(spec.id, size_pixels=300)
|
|
283
|
+
|
|
284
|
+
# Convert to PIL for ReportLab
|
|
285
|
+
from PIL import Image
|
|
286
|
+
pil_img = Image.fromarray(marker_img)
|
|
287
|
+
|
|
288
|
+
# Save to bytes
|
|
289
|
+
img_buffer = io.BytesIO()
|
|
290
|
+
pil_img.save(img_buffer, format='PNG')
|
|
291
|
+
img_buffer.seek(0)
|
|
292
|
+
|
|
293
|
+
# Draw cutting guides (dashed rectangle)
|
|
294
|
+
padding = 5 * mm
|
|
295
|
+
cut_x = x - padding
|
|
296
|
+
cut_y = y - padding
|
|
297
|
+
cut_size = size + 2 * padding
|
|
298
|
+
|
|
299
|
+
c.setStrokeColor(gray)
|
|
300
|
+
c.setDash(3, 3)
|
|
301
|
+
c.rect(cut_x, cut_y, cut_size, cut_size, fill=False, stroke=True)
|
|
302
|
+
c.setDash() # Reset to solid
|
|
303
|
+
|
|
304
|
+
# Corner marks for cutting
|
|
305
|
+
mark_len = 3 * mm
|
|
306
|
+
c.setStrokeColor(black)
|
|
307
|
+
c.setLineWidth(0.5)
|
|
308
|
+
|
|
309
|
+
# Top-left corner
|
|
310
|
+
c.line(cut_x - mark_len, cut_y + cut_size, cut_x, cut_y + cut_size)
|
|
311
|
+
c.line(cut_x, cut_y + cut_size, cut_x, cut_y + cut_size + mark_len)
|
|
312
|
+
|
|
313
|
+
# Top-right corner
|
|
314
|
+
c.line(cut_x + cut_size, cut_y + cut_size, cut_x + cut_size + mark_len, cut_y + cut_size)
|
|
315
|
+
c.line(cut_x + cut_size, cut_y + cut_size, cut_x + cut_size, cut_y + cut_size + mark_len)
|
|
316
|
+
|
|
317
|
+
# Bottom-left corner
|
|
318
|
+
c.line(cut_x - mark_len, cut_y, cut_x, cut_y)
|
|
319
|
+
c.line(cut_x, cut_y, cut_x, cut_y - mark_len)
|
|
320
|
+
|
|
321
|
+
# Bottom-right corner
|
|
322
|
+
c.line(cut_x + cut_size, cut_y, cut_x + cut_size + mark_len, cut_y)
|
|
323
|
+
c.line(cut_x + cut_size, cut_y, cut_x + cut_size, cut_y - mark_len)
|
|
324
|
+
|
|
325
|
+
# Draw the marker image
|
|
326
|
+
from reportlab.lib.utils import ImageReader
|
|
327
|
+
img_reader = ImageReader(img_buffer)
|
|
328
|
+
c.drawImage(img_reader, x, y, width=size, height=size)
|
|
329
|
+
|
|
330
|
+
# Label below marker
|
|
331
|
+
c.setFont("Helvetica", 8)
|
|
332
|
+
c.setFillColor(black)
|
|
333
|
+
label = f"ID: {spec.id}"
|
|
334
|
+
if spec.label:
|
|
335
|
+
label = f"{spec.label} (ID: {spec.id})"
|
|
336
|
+
c.drawCentredString(x + size / 2, y - 4 * mm, label)
|
|
337
|
+
|
|
338
|
+
# Size indicator
|
|
339
|
+
c.setFont("Helvetica", 6)
|
|
340
|
+
c.setFillColor(gray)
|
|
341
|
+
c.drawCentredString(x + size / 2, y - 7 * mm, f"{spec.size_mm:.0f}mm")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def generate_robot_markers(
|
|
345
|
+
robot_joints: List[dict],
|
|
346
|
+
output_path: str,
|
|
347
|
+
robot_name: str,
|
|
348
|
+
base_size_mm: float = 25.0,
|
|
349
|
+
) -> str:
|
|
350
|
+
"""Generate markers specifically for a robot's joints.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
robot_joints: List of joint dicts with 'name', 'type', optional 'segment'
|
|
354
|
+
output_path: Path to save the PDF
|
|
355
|
+
robot_name: Name of the robot
|
|
356
|
+
base_size_mm: Base marker size in mm
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Path to the generated PDF
|
|
360
|
+
"""
|
|
361
|
+
# Create marker specs from joint definitions
|
|
362
|
+
specs = []
|
|
363
|
+
for i, joint in enumerate(robot_joints):
|
|
364
|
+
name = joint.get('name', f'Joint {i}')
|
|
365
|
+
segment = joint.get('segment', '')
|
|
366
|
+
|
|
367
|
+
label = name
|
|
368
|
+
if segment:
|
|
369
|
+
label = f"{segment}: {name}"
|
|
370
|
+
|
|
371
|
+
specs.append(MarkerSpec(
|
|
372
|
+
id=i,
|
|
373
|
+
label=label,
|
|
374
|
+
size_mm=base_size_mm,
|
|
375
|
+
))
|
|
376
|
+
|
|
377
|
+
return generate_marker_pdf(
|
|
378
|
+
output_path=output_path,
|
|
379
|
+
marker_specs=specs,
|
|
380
|
+
robot_name=robot_name,
|
|
381
|
+
include_instructions=True,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def generate_quick_markers(
|
|
386
|
+
output_path: str = "aruco_markers.pdf",
|
|
387
|
+
count: int = 12,
|
|
388
|
+
size_mm: float = 30.0,
|
|
389
|
+
) -> str:
|
|
390
|
+
"""Quick generation of generic calibration markers.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
output_path: Path to save the PDF
|
|
394
|
+
count: Number of markers to generate
|
|
395
|
+
size_mm: Size of each marker in millimeters
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Path to the generated PDF
|
|
399
|
+
"""
|
|
400
|
+
return generate_marker_pdf(
|
|
401
|
+
output_path=output_path,
|
|
402
|
+
count=count,
|
|
403
|
+
size_mm=size_mm,
|
|
404
|
+
include_instructions=True,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# Robot-specific marker presets
|
|
409
|
+
# These encode expert knowledge about marker placement and sizes
|
|
410
|
+
ROBOT_MARKER_PRESETS = {
|
|
411
|
+
"mechdog-mini": {
|
|
412
|
+
"robot_name": "Hiwonder MechDog Mini",
|
|
413
|
+
"markers": [
|
|
414
|
+
# Body - large, visible from distance
|
|
415
|
+
MarkerSpec(id=0, label="Body Center", size_mm=35),
|
|
416
|
+
# Front Right Leg
|
|
417
|
+
MarkerSpec(id=1, label="FR Hip", size_mm=25),
|
|
418
|
+
MarkerSpec(id=2, label="FR Thigh", size_mm=25),
|
|
419
|
+
MarkerSpec(id=3, label="FR Shin", size_mm=20),
|
|
420
|
+
# Front Left Leg
|
|
421
|
+
MarkerSpec(id=4, label="FL Hip", size_mm=25),
|
|
422
|
+
MarkerSpec(id=5, label="FL Thigh", size_mm=25),
|
|
423
|
+
MarkerSpec(id=6, label="FL Shin", size_mm=20),
|
|
424
|
+
# Back Right Leg
|
|
425
|
+
MarkerSpec(id=7, label="BR Hip", size_mm=25),
|
|
426
|
+
MarkerSpec(id=8, label="BR Thigh", size_mm=25),
|
|
427
|
+
MarkerSpec(id=9, label="BR Shin", size_mm=20),
|
|
428
|
+
# Back Left Leg
|
|
429
|
+
MarkerSpec(id=10, label="BL Hip", size_mm=25),
|
|
430
|
+
MarkerSpec(id=11, label="BL Thigh", size_mm=25),
|
|
431
|
+
MarkerSpec(id=12, label="BL Shin", size_mm=20),
|
|
432
|
+
# Arm (servo 11, 12, 13 from our earlier discovery)
|
|
433
|
+
MarkerSpec(id=13, label="Arm Shoulder", size_mm=25),
|
|
434
|
+
MarkerSpec(id=14, label="Arm Elbow", size_mm=20),
|
|
435
|
+
MarkerSpec(id=15, label="Gripper", size_mm=15),
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
"unitree-go1": {
|
|
439
|
+
"robot_name": "Unitree Go1",
|
|
440
|
+
"markers": [
|
|
441
|
+
MarkerSpec(id=0, label="Body Center", size_mm=60),
|
|
442
|
+
# Larger robot = larger markers
|
|
443
|
+
MarkerSpec(id=1, label="FR Hip", size_mm=40),
|
|
444
|
+
MarkerSpec(id=2, label="FR Thigh", size_mm=40),
|
|
445
|
+
MarkerSpec(id=3, label="FR Calf", size_mm=35),
|
|
446
|
+
MarkerSpec(id=4, label="FL Hip", size_mm=40),
|
|
447
|
+
MarkerSpec(id=5, label="FL Thigh", size_mm=40),
|
|
448
|
+
MarkerSpec(id=6, label="FL Calf", size_mm=35),
|
|
449
|
+
MarkerSpec(id=7, label="RR Hip", size_mm=40),
|
|
450
|
+
MarkerSpec(id=8, label="RR Thigh", size_mm=40),
|
|
451
|
+
MarkerSpec(id=9, label="RR Calf", size_mm=35),
|
|
452
|
+
MarkerSpec(id=10, label="RL Hip", size_mm=40),
|
|
453
|
+
MarkerSpec(id=11, label="RL Thigh", size_mm=40),
|
|
454
|
+
MarkerSpec(id=12, label="RL Calf", size_mm=35),
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
"xarm-6": {
|
|
458
|
+
"robot_name": "xArm 6-DOF",
|
|
459
|
+
"markers": [
|
|
460
|
+
MarkerSpec(id=0, label="Base", size_mm=50),
|
|
461
|
+
MarkerSpec(id=1, label="Shoulder", size_mm=45),
|
|
462
|
+
MarkerSpec(id=2, label="Upper Arm", size_mm=40),
|
|
463
|
+
MarkerSpec(id=3, label="Forearm", size_mm=40),
|
|
464
|
+
MarkerSpec(id=4, label="Wrist 1", size_mm=35),
|
|
465
|
+
MarkerSpec(id=5, label="Wrist 2", size_mm=35),
|
|
466
|
+
MarkerSpec(id=6, label="End Effector", size_mm=30),
|
|
467
|
+
],
|
|
468
|
+
},
|
|
469
|
+
"generic-quadruped": {
|
|
470
|
+
"robot_name": "Generic Quadruped",
|
|
471
|
+
"markers": [
|
|
472
|
+
MarkerSpec(id=0, label="Body", size_mm=40),
|
|
473
|
+
MarkerSpec(id=1, label="FR Segment 1", size_mm=30),
|
|
474
|
+
MarkerSpec(id=2, label="FR Segment 2", size_mm=25),
|
|
475
|
+
MarkerSpec(id=3, label="FR Segment 3", size_mm=20),
|
|
476
|
+
MarkerSpec(id=4, label="FL Segment 1", size_mm=30),
|
|
477
|
+
MarkerSpec(id=5, label="FL Segment 2", size_mm=25),
|
|
478
|
+
MarkerSpec(id=6, label="FL Segment 3", size_mm=20),
|
|
479
|
+
MarkerSpec(id=7, label="BR Segment 1", size_mm=30),
|
|
480
|
+
MarkerSpec(id=8, label="BR Segment 2", size_mm=25),
|
|
481
|
+
MarkerSpec(id=9, label="BR Segment 3", size_mm=20),
|
|
482
|
+
MarkerSpec(id=10, label="BL Segment 1", size_mm=30),
|
|
483
|
+
MarkerSpec(id=11, label="BL Segment 2", size_mm=25),
|
|
484
|
+
MarkerSpec(id=12, label="BL Segment 3", size_mm=20),
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
"generic-arm": {
|
|
488
|
+
"robot_name": "Generic Robot Arm",
|
|
489
|
+
"markers": [
|
|
490
|
+
MarkerSpec(id=0, label="Base", size_mm=45),
|
|
491
|
+
MarkerSpec(id=1, label="Joint 1", size_mm=40),
|
|
492
|
+
MarkerSpec(id=2, label="Joint 2", size_mm=35),
|
|
493
|
+
MarkerSpec(id=3, label="Joint 3", size_mm=35),
|
|
494
|
+
MarkerSpec(id=4, label="Joint 4", size_mm=30),
|
|
495
|
+
MarkerSpec(id=5, label="Joint 5", size_mm=30),
|
|
496
|
+
MarkerSpec(id=6, label="End Effector", size_mm=25),
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def get_preset_markers(preset_name: str) -> Tuple[List[MarkerSpec], str]:
|
|
503
|
+
"""Get marker specs for a known robot type.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
preset_name: Robot preset name (e.g., "mechdog-mini", "unitree-go1")
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Tuple of (marker_specs, robot_name)
|
|
510
|
+
|
|
511
|
+
Raises:
|
|
512
|
+
KeyError if preset not found
|
|
513
|
+
"""
|
|
514
|
+
if preset_name not in ROBOT_MARKER_PRESETS:
|
|
515
|
+
available = ", ".join(ROBOT_MARKER_PRESETS.keys())
|
|
516
|
+
raise KeyError(f"Unknown preset '{preset_name}'. Available: {available}")
|
|
517
|
+
|
|
518
|
+
preset = ROBOT_MARKER_PRESETS[preset_name]
|
|
519
|
+
return preset["markers"], preset["robot_name"]
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def list_presets() -> List[dict]:
|
|
523
|
+
"""List available robot presets."""
|
|
524
|
+
return [
|
|
525
|
+
{
|
|
526
|
+
"name": name,
|
|
527
|
+
"robot_name": preset["robot_name"],
|
|
528
|
+
"marker_count": len(preset["markers"]),
|
|
529
|
+
}
|
|
530
|
+
for name, preset in ROBOT_MARKER_PRESETS.items()
|
|
531
|
+
]
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
# CLI integration
|
|
535
|
+
def add_marker_command(subparsers):
|
|
536
|
+
"""Add the generate-markers command to the CLI."""
|
|
537
|
+
parser = subparsers.add_parser(
|
|
538
|
+
"generate-markers",
|
|
539
|
+
help="Generate printable ArUco markers for robot calibration",
|
|
540
|
+
)
|
|
541
|
+
parser.add_argument(
|
|
542
|
+
"--output", "-o",
|
|
543
|
+
default="aruco_markers.pdf",
|
|
544
|
+
help="Output PDF path (default: aruco_markers.pdf)",
|
|
545
|
+
)
|
|
546
|
+
parser.add_argument(
|
|
547
|
+
"--count", "-n",
|
|
548
|
+
type=int,
|
|
549
|
+
default=12,
|
|
550
|
+
help="Number of markers to generate (default: 12)",
|
|
551
|
+
)
|
|
552
|
+
parser.add_argument(
|
|
553
|
+
"--size",
|
|
554
|
+
type=float,
|
|
555
|
+
default=30.0,
|
|
556
|
+
help="Marker size in millimeters (default: 30)",
|
|
557
|
+
)
|
|
558
|
+
parser.add_argument(
|
|
559
|
+
"--robot-name",
|
|
560
|
+
help="Optional robot name for labeling",
|
|
561
|
+
)
|
|
562
|
+
parser.add_argument(
|
|
563
|
+
"--page-size",
|
|
564
|
+
choices=["letter", "a4"],
|
|
565
|
+
default="letter",
|
|
566
|
+
help="Page size (default: letter)",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
return parser
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def run_marker_command(args):
|
|
573
|
+
"""Run the generate-markers command."""
|
|
574
|
+
from reportlab.lib.pagesizes import LETTER, A4
|
|
575
|
+
|
|
576
|
+
page_size = LETTER if args.page_size == "letter" else A4
|
|
577
|
+
|
|
578
|
+
print(f"Generating {args.count} ArUco markers...")
|
|
579
|
+
print(f" Size: {args.size}mm")
|
|
580
|
+
print(f" Output: {args.output}")
|
|
581
|
+
|
|
582
|
+
output = generate_marker_pdf(
|
|
583
|
+
output_path=args.output,
|
|
584
|
+
count=args.count,
|
|
585
|
+
size_mm=args.size,
|
|
586
|
+
page_size=page_size,
|
|
587
|
+
robot_name=args.robot_name,
|
|
588
|
+
include_instructions=True,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
print(f"\n[OK] Markers saved to: {output}")
|
|
592
|
+
print("\nNext steps:")
|
|
593
|
+
print(" 1. Print the PDF at 100% scale")
|
|
594
|
+
print(" 2. Cut out markers along the dashed lines")
|
|
595
|
+
print(" 3. Attach to your robot's joints")
|
|
596
|
+
print(" 4. Run: ate robot calibrate --method aruco")
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
if __name__ == "__main__":
|
|
600
|
+
# Quick test
|
|
601
|
+
import sys
|
|
602
|
+
|
|
603
|
+
output = "/tmp/test_markers.pdf"
|
|
604
|
+
print(f"Generating test markers to {output}...")
|
|
605
|
+
|
|
606
|
+
try:
|
|
607
|
+
generate_quick_markers(output, count=6, size_mm=40)
|
|
608
|
+
print(f"Done! Open {output} to view.")
|
|
609
|
+
except ImportError as e:
|
|
610
|
+
print(f"Error: {e}")
|
|
611
|
+
sys.exit(1)
|