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.
Files changed (44) hide show
  1. py3DCal/__init__.py +12 -0
  2. py3DCal/data_collection/Calibrator.py +298 -0
  3. py3DCal/data_collection/Printers/Ender3/Ender3.py +82 -0
  4. py3DCal/data_collection/Printers/Ender3/__init__.py +0 -0
  5. py3DCal/data_collection/Printers/Printer.py +63 -0
  6. py3DCal/data_collection/Printers/__init__.py +0 -0
  7. py3DCal/data_collection/Sensors/DIGIT/DIGIT.py +47 -0
  8. py3DCal/data_collection/Sensors/DIGIT/__init__.py +0 -0
  9. py3DCal/data_collection/Sensors/DIGIT/default.csv +1222 -0
  10. py3DCal/data_collection/Sensors/GelsightMini/GelsightMini.py +45 -0
  11. py3DCal/data_collection/Sensors/GelsightMini/__init__.py +0 -0
  12. py3DCal/data_collection/Sensors/GelsightMini/default.csv +1210 -0
  13. py3DCal/data_collection/Sensors/Sensor.py +35 -0
  14. py3DCal/data_collection/Sensors/__init__.py +0 -0
  15. py3DCal/data_collection/__init__.py +0 -0
  16. py3DCal/model_training/__init__.py +0 -0
  17. py3DCal/model_training/datasets/DIGIT_dataset.py +75 -0
  18. py3DCal/model_training/datasets/GelSightMini_dataset.py +73 -0
  19. py3DCal/model_training/datasets/__init__.py +3 -0
  20. py3DCal/model_training/datasets/split_dataset.py +38 -0
  21. py3DCal/model_training/datasets/tactile_sensor_dataset.py +82 -0
  22. py3DCal/model_training/lib/__init__.py +0 -0
  23. py3DCal/model_training/lib/add_coordinate_embeddings.py +29 -0
  24. py3DCal/model_training/lib/depthmaps.py +74 -0
  25. py3DCal/model_training/lib/fast_poisson.py +51 -0
  26. py3DCal/model_training/lib/get_gradient_map.py +39 -0
  27. py3DCal/model_training/lib/precompute_gradients.py +61 -0
  28. py3DCal/model_training/lib/train_model.py +96 -0
  29. py3DCal/model_training/lib/validate_device.py +22 -0
  30. py3DCal/model_training/lib/validate_parameters.py +45 -0
  31. py3DCal/model_training/models/__init__.py +1 -0
  32. py3DCal/model_training/models/touchnet.py +211 -0
  33. py3DCal/model_training/touchnet/__init__.py +0 -0
  34. py3DCal/model_training/touchnet/dataset.py +78 -0
  35. py3DCal/model_training/touchnet/touchnet.py +736 -0
  36. py3DCal/model_training/touchnet/touchnet_architecture.py +72 -0
  37. py3DCal/utils/__init__.py +0 -0
  38. py3DCal/utils/utils.py +32 -0
  39. py3dcal-1.0.0.dist-info/LICENSE +21 -0
  40. py3dcal-1.0.0.dist-info/METADATA +29 -0
  41. py3dcal-1.0.0.dist-info/RECORD +44 -0
  42. py3dcal-1.0.0.dist-info/WHEEL +5 -0
  43. py3dcal-1.0.0.dist-info/entry_points.txt +3 -0
  44. 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