highres-cortex 6.0.4
laplace_solver.tcc
Go to the documentation of this file.
1/*
2Copyright Télécom ParisTech (2015).
3
4Contributor: Yann Leprince <yann.leprince@ylep.fr>.
5
6This file is part of highres-cortex, a collection of software designed
7to process high-resolution magnetic resonance images of the cerebral
8cortex.
9
10This software is governed by the CeCILL licence under French law and
11abiding by the rules of distribution of free software. You can use,
12modify and/or redistribute the software under the terms of the CeCILL
13licence as circulated by CEA, CNRS and INRIA at the following URL:
14<http://www.cecill.info/>.
15
16As a counterpart to the access to the source code and rights to copy,
17modify and redistribute granted by the licence, users are provided only
18with a limited warranty and the software's author, the holder of the
19economic rights, and the successive licensors have only limited
20liability.
21
22In this respect, the user's attention is drawn to the risks associated
23with loading, using, modifying and/or developing or reproducing the
24software by the user in light of its specific status of scientific
25software, that may mean that it is complicated to manipulate, and that
26also therefore means that it is reserved for developers and experienced
27professionals having in-depth computer knowledge. Users are therefore
28encouraged to load and test the software's suitability as regards their
29requirements in conditions enabling the security of their systems and/or
30data to be ensured and, more generally, to use and operate it in the
31same conditions as regards security.
32
33The fact that you are presently reading this means that you have had
34knowledge of the CeCILL licence and that you accept its terms.
35*/
36
37#include "laplace_solver.hh"
38
39#include <iostream>
40
41#include <boost/format.hpp>
42#include <boost/unordered_set.hpp>
43#include <boost/functional/hash.hpp>
44
45// Fix obscure compilation error, we do not use float128 anyway
46#define BOOST_MATH_DISABLE_FLOAT128
47#include <boost/math/constants/constants.hpp>
48
49#include <aims/utility/converter_volume.h>
50#include <aims/connectivity/structuring_element.h>
51
52#include "cortex.hh"
53#include "front.hh"
54
55namespace {
56
57template <typename T>
58inline T square(T x)
59{
60 return x * x;
61}
62
63/** Check if there is a local extremum at the given coordinates.
64
65 The check always accesses all 6-neighbours of the given point: therefore,
66 all coordinates must be between 1 and (size - 2).
67 */
68template <typename Real>
69bool
70is_local_extremum(const carto::VolumeRef<Real>& solution,
71 const int x, const int y, const int z)
72{
73 unsigned int num_smaller = 0;
74 unsigned int num_larger = 0;
75
76 if(solution(x, y, z) < solution(x - 1, y, z))
77 ++num_smaller;
78 else if(solution(x, y, z) > solution(x - 1, y, z))
79 ++num_larger;
80
81 if(solution(x, y, z) < solution(x + 1, y, z))
82 ++num_smaller;
83 else if(solution(x, y, z) > solution(x + 1, y, z))
84 ++num_larger;
85
86 if(solution(x, y, z) < solution(x, y - 1, z))
87 ++num_smaller;
88 else if(solution(x, y, z) > solution(x, y - 1, z))
89 ++num_larger;
90
91 if(solution(x, y, z) < solution(x, y + 1, z))
92 ++num_smaller;
93 else if(solution(x, y, z) > solution(x, y + 1, z))
94 ++num_larger;
95
96 if(solution(x, y, z) < solution(x, y, z - 1))
97 ++num_smaller;
98 else if(solution(x, y, z) > solution(x, y, z - 1))
99 ++num_larger;
100
101 if(solution(x, y, z) < solution(x, y, z + 1))
102 ++num_smaller;
103 else if(solution(x, y, z) > solution(x, y, z + 1))
104 ++num_larger;
105
106 return (num_smaller == 6 || num_larger == 6);
107}
108
109} // end of anonymous namespace
110
111
112template <typename Real>
113yl::LaplaceSolver<Real>::
114LaplaceSolver(const carto::VolumeRef<int16_t>& classif)
115 : m_classif(classif),
116 m_solution(classif->getSizeX(),
117 classif->getSizeY(),
118 classif->getSizeZ(),
119 1, s_border_width),
120 m_verbosity(0)
121{
122 m_solution->copyHeaderFrom(classif->header());
123}
124
125template <typename Real>
126void
127yl::LaplaceSolver<Real>::
128initialize_solution()
129{
130 carto::RescalerInfo rescaler_info;
131 rescaler_info.vmin = yl::CSF_LABEL; // 0
132 rescaler_info.vmax = yl::WHITE_LABEL; // 200
133 rescaler_info.omin = 0;
134 rescaler_info.omax = 1;
135
136 carto::Converter<carto::VolumeRef<int16_t>, carto::VolumeRef<Real> >
137 converter(true, rescaler_info);
138 converter.convert(m_classif, m_solution);
139}
140
141// This is a Successive Over-Relaxation method using a Gauss-Seidel iteration
142// with the largest time step ensuring stability, and even-odd traversal with
143// Chebyshev acceleration. See W.H. Press, W.H Teukolsky, W.T. Vetterling and
144// B.P. Flannery; Numerical Recipes in C++: The Art of Scientific Computing;
145// (Cambridge University Press, 2002); pages 868--871.
146template <typename Real>
147void
148yl::LaplaceSolver<Real>::
149SOR(const Real absolute_precision,
150 const float typical_cortical_thickness)
151{
152 static const Real pi = boost::math::constants::pi<Real>();
153
154 assert(absolute_precision > 0);
155 assert(typical_cortical_thickness > 0);
156
157 const int size_x = m_classif->getSizeX();
158 const int size_y = m_classif->getSizeY();
159 const int size_z = m_classif->getSizeZ();
160
161 const std::vector<float> voxsize = m_classif->getVoxelSize();
162 const Real voxsize_x = voxsize[0];
163 const Real voxsize_y = voxsize[1];
164 const Real voxsize_z = voxsize[2];
165 const Real average_voxsize = (voxsize_x + voxsize_y + voxsize_z) / 3;
166
167 const Real factx = 0.5f / (1 + square(voxsize_x / voxsize_y) +
168 square(voxsize_x / voxsize_z));
169 const Real facty = 0.5f / (square(voxsize_y / voxsize_x) + 1 +
170 square(voxsize_y / voxsize_z));
171 const Real factz = 0.5f / (square(voxsize_z / voxsize_x) +
172 square(voxsize_z / voxsize_y) + 1);
173
174 // Using the average of the voxel size in the estimation of cortical
175 // thickness in voxels gave the best result in some tests with 3:1
176 // anisotropic voxels.
177 const Real cortical_voxels = typical_cortical_thickness / average_voxsize;
178 const Real estimated_jacobi_spectral_radius =
179 (std::cos(pi / cortical_voxels) + 2) / 3;
180 const Real omega_update_factor =
181 0.25f * square(estimated_jacobi_spectral_radius);
182
183 if(m_verbosity) {
184 std::clog << boost::format(
185 "solving Laplace equation using Successive Over-Relaxation\n"
186 "(requested absolute precision %1$.1e)...") % absolute_precision
187 << std::endl;
188 }
189
190 Real max_residual;
191 Real omega = 1;
192 unsigned int iter = 1;
193 unsigned int max_iter = 0; // initially no limit on iteration count
194 do {
195 max_residual = 0;
196 for(int even_odd = 0; even_odd <= 1; ++even_odd) {
197 #if _OPENMP >= 201107
198 // OpenMP 3.1 or later (201107) is needed for the reduction(max) clause
199 #pragma omp parallel for schedule(dynamic) reduction(max:max_residual)
200 #endif
201 for(int z = 0 ; z < size_z ; ++z)
202 for(int y = 0 ; y < size_y ; ++y)
203 for(int x = (y + z + even_odd) % 2 ; x < size_x ; x += 2) {
204 if(m_classif(x, y, z) == yl::CORTEX_LABEL) {
205 const Real old_value = m_solution(x, y, z);
206 const Real gauss_seidel_new_value =
207 factx * (m_solution(x - 1, y, z) + m_solution(x + 1, y, z)) +
208 facty * (m_solution(x, y - 1, z) + m_solution(x, y + 1, z)) +
209 factz * (m_solution(x, y, z - 1) + m_solution(x, y, z + 1));
210 m_solution(x, y, z) = omega * gauss_seidel_new_value
211 + (1 - omega) * old_value;
212 const Real abs_residual = std::abs(gauss_seidel_new_value
213 - old_value);
214 if(abs_residual > max_residual) {
215 max_residual = abs_residual;
216 }
217 }
218 }
219
220 // Chebyshev acceleration
221 if(iter == 1 && even_odd == 0)
222 omega = 1 / (1 - 0.5f * square(estimated_jacobi_spectral_radius));
223 else
224 omega = 1 / (1 - omega * omega_update_factor);
225 assert(omega > 0);
226 assert(omega < 2);
227 }
228
229 // The residual cannot be guaranteed to decrease below this threshold
230 // because of numerical errors. The value 11 * epsilon comes from a crude
231 // analysis of the propagation of numerical errors (potential infinite loop
232 // BUG if this has been underestimated...)
233 //
234 // When we detect this, we can predict the number of iterations necessary
235 // to reach machine precision, assuming geometric convergence rate.
236 //
237 // Possible refinement: set omega = 1 for a few last steps (how many?):
238 // that should decrease numerical errors by up to a factor omega.
239 static const Real numerical_error_threshold
240 = 11 * std::numeric_limits<Real>::epsilon();
241 if(max_residual <= numerical_error_threshold && max_iter == 0) {
242 static const Real epsilon = std::numeric_limits<Real>::epsilon();
243 static const Real initial_error = 0.5;
244
245 max_iter = static_cast<unsigned int>(
246 iter * (std::log(epsilon / initial_error)
247 / std::log(numerical_error_threshold / initial_error)) + 1);
248 // Prevent infinite looping if an error in the above calculation leads to
249 // max_iter <= iter
250 max_iter = std::max(max_iter, iter + 1);
251 if(m_verbosity) {
252 std::clog << boost::format("\nmachine precision (%1$.1e) should"
253 " be attained by iteration %2$u")
254 % epsilon % max_iter << std::endl;
255 }
256 }
257
258 if(m_verbosity) {
259 std::clog << boost::format("\riteration %1$u, max residual = %2$.1e")
260 % iter % max_residual << std::flush;
261 }
262
263 ++iter;
264 } while(max_residual > absolute_precision && iter != max_iter);
265
266 if(m_verbosity)
267 std::clog << std::endl;
268}
269
270template <typename Real>
271void
272yl::LaplaceSolver<Real>::
273eliminate_extrema()
274{
275 const int size_x = m_classif->getSizeX();
276 const int size_y = m_classif->getSizeY();
277 const int size_z = m_classif->getSizeZ();
278
279 const std::vector<float> voxsize = m_classif->getVoxelSize();
280 const Real voxsize_x = voxsize[0];
281 const Real voxsize_y = voxsize[1];
282 const Real voxsize_z = voxsize[2];
283
284 const Real factx = 0.5f / (1 + square(voxsize_x / voxsize_y) +
285 square(voxsize_x / voxsize_z));
286 const Real facty = 0.5f / (square(voxsize_y / voxsize_x) + 1 +
287 square(voxsize_y / voxsize_z));
288 const Real factz = 0.5f / (square(voxsize_z / voxsize_x) +
289 square(voxsize_z / voxsize_y) + 1);
290
291 if(m_verbosity) {
292 std::clog << "searching for remaining local extrema... "
293 << std::flush;
294 }
295
296 typedef boost::unordered_set<Point3d, yl::PointHasher<Point3d> >
297 PointSetType;
298 PointSetType extremum_points;
299
300 #pragma omp parallel for schedule(dynamic)
301 for(int z = 1 ; z < size_z - 1 ; ++z)
302 for(int y = 1 ; y < size_y - 1 ; ++y)
303 for(int x = 1 ; x < size_x - 1 ; ++x) {
304 if(m_classif(x, y, z) == yl::CORTEX_LABEL) {
305 if(is_local_extremum(m_solution, x, y, z)) {
306 #pragma omp critical(extremum_point_set)
307 extremum_points.insert(Point3d(x, y, z));
308 }
309 }
310 }
311
312 if(m_verbosity)
313 std::clog << extremum_points.size() << " found." << std::endl;
314
315 if(extremum_points.empty())
316 return;
317
318 std::auto_ptr<aims::strel::Connectivity> connectivity(
319 aims::strel::ConnectivityFactory::create("6"));
320
321 while(!extremum_points.empty()) {
322 PointSetType::const_iterator point_it = extremum_points.begin();
323 const Point3d point = *point_it;
324 extremum_points.erase(point_it);
325
326 const int x = point[0];
327 const int y = point[1];
328 const int z = point[2];
329
330 const Real new_value =
331 factx * (m_solution(x - 1, y, z) + m_solution(x + 1, y, z)) +
332 facty * (m_solution(x, y - 1, z) + m_solution(x, y + 1, z)) +
333 factz * (m_solution(x, y, z - 1) + m_solution(x, y, z + 1));
334 m_solution(x, y, z) = new_value;
335
336 if(m_verbosity && is_local_extremum(m_solution, x, y, z)) {
337 std::clog << "\nwarning: local extremum at " << point
338 << " could not be eliminated" << std::endl;
339 }
340
341 for(aims::strel::Connectivity::const_iterator
342 neighbour_it = connectivity->begin() ;
343 neighbour_it != connectivity->end() ;
344 ++neighbour_it) {
345 const Point3d& neighbour_offset = Point3d((*neighbour_it)[0],
346 (*neighbour_it)[1],
347 (*neighbour_it)[2]);
348 const Point3d neighbour_point = point + neighbour_offset;
349 const int nx = neighbour_point[0];
350 const int ny = neighbour_point[1];
351 const int nz = neighbour_point[2];
352
353 if(!(nx >= 1 && nx < size_x - 1
354 && ny >= 1 && ny < size_y - 1
355 && nz >= 1 && nz < size_z - 1)) {
356 // We do not check for local extrema on the edge of the volume.
357 continue;
358 }
359
360 if(m_classif(nx, ny, nz) == yl::CORTEX_LABEL
361 && is_local_extremum(m_solution, nx, ny, nz)) {
362 extremum_points.insert(neighbour_point);
363 }
364 }
365
366 if(m_verbosity) {
367 std::clog << "\reliminating local extrema... "
368 << extremum_points.size() << " remaining. "
369 << std::flush;
370 }
371 }
372
373 if(m_verbosity)
374 std::clog << std::endl;
375}
376
377template <typename Real>
378void
379yl::LaplaceSolver<Real>::
380clamp_to_range(const Real min, const Real max)
381{
382 assert(max >= min);
383
384 const int size_x = m_solution->getSizeX();
385 const int size_y = m_solution->getSizeY();
386 const int size_z = m_solution->getSizeZ();
387
388 #pragma omp parallel for
389 for(int z = 0 ; z < size_z ; ++z)
390 for(int y = 0 ; y < size_y ; ++y)
391 for(int x = 0 ; x < size_x ; ++x) {
392 if(m_solution(x, y, z) < min)
393 m_solution(x, y, z) = min;
394 else if(m_solution(x, y, z) > max)
395 m_solution(x, y, z) = max;
396 }
397}
398
399template <typename Real>
400carto::VolumeRef<Real>
401yl::LaplaceSolver<Real>::
402solution() const
403{
404 return m_solution;
405}