nb-touch 0.1.0__tar.gz
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.
- nb_touch-0.1.0/LICENSE +21 -0
- nb_touch-0.1.0/PKG-INFO +208 -0
- nb_touch-0.1.0/README.md +177 -0
- nb_touch-0.1.0/pyproject.toml +61 -0
- nb_touch-0.1.0/pyproject.toml.orig +58 -0
- nb_touch-0.1.0/src/nb_touch/__init__.py +5 -0
- nb_touch-0.1.0/src/nb_touch/__tests__/test_init.py +6 -0
- nb_touch-0.1.0/src/nb_touch/calibration/__init__.py +9 -0
- nb_touch-0.1.0/src/nb_touch/calibration/__tests__/test_orientation.py +170 -0
- nb_touch-0.1.0/src/nb_touch/calibration/__tests__/test_samples.py +105 -0
- nb_touch-0.1.0/src/nb_touch/calibration/__tests__/test_synthetic.py +107 -0
- nb_touch-0.1.0/src/nb_touch/calibration/orientation.py +173 -0
- nb_touch-0.1.0/src/nb_touch/calibration/samples.py +138 -0
- nb_touch-0.1.0/src/nb_touch/calibration/synthetic.py +158 -0
- nb_touch-0.1.0/src/nb_touch/contact/__init__.py +27 -0
- nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_active.py +16 -0
- nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_contacts.py +189 -0
- nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_cop_model.py +162 -0
- nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_detector.py +72 -0
- nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_filters.py +61 -0
- nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_zones.py +36 -0
- nb_touch-0.1.0/src/nb_touch/contact/active.py +25 -0
- nb_touch-0.1.0/src/nb_touch/contact/contacts.py +208 -0
- nb_touch-0.1.0/src/nb_touch/contact/cop_model.py +205 -0
- nb_touch-0.1.0/src/nb_touch/contact/detector.py +68 -0
- nb_touch-0.1.0/src/nb_touch/contact/filters.py +113 -0
- nb_touch-0.1.0/src/nb_touch/contact/zones.py +55 -0
- nb_touch-0.1.0/src/nb_touch/geometry/__init__.py +24 -0
- nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_frames.py +66 -0
- nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_neighbours.py +34 -0
- nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_normals.py +50 -0
- nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_so3.py +87 -0
- nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_synthetic.py +81 -0
- nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_transforms.py +77 -0
- nb_touch-0.1.0/src/nb_touch/geometry/frames.py +73 -0
- nb_touch-0.1.0/src/nb_touch/geometry/neighbours.py +48 -0
- nb_touch-0.1.0/src/nb_touch/geometry/normals.py +127 -0
- nb_touch-0.1.0/src/nb_touch/geometry/so3.py +75 -0
- nb_touch-0.1.0/src/nb_touch/geometry/synthetic.py +130 -0
- nb_touch-0.1.0/src/nb_touch/geometry/transforms.py +73 -0
- nb_touch-0.1.0/src/nb_touch/io/__init__.py +17 -0
- nb_touch-0.1.0/src/nb_touch/io/__tests__/test_recordings.py +145 -0
- nb_touch-0.1.0/src/nb_touch/io/__tests__/test_sensor_geometry.py +166 -0
- nb_touch-0.1.0/src/nb_touch/io/recordings.py +149 -0
- nb_touch-0.1.0/src/nb_touch/io/sensor_geometry.py +209 -0
- nb_touch-0.1.0/src/nb_touch/py.typed +0 -0
- nb_touch-0.1.0/src/nb_touch/utils/__init__.py +1 -0
- nb_touch-0.1.0/src/nb_touch/utils/__tests__/test_units.py +14 -0
- nb_touch-0.1.0/src/nb_touch/utils/__tests__/test_yaml_io.py +39 -0
- nb_touch-0.1.0/src/nb_touch/utils/units.py +19 -0
- nb_touch-0.1.0/src/nb_touch/utils/yaml_io.py +46 -0
- nb_touch-0.1.0/src/nb_touch/viz/__init__.py +2 -0
- nb_touch-0.1.0/src/nb_touch/viz/__tests__/conftest.py +15 -0
- nb_touch-0.1.0/src/nb_touch/viz/__tests__/test_arrows.py +134 -0
- nb_touch-0.1.0/src/nb_touch/viz/__tests__/test_markers.py +31 -0
- nb_touch-0.1.0/src/nb_touch/viz/__tests__/test_sensor.py +116 -0
- nb_touch-0.1.0/src/nb_touch/viz/__tests__/test_sliders.py +47 -0
- nb_touch-0.1.0/src/nb_touch/viz/arrows.py +139 -0
- nb_touch-0.1.0/src/nb_touch/viz/markers.py +41 -0
- nb_touch-0.1.0/src/nb_touch/viz/sensor.py +152 -0
- nb_touch-0.1.0/src/nb_touch/viz/sliders.py +56 -0
nb_touch-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Océan·e Patiny
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
nb_touch-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nb-touch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hardware-agnostic processing and calibration of taxel array touch sensors.
|
|
5
|
+
Keywords: tactile,touch sensor,taxel,robotics,center of pressure
|
|
6
|
+
Author: Oceane Patiny
|
|
7
|
+
Author-email: Oceane Patiny <oceane@patiny.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: numpy>=1.26
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Requires-Dist: torch>=2.2 ; extra == 'calibration'
|
|
22
|
+
Requires-Dist: mujoco>=3.16.0 ; extra == 'viz'
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Project-URL: Homepage, https://github.com/opatiny/nb-touch
|
|
25
|
+
Project-URL: Repository, https://github.com/opatiny/nb-touch
|
|
26
|
+
Project-URL: Issues, https://github.com/opatiny/nb-touch/issues
|
|
27
|
+
Project-URL: Paper, https://arxiv.org/abs/2605.28812
|
|
28
|
+
Provides-Extra: calibration
|
|
29
|
+
Provides-Extra: viz
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Beyond binary touch sensing
|
|
33
|
+
|
|
34
|
+
This library is useful for: processing data of a robot equipped with taxel array sensors, as well as to detect the taxels orientation based on experimental measurements.
|
|
35
|
+
|
|
36
|
+
Hardware-agnostic python library implementing the main functionalities of the article: Pan, Jiahe, et al. "Beyond Binary: Sim-to-Real Dexterous Manipulation with Physics-Grounded Contact Representation." arXiv preprint arXiv:2605.28812 (2026).
|
|
37
|
+
Url: https://arxiv.org/abs/2605.28812
|
|
38
|
+
|
|
39
|
+
Main features:
|
|
40
|
+
|
|
41
|
+
- Conversion from Center of Pressure (CoP) force to individual taxel forces.
|
|
42
|
+
- Conversion from taxel forces to CoP force.
|
|
43
|
+
- Tools for automatic taxel orientation detection: this requires torque sensors for all the links active when touching the sensor.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
- `pip install nb-touch`: CoP model, contact detection, geometry and I/O.
|
|
48
|
+
- `pip install "nb-touch[calibration]"`: adds the taxel orientation calibration (needs `torch`).
|
|
49
|
+
- `pip install "nb-touch[viz]"`: adds the MuJoCo drawing helpers used by the example (needs `mujoco`).
|
|
50
|
+
|
|
51
|
+
## Conventions
|
|
52
|
+
|
|
53
|
+
- Normals at the taxels point outward, out of the sensing surface. The sensor frame and every taxel frame are right-handed, with z along the outward normal.
|
|
54
|
+
- Forces are the forces acting on the sensor, as the taxels feel them: a push points against the outward normal.
|
|
55
|
+
|
|
56
|
+
Be careful:
|
|
57
|
+
|
|
58
|
+
- The paper's normals $n_i$ point inward: they are the opposites of ours.
|
|
59
|
+
- Hardware readings must be converted to these conventions before use.
|
|
60
|
+
|
|
61
|
+
Source: [arXiv 2605.28812](https://arxiv.org/html/2605.28812v1), Sections 3.2 (inward normals, $f_{cop}$ acting on the robot link) and 3.3 ($\tau = -J^T f$).
|
|
62
|
+
|
|
63
|
+
## From taxels to resultant force (and conversely)
|
|
64
|
+
|
|
65
|
+
The resultant force is computed from the taxel forces using the CoP model presented in the paper.
|
|
66
|
+
|
|
67
|
+
Both examples use a flat 3×3 grid of taxels, 3 mm apart, numbered row by row: taxel 4 is the centre one, at the origin. Positions are in m, forces in N, both in the sensor frame.
|
|
68
|
+
|
|
69
|
+
### From taxels to CoP
|
|
70
|
+
|
|
71
|
+
With a 1 N push on the centre taxel only:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import numpy as np
|
|
75
|
+
|
|
76
|
+
from nb_touch.contact import ContactDetector, ContactSettings
|
|
77
|
+
from nb_touch.geometry.synthetic import plane_sensor
|
|
78
|
+
|
|
79
|
+
np.set_printoptions(precision=3, suppress=True)
|
|
80
|
+
sensor = plane_sensor(rows=3, cols=3, spacing=0.003)
|
|
81
|
+
detector = ContactDetector(sensor, ContactSettings(min_taxels=1))
|
|
82
|
+
|
|
83
|
+
forces = np.zeros((9, 3)) # one force per taxel
|
|
84
|
+
forces[4] = [0.0, 0.0, -1.0]
|
|
85
|
+
contacts = detector.update(forces, in_taxel_frames=False)
|
|
86
|
+
print(contacts[0].cop) # [0. 0. 0.]
|
|
87
|
+
print(contacts[0].cop_force) # [ 0. 0. -1.]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`min_taxels=1` lets a single taxel make a contact (default: 3).
|
|
91
|
+
|
|
92
|
+
### From CoP to taxels
|
|
93
|
+
|
|
94
|
+
With a 1 N push at the centre:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from nb_touch.contact import (
|
|
98
|
+
cop_normal,
|
|
99
|
+
cop_to_taxel_forces,
|
|
100
|
+
spacing_sigma,
|
|
101
|
+
taxel_force_matrices,
|
|
102
|
+
)
|
|
103
|
+
from nb_touch.geometry import normals_from_orientations
|
|
104
|
+
|
|
105
|
+
normals = normals_from_orientations(sensor.orientations)
|
|
106
|
+
cop = np.zeros(3)
|
|
107
|
+
normal = cop_normal(sensor.positions, normals, cop)
|
|
108
|
+
sigma = spacing_sigma(sensor.positions) # 1.5 x the median taxel spacing
|
|
109
|
+
matrices = taxel_force_matrices(sensor.positions, normals, cop, normal, sigma)
|
|
110
|
+
print(cop_to_taxel_forces([0.0, 0.0, -1.0], matrices))
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Expected print, one row per taxel:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
[[-0.221 -0.221 -0.56 ]
|
|
117
|
+
[ 0. -0.193 -0.777]
|
|
118
|
+
[ 0.221 -0.221 -0.56 ]
|
|
119
|
+
[-0.193 0. -0.777]
|
|
120
|
+
[ 0. 0. -1. ]
|
|
121
|
+
[ 0.193 0. -0.777]
|
|
122
|
+
[-0.221 0.221 -0.56 ]
|
|
123
|
+
[ 0. 0.193 -0.777]
|
|
124
|
+
[ 0.221 0.221 -0.56 ]]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The centre taxel gets the whole push. The others get less the further they are, tilted away from the CoP.
|
|
128
|
+
|
|
129
|
+
For more details, see [docs/theory/cop.md](https://github.com/opatiny/nb-touch/blob/main/docs/theory/cop.md).
|
|
130
|
+
|
|
131
|
+
## CoP examples
|
|
132
|
+
|
|
133
|
+
An example script for the taxel to CoP conversion can be found in [`examples/cop_in_mujoco.py`](https://github.com/opatiny/nb-touch/blob/main/examples/cop_in_mujoco.py). It provides the taxel-CoP mapping on a synthetic sensor, shown in MuJoCo. It needs the viz extra. Run it in an activated terminal with `python examples/cop_in_mujoco.py` (on macOS: `mjpython examples/cop_in_mujoco.py`).
|
|
134
|
+
|
|
135
|
+
It shows:
|
|
136
|
+
|
|
137
|
+
- **gray spheres:** all the sensor taxels
|
|
138
|
+
- **yellow spheres**: taxels in a contact zone
|
|
139
|
+
- **blue arrows**: resultant forces for each taxel
|
|
140
|
+
- **green arrow and dot**: true CoP force and position
|
|
141
|
+
- **red arrow and dot**: estimated CoP force and position, recomputed from the taxel forces
|
|
142
|
+
|
|
143
|
+
Why the green and red arrows don't match perfectly? See [Known bias](https://github.com/opatiny/nb-touch/blob/main/docs/theory/cop.md#known-bias).
|
|
144
|
+
|
|
145
|
+
### Planar sensor
|
|
146
|
+
|
|
147
|
+
https://github.com/user-attachments/assets/189b1fb1-31b1-47bc-b6a7-87e07696a47e
|
|
148
|
+
|
|
149
|
+
### Cylindrical sensor
|
|
150
|
+
|
|
151
|
+
https://github.com/user-attachments/assets/d66bacdf-331c-4f35-a88c-500e3954136c
|
|
152
|
+
|
|
153
|
+
### Spherical sensor
|
|
154
|
+
|
|
155
|
+
https://github.com/user-attachments/assets/cd5f517f-bf19-4a19-b95b-86a3a4ac1ba7
|
|
156
|
+
|
|
157
|
+
## Taxel orientation detection
|
|
158
|
+
|
|
159
|
+
### Generate a recording file
|
|
160
|
+
|
|
161
|
+
A recording holds, for each sample, the taxel forces (each in its taxel's frame), the joint angles and the joint torques of the finger carrying the sensor, while an object is pressed at random places on the sensor. The paper uses 2400 samples: 2 min at 20 Hz ([arXiv 2605.28812](https://arxiv.org/html/2605.28812v1), Appendix A).
|
|
162
|
+
|
|
163
|
+
File format: [Recording format](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md#recording-format).
|
|
164
|
+
|
|
165
|
+
Synthetic recording of 2400 samples on the 3×3 grid above, mounted on a 4-joint finger, with true taxel orientations (the orientations on the real hardware) up to 20° away from the geometry's:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from nb_touch.calibration.synthetic import perturbed_orientations, synthetic_recording
|
|
169
|
+
from nb_touch.io import save_recording
|
|
170
|
+
|
|
171
|
+
rng = np.random.default_rng(0)
|
|
172
|
+
true_orientations = perturbed_orientations(sensor.orientations, np.radians(20), rng)
|
|
173
|
+
recording = synthetic_recording(sensor, true_orientations, num_samples=2400, rng=rng)
|
|
174
|
+
save_recording(recording, "results/recording") # recording.npz and recording.yaml
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
How the joint torques and taxel forces are simulated: [docs/synthetic-recording.md](https://github.com/opatiny/nb-touch/blob/main/docs/synthetic-recording.md).
|
|
178
|
+
|
|
179
|
+
With real hardware, build a `Recording` from the measured arrays instead (see [Reading and writing a recording](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md#reading-and-writing-a-recording)).
|
|
180
|
+
|
|
181
|
+
### Use recording to compute the taxel orientations
|
|
182
|
+
|
|
183
|
+
The calibration needs the recording, the sensor geometry (orientations are optional, they are only the starting point) and a function `jacobian(q, link, point)` returning the `(3, J)` position Jacobian of a point of the sensor, in the sensor frame. Here, the Jacobian of the synthetic finger:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from nb_touch.calibration import calibrate_orientations
|
|
187
|
+
from nb_touch.calibration.synthetic import finger_jacobian
|
|
188
|
+
from nb_touch.geometry.transforms import angles_between_rotations
|
|
189
|
+
from nb_touch.io import load_recording
|
|
190
|
+
|
|
191
|
+
recording = load_recording("results/recording")
|
|
192
|
+
result = calibrate_orientations(recording, sensor, finger_jacobian)
|
|
193
|
+
before = angles_between_rotations(sensor.orientations, true_orientations)
|
|
194
|
+
after = angles_between_rotations(result.geometry.orientations, true_orientations)
|
|
195
|
+
print(np.degrees(before).max().round(1), np.degrees(after).max().round(1)) # 19.6 6.4
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`result.geometry` is the sensor geometry with the calibrated orientations, `result.losses` the torque MSE at each step. The paper's 100 Adam steps are the default; with `settings=CalibrationSettings(steps=1000)`, the largest is expected to drop to 1.0°.
|
|
199
|
+
|
|
200
|
+
For more details, see [docs/theory/taxel-orientation.md](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md).
|
|
201
|
+
|
|
202
|
+
## Recording calibration data on a real hand
|
|
203
|
+
|
|
204
|
+
This library was tested on a real robotic hand: the Orca hand touch. The code and results are accessible here:
|
|
205
|
+
|
|
206
|
+
Coming soon
|
|
207
|
+
|
|
208
|
+
<!--- todo: add link once code is public -->
|
nb_touch-0.1.0/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Beyond binary touch sensing
|
|
2
|
+
|
|
3
|
+
This library is useful for: processing data of a robot equipped with taxel array sensors, as well as to detect the taxels orientation based on experimental measurements.
|
|
4
|
+
|
|
5
|
+
Hardware-agnostic python library implementing the main functionalities of the article: Pan, Jiahe, et al. "Beyond Binary: Sim-to-Real Dexterous Manipulation with Physics-Grounded Contact Representation." arXiv preprint arXiv:2605.28812 (2026).
|
|
6
|
+
Url: https://arxiv.org/abs/2605.28812
|
|
7
|
+
|
|
8
|
+
Main features:
|
|
9
|
+
|
|
10
|
+
- Conversion from Center of Pressure (CoP) force to individual taxel forces.
|
|
11
|
+
- Conversion from taxel forces to CoP force.
|
|
12
|
+
- Tools for automatic taxel orientation detection: this requires torque sensors for all the links active when touching the sensor.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
- `pip install nb-touch`: CoP model, contact detection, geometry and I/O.
|
|
17
|
+
- `pip install "nb-touch[calibration]"`: adds the taxel orientation calibration (needs `torch`).
|
|
18
|
+
- `pip install "nb-touch[viz]"`: adds the MuJoCo drawing helpers used by the example (needs `mujoco`).
|
|
19
|
+
|
|
20
|
+
## Conventions
|
|
21
|
+
|
|
22
|
+
- Normals at the taxels point outward, out of the sensing surface. The sensor frame and every taxel frame are right-handed, with z along the outward normal.
|
|
23
|
+
- Forces are the forces acting on the sensor, as the taxels feel them: a push points against the outward normal.
|
|
24
|
+
|
|
25
|
+
Be careful:
|
|
26
|
+
|
|
27
|
+
- The paper's normals $n_i$ point inward: they are the opposites of ours.
|
|
28
|
+
- Hardware readings must be converted to these conventions before use.
|
|
29
|
+
|
|
30
|
+
Source: [arXiv 2605.28812](https://arxiv.org/html/2605.28812v1), Sections 3.2 (inward normals, $f_{cop}$ acting on the robot link) and 3.3 ($\tau = -J^T f$).
|
|
31
|
+
|
|
32
|
+
## From taxels to resultant force (and conversely)
|
|
33
|
+
|
|
34
|
+
The resultant force is computed from the taxel forces using the CoP model presented in the paper.
|
|
35
|
+
|
|
36
|
+
Both examples use a flat 3×3 grid of taxels, 3 mm apart, numbered row by row: taxel 4 is the centre one, at the origin. Positions are in m, forces in N, both in the sensor frame.
|
|
37
|
+
|
|
38
|
+
### From taxels to CoP
|
|
39
|
+
|
|
40
|
+
With a 1 N push on the centre taxel only:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import numpy as np
|
|
44
|
+
|
|
45
|
+
from nb_touch.contact import ContactDetector, ContactSettings
|
|
46
|
+
from nb_touch.geometry.synthetic import plane_sensor
|
|
47
|
+
|
|
48
|
+
np.set_printoptions(precision=3, suppress=True)
|
|
49
|
+
sensor = plane_sensor(rows=3, cols=3, spacing=0.003)
|
|
50
|
+
detector = ContactDetector(sensor, ContactSettings(min_taxels=1))
|
|
51
|
+
|
|
52
|
+
forces = np.zeros((9, 3)) # one force per taxel
|
|
53
|
+
forces[4] = [0.0, 0.0, -1.0]
|
|
54
|
+
contacts = detector.update(forces, in_taxel_frames=False)
|
|
55
|
+
print(contacts[0].cop) # [0. 0. 0.]
|
|
56
|
+
print(contacts[0].cop_force) # [ 0. 0. -1.]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`min_taxels=1` lets a single taxel make a contact (default: 3).
|
|
60
|
+
|
|
61
|
+
### From CoP to taxels
|
|
62
|
+
|
|
63
|
+
With a 1 N push at the centre:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from nb_touch.contact import (
|
|
67
|
+
cop_normal,
|
|
68
|
+
cop_to_taxel_forces,
|
|
69
|
+
spacing_sigma,
|
|
70
|
+
taxel_force_matrices,
|
|
71
|
+
)
|
|
72
|
+
from nb_touch.geometry import normals_from_orientations
|
|
73
|
+
|
|
74
|
+
normals = normals_from_orientations(sensor.orientations)
|
|
75
|
+
cop = np.zeros(3)
|
|
76
|
+
normal = cop_normal(sensor.positions, normals, cop)
|
|
77
|
+
sigma = spacing_sigma(sensor.positions) # 1.5 x the median taxel spacing
|
|
78
|
+
matrices = taxel_force_matrices(sensor.positions, normals, cop, normal, sigma)
|
|
79
|
+
print(cop_to_taxel_forces([0.0, 0.0, -1.0], matrices))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Expected print, one row per taxel:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
[[-0.221 -0.221 -0.56 ]
|
|
86
|
+
[ 0. -0.193 -0.777]
|
|
87
|
+
[ 0.221 -0.221 -0.56 ]
|
|
88
|
+
[-0.193 0. -0.777]
|
|
89
|
+
[ 0. 0. -1. ]
|
|
90
|
+
[ 0.193 0. -0.777]
|
|
91
|
+
[-0.221 0.221 -0.56 ]
|
|
92
|
+
[ 0. 0.193 -0.777]
|
|
93
|
+
[ 0.221 0.221 -0.56 ]]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The centre taxel gets the whole push. The others get less the further they are, tilted away from the CoP.
|
|
97
|
+
|
|
98
|
+
For more details, see [docs/theory/cop.md](https://github.com/opatiny/nb-touch/blob/main/docs/theory/cop.md).
|
|
99
|
+
|
|
100
|
+
## CoP examples
|
|
101
|
+
|
|
102
|
+
An example script for the taxel to CoP conversion can be found in [`examples/cop_in_mujoco.py`](https://github.com/opatiny/nb-touch/blob/main/examples/cop_in_mujoco.py). It provides the taxel-CoP mapping on a synthetic sensor, shown in MuJoCo. It needs the viz extra. Run it in an activated terminal with `python examples/cop_in_mujoco.py` (on macOS: `mjpython examples/cop_in_mujoco.py`).
|
|
103
|
+
|
|
104
|
+
It shows:
|
|
105
|
+
|
|
106
|
+
- **gray spheres:** all the sensor taxels
|
|
107
|
+
- **yellow spheres**: taxels in a contact zone
|
|
108
|
+
- **blue arrows**: resultant forces for each taxel
|
|
109
|
+
- **green arrow and dot**: true CoP force and position
|
|
110
|
+
- **red arrow and dot**: estimated CoP force and position, recomputed from the taxel forces
|
|
111
|
+
|
|
112
|
+
Why the green and red arrows don't match perfectly? See [Known bias](https://github.com/opatiny/nb-touch/blob/main/docs/theory/cop.md#known-bias).
|
|
113
|
+
|
|
114
|
+
### Planar sensor
|
|
115
|
+
|
|
116
|
+
https://github.com/user-attachments/assets/189b1fb1-31b1-47bc-b6a7-87e07696a47e
|
|
117
|
+
|
|
118
|
+
### Cylindrical sensor
|
|
119
|
+
|
|
120
|
+
https://github.com/user-attachments/assets/d66bacdf-331c-4f35-a88c-500e3954136c
|
|
121
|
+
|
|
122
|
+
### Spherical sensor
|
|
123
|
+
|
|
124
|
+
https://github.com/user-attachments/assets/cd5f517f-bf19-4a19-b95b-86a3a4ac1ba7
|
|
125
|
+
|
|
126
|
+
## Taxel orientation detection
|
|
127
|
+
|
|
128
|
+
### Generate a recording file
|
|
129
|
+
|
|
130
|
+
A recording holds, for each sample, the taxel forces (each in its taxel's frame), the joint angles and the joint torques of the finger carrying the sensor, while an object is pressed at random places on the sensor. The paper uses 2400 samples: 2 min at 20 Hz ([arXiv 2605.28812](https://arxiv.org/html/2605.28812v1), Appendix A).
|
|
131
|
+
|
|
132
|
+
File format: [Recording format](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md#recording-format).
|
|
133
|
+
|
|
134
|
+
Synthetic recording of 2400 samples on the 3×3 grid above, mounted on a 4-joint finger, with true taxel orientations (the orientations on the real hardware) up to 20° away from the geometry's:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from nb_touch.calibration.synthetic import perturbed_orientations, synthetic_recording
|
|
138
|
+
from nb_touch.io import save_recording
|
|
139
|
+
|
|
140
|
+
rng = np.random.default_rng(0)
|
|
141
|
+
true_orientations = perturbed_orientations(sensor.orientations, np.radians(20), rng)
|
|
142
|
+
recording = synthetic_recording(sensor, true_orientations, num_samples=2400, rng=rng)
|
|
143
|
+
save_recording(recording, "results/recording") # recording.npz and recording.yaml
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
How the joint torques and taxel forces are simulated: [docs/synthetic-recording.md](https://github.com/opatiny/nb-touch/blob/main/docs/synthetic-recording.md).
|
|
147
|
+
|
|
148
|
+
With real hardware, build a `Recording` from the measured arrays instead (see [Reading and writing a recording](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md#reading-and-writing-a-recording)).
|
|
149
|
+
|
|
150
|
+
### Use recording to compute the taxel orientations
|
|
151
|
+
|
|
152
|
+
The calibration needs the recording, the sensor geometry (orientations are optional, they are only the starting point) and a function `jacobian(q, link, point)` returning the `(3, J)` position Jacobian of a point of the sensor, in the sensor frame. Here, the Jacobian of the synthetic finger:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from nb_touch.calibration import calibrate_orientations
|
|
156
|
+
from nb_touch.calibration.synthetic import finger_jacobian
|
|
157
|
+
from nb_touch.geometry.transforms import angles_between_rotations
|
|
158
|
+
from nb_touch.io import load_recording
|
|
159
|
+
|
|
160
|
+
recording = load_recording("results/recording")
|
|
161
|
+
result = calibrate_orientations(recording, sensor, finger_jacobian)
|
|
162
|
+
before = angles_between_rotations(sensor.orientations, true_orientations)
|
|
163
|
+
after = angles_between_rotations(result.geometry.orientations, true_orientations)
|
|
164
|
+
print(np.degrees(before).max().round(1), np.degrees(after).max().round(1)) # 19.6 6.4
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`result.geometry` is the sensor geometry with the calibrated orientations, `result.losses` the torque MSE at each step. The paper's 100 Adam steps are the default; with `settings=CalibrationSettings(steps=1000)`, the largest is expected to drop to 1.0°.
|
|
168
|
+
|
|
169
|
+
For more details, see [docs/theory/taxel-orientation.md](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md).
|
|
170
|
+
|
|
171
|
+
## Recording calibration data on a real hand
|
|
172
|
+
|
|
173
|
+
This library was tested on a real robotic hand: the Orca hand touch. The code and results are accessible here:
|
|
174
|
+
|
|
175
|
+
Coming soon
|
|
176
|
+
|
|
177
|
+
<!--- todo: add link once code is public -->
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nb-touch"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Hardware-agnostic processing and calibration of taxel array touch sensors."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
keywords = [
|
|
10
|
+
"tactile",
|
|
11
|
+
"touch sensor",
|
|
12
|
+
"taxel",
|
|
13
|
+
"robotics",
|
|
14
|
+
"center of pressure",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Scientific/Engineering",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"numpy>=1.26",
|
|
29
|
+
"pyyaml>=6.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[[project.authors]]
|
|
33
|
+
name = "Oceane Patiny"
|
|
34
|
+
email = "oceane@patiny.com"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/opatiny/nb-touch"
|
|
38
|
+
Repository = "https://github.com/opatiny/nb-touch"
|
|
39
|
+
Issues = "https://github.com/opatiny/nb-touch/issues"
|
|
40
|
+
Paper = "https://arxiv.org/abs/2605.28812"
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
calibration = ["torch>=2.2"]
|
|
44
|
+
viz = ["mujoco>=3.16.0"]
|
|
45
|
+
|
|
46
|
+
[dependency-groups]
|
|
47
|
+
dev = [
|
|
48
|
+
"nb-touch[calibration]",
|
|
49
|
+
"pytest",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[build-system]
|
|
53
|
+
requires = ["uv_build>=0.12.15,<0.13.0"]
|
|
54
|
+
build-backend = "uv_build"
|
|
55
|
+
|
|
56
|
+
[tool.uv.build-backend]
|
|
57
|
+
wheel-exclude = ["**/__tests__"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["src"]
|
|
61
|
+
addopts = ["--import-mode=importlib"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nb-touch"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Hardware-agnostic processing and calibration of taxel array touch sensors."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Oceane Patiny", email = "oceane@patiny.com" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
keywords = ["tactile", "touch sensor", "taxel", "robotics", "center of pressure"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Scientific/Engineering",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"numpy>=1.26",
|
|
26
|
+
"pyyaml>=6.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/opatiny/nb-touch"
|
|
31
|
+
Repository = "https://github.com/opatiny/nb-touch"
|
|
32
|
+
Issues = "https://github.com/opatiny/nb-touch/issues"
|
|
33
|
+
Paper = "https://arxiv.org/abs/2605.28812"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
calibration = [
|
|
37
|
+
"torch>=2.2",
|
|
38
|
+
]
|
|
39
|
+
viz = [
|
|
40
|
+
"mujoco>=3.16.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = [
|
|
45
|
+
"nb-touch[calibration]",
|
|
46
|
+
"pytest",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["uv_build>=0.12.15,<0.13.0"]
|
|
51
|
+
build-backend = "uv_build"
|
|
52
|
+
|
|
53
|
+
[tool.uv.build-backend]
|
|
54
|
+
wheel-exclude = ["**/__tests__"]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["src"]
|
|
58
|
+
addopts = ["--import-mode=importlib"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Taxel orientation calibration from joint torques."""
|
|
2
|
+
|
|
3
|
+
from nb_touch.calibration.orientation import (
|
|
4
|
+
CalibrationResult,
|
|
5
|
+
CalibrationSettings,
|
|
6
|
+
calibrate_orientations,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = ["CalibrationResult", "CalibrationSettings", "calibrate_orientations"]
|