3D Cal: An open-source software library for calibrating tactile sensors

Center for Robotics and Biosystems, Northwestern University
Conference name and year

For help, please contact: rohankota2026@u.northwestern.edu
Hemispheres Depth Map

Hemispheres

Pawn Depth Map

Pawn

Pill Depth Map

Pill

Introducing 3D-Cal, an open-source library for automated calibration of vision-based tactile sensors and TouchNet, a fully convolutional neural network for depth estimation from sensor readings.


Install the library:

pip3 install py3dcal

Abstract

Tactile sensing plays a key role in enabling dexterous and reliable robotic manipulation, but realizing this capability requires substantial calibration to convert raw sensor readings into physically meaningful quantities. Despite its near-universal necessity, the calibration process remains ad hoc and labor-intensive. Here, we introduce 3D Cal, an open-source library that transforms a low-cost 3D printer into an automated probing device capable of generating large volumes of labeled training data for tactile sensor calibration. We demonstrate the utility of 3D Cal by calibrating two commercially available vision-based tactile sensors, DIGIT and GelSight Mini, to reconstruct high-quality depth maps using the collected data and a custom convolutional neural network. In addition, we perform a data ablation study to determine how much data is needed for accurate calibration, providing practical guidelines for researchers working with these specific sensors, and we benchmark the trained models on previously unseen objects to evaluate calibration accuracy and generalization performance. By automating tactile sensor calibration, 3D Cal can accelerate tactile sensing research, simplify sensor deployment, and promote the practical integration of tactile sensing in robotic platforms.

Sensor Calibration

Step 1: Print Base

Sensor calibration begins by printing a base for your tactile sensor, constraining the position of the sensor within the 3D printer’s workspace. This ensures repeatability and eliminates the need to calibrate the sensor’s position each time. Bases for the DIGIT and Gelsight Mini can be found below. If using a standard Ender 3, we recommend directly uploading the G-Code (Ender 3) file to your 3D printer. For those who wish to modify the bases or slice them for different 3D printers, we have included STEP/STL files and a link to the Onshape document.

After selecting and loading the necessary files into the 3D printer, print the base:

Step 2: Insert Sensor into Base

Step 3: Attach Probe to Printhead

Once the base is printed, you can convert your 3D printer into an automated probing device by attaching a probe tip to the printhead. We use a ruby probe tip from McMaster probe (part no. 85175A586), which we attach to the printhead using a 3D printed adapter. The files for the adapter can be found below.


If you wish to use a custom probe tip, you can design a custom adapter. The geometry for attaching to the printhead of the Ender 3 can be found in the drawing and CAD files under the "Custom Probe" tab below.

Step 4: Probe Sensor

The source code for the 3D Cal library can be found here. Below is a guide to get you started.

Initial Setup

+

To make sure you have all the necessary Python libraries, run the following line in your terminal:

pip3 install py3DCal

You can then import the library in your Python scripts:

import py3DCal as p3d

Quick Start

+

Below is a minimal example of how to use py3DCal to collect probe data.


Note: Replace the /dev/ttyUSB0 with the appropriate serial port for the Ender3. If you don't know the name of the port, the serial ports can be printed using p3d.list_com_ports() or typing list-com-ports in the terminal.

DIGIT:

import py3DCal as p3d

ender3 = p3d.Ender3("/dev/ttyUSB0")
digit = p3d.DIGIT("D20966")
calibrator = p3d.Calibrator(printer=ender3, sensor=digit)
calibrator.probe()

Replace the "D20966" with the serial number of your DIGIT sensor.

GelSight Mini:

import py3DCal as p3d

ender3 = p3d.Ender3("/dev/ttyUSB0")
gsmini = p3d.GelsightMini()
calibrator = p3d.Calibrator(printer=ender3, sensor=gsmini)
calibrator.probe()

Before using the GelSight Mini, make sure you have the GelSight SDK installed.

Additional printer functionality

+

Connect to the printer

ender3.connect()

Homing the printer

ender3.initialize(xy_only=False)

Parameters:

  • xy_only=False (default): Home the XYZ axes of the printer.
  • xy_only=True: Only home the XY axes of the printer.

Moving the printer

ender3.go_to(x=5, y=5, z=2)

