1
2 """
3 The :mod:`parsimony.functions.nesterov.tv` module contains the loss function
4 and helper functions for Total variation, TV, smoothed using Nesterov's
5 technique.
6
7 Created on Mon Feb 3 10:46:47 2014
8
9 Copyright (c) 2013-2014, CEA/DSV/I2BM/Neurospin. All rights reserved.
10
11 @author: Tommy Löfstedt, Edouard Duchesnay
12 @email: lofstedt.tommy@gmail.com, edouard.duchesnay@cea.fr
13 @license: BSD 3-clause.
14 """
15 import math
16
17 import scipy.sparse as sparse
18 import numpy as np
19
20 from .. import properties
21 import parsimony.utils.consts as consts
22 import parsimony.utils.maths as maths
23 import parsimony.utils as utils
24
25 __all__ = ["TotalVariation",
26 "linear_operator_from_mask", "A_from_mask",
27 "linear_operator_from_subset_mask", "A_from_subset_mask",
28 "linear_operator_from_shape", "A_from_shape",
29 "linear_operator_from_mesh", "A_from_mesh",
30 "nesterov_linear_operator_from_mesh"]
31
32
33 -class TotalVariation(properties.NesterovFunction,
34 properties.Penalty,
35 properties.Constraint):
36 """The smoothed Total variation (TV) function
37
38 f(beta) = l * (TV(beta) - c),
39
40 where TV(beta) is the smoothed total variation function. The constrained
41 version has the form
42
43 TV(beta) <= c.
44 """
45 - def __init__(self, l, c=0.0, A=None, mu=0.0, penalty_start=0):
46 """
47 Parameters
48 ----------
49 l : Non-negative float. The Lagrange multiplier, or regularisation
50 constant, of the function.
51
52 c : Float. The limit of the constraint. The function is feasible if
53 TV(beta) <= c. The default value is c=0, i.e. the default is a
54 regularisation formulation.
55
56 A : Numpy array (usually sparse). The linear operator for the Nesterov
57 formulation. May not be None!
58
59 mu : Non-negative float. The regularisation constant for the smoothing.
60
61 penalty_start : Non-negative integer. The number of columns, variables
62 etc., to exempt from penalisation. Equivalently, the first
63 index to be penalised. Default is 0, all columns are included.
64 """
65 super(TotalVariation, self).__init__(l, A=A, mu=mu,
66 penalty_start=penalty_start)
67 self._p = A[0].shape[1]
68
69 self.c = float(c)
70
71 self.reset()
72
74
75 self._lambda_max = None
76
78 """ Function value.
79 """
80 if self.l < consts.TOLERANCE:
81 return 0.0
82
83 if self.penalty_start > 0:
84 beta_ = beta[self.penalty_start:, :]
85 else:
86 beta_ = beta
87
88 A = self.A()
89 abeta2 = A[0].dot(beta_) ** 2.0
90 for k in xrange(1, len(A)):
91 abeta2 += A[k].dot(beta_) ** 2
92
93 return self.l * (np.sum(np.sqrt(abeta2)) - self.c)
94
95
96
97
98
99 - def phi(self, alpha, beta):
100 """Function value with known alpha.
101
102 From the interface "NesterovFunction".
103 """
104 if self.l < consts.TOLERANCE:
105 return 0.0
106
107 if self.penalty_start > 0:
108 beta_ = beta[self.penalty_start:, :]
109 else:
110 beta_ = beta
111
112 Aa = self.Aa(alpha)
113
114 alpha_sqsum = 0.0
115 for a in alpha:
116 alpha_sqsum += np.sum(a ** 2.0)
117
118 mu = self.get_mu()
119
120 return self.l * ((np.dot(beta_.T, Aa)[0, 0]
121 - (mu / 2.0) * alpha_sqsum) - self.c)
122
124 """Feasibility of the constraint.
125
126 From the interface "Constraint".
127 """
128 if self.penalty_start > 0:
129 beta_ = beta[self.penalty_start:, :]
130 else:
131 beta_ = beta
132
133 A = self.A()
134 abeta2 = A[0].dot(beta_) ** 2.0
135 for k in xrange(1, len(A)):
136 abeta2 += A[k].dot(beta_) ** 2
137 val = np.sum(np.sqrt(abeta2))
138
139
140
141 return val <= self.c
142
144 """ Lipschitz constant of the gradient.
145
146 From the interface "LipschitzContinuousGradient".
147 """
148 if self.l < consts.TOLERANCE:
149 return 0.0
150
151 lmaxA = self.lambda_max()
152
153 return self.l * lmaxA / self.mu
154
156 """ Largest eigenvalue of the corresponding covariance matrix.
157
158 From the interface "Eigenvalues".
159 """
160
161
162
163 if len(self._A) == 3 \
164 and self._A[1].nnz == 0 and self._A[2].nnz == 0:
165
166
167 self._lambda_max = 2.0 * (1.0 - math.cos(float(self._p - 1)
168 * math.pi
169 / float(self._p)))
170
171 elif self._lambda_max is None:
172
173 from parsimony.algorithms.nipals import FastSparseSVD
174
175 A = sparse.vstack(self.A())
176
177 v = FastSparseSVD().run(A)
178 us = A.dot(v)
179 self._lambda_max = np.sum(us ** 2.0)
180
181 return self._lambda_max
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
243 """ Projection onto the compact space of the Nesterov function.
244
245 From the interface "NesterovFunction".
246 """
247 anorm = a[0] ** 2.0
248 for k in xrange(1, len(a)):
249 anorm += a[k] ** 2
250 i = anorm > 1.0
251
252 anorm_i = anorm[i] ** 0.5
253 for k in xrange(len(a)):
254 a[k][i] = np.divide(a[k][i], anorm_i)
255
256 return a
257
259 """ The maximum value of the regularisation of the dual variable. We
260 have
261
262 M = max_{alpha in K} 0.5*|alpha|²_2.
263
264 From the interface "NesterovFunction".
265 """
266 return self._A[0].shape[0] / 2.0
267
269 """ Computes a "good" value of mu with respect to the given beta.
270
271 From the interface "NesterovFunction".
272 """
273 if self.penalty_start > 0:
274 beta_ = beta[self.penalty_start:, :]
275 else:
276 beta_ = beta
277
278 A = self.A()
279 normAg = np.zeros((A[0].shape[0], 1))
280 for Ai in A:
281 normAg += Ai.dot(beta_) ** 2.0
282 normAg = np.sqrt(normAg)
283 mu = np.max(normAg)
284
285 return mu
286
292
295 """Generates the linear operator for the total variation Nesterov function
296 from a mask for a 3D image.
297
298 Parameters
299 ----------
300 mask : Numpy array of integers. The mask has the same shape as the original
301 data. Non-null values correspond to columns of X. Groups may be
302 defined using different values in the mask. TV will be applied
303 within groups of the same value in the mask.
304
305 offset: Non-negative integer. The index of the first column, variable,
306 where TV applies. This is different from penalty_start which
307 define where the penalty applies. The offset defines where TV
308 applies within the penalised variables.
309
310 Example: X := [Intercept, Age, Weight, Image]. Intercept is
311 not penalized, TV does not apply on Age and Weight but only on
312 Image. Thus: penalty_start = 1, offset = 2 (skip Age and
313 Weight).
314
315 weights : Numpy array. The weight put on the gradient of every point.
316 Default is weight 1 for each point, or equivalently, no weight. The
317 weights is a numpy array of the same shape as mask.
318 """
319 while len(mask.shape) < 3:
320 mask = mask[..., np.newaxis]
321
322 if weights is not None:
323 while len(weights.shape) < 3:
324 weights = weights[..., np.newaxis]
325
326 nx, ny, nz = mask.shape
327 mask_bool = mask != 0
328 xyz_mask = np.where(mask_bool)
329 Ax_i = list()
330 Ax_j = list()
331 Ax_v = list()
332 Ay_i = list()
333 Ay_j = list()
334 Ay_v = list()
335 Az_i = list()
336 Az_j = list()
337 Az_v = list()
338 n_compacts = 0
339 p = np.sum(mask_bool) + offset
340
341
342 im2flat = np.zeros(mask.shape, dtype=int)
343 im2flat[:] = -1
344 im2flat[mask_bool] = np.arange(np.sum(mask_bool)) + offset
345
346 for pt in xrange(len(xyz_mask[0])):
347
348 found = False
349 x, y, z = xyz_mask[0][pt], xyz_mask[1][pt], xyz_mask[2][pt]
350 i_pt = im2flat[x, y, z]
351 val = mask[x, y, z]
352
353 if weights is not None:
354 w = weights[x, y, z]
355 else:
356 w = 1.0
357
358 if x + 1 < nx and (mask[x + 1, y, z] == val):
359 found = True
360 Ax_i += [i_pt, i_pt]
361 Ax_j += [i_pt, im2flat[x + 1, y, z]]
362 Ax_v += [-w, w]
363 if y + 1 < ny and (mask[x, y + 1, z] == val):
364 found = True
365 Ay_i += [i_pt, i_pt]
366 Ay_j += [i_pt, im2flat[x, y + 1, z]]
367 Ay_v += [-w, w]
368 if z + 1 < nz and (mask[x, y, z + 1] == val):
369 found = True
370 Az_i += [i_pt, i_pt]
371 Az_j += [i_pt, im2flat[x, y, z + 1]]
372 Az_v += [-w, w]
373
374 if found:
375 n_compacts += 1
376
377 Ax = sparse.csr_matrix((Ax_v, (Ax_i, Ax_j)), shape=(p, p))
378 Ay = sparse.csr_matrix((Ay_v, (Ay_i, Ay_j)), shape=(p, p))
379 Az = sparse.csr_matrix((Az_v, (Az_i, Az_j)), shape=(p, p))
380
381 return [Ax, Ay, Az], n_compacts
382
388
391 """Generates the linear operator for the total variation Nesterov function
392 from a mask for a 3D image.
393
394 The binary mask marks a subset of the variables that are supposed to be
395 smoothed. The mask has the same size as the input and output image.
396
397 Parameters
398 ----------
399 mask : Numpy array. The mask. The mask does not involve any intercept
400 variables.
401
402 weights : Numpy array. The weight put on the gradient of every point.
403 Default is weight 1 for each point, or equivalently, no weight. The
404 weights is a numpy array of the same shape as mask.
405 """
406 while len(mask.shape) < 3:
407 mask = mask[np.newaxis, :]
408
409 if weights is not None:
410 while len(weights.shape) < 3:
411 weights = weights[np.newaxis, :]
412
413 nz, ny, nx = mask.shape
414 mask = mask.astype(bool)
415 zyx_mask = np.where(mask)
416 Ax_i = list()
417 Ax_j = list()
418 Ax_v = list()
419 Ay_i = list()
420 Ay_j = list()
421 Ay_v = list()
422 Az_i = list()
423 Az_j = list()
424 Az_v = list()
425 num_compacts = 0
426
427
428
429 def im2flat(sub, dims):
430 return sub[0] * dims[2] * dims[1] + \
431 sub[1] * dims[2] + \
432 sub[2]
433
434
435
436
437
438 for pt in xrange(len(zyx_mask[0])):
439
440 found = False
441 z, y, x = zyx_mask[0][pt], zyx_mask[1][pt], zyx_mask[2][pt]
442 i_pt = im2flat((z, y, x), mask.shape)
443
444 if weights is not None:
445 w = weights[z, y, x]
446 else:
447 w = 1.0
448
449 if z + 1 < nz and mask[z + 1, y, x]:
450 found = True
451 Az_i += [i_pt, i_pt]
452 Az_j += [i_pt, im2flat((z + 1, y, x), mask.shape)]
453 Az_v += [-w, w]
454 if y + 1 < ny and mask[z, y + 1, x]:
455 found = True
456 Ay_i += [i_pt, i_pt]
457 Ay_j += [i_pt, im2flat((z, y + 1, x), mask.shape)]
458 Ay_v += [-w, w]
459 if x + 1 < nx and mask[z, y, x + 1]:
460 found = True
461 Ax_i += [i_pt, i_pt]
462 Ax_j += [i_pt, im2flat((z, y, x + 1), mask.shape)]
463 Ax_v += [-w, w]
464
465 if found:
466 num_compacts += 1
467
468 p = np.prod(mask.shape)
469 Az = sparse.csr_matrix((Az_v, (Az_i, Az_j)), shape=(p, p))
470 Ay = sparse.csr_matrix((Ay_v, (Ay_i, Ay_j)), shape=(p, p))
471 Ax = sparse.csr_matrix((Ax_v, (Ax_i, Ax_j)), shape=(p, p))
472
473 return [Ax, Ay, Az], num_compacts
474
480
483 """Generates the linear operator for the total variation Nesterov function
484 from the shape of a 1D, 2D or 3D image.
485
486 Parameters
487 ----------
488 shape : List or tuple with 1, 2 or 3 integers. The shape of the 1D, 2D or
489 3D image. shape has the form X, (X,), (Y, X) or (Z, Y, X), where Z
490 is the number of "layers", Y is the number of rows and X is the
491 number of columns. The shape does not involve any intercept
492 variables.
493
494 weights : Sequence, e.g. list or numpy (p-by-1) array. Weights put on the
495 groups. Default is weight 1 for each group, i.e. no weight.
496 """
497 if not isinstance(shape, (list, tuple)):
498 shape = [shape]
499 while len(shape) < 3:
500 shape = tuple([1] + list(shape))
501
502 nz = shape[0]
503 ny = shape[1]
504 nx = shape[2]
505 p = nx * ny * nz
506 ind = np.arange(p).reshape((nz, ny, nx))
507
508 if weights is not None:
509 weights = np.array(weights)
510 weights = weights.ravel()
511
512
513 if nx > 1:
514 if weights is not None:
515 Ax = sparse.spdiags(weights, -1, p, p).T - \
516 sparse.spdiags(weights, 0, p, p)
517 Ax = Ax.tocsr()
518 else:
519 Ax = sparse.eye(p, p, 1, format='csr') - \
520 sparse.eye(p, p)
521 zind = ind[:, :, -1].ravel()
522 for i in zind:
523 Ax.data[Ax.indptr[i]: \
524 Ax.indptr[i + 1]] = 0
525 Ax.eliminate_zeros()
526 else:
527 Ax = sparse.csr_matrix((p, p), dtype=float)
528
529 if ny > 1:
530 if weights is not None:
531 Ay = sparse.spdiags(weights, -nx, p, p).T - \
532 sparse.spdiags(weights, 0, p, p)
533 Ay = Ay.tocsr()
534 else:
535 Ay = sparse.eye(p, p, nx, format='csr') - \
536 sparse.eye(p, p)
537
538 yind = ind[:, -1, :].ravel()
539 for i in yind:
540 Ay.data[Ay.indptr[i]: \
541 Ay.indptr[i + 1]] = 0
542 Ay.eliminate_zeros()
543 else:
544 Ay = sparse.csr_matrix((p, p), dtype=float)
545
546 if nz > 1:
547 if weights is not None:
548 Az = sparse.spdiags(weights, -(ny * nx), p, p).T - \
549 sparse.spdiags(weights, 0, p, p)
550 Az = Az.tocsr()
551 else:
552 Az = (sparse.eye(p, p, ny * nx, format='csr') - \
553 sparse.eye(p, p))
554
555 xind = ind[-1, :, :].ravel()
556 for i in xind:
557 Az.data[Az.indptr[i]: \
558 Az.indptr[i + 1]] = 0
559 Az.eliminate_zeros()
560 else:
561 Az = sparse.csr_matrix((p, p), dtype=float)
562
563 return [Ax, Ay, Az], (nz * ny * nx - 1)
564
570
576
580 """Generates the linear operator for the total variation Nesterov function
581 from a mesh.
582
583 Parameters
584 ----------
585 mesh_coord : Numpy array [n, 3] of float.
586
587 mesh_triangles : Numpy array, n_triangles-by-3. The (integer) indices of
588 the three nodes forming the triangle.
589
590 mask : Numpy array (shape (n,)) of integers/boolean. Non-null values
591 correspond to columns of X. Groups may be defined using different
592 values in the mask. TV will be applied within groups of the same
593 value in the mask.
594
595 offset : Non-negative integer. The index of the first column, variable,
596 where TV applies. This is different from penalty_start which
597 define where the penalty applies. The offset defines where TV
598 applies within the penalised variables.
599
600 Example: X := [Intercept, Age, Weight, Image]. Intercept is
601 not penalized, TV does not apply on Age and Weight but only on
602 Image. Thus: penalty_start = 1, offset = 2 (skip Age and
603 Weight).
604
605 weights : Numpy array. The weight put on the gradient of every point.
606 Default is weight 1 for each point, or equivalently, no weight. The
607 weights is a numpy array of the same shape as mask.
608
609 Returns
610 -------
611 out1 : List or sparse matrices. Linear operator for the total variation
612 Nesterov function computed over a mesh.
613
614 out2 : Integer. The number of compacts.
615
616 Examples
617 --------
618 >>> import numpy as np
619 >>> import parsimony.functions.nesterov.tv as tv_helper
620 >>> mesh_coord = np.array([[0, 0], [1, 0], [0, 1], [1, 1], [0, 2], [1, 2]])
621 >>> mesh_triangles = np.array([[0 ,1, 3], [0, 2 ,3], [2, 3, 5], [2, 4, 5]])
622 >>> A, _ = tv_helper.nesterov_linear_operator_from_mesh(mesh_coord,
623 ... mesh_triangles)
624 """
625 if mask is None:
626 mask = np.ones(mesh_coord.shape[0], dtype=bool)
627 assert mask.shape[0] == mesh_coord.shape[0]
628 mask_bool = mask != 0
629 mask_idx = np.where(mask_bool)[0]
630
631 map_full2masked = np.zeros(mask.shape, dtype=int)
632 map_full2masked[:] = -1
633 map_full2masked[mask_bool] = np.arange(np.sum(mask_bool)) + offset
634
635 nodes_with_edges = [[] for i in xrange(mesh_coord.shape[0])]
636
637 def connect_edge_to_node(node_idx1, node_idx2, nodes_with_edges):
638
639 if np.sum(mesh_coord[node_idx1] - mesh_coord[node_idx2]) >= 0:
640 edge = [node_idx1, node_idx2]
641 if not edge in nodes_with_edges[node_idx1]:
642 nodes_with_edges[node_idx1].append(edge)
643 else:
644 edge = [node_idx2, node_idx1]
645 if not edge in nodes_with_edges[node_idx2]:
646 nodes_with_edges[node_idx2].append(edge)
647 for i in xrange(mesh_triangles.shape[0]):
648 t = mesh_triangles[i, :]
649 connect_edge_to_node(t[0], t[1], nodes_with_edges)
650 connect_edge_to_node(t[0], t[2], nodes_with_edges)
651 connect_edge_to_node(t[1], t[2], nodes_with_edges)
652 max_connectivity = np.max(np.array([len(n) for n in nodes_with_edges]))
653
654
655 A = [[[], [], []] for i in xrange(max_connectivity)]
656 n_compacts = 0
657 for node_idx in mask_idx:
658
659 found = False
660 node = nodes_with_edges[node_idx]
661 for i, v in enumerate(node):
662 found = False
663 if weights is not None:
664 w = weights[i]
665 else:
666 w = 1.0
667
668 node1_idx, node2_idx = v
669 if mask_bool[node1_idx] and mask_bool[node2_idx]:
670 found = True
671 A[i][0] += [map_full2masked[node1_idx],
672 map_full2masked[node1_idx]]
673 A[i][1] += [map_full2masked[node1_idx],
674 map_full2masked[node2_idx]]
675 A[i][2] += [-w, w]
676 if found:
677 n_compacts += 1
678 p = mask.sum()
679 A = [sparse.csr_matrix((A[i][2], (A[i][0], A[i][1])),
680 shape=(p, p)) for i in xrange(len(A))]
681
682 return A, n_compacts
683