Skip to content

Commit d7879de

Browse files
committed
Code and data release
1 parent d334f37 commit d7879de

8 files changed

Lines changed: 519 additions & 1 deletion

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ __pycache__/
33
*.py[cod]
44
*$py.class
55

6+
*.DS_Store
7+
*.h5
8+
*.csv
9+
*.zip
10+
611
# C extensions
712
*.so
813

3d_models/scio_camera_holder.stl

223 KB
Binary file not shown.

README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,42 @@
11
# SpectroVision
22
The SpectroVision dataset and associated code for the paper "Multimodal Material Classification for Robots using Spectroscopy and High Resolution Texture Imaging"
33

4-
Full code release and dataset will become public in the next few days.
4+
![SpectroVision](images/spectrovision.jpg "SpectroVision")
5+
6+
## Install
7+
```bash
8+
git clone https://github.com/Healthcare-Robotics/spectrovision.git
9+
cd spectrovision
10+
# Setup a virtual python environment to install requirements
11+
python3 -m pip install --user virtualenv
12+
python3 -m venv env
13+
source env/bin/activate
14+
pip3 install -r requirements.txt
15+
```
16+
17+
## Download SpectroVision Dataset
18+
Download the processed image and spectral data used for training neural network models:
19+
```bash
20+
wget -O dataset/spectrovision_dataset.zip https://github.com/Healthcare-Robotics/spectrovision/releases/download/1.0/spectrovision_dataset.zip
21+
unzip dataset/spectrovision_dataset.zip -d dataset
22+
```
23+
Download the raw images and spectral measurements:
24+
```bash
25+
wget -O dataset/spectrovision_raw_dataset.zip https://github.com/Healthcare-Robotics/spectrovision/releases/download/1.0/spectrovision_raw_dataset.zip
26+
unzip dataset/spectrovision_raw_dataset.zip -d dataset
27+
```
28+
29+
## Retrain Networks (recompute results from paper)
30+
Note: These computations will take a long time. Computing leave-one-object-out results for a training set of 104 material objects requires training 104 neural networks.
31+
```bash
32+
python3 main.py
33+
```
34+
Results are currently averaged over 10 seeds (which takes 10 times as long). If you wish to use only a single seed to speed up result computations (at the risk of large variation in results), you can use the following command:
35+
```bash
36+
python3 main.py --seed 8000
37+
```
38+
You may also run the script in the background using nohup. This is especially helpful when computing results on a remote machine.
39+
```bash
40+
nohup python3 main.py > results.out &
41+
```
42+

dataset/__init__.py

Whitespace-only changes.

images/spectrovision.jpg

294 KB
Loading