Parameters:

  • x (optional): The desired x-coordinate of the print head.
  • y (optional): The desired y-coordinate of the print head.
  • z (optional): The desired z-coordinate of the print head.

Note: Any of the above parameters can be omitted if you don't want to move certain axes.

Sending G-Code (Ender 3) to the printer

ender3.send_gcode(command="G28")

Parameters:

  • command: The G-Code (Ender 3) command to be sent to the printer.

Getting response from the printer

ender3.get_response()

Disconnect from printer

ender3.disconnect()

Additional sensor functionality

+

Connect to the sensor

digit.connect()

Taking an image with the sensor

digit.capture_image()

Disconnect from the sensor

digit.disconnect()

Adding a new 3D printer

+

To add a new 3D printer, extend the Printer class and implement the following 6 functions:

def __init__(self):
    # Code to initialize printer variables
def connect(self):
    # Code to connect to the printer
def disconnect(self):
    # Code to disconnect from the printer
def send_gcode(self, command):
    # Code to execute G-Code (Ender 3) command on the printer
def get_response(self):
    # Code to return message from the printer
def initialize(self, xy_only=False):
    # Code to initialize printer (home, set_units, etc)

Example implementation for Ender3:

from p3d import Printer
import serial

class Ender3(Printer):
    def __init__(self, port):
        self.port = port
        self.name = "Ender 3"

    def connect(self):
        # Code to connect to the printer
        self.ser = serial.Serial(self.port, 115200)

    def disconnect(self):
        # Code to disconnect from the printer
        self.ser.close()

    def send_gcode(self, command):
        # Code to execute G-Code (Ender 3) command on the printer
        self.ser.write(str.encode(command + "\r\n"))

    def get_response(self):
        # Code to return message from the printer
        reading = self.ser.readline().decode('utf-8')

        return reading

    def initialize(self, xy_only=False):
        # Code to initialize printer (home, set units, set absolute/relative movements, adjust fan speeds, etc.)

        # Use Metric Values
        self.send_gcode("G21")

        # Absolute Positioning
        self.send_gcode("G90")

        # Fan Off
        self.send_gcode("M107")

        if xy_only:
            # Home Printer X Y
            self.send_gcode("G28 X Y")
        else:
            # Home Printer X Y Z
            self.send_gcode("G28")

        # Check if homing is complete
        ok_count = 0

        while ok_count < 4:
            if "ok" in self.get_response():
                ok_count += 1

        return True

Adding a new tactile sensor

+

To add a new tactile sensor, extend the Sensor class and implement the following 4 functions:

def __init__(self):
    # Sensor name
    self.name = "DIGIT"

    # The printer's x, y, and z coordinates at the sensor's origin
    self.x_offset = ...
    self.y_offset = ...
    self.z_offset = ...

    # How high the printer should move above the sensor before moving sideways
    self.z_clearance = ...

    # Maximum penetration for the sensor. The printer will not push into the sensor more than this value.
    self.max_penetration = ...
def connect(self):
    # Code to connect to the sensor
def disconnect(self):
    # Code to disconnect from the sensor
def capture_image(self):
    # Code to return an image from the sensor

Example implementation for DIGIT:

from tactile_calibration import Sensor
from digit_interface import Digit
import cv2

class GelsightDigit(Sensor):
    def __init__(self, serial_number):
        self.serial_number = serial_number
        self.name = "DIGIT"
        self.x_offset = 110
        self.y_offset = 111.5
        self.z_offset = 137
        self.z_clearance = 2
        self.max_penetration = 4
        self.default_calibration_file = "calibration_procs/digit/default.csv"

    def connect(self):
        # Code to connect to the sensor
        self.sensor = Digit(self.serial_number)
        self.sensor.connect()
        self.sensor.set_fps(30)
        
    def disconnect(self):
        # Code to disconnect from the sensor
        self.sensor.disconnect()

    def capture_image(self):
        # Code to return an image from the sensor
        return cv2.flip(self.sensor.get_frame(), 1)

Step 5: Annotate Data

Quick Start

+

To calculate the mapping from printer coordinates to pixel space, you only need to fit circles on 2 images. Annotations for the rest of the data will be automatically computed. To perform the circle fitting, use the following function:

