py3dcal 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py3DCal/__init__.py +12 -0
- py3DCal/data_collection/Calibrator.py +298 -0
- py3DCal/data_collection/Printers/Ender3/Ender3.py +82 -0
- py3DCal/data_collection/Printers/Ender3/__init__.py +0 -0
- py3DCal/data_collection/Printers/Printer.py +63 -0
- py3DCal/data_collection/Printers/__init__.py +0 -0
- py3DCal/data_collection/Sensors/DIGIT/DIGIT.py +47 -0
- py3DCal/data_collection/Sensors/DIGIT/__init__.py +0 -0
- py3DCal/data_collection/Sensors/DIGIT/default.csv +1222 -0
- py3DCal/data_collection/Sensors/GelsightMini/GelsightMini.py +45 -0
- py3DCal/data_collection/Sensors/GelsightMini/__init__.py +0 -0
- py3DCal/data_collection/Sensors/GelsightMini/default.csv +1210 -0
- py3DCal/data_collection/Sensors/Sensor.py +35 -0
- py3DCal/data_collection/Sensors/__init__.py +0 -0
- py3DCal/data_collection/__init__.py +0 -0
- py3DCal/model_training/__init__.py +0 -0
- py3DCal/model_training/datasets/DIGIT_dataset.py +75 -0
- py3DCal/model_training/datasets/GelSightMini_dataset.py +73 -0
- py3DCal/model_training/datasets/__init__.py +3 -0
- py3DCal/model_training/datasets/split_dataset.py +38 -0
- py3DCal/model_training/datasets/tactile_sensor_dataset.py +82 -0
- py3DCal/model_training/lib/__init__.py +0 -0
- py3DCal/model_training/lib/add_coordinate_embeddings.py +29 -0
- py3DCal/model_training/lib/depthmaps.py +74 -0
- py3DCal/model_training/lib/fast_poisson.py +51 -0
- py3DCal/model_training/lib/get_gradient_map.py +39 -0
- py3DCal/model_training/lib/precompute_gradients.py +61 -0
- py3DCal/model_training/lib/train_model.py +96 -0
- py3DCal/model_training/lib/validate_device.py +22 -0
- py3DCal/model_training/lib/validate_parameters.py +45 -0
- py3DCal/model_training/models/__init__.py +1 -0
- py3DCal/model_training/models/touchnet.py +211 -0
- py3DCal/model_training/touchnet/__init__.py +0 -0
- py3DCal/model_training/touchnet/dataset.py +78 -0
- py3DCal/model_training/touchnet/touchnet.py +736 -0
- py3DCal/model_training/touchnet/touchnet_architecture.py +72 -0
- py3DCal/utils/__init__.py +0 -0
- py3DCal/utils/utils.py +32 -0
- py3dcal-1.0.0.dist-info/LICENSE +21 -0
- py3dcal-1.0.0.dist-info/METADATA +29 -0
- py3dcal-1.0.0.dist-info/RECORD +44 -0
- py3dcal-1.0.0.dist-info/WHEEL +5 -0
- py3dcal-1.0.0.dist-info/entry_points.txt +3 -0
- py3dcal-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
def validate_device(device: str):
|
|
4
|
+
"""
|
|
5
|
+
Validates the device by converting it to a torch.device object.
|
|
6
|
+
Args:
|
|
7
|
+
device (str): Device to run the model on.
|
|
8
|
+
Returns:
|
|
9
|
+
None.
|
|
10
|
+
Raises:
|
|
11
|
+
ValueError: If the device is not specified or invalid.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
device = torch.device(device)
|
|
15
|
+
except Exception as e:
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"Invalid device '{device}'. Valid options include:\n"
|
|
18
|
+
" - 'cpu': CPU processing\n"
|
|
19
|
+
" - 'cuda' or 'cuda:0': NVIDIA GPU\n"
|
|
20
|
+
" - 'mps': Apple Silicon GPU\n"
|
|
21
|
+
"See: https://pytorch.org/docs/stable/tensor_attributes.html#torch.device"
|
|
22
|
+
) from e
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from typing import Union
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def validate_device(device: str):
|
|
6
|
+
"""
|
|
7
|
+
Validates the device by converting it to a torch.device object.
|
|
8
|
+
Args:
|
|
9
|
+
device (str): Device to run the model on.
|
|
10
|
+
Returns:
|
|
11
|
+
None.
|
|
12
|
+
Raises:
|
|
13
|
+
ValueError: If the device is not specified or invalid.
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
device = torch.device(device)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
raise ValueError(
|
|
19
|
+
f"Invalid device '{device}'. Valid options include:\n"
|
|
20
|
+
" - 'cpu': CPU processing\n"
|
|
21
|
+
" - 'cuda' or 'cuda:0': NVIDIA GPU\n"
|
|
22
|
+
" - 'mps': Apple Silicon GPU\n"
|
|
23
|
+
"See: https://pytorch.org/docs/stable/tensor_attributes.html#torch.device"
|
|
24
|
+
) from e
|
|
25
|
+
|
|
26
|
+
def validate_root(root):
|
|
27
|
+
"""
|
|
28
|
+
Validates the root path specified by the user.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
root: root path specified by the user.
|
|
32
|
+
Returns:
|
|
33
|
+
None.
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If the root is not specified or invalid.
|
|
36
|
+
"""
|
|
37
|
+
if root is None :
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"root directory cannot be None.\n"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not isinstance(root, (str, Path)):
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"root directory must be a valid file system path as a string or pathlib.Path object\n"
|
|
45
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .touchnet import SensorType, TouchNet
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
from tqdm import tqdm
|
|
4
|
+
import torch
|
|
5
|
+
import torch.nn as nn
|
|
6
|
+
import torch.nn.functional as F
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Union
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
class SensorType(Enum):
|
|
12
|
+
"""
|
|
13
|
+
SensorType: Available sensor types with pretrained weights and compiled datasets
|
|
14
|
+
"""
|
|
15
|
+
DIGIT = "DIGIT"
|
|
16
|
+
GELSIGHTMINI = "GelSightMini"
|
|
17
|
+
|
|
18
|
+
class TouchNet(nn.Module):
|
|
19
|
+
"""
|
|
20
|
+
TouchNet: A PyTorch neural network for producing surface normal maps from tactile sensor images.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
load_pretrained (bool): If True, loads pretrained weights for the specified sensor type.
|
|
24
|
+
sensor_type (SensorType): The type of tactile sensor. Must be specified if load_pretrained is True.
|
|
25
|
+
root (str or pathlib.Path): The root directory for saving/loading the pretrained_weights (.pth) file.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, load_pretrained: bool = False, sensor_type: SensorType = None, root: Union[str, Path] = "."):
|
|
28
|
+
super().__init__()
|
|
29
|
+
|
|
30
|
+
self._validate_parameters(load_pretrained, sensor_type, root)
|
|
31
|
+
|
|
32
|
+
self.conv1 = nn.Conv2d(5, 32, kernel_size=7, padding=3)
|
|
33
|
+
self.bn1 = nn.BatchNorm2d(32)
|
|
34
|
+
self.dropout1 = nn.Dropout2d(0.2)
|
|
35
|
+
|
|
36
|
+
self.conv2 = nn.Conv2d(32, 64, kernel_size=7, padding=3)
|
|
37
|
+
self.bn2 = nn.BatchNorm2d(64)
|
|
38
|
+
self.dropout2 = nn.Dropout2d(0.2)
|
|
39
|
+
|
|
40
|
+
self.conv3 = nn.Conv2d(64, 128, kernel_size=7, padding=3)
|
|
41
|
+
self.bn3 = nn.BatchNorm2d(128)
|
|
42
|
+
self.dropout3 = nn.Dropout2d(0.2)
|
|
43
|
+
|
|
44
|
+
self.conv4 = nn.Conv2d(128, 256, kernel_size=5, padding=2)
|
|
45
|
+
self.bn4 = nn.BatchNorm2d(256)
|
|
46
|
+
self.dropout4 = nn.Dropout2d(0.3)
|
|
47
|
+
|
|
48
|
+
self.conv5 = nn.Conv2d(256, 256, kernel_size=5, padding=2)
|
|
49
|
+
self.bn5 = nn.BatchNorm2d(256)
|
|
50
|
+
self.dropout5 = nn.Dropout2d(0.3)
|
|
51
|
+
|
|
52
|
+
self.conv6 = nn.Conv2d(256, 128, kernel_size=5, padding=2)
|
|
53
|
+
self.bn6 = nn.BatchNorm2d(128)
|
|
54
|
+
self.dropout6 = nn.Dropout2d(0.2)
|
|
55
|
+
|
|
56
|
+
self.conv7 = nn.Conv2d(128, 64, kernel_size=3, padding=1)
|
|
57
|
+
self.bn7 = nn.BatchNorm2d(64)
|
|
58
|
+
self.dropout7 = nn.Dropout2d(0.2)
|
|
59
|
+
|
|
60
|
+
self.conv8 = nn.Conv2d(64, 32, kernel_size=3, padding=1)
|
|
61
|
+
self.bn8 = nn.BatchNorm2d(32)
|
|
62
|
+
self.dropout8 = nn.Dropout2d(0.2)
|
|
63
|
+
|
|
64
|
+
self.conv9 = nn.Conv2d(32, 2, kernel_size=1)
|
|
65
|
+
|
|
66
|
+
if load_pretrained:
|
|
67
|
+
self._load_pretrained_model(root, sensor_type)
|
|
68
|
+
|
|
69
|
+
def forward(self, x):
|
|
70
|
+
x = F.relu(self.bn1(self.conv1(x)))
|
|
71
|
+
x = self.dropout1(x)
|
|
72
|
+
|
|
73
|
+
x = F.relu(self.bn2(self.conv2(x)))
|
|
74
|
+
x = self.dropout2(x)
|
|
75
|
+
|
|
76
|
+
x = F.relu(self.bn3(self.conv3(x)))
|
|
77
|
+
x = self.dropout3(x)
|
|
78
|
+
|
|
79
|
+
x = F.relu(self.bn4(self.conv4(x)))
|
|
80
|
+
x = self.dropout4(x)
|
|
81
|
+
|
|
82
|
+
x = F.relu(self.bn5(self.conv5(x)))
|
|
83
|
+
x = self.dropout5(x)
|
|
84
|
+
|
|
85
|
+
x = F.relu(self.bn6(self.conv6(x)))
|
|
86
|
+
x = self.dropout6(x)
|
|
87
|
+
|
|
88
|
+
x = F.relu(self.bn7(self.conv7(x)))
|
|
89
|
+
x = self.dropout7(x)
|
|
90
|
+
|
|
91
|
+
x = F.relu(self.bn8(self.conv8(x)))
|
|
92
|
+
x = self.dropout8(x)
|
|
93
|
+
|
|
94
|
+
x = self.conv9(x)
|
|
95
|
+
|
|
96
|
+
return x
|
|
97
|
+
|
|
98
|
+
def _validate_parameters(self, load_pretrained, sensor_type, root):
|
|
99
|
+
if load_pretrained and sensor_type is None:
|
|
100
|
+
raise ValueError("sensor_type must be specified when load_pretrained is True. sensor_type must be either SensorType.DIGIT or SensorType.GELSIGHTMINI.")
|
|
101
|
+
|
|
102
|
+
if load_pretrained and not isinstance(sensor_type, SensorType):
|
|
103
|
+
raise ValueError("sensor_type must be either SensorType.DIGIT or SensorType.GELSIGHTMINI.")
|
|
104
|
+
|
|
105
|
+
if load_pretrained and root is None:
|
|
106
|
+
raise ValueError("root directory for storing/loading model cannot be None when load_pretrained is True.")
|
|
107
|
+
|
|
108
|
+
if load_pretrained and not isinstance(root, (str, Path)):
|
|
109
|
+
raise ValueError("root directory must be a valid file system path as a string or pathlib.Path object when load_pretrained is True.")
|
|
110
|
+
|
|
111
|
+
if not load_pretrained and sensor_type is not None:
|
|
112
|
+
print("Warning: sensor_type parameter is ignored when load_pretrained is False.")
|
|
113
|
+
|
|
114
|
+
if not load_pretrained and root is not ".":
|
|
115
|
+
print("Warning: root parameter is ignored when load_pretrained is False.")
|
|
116
|
+
|
|
117
|
+
def _load_pretrained_model(self, root, sensor_type):
|
|
118
|
+
"""
|
|
119
|
+
Loads a pretrained model for either the DIGIT or GelSightMini sensor.
|
|
120
|
+
Args:
|
|
121
|
+
None.
|
|
122
|
+
Returns:
|
|
123
|
+
None.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
if sensor_type == SensorType.DIGIT:
|
|
127
|
+
file_path = os.path.join(root, "digit_pretrained_weights.pth")
|
|
128
|
+
|
|
129
|
+
# Check if DIGIT pretrained weights exist locally, if not download them
|
|
130
|
+
if not os.path.exists(file_path):
|
|
131
|
+
|
|
132
|
+
print(f"Downloading DIGIT pretrained weights ...")
|
|
133
|
+
response = requests.get('https://zenodo.org/records/17517028/files/digit_pretrained_weights.pth?download=1', stream=True)
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
|
|
136
|
+
total_size = int(response.headers.get('content-length', 0))
|
|
137
|
+
block_size = 1024
|
|
138
|
+
|
|
139
|
+
# Save file in chunks to handle large datasets
|
|
140
|
+
with open(file_path, 'wb') as f, tqdm(
|
|
141
|
+
total=total_size,
|
|
142
|
+
unit='B',
|
|
143
|
+
unit_scale=True,
|
|
144
|
+
desc="Downloading",
|
|
145
|
+
ncols=80
|
|
146
|
+
) as progress_bar:
|
|
147
|
+
for chunk in response.iter_content(chunk_size=block_size):
|
|
148
|
+
if chunk:
|
|
149
|
+
f.write(chunk)
|
|
150
|
+
progress_bar.update(len(chunk))
|
|
151
|
+
|
|
152
|
+
print(f"Download complete!")
|
|
153
|
+
else:
|
|
154
|
+
print(f"DIGIT pretrained weights already exists at: {file_path}/")
|
|
155
|
+
|
|
156
|
+
elif sensor_type == SensorType.GELSIGHTMINI:
|
|
157
|
+
file_path = os.path.join(root, "gsmini_pretrained_weights.pth")
|
|
158
|
+
|
|
159
|
+
# Check if GelSight Mini pretrained weights exist locally, if not download them
|
|
160
|
+
if not os.path.exists(file_path):
|
|
161
|
+
|
|
162
|
+
print(f"Downloading GelSight Mini pretrained weights ...")
|
|
163
|
+
response = requests.get('https://zenodo.org/records/17517028/files/gsmini_pretrained_weights.pth?download=1', stream=True)
|
|
164
|
+
response.raise_for_status()
|
|
165
|
+
|
|
166
|
+
total_size = int(response.headers.get('content-length', 0))
|
|
167
|
+
block_size = 1024
|
|
168
|
+
|
|
169
|
+
# Save file in chunks to handle large datasets
|
|
170
|
+
with open(file_path, 'wb') as f, tqdm(
|
|
171
|
+
total=total_size,
|
|
172
|
+
unit='B',
|
|
173
|
+
unit_scale=True,
|
|
174
|
+
desc="Downloading",
|
|
175
|
+
ncols=80
|
|
176
|
+
) as progress_bar:
|
|
177
|
+
for chunk in response.iter_content(chunk_size=block_size):
|
|
178
|
+
if chunk:
|
|
179
|
+
f.write(chunk)
|
|
180
|
+
progress_bar.update(len(chunk))
|
|
181
|
+
|
|
182
|
+
print(f"Download complete!")
|
|
183
|
+
else:
|
|
184
|
+
print(f"GelSight Mini pretrained weights already exists at: {file_path}/")
|
|
185
|
+
|
|
186
|
+
state_dict = torch.load(file_path, map_location="cpu")
|
|
187
|
+
|
|
188
|
+
self.load_state_dict(state_dict)
|
|
189
|
+
|
|
190
|
+
def load_weights(self, weights_path: Union[str, Path]):
|
|
191
|
+
"""
|
|
192
|
+
Loads model weights from a specified .pth file.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
weights_path (str or pathlib.Path): The file path to the .pth file containing the model weights.
|
|
196
|
+
Returns:
|
|
197
|
+
None.
|
|
198
|
+
Raises:
|
|
199
|
+
ValueError: If the weights_path is not specified or invalid.
|
|
200
|
+
"""
|
|
201
|
+
if weights_path is None:
|
|
202
|
+
raise ValueError("weights_path cannot be None.")
|
|
203
|
+
|
|
204
|
+
if not isinstance(weights_path, (str, Path)):
|
|
205
|
+
raise ValueError("weights_path must be a valid file system path as a string or pathlib.Path object.")
|
|
206
|
+
|
|
207
|
+
if not os.path.exists(weights_path):
|
|
208
|
+
raise ValueError(f"The specified weights_path does not exist: {weights_path}")
|
|
209
|
+
|
|
210
|
+
state_dict = torch.load(weights_path, map_location="cpu")
|
|
211
|
+
self.load_state_dict(state_dict)
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import torch
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from PIL import Image
|
|
7
|
+
from torch.utils.data import Dataset
|
|
8
|
+
from ..lib.precompute_gradients import precompute_gradients
|
|
9
|
+
from ..lib.get_gradient_map import get_gradient_map
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TactileSensorDataset(Dataset):
|
|
13
|
+
"""
|
|
14
|
+
Tactile Sensor dataset.
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self, dataset_path, annotation_path, blank_image_path, transform=None, radius=36):
|
|
17
|
+
self.dataset_path = dataset_path
|
|
18
|
+
self.annotation_path = annotation_path
|
|
19
|
+
self.blank_image_path = blank_image_path
|
|
20
|
+
self.transform = transform
|
|
21
|
+
self.csv_file = dataset_path # Store the CSV filename
|
|
22
|
+
|
|
23
|
+
# Load the CSV data
|
|
24
|
+
self.data = pd.read_csv(annotation_path, comment='#')
|
|
25
|
+
|
|
26
|
+
self.precomputed_gradients = precompute_gradients(dataset_path, annotation_path, r=radius)
|
|
27
|
+
self.blank_tensor = self.transform(Image.open(blank_image_path).convert("RGB"))
|
|
28
|
+
|
|
29
|
+
def _add_coordinate_channels(self, image):
|
|
30
|
+
"""
|
|
31
|
+
Add x and y coordinate channels to the input image.
|
|
32
|
+
X channel: column indices (1s in first column, 2s in second column, etc.)
|
|
33
|
+
Y channel: row indices (1s in first row, 2s in second row, etc.)
|
|
34
|
+
"""
|
|
35
|
+
# Get image dimensions
|
|
36
|
+
_, height, width = image.shape
|
|
37
|
+
|
|
38
|
+
# Create x coordinate channel (column indices)
|
|
39
|
+
x_coords = torch.arange(1, width + 1, dtype=torch.float32).unsqueeze(0).repeat(height, 1)
|
|
40
|
+
x_channel = x_coords.unsqueeze(0) # Add channel dimension
|
|
41
|
+
|
|
42
|
+
# Create y coordinate channel (row indices)
|
|
43
|
+
y_coords = torch.arange(1, height + 1, dtype=torch.float32).unsqueeze(1).repeat(1, width)
|
|
44
|
+
y_channel = y_coords.unsqueeze(0) # Add channel dimension
|
|
45
|
+
|
|
46
|
+
# Concatenate original image with coordinate channels
|
|
47
|
+
image_with_coords = torch.cat([image, x_channel, y_channel], dim=0)
|
|
48
|
+
|
|
49
|
+
return image_with_coords
|
|
50
|
+
|
|
51
|
+
def __len__(self):
|
|
52
|
+
return len(self.data) # Use the DataFrame length
|
|
53
|
+
|
|
54
|
+
def __getitem__(self, idx):
|
|
55
|
+
# Check if index is valid
|
|
56
|
+
if idx < 0 or idx >= len(self.data):
|
|
57
|
+
raise IndexError("Index out of range")
|
|
58
|
+
|
|
59
|
+
if torch.is_tensor(idx):
|
|
60
|
+
idx = idx.tolist()
|
|
61
|
+
|
|
62
|
+
img_name = os.path.join(self.dataset_path, self.data.iloc[idx, 0])
|
|
63
|
+
img = Image.open(img_name).convert("RGB")
|
|
64
|
+
image = np.array(img, copy=True)
|
|
65
|
+
|
|
66
|
+
target = get_gradient_map(idx, annotation_path=self.annotation_path, precomputed_gradients=self.precomputed_gradients)
|
|
67
|
+
|
|
68
|
+
# if self.transform:
|
|
69
|
+
image = self.transform(image)
|
|
70
|
+
target = self.transform(target)
|
|
71
|
+
|
|
72
|
+
# Subtract pre-transformed blank tensor
|
|
73
|
+
image = image - self.blank_tensor
|
|
74
|
+
# Add coordinate channels
|
|
75
|
+
image = self._add_coordinate_channels(image)
|
|
76
|
+
|
|
77
|
+
sample = (image, target)
|
|
78
|
+
return sample
|