main.py

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import os, random, warnings, argparse
2+
warnings.filterwarnings('ignore')
3+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
4+
import torch
5+
import numpy as np
6+
7+
seed_value = 1000
8+
os.environ['PYTHONHASHSEED'] = str(seed_value)
9+
torch.manual_seed(seed_value)
10+
torch.cuda.manual_seed(seed_value)
11+
torch.cuda.manual_seed_all(seed_value)
12+
np.random.seed(seed_value)
13+
random.seed(seed_value)
14+
torch.manual_seed(seed_value)
15+
torch.backends.cudnn.benchmark = False
16+
torch.backends.cudnn.deterministic = True
17+
torch.set_num_threads(1)
18+
19+
import sys, pickle, time, gc
20+
from poutyne.framework import Model
21+
import multiprocessing
22+
from functools import partial
23+
from multiprocessing.pool import Pool
24+
import util
25+
26+
from sklearn.utils import shuffle
27+
from sklearn import preprocessing
28+
from sklearn.model_selection import StratifiedKFold
29+
from sklearn.metrics import confusion_matrix
30+
31+
def first_deriv(x, wavelengths):
32+
# First derivative of measurements with respect to wavelength
33+
x = np.copy(x)
34+
for i, xx in enumerate(x):
35+
dx = np.zeros(xx.shape, np.float)
36+
dx[0:-1] = np.diff(xx)/np.diff(wavelengths)
37+
dx[-1] = (xx[-1] - xx[-2])/(wavelengths[-1] - wavelengths[-2])
38+
x[i] = dx
39+
return x
40+
41+
def prepare_data(x_spectral_train, x_image_train, y_train, x_spectral_test, x_image_test, y_test, wavelengths, deriv=True, data_type='spectral_image', image_preprocess='resnet', objs=None):
42+
if deriv:
43+
# Finite difference (Numerical differentiation)
44+
x_spectral_train = np.concatenate([x_spectral_train, first_deriv(x_spectral_train, wavelengths)], axis=-1)
45+
x_spectral_test = np.concatenate([x_spectral_test, first_deriv(x_spectral_test, wavelengths)], axis=-1)
46+
47+
if data_type == 'spectral':
48+
# Zero mean unit variance
49+
scaler_spectral = preprocessing.StandardScaler()
50+
x_spectral_train = scaler_spectral.fit_transform(x_spectral_train)
51+
x_spectral_test = scaler_spectral.transform(x_spectral_test)
52+
elif data_type == 'image':
53+
if 'none' in image_preprocess:
54+
scaler_image = preprocessing.StandardScaler()
55+
x_image_train = np.reshape(scaler_image.fit_transform(np.reshape(x_image_train, (len(x_image_train), -1))), np.shape(x_image_train))
56+
x_image_test = np.reshape(scaler_image.transform(np.reshape(x_image_test, (len(x_image_test), -1))), np.shape(x_image_test))
57+
else:
58+
scaler_image = preprocessing.StandardScaler()
59+
x_image_train = scaler_image.fit_transform(x_image_train)
60+
x_image_test = scaler_image.transform(x_image_test)
61+
elif data_type == 'spectral_image':
62+
scaler_image = preprocessing.StandardScaler()
63+
x = scaler_image.fit_transform(np.concatenate([x_spectral_train, x_image_train], axis=-1))
64+
x_spectral_train = x[:, :np.shape(x_spectral_train)[-1]]
65+
x_image_train = x[:, np.shape(x_spectral_train)[-1]:]
66+
x = scaler_image.transform(np.concatenate([x_spectral_test, x_image_test], axis=-1))
67+
x_spectral_test = x[:, :np.shape(x_spectral_test)[-1]]
68+
x_image_test = x[:, np.shape(x_spectral_test)[-1]:]
69+
70+
return x_spectral_train, x_image_train, y_train, x_spectral_test, x_image_test, y_test
71+
72+
def nn(input_size, layers, dropout, material_count=None, batchnorm=True):
73+
modules = []
74+
for i in range(len(layers)-1):
75+
modules.append(torch.nn.Linear(input_size if i == 0 else layers[i-1], layers[i]))
76+
if batchnorm:
77+
modules.append(torch.nn.BatchNorm1d(layers[i]))
78+
modules.append(torch.nn.LeakyReLU())
79+
if dropout > 0:
80+
modules.append(torch.nn.Dropout(dropout))
81+
modules.append(torch.nn.Linear(layers[-2], layers[-1]) if len(layers) > 1 else torch.nn.Linear(input_size, layers[-1]))
82+
if batchnorm:
83+
modules.append(torch.nn.BatchNorm1d(layers[-1]))
84+
modules.append(torch.nn.LeakyReLU())
85+
if material_count is not None:
86+
modules.append(torch.nn.Linear(layers[-1], material_count))
87+
return torch.nn.Sequential(*modules)
88+
89+
def learn(o, X_spectral, X_image, y, objs, wavelengths, data_type='spectral_image', image_preprocess='resnet', epochs=50, batch_size=128, material_count=8, layers=[64]*2, dropout=0.0, lr=0.0005, seed=1000, test='looo', verbose=False):
90+
torch.manual_seed(seed)
91+
np.random.seed(seed)
92+
random.seed(seed)
93+
torch.set_num_threads(1)
94+
95+
if 'test' in test:
96+
train_idx, test_idx = o
97+
X_spectral_train = X_spectral[train_idx]
98+
X_image_train = X_image[train_idx]
99+
y_train = y[train_idx]
100+
X_spectral_test = X_spectral[test_idx]
101+
X_image_test = X_image[test_idx]
102+
y_test = y[test_idx]
103+
objs_train = objs[train_idx]
104+
elif 'looo' in test:
105+
_, obj = o
106+
# Set up leave-one-object-out training and test sets
107+
X_spectral_train = X_spectral[objs != obj]
108+
X_image_train = X_image[objs != obj]
109+
y_train = y[objs != obj]
110+
X_spectral_test = X_spectral[objs == obj]
111+
X_image_test = X_image[objs == obj]
112+
y_test = y[objs == obj]
113+
objs_train = objs[objs != obj]
114+
115+
X_spectral_train, X_image_train, y_train, X_spectral_test, X_image_test, y_test = prepare_data(X_spectral_train, X_image_train, y_train, X_spectral_test, X_image_test, y_test, wavelengths, deriv=True, data_type=data_type, image_preprocess=image_preprocess, objs=objs)
116+
117+
if data_type == 'spectral':
118+
X_spectral_train, y_train = shuffle(X_spectral_train, y_train)
119+
elif data_type == 'image':
120+
X_image_train, y_train = shuffle(X_image_train, y_train)
121+
elif data_type == 'spectral_image':
122+
X_spectral_train, X_image_train, y_train = shuffle(X_spectral_train, X_image_train, y_train)
123+
124+
y_train = torch.tensor(y_train, dtype=torch.long)
125+
y_test = torch.tensor(y_test, dtype=torch.long)
126+
127+
# Image layers
128+
if data_type == 'spectral_image':
129+
spectral_layers = [64, 64, 32, 32]
130+
spectral_dropout = 0.25
131+
spectral_epochs = 50
132+
image_layers = [128, 64, 32]
133+
image_dropout = 0.1
134+
image_epochs = 50
135+
136+
class ConcatPretrainedNetwork(torch.nn.Module):
137+
def __init__(self):
138+
# global spectral_accuracy, image_accuracy
139+
super().__init__()
140+
spectral_net = nn(np.shape(X_spectral_train)[-1], spectral_layers, spectral_dropout, material_count)
141+
opt = torch.optim.Adam(spectral_net.parameters(), lr=lr)
142+
spectral_model = Model(spectral_net, opt, 'cross_entropy', batch_metrics=['accuracy'])
143+
spectral_model.fit(X_spectral_train, y_train, epochs=spectral_epochs, batch_size=batch_size, verbose=False)
144+
145+
image_net = nn(np.shape(X_image_train)[-1], image_layers, image_dropout, material_count)
146+
opt = torch.optim.Adam(image_net.parameters(), lr=lr)
147+
image_model = Model(image_net, opt, 'cross_entropy', batch_metrics=['accuracy'])
148+
image_model.fit(X_image_train, y_train, epochs=image_epochs, batch_size=batch_size, verbose=False)
149+
150+
# Disable dropout, remove last layer and freeze network
151+
self.trained_spectral_model = torch.nn.Sequential(*(list(spectral_net.children())[:-1]))
152+
for p in self.trained_spectral_model.parameters():
153+
p.requires_grad = False
154+
self.trained_image_model = torch.nn.Sequential(*(list(image_net.children())[:-1]))
155+
for p in self.trained_image_model.parameters():
156+
p.requires_grad = False
157+
158+
self.concat_net = nn(spectral_layers[-1] + image_layers[-1], layers, dropout, material_count, batchnorm=False)
159+
def forward(self, x_spectral, x_image):
160+
y1 = self.trained_spectral_model(x_spectral)
161+
y2 = self.trained_image_model(x_image)
162+
concat = torch.cat((y1, y2), -1)
163+
return self.concat_net(concat)
164+
net = ConcatPretrainedNetwork()
165+
X_train = [X_spectral_train, X_image_train]
166+
X_test = [X_spectral_test, X_image_test]
167+
elif data_type == 'image':
168+
net = nn(np.shape(X_image_train)[-1], layers, dropout, material_count)
169+
X_train = X_image_train
170+
X_test = X_image_test
171+
elif data_type == 'spectral':
172+
net = nn(np.shape(X_spectral_train)[-1], layers, dropout, material_count)
173+
X_train = X_spectral_train
174+
X_test = X_spectral_test
175+
176+
opt = torch.optim.Adam(net.parameters(), lr=lr)
177+
model = Model(net, opt, 'cross_entropy', batch_metrics=['accuracy'])
178+
179+
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, batch_size=batch_size, verbose=False)
180+
cm = confusion_matrix(y_test, model.predict(X_test).argmax(axis=-1), labels=range(material_count))
181+
# Return accuracy and confusion matrices
182+
if 'backprop' in test:
183+
return {'accuracy': model.evaluate(X_test, y_test)[-1], 'cm': cm, 'obj_cm': np.copy(cm[y_test[0]]), 'model': model, 'net': net, 'X_test': X_test, 'y_test': y_test}
184+
else:
185+
if verbose:
186+
print(obj, model.evaluate(X_test, y_test)[-1])
187+
return {'accuracy': model.evaluate(X_test, y_test)[-1], 'cm': cm, 'obj_cm': np.copy(cm[y_test[0]])}
188+
189+
# NOTE: Leave-one-object-out cross-validation.
190+
191+
def looo_cv(X_spectral, X_image, y, objs, wavelengths, data_type, image_preprocess, layers, dropout, epochs, batch_size, lr, seeds, material_count, jobs, test='looo', verbose=False):
192+
objs_set = []
193+
for o in objs:
194+
if o not in objs_set:
195+
objs_set.append(o)
196+
accuracies = []
197+
confusion_mat = None
198+
object_confusion_matrix = []
199+
for s in seeds:
200+
pool = Pool(processes=jobs)
201+
results = pool.imap(partial(learn, X_spectral=X_spectral, X_image=X_image, y=y, objs=objs, wavelengths=wavelengths, data_type=data_type, image_preprocess=image_preprocess, epochs=epochs, batch_size=batch_size, material_count=material_count, layers=layers, dropout=dropout, lr=lr, seed=s, test=test, verbose=verbose), list(enumerate(objs_set)))
202+
pool.close()
203+
pool.join()
204+
205+
seed_accuracy = []
206+
for result in list(results):
207+
accuracies.append(result['accuracy'])
208+
seed_accuracy.append(result['accuracy'])
209+
if confusion_mat is None:
210+
confusion_mat = result['cm']
211+
else:
212+
confusion_mat += result['cm']
213+
object_confusion_matrix.append(result['obj_cm'])
214+
print('Accuracy with seed', s, ':', np.mean(seed_accuracy))
215+
print('Results for:', data_type, '5 materials' if five_mats else '8 materials', image_preprocess, '- Accuracy:', np.mean(accuracies))
216+
# print(confusion_mat.astype('float') / confusion_mat.sum(axis=1)[:, np.newaxis], '\n')
217+
sys.stdout.flush()
218+
219+
if __name__ == '__main__':
220+
parser = argparse.ArgumentParser(description='Training and Evaluation for SpectroVision')
221+
parser.add_argument('-s', '--seed', default=-1, help='Seed used for random number generators. (default -1) for averaging of 10 seeds', type=int)
222+
parser.add_argument('-v', '--verbose', help='Verbose', action='store_true')
223+
args = parser.parse_args()
224+
225+
verbose = args.verbose
226+
jobs = multiprocessing.cpu_count()
227+
layers = {'spectral': [64, 64, 32, 32], 'image': [128, 64, 32], 'spectral_image': [32]}
228+
dropout = {'spectral': 0.25, 'image': 0.1, 'spectral_image': 0.0}
229+
epochs = {'spectral': 50, 'image': 50, 'spectral_image': 10}
230+
lr = 0.0005
231+
batch_size = 128
232+
seeds = range(8000, 8010) if args.seed == -1 else [args.seed]
233+
parent_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dataset')
234+
235+
# Table I, Leave-one-object-out
236+
print('Table I: leave-one-object-out')
237+
for five_mats in [True, False]:
238+
for data_type in ['spectral', 'image', 'spectral_image']:
239+
X_spectral, X_image, y, objs, wavelengths = util.load_data(parent_directory, data_type, 'densenet201_240_320', (240, 320), five_mats)
240+
# print(np.shape(X_spectral), np.shape(X_image))
241+
print('Computing results for:', data_type, '5 materials' if five_mats else '8 materials')
242+
looo_cv(X_spectral, X_image, y, objs, wavelengths, data_type, 'densenet201_240_320', layers[data_type], dropout[data_type], epochs[data_type], batch_size, lr, seeds, 5 if five_mats else 8, jobs, 'looo', verbose)
243+
244+
# Table II, Test set
245+
print('Table II: test set')
246+
for five_mats in [True, False]:
247+
for data_type in ['spectral', 'image', 'spectral_image']:
248+
X_spectral, X_image, y, objs, wavelengths = util.load_data(parent_directory, data_type, 'densenet201_240_320', (240, 320), five_mats)
249+
X_spectral_test, X_image_test, y_test, objs_test, wavelengths_test = util.load_data(parent_directory, data_type, 'densenet201_240_320', (240, 320), five_mats, test_set=True)
250+
accuracies = []
251+
confusion_mat = None
252+
for seed in seeds:
253+
results = learn([list(range(len(y))), list(range(len(y), len(y)+len(y_test)))], np.concatenate([X_spectral, X_spectral_test], axis=0), np.concatenate([X_image, X_image_test], axis=0), np.concatenate([y, y_test], axis=0), np.concatenate([objs, objs_test], axis=0), wavelengths, data_type, 'densenet201_240_320', epochs[data_type], batch_size, 5 if five_mats else 8, layers[data_type], dropout[data_type], lr, seed, 'test', verbose)
254+
accuracies.append(results['accuracy'])
255+
if confusion_mat is None:
256+
confusion_mat = results['cm']
257+
else:
258+
confusion_mat += results['cm']
259+
print(data_type, '5 materials' if five_mats else '8 materials', '- Accuracy:', np.mean(accuracies))
260+
# print(confusion_mat.astype('float') / confusion_mat.sum(axis=1)[:, np.newaxis], '\n')
261+
sys.stdout.flush()
262+
263+
# Table IV, Resizing and cropping images
264+
print('Table IV: resizing and cropping images')
265+
for image_shape, crop in [((240, 320), False), ((240, 320), True), ((480, 640), False), ((480, 640), True), ((960, 1280), False)]:
266+
five_mats = False
267+
data_type = 'image'
268+
image_preprocess = 'densenet201_%d_%d%s' % (image_shape[0], image_shape[1], '_crop' if crop else '')
269+
X_spectral, X_image, y, objs, wavelengths = util.load_data(parent_directory, data_type, image_preprocess, (240, 320), five_mats)
270+
looo_cv(X_spectral, X_image, y, objs, wavelengths, data_type, image_preprocess, layers[data_type], dropout[data_type], epochs[data_type], batch_size, lr, seeds, 5 if five_mats else 8, jobs, 'looo', verbose)
271+
272+
# Table V, ImageNet models
273+
print('Table V: ImageNet models')
274+
for model in ['vgg19', 'resnet50', 'resnet101', 'resnet152', 'densenet201', 'resnext101', 'efficientnet-b5']:
275+
five_mats = False
276+
data_type = 'image'
277+
image_preprocess = ('%s_240_320' % model) if 'efficient' not in model else ('%s_456_608' % model)
278+
X_spectral, X_image, y, objs, wavelengths = util.load_data(parent_directory, data_type, image_preprocess, (240, 320), five_mats)
279+
looo_cv(X_spectral, X_image, y, objs, wavelengths, data_type, image_preprocess, layers[data_type], dropout[data_type], epochs[data_type], batch_size, lr, seeds, 5 if five_mats else 8, jobs, 'looo', verbose)
280+

requirements.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
cycler==0.10.0
2+
h5py==2.10.0
3+
joblib==0.14.1
4+
kiwisolver==1.2.0
5+
matplotlib==3.2.1
6+
numpy==1.18.2
7+
Pillow==7.1.1
8+
Poutyne==0.7.2
9+
pyparsing==2.4.6
10+
python-dateutil==2.8.1
11+
scikit-learn==0.22.2.post1
12+
scipy==1.4.1
13+
six==1.14.0
14+
torch==1.4.0
15+
torchvision==0.5.0

0 commit comments

Comments
 (0)