tangle-cnn 0.98.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.
- tangle_cnn/__init__.py +0 -0
- tangle_cnn/__main__.py +4 -0
- tangle_cnn/blockface_to_multichannel.py +105 -0
- tangle_cnn/deepcluster/__init__.py +0 -0
- tangle_cnn/deepcluster/clustering.py +378 -0
- tangle_cnn/deepcluster/eval_linear.py +319 -0
- tangle_cnn/deepcluster/eval_retrieval.py +481 -0
- tangle_cnn/deepcluster/eval_voc_classif.py +293 -0
- tangle_cnn/deepcluster/main.py +327 -0
- tangle_cnn/deepcluster/models/__init__.py +8 -0
- tangle_cnn/deepcluster/models/alexnet.py +98 -0
- tangle_cnn/deepcluster/models/vgg16.py +94 -0
- tangle_cnn/deepcluster/util.py +132 -0
- tangle_cnn/deepcluster/visu/activ-retrieval.py +114 -0
- tangle_cnn/deepcluster/visu/gradient_ascent.py +139 -0
- tangle_cnn/levels.py +5 -0
- tangle_cnn/nissl_to_multichannel.py +127 -0
- tangle_cnn/osl_worker.py +110 -0
- tangle_cnn/unet_wildcat.py +206 -0
- tangle_cnn/unet_wildcat_gmm.py +635 -0
- tangle_cnn/wildcat_main.py +1066 -0
- tangle_cnn/wildcat_pytorch/picsl_wildcat/gmm.py +737 -0
- tangle_cnn/wildcat_pytorch/picsl_wildcat/losses.py +140 -0
- tangle_cnn/wildcat_pytorch/picsl_wildcat/models.py +245 -0
- tangle_cnn/wildcat_pytorch/picsl_wildcat/util.py +170 -0
- tangle_cnn/wildcat_pytorch/wildcat/__init__.py +0 -0
- tangle_cnn/wildcat_pytorch/wildcat/demo_mit67.py +80 -0
- tangle_cnn/wildcat_pytorch/wildcat/demo_voc2007.py +80 -0
- tangle_cnn/wildcat_pytorch/wildcat/engine.py +510 -0
- tangle_cnn/wildcat_pytorch/wildcat/mit67.py +167 -0
- tangle_cnn/wildcat_pytorch/wildcat/models.py +61 -0
- tangle_cnn/wildcat_pytorch/wildcat/pooling.py +139 -0
- tangle_cnn/wildcat_pytorch/wildcat/util.py +176 -0
- tangle_cnn/wildcat_pytorch/wildcat/voc.py +259 -0
- tangle_cnn-0.98.0.dist-info/METADATA +219 -0
- tangle_cnn-0.98.0.dist-info/RECORD +39 -0
- tangle_cnn-0.98.0.dist-info/WHEEL +5 -0
- tangle_cnn-0.98.0.dist-info/licenses/LICENSE.txt +674 -0
- tangle_cnn-0.98.0.dist-info/top_level.txt +1 -0
tangle_cnn/__init__.py
ADDED
|
File without changes
|
tangle_cnn/__main__.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Set up
|
|
2
|
+
import torchvision.transforms as transforms
|
|
3
|
+
import torch.utils.data
|
|
4
|
+
import numpy as np
|
|
5
|
+
import argparse
|
|
6
|
+
import SimpleITK as sitk
|
|
7
|
+
from PIL import Image
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
sys.path.append("deepcluster")
|
|
11
|
+
from deepcluster.util import load_model
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Process a slide
|
|
15
|
+
def apply_to_slide(args):
|
|
16
|
+
|
|
17
|
+
# Read the trained model
|
|
18
|
+
model = load_model(args.network)
|
|
19
|
+
model.cuda()
|
|
20
|
+
model.eval()
|
|
21
|
+
|
|
22
|
+
# Read the slide
|
|
23
|
+
I = Image.open(args.slide)
|
|
24
|
+
h,w=I.size
|
|
25
|
+
|
|
26
|
+
# Split the transforms
|
|
27
|
+
tran_tt = transforms.ToTensor()
|
|
28
|
+
tran_model=transforms.Compose([
|
|
29
|
+
transforms.ToPILImage(),
|
|
30
|
+
transforms.Resize(256),
|
|
31
|
+
transforms.CenterCrop(224),
|
|
32
|
+
transforms.ToTensor(),
|
|
33
|
+
transforms.Normalize(mean=[0.485, 0.456, 0.406],
|
|
34
|
+
std=[0.229, 0.224, 0.225])])
|
|
35
|
+
|
|
36
|
+
# Get the patch size and downsample factor
|
|
37
|
+
patch_size, ds = int(args.patch), int(args.downsample)
|
|
38
|
+
|
|
39
|
+
# Image as tensor
|
|
40
|
+
I_crop=I.crop((0, 0, ds*int(h/ds), ds*int(w/ds)))
|
|
41
|
+
IT = tran_tt(I_crop)
|
|
42
|
+
|
|
43
|
+
# Image unfolded into patches
|
|
44
|
+
IT_unfold = IT.unfold(1, patch_size, ds).unfold(2, patch_size, ds).permute(1, 2, 0, 3, 4)
|
|
45
|
+
|
|
46
|
+
# Output dimensions (before padding)
|
|
47
|
+
(ow, oh) = IT_unfold.shape[0], IT_unfold.shape[1]
|
|
48
|
+
|
|
49
|
+
# Flat array of inputs
|
|
50
|
+
batches = IT_unfold.reshape(-1, 3, patch_size, patch_size)
|
|
51
|
+
|
|
52
|
+
# Break into digestable batches
|
|
53
|
+
bs = int(args.batch_size)
|
|
54
|
+
for k in range(0, batches.shape[0], bs):
|
|
55
|
+
k_end = min(k + bs, batches.shape[0])
|
|
56
|
+
batch=torch.zeros((k_end-k,3,224,224))
|
|
57
|
+
for j in range(k,k_end):
|
|
58
|
+
batch[j-k,:,:,:]=tran_model(batches[j,:,:,:])
|
|
59
|
+
with torch.no_grad():
|
|
60
|
+
res_batch=model(batch.cuda()).detach().cpu()
|
|
61
|
+
if k == 0:
|
|
62
|
+
result = res_batch
|
|
63
|
+
else:
|
|
64
|
+
result = torch.cat((result, res_batch), 0)
|
|
65
|
+
print('Batch %d of %d' % (k / bs, batches.shape[0] / bs))
|
|
66
|
+
|
|
67
|
+
# Reformat into a 20xWxH image
|
|
68
|
+
Z = result.permute(1, 0).reshape(-1, ow, oh)
|
|
69
|
+
|
|
70
|
+
# Pad the result to desired size
|
|
71
|
+
owp, ohp = int(w * 1.0 / ds + 0.5), int(h * 1.0 / ds + 0.5)
|
|
72
|
+
pw0, ph0 = int((owp - ow) / 2), int((ohp - oh) / 2)
|
|
73
|
+
pw1, ph1 = owp - ow - pw0, ohp - oh - ph0
|
|
74
|
+
Z = torch.nn.functional.pad(Z, (ph0, ph1, pw0, pw1, 0, 0), 'constant', 0)
|
|
75
|
+
|
|
76
|
+
# Write the result as a NIFTI file
|
|
77
|
+
nii = sitk.GetImageFromArray(Z.permute(1,2,0), True)
|
|
78
|
+
nii.SetSpacing((ds, ds))
|
|
79
|
+
sitk.WriteImage(nii, args.output)
|
|
80
|
+
|
|
81
|
+
# Write the optional thumb
|
|
82
|
+
if args.thumb is not None:
|
|
83
|
+
rgb = np.asarray(I.resize((ohp,owp),Image.LANCZOS))
|
|
84
|
+
nii = sitk.GetImageFromArray(rgb, True)
|
|
85
|
+
nii.SetSpacing((ds, ds))
|
|
86
|
+
sitk.WriteImage(nii, args.thumb)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Set up an argument parser
|
|
90
|
+
parser = argparse.ArgumentParser()
|
|
91
|
+
subparsers = parser.add_subparsers()
|
|
92
|
+
|
|
93
|
+
apply_parser = subparsers.add_parser('apply')
|
|
94
|
+
apply_parser.add_argument('--slide', help='Input histology slide to process')
|
|
95
|
+
apply_parser.add_argument('--output', help='Where to store the output density map')
|
|
96
|
+
apply_parser.add_argument('--thumb', help='Optional thumbnail output', default=None)
|
|
97
|
+
apply_parser.add_argument('--network', help='Network saved during training')
|
|
98
|
+
apply_parser.add_argument('--patch', help='Patch size used during training', default=64)
|
|
99
|
+
apply_parser.add_argument('--downsample', help='Input to output downsampling ratio', default=16)
|
|
100
|
+
apply_parser.add_argument('--batch-size', help='GPU batch size', default=256)
|
|
101
|
+
apply_parser.set_defaults(func=apply_to_slide)
|
|
102
|
+
|
|
103
|
+
if __name__ == '__main__':
|
|
104
|
+
args = parser.parse_args()
|
|
105
|
+
args.func(args)
|
|
File without changes
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# Copyright (c) 2017-present, Facebook, Inc.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This source code is licensed under the license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
#
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import faiss
|
|
10
|
+
import numpy as np
|
|
11
|
+
from PIL import Image
|
|
12
|
+
from PIL import ImageFile
|
|
13
|
+
from scipy.sparse import csr_matrix, find
|
|
14
|
+
import torch
|
|
15
|
+
import torch.utils.data as data
|
|
16
|
+
import torchvision.transforms as transforms
|
|
17
|
+
|
|
18
|
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
19
|
+
|
|
20
|
+
__all__ = ['PIC', 'Kmeans', 'cluster_assign', 'arrange_clustering']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def pil_loader(path):
|
|
24
|
+
"""Loads an image.
|
|
25
|
+
Args:
|
|
26
|
+
path (string): path to image file
|
|
27
|
+
Returns:
|
|
28
|
+
Image
|
|
29
|
+
"""
|
|
30
|
+
with open(path, 'rb') as f:
|
|
31
|
+
img = Image.open(f)
|
|
32
|
+
return img.convert('RGB')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReassignedDataset(data.Dataset):
|
|
36
|
+
"""A dataset where the new images labels are given in argument.
|
|
37
|
+
Args:
|
|
38
|
+
image_indexes (list): list of data indexes
|
|
39
|
+
pseudolabels (list): list of labels for each data
|
|
40
|
+
dataset (list): list of tuples with paths to images
|
|
41
|
+
transform (callable, optional): a function/transform that takes in
|
|
42
|
+
an PIL image and returns a
|
|
43
|
+
transformed version
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, image_indexes, pseudolabels, dataset, transform=None):
|
|
47
|
+
self.imgs = self.make_dataset(image_indexes, pseudolabels, dataset)
|
|
48
|
+
self.transform = transform
|
|
49
|
+
|
|
50
|
+
def make_dataset(self, image_indexes, pseudolabels, dataset):
|
|
51
|
+
label_to_idx = {label: idx for idx, label in enumerate(set(pseudolabels))}
|
|
52
|
+
images = []
|
|
53
|
+
for j, idx in enumerate(image_indexes):
|
|
54
|
+
path = dataset[idx][0]
|
|
55
|
+
pseudolabel = label_to_idx[pseudolabels[j]]
|
|
56
|
+
images.append((path, pseudolabel))
|
|
57
|
+
return images
|
|
58
|
+
|
|
59
|
+
def __getitem__(self, index):
|
|
60
|
+
"""
|
|
61
|
+
Args:
|
|
62
|
+
index (int): index of data
|
|
63
|
+
Returns:
|
|
64
|
+
tuple: (image, pseudolabel) where pseudolabel is the cluster of index datapoint
|
|
65
|
+
"""
|
|
66
|
+
path, pseudolabel = self.imgs[index]
|
|
67
|
+
img = pil_loader(path)
|
|
68
|
+
if self.transform is not None:
|
|
69
|
+
img = self.transform(img)
|
|
70
|
+
return img, pseudolabel
|
|
71
|
+
|
|
72
|
+
def __len__(self):
|
|
73
|
+
return len(self.imgs)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def preprocess_features(npdata, pca=256):
|
|
77
|
+
"""Preprocess an array of features.
|
|
78
|
+
Args:
|
|
79
|
+
npdata (np.array N * ndim): features to preprocess
|
|
80
|
+
pca (int): dim of output
|
|
81
|
+
Returns:
|
|
82
|
+
np.array of dim N * pca: data PCA-reduced, whitened and L2-normalized
|
|
83
|
+
"""
|
|
84
|
+
_, ndim = npdata.shape
|
|
85
|
+
npdata = npdata.astype('float32')
|
|
86
|
+
|
|
87
|
+
# Apply PCA-whitening with Faiss
|
|
88
|
+
mat = faiss.PCAMatrix (ndim, pca, eigen_power=-0.5)
|
|
89
|
+
mat.train(npdata)
|
|
90
|
+
assert mat.is_trained
|
|
91
|
+
npdata = mat.apply_py(npdata)
|
|
92
|
+
|
|
93
|
+
# L2 normalization
|
|
94
|
+
row_sums = np.linalg.norm(npdata, axis=1)
|
|
95
|
+
npdata = npdata / row_sums[:, np.newaxis]
|
|
96
|
+
|
|
97
|
+
return npdata
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def make_graph(xb, nnn):
|
|
101
|
+
"""Builds a graph of nearest neighbors.
|
|
102
|
+
Args:
|
|
103
|
+
xb (np.array): data
|
|
104
|
+
nnn (int): number of nearest neighbors
|
|
105
|
+
Returns:
|
|
106
|
+
list: for each data the list of ids to its nnn nearest neighbors
|
|
107
|
+
list: for each data the list of distances to its nnn NN
|
|
108
|
+
"""
|
|
109
|
+
N, dim = xb.shape
|
|
110
|
+
|
|
111
|
+
# we need only a StandardGpuResources per GPU
|
|
112
|
+
res = faiss.StandardGpuResources()
|
|
113
|
+
|
|
114
|
+
# L2
|
|
115
|
+
flat_config = faiss.GpuIndexFlatConfig()
|
|
116
|
+
flat_config.device = int(torch.cuda.device_count()) - 1
|
|
117
|
+
index = faiss.GpuIndexFlatL2(res, dim, flat_config)
|
|
118
|
+
index.add(xb)
|
|
119
|
+
D, I = index.search(xb, nnn + 1)
|
|
120
|
+
return I, D
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def cluster_assign(images_lists, dataset):
|
|
124
|
+
"""Creates a dataset from clustering, with clusters as labels.
|
|
125
|
+
Args:
|
|
126
|
+
images_lists (list of list): for each cluster, the list of image indexes
|
|
127
|
+
belonging to this cluster
|
|
128
|
+
dataset (list): initial dataset
|
|
129
|
+
Returns:
|
|
130
|
+
ReassignedDataset(torch.utils.data.Dataset): a dataset with clusters as
|
|
131
|
+
labels
|
|
132
|
+
"""
|
|
133
|
+
assert images_lists is not None
|
|
134
|
+
pseudolabels = []
|
|
135
|
+
image_indexes = []
|
|
136
|
+
for cluster, images in enumerate(images_lists):
|
|
137
|
+
image_indexes.extend(images)
|
|
138
|
+
pseudolabels.extend([cluster] * len(images))
|
|
139
|
+
|
|
140
|
+
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],
|
|
141
|
+
std=[0.229, 0.224, 0.225])
|
|
142
|
+
t = transforms.Compose([transforms.RandomResizedCrop(224),
|
|
143
|
+
transforms.RandomHorizontalFlip(),
|
|
144
|
+
transforms.ToTensor(),
|
|
145
|
+
normalize])
|
|
146
|
+
|
|
147
|
+
return ReassignedDataset(image_indexes, pseudolabels, dataset, t)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def run_kmeans(x, nmb_clusters, verbose=False):
|
|
151
|
+
"""Runs kmeans on 1 GPU.
|
|
152
|
+
Args:
|
|
153
|
+
x: data
|
|
154
|
+
nmb_clusters (int): number of clusters
|
|
155
|
+
Returns:
|
|
156
|
+
list: ids of data in each cluster
|
|
157
|
+
"""
|
|
158
|
+
n_data, d = x.shape
|
|
159
|
+
|
|
160
|
+
# faiss implementation of k-means
|
|
161
|
+
clus = faiss.Clustering(d, nmb_clusters)
|
|
162
|
+
|
|
163
|
+
# Change faiss seed at each k-means so that the randomly picked
|
|
164
|
+
# initialization centroids do not correspond to the same feature ids
|
|
165
|
+
# from an epoch to another.
|
|
166
|
+
clus.seed = np.random.randint(1234)
|
|
167
|
+
|
|
168
|
+
clus.niter = 20
|
|
169
|
+
clus.max_points_per_centroid = 10000000
|
|
170
|
+
res = faiss.StandardGpuResources()
|
|
171
|
+
flat_config = faiss.GpuIndexFlatConfig()
|
|
172
|
+
flat_config.useFloat16 = False
|
|
173
|
+
flat_config.device = 0
|
|
174
|
+
index = faiss.GpuIndexFlatL2(res, d, flat_config)
|
|
175
|
+
|
|
176
|
+
# perform the training
|
|
177
|
+
clus.train(x, index)
|
|
178
|
+
_, I = index.search(x, 1)
|
|
179
|
+
losses = faiss.vector_to_array(clus.obj)
|
|
180
|
+
if verbose:
|
|
181
|
+
print('k-means loss evolution: {0}'.format(losses))
|
|
182
|
+
|
|
183
|
+
return [int(n[0]) for n in I], losses[-1]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def arrange_clustering(images_lists):
|
|
187
|
+
pseudolabels = []
|
|
188
|
+
image_indexes = []
|
|
189
|
+
for cluster, images in enumerate(images_lists):
|
|
190
|
+
image_indexes.extend(images)
|
|
191
|
+
pseudolabels.extend([cluster] * len(images))
|
|
192
|
+
indexes = np.argsort(image_indexes)
|
|
193
|
+
return np.asarray(pseudolabels)[indexes]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Kmeans(object):
|
|
197
|
+
def __init__(self, k):
|
|
198
|
+
self.k = k
|
|
199
|
+
|
|
200
|
+
def cluster(self, data, verbose=False):
|
|
201
|
+
"""Performs k-means clustering.
|
|
202
|
+
Args:
|
|
203
|
+
x_data (np.array N * dim): data to cluster
|
|
204
|
+
"""
|
|
205
|
+
end = time.time()
|
|
206
|
+
|
|
207
|
+
# PCA-reducing, whitening and L2-normalization
|
|
208
|
+
xb = preprocess_features(data)
|
|
209
|
+
|
|
210
|
+
# cluster the data
|
|
211
|
+
I, loss = run_kmeans(xb, self.k, verbose)
|
|
212
|
+
self.images_lists = [[] for i in range(self.k)]
|
|
213
|
+
for i in range(len(data)):
|
|
214
|
+
self.images_lists[I[i]].append(i)
|
|
215
|
+
|
|
216
|
+
if verbose:
|
|
217
|
+
print('k-means time: {0:.0f} s'.format(time.time() - end))
|
|
218
|
+
|
|
219
|
+
return loss
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def make_adjacencyW(I, D, sigma):
|
|
223
|
+
"""Create adjacency matrix with a Gaussian kernel.
|
|
224
|
+
Args:
|
|
225
|
+
I (numpy array): for each vertex the ids to its nnn linked vertices
|
|
226
|
+
+ first column of identity.
|
|
227
|
+
D (numpy array): for each data the l2 distances to its nnn linked vertices
|
|
228
|
+
+ first column of zeros.
|
|
229
|
+
sigma (float): Bandwidth of the Gaussian kernel.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
csr_matrix: affinity matrix of the graph.
|
|
233
|
+
"""
|
|
234
|
+
V, k = I.shape
|
|
235
|
+
k = k - 1
|
|
236
|
+
indices = np.reshape(np.delete(I, 0, 1), (1, -1))
|
|
237
|
+
indptr = np.multiply(k, np.arange(V + 1))
|
|
238
|
+
|
|
239
|
+
def exp_ker(d):
|
|
240
|
+
return np.exp(-d / sigma**2)
|
|
241
|
+
|
|
242
|
+
exp_ker = np.vectorize(exp_ker)
|
|
243
|
+
res_D = exp_ker(D)
|
|
244
|
+
data = np.reshape(np.delete(res_D, 0, 1), (1, -1))
|
|
245
|
+
adj_matrix = csr_matrix((data[0], indices[0], indptr), shape=(V, V))
|
|
246
|
+
return adj_matrix
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def run_pic(I, D, sigma, alpha):
|
|
250
|
+
"""Run PIC algorithm"""
|
|
251
|
+
a = make_adjacencyW(I, D, sigma)
|
|
252
|
+
graph = a + a.transpose()
|
|
253
|
+
cgraph = graph
|
|
254
|
+
nim = graph.shape[0]
|
|
255
|
+
|
|
256
|
+
W = graph
|
|
257
|
+
t0 = time.time()
|
|
258
|
+
|
|
259
|
+
v0 = np.ones(nim) / nim
|
|
260
|
+
|
|
261
|
+
# power iterations
|
|
262
|
+
v = v0.astype('float32')
|
|
263
|
+
|
|
264
|
+
t0 = time.time()
|
|
265
|
+
dt = 0
|
|
266
|
+
for i in range(200):
|
|
267
|
+
vnext = np.zeros(nim, dtype='float32')
|
|
268
|
+
|
|
269
|
+
vnext = vnext + W.transpose().dot(v)
|
|
270
|
+
|
|
271
|
+
vnext = alpha * vnext + (1 - alpha) / nim
|
|
272
|
+
# L1 normalize
|
|
273
|
+
vnext /= vnext.sum()
|
|
274
|
+
v = vnext
|
|
275
|
+
|
|
276
|
+
if i == 200 - 1:
|
|
277
|
+
clust = find_maxima_cluster(W, v)
|
|
278
|
+
|
|
279
|
+
return [int(i) for i in clust]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def find_maxima_cluster(W, v):
|
|
283
|
+
n, m = W.shape
|
|
284
|
+
assert (n == m)
|
|
285
|
+
assign = np.zeros(n)
|
|
286
|
+
# for each node
|
|
287
|
+
pointers = list(range(n))
|
|
288
|
+
for i in range(n):
|
|
289
|
+
best_vi = 0
|
|
290
|
+
l0 = W.indptr[i]
|
|
291
|
+
l1 = W.indptr[i + 1]
|
|
292
|
+
for l in range(l0, l1):
|
|
293
|
+
j = W.indices[l]
|
|
294
|
+
vi = W.data[l] * (v[j] - v[i])
|
|
295
|
+
if vi > best_vi:
|
|
296
|
+
best_vi = vi
|
|
297
|
+
pointers[i] = j
|
|
298
|
+
n_clus = 0
|
|
299
|
+
cluster_ids = -1 * np.ones(n)
|
|
300
|
+
for i in range(n):
|
|
301
|
+
if pointers[i] == i:
|
|
302
|
+
cluster_ids[i] = n_clus
|
|
303
|
+
n_clus = n_clus + 1
|
|
304
|
+
for i in range(n):
|
|
305
|
+
# go from pointers to pointers starting from i until reached a local optim
|
|
306
|
+
current_node = i
|
|
307
|
+
while pointers[current_node] != current_node:
|
|
308
|
+
current_node = pointers[current_node]
|
|
309
|
+
|
|
310
|
+
assign[i] = cluster_ids[current_node]
|
|
311
|
+
assert (assign[i] >= 0)
|
|
312
|
+
return assign
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class PIC(object):
|
|
316
|
+
"""Class to perform Power Iteration Clustering on a graph of nearest neighbors.
|
|
317
|
+
Args:
|
|
318
|
+
args: for consistency with k-means init
|
|
319
|
+
sigma (float): bandwidth of the Gaussian kernel (default 0.2)
|
|
320
|
+
nnn (int): number of nearest neighbors (default 5)
|
|
321
|
+
alpha (float): parameter in PIC (default 0.001)
|
|
322
|
+
distribute_singletons (bool): If True, reassign each singleton to
|
|
323
|
+
the cluster of its closest non
|
|
324
|
+
singleton nearest neighbors (up to nnn
|
|
325
|
+
nearest neighbors).
|
|
326
|
+
Attributes:
|
|
327
|
+
images_lists (list of list): for each cluster, the list of image indexes
|
|
328
|
+
belonging to this cluster
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def __init__(self, args=None, sigma=0.2, nnn=5, alpha=0.001, distribute_singletons=True):
|
|
332
|
+
self.sigma = sigma
|
|
333
|
+
self.alpha = alpha
|
|
334
|
+
self.nnn = nnn
|
|
335
|
+
self.distribute_singletons = distribute_singletons
|
|
336
|
+
|
|
337
|
+
def cluster(self, data, verbose=False):
|
|
338
|
+
end = time.time()
|
|
339
|
+
|
|
340
|
+
# preprocess the data
|
|
341
|
+
xb = preprocess_features(data)
|
|
342
|
+
|
|
343
|
+
# construct nnn graph
|
|
344
|
+
I, D = make_graph(xb, self.nnn)
|
|
345
|
+
|
|
346
|
+
# run PIC
|
|
347
|
+
clust = run_pic(I, D, self.sigma, self.alpha)
|
|
348
|
+
images_lists = {}
|
|
349
|
+
for h in set(clust):
|
|
350
|
+
images_lists[h] = []
|
|
351
|
+
for data, c in enumerate(clust):
|
|
352
|
+
images_lists[c].append(data)
|
|
353
|
+
|
|
354
|
+
# allocate singletons to clusters of their closest NN not singleton
|
|
355
|
+
if self.distribute_singletons:
|
|
356
|
+
clust_NN = {}
|
|
357
|
+
for i in images_lists:
|
|
358
|
+
# if singleton
|
|
359
|
+
if len(images_lists[i]) == 1:
|
|
360
|
+
s = images_lists[i][0]
|
|
361
|
+
# for NN
|
|
362
|
+
for n in I[s, 1:]:
|
|
363
|
+
# if NN is not a singleton
|
|
364
|
+
if not len(images_lists[clust[n]]) == 1:
|
|
365
|
+
clust_NN[s] = n
|
|
366
|
+
break
|
|
367
|
+
for s in clust_NN:
|
|
368
|
+
del images_lists[clust[s]]
|
|
369
|
+
clust[s] = clust[clust_NN[s]]
|
|
370
|
+
images_lists[clust[s]].append(s)
|
|
371
|
+
|
|
372
|
+
self.images_lists = []
|
|
373
|
+
for c in images_lists:
|
|
374
|
+
self.images_lists.append(images_lists[c])
|
|
375
|
+
|
|
376
|
+
if verbose:
|
|
377
|
+
print('pic time: {0:.0f} s'.format(time.time() - end))
|
|
378
|
+
return 0
|