import py3DCal as p3d

p3d.annotate(dataset_path="path/to/your/sensor_calibration_data", probe_radius_mm=2)

Performing the annotation is a 3 step process:

  1. Fit the circle to the first image.
  2. Fit the circle to the second image.
  3. This screen shows you a sample of the circle fitting for 9 randomly selected images in your dataset. To make minor adjustments, you can increase/decrease the pixel-to-mm ratio using the r and f keys. You can also shift all of the circles up/down/left/right using the arrow keys.
The annotations for all of the images will be automatically computed and saved in a file named annotations.csv in the annotations folder of the dataset directory.

Visualizing Annotations

+

To visualize the annotations (i.e., circle fitting), you can use the following function:

import py3DCal as p3d

p3d.visualize_annotations(dataset_path="path/to/your/sensor_calibration_data")

Parameters:

  • dataset_path: The path to your sensor calibration data directory.
  • img_idxs (optional): A tuple or list of image indices to visualize. By default, shows 3 random images.
  • save_path (optional): The path to save the visualized annotations. If not specified, the visualizations will be displayed but not saved.

Step 6: Train Model

Quick Start

+

The following is a minimal example of how to train a TouchNet model from scratch using the data collected in Step 4:

import py3DCal as p3d
from py3DCal import datasets, models

# Create the dataset
my_dataset = datasets.TactileSensorDataset(root="./sensor_calibration_data")

# Create the model
touchnet = models.TouchNet()

# Train the model
p3d.train_model(model=touchnet, dataset=my_dataset, device="cuda")

# Generate depth maps using the trained model
depthmap = p3d.get_depthmap(model=touchnet,
  image_path="path/to/your/image.png",
  blank_image_path="path/to/your/blank_image.png",
  device="cuda"
)

Parameters for py3DCal.datasets.TactileSensorDataset():

  • root: Root directory of the dataset.
  • add_coordinate_embeddings (optional): Determines whether to add xy coordinate embeddings to the input images. Default is True.
  • subtract_blank (optional): Determines whether to subtract the blank image from the input images. Default is True.
  • transform (optional): Apply custom transformations to the input images. Default is torchvision.transforms.ToTensor().

Parameters for py3DCal.train_model():

  • model: A torch.nn.Module object.
  • dataset: A py3DCal.datasets.TactileSensorDataset object.
  • num_epochs (optional): The number of epochs to train for. Default is 60.
  • batch_size (optional): The batch size to use for training. Default is 64.
  • learning_rate (optional): The learning rate to use for training. Default is 1e-4.
  • train_ratio (optional): The split ratio for train and test sets. Default is 0.8.
  • loss_fn (optional): The loss function to use for training. Default is nn.MSELoss().
  • device (optional): Compute device to use ("cuda", "mps", "cpu" ). Default is "cpu".

Loading a pre-trained model

+

To load a pretrained model, set load_pretrained=True when creating the TouchNet model:

DIGIT:

import py3DCal as p3d
from py3DCal import models

touchnet = models.TouchNet(
  load_pretrained=True,
  sensor_type=p3d.SensorType.DIGIT
  root="."
)

GelSight Mini:

import py3DCal as p3d
from py3DCal import models

touchnet = models.TouchNet(
  load_pretrained=True,
  sensor_type=p3d.SensorType.GELSIGHTMINI
  root="."
)

