v2.0.0
Loading...
Searching...
No Matches
mne_msh_display_surface.cpp
Go to the documentation of this file.
1//=============================================================================================================
16
17//=============================================================================================================
18// INCLUDES
19//=============================================================================================================
20
22#include "mne_surface.h"
23#include "mne_morph_map.h"
24#include "mne_msh_picked.h"
26
27#include <fiff/fiff_stream.h>
30#include <fiff/fiff_dig_point.h>
31
32#include <math/sphere.h>
33
34#include <Eigen/Geometry>
35
36// MSVC builds already define _USE_MATH_DEFINES globally (see src/CMakeLists.txt),
37// so define it here only for the toolchains that do not.
38#ifndef _USE_MATH_DEFINES
39#define _USE_MATH_DEFINES
40#endif
41#include <math.h>
42#include <QDebug>
43
44//=============================================================================================================
45// USED NAMESPACES
46//=============================================================================================================
47
48using namespace Eigen;
49using namespace FIFFLIB;
50using namespace MNELIB;
51
52//============================= local macros =================================
53
54constexpr int FAIL = -1;
55constexpr int OK = 0;
56
57// Axis indices for coordinate access (kept for documentation).
58[[maybe_unused]] constexpr int X = 0;
59[[maybe_unused]] constexpr int Y = 1;
60[[maybe_unused]] constexpr int Z = 2;
61
62constexpr int SHOW_CURVATURE_NONE = 0;
63constexpr int SHOW_CURVATURE_OVERLAY = 1;
64constexpr int SHOW_OVERLAY_HEAT = 1;
65
66constexpr float POS_CURV_COLOR = 0.25f;
67constexpr float NEG_CURV_COLOR = 0.375f;
68constexpr float EVEN_CURV_COLOR = 0.375f;
69
70//=============================================================================================================
71// DEFINE MEMBER METHODS
72//=============================================================================================================
73
75
76//=============================================================================================================
77
83
84//=============================================================================================================
85// Alignment functions (moved from MNESurfaceOrVolume)
86//=============================================================================================================
87
89 const FiffDigitizerData& mri_dig,
90 int niter,
91 int scale_head,
92 float omit_dist,
93 Eigen::Vector3f& scales)
94
95{
96 using FidMatrix = Eigen::Matrix<float, 3, 3, Eigen::RowMajor>;
97 FidMatrix head_fid, mri_fid;
98 bool head_fid_found[3] = {false, false, false};
99 bool mri_fid_found[3] = {false, false, false};
100 int j, k;
101 FiffDigPoint p;
102 float nasion_weight = 5.0;
103
104 for (j = 0; j < 2; j++) {
105 const FiffDigitizerData& d = (j == 0) ? head_dig : mri_dig;
106 FidMatrix& fid = (j == 0) ? head_fid : mri_fid;
107 bool* found = (j == 0) ? head_fid_found : mri_fid_found;
108
109 for (k = 0; k < d.npoint; k++) {
110 p = d.points[k];
111 if (p.kind == FIFFV_POINT_CARDINAL) {
112 if (p.ident == FIFFV_POINT_LPA) {
113 fid.row(0) = Eigen::Map<const Eigen::RowVector3f>(d.points[k].r);
114 found[0] = true;
115 } else if (p.ident == FIFFV_POINT_NASION) {
116 fid.row(1) = Eigen::Map<const Eigen::RowVector3f>(d.points[k].r);
117 found[1] = true;
118 } else if (p.ident == FIFFV_POINT_RPA) {
119 fid.row(2) = Eigen::Map<const Eigen::RowVector3f>(d.points[k].r);
120 found[2] = true;
121 }
122 }
123 }
124 }
125
126 for (k = 0; k < 3; k++) {
127 if (!head_fid_found[k]) {
128 qCritical("Some of the MEG fiducials were missing");
129 return FAIL;
130 }
131
132 if (!mri_fid_found[k]) {
133 qCritical("Some of the MRI fiducials were missing");
134 return FAIL;
135 }
136 }
137
138 if (scale_head) {
139 get_head_scale(head_dig, mri_fid, scales);
140 qInfo("xscale = %.3f yscale = %.3f zscale = %.3f\n", scales[0], scales[1], scales[2]);
141
142 for (j = 0; j < 3; j++)
143 for (k = 0; k < 3; k++)
144 mri_fid(j, k) = mri_fid(j, k) * scales[k];
145
146 scale(scales);
147 }
148
149 // Initial alignment
151 mri_fid.row(0).data(), mri_fid.row(1).data(), mri_fid.row(2).data()));
152
153 // Populate mri_fids from cardinal digitizer points transformed into MRI coords
154 head_dig.pickCardinalFiducials();
155
156 // Overwrite the fiducial locations with the ones from the MRI digitizer data
157 for (k = 0; k < head_dig.nfids(); k++)
158 Eigen::Map<Eigen::Vector3f>(head_dig.mri_fids[k].r) = mri_fid.row(k).transpose();
159 head_dig.head_mri_t_adj->print();
160 qInfo("After simple alignment : \n");
161
162 if (omit_dist > 0)
163 discard_outlier_digitizer_points(head_dig, omit_dist);
164
165 // Optional iterative refinement
166 if (niter > 0) {
167 for (k = 0; k < niter; k++) {
168 if (iterate_alignment_once(head_dig, nasion_weight, Eigen::Vector3f(mri_fid.row(1).transpose()), k == niter - 1 && niter > 1) == FAIL)
169 return FAIL;
170 }
171
172 qInfo("%d / %d iterations done. RMS dist = %7.1f mm\n", k, niter,
173 1000.0 * rms_digitizer_distance(head_dig));
174 qInfo("After refinement :\n");
175 head_dig.head_mri_t_adj->print();
176 }
177
178 return OK;
179}
180
181//=============================================================================================================
182
183// Simple head size fit
185 const Eigen::Matrix<float, 3, 3, Eigen::RowMajor>& mri_fid,
186 Eigen::Vector3f& scales)
187{
188 int k, ndig, nhead;
189 float simplex_size = 2e-2f;
190 Eigen::VectorXf r0(3);
191 float Rdig, Rscalp;
192
193 scales[0] = scales[1] = scales[2] = 1.0;
194
195 Eigen::MatrixXf dig_rr(dig.npoint, 3);
196 Eigen::MatrixXf head_rr(np, 3);
197
198 // Pick only the points with positive z
199 for (k = 0, ndig = 0; k < dig.npoint; k++) {
200 if (dig.points[k].r[2] > 0) {
201 dig_rr.row(ndig++) = Eigen::Map<const Eigen::RowVector3f>(dig.points[k].r);
202 }
203 }
204
205 if (!UTILSLIB::Sphere::fit_sphere_to_points(dig_rr.topRows(ndig), simplex_size, r0, Rdig)) {
206 return;
207 }
208
209 qInfo("Polhemus : (%.1f %.1f %.1f) mm R = %.1f mm\n", 1000 * r0[0], 1000 * r0[1], 1000 * r0[2], 1000 * Rdig);
210
211 // Pick only the points above the fiducial plane
212 Eigen::Vector3f LR = mri_fid.row(2).transpose() - mri_fid.row(0).transpose();
213 Eigen::Vector3f LN = mri_fid.row(1).transpose() - mri_fid.row(0).transpose();
214 Eigen::Vector3f norm = LR.cross(LN);
215 norm.normalize();
216
217 for (k = 0, nhead = 0; k < np; k++) {
218 Eigen::Vector3f diff_vec = rr.row(k).transpose() - mri_fid.row(0).transpose();
219 if (diff_vec.dot(norm) > 0) {
220 head_rr.row(nhead++) = rr.row(k);
221 }
222 }
223
224 if (!UTILSLIB::Sphere::fit_sphere_to_points(head_rr.topRows(nhead), simplex_size, r0, Rscalp)) {
225 return;
226 }
227
228 qInfo("Scalp : (%.1f %.1f %.1f) mm R = %.1f mm\n", 1000 * r0[0], 1000 * r0[1], 1000 * r0[2], 1000 * Rscalp);
229
230 scales[0] = scales[1] = scales[2] = Rdig / Rscalp;
231}
232
233//=============================================================================================================
234
236 float maxdist) const
237/*
238 * Discard outlier digitizer points
239 */
240{
241 int discarded = 0;
242 int k;
243
244 d.dist_valid = false;
245 calculate_digitizer_distances(d, true, true);
246 for (k = 0; k < d.npoint; k++) {
247 d.discard[k] = 0;
248 /*
249 * Discard unless cardinal landmark or HPI coil
250 */
251 if (std::fabs(d.dist(k)) > maxdist &&
252 d.points[k].kind != FIFFV_POINT_CARDINAL &&
253 d.points[k].kind != FIFFV_POINT_HPI) {
254 discarded++;
255 d.discard[k] = 1;
256 }
257 }
258 qInfo("%d points discarded (maxdist = %6.1f mm).\n", discarded, 1000 * maxdist);
259
260 return discarded;
261}
262
263//=============================================================================================================
264
266 bool do_all, bool do_approx) const
267/*
268 * Calculate the distances from the scalp surface
269 */
270{
271 int k, nactive;
273 Q_ASSERT(dig.head_mri_t);
274 const FiffCoordTrans& t = (dig.head_mri_t_adj && !dig.head_mri_t_adj->isEmpty()) ? *dig.head_mri_t_adj : *dig.head_mri_t;
275 int nstep = 4;
276
277 if (dig.dist_valid)
278 return;
279
280 PointsT digPoints(dig.npoint, 3);
281
282 dig.dist.conservativeResize(dig.npoint);
283 if (dig.closest.size() == 0) {
284 /*
285 * Ensure that all closest values are initialized correctly
286 */
287 dig.closest = Eigen::VectorXi::Constant(dig.npoint, -1);
288 }
289
290 dig.closest_point.setZero(dig.npoint, 3);
291 Eigen::VectorXi closest(dig.npoint);
292 Eigen::VectorXf dists(dig.npoint);
293
294 for (k = 0, nactive = 0; k < dig.npoint; k++) {
295 if ((dig.active[k] && !dig.discard[k]) || do_all) {
296 point = dig.points.at(k);
297 digPoints.row(nactive) = Eigen::Map<const Eigen::RowVector3f>(point.r);
298 FiffCoordTrans::apply_trans(digPoints.row(nactive).data(), t, FIFFV_MOVE);
299 if (do_approx) {
300 closest[nactive] = dig.closest(k);
301 if (closest[nactive] < 0)
302 do_approx = false;
303 } else
304 closest[nactive] = -1;
305 nactive++;
306 }
307 }
308
309 find_closest_on_surface_approx(digPoints, nactive, closest, dists, nstep);
310 /*
311 * Project the points on the triangles
312 */
313 if (!do_approx)
314 qInfo("Inside or outside for %d points...", nactive);
315 for (k = 0, nactive = 0; k < dig.npoint; k++) {
316 if ((dig.active[k] && !dig.discard[k]) || do_all) {
317 dig.dist(k) = dists[nactive];
318 dig.closest(k) = closest[nactive];
319 {
320 Eigen::Vector3f pt = Eigen::Map<const Eigen::Vector3f>(digPoints.row(nactive).data());
321 Eigen::Vector3f proj = project_to_triangle(dig.closest(k), pt);
322 dig.closest_point.row(k) = proj.transpose();
323 }
324 // The distance above is with respect to the closest triangle only,
325 // so its sign says which side of that one triangle the point is on
326 // rather than whether the point is inside the surface. The solid
327 // angle criterion below answers the latter, and is the check MNE-C
328 // describes here.
329 //
330 // It is off on purpose, and not because it is wrong or unfinished:
331 //
332 // - The criterion is correct. sum_solids adds 2*atan2 per
333 // triangle, so a closed surface gives 4*pi for a point inside
334 // and 0 for one outside, which makes the 0.9 threshold exact.
335 // Verified on a closed cube and on a 9 cm sphere, where it
336 // still separates cleanly 2 mm either side of the surface.
337 // mne-python computes the same quantity for its inside/outside
338 // test, only normalised to 2*pi because its per triangle term
339 // is -arctan2 rather than 2*atan2.
340 //
341 // - Nothing reads the sign. Both consumers of dig.dist discard
342 // it: discard_outlier_digitizer_points compares
343 // std::fabs(dist) against maxdist, and rms_digitizer_distance
344 // squares it.
345 //
346 // Enabling it therefore costs a full pass over every triangle for
347 // each digitizer point, thousands of atan2 calls per point, and
348 // changes no observable behaviour. Flip the constant if a caller
349 // ever needs a signed distance.
350 constexpr bool bUseSolidAngleSign = false;
351 if (bUseSolidAngleSign && !do_approx) {
352 Eigen::Vector3f pt = Eigen::Map<const Eigen::Vector3f>(digPoints.row(nactive).data());
353 if (sum_solids(pt) / (4 * M_PI) > 0.9)
354 dig.dist(k) = -std::fabs(dig.dist(k));
355 else
356 dig.dist(k) = std::fabs(dig.dist(k));
357 }
358 nactive++;
359 }
360 }
361
362 if (!do_approx)
363 qInfo("[done]\n");
364
365 dig.dist_valid = true;
366
367 return;
368}
369
370//=============================================================================================================
371
373 int nasion_weight, /* Weight for the nasion */
374 const std::optional<Eigen::Vector3f>& nasion_mri, /* Fixed correspondence point for the nasion (optional) */
375 int last_step) const /* Is this the last iteration step */
376/*
377 * Find the best alignment of the coordinate frames
378 */
379{
380 Eigen::MatrixXf rr_head(dig.npoint, 3);
381 Eigen::MatrixXf rr_mri(dig.npoint, 3);
382 Eigen::VectorXf w = Eigen::VectorXf::Zero(dig.npoint);
383 int k, nactive;
386 float max_diff = 40e-3f;
387
388 if (!dig.head_mri_t_adj) {
389 qCritical() << "Not adjusting the transformation";
390 return FAIL;
391 }
392 /*
393 * Calculate initial distances
394 */
395 calculate_digitizer_distances(dig, false, true);
396
397 for (k = 0, nactive = 0; k < dig.npoint; k++) {
398 if (dig.active[k] && !dig.discard[k]) {
399 point = dig.points.at(k);
400 rr_head.row(nactive) = Eigen::Map<const Eigen::RowVector3f>(point.r);
401 rr_mri.row(nactive) = dig.closest_point.row(k);
402 /*
403 * Special handling for the nasion
404 */
405 if (point.kind == FIFFV_POINT_CARDINAL &&
406 point.ident == FIFFV_POINT_NASION) {
407 w[nactive] = nasion_weight;
408 if (nasion_mri) {
409 rr_mri.row(nactive) = nasion_mri->transpose();
410 // rr_head is column-major, so its rows are not contiguous; transform a copy.
411 Eigen::Vector3f nasion_head = *nasion_mri;
412 Q_ASSERT(dig.head_mri_t || dig.head_mri_t_adj);
413 FiffCoordTrans::apply_inverse_trans(nasion_head.data(),
414 dig.head_mri_t_adj ? *dig.head_mri_t_adj : *dig.head_mri_t,
415 FIFFV_MOVE);
416 rr_head.row(nactive) = nasion_head.transpose();
417 }
418 } else
419 w[nactive] = 1.0;
420 nactive++;
421 }
422 }
423 if (nactive < 3) {
424 qCritical() << "Not enough points to do the alignment";
425 return FAIL;
426 }
428 rr_head.topRows(nactive),
429 rr_mri.topRows(nactive),
430 w.head(nactive),
431 max_diff))
432 .isEmpty())
433 return FAIL;
434
435 if (dig.head_mri_t_adj)
436 dig.head_mri_t_adj = std::make_unique<FiffCoordTrans>(t);
437 /*
438 * Calculate final distances
439 */
440 dig.dist_valid = false;
441 calculate_digitizer_distances(dig, false, !last_step);
442 return OK;
443}
444
445//=============================================================================================================
446
448{
449 float rms;
450 int k, nactive;
451
452 calculate_digitizer_distances(dig, false, true);
453
454 for (k = 0, rms = 0.0, nactive = 0; k < dig.npoint; k++)
455 if (dig.active[k] && !dig.discard[k]) {
456 rms = rms + dig.dist(k) * dig.dist(k);
457 nactive++;
458 }
459 if (nactive > 1)
460 rms = rms / (nactive - 1);
461 return sqrt(rms);
462}
463
464//=============================================================================================================
465
466void MNEMshDisplaySurface::scale(const Eigen::Vector3f& scales)
467/*
468 * Not quite complete yet
469 */
470{
471 int j, k;
472
473 for (k = 0; k < 3; k++) {
474 minv[k] = scales[k] * minv[k];
475 maxv[k] = scales[k] * maxv[k];
476 }
477 for (j = 0; j < np; j++)
478 for (k = 0; k < 3; k++)
479 rr(j, k) = rr(j, k) * scales[k];
480 return;
481}
482
483//=============================================================================================================
484
485void MNEMshDisplaySurface::decide_surface_extent([[maybe_unused]] const QString& tag)
486{
487 Eigen::Vector3f mn = rr.row(0).transpose();
488 Eigen::Vector3f mx = mn;
489
490 for (int k = 1; k < np; k++) {
491 Eigen::Vector3f r = rr.row(k).transpose();
492 mn = mn.cwiseMin(r);
493 mx = mx.cwiseMax(r);
494 }
495
496#ifdef DEBUG
497 qInfo("%s:\n", tag.toUtf8().constData());
498 qInfo("\tx = %f ... %f mm\n", 1000 * mn[0], 1000 * mx[0]);
499 qInfo("\ty = %f ... %f mm\n", 1000 * mn[1], 1000 * mx[1]);
500 qInfo("\tz = %f ... %f mm\n", 1000 * mn[2], 1000 * mx[2]);
501#endif
502
503 fov = std::max(mn.cwiseAbs().maxCoeff(), mx.cwiseAbs().maxCoeff());
504
505 minv = mn;
506 maxv = mx;
507 fov_scale = 1.1f;
508}
509
510//=============================================================================================================
511
513{
514 if (name.startsWith("inflated") || name.startsWith("sphere") || name.startsWith("white"))
516 else
519}
520
521//=============================================================================================================
522
524{
525 if (np == 0)
526 return;
527
528 const int ncolor = nvertex_colors;
529 const int totalSize = ncolor * np;
530
531 if (vertex_colors.size() == 0)
532 vertex_colors.resize(totalSize);
533
534 [[maybe_unused]] float curv_sum = 0.0f;
536 for (int k = 0; k < np; k++) {
537 const int base = k * ncolor;
538 curv_sum += std::fabs(curv[k]);
539 const float c = (curv[k] > 0) ? POS_CURV_COLOR : NEG_CURV_COLOR;
540 for (int j = 0; j < 3; j++)
541 vertex_colors[base + j] = c;
542 if (ncolor == 4)
543 vertex_colors[base + 3] = 1.0f;
544 }
545 } else {
546 for (int k = 0; k < np; k++) {
547 const int base = k * ncolor;
548 curv_sum += std::fabs(curv[k]);
549 for (int j = 0; j < 3; j++)
550 vertex_colors[base + j] = EVEN_CURV_COLOR;
551 if (ncolor == 4)
552 vertex_colors[base + 3] = 1.0f;
553 }
554 }
555#ifdef DEBUG
556 qInfo("Average curvature : %f\n", curv_sum / np);
557#endif
558}
#define FIFFV_POINT_CARDINAL
#define FIFFV_POINT_RPA
#define FIFFV_COORD_HEAD
#define FIFFV_COORD_MRI
#define FIFFV_MOVE
#define FIFFV_POINT_LPA
#define FIFFV_POINT_HPI
#define FIFFV_POINT_NASION
FIFF binary tag-stream layer: wraps a QIODevice to read and write FIFF tags, directories,...
4x4 affine FIFF coordinate transform (FIFF_COORD_TRANS) annotated with source/destination coordinate-...
Single digitization point (FIFF_DIG_POINT) with kind (cardinal/HPI/EEG/extra), identifier and 3D coor...
High-level digitization data: dig points plus the device→head transform and fitting metadata that tog...
#define M_PI
constexpr int FAIL
constexpr int Y
constexpr int Z
constexpr int OK
constexpr int X
Best-fit sphere from a 3-D point cloud with closed-form and Nelder–Mead solvers.
constexpr float NEG_CURV_COLOR
constexpr int SHOW_CURVATURE_OVERLAY
constexpr float POS_CURV_COLOR
constexpr int SHOW_CURVATURE_NONE
constexpr int SHOW_OVERLAY_HEAT
constexpr float EVEN_CURV_COLOR
One renderable surface (cortex / pial / inflated / BEM) inside an MSH display set.
Lightweight triangulated surface (vertices, triangles, normals) used by surface-based routines.
Sphere-registration morph map of one hemisphere: the sparse matrix taking vertex values from one subj...
Picked vertices / triangles on a mesh display surface.
Mesh-display colour scale definition (FreeSurfer mri_glm style).
Core MNE data structures (source spaces, source estimates, hemispheres).
FIFF file I/O, in-memory data structures and high-level readers/writers.
Labelled 4x4 FIFF affine: source frame, destination frame, rotation, translation and cached inverse.
static FiffCoordTrans procrustesAlign(int from_frame, int to_frame, const Eigen::MatrixXf &fromp, const Eigen::MatrixXf &top, const Eigen::VectorXf &w, float max_diff)
Eigen::MatrixX3f apply_inverse_trans(const Eigen::MatrixX3f &rr, bool do_move=true) const
Eigen::MatrixX3f apply_trans(const Eigen::MatrixX3f &rr, bool do_move=true) const
static FiffCoordTrans fromCardinalPoints(int from, int to, const float *rL, const float *rN, const float *rR)
One digitizer point: kind (cardinal/HPI/EEG/extra), ident, 3D position in FIFFV_COORD_HEAD.
Registration-ready digitization data: dig points, device→head transform and HPI fit metadata.
QList< FIFFLIB::FiffDigPoint > points
std::unique_ptr< FiffCoordTrans > head_mri_t_adj
QList< FIFFLIB::FiffDigPoint > mri_fids
static bool fit_sphere_to_points(const Eigen::MatrixXf &rr, float simplex_size, Eigen::VectorXf &r0, float &R)
void get_head_scale(FIFFLIB::FiffDigitizerData &dig, const Eigen::Matrix< float, 3, 3, Eigen::RowMajor > &mri_fid, Eigen::Vector3f &scales)
float rms_digitizer_distance(FIFFLIB::FiffDigitizerData &dig) const
void decide_surface_extent(const QString &tag)
void scale(const Eigen::Vector3f &scales)
void calculate_digitizer_distances(FIFFLIB::FiffDigitizerData &dig, bool do_all, bool do_approx) const
int align_fiducials(FIFFLIB::FiffDigitizerData &head_dig, const FIFFLIB::FiffDigitizerData &mri_dig, int niter, int scale_head, float omit_dist, Eigen::Vector3f &scales)
int discard_outlier_digitizer_points(FIFFLIB::FiffDigitizerData &d, float maxdist) const
int iterate_alignment_once(FIFFLIB::FiffDigitizerData &dig, int nasion_weight, const std::optional< Eigen::Vector3f > &nasion_mri, int last_step) const
void decide_curv_display(const QString &name)
double sum_solids(const Eigen::Vector3f &from) const
Eigen::Vector3f project_to_triangle(int tri, float p, float q) const
void find_closest_on_surface_approx(const PointsT &r, int np, Eigen::VectorXi &nearest_tri, Eigen::VectorXf &distances, int nstep) const
Eigen::Matrix< float, Eigen::Dynamic, 3, Eigen::RowMajor > PointsT
Eigen::Map< const Eigen::Vector3f > point(int k) const