Skip to content

Commit 91b76e8

Browse files
committed
improve documentation for dykstra2012 algorithm
1 parent df4176c commit 91b76e8

File tree

5 files changed

+135
-106
lines changed

5 files changed

+135
-106
lines changed

ft_electroderealign.m

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
% 'nonlin3' apply a 3rd order non-linear warp
5454
% 'nonlin4' apply a 4th order non-linear warp
5555
% 'nonlin5' apply a 5th order non-linear warp
56+
% 'dykstra2012' non-linear wrap only for headshape
57+
% method useful for projecting ECoG onto
58+
% cortex hull.
5659
% cfg.channel = Nx1 cell-array with selection of channels (default = 'all'),
5760
% see FT_CHANNELSELECTION for details
5861
% cfg.fiducial = cell-array with the name of three fiducials used for
@@ -85,7 +88,21 @@
8588
% single triangulated boundary, or a Nx3 matrix with surface
8689
% points
8790
%
88-
% See also FT_READ_SENS, FT_VOLUMEREALIGN, FT_INTERACTIVEREALIGN
91+
% If you want to align ECoG electrodes to the pial surface, you first need to
92+
% compute the cortex hull with FT_PREPARE_MESH. dykstra2012 uses algorithm
93+
% described in Dykstra et al. (2012, Neuroimage) in which electrodes are
94+
% projected onto pial surface while minimizing the displacement of the
95+
% electrodes from original location and maintaining the grid shape. It relies
96+
% on the optimization toolbox.
97+
% cfg.method = 'headshape'
98+
% cfg.warp = 'dykstra2012'
99+
% cfg.headshape = a filename containing headshape, a structure containing a
100+
% single triangulated boundary, or a Nx3 matrix with surface
101+
% points
102+
% cfg.feedback = 'yes' or 'no' (feedback includes the output of the iteration
103+
% procedure.
104+
%
105+
% See also FT_READ_SENS, FT_VOLUMEREALIGN, FT_INTERACTIVEREALIGN, FT_PREPARE_MESH
89106

90107
% Copyright (C) 2005-2015, Robert Oostenveld
91108
%
@@ -297,13 +314,13 @@
297314
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
298315
if strcmp(cfg.method, 'template')
299316
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
300-
317+
301318
% determine electrode selection and overlapping subset for warping
302319
cfg.channel = ft_channelselection(cfg.channel, elec.label);
303320
for i=1:Ntemplate
304321
cfg.channel = ft_channelselection(cfg.channel, target(i).label);
305322
end
306-
323+
307324
% make consistent subselection of electrodes
308325
[cfgsel, datsel] = match_str(cfg.channel, elec.label);
309326
elec.label = elec.label(datsel);
@@ -313,18 +330,18 @@
313330
target(i).label = target(i).label(datsel);
314331
target(i).elecpos = target(i).elecpos(datsel,:);
315332
end
316-
333+
317334
% compute the average of the target electrode positions
318335
average = ft_average_sens(target);
319-
336+
320337
fprintf('warping electrodes to average template... '); % the newline comes later
321338
[norm.elecpos, norm.m] = ft_warp_optim(elec.elecpos, average.elecpos, cfg.warp);
322339
norm.label = elec.label;
323-
340+
324341
dpre = mean(sqrt(sum((average.elecpos - elec.elecpos).^2, 2)));
325342
dpost = mean(sqrt(sum((average.elecpos - norm.elecpos).^2, 2)));
326343
fprintf('mean distance prior to warping %f, after warping %f\n', dpre, dpost);
327-
344+
328345
if strcmp(cfg.feedback, 'yes')
329346
% create an empty figure, continued below...
330347
figure
@@ -334,28 +351,28 @@
334351
xlabel('x')
335352
ylabel('y')
336353
zlabel('z')
337-
354+
338355
% plot all electrodes before warping
339356
ft_plot_sens(elec, 'r*');
340-
357+
341358
% plot all electrodes after warping
342359
ft_plot_sens(norm, 'm.', 'label', 'label');
343-
360+
344361
% plot the template electrode locations
345362
ft_plot_sens(average, 'b.');
346-
363+
347364
% plot lines connecting the input and the realigned electrode locations with the template locations
348365
my_line3(elec.elecpos, average.elecpos, 'color', 'r');
349366
my_line3(norm.elecpos, average.elecpos, 'color', 'm');
350367
end
351-
368+
352369
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
353370
elseif strcmp(cfg.method, 'headshape')
354371
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
355-
372+
356373
% determine electrode selection and overlapping subset for warping
357374
cfg.channel = ft_channelselection(cfg.channel, elec.label);
358-
375+
359376
% make subselection of electrodes
360377
[cfgsel, datsel] = match_str(cfg.channel, elec.label);
361378
elec.label = elec.label(datsel);
@@ -372,14 +389,14 @@
372389
dpost = ft_warp_error(norm.m, elec.elecpos, headshape, cfg.warp);
373390
fprintf('mean distance prior to warping %f, after warping %f\n', dpre, dpost);
374391
end
375-
392+
376393
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
377394
elseif strcmp(cfg.method, 'fiducial')
378395
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
379-
396+
380397
% the fiducials have to be present in the electrodes and in the template set
381398
label = intersect(lower(elec.label), lower(target.label));
382-
399+
383400
if ~isfield(cfg, 'fiducial') || isempty(cfg.fiducial)
384401
% try to determine the names of the fiducials automatically
385402
option1 = {'nasion' 'left' 'right'};
@@ -405,17 +422,17 @@
405422
end
406423
end
407424
fprintf('matching fiducials {''%s'', ''%s'', ''%s''}\n', cfg.fiducial{1}, cfg.fiducial{2}, cfg.fiducial{3});
408-
425+
409426
% determine electrode selection
410427
cfg.channel = ft_channelselection(cfg.channel, elec.label);
411428
[cfgsel, datsel] = match_str(cfg.channel, elec.label);
412429
elec.label = elec.label(datsel);
413430
elec.elecpos = elec.elecpos(datsel,:);
414-
431+
415432
if length(cfg.fiducial)~=3
416433
error('you must specify exaclty three fiducials');
417434
end
418-
435+
419436
% do case-insensitive search for fiducial locations
420437
nas_indx = match_str(lower(elec.label), lower(cfg.fiducial{1}));
421438
lpa_indx = match_str(lower(elec.label), lower(cfg.fiducial{2}));
@@ -426,11 +443,11 @@
426443
elec_nas = elec.elecpos(nas_indx,:);
427444
elec_lpa = elec.elecpos(lpa_indx,:);
428445
elec_rpa = elec.elecpos(rpa_indx,:);
429-
446+
430447
% FIXME change the flow in the remainder
431448
% if one or more template electrode sets are specified, then align to the average of those
432449
% if no template is specified, then align so that the fiducials are along the axis
433-
450+
434451
% find the matching fiducials in the template and average them
435452
tmpl_nas = nan(Ntemplate,3);
436453
tmpl_lpa = nan(Ntemplate,3);
@@ -449,21 +466,21 @@
449466
tmpl_nas = mean(tmpl_nas,1);
450467
tmpl_lpa = mean(tmpl_lpa,1);
451468
tmpl_rpa = mean(tmpl_rpa,1);
452-
469+
453470
% realign both to a common coordinate system
454471
elec2common = ft_headcoordinates(elec_nas, elec_lpa, elec_rpa);
455472
templ2common = ft_headcoordinates(tmpl_nas, tmpl_lpa, tmpl_rpa);
456-
473+
457474
% compute the combined transform
458475
norm = [];
459476
norm.m = templ2common \ elec2common;
460-
477+
461478
% apply the transformation to the fiducials as sanity check
462479
norm.elecpos(1,:) = ft_warp_apply(norm.m, elec_nas, 'homogeneous');
463480
norm.elecpos(2,:) = ft_warp_apply(norm.m, elec_lpa, 'homogeneous');
464481
norm.elecpos(3,:) = ft_warp_apply(norm.m, elec_rpa, 'homogeneous');
465482
norm.label = cfg.fiducial;
466-
483+
467484
nas_indx = match_str(lower(elec.label), lower(cfg.fiducial{1}));
468485
lpa_indx = match_str(lower(elec.label), lower(cfg.fiducial{2}));
469486
rpa_indx = match_str(lower(elec.label), lower(cfg.fiducial{3}));
@@ -473,7 +490,7 @@
473490
rpa_indx = match_str(lower(norm.label), lower(cfg.fiducial{3}));
474491
dpost = mean(sqrt(sum((norm.elecpos([nas_indx lpa_indx rpa_indx],:) - [tmpl_nas; tmpl_lpa; tmpl_rpa]).^2, 2)));
475492
fprintf('mean distance between fiducials prior to realignment %f, after realignment %f\n', dpre, dpost);
476-
493+
477494
if strcmp(cfg.feedback, 'yes')
478495
% create an empty figure, continued below...
479496
figure
@@ -483,23 +500,23 @@
483500
xlabel('x')
484501
ylabel('y')
485502
zlabel('z')
486-
503+
487504
% plot the first three electrodes before transformation
488505
my_plot3(elec.elecpos(1,:), 'r*');
489506
my_plot3(elec.elecpos(2,:), 'r*');
490507
my_plot3(elec.elecpos(3,:), 'r*');
491508
my_text3(elec.elecpos(1,:), elec.label{1}, 'color', 'r');
492509
my_text3(elec.elecpos(2,:), elec.label{2}, 'color', 'r');
493510
my_text3(elec.elecpos(3,:), elec.label{3}, 'color', 'r');
494-
511+
495512
% plot the template fiducials
496513
my_plot3(tmpl_nas, 'b*');
497514
my_plot3(tmpl_lpa, 'b*');
498515
my_plot3(tmpl_rpa, 'b*');
499516
my_text3(tmpl_nas, ' nas', 'color', 'b');
500517
my_text3(tmpl_lpa, ' lpa', 'color', 'b');
501518
my_text3(tmpl_rpa, ' rpa', 'color', 'b');
502-
519+
503520
% plot all electrodes after transformation
504521
my_plot3(norm.elecpos, 'm.');
505522
my_plot3(norm.elecpos(1,:), 'm*');
@@ -509,7 +526,7 @@
509526
my_text3(norm.elecpos(2,:), norm.label{2}, 'color', 'm');
510527
my_text3(norm.elecpos(3,:), norm.label{3}, 'color', 'm');
511528
end
512-
529+
513530
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
514531
elseif strcmp(cfg.method, 'interactive')
515532
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -540,14 +557,14 @@
540557
clear global norm
541558
norm = tmp;
542559
clear tmp
543-
560+
544561
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
545562
elseif strcmp(cfg.method, 'project')
546563
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
547564
[dum, prj] = project_elec(elec.elecpos, headshape.pos, headshape.tri);
548565
% replace the electrodes with the projected version
549566
elec.elecpos = prj;
550-
567+
551568
else
552569
error('unknown method');
553570
end % if method
@@ -560,7 +577,7 @@
560577
if strcmp(lower(cfg.warp), 'dykstra2012')
561578
elec_realigned = norm;
562579
elec_realigned.unit = elec_original.unit;
563-
580+
564581
else
565582
% the transformation is a linear or non-linear warp, i.e. a vector
566583
try
@@ -574,20 +591,20 @@
574591
end
575592
% remember the transformation
576593
elec_realigned.(cfg.warp) = norm.m;
577-
594+
578595
end
579-
596+
580597
case {'fiducial' 'interactive'}
581598
% the transformation is a 4x4 homogenous matrix
582599
% apply the transformation to the original complete set of sensors
583600
elec_realigned = ft_transform_sens(norm.m, elec_original);
584601
% remember the transformation
585602
elec_realigned.homogeneous = norm.m;
586-
603+
587604
case 'project'
588605
% nothing to be done
589606
elec_realigned = elec;
590-
607+
591608
otherwise
592609
error('unknown method');
593610
end

ft_prepare_mesh.m

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
% FT_PREPARE_MESH creates a triangulated surface mesh for the volume
44
% conduction model. The mesh can either be selected manually from raw
55
% mri data or can be generated starting from a segmented volume
6-
% information stored in the mri structure. The result is a bnd
7-
% structure which contains the information about all segmented surfaces
8-
% related to mri and are expressed in world coordinates.
6+
% information stored in the mri structure. FT_PREPARE_MESH can be used
7+
% to create a cortex hull, i.e. the smoothed envelope around the pial
8+
% surface created by freesurfer. The result is a bnd structure which
9+
% contains the information about all segmented surfaces related to mri
10+
% sand are expressed in world coordinates.
911
%
1012
% Use as
1113
% bnd = ft_prepare_mesh(cfg, mri)
1214
% bnd = ft_prepare_mesh(cfg, seg)
15+
% bnd = ft_prepare_mesh(cfg) # for cortexhull
1316
%
1417
% Configuration options:
1518
% cfg.method = string, can be 'interactive', 'projectmesh', 'iso2mesh', 'isosurface',
@@ -20,6 +23,12 @@
2023
% cfg.headshape = (optional) a filename containing headshape, a Nx3 matrix with surface
2124
% points, or a structure with a single or multiple boundaries
2225
%
26+
% Method 'cortexhull' has its own specific configuration options:
27+
% cfg.headshape = a filename containing the pial surface computed by
28+
% freesurfer recon-all ('/path/to/surf/lh.pial')
29+
% cfg.
30+
%
31+
%
2332
% To facilitate data-handling and distributed computing you can use
2433
% cfg.inputfile = ...
2534
% cfg.outputfile = ...
@@ -40,6 +49,11 @@
4049
% cfg.numvertices = [800, 1600, 2400];
4150
% bnd = ft_prepare_mesh(cfg, segmentation);
4251
%
52+
% cfg = [];
53+
% cfg.method = 'cortexhull';
54+
% cfg.headshape = '/path/to/surf/lh.pial';
55+
% cortex_hull = ft_prepare_mesh(cfg);
56+
%
4357
% See also FT_VOLUMESEGMENT, FT_PREPARE_HEADMODEL, FT_PLOT_MESH
4458

4559
% Undocumented functionality: at this moment it allows for either
@@ -175,8 +189,8 @@
175189
end
176190

177191
case 'cortexhull'
178-
bnd = prepare_cortexhull(cfg);
179-
192+
bnd = prepare_cortexhull(cfg);
193+
180194
otherwise
181195
error('unsupported cfg.method')
182196
end

0 commit comments

Comments
 (0)