Parameters:

  • load_pretrained (optional): Whether to load a pretrained model. Default is False
  • sensor_type (optional if load_pretrained=False): If loading a pretrained model, choose the sensor type:
    • p3d.SensorType.DIGIT: Use for the DIGIT sensor.
    • p3d.SensorType.GELSIGHTMINI: Use for the GelSight Mini sensor.
  • root (optional): Root directory for storing the downloaded pretrained weights (or loading them if they've already been downloaded). Defaults to the current working directory.

Loading the provided datasets

+

To load our dataset for the DIGIT (36,630 images) or GelSight Mini (36,270 images), use the following code:

DIGIT:

import py3DCal as p3d
from py3DCal import datasets

dataset = datasets.DIGIT(root='.', download=True)

GelSight Mini:

import py3DCal as p3d
from py3DCal import datasets

dataset = datasets.GelsightMini(root='.', download=True)

Parameters:

  • root: The root directory where the dataset should be stored, or the root directory containting the digit_calibration_data or gsmini_calibration_data folders if the data has already been downloaded. Defaults to the current working directory.
  • download (optional): Whether to download the dataset if it is not already present in the root directory. Default is False.
  • add_coordinate_embeddings (optional): Whether to add coordinate embeddings to the dataset. Default is True.
  • subtract_blank (optional): Whether to subtract the blank image from the dataset. Default is True.
  • transform (optional): A transform to apply to the dataset. Default is torchvision.transforms.ToTensor().

About TouchNet

+

TouchNet is a fully convolutional neural network designed to extract depth information from sensor readings. It takes as input an RGB tactile image augmented with a 2-channel x,y coordinate embedding, which provides the model with spatial context.

  • The first channel encodes column indices as increasing pixel values across each row.
  • The second channel encodes row indices as increasing pixel values across each column.

As shown in the figure below, TouchNet follows a 9-layer convolutional architecture that expands the 5-channel input to 256 channels, then contracts it to a 2-channel gradient map. The output channels correspond to the gradient in the x-direction (Gx) and gradient in the y-direction (Gy). Each module consists of a convolutional layer, batch normalization, a ReLU activation, and spatial dropout for regularization.

TouchNet Architecture

Step 7: Run Model

Depthmap Animation

Generating Depth Maps

+

To generate a depth map in the form of a numpy.ndarray:

import py3DCal as p3d

depthmap = p3d.get_depthmap(
  model=touchnet,
  image_path="path/to/your/image.png",
  blank_image_path="path/to/your/blank_image.png",
  device="cuda"
)

Parameters:

  • model: The model to use for depth map generation.
  • image_path: The path to the input image for which the depth map should be generated.
  • blank_image_path: The path to the blank image to be used for depth map generation.
  • device: The device to run the model on ("cuda", "mps", or "cpu" ). Default is "cpu".

Saving Depth Maps

+

To save the 2D depth map as a PNG image:

import py3DCal as p3d

depthmap = p3d.save_2d_depthmap(
  model=touchnet,
  image_path="path/to/your/image.png",
  blank_image_path="path/to/your/blank_image.png",
  device="cuda",
  save_path="path/to/save/depthmap.png"
)

Parameters:

  • model: The model to use for depth map generation.
  • image_path: The path to the input image for which the depth map should be generated.
  • blank_image_path: The path to the blank image to be used for depth map generation.
  • device: The device to run the model on ("cuda", "mps", or "cpu" ). Default is "cpu".
  • save_path: The path where the generated depth map should be saved. Default is "./depthmap.png".

Displaying Depth Maps

+

To display the 2D depth map using matplotlib:

import py3DCal as p3d

depthmap = p3d.show_2d_depthmap(
  model=touchnet,
  image_path="path/to/your/image.png",
  blank_image_path="path/to/your/blank_image.png",
  device="cuda"
)

Parameters:

  • model: The model to use for depth map generation.
  • image_path: The path to the input image for which the depth map should be generated.
  • blank_image_path: The path to the blank image to be used for depth map generation.
  • device: The device to run the model on ("cuda", "mps", or "cpu" ). Default is "cpu".

Integrating Gradient Maps

+

If you wish to directly integrate the gradient maps predicted by your model, py3DCal provides a Fast Poisson implementation that allows you to produce a 2D depthmap from a map of x- and y-gradients.

import py3DCal as p3d

depthmap = p3d.fast_poisson(Gx, Gy)

Parameters:

  • Gx (np.ndarray): The map of gradients in the x-direction.
  • Gy (np.ndarray): The map of gradients in the y-direction.

Open Source Dataset

With the Ender 3 3D printer, DIGIT and GelSight Mini vision-based tactile sensors, and the automated calibration setup described above, we collected over 70,000 calibration images along a 0.5 mm x 0.5 mm grid, with 30 images obtained during each indentation.

In an effort to stimulate the future research and development of vision-based tactile sensors, we release our dataset as a part of our open-source library, 3D Cal. This dataset consists of 36,630 images from our DIGIT sensor and 36,270 images from our GelSight Mini sensor, and it can be accessed by clicking the button below.