Understand Tactile Data
Learn the pressure array structure, calibrate contact detection thresholds, implement grasp detection logic, and visualize contact events over time with the Paxini Gen3.
Unit 2 of 5 · Understand Data · ~1.5 hours
In this unit you will learn the spatial structure of the pressure array, calibrate contact detection thresholds for your gripper, implement grasp detection logic, and visualize contact events over time.
Concept — Pressure Array Structure
The Spatial Pressure Map
The frame.pressure_map field is a 2D NumPy array where each element represents one taxel (tactile pixel). For the fingertip variant, the array is 8×8, covering the 14×14 mm sensing area at 1.75 mm pitch. Each value is in kilopascals (kPa).
Pressure (kPa)
Force per unit area. Range: 0–600 kPa. Normal finger contact is 10–150 kPa depending on grip force.
Contact Area (mm²)
Sum of taxel areas where pressure exceeds threshold. A typical fingertip-object contact is 15–60 mm².
Total Force (N)
Integral of pressure over contact area. Proportional to gripper closing force. Typical grasp: 1–20 N.
Centroid (row, col)
Center of mass of the contact region in taxel coordinates. Shifts when object slips.
# Explore the pressure map structure import paxini import numpy as np sensor = paxini.Sensor() sensor.start() sensor.calibrate() # zero-offset with no contact frame = sensor.latest() print("Array shape:", frame.pressure_map.shape) # (8, 8) print("Max pressure:", frame.pressure_map.max(), "kPa") print("Total force:", frame.total_force_n, "N") print("Contact area:", frame.contact_area_mm2, "mm²") print("Centroid:", frame.contact_centroid) # (row, col) float
Step 1 — Threshold Calibration
Contact vs. No-Contact Threshold
The default contact threshold is 5 kPa per taxel and 0.05 N total force. These defaults work for most setups but may need adjustment based on your gripper's preload and cable stress on the sensor.
Run this calibration script to measure your sensor's resting noise level and set an appropriate threshold:
import paxini import numpy as np sensor = paxini.Sensor() sensor.start() sensor.calibrate() # zero-offset first # Sample 100 frames with no contact baseline_forces = [] for i, frame in enumerate(sensor.stream()): baseline_forces.append(frame.total_force_n) if i >= 99: break noise_floor = np.max(baseline_forces) recommended_threshold = noise_floor * 3.0 print(f"Noise floor: {noise_floor:.4f} N") print(f"Recommended contact threshold: {recommended_threshold:.4f} N") # Apply the threshold sensor.set_contact_threshold_n(recommended_threshold)
Typical values Noise floor is usually 0.005–0.02 N. A threshold of 0.05–0.06 N gives reliable contact detection without false positives from cable movement.
Step 2 — Grasp Detection
Grasp Detection Logic
A simple but robust grasp detector uses three conditions simultaneously: contact flag true, contact area above minimum, and stable centroid (centroid not moving rapidly). This eliminates false positives from light brush contacts:
import paxini import numpy as np from collections import deque sensor = paxini.Sensor() sensor.start() sensor.calibrate() centroid_history = deque(maxlen=10) # last 10 frames def is_stable_grasp(frame): if not frame.in_contact: return False if frame.contact_area_mm2 < 8.0: # at least ~3 taxels return False centroid_history.append(frame.contact_centroid) if len(centroid_history) < 5: return False centroids = np.array(centroid_history) centroid_std = centroids.std(axis=0).max() return centroid_std < 0.5 # centroid stable within 0.5 taxel for frame in sensor.stream(): state = "STABLE GRASP" if is_stable_grasp(frame) else "no contact" print(f"{state} F={frame.total_force_n:.2f}N A={frame.contact_area_mm2:.1f}mm²")
Step 3 — Contact Event Visualization
Visualizing Contact Events Over Time
Plot total force and contact state over a 10-second recording to understand the temporal structure of a grasp-and-release cycle:
import paxini import numpy as np import matplotlib.pyplot as plt sensor = paxini.Sensor() sensor.start() sensor.calibrate() forces = [] contacts = [] timestamps = [] print("Recording 10 seconds — grasp and release the sensor...") for frame in sensor.stream(): forces.append(frame.total_force_n) contacts.append(float(frame.in_contact)) timestamps.append(frame.timestamp_ns / 1e9) if timestamps[-1] - timestamps[0] >= 10.0: break t = np.array(timestamps) - timestamps[0] fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 5), sharex=True) ax1.plot(t, forces, color='#3b82f6') ax1.set_ylabel("Force (N)") ax1.set_title("Grasp Force Over Time") ax2.fill_between(t, contacts, alpha=0.5, color='#10b981') ax2.set_ylabel("In Contact") ax2.set_xlabel("Time (s)") plt.tight_layout() plt.savefig("contact_events.png") print("Saved contact_events.png")
For force-torque terminology used in the field, see the robotics glossary.
Unit 2 Complete When...
You can print the pressure map shape and confirm it is (8, 8) for fingertip variant. Your calibrated contact threshold produces no false positives when you shake the sensor cable gently. The grasp detector prints "STABLE GRASP" within 0.5 seconds of gripping the sensor surface firmly. Your contact events plot shows clear rising/falling edges at grasp and release.







