2Copyright Télécom ParisTech (2015).
4Contributor: Yann Leprince <yann.leprince@ylep.fr>.
6This file is part of highres-cortex, a collection of software designed
7to process high-resolution magnetic resonance images of the cerebral
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/>.
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
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.
33The fact that you are presently reading this means that you have had
34knowledge of the CeCILL licence and that you accept its terms.
37#include "laplace_solver.hh"
41#include <boost/format.hpp>
42#include <boost/unordered_set.hpp>
43#include <boost/functional/hash.hpp>
45// Fix obscure compilation error, we do not use float128 anyway
46#define BOOST_MATH_DISABLE_FLOAT128
47#include <boost/math/constants/constants.hpp>
49#include <aims/utility/converter_volume.h>
50#include <aims/connectivity/structuring_element.h>
63/** Check if there is a local extremum at the given coordinates.
65 The check always accesses all 6-neighbours of the given point: therefore,
66 all coordinates must be between 1 and (size - 2).
68template <typename Real>
70is_local_extremum(const carto::VolumeRef<Real>& solution,
71 const int x, const int y, const int z)
73 unsigned int num_smaller = 0;
74 unsigned int num_larger = 0;
76 if(solution(x, y, z) < solution(x - 1, y, z))
78 else if(solution(x, y, z) > solution(x - 1, y, z))
81 if(solution(x, y, z) < solution(x + 1, y, z))
83 else if(solution(x, y, z) > solution(x + 1, y, z))
86 if(solution(x, y, z) < solution(x, y - 1, z))
88 else if(solution(x, y, z) > solution(x, y - 1, z))
91 if(solution(x, y, z) < solution(x, y + 1, z))
93 else if(solution(x, y, z) > solution(x, y + 1, z))
96 if(solution(x, y, z) < solution(x, y, z - 1))
98 else if(solution(x, y, z) > solution(x, y, z - 1))
101 if(solution(x, y, z) < solution(x, y, z + 1))
103 else if(solution(x, y, z) > solution(x, y, z + 1))
106 return (num_smaller == 6 || num_larger == 6);
109} // end of anonymous namespace
112template <typename Real>
113yl::LaplaceSolver<Real>::
114LaplaceSolver(const carto::VolumeRef<int16_t>& classif)
115 : m_classif(classif),
116 m_solution(classif->getSizeX(),
122 m_solution->copyHeaderFrom(classif->header());
125template <typename Real>
127yl::LaplaceSolver<Real>::
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;
136 carto::Converter<carto::VolumeRef<int16_t>, carto::VolumeRef<Real> >
137 converter(true, rescaler_info);
138 converter.convert(m_classif, m_solution);
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>
148yl::LaplaceSolver<Real>::
149SOR(const Real absolute_precision,
150 const float typical_cortical_thickness)
152 static const Real pi = boost::math::constants::pi<Real>();
154 assert(absolute_precision > 0);
155 assert(typical_cortical_thickness > 0);
157 const int size_x = m_classif->getSizeX();
158 const int size_y = m_classif->getSizeY();
159 const int size_z = m_classif->getSizeZ();
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;
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);
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);
184 std::clog << boost::format(
185 "solving Laplace equation using Successive Over-Relaxation\n"
186 "(requested absolute precision %1$.1e)...") % absolute_precision
192 unsigned int iter = 1;
193 unsigned int max_iter = 0; // initially no limit on iteration count
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)
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
214 if(abs_residual > max_residual) {
215 max_residual = abs_residual;
220 // Chebyshev acceleration
221 if(iter == 1 && even_odd == 0)
222 omega = 1 / (1 - 0.5f * square(estimated_jacobi_spectral_radius));
224 omega = 1 / (1 - omega * omega_update_factor);
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...)
234 // When we detect this, we can predict the number of iterations necessary
235 // to reach machine precision, assuming geometric convergence rate.
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;
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
250 max_iter = std::max(max_iter, iter + 1);
252 std::clog << boost::format("\nmachine precision (%1$.1e) should"
253 " be attained by iteration %2$u")
254 % epsilon % max_iter << std::endl;
259 std::clog << boost::format("\riteration %1$u, max residual = %2$.1e")
260 % iter % max_residual << std::flush;
264 } while(max_residual > absolute_precision && iter != max_iter);
267 std::clog << std::endl;
270template <typename Real>
272yl::LaplaceSolver<Real>::
275 const int size_x = m_classif->getSizeX();
276 const int size_y = m_classif->getSizeY();
277 const int size_z = m_classif->getSizeZ();
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];
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);
292 std::clog << "searching for remaining local extrema... "
296 typedef boost::unordered_set<Point3d, yl::PointHasher<Point3d> >
298 PointSetType extremum_points;
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));
313 std::clog << extremum_points.size() << " found." << std::endl;
315 if(extremum_points.empty())
318 std::auto_ptr<aims::strel::Connectivity> connectivity(
319 aims::strel::ConnectivityFactory::create("6"));
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);
326 const int x = point[0];
327 const int y = point[1];
328 const int z = point[2];
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;
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;
341 for(aims::strel::Connectivity::const_iterator
342 neighbour_it = connectivity->begin() ;
343 neighbour_it != connectivity->end() ;
345 const Point3d& neighbour_offset = Point3d((*neighbour_it)[0],
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];
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.
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);
367 std::clog << "\reliminating local extrema... "
368 << extremum_points.size() << " remaining. "
374 std::clog << std::endl;
377template <typename Real>
379yl::LaplaceSolver<Real>::
380clamp_to_range(const Real min, const Real max)
384 const int size_x = m_solution->getSizeX();
385 const int size_y = m_solution->getSizeY();
386 const int size_z = m_solution->getSizeZ();
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;
399template <typename Real>
400carto::VolumeRef<Real>
401yl::LaplaceSolver<Real>::