Add real-arm deployment code and final RL policy
Browse files- .gitattributes +24 -0
- rl_deploy/README.md +47 -0
- rl_deploy/calib_debug/p1_1792287841_after.png +3 -0
- rl_deploy/calib_debug/p1_1792287841_bluemask.png +0 -0
- rl_deploy/calib_debug/p1_1792287841_ref.png +3 -0
- rl_deploy/calib_debug/p2_1792287871_after.png +3 -0
- rl_deploy/calib_debug/p2_1792287871_bluemask.png +0 -0
- rl_deploy/calib_debug/p2_1792287871_ref.png +3 -0
- rl_deploy/calib_debug/p3_1792287891_after.png +3 -0
- rl_deploy/calib_debug/p3_1792287891_bluemask.png +0 -0
- rl_deploy/calib_debug/p3_1792287891_ref.png +3 -0
- rl_deploy/calib_debug/p4_1792287916_after.png +3 -0
- rl_deploy/calib_debug/p4_1792287916_bluemask.png +0 -0
- rl_deploy/calib_debug/p4_1792287916_ref.png +3 -0
- rl_deploy/calib_debug/p5_1792287936_after.png +3 -0
- rl_deploy/calib_debug/p5_1792287936_bluemask.png +0 -0
- rl_deploy/calib_debug/p5_1792287936_ref.png +3 -0
- rl_deploy/calib_debug/p6_1792287959_after.png +3 -0
- rl_deploy/calib_debug/p6_1792287959_bluemask.png +0 -0
- rl_deploy/calib_debug/p6_1792287959_ref.png +3 -0
- rl_deploy/calib_debug/p7_1792287977_after.png +3 -0
- rl_deploy/calib_debug/p7_1792287977_bluemask.png +0 -0
- rl_deploy/calib_debug/p7_1792287977_ref.png +3 -0
- rl_deploy/calib_debug/p8_1792287997_after.png +3 -0
- rl_deploy/calib_debug/p8_1792287997_bluemask.png +0 -0
- rl_deploy/calib_debug/p8_1792287997_ref.png +3 -0
- rl_deploy/calib_point_1.png +3 -0
- rl_deploy/calib_point_2.png +3 -0
- rl_deploy/calib_point_3.png +3 -0
- rl_deploy/calib_point_4.png +3 -0
- rl_deploy/calib_point_5.png +3 -0
- rl_deploy/calib_point_6.png +3 -0
- rl_deploy/calib_point_7.png +3 -0
- rl_deploy/calib_point_8.png +3 -0
- rl_deploy/calib_points.json +162 -0
- rl_deploy/calibrate_table.py +143 -0
- rl_deploy/common.py +164 -0
- rl_deploy/deploy_rl.py +156 -0
- rl_deploy/export_policy.py +42 -0
- rl_deploy/policy.pt +3 -0
- rl_deploy/policy_final.pt +3 -0
- rl_deploy/so101_fk.py +83 -0
- rl_deploy/so101_new_calib.urdf +453 -0
- rl_deploy/table_calib.json +205 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,27 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
rl_deploy/calib_debug/p1_1792287841_after.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
rl_deploy/calib_debug/p1_1792287841_ref.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
rl_deploy/calib_debug/p2_1792287871_after.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
rl_deploy/calib_debug/p2_1792287871_ref.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
rl_deploy/calib_debug/p3_1792287891_after.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
rl_deploy/calib_debug/p3_1792287891_ref.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
rl_deploy/calib_debug/p4_1792287916_after.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
rl_deploy/calib_debug/p4_1792287916_ref.png filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
rl_deploy/calib_debug/p5_1792287936_after.png filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
rl_deploy/calib_debug/p5_1792287936_ref.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
rl_deploy/calib_debug/p6_1792287959_after.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
rl_deploy/calib_debug/p6_1792287959_ref.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
rl_deploy/calib_debug/p7_1792287977_after.png filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
rl_deploy/calib_debug/p7_1792287977_ref.png filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
rl_deploy/calib_debug/p8_1792287997_after.png filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
rl_deploy/calib_debug/p8_1792287997_ref.png filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
rl_deploy/calib_point_1.png filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
rl_deploy/calib_point_2.png filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
rl_deploy/calib_point_3.png filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
rl_deploy/calib_point_4.png filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
rl_deploy/calib_point_5.png filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
rl_deploy/calib_point_6.png filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
rl_deploy/calib_point_7.png filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
rl_deploy/calib_point_8.png filter=lfs diff=lfs merge=lfs -text
|
rl_deploy/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# RL candy-pick policy on the real SO101
|
| 2 |
+
|
| 3 |
+
Trained in Isaac Lab (`Isaac-SO101-CandyPick-Direct`, state-based PPO). The policy sees joint
|
| 4 |
+
angles plus the blue candy's table position, which is detected once from the phone camera. So the
|
| 5 |
+
phone camera has to be calibrated against the table first.
|
| 6 |
+
|
| 7 |
+
None of these scripts move the arm unless you pass `--go` to `deploy_rl.py` and type `go`.
|
| 8 |
+
|
| 9 |
+
## 1. Calibrate the phone against the table (once per camera/robot placement)
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
cd ~/B1Hackathon/tasks/candy_sort/rl_deploy && ~/miniconda3/envs/lerobot/bin/python calibrate_table.py --points 8
|
| 13 |
+
```
|
| 14 |
+
The follower's torque is switched off. For each point, put one blue candy on the table and press
|
| 15 |
+
Enter. Then move the arm by hand until the gripper tip touches the middle of the candy, and press
|
| 16 |
+
Enter. Spread the points over the area where candies go. The script saves `table_calib.json` and
|
| 17 |
+
prints the fit error, which should be under 2 cm.
|
| 18 |
+
|
| 19 |
+
## 2. Get the policy (export runs on the L40S)
|
| 20 |
+
|
| 21 |
+
```bash
|
| 22 |
+
ssh drishti-karma-devarshi 'cd ~/work/twin && ~/miniconda3/envs/lerobot/bin/python export_policy.py $(ls -d ~/work/IsaacLab/logs/rsl_rl/so101_candy_pick/*v2* | tail -1)/model_1400.pt ~/work/twin/rl_policy.pt'
|
| 23 |
+
```
|
| 24 |
+
```bash
|
| 25 |
+
scp drishti-karma-devarshi:work/twin/rl_policy.pt ~/B1Hackathon/tasks/candy_sort/rl_deploy/policy.pt
|
| 26 |
+
```
|
| 27 |
+
Replace `model_1400.pt` with whichever checkpoint the thread recommends.
|
| 28 |
+
|
| 29 |
+
## 3. Run
|
| 30 |
+
|
| 31 |
+
Dry run first. It detects the candy, prints its table position and the targets it would send, and
|
| 32 |
+
does not move the arm:
|
| 33 |
+
```bash
|
| 34 |
+
cd ~/B1Hackathon/tasks/candy_sort/rl_deploy && ~/miniconda3/envs/lerobot/bin/python deploy_rl.py --policy policy.pt
|
| 35 |
+
```
|
| 36 |
+
Real run (the arm moves for 10 s; keep a hand on the power switch):
|
| 37 |
+
```bash
|
| 38 |
+
cd ~/B1Hackathon/tasks/candy_sort/rl_deploy && ~/miniconda3/envs/lerobot/bin/python deploy_rl.py --policy policy.pt --go
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
## Notes
|
| 42 |
+
- Candies must lie in the trained area. That's where you picked them in the grid recordings, in
|
| 43 |
+
front of the arm: about x -0.03..0.15 m and y -0.29..-0.15 m in the policy frame. The towel stays
|
| 44 |
+
where it was during those recordings.
|
| 45 |
+
- `so101_fk.py` is the arm's forward kinematics, from the official URDF. Its tip positions agree with
|
| 46 |
+
the simulator's to 1 cm (median) on 146 real poses.
|
| 47 |
+
- Only one blue candy is targeted per run. Candies the calibration places on the towel are ignored.
|
rl_deploy/calib_debug/p1_1792287841_after.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p1_1792287841_bluemask.png
ADDED
|
rl_deploy/calib_debug/p1_1792287841_ref.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p2_1792287871_after.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p2_1792287871_bluemask.png
ADDED
|
rl_deploy/calib_debug/p2_1792287871_ref.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p3_1792287891_after.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p3_1792287891_bluemask.png
ADDED
|
rl_deploy/calib_debug/p3_1792287891_ref.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p4_1792287916_after.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p4_1792287916_bluemask.png
ADDED
|
rl_deploy/calib_debug/p4_1792287916_ref.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p5_1792287936_after.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p5_1792287936_bluemask.png
ADDED
|
rl_deploy/calib_debug/p5_1792287936_ref.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p6_1792287959_after.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p6_1792287959_bluemask.png
ADDED
|
rl_deploy/calib_debug/p6_1792287959_ref.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p7_1792287977_after.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p7_1792287977_bluemask.png
ADDED
|
rl_deploy/calib_debug/p7_1792287977_ref.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p8_1792287997_after.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_debug/p8_1792287997_bluemask.png
ADDED
|
rl_deploy/calib_debug/p8_1792287997_ref.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_point_1.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_point_2.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_point_3.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_point_4.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_point_5.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_point_6.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_point_7.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_point_8.png
ADDED
|
Git LFS Details
|
rl_deploy/calib_points.json
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"uv": [
|
| 4 |
+
283.3632596685083,
|
| 5 |
+
232.50552486187846
|
| 6 |
+
],
|
| 7 |
+
"area": 724,
|
| 8 |
+
"joints_deg": [
|
| 9 |
+
-22.681318681318682,
|
| 10 |
+
-1.2307692307692308,
|
| 11 |
+
41.18681318681319,
|
| 12 |
+
39.07692307692308,
|
| 13 |
+
-2.5054945054945055,
|
| 14 |
+
1.1026878015161956
|
| 15 |
+
],
|
| 16 |
+
"tip": [
|
| 17 |
+
0.0885746699621676,
|
| 18 |
+
-0.2211262158968657,
|
| 19 |
+
-0.0018728227502649436
|
| 20 |
+
]
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"uv": [
|
| 24 |
+
223.27258566978193,
|
| 25 |
+
174.84112149532712
|
| 26 |
+
],
|
| 27 |
+
"area": 642,
|
| 28 |
+
"joints_deg": [
|
| 29 |
+
-6.065934065934066,
|
| 30 |
+
13.098901098901099,
|
| 31 |
+
21.846153846153847,
|
| 32 |
+
39.42857142857143,
|
| 33 |
+
-2.4175824175824174,
|
| 34 |
+
1.1026878015161956
|
| 35 |
+
],
|
| 36 |
+
"tip": [
|
| 37 |
+
0.04521679450712202,
|
| 38 |
+
-0.28363020045294185,
|
| 39 |
+
0.00034617923123554314
|
| 40 |
+
]
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"uv": [
|
| 44 |
+
324.1647940074906,
|
| 45 |
+
146.14606741573033
|
| 46 |
+
],
|
| 47 |
+
"area": 534,
|
| 48 |
+
"joints_deg": [
|
| 49 |
+
10.54945054945055,
|
| 50 |
+
-5.450549450549451,
|
| 51 |
+
41.714285714285715,
|
| 52 |
+
58.59340659340659,
|
| 53 |
+
-2.32967032967033,
|
| 54 |
+
1.1026878015161956
|
| 55 |
+
],
|
| 56 |
+
"tip": [
|
| 57 |
+
-0.009996185007877991,
|
| 58 |
+
-0.19178363754922642,
|
| 59 |
+
0.006818864733087288
|
| 60 |
+
]
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"uv": [
|
| 64 |
+
323.44,
|
| 65 |
+
294.63470588235293
|
| 66 |
+
],
|
| 67 |
+
"area": 1700,
|
| 68 |
+
"joints_deg": [
|
| 69 |
+
-41.67032967032967,
|
| 70 |
+
7.824175824175824,
|
| 71 |
+
25.45054945054945,
|
| 72 |
+
52.879120879120876,
|
| 73 |
+
-2.5934065934065935,
|
| 74 |
+
1.1026878015161956
|
| 75 |
+
],
|
| 76 |
+
"tip": [
|
| 77 |
+
0.14299164473120482,
|
| 78 |
+
-0.19023792065397205,
|
| 79 |
+
0.003980910785919246
|
| 80 |
+
]
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"uv": [
|
| 84 |
+
446.7759784075574,
|
| 85 |
+
253.0425101214575
|
| 86 |
+
],
|
| 87 |
+
"area": 1482,
|
| 88 |
+
"joints_deg": [
|
| 89 |
+
-52.30769230769231,
|
| 90 |
+
-23.64835164835165,
|
| 91 |
+
56.30769230769231,
|
| 92 |
+
63.51648351648352,
|
| 93 |
+
84.26373626373626,
|
| 94 |
+
1.1026878015161956
|
| 95 |
+
],
|
| 96 |
+
"tip": [
|
| 97 |
+
0.09268488377845423,
|
| 98 |
+
-0.1230287165562727,
|
| 99 |
+
0.013203728373691447
|
| 100 |
+
]
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"uv": [
|
| 104 |
+
177.62869198312237,
|
| 105 |
+
300.67148884870403
|
| 106 |
+
],
|
| 107 |
+
"area": 1659,
|
| 108 |
+
"joints_deg": [
|
| 109 |
+
-32.879120879120876,
|
| 110 |
+
30.24175824175824,
|
| 111 |
+
-1.2747252747252746,
|
| 112 |
+
42.15384615384615,
|
| 113 |
+
-17.53846153846154,
|
| 114 |
+
1.1716057891109581
|
| 115 |
+
],
|
| 116 |
+
"tip": [
|
| 117 |
+
0.17211300438132343,
|
| 118 |
+
-0.2762982688066562,
|
| 119 |
+
-0.005484126509456877
|
| 120 |
+
]
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"uv": [
|
| 124 |
+
203.57355679702047,
|
| 125 |
+
143.64245810055866
|
| 126 |
+
],
|
| 127 |
+
"area": 537,
|
| 128 |
+
"joints_deg": [
|
| 129 |
+
3.6043956043956045,
|
| 130 |
+
25.406593406593405,
|
| 131 |
+
5.934065934065934,
|
| 132 |
+
46.81318681318681,
|
| 133 |
+
-16.923076923076923,
|
| 134 |
+
1.1716057891109581
|
| 135 |
+
],
|
| 136 |
+
"tip": [
|
| 137 |
+
0.00809345599756148,
|
| 138 |
+
-0.30200666202286097,
|
| 139 |
+
-0.007828562295707622
|
| 140 |
+
]
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"uv": [
|
| 144 |
+
326.9448818897638,
|
| 145 |
+
185.59186351706037
|
| 146 |
+
],
|
| 147 |
+
"area": 762,
|
| 148 |
+
"joints_deg": [
|
| 149 |
+
-11.516483516483516,
|
| 150 |
+
-11.516483516483516,
|
| 151 |
+
47.07692307692308,
|
| 152 |
+
57.45054945054945,
|
| 153 |
+
-17.36263736263736,
|
| 154 |
+
1.1026878015161956
|
| 155 |
+
],
|
| 156 |
+
"tip": [
|
| 157 |
+
0.0432111876213237,
|
| 158 |
+
-0.18203129452029243,
|
| 159 |
+
0.008625103296823854
|
| 160 |
+
]
|
| 161 |
+
}
|
| 162 |
+
]
|
rl_deploy/calibrate_table.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Calibrate the phone camera against the table, using the arm itself as the measuring stick.
|
| 2 |
+
|
| 3 |
+
The script never moves the arm: the follower's torque is switched OFF and you move it by hand.
|
| 4 |
+
For every point it asks you to:
|
| 5 |
+
A. clear the spot (no candy there yet, hands out of view) -> reference photo
|
| 6 |
+
B. place ONE blue candy at a new spot (hands out of view) -> photo; only blue that is NEW
|
| 7 |
+
compared with the reference counts, so blue things already in the scene are ignored
|
| 8 |
+
C. check the detection (preview saved as calib_point_N.png) and confirm it
|
| 9 |
+
D. touch the gripper tip to the middle of the candy -> joint angles -> tip position
|
| 10 |
+
Points are saved after every step to calib_points.json, so a crash loses nothing: run again with
|
| 11 |
+
--resume to continue. With 6+ points it fits the pixel -> table map and saves table_calib.json.
|
| 12 |
+
|
| 13 |
+
cd ~/B1Hackathon/tasks/candy_sort/rl_deploy && ~/miniconda3/envs/lerobot/bin/python calibrate_table.py --points 8
|
| 14 |
+
~/miniconda3/envs/lerobot/bin/python calibrate_table.py --points 8 --resume # continue after a crash
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import argparse
|
| 18 |
+
import json
|
| 19 |
+
import os
|
| 20 |
+
import time
|
| 21 |
+
|
| 22 |
+
import cv2
|
| 23 |
+
import numpy as np
|
| 24 |
+
|
| 25 |
+
from common import CALIB, HERE, Phone, apply_h, blue_blobs, blue_mask, blue_report, connect_robot, fit_homography, read_joints
|
| 26 |
+
from so101_fk import SO101FK
|
| 27 |
+
|
| 28 |
+
POINTS = os.path.join(HERE, "calib_points.json")
|
| 29 |
+
MIN_PIXEL_GAP = 20 # a new point must be at least this far (px) from earlier ones
|
| 30 |
+
|
| 31 |
+
p = argparse.ArgumentParser()
|
| 32 |
+
p.add_argument("--points", type=int, default=8)
|
| 33 |
+
p.add_argument("--resume", action="store_true", help="keep the points in calib_points.json and add more")
|
| 34 |
+
p.add_argument("--phone-url", default=None, help="phone stream URL (default: PHONE_URL env or http://127.0.0.1:4747/video)")
|
| 35 |
+
a = p.parse_args()
|
| 36 |
+
|
| 37 |
+
pts = json.load(open(POINTS)) if (a.resume and os.path.exists(POINTS)) else []
|
| 38 |
+
if pts:
|
| 39 |
+
print(f"Resuming with {len(pts)} saved point(s).")
|
| 40 |
+
elif os.path.exists(POINTS):
|
| 41 |
+
print(f"Starting fresh (the old {os.path.basename(POINTS)} will be overwritten; use --resume to keep it).")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def save():
|
| 45 |
+
json.dump(pts, open(POINTS, "w"), indent=1)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def fit_and_report():
|
| 49 |
+
uv = np.array([q["uv"] for q in pts])
|
| 50 |
+
xy = np.array([q["tip"][:2] for q in pts])
|
| 51 |
+
H = fit_homography(uv, xy)
|
| 52 |
+
err = np.linalg.norm(apply_h(H, uv) - xy, axis=1)
|
| 53 |
+
loo = [] # leave-one-out: how well each point is predicted by the others
|
| 54 |
+
for i in range(len(pts)):
|
| 55 |
+
m = np.arange(len(pts)) != i
|
| 56 |
+
loo.append(np.linalg.norm(apply_h(fit_homography(uv[m], xy[m]), uv[i:i + 1])[0] - xy[i]))
|
| 57 |
+
print("\nPoint pixel (u, v) tip (x, y, z) [m] fit err leave-one-out err")
|
| 58 |
+
for i, q in enumerate(pts):
|
| 59 |
+
print(f"{i + 1:5d} ({q['uv'][0]:6.1f}, {q['uv'][1]:5.1f}) ({q['tip'][0]:+.3f}, {q['tip'][1]:+.3f}, {q['tip'][2]:+.3f})"
|
| 60 |
+
f" {1000 * err[i]:5.1f} mm {1000 * loo[i]:6.1f} mm")
|
| 61 |
+
print(f"Fit error median {1000 * np.median(err):.1f} mm, max {1000 * err.max():.1f} mm; "
|
| 62 |
+
f"leave-one-out median {1000 * np.median(loo):.1f} mm, max {1000 * max(loo):.1f} mm")
|
| 63 |
+
return H, err, loo
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
fk = SO101FK()
|
| 67 |
+
robot = connect_robot()
|
| 68 |
+
robot.bus.disable_torque()
|
| 69 |
+
print("\nFollower torque is OFF: you move the arm by hand. Nothing in this script moves it.")
|
| 70 |
+
print("Hold the arm when the script ends: it stays limp.\n")
|
| 71 |
+
phone = Phone(a.phone_url) if a.phone_url else Phone()
|
| 72 |
+
try:
|
| 73 |
+
while len(pts) < a.points:
|
| 74 |
+
k = len(pts) + 1
|
| 75 |
+
print(f"\n===== Point {k}/{a.points} =====")
|
| 76 |
+
input(f"[A] Make sure there is NO candy at the spot you will use next and your hands are out of view.\n"
|
| 77 |
+
f" Press Enter to take the reference photo...")
|
| 78 |
+
ref = phone.frame()
|
| 79 |
+
input("[B] Put ONE blue candy at a new spot in the candy area (not where earlier points were),\n"
|
| 80 |
+
" take your hands out of view, press Enter...")
|
| 81 |
+
img = phone.frame()
|
| 82 |
+
new = sorted(blue_blobs(img, ignore=ref), key=lambda b: -b["area"])
|
| 83 |
+
allb = blue_blobs(img)
|
| 84 |
+
# diagnostics: raw photos and blue masks are saved for every attempt
|
| 85 |
+
dd = os.path.join(HERE, "calib_debug")
|
| 86 |
+
os.makedirs(dd, exist_ok=True)
|
| 87 |
+
tag = f"p{k}_{int(time.time())}"
|
| 88 |
+
cv2.imwrite(os.path.join(dd, f"{tag}_ref.png"), cv2.cvtColor(ref, cv2.COLOR_RGB2BGR))
|
| 89 |
+
cv2.imwrite(os.path.join(dd, f"{tag}_after.png"), cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
|
| 90 |
+
cv2.imwrite(os.path.join(dd, f"{tag}_bluemask.png"), blue_mask(img))
|
| 91 |
+
rep = blue_report(img)
|
| 92 |
+
diff = float(np.abs(img.astype(np.int16) - ref.astype(np.int16)).mean())
|
| 93 |
+
print(f" photo {img.shape[1]}x{img.shape[0]}, brightness {rep['mean_brightness']:.0f}, change vs reference {diff:.1f} "
|
| 94 |
+
f"(near 0 = the two photos are the same frame)")
|
| 95 |
+
print(f" blue pixels {rep['blue_px']}; largest blue patch {rep['largest_area']} px at {rep['largest_at']} "
|
| 96 |
+
f"(blobs under 25 px are ignored); photos saved in calib_debug/{tag}_*.png")
|
| 97 |
+
print(f" blue blobs in the photo: {len(allb)}; NEW since the reference photo: {len(new)}")
|
| 98 |
+
for j, b in enumerate(new):
|
| 99 |
+
print(f" new blob {j + 1}: pixel ({b['uv'][0]:.0f}, {b['uv'][1]:.0f}), {b['area']} px")
|
| 100 |
+
if not new:
|
| 101 |
+
print(" No new blue candy found. Check it is in view and not hidden by the arm, then redo this point.")
|
| 102 |
+
continue
|
| 103 |
+
b = new[0]
|
| 104 |
+
near = [i + 1 for i, q in enumerate(pts) if np.linalg.norm(np.array(q["uv"]) - b["uv"]) < MIN_PIXEL_GAP]
|
| 105 |
+
if near:
|
| 106 |
+
print(f" This is the same spot as point(s) {near}. Move the candy somewhere new and redo this point.")
|
| 107 |
+
continue
|
| 108 |
+
dbg = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
| 109 |
+
for j, bb in enumerate(new):
|
| 110 |
+
cv2.circle(dbg, tuple(int(v) for v in bb["uv"]), 8, (0, 0, 255) if j == 0 else (0, 255, 255), 2)
|
| 111 |
+
prev = os.path.join(HERE, f"calib_point_{k}.png")
|
| 112 |
+
cv2.imwrite(prev, dbg)
|
| 113 |
+
print(f"[C] Using new blob 1 (red circle in {os.path.basename(prev)}).")
|
| 114 |
+
if input(" Is that the candy you placed? [Y/n] ").strip().lower().startswith("n"):
|
| 115 |
+
print(" Skipped. Redo this point.")
|
| 116 |
+
continue
|
| 117 |
+
input("[D] Move the gripper by hand until its tip touches the MIDDLE of that candy, press Enter...")
|
| 118 |
+
q = read_joints(robot)
|
| 119 |
+
tip = fk.tcp(q)
|
| 120 |
+
print(f" joints [deg] {np.round(q, 1).tolist()}")
|
| 121 |
+
print(f" tip position {np.round(tip, 3).tolist()} m (z should be about 0.00 to 0.02)")
|
| 122 |
+
if not -0.03 < tip[2] < 0.04:
|
| 123 |
+
print(" The tip is not at table height: lower the gripper onto the candy and redo this point.")
|
| 124 |
+
continue
|
| 125 |
+
pts.append({"uv": b["uv"].tolist(), "area": b["area"], "joints_deg": q.tolist(), "tip": tip.tolist()})
|
| 126 |
+
save()
|
| 127 |
+
print(f" Saved point {k} to {os.path.basename(POINTS)}. You can now remove the candy.")
|
| 128 |
+
if len(pts) >= 4:
|
| 129 |
+
fit_and_report()
|
| 130 |
+
finally:
|
| 131 |
+
phone.close()
|
| 132 |
+
robot.disconnect()
|
| 133 |
+
|
| 134 |
+
if len(pts) < 6:
|
| 135 |
+
raise SystemExit(f"Only {len(pts)} points; need at least 6. Run again with --resume.")
|
| 136 |
+
H, err, loo = fit_and_report()
|
| 137 |
+
json.dump({"H": H.tolist(), "image_size": [640, 360], "points": pts, "err_mm": (1000 * err).tolist(),
|
| 138 |
+
"loo_mm": [1000 * v for v in loo]}, open(CALIB, "w"), indent=1)
|
| 139 |
+
print(f"\nSaved {CALIB}")
|
| 140 |
+
if max(loo) > 0.02:
|
| 141 |
+
worst = int(np.argmax(loo)) + 1
|
| 142 |
+
print(f"Leave-one-out error above 2 cm (worst: point {worst}). Delete it from {os.path.basename(POINTS)} "
|
| 143 |
+
f"and add a point with --resume, or add more points.")
|
rl_deploy/common.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared pieces for the RL policy on the real SO101: robot/camera setup, blue-candy detection,
|
| 2 |
+
and the phone-pixel -> table homography."""
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
|
| 8 |
+
import cv2
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
+
CALIB = os.path.join(HERE, "table_calib.json")
|
| 13 |
+
FOLLOWER_PORT = "/dev/serial/by-id/usb-1a86_USB_Single_Serial_5AE6057813-if00"
|
| 14 |
+
FOLLOWER_ID = "my_follower_arm"
|
| 15 |
+
PHONE_URL = os.environ.get("PHONE_URL", "http://127.0.0.1:4747/video") # DroidCam on localhost; PHONE_URL=... to override
|
| 16 |
+
IMG_W, IMG_H = 640, 360 # the phone view is used at this size (same as the half-res datasets)
|
| 17 |
+
JOINTS = ["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper"]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def connect_robot():
|
| 21 |
+
from lerobot.robots.so_follower.config_so_follower import SOFollowerRobotConfig
|
| 22 |
+
from lerobot.robots.so_follower.so_follower import SOFollower
|
| 23 |
+
|
| 24 |
+
robot = SOFollower(SOFollowerRobotConfig(port=FOLLOWER_PORT, id=FOLLOWER_ID))
|
| 25 |
+
robot.connect(calibrate=False)
|
| 26 |
+
return robot
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def read_joints(robot):
|
| 30 |
+
obs = robot.get_observation()
|
| 31 |
+
return np.array([obs[f"{n}.pos"] for n in JOINTS], dtype=np.float64)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class Phone:
|
| 35 |
+
"""DroidCam stream, resized to 640x360 RGB.
|
| 36 |
+
|
| 37 |
+
A background thread reads the stream continuously and keeps only the newest frame, so a photo
|
| 38 |
+
is always current (OpenCV otherwise hands back frames buffered seconds earlier, which made the
|
| 39 |
+
"before" and "after" photos identical). Reconnects if the HTTP stream drops."""
|
| 40 |
+
|
| 41 |
+
def __init__(self, url=PHONE_URL):
|
| 42 |
+
import threading
|
| 43 |
+
|
| 44 |
+
self.url = url
|
| 45 |
+
self.cap = None
|
| 46 |
+
self.latest, self.stamp, self.count = None, 0.0, 0
|
| 47 |
+
self.lock = threading.Lock()
|
| 48 |
+
self.running = True
|
| 49 |
+
self._open()
|
| 50 |
+
self.thread = threading.Thread(target=self._reader, daemon=True)
|
| 51 |
+
self.thread.start()
|
| 52 |
+
t0 = time.time()
|
| 53 |
+
while self.latest is None and time.time() - t0 < 10:
|
| 54 |
+
time.sleep(0.05)
|
| 55 |
+
if self.latest is None:
|
| 56 |
+
raise RuntimeError(f"no frames from {self.url} within 10 s")
|
| 57 |
+
|
| 58 |
+
def _open(self):
|
| 59 |
+
if self.cap is not None:
|
| 60 |
+
self.cap.release()
|
| 61 |
+
self.cap = cv2.VideoCapture(self.url)
|
| 62 |
+
if not self.cap.isOpened():
|
| 63 |
+
raise RuntimeError(f"cannot open phone stream {self.url} (is DroidCam running? close any browser tab showing it)")
|
| 64 |
+
|
| 65 |
+
def _reader(self):
|
| 66 |
+
fails = 0
|
| 67 |
+
while self.running:
|
| 68 |
+
ok, img = self.cap.read()
|
| 69 |
+
if ok and img is not None:
|
| 70 |
+
fails = 0
|
| 71 |
+
with self.lock:
|
| 72 |
+
self.latest, self.stamp = img, time.time()
|
| 73 |
+
self.count += 1
|
| 74 |
+
continue
|
| 75 |
+
fails += 1
|
| 76 |
+
print(f" (phone stream dropped, reconnecting: attempt {fails})")
|
| 77 |
+
time.sleep(1.0)
|
| 78 |
+
try:
|
| 79 |
+
self._open()
|
| 80 |
+
except RuntimeError as e:
|
| 81 |
+
print(" ", e)
|
| 82 |
+
|
| 83 |
+
def frame(self, max_age=0.5, timeout=10.0):
|
| 84 |
+
"""Newest frame (taken after this call started), 640x360 RGB."""
|
| 85 |
+
t_call = time.time()
|
| 86 |
+
while time.time() - t_call < timeout:
|
| 87 |
+
with self.lock:
|
| 88 |
+
img, stamp = self.latest, self.stamp
|
| 89 |
+
if img is not None and stamp > t_call and time.time() - stamp < max_age:
|
| 90 |
+
return cv2.cvtColor(cv2.resize(img, (IMG_W, IMG_H), interpolation=cv2.INTER_AREA), cv2.COLOR_BGR2RGB)
|
| 91 |
+
time.sleep(0.02)
|
| 92 |
+
raise RuntimeError("no fresh phone frame for %.0f s; check the DroidCam app and connection" % timeout)
|
| 93 |
+
|
| 94 |
+
def close(self):
|
| 95 |
+
self.running = False
|
| 96 |
+
self.thread.join(timeout=2)
|
| 97 |
+
self.cap.release()
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def blue_mask(rgb):
|
| 101 |
+
hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)
|
| 102 |
+
m = cv2.inRange(hsv, (95, 90, 70), (125, 255, 255)) # OpenCV hue 0..180: blue ~ 100-125
|
| 103 |
+
return cv2.morphologyEx(m, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def blue_report(rgb):
|
| 107 |
+
"""Why a photo has (no) blue blobs: blue pixel count and the largest blue component."""
|
| 108 |
+
m = blue_mask(rgb)
|
| 109 |
+
n, _, stats, cent = cv2.connectedComponentsWithStats(m)
|
| 110 |
+
big = max(range(1, n), key=lambda i: stats[i, cv2.CC_STAT_AREA], default=None)
|
| 111 |
+
hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)
|
| 112 |
+
return {"blue_px": int((m > 0).sum()), "largest_area": int(stats[big, cv2.CC_STAT_AREA]) if big else 0,
|
| 113 |
+
"largest_at": cent[big].round(0).tolist() if big else None, "mean_brightness": float(hsv[..., 2].mean())}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def blue_blobs(rgb, min_px=25, ignore=None):
|
| 117 |
+
"""Blue wrapper blobs: list of dicts with pixel centroid, area and the blob's long-axis angle.
|
| 118 |
+
``ignore``: a reference image; blue that was already there (dilated a little) is left out."""
|
| 119 |
+
m = blue_mask(rgb)
|
| 120 |
+
if ignore is not None:
|
| 121 |
+
m = cv2.bitwise_and(m, cv2.bitwise_not(cv2.dilate(blue_mask(ignore), np.ones((9, 9), np.uint8))))
|
| 122 |
+
n, lab, stats, cent = cv2.connectedComponentsWithStats(m)
|
| 123 |
+
out = []
|
| 124 |
+
for i in range(1, n):
|
| 125 |
+
if stats[i, cv2.CC_STAT_AREA] < min_px:
|
| 126 |
+
continue
|
| 127 |
+
ys, xs = np.nonzero(lab == i)
|
| 128 |
+
pts = np.c_[xs, ys].astype(np.float64)
|
| 129 |
+
c = pts.mean(0)
|
| 130 |
+
evals, evecs = np.linalg.eigh(np.cov((pts - c).T))
|
| 131 |
+
major = evecs[:, np.argmax(evals)]
|
| 132 |
+
out.append({"uv": c, "area": int(stats[i, cv2.CC_STAT_AREA]), "axis_uv": major})
|
| 133 |
+
return out
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def fit_homography(uv, xy):
|
| 137 |
+
"""Least-squares DLT homography (u, v) -> (x, y)."""
|
| 138 |
+
A = []
|
| 139 |
+
for (u, v), (x, y) in zip(uv, xy):
|
| 140 |
+
A.append([-u, -v, -1, 0, 0, 0, x * u, x * v, x])
|
| 141 |
+
A.append([0, 0, 0, -u, -v, -1, y * u, y * v, y])
|
| 142 |
+
_, _, Vt = np.linalg.svd(np.array(A))
|
| 143 |
+
return Vt[-1].reshape(3, 3) / Vt[-1, -1]
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def apply_h(H, uv):
|
| 147 |
+
uv = np.atleast_2d(uv)
|
| 148 |
+
q = np.c_[uv, np.ones(len(uv))] @ H.T
|
| 149 |
+
return q[:, :2] / q[:, 2:3]
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def load_calib():
|
| 153 |
+
if not os.path.exists(CALIB):
|
| 154 |
+
raise SystemExit(f"No {CALIB}. Run calibrate_table.py first.")
|
| 155 |
+
return np.array(json.load(open(CALIB))["H"])
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def candy_on_table(H, blob):
|
| 159 |
+
"""Table x, y [m] and yaw [rad] of a blob (yaw from the blob's long axis mapped onto the table)."""
|
| 160 |
+
c = blob["uv"]
|
| 161 |
+
p0, p1 = apply_h(H, [c - 8 * blob["axis_uv"], c + 8 * blob["axis_uv"]])
|
| 162 |
+
d = p1 - p0
|
| 163 |
+
xy = apply_h(H, c)[0]
|
| 164 |
+
return xy, float(np.arctan2(d[1], d[0]))
|
rl_deploy/deploy_rl.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run the Isaac Lab RL candy-pick policy on the real SO101 (state-based: joints + detected candy).
|
| 2 |
+
|
| 3 |
+
Same observation and action conventions as Isaac-SO101-CandyPick-Direct:
|
| 4 |
+
obs = arm joints [rad] + gripper opening [0..1], joint velocities x0.1, candy (x, y, sin2yaw,
|
| 5 |
+
cos2yaw) from the phone at the start, towel (x, y), last action, episode time fraction
|
| 6 |
+
act = 5 arm-joint target deltas (x0.06 rad per 30 Hz step, clamped to the real-demo ranges)
|
| 7 |
+
+ gripper command (-1 closed .. +1 = LeRobot 45 open)
|
| 8 |
+
|
| 9 |
+
By default it only detects the candy and prints what it would do (DRY RUN, arm not moved).
|
| 10 |
+
Add --go to move the arm; you are asked to confirm first. Keep a hand near the power switch.
|
| 11 |
+
|
| 12 |
+
cd ~/B1Hackathon/tasks/candy_sort/rl_deploy
|
| 13 |
+
~/miniconda3/envs/lerobot/bin/python deploy_rl.py --policy policy.pt # dry run
|
| 14 |
+
~/miniconda3/envs/lerobot/bin/python deploy_rl.py --policy policy.pt --go # moves the arm
|
| 15 |
+
Safety (real arm only): targets change by at most --max-step-deg per step (default 1 deg = 30 deg/s,
|
| 16 |
+
about a third of what the sim policy may command) and actions are low-pass filtered (--smooth).
|
| 17 |
+
Every run saves ../eval_videos/rl_<policy>_<time>.mp4 (phone + wrist) and a .csv of measured vs
|
| 18 |
+
commanded joints, unless --no-video.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import argparse
|
| 22 |
+
import time
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
import torch
|
| 26 |
+
|
| 27 |
+
from common import HERE, JOINTS, Phone, blue_blobs, candy_on_table, connect_robot, load_calib, read_joints
|
| 28 |
+
|
| 29 |
+
WRIST_DEV = "/dev/v4l/by-id/usb-Ausha_Webcam_Ausha_Webcam-video-index0"
|
| 30 |
+
|
| 31 |
+
# Must match so101_candy/candy_env_cfg.py
|
| 32 |
+
REAL_ARM_RANGE_DEG = np.array([(-44.9, 80.9), (-102.9, 32.6), (-12.2, 93.1), (0.2, 104.3), (-61.1, 99.7)])
|
| 33 |
+
ARM_SCALE_RAD = 0.06
|
| 34 |
+
GRIP_OPEN_LR, GRIP_CLOSED_LR = 45.0, 0.0
|
| 35 |
+
GRIP_LO, GRIP_HI = -0.174533, 1.74533 # sim gripper joint limits [rad] (LeRobot 0..100 maps onto them)
|
| 36 |
+
TOWEL_XY = (-0.20, -0.17)
|
| 37 |
+
TOWEL_HALF = (0.08, 0.12)
|
| 38 |
+
CANDY_X, CANDY_Y = (-0.06, 0.18), (-0.32, -0.12) # spawn box of the sim task, with some margin
|
| 39 |
+
EPISODE_STEPS = 300 # 10 s at 30 Hz
|
| 40 |
+
HZ = 30.0
|
| 41 |
+
|
| 42 |
+
p = argparse.ArgumentParser()
|
| 43 |
+
p.add_argument("--policy", required=True, help="TorchScript policy exported from Isaac Lab (policy.pt)")
|
| 44 |
+
p.add_argument("--go", action="store_true", help="actually move the arm")
|
| 45 |
+
p.add_argument("--seconds", type=float, default=10.0)
|
| 46 |
+
p.add_argument("--max-step-deg", type=float, default=1.0,
|
| 47 |
+
help="safety: max change of any joint target per 30 Hz step [deg] (sim policy allows ~3.4)")
|
| 48 |
+
p.add_argument("--smooth", type=float, default=0.5, help="safety: action low-pass, 0 = off, 0.5 = average with last")
|
| 49 |
+
p.add_argument("--no-video", action="store_true", help="don't save the phone/wrist video and the per-step log")
|
| 50 |
+
p.add_argument("--phone-url", default=None, help="phone stream URL (default: PHONE_URL env or http://127.0.0.1:4747/video)")
|
| 51 |
+
a = p.parse_args()
|
| 52 |
+
|
| 53 |
+
policy = torch.jit.load(a.policy, map_location="cpu").eval()
|
| 54 |
+
H = load_calib()
|
| 55 |
+
|
| 56 |
+
# 1) Detect the target candy (blue, not already on the towel, inside the trained workspace).
|
| 57 |
+
phone = Phone(a.phone_url) if a.phone_url else Phone()
|
| 58 |
+
img = phone.frame()
|
| 59 |
+
cands = []
|
| 60 |
+
for b in blue_blobs(img):
|
| 61 |
+
xy, yaw = candy_on_table(H, b)
|
| 62 |
+
on_towel = abs(xy[0] - TOWEL_XY[0]) < TOWEL_HALF[0] and abs(xy[1] - TOWEL_XY[1]) < TOWEL_HALF[1]
|
| 63 |
+
in_ws = CANDY_X[0] <= xy[0] <= CANDY_X[1] and CANDY_Y[0] <= xy[1] <= CANDY_Y[1]
|
| 64 |
+
print(f"blue blob at pixel {b['uv'].round(0)} -> table {xy.round(3)} m, yaw {np.degrees(yaw):.0f} deg"
|
| 65 |
+
f"{' (on towel)' if on_towel else ''}{' (outside trained area)' if not in_ws else ''}")
|
| 66 |
+
if not on_towel and in_ws:
|
| 67 |
+
cands.append((xy, yaw))
|
| 68 |
+
if not cands:
|
| 69 |
+
raise SystemExit("No blue candy inside the trained workspace. Move one closer to the arm and retry.")
|
| 70 |
+
xy, yaw = min(cands, key=lambda c: np.linalg.norm(c[0] - np.array([0.06, -0.22])))
|
| 71 |
+
candy_obs = np.array([xy[0], xy[1], np.sin(2 * yaw), np.cos(2 * yaw)])
|
| 72 |
+
print(f"target candy: {xy.round(3)} m, yaw {np.degrees(yaw):.0f} deg")
|
| 73 |
+
|
| 74 |
+
robot = connect_robot()
|
| 75 |
+
q = read_joints(robot)
|
| 76 |
+
print("current joints [deg]:", q.round(1))
|
| 77 |
+
if a.go:
|
| 78 |
+
if input("The arm WILL move for %.0f s. Type 'go' to start: " % a.seconds).strip() != "go":
|
| 79 |
+
robot.disconnect()
|
| 80 |
+
raise SystemExit("aborted")
|
| 81 |
+
else:
|
| 82 |
+
print("DRY RUN: computing actions without sending them (add --go to move the arm)")
|
| 83 |
+
|
| 84 |
+
# Documentation: phone (+ wrist if available) video and a per-step CSV of commanded vs measured joints.
|
| 85 |
+
import csv
|
| 86 |
+
import os
|
| 87 |
+
from datetime import datetime
|
| 88 |
+
|
| 89 |
+
import cv2
|
| 90 |
+
|
| 91 |
+
rec = None
|
| 92 |
+
if not a.no_video:
|
| 93 |
+
os.makedirs(os.path.join(HERE, "..", "eval_videos"), exist_ok=True)
|
| 94 |
+
stem = os.path.join(HERE, "..", "eval_videos", f"rl_{os.path.splitext(os.path.basename(a.policy))[0]}_{datetime.now():%Y%m%d_%H%M%S}"
|
| 95 |
+
+ ("" if a.go else "_dryrun"))
|
| 96 |
+
wrist = cv2.VideoCapture(WRIST_DEV)
|
| 97 |
+
wrist = wrist if wrist.isOpened() else None
|
| 98 |
+
vw = cv2.VideoWriter(stem + ".mp4", cv2.VideoWriter_fourcc(*"mp4v"), HZ, (640 + (320 if wrist else 0), 360))
|
| 99 |
+
logf = open(stem + ".csv", "w", newline="")
|
| 100 |
+
log = csv.writer(logf)
|
| 101 |
+
log.writerow(["t"] + [f"meas_{n}" for n in JOINTS] + [f"target_{n}" for n in JOINTS] + [f"act_{i}" for i in range(6)])
|
| 102 |
+
rec = (vw, wrist, log, logf)
|
| 103 |
+
print(f"recording to {stem}.mp4 / .csv")
|
| 104 |
+
|
| 105 |
+
target = q.copy() # LeRobot units
|
| 106 |
+
last_act = np.zeros(6)
|
| 107 |
+
prev_cmd = np.zeros(6)
|
| 108 |
+
prev_q, prev_t = q.copy(), time.perf_counter()
|
| 109 |
+
steps = int(a.seconds * HZ)
|
| 110 |
+
try:
|
| 111 |
+
for k in range(steps):
|
| 112 |
+
t0 = time.perf_counter()
|
| 113 |
+
q = read_joints(robot)
|
| 114 |
+
dt = max(t0 - prev_t, 1e-3)
|
| 115 |
+
q_rad = np.deg2rad(q[:5])
|
| 116 |
+
grip_open = q[5] / 100.0
|
| 117 |
+
vel = np.r_[np.deg2rad(q[:5] - prev_q[:5]), (q[5] - prev_q[5]) / 100.0 * (GRIP_HI - GRIP_LO)] / dt
|
| 118 |
+
prev_q, prev_t = q.copy(), t0
|
| 119 |
+
obs = np.r_[q_rad, grip_open, 0.1 * vel, candy_obs, TOWEL_XY, last_act, min(k, EPISODE_STEPS) / EPISODE_STEPS]
|
| 120 |
+
with torch.no_grad():
|
| 121 |
+
act = policy(torch.tensor(obs, dtype=torch.float32)[None])[0].numpy()
|
| 122 |
+
act = np.clip(act, -1.0, 1.0)
|
| 123 |
+
last_act = act # the policy sees its own (unsmoothed) action, as in sim
|
| 124 |
+
cmd = a.smooth * prev_cmd + (1.0 - a.smooth) * act if a.smooth > 0 else act
|
| 125 |
+
prev_cmd = cmd
|
| 126 |
+
step = np.clip(np.rad2deg(ARM_SCALE_RAD * cmd[:5]), -a.max_step_deg, a.max_step_deg)
|
| 127 |
+
target[:5] = np.clip(target[:5] + step, REAL_ARM_RANGE_DEG[:, 0], REAL_ARM_RANGE_DEG[:, 1])
|
| 128 |
+
act = cmd
|
| 129 |
+
target[5] = GRIP_CLOSED_LR + (GRIP_OPEN_LR - GRIP_CLOSED_LR) * (act[5] + 1.0) / 2.0
|
| 130 |
+
if a.go:
|
| 131 |
+
robot.send_action({f"{n}.pos": float(v) for n, v in zip(
|
| 132 |
+
["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper"], target)})
|
| 133 |
+
elif k % 15 == 0:
|
| 134 |
+
print(f"t={k / HZ:4.1f}s target [deg]: {target.round(1)}")
|
| 135 |
+
if rec:
|
| 136 |
+
vw, wrist, log, _ = rec
|
| 137 |
+
frame = cv2.cvtColor(phone.frame(max_age=1.0), cv2.COLOR_RGB2BGR)
|
| 138 |
+
if wrist is not None:
|
| 139 |
+
ok, w = wrist.read()
|
| 140 |
+
w = cv2.resize(w, (320, 240)) if ok else np.zeros((240, 320, 3), np.uint8)
|
| 141 |
+
frame = np.concatenate([frame, np.vstack([w, np.zeros((120, 320, 3), np.uint8)])], axis=1)
|
| 142 |
+
cv2.putText(frame, f"t={k / HZ:4.1f}s {'GO' if a.go else 'DRY'}", (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
|
| 143 |
+
vw.write(frame)
|
| 144 |
+
log.writerow([round(k / HZ, 3)] + q.round(2).tolist() + target.round(2).tolist() + np.round(act, 3).tolist())
|
| 145 |
+
time.sleep(max(0.0, 1.0 / HZ - (time.perf_counter() - t0)))
|
| 146 |
+
except KeyboardInterrupt:
|
| 147 |
+
print("\nstopped by Ctrl-C")
|
| 148 |
+
finally:
|
| 149 |
+
robot.disconnect()
|
| 150 |
+
phone.close()
|
| 151 |
+
if rec:
|
| 152 |
+
rec[0].release()
|
| 153 |
+
if rec[1] is not None:
|
| 154 |
+
rec[1].release()
|
| 155 |
+
rec[3].close()
|
| 156 |
+
print("saved video and per-step log next to", os.path.basename(stem))
|
rl_deploy/export_policy.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Turn an rsl_rl checkpoint (model_XXXX.pt) of Isaac-SO101-CandyPick-Direct into a TorchScript policy.
|
| 2 |
+
|
| 3 |
+
The exported module takes the raw 25-d observation and returns the deterministic action (mean),
|
| 4 |
+
with the observation normalizer baked in. No Isaac Sim needed, so it can run while training.
|
| 5 |
+
|
| 6 |
+
python export_policy.py <run dir>/model_1200.pt policy.pt
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import sys
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from torch import nn
|
| 13 |
+
|
| 14 |
+
EPS = 1e-2 # rsl_rl EmpiricalNormalization
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class Policy(nn.Module):
|
| 18 |
+
def __init__(self, sd):
|
| 19 |
+
super().__init__()
|
| 20 |
+
self.register_buffer("mean", sd["obs_normalizer._mean"].clone())
|
| 21 |
+
self.register_buffer("std", sd["obs_normalizer._std"].clone())
|
| 22 |
+
layers, i = [], 0
|
| 23 |
+
while f"mlp.{i}.weight" in sd:
|
| 24 |
+
w = sd[f"mlp.{i}.weight"]
|
| 25 |
+
lin = nn.Linear(w.shape[1], w.shape[0])
|
| 26 |
+
lin.weight.data.copy_(w)
|
| 27 |
+
lin.bias.data.copy_(sd[f"mlp.{i}.bias"])
|
| 28 |
+
layers.append(lin)
|
| 29 |
+
if f"mlp.{i + 2}.weight" in sd:
|
| 30 |
+
layers.append(nn.ELU())
|
| 31 |
+
i += 2
|
| 32 |
+
self.mlp = nn.Sequential(*layers)
|
| 33 |
+
|
| 34 |
+
def forward(self, obs: torch.Tensor) -> torch.Tensor:
|
| 35 |
+
return self.mlp((obs - self.mean) / (self.std + 1e-2)) # EPS
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
ckpt = torch.load(sys.argv[1], map_location="cpu", weights_only=False)
|
| 39 |
+
pol = Policy(ckpt["actor_state_dict"]).eval()
|
| 40 |
+
ts = torch.jit.script(pol)
|
| 41 |
+
ts.save(sys.argv[2])
|
| 42 |
+
print("saved", sys.argv[2], "from iteration", ckpt.get("iter"), "| obs dim", pol.mean.shape[-1])
|
rl_deploy/policy.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cd7743047a9c53d7f73cb9fc36fbb311d207c97ee991ff78575ae4db73156015
|
| 3 |
+
size 206011
|
rl_deploy/policy_final.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1188b99a9d7ea964525e1a63286635d08c3b6c189ec891ca305bd3bf6997758c
|
| 3 |
+
size 206389
|
rl_deploy/so101_fk.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Forward kinematics of the SO101 from the official so101_new_calib URDF (numpy only).
|
| 2 |
+
|
| 3 |
+
Joint angles are LeRobot degrees (0 = middle of the calibrated range), which map 1:1 onto the URDF
|
| 4 |
+
joints in radians. Positions are returned in the frame the RL policy uses: the robot base on the
|
| 5 |
+
table, z up, with the arm's zero pan pointing along -y (Isaac Lab's so101_new_calib USD, mounted
|
| 6 |
+
3 cm into the table as in the sim task). See _to_policy_frame.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import xml.etree.ElementTree as ET
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
URDF = os.path.join(os.path.dirname(os.path.abspath(__file__)), "so101_new_calib.urdf")
|
| 15 |
+
ARM = ["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _rpy(r, p, y):
|
| 19 |
+
cr, sr, cp, sp, cy, sy = np.cos(r), np.sin(r), np.cos(p), np.sin(p), np.cos(y), np.sin(y)
|
| 20 |
+
return np.array([[cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr],
|
| 21 |
+
[sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr],
|
| 22 |
+
[-sp, cp * sr, cp * cr]])
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _axis_angle(axis, a):
|
| 26 |
+
axis = np.asarray(axis, float) / np.linalg.norm(axis)
|
| 27 |
+
K = np.array([[0, -axis[2], axis[1]], [axis[2], 0, -axis[0]], [-axis[1], axis[0], 0]])
|
| 28 |
+
return np.eye(3) + np.sin(a) * K + (1 - np.cos(a)) * K @ K
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _tf(R, t):
|
| 32 |
+
T = np.eye(4)
|
| 33 |
+
T[:3, :3], T[:3, 3] = R, t
|
| 34 |
+
return T
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class SO101FK:
|
| 38 |
+
def __init__(self, urdf=URDF):
|
| 39 |
+
root = ET.parse(urdf).getroot()
|
| 40 |
+
self.joints = {}
|
| 41 |
+
for j in root.findall("joint"):
|
| 42 |
+
o = j.find("origin")
|
| 43 |
+
xyz = [float(v) for v in (o.get("xyz") if o is not None else "0 0 0").split()]
|
| 44 |
+
rpy = [float(v) for v in (o.get("rpy") if o is not None else "0 0 0").split()]
|
| 45 |
+
ax = j.find("axis")
|
| 46 |
+
self.joints[j.get("name")] = {
|
| 47 |
+
"parent": j.find("parent").get("link"), "child": j.find("child").get("link"),
|
| 48 |
+
"T": _tf(_rpy(*rpy), xyz), "axis": [float(v) for v in ax.get("xyz").split()] if ax is not None else None,
|
| 49 |
+
"type": j.get("type"),
|
| 50 |
+
}
|
| 51 |
+
self.by_child = {v["child"]: k for k, v in self.joints.items()}
|
| 52 |
+
|
| 53 |
+
def link_pose(self, link, state_deg):
|
| 54 |
+
"""4x4 pose of ``link`` in the policy's base frame for a LeRobot state (deg)."""
|
| 55 |
+
q = {n: np.deg2rad(state_deg[i]) for i, n in enumerate(ARM)}
|
| 56 |
+
chain = []
|
| 57 |
+
while link in self.by_child:
|
| 58 |
+
name = self.by_child[link]
|
| 59 |
+
chain.append(name)
|
| 60 |
+
link = self.joints[name]["parent"]
|
| 61 |
+
T = np.eye(4)
|
| 62 |
+
for name in reversed(chain):
|
| 63 |
+
j = self.joints[name]
|
| 64 |
+
T = T @ j["T"]
|
| 65 |
+
if j["type"] == "revolute" and name in q:
|
| 66 |
+
T = T @ _tf(_axis_angle(j["axis"], q[name]), [0, 0, 0])
|
| 67 |
+
return _to_policy_frame() @ T
|
| 68 |
+
|
| 69 |
+
def tcp(self, state_deg):
|
| 70 |
+
"""Gripper tip position (``gripper_frame_link``), metres, policy base frame."""
|
| 71 |
+
return self.link_pose("gripper_frame_link", state_deg)[:3, 3]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _to_policy_frame():
|
| 75 |
+
# URDF base_link -> sim policy frame. Fitted (least squares, rigid) so URDF tip positions match
|
| 76 |
+
# Isaac Lab's so101_new_calib USD on 146 real grasp/release poses: about -87 deg about z plus a
|
| 77 |
+
# small offset; residual 10 mm median, 27 mm max (the two models differ slightly).
|
| 78 |
+
R = np.array([[5.790e-02, 9.983e-01, 2.300e-03],
|
| 79 |
+
[-9.983e-01, 5.790e-02, -4.000e-04],
|
| 80 |
+
[-5.000e-04, -2.300e-03, 1.000e00]])
|
| 81 |
+
U, _, Vt = np.linalg.svd(R)
|
| 82 |
+
# (fitted on poses already lowered by the 3 cm mount, so that offset is applied first)
|
| 83 |
+
return _tf(U @ Vt, [0.0049, -0.0160, 0.0392]) @ _tf(np.eye(3), [0.0, 0.0, -0.03008])
|
rl_deploy/so101_new_calib.urdf
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<!-- Generated using onshape-to-robot -->
|
| 3 |
+
<!-- Onshape https://cad.onshape.com/documents/7715cc284bb430fe6dab4ffd/w/4fd0791b683777b02f8d975a/e/826c553ede3b7592eb9ca800 -->
|
| 4 |
+
<robot name="so101_new_calib">
|
| 5 |
+
|
| 6 |
+
<!-- Materials -->
|
| 7 |
+
<material name="3d_printed">
|
| 8 |
+
<color rgba="1.0 0.82 0.12 1.0"/>
|
| 9 |
+
</material>
|
| 10 |
+
<material name="sts3215">
|
| 11 |
+
<color rgba="0.1 0.1 0.1 1.0"/>
|
| 12 |
+
</material>
|
| 13 |
+
|
| 14 |
+
<!-- Link base -->
|
| 15 |
+
<link name="base_link">
|
| 16 |
+
<inertial>
|
| 17 |
+
<origin xyz="0.0137179 -5.19711e-05 0.0334843" rpy="0 0 0"/>
|
| 18 |
+
<mass value="0.147"/>
|
| 19 |
+
<inertia ixx="0.000114686" ixy="-4.59787e-07" ixz="4.97151e-06" iyy="0.000136117" iyz="9.75275e-08" izz="0.000130364"/>
|
| 20 |
+
</inertial>
|
| 21 |
+
<!-- Part base_motor_holder_so101_v1 -->
|
| 22 |
+
<visual>
|
| 23 |
+
<origin xyz="-0.00636471 -9.94414e-05 -0.0024" rpy="1.5708 -1.67685e-15 1.5708"/>
|
| 24 |
+
<geometry>
|
| 25 |
+
<mesh filename="assets/base_motor_holder_so101_v1.stl"/>
|
| 26 |
+
</geometry>
|
| 27 |
+
<material name="3d_printed"/>
|
| 28 |
+
</visual>
|
| 29 |
+
<collision>
|
| 30 |
+
<origin xyz="-0.00636471 -9.94414e-05 -0.0024" rpy="1.5708 -1.67685e-15 1.5708"/>
|
| 31 |
+
<geometry>
|
| 32 |
+
<mesh filename="assets/base_motor_holder_so101_v1.stl"/>
|
| 33 |
+
</geometry>
|
| 34 |
+
</collision>
|
| 35 |
+
<!-- Part base_so101_v2 -->
|
| 36 |
+
<visual>
|
| 37 |
+
<origin xyz="-0.00636471 -8.97657e-09 -0.0024" rpy="1.5708 -2.78073e-29 1.5708"/>
|
| 38 |
+
<geometry>
|
| 39 |
+
<mesh filename="assets/base_so101_v2.stl"/>
|
| 40 |
+
</geometry>
|
| 41 |
+
<material name="3d_printed"/>
|
| 42 |
+
</visual>
|
| 43 |
+
<collision>
|
| 44 |
+
<origin xyz="-0.00636471 -8.97657e-09 -0.0024" rpy="1.5708 -2.78073e-29 1.5708"/>
|
| 45 |
+
<geometry>
|
| 46 |
+
<mesh filename="assets/base_so101_v2.stl"/>
|
| 47 |
+
</geometry>
|
| 48 |
+
</collision>
|
| 49 |
+
<!-- Part sts3215_03a_v1 -->
|
| 50 |
+
<visual>
|
| 51 |
+
<origin xyz="0.0263353 -8.97657e-09 0.0437" rpy="-8.21148e-16 7.84513e-18 1.249e-15"/>
|
| 52 |
+
<geometry>
|
| 53 |
+
<mesh filename="assets/sts3215_03a_v1.stl"/>
|
| 54 |
+
</geometry>
|
| 55 |
+
<material name="sts3215"/>
|
| 56 |
+
</visual>
|
| 57 |
+
<collision>
|
| 58 |
+
<origin xyz="0.0263353 -8.97657e-09 0.0437" rpy="-8.21148e-16 7.84513e-18 1.249e-15"/>
|
| 59 |
+
<geometry>
|
| 60 |
+
<mesh filename="assets/sts3215_03a_v1.stl"/>
|
| 61 |
+
</geometry>
|
| 62 |
+
</collision>
|
| 63 |
+
<!-- Part waveshare_mounting_plate_so101_v2 -->
|
| 64 |
+
<visual>
|
| 65 |
+
<origin xyz="-0.0309827 -0.000199441 0.0474" rpy="1.5708 -1.35493e-14 1.5708"/>
|
| 66 |
+
<geometry>
|
| 67 |
+
<mesh filename="assets/waveshare_mounting_plate_so101_v2.stl"/>
|
| 68 |
+
</geometry>
|
| 69 |
+
<material name="3d_printed"/>
|
| 70 |
+
</visual>
|
| 71 |
+
<collision>
|
| 72 |
+
<origin xyz="-0.0309827 -0.000199441 0.0474" rpy="1.5708 -1.35493e-14 1.5708"/>
|
| 73 |
+
<geometry>
|
| 74 |
+
<mesh filename="assets/waveshare_mounting_plate_so101_v2.stl"/>
|
| 75 |
+
</geometry>
|
| 76 |
+
</collision>
|
| 77 |
+
</link>
|
| 78 |
+
|
| 79 |
+
<!-- Link shoulder -->
|
| 80 |
+
<link name="shoulder_link">
|
| 81 |
+
<inertial>
|
| 82 |
+
<origin xyz="-0.0307604 -1.66727e-05 -0.0252713" rpy="0 0 0"/>
|
| 83 |
+
<mass value="0.100006"/>
|
| 84 |
+
<inertia ixx="8.3759e-05" ixy="7.55525e-08" ixz="-1.16342e-06" iyy="8.10403e-05" iyz="1.54663e-07" izz="2.39783e-05"/>
|
| 85 |
+
</inertial>
|
| 86 |
+
<!-- Part sts3215_03a_v1_2 -->
|
| 87 |
+
<visual>
|
| 88 |
+
<origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
|
| 89 |
+
<geometry>
|
| 90 |
+
<mesh filename="assets/sts3215_03a_v1.stl"/>
|
| 91 |
+
</geometry>
|
| 92 |
+
<material name="sts3215"/>
|
| 93 |
+
</visual>
|
| 94 |
+
<collision>
|
| 95 |
+
<origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
|
| 96 |
+
<geometry>
|
| 97 |
+
<mesh filename="assets/sts3215_03a_v1.stl"/>
|
| 98 |
+
</geometry>
|
| 99 |
+
</collision>
|
| 100 |
+
<!-- Part motor_holder_so101_base_v1 -->
|
| 101 |
+
<visual>
|
| 102 |
+
<origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
|
| 103 |
+
<geometry>
|
| 104 |
+
<mesh filename="assets/motor_holder_so101_base_v1.stl"/>
|
| 105 |
+
</geometry>
|
| 106 |
+
<material name="3d_printed"/>
|
| 107 |
+
</visual>
|
| 108 |
+
<collision>
|
| 109 |
+
<origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
|
| 110 |
+
<geometry>
|
| 111 |
+
<mesh filename="assets/motor_holder_so101_base_v1.stl"/>
|
| 112 |
+
</geometry>
|
| 113 |
+
</collision>
|
| 114 |
+
<!-- Part rotation_pitch_so101_v1 -->
|
| 115 |
+
<visual>
|
| 116 |
+
<origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 2.35221e-33 0"/>
|
| 117 |
+
<geometry>
|
| 118 |
+
<mesh filename="assets/rotation_pitch_so101_v1.stl"/>
|
| 119 |
+
</geometry>
|
| 120 |
+
<material name="3d_printed"/>
|
| 121 |
+
</visual>
|
| 122 |
+
<collision>
|
| 123 |
+
<origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 2.35221e-33 0"/>
|
| 124 |
+
<geometry>
|
| 125 |
+
<mesh filename="assets/rotation_pitch_so101_v1.stl"/>
|
| 126 |
+
</geometry>
|
| 127 |
+
</collision>
|
| 128 |
+
</link>
|
| 129 |
+
|
| 130 |
+
<!-- Link upper_arm -->
|
| 131 |
+
<link name="upper_arm_link">
|
| 132 |
+
<inertial>
|
| 133 |
+
<origin xyz="-0.0898471 -0.00838224 0.0184089" rpy="0 0 0"/>
|
| 134 |
+
<mass value="0.103"/>
|
| 135 |
+
<inertia ixx="4.08002e-05" ixy="-1.97819e-05" ixz="-4.03016e-08" iyy="0.000147318" iyz="8.97326e-09" izz="0.000142487"/>
|
| 136 |
+
</inertial>
|
| 137 |
+
<!-- Part sts3215_03a_v1_3 -->
|
| 138 |
+
<visual>
|
| 139 |
+
<origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -5.27356e-16 -1.5708"/>
|
| 140 |
+
<geometry>
|
| 141 |
+
<mesh filename="assets/sts3215_03a_v1.stl"/>
|
| 142 |
+
</geometry>
|
| 143 |
+
<material name="sts3215"/>
|
| 144 |
+
</visual>
|
| 145 |
+
<collision>
|
| 146 |
+
<origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -5.27356e-16 -1.5708"/>
|
| 147 |
+
<geometry>
|
| 148 |
+
<mesh filename="assets/sts3215_03a_v1.stl"/>
|
| 149 |
+
</geometry>
|
| 150 |
+
</collision>
|
| 151 |
+
<!-- Part upper_arm_so101_v1 -->
|
| 152 |
+
<visual>
|
| 153 |
+
<origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -0 -1.30911e-30"/>
|
| 154 |
+
<geometry>
|
| 155 |
+
<mesh filename="assets/upper_arm_so101_v1.stl"/>
|
| 156 |
+
</geometry>
|
| 157 |
+
<material name="3d_printed"/>
|
| 158 |
+
</visual>
|
| 159 |
+
<collision>
|
| 160 |
+
<origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -0 -1.30911e-30"/>
|
| 161 |
+
<geometry>
|
| 162 |
+
<mesh filename="assets/upper_arm_so101_v1.stl"/>
|
| 163 |
+
</geometry>
|
| 164 |
+
</collision>
|
| 165 |
+
</link>
|
| 166 |
+
|
| 167 |
+
<!-- Link lower_arm -->
|
| 168 |
+
<link name="lower_arm_link">
|
| 169 |
+
<inertial>
|
| 170 |
+
<origin xyz="-0.0980701 0.00324376 0.0182831" rpy="0 0 0"/>
|
| 171 |
+
<mass value="0.104"/>
|
| 172 |
+
<inertia ixx="2.87438e-05" ixy="7.41152e-06" ixz="1.26409e-06" iyy="0.000159844" iyz="-4.90188e-08" izz="0.00014529"/>
|
| 173 |
+
</inertial>
|
| 174 |
+
<!-- Part under_arm_so101_v1 -->
|
| 175 |
+
<visual>
|
| 176 |
+
<origin xyz="-0.0648499 -0.032 0.0182" rpy="3.14159 -0 6.67202e-31"/>
|
| 177 |
+
<geometry>
|
| 178 |
+
<mesh filename="assets/under_arm_so101_v1.stl"/>
|
| 179 |
+
</geometry>
|
| 180 |
+
<material name="3d_printed"/>
|
| 181 |
+
</visual>
|
| 182 |
+
<collision>
|
| 183 |
+
<origin xyz="-0.0648499 -0.032 0.0182" rpy="3.14159 -0 6.67202e-31"/>
|
| 184 |
+
<geometry>
|
| 185 |
+
<mesh filename="assets/under_arm_so101_v1.stl"/>
|
| 186 |
+
</geometry>
|
| 187 |
+
</collision>
|
| 188 |
+
<!-- Part motor_holder_so101_wrist_v1 -->
|
| 189 |
+
<visual>
|
| 190 |
+
<origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 -2.55351e-15 -1.83387e-30"/>
|
| 191 |
+
<geometry>
|
| 192 |
+
<mesh filename="assets/motor_holder_so101_wrist_v1.stl"/>
|
| 193 |
+
</geometry>
|
| 194 |
+
<material name="3d_printed"/>
|
| 195 |
+
</visual>
|
| 196 |
+
<collision>
|
| 197 |
+
<origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 -2.55351e-15 -1.83387e-30"/>
|
| 198 |
+
<geometry>
|
| 199 |
+
<mesh filename="assets/motor_holder_so101_wrist_v1.stl"/>
|
| 200 |
+
</geometry>
|
| 201 |
+
</collision>
|
| 202 |
+
<!-- Part sts3215_03a_v1_4 -->
|
| 203 |
+
<visual>
|
| 204 |
+
<origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -7.88861e-31 -3.14159"/>
|
| 205 |
+
<geometry>
|
| 206 |
+
<mesh filename="assets/sts3215_03a_v1.stl"/>
|
| 207 |
+
</geometry>
|
| 208 |
+
<material name="sts3215"/>
|
| 209 |
+
</visual>
|
| 210 |
+
<collision>
|
| 211 |
+
<origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -7.88861e-31 -3.14159"/>
|
| 212 |
+
<geometry>
|
| 213 |
+
<mesh filename="assets/sts3215_03a_v1.stl"/>
|
| 214 |
+
</geometry>
|
| 215 |
+
</collision>
|
| 216 |
+
</link>
|
| 217 |
+
|
| 218 |
+
<!-- Link wrist -->
|
| 219 |
+
<link name="wrist_link">
|
| 220 |
+
<inertial>
|
| 221 |
+
<origin xyz="-0.000103312 -0.0386143 0.0281156" rpy="0 0 0"/>
|
| 222 |
+
<mass value="0.079"/>
|
| 223 |
+
<inertia ixx="3.68263e-05" ixy="1.7893e-08" ixz="-5.28128e-08" iyy="2.5391e-05" iyz="3.6412e-06" izz="2.1e-05"/>
|
| 224 |
+
</inertial>
|
| 225 |
+
<!-- Part sts3215_03a_no_horn_v1 -->
|
| 226 |
+
<visual>
|
| 227 |
+
<origin xyz="8.32667e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
|
| 228 |
+
<geometry>
|
| 229 |
+
<mesh filename="assets/sts3215_03a_no_horn_v1.stl"/>
|
| 230 |
+
</geometry>
|
| 231 |
+
<material name="sts3215"/>
|
| 232 |
+
</visual>
|
| 233 |
+
<collision>
|
| 234 |
+
<origin xyz="8.32667e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
|
| 235 |
+
<geometry>
|
| 236 |
+
<mesh filename="assets/sts3215_03a_no_horn_v1.stl"/>
|
| 237 |
+
</geometry>
|
| 238 |
+
</collision>
|
| 239 |
+
<!-- Part wrist_roll_pitch_so101_v2 -->
|
| 240 |
+
<visual>
|
| 241 |
+
<origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
|
| 242 |
+
<geometry>
|
| 243 |
+
<mesh filename="assets/wrist_roll_pitch_so101_v2.stl"/>
|
| 244 |
+
</geometry>
|
| 245 |
+
<material name="3d_printed"/>
|
| 246 |
+
</visual>
|
| 247 |
+
<collision>
|
| 248 |
+
<origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
|
| 249 |
+
<geometry>
|
| 250 |
+
<mesh filename="assets/wrist_roll_pitch_so101_v2.stl"/>
|
| 251 |
+
</geometry>
|
| 252 |
+
</collision>
|
| 253 |
+
</link>
|
| 254 |
+
|
| 255 |
+
<!-- Link gripper -->
|
| 256 |
+
<link name="gripper_link">
|
| 257 |
+
<inertial>
|
| 258 |
+
<origin xyz="0.000213627 0.000245138 -0.025187" rpy="0 0 0"/>
|
| 259 |
+
<mass value="0.087"/>
|
| 260 |
+
<inertia ixx="2.75087e-05" ixy="-3.35241e-07" ixz="-5.7352e-06" iyy="4.33657e-05" iyz="-5.17847e-08" izz="3.45059e-05"/>
|
| 261 |
+
</inertial>
|
| 262 |
+
<!-- Part sts3215_03a_v1_5 -->
|
| 263 |
+
<visual>
|
| 264 |
+
<origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.19179e-17 -1.66533e-16"/>
|
| 265 |
+
<geometry>
|
| 266 |
+
<mesh filename="assets/sts3215_03a_v1.stl"/>
|
| 267 |
+
</geometry>
|
| 268 |
+
<material name="sts3215"/>
|
| 269 |
+
</visual>
|
| 270 |
+
<collision>
|
| 271 |
+
<origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.19179e-17 -1.66533e-16"/>
|
| 272 |
+
<geometry>
|
| 273 |
+
<mesh filename="assets/sts3215_03a_v1.stl"/>
|
| 274 |
+
</geometry>
|
| 275 |
+
</collision>
|
| 276 |
+
<!-- Part wrist_roll_follower_so101_v1 -->
|
| 277 |
+
<visual>
|
| 278 |
+
<origin xyz="8.32667e-17 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 0"/>
|
| 279 |
+
<geometry>
|
| 280 |
+
<mesh filename="assets/wrist_roll_follower_so101_v1.stl"/>
|
| 281 |
+
</geometry>
|
| 282 |
+
<material name="3d_printed"/>
|
| 283 |
+
</visual>
|
| 284 |
+
<collision>
|
| 285 |
+
<origin xyz="8.32667e-17 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 0"/>
|
| 286 |
+
<geometry>
|
| 287 |
+
<mesh filename="assets/wrist_roll_follower_so101_v1.stl"/>
|
| 288 |
+
</geometry>
|
| 289 |
+
</collision>
|
| 290 |
+
</link>
|
| 291 |
+
|
| 292 |
+
<!-- Gripper frame (dummy link + fixed joint) -->
|
| 293 |
+
<link name="gripper_frame_link">
|
| 294 |
+
<origin xyz="0 0 0" rpy="0 -0 0"/>
|
| 295 |
+
<inertial>
|
| 296 |
+
<origin xyz="0 0 0" rpy="0 0 0"/>
|
| 297 |
+
<mass value="1e-9"/>
|
| 298 |
+
<inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0"/>
|
| 299 |
+
</inertial>
|
| 300 |
+
</link>
|
| 301 |
+
|
| 302 |
+
<joint name="gripper_frame_joint" type="fixed">
|
| 303 |
+
<origin xyz="-0.0079 -0.000218121 -0.0981274" rpy="0 3.14159 0"/>
|
| 304 |
+
<parent link="gripper_link"/>
|
| 305 |
+
<child link="gripper_frame_link"/>
|
| 306 |
+
<axis xyz="0 0 0"/>
|
| 307 |
+
</joint>
|
| 308 |
+
|
| 309 |
+
<!-- Link moving_jaw_so101_v1 -->
|
| 310 |
+
<link name="moving_jaw_so101_v1_link">
|
| 311 |
+
<inertial>
|
| 312 |
+
<origin xyz="-0.00157495 -0.0300244 0.0192755" rpy="0 0 0"/>
|
| 313 |
+
<mass value="0.012"/>
|
| 314 |
+
<inertia ixx="6.61427e-06" ixy="-3.19807e-07" ixz="-5.90717e-09" iyy="1.89032e-06" iyz="-1.09945e-07" izz="5.28738e-06"/>
|
| 315 |
+
</inertial>
|
| 316 |
+
<!-- Part moving_jaw_so101_v1 -->
|
| 317 |
+
<visual>
|
| 318 |
+
<origin xyz="-5.55112e-17 -5.55112e-17 0.0189" rpy="9.53145e-17 6.93889e-18 1.24077e-24"/>
|
| 319 |
+
<geometry>
|
| 320 |
+
<mesh filename="assets/moving_jaw_so101_v1.stl"/>
|
| 321 |
+
</geometry>
|
| 322 |
+
<material name="3d_printed"/>
|
| 323 |
+
</visual>
|
| 324 |
+
<collision>
|
| 325 |
+
<origin xyz="-5.55112e-17 -5.55112e-17 0.0189" rpy="9.53145e-17 6.93889e-18 1.24077e-24"/>
|
| 326 |
+
<geometry>
|
| 327 |
+
<mesh filename="assets/moving_jaw_so101_v1.stl"/>
|
| 328 |
+
</geometry>
|
| 329 |
+
</collision>
|
| 330 |
+
</link>
|
| 331 |
+
|
| 332 |
+
<!-- Joint from gripper to moving_jaw_so101_v1 -->
|
| 333 |
+
<joint name="gripper" type="revolute">
|
| 334 |
+
<origin xyz="0.0202 0.0188 -0.0234" rpy="1.5708 -5.24284e-08 -1.41553e-15"/>
|
| 335 |
+
<parent link="gripper_link"/>
|
| 336 |
+
<child link="moving_jaw_so101_v1_link"/>
|
| 337 |
+
<axis xyz="0 0 1"/>
|
| 338 |
+
<limit effort="10" velocity="10" lower="-0.174533" upper="1.74533"/>
|
| 339 |
+
</joint>
|
| 340 |
+
|
| 341 |
+
<transmission name="gripper_trans">
|
| 342 |
+
<type>transmission_interface/SimpleTransmission</type>
|
| 343 |
+
<joint name="gripper">
|
| 344 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 345 |
+
</joint>
|
| 346 |
+
<actuator name="motor6">
|
| 347 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 348 |
+
<mechanicalReduction>1</mechanicalReduction>
|
| 349 |
+
</actuator>
|
| 350 |
+
</transmission>
|
| 351 |
+
|
| 352 |
+
<!-- Joint from wrist to gripper -->
|
| 353 |
+
<joint name="wrist_roll" type="revolute">
|
| 354 |
+
<origin xyz="5.55112e-17 -0.0611 0.0181" rpy="1.5708 0.0486795 3.14159"/>
|
| 355 |
+
<parent link="wrist_link"/>
|
| 356 |
+
<child link="gripper_link"/>
|
| 357 |
+
<axis xyz="0 0 1"/>
|
| 358 |
+
<limit effort="10" velocity="10" lower="-2.74385" upper="2.84121"/>
|
| 359 |
+
</joint>
|
| 360 |
+
|
| 361 |
+
<transmission name="wrist_roll_trans">
|
| 362 |
+
<type>transmission_interface/SimpleTransmission</type>
|
| 363 |
+
<joint name="wrist_roll">
|
| 364 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 365 |
+
</joint>
|
| 366 |
+
<actuator name="motor5">
|
| 367 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 368 |
+
<mechanicalReduction>1</mechanicalReduction>
|
| 369 |
+
</actuator>
|
| 370 |
+
</transmission>
|
| 371 |
+
|
| 372 |
+
<!-- Joint from lower_arm to wrist -->
|
| 373 |
+
<joint name="wrist_flex" type="revolute">
|
| 374 |
+
<origin xyz="-0.1349 0.0052 3.62355e-17" rpy="4.02456e-15 8.67362e-16 -1.5708"/>
|
| 375 |
+
<parent link="lower_arm_link"/>
|
| 376 |
+
<child link="wrist_link"/>
|
| 377 |
+
<axis xyz="0 0 1"/>
|
| 378 |
+
<limit effort="10" velocity="10" lower="-1.65806" upper="1.65806"/>
|
| 379 |
+
</joint>
|
| 380 |
+
|
| 381 |
+
<transmission name="wrist_flex_trans">
|
| 382 |
+
<type>transmission_interface/SimpleTransmission</type>
|
| 383 |
+
<joint name="wrist_flex">
|
| 384 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 385 |
+
</joint>
|
| 386 |
+
<actuator name="motor4">
|
| 387 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 388 |
+
<mechanicalReduction>1</mechanicalReduction>
|
| 389 |
+
</actuator>
|
| 390 |
+
</transmission>
|
| 391 |
+
|
| 392 |
+
<!-- Joint from upper_arm to lower_arm -->
|
| 393 |
+
<!-- Note: 5-degree calibration offset applied to joint limits -->
|
| 394 |
+
<joint name="elbow_flex" type="revolute">
|
| 395 |
+
<origin xyz="-0.11257 -0.028 1.73763e-16" rpy="-3.63608e-16 8.74301e-16 1.5708"/>
|
| 396 |
+
<parent link="upper_arm_link"/>
|
| 397 |
+
<child link="lower_arm_link"/>
|
| 398 |
+
<axis xyz="0 0 1"/>
|
| 399 |
+
<limit effort="10" velocity="10" lower="-1.69" upper="1.69"/>
|
| 400 |
+
</joint>
|
| 401 |
+
|
| 402 |
+
<transmission name="elbow_flex_trans">
|
| 403 |
+
<type>transmission_interface/SimpleTransmission</type>
|
| 404 |
+
<joint name="elbow_flex">
|
| 405 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 406 |
+
</joint>
|
| 407 |
+
<actuator name="motor3">
|
| 408 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 409 |
+
<mechanicalReduction>1</mechanicalReduction>
|
| 410 |
+
</actuator>
|
| 411 |
+
</transmission>
|
| 412 |
+
|
| 413 |
+
<!-- Joint from shoulder to upper_arm -->
|
| 414 |
+
<joint name="shoulder_lift" type="revolute">
|
| 415 |
+
<origin xyz="-0.0303992 -0.0182778 -0.0542" rpy="-1.5708 -1.5708 0"/>
|
| 416 |
+
<parent link="shoulder_link"/>
|
| 417 |
+
<child link="upper_arm_link"/>
|
| 418 |
+
<axis xyz="0 0 1"/>
|
| 419 |
+
<limit effort="10" velocity="10" lower="-1.74533" upper="1.74533"/>
|
| 420 |
+
</joint>
|
| 421 |
+
|
| 422 |
+
<transmission name="shoulder_lift_trans">
|
| 423 |
+
<type>transmission_interface/SimpleTransmission</type>
|
| 424 |
+
<joint name="shoulder_lift">
|
| 425 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 426 |
+
</joint>
|
| 427 |
+
<actuator name="motor2">
|
| 428 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 429 |
+
<mechanicalReduction>1</mechanicalReduction>
|
| 430 |
+
</actuator>
|
| 431 |
+
</transmission>
|
| 432 |
+
|
| 433 |
+
<!-- Joint from base to shoulder -->
|
| 434 |
+
<joint name="shoulder_pan" type="revolute">
|
| 435 |
+
<origin xyz="0.0388353 -8.97657e-09 0.0624" rpy="3.14159 4.18253e-17 -3.14159"/>
|
| 436 |
+
<parent link="base_link"/>
|
| 437 |
+
<child link="shoulder_link"/>
|
| 438 |
+
<axis xyz="0 0 1"/>
|
| 439 |
+
<limit effort="10" velocity="10" lower="-1.91986" upper="1.91986"/>
|
| 440 |
+
</joint>
|
| 441 |
+
|
| 442 |
+
<transmission name="shoulder_pan_trans">
|
| 443 |
+
<type>transmission_interface/SimpleTransmission</type>
|
| 444 |
+
<joint name="shoulder_pan">
|
| 445 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 446 |
+
</joint>
|
| 447 |
+
<actuator name="motor1">
|
| 448 |
+
<hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
|
| 449 |
+
<mechanicalReduction>1</mechanicalReduction>
|
| 450 |
+
</actuator>
|
| 451 |
+
</transmission>
|
| 452 |
+
|
| 453 |
+
</robot>
|
rl_deploy/table_calib.json
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"H": [
|
| 3 |
+
[
|
| 4 |
+
-0.00020641936356017595,
|
| 5 |
+
0.001605983436679173,
|
| 6 |
+
-0.17743518923975418
|
| 7 |
+
],
|
| 8 |
+
[
|
| 9 |
+
0.0009714184562970813,
|
| 10 |
+
-0.0002591502281196994,
|
| 11 |
+
-0.5384275139337167
|
| 12 |
+
],
|
| 13 |
+
[
|
| 14 |
+
5.463862597621938e-05,
|
| 15 |
+
0.0019026390841230777,
|
| 16 |
+
1.0
|
| 17 |
+
]
|
| 18 |
+
],
|
| 19 |
+
"image_size": [
|
| 20 |
+
640,
|
| 21 |
+
360
|
| 22 |
+
],
|
| 23 |
+
"points": [
|
| 24 |
+
{
|
| 25 |
+
"uv": [
|
| 26 |
+
283.3632596685083,
|
| 27 |
+
232.50552486187846
|
| 28 |
+
],
|
| 29 |
+
"area": 724,
|
| 30 |
+
"joints_deg": [
|
| 31 |
+
-22.681318681318682,
|
| 32 |
+
-1.2307692307692308,
|
| 33 |
+
41.18681318681319,
|
| 34 |
+
39.07692307692308,
|
| 35 |
+
-2.5054945054945055,
|
| 36 |
+
1.1026878015161956
|
| 37 |
+
],
|
| 38 |
+
"tip": [
|
| 39 |
+
0.0885746699621676,
|
| 40 |
+
-0.2211262158968657,
|
| 41 |
+
-0.0018728227502649436
|
| 42 |
+
]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"uv": [
|
| 46 |
+
223.27258566978193,
|
| 47 |
+
174.84112149532712
|
| 48 |
+
],
|
| 49 |
+
"area": 642,
|
| 50 |
+
"joints_deg": [
|
| 51 |
+
-6.065934065934066,
|
| 52 |
+
13.098901098901099,
|
| 53 |
+
21.846153846153847,
|
| 54 |
+
39.42857142857143,
|
| 55 |
+
-2.4175824175824174,
|
| 56 |
+
1.1026878015161956
|
| 57 |
+
],
|
| 58 |
+
"tip": [
|
| 59 |
+
0.04521679450712202,
|
| 60 |
+
-0.28363020045294185,
|
| 61 |
+
0.00034617923123554314
|
| 62 |
+
]
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"uv": [
|
| 66 |
+
324.1647940074906,
|
| 67 |
+
146.14606741573033
|
| 68 |
+
],
|
| 69 |
+
"area": 534,
|
| 70 |
+
"joints_deg": [
|
| 71 |
+
10.54945054945055,
|
| 72 |
+
-5.450549450549451,
|
| 73 |
+
41.714285714285715,
|
| 74 |
+
58.59340659340659,
|
| 75 |
+
-2.32967032967033,
|
| 76 |
+
1.1026878015161956
|
| 77 |
+
],
|
| 78 |
+
"tip": [
|
| 79 |
+
-0.009996185007877991,
|
| 80 |
+
-0.19178363754922642,
|
| 81 |
+
0.006818864733087288
|
| 82 |
+
]
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"uv": [
|
| 86 |
+
323.44,
|
| 87 |
+
294.63470588235293
|
| 88 |
+
],
|
| 89 |
+
"area": 1700,
|
| 90 |
+
"joints_deg": [
|
| 91 |
+
-41.67032967032967,
|
| 92 |
+
7.824175824175824,
|
| 93 |
+
25.45054945054945,
|
| 94 |
+
52.879120879120876,
|
| 95 |
+
-2.5934065934065935,
|
| 96 |
+
1.1026878015161956
|
| 97 |
+
],
|
| 98 |
+
"tip": [
|
| 99 |
+
0.14299164473120482,
|
| 100 |
+
-0.19023792065397205,
|
| 101 |
+
0.003980910785919246
|
| 102 |
+
]
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"uv": [
|
| 106 |
+
446.7759784075574,
|
| 107 |
+
253.0425101214575
|
| 108 |
+
],
|
| 109 |
+
"area": 1482,
|
| 110 |
+
"joints_deg": [
|
| 111 |
+
-52.30769230769231,
|
| 112 |
+
-23.64835164835165,
|
| 113 |
+
56.30769230769231,
|
| 114 |
+
63.51648351648352,
|
| 115 |
+
84.26373626373626,
|
| 116 |
+
1.1026878015161956
|
| 117 |
+
],
|
| 118 |
+
"tip": [
|
| 119 |
+
0.09268488377845423,
|
| 120 |
+
-0.1230287165562727,
|
| 121 |
+
0.013203728373691447
|
| 122 |
+
]
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"uv": [
|
| 126 |
+
177.62869198312237,
|
| 127 |
+
300.67148884870403
|
| 128 |
+
],
|
| 129 |
+
"area": 1659,
|
| 130 |
+
"joints_deg": [
|
| 131 |
+
-32.879120879120876,
|
| 132 |
+
30.24175824175824,
|
| 133 |
+
-1.2747252747252746,
|
| 134 |
+
42.15384615384615,
|
| 135 |
+
-17.53846153846154,
|
| 136 |
+
1.1716057891109581
|
| 137 |
+
],
|
| 138 |
+
"tip": [
|
| 139 |
+
0.17211300438132343,
|
| 140 |
+
-0.2762982688066562,
|
| 141 |
+
-0.005484126509456877
|
| 142 |
+
]
|
| 143 |
+
},
|
| 144 |
+
{
|
| 145 |
+
"uv": [
|
| 146 |
+
203.57355679702047,
|
| 147 |
+
143.64245810055866
|
| 148 |
+
],
|
| 149 |
+
"area": 537,
|
| 150 |
+
"joints_deg": [
|
| 151 |
+
3.6043956043956045,
|
| 152 |
+
25.406593406593405,
|
| 153 |
+
5.934065934065934,
|
| 154 |
+
46.81318681318681,
|
| 155 |
+
-16.923076923076923,
|
| 156 |
+
1.1716057891109581
|
| 157 |
+
],
|
| 158 |
+
"tip": [
|
| 159 |
+
0.00809345599756148,
|
| 160 |
+
-0.30200666202286097,
|
| 161 |
+
-0.007828562295707622
|
| 162 |
+
]
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"uv": [
|
| 166 |
+
326.9448818897638,
|
| 167 |
+
185.59186351706037
|
| 168 |
+
],
|
| 169 |
+
"area": 762,
|
| 170 |
+
"joints_deg": [
|
| 171 |
+
-11.516483516483516,
|
| 172 |
+
-11.516483516483516,
|
| 173 |
+
47.07692307692308,
|
| 174 |
+
57.45054945054945,
|
| 175 |
+
-17.36263736263736,
|
| 176 |
+
1.1026878015161956
|
| 177 |
+
],
|
| 178 |
+
"tip": [
|
| 179 |
+
0.0432111876213237,
|
| 180 |
+
-0.18203129452029243,
|
| 181 |
+
0.008625103296823854
|
| 182 |
+
]
|
| 183 |
+
}
|
| 184 |
+
],
|
| 185 |
+
"err_mm": [
|
| 186 |
+
5.768327483946577,
|
| 187 |
+
11.168090471611537,
|
| 188 |
+
10.273185868475645,
|
| 189 |
+
2.103149561433357,
|
| 190 |
+
10.31306901446774,
|
| 191 |
+
4.800515995559936,
|
| 192 |
+
7.8180204156784345,
|
| 193 |
+
14.80895048151171
|
| 194 |
+
],
|
| 195 |
+
"loo_mm": [
|
| 196 |
+
10.140745687901573,
|
| 197 |
+
14.396130379327072,
|
| 198 |
+
23.67257095374759,
|
| 199 |
+
4.647962466997839,
|
| 200 |
+
50.30546068867357,
|
| 201 |
+
21.273626361652976,
|
| 202 |
+
56.748608120772445,
|
| 203 |
+
17.030096025168582
|
| 204 |
+
]
|
| 205 |
+
}
|