├── emptyEEG.mat ├── Cohen2020_gedBounds.pdf ├── README.md ├── filterFGx.m ├── gedBounds_empirical.m ├── gedBounds_simulation.m └── topoplotIndie.m /emptyEEG.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikexcohen/gedBounds/main/emptyEEG.mat -------------------------------------------------------------------------------- /Cohen2020_gedBounds.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikexcohen/gedBounds/main/Cohen2020_gedBounds.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code accompanying the paper "A data-driven method to identify frequency boundaries in multichannel electrophysiology data" 2 | ## Michael X Cohen, 2020, J Neuroscience Methods 3 | 4 | Publication pdf is one of the files listed above. Or see: 5 | 6 | https://www.sciencedirect.com/science/article/abs/pii/S0165027020303721 7 | 8 | https://www.biorxiv.org/content/10.1101/2020.07.09.195784v2 9 | 10 | ## See also a YT talk about the method: 11 | https://www.youtube.com/watch?v=REt4v3owrCs&ab_channel=MikeXCohen 12 | -------------------------------------------------------------------------------- /filterFGx.m: -------------------------------------------------------------------------------- 1 | function [filtdat,empVals,fx] = filterFGx(data,srate,f,fwhm,showplot) 2 | % filterFGx Narrow-band filter via frequency-domain Gaussian 3 | % [filtdat,empVals] = filterFGx(data,srate,f,fwhm,showplot) 4 | % 5 | % 6 | % INPUTS 7 | % data : 1 X time or chans X time 8 | % srate : sampling rate in Hz 9 | % f : peak frequency of filter 10 | % fhwm : standard deviation of filter, 11 | % defined as full-width at half-maximum in Hz 12 | % showplot : set to true to show the frequency-domain filter shape 13 | % 14 | % OUTPUTS 15 | % filtdat : filtered data 16 | % empVals : the empirical frequency and FWHM (in Hz and in ms) 17 | % 18 | % Empirical frequency and FWHM depend on the sampling rate and the 19 | % number of time points, and may thus be slightly different from 20 | % the requested values. 21 | % 22 | % mikexcohen@gmail.com 23 | 24 | %% input check 25 | 26 | if size(data,1)>size(data,2) 27 | % help filterFGx 28 | % error('Check data size') 29 | end 30 | 31 | if (f-fwhm)<0 32 | % help filterFGx 33 | % error('increase frequency or decrease FWHM') 34 | end 35 | 36 | if nargin<4 37 | help filterFGx 38 | error('Not enough inputs') 39 | end 40 | 41 | if fwhm<=0 42 | error('FWHM must be greater than 0') 43 | end 44 | 45 | if nargin<5 46 | showplot=false; 47 | end 48 | 49 | %% compute and apply filter 50 | 51 | % frequencies 52 | hz = linspace(0,srate,size(data,2)); 53 | 54 | % create Gaussian 55 | s = fwhm*(2*pi-1)/(4*pi); % normalized width 56 | x = hz-f; % shifted frequencies 57 | fx = exp(-.5*(x/s).^2); % gaussian 58 | fx = fx./abs(max(fx)); % gain-normalized 59 | 60 | %% filter 61 | 62 | % filtdat = 2*real( ifft( bsxfun(@times,fft(data,[],2),fx) ,[],2) ); 63 | filtdat = 2*real( ifft( fft(data',[],1).*fx') )'; 64 | 65 | %% compute empirical frequency and standard deviation 66 | 67 | idx = dsearchn(hz',f); 68 | empVals(1) = hz(idx); 69 | 70 | % find values closest to .5 after MINUS before the peak 71 | empVals(2) = hz(idx-1+dsearchn(fx(idx:end)',.5)) - hz(dsearchn(fx(1:idx)',.5)); 72 | 73 | % also temporal FWHM 74 | tmp = abs(hilbert(real(fftshift(ifft(fx))))); 75 | tmp = tmp./max(tmp); 76 | tx = (0:length(data)-1)/srate; 77 | [~,idxt] = max(tmp); 78 | empVals(3) = (tx(idxt-1+dsearchn(tmp(idxt:end)',.5)) - tx(dsearchn(tmp(1:idxt)',.5)))*1000; 79 | 80 | %% inspect the Gaussian (turned off by default) 81 | 82 | if showplot 83 | figure(10001+showplot),clf 84 | subplot(211) 85 | plot(hz,fx,'o-') 86 | hold on 87 | plot([hz(dsearchn(fx(1:idx)',.5)) hz(idx-1+dsearchn(fx(idx:end)',.5))],[fx(dsearchn(fx(1:idx)',.5)) fx(idx-1+dsearchn(fx(idx:end)',.5))],'k--') 88 | set(gca,'xlim',[max(f-10,0) f+10]); 89 | 90 | title([ 'Requested: ' num2str(f) ', ' num2str(fwhm) ' Hz; Empirical: ' num2str(empVals(1)) ', ' num2str(empVals(2)) ' Hz' ]) 91 | xlabel('Frequency (Hz)'), ylabel('Amplitude gain') 92 | 93 | subplot(212) 94 | tmp1 = real(fftshift(ifft(fx))); tmp1 = tmp1./max(tmp1); 95 | tmp2 = abs(hilbert(tmp1)); 96 | plot(tx,tmp1, tx,tmp2), zoom on 97 | xlabel('Time (s)'), ylabel('Amplitude gain') 98 | end 99 | 100 | %% done. 101 | -------------------------------------------------------------------------------- /gedBounds_empirical.m: -------------------------------------------------------------------------------- 1 | %% gedBounds method to obtain empirical frequency boundaries based on spatial correlations of eigenvectors. 2 | % 3 | % IMPORTANT NOTE! This is designed to work for empirical EEG data in eeglab 4 | % format. It is highly likely that you will need to make at least minor 5 | % modifications to the code for it to work on your data. 6 | % 7 | % Questions? -> mikexcohen@gmail.com 8 | % 9 | 10 | clear 11 | 12 | %% load data file 13 | 14 | load([ homedir 'proc\' num2str(subIClist{datai,1}) '_proc.mat' ]) 15 | 16 | %% possible additional data cleaning or preparation... 17 | 18 | %% frequency parameters 19 | 20 | % frequency resolution and range 21 | numfrex = 100; 22 | lowfreq = 2; % Hz 23 | highfreq = 80; % Hz 24 | frex = logspace(log10(lowfreq),log10(highfreq),numfrex); 25 | 26 | % standard deviations for the Gaussian filtering 27 | stds = linspace(2,5,numfrex); 28 | 29 | % onset times for epoching resting-state data 30 | onsets = EEG.srate*2:2*EEG.srate:EEG.pnts-EEG.srate*4; 31 | snipn = 2*EEG.srate; 32 | 33 | % initialize some variables 34 | [evals,evecs,maps] = deal(zeros(numfrex,EEG.nbchan)); 35 | 36 | %% create R covariance matrix 37 | 38 | % full R 39 | R = zeros(length(onsets),EEG.nbchan,EEG.nbchan); 40 | for segi=1:length(onsets) 41 | snipdat = EEG.data(:,onsets(segi):onsets(segi)+snipn); 42 | snipdat = bsxfun(@minus,snipdat,mean(snipdat,2)); 43 | R(segi,:,:) = snipdat*snipdat'/snipn; 44 | end 45 | 46 | % clean R 47 | meanR = squeeze(mean(R)); 48 | dists = zeros(1,size(R,1)); 49 | for segi=1:size(R,1) 50 | r = R(segi,:,:); 51 | dists(segi) = sqrt( sum((r(:)-meanR(:)).^2) ); 52 | end 53 | R = squeeze(mean( R(zscore(dists)<3,:,:) ,1)); 54 | 55 | % regularized R 56 | gamma = .01; 57 | Rr = R*(1-gamma) + eye(EEG.nbchan)*gamma*mean(eig(R)); 58 | 59 | %% loop over frequencies 60 | 61 | for fi=1:numfrex 62 | 63 | % filter data 64 | fdat = filterFGx(EEG.data,EEG.srate,frex(fi),stds(fi)); 65 | 66 | %%% compute S 67 | % full S 68 | S = zeros(length(onsets),EEG.nbchan,EEG.nbchan); 69 | for segi=1:length(onsets) 70 | snipdat = fdat(:,onsets(segi):onsets(segi)+snipn); 71 | snipdat = bsxfun(@minus,snipdat,mean(snipdat,2)); 72 | S(segi,:,:) = snipdat*snipdat'/snipn; 73 | end 74 | 75 | % clean S 76 | meanS = squeeze(mean(S)); 77 | dists = zeros(1,size(S,1)); 78 | for segi=1:size(S,1) 79 | s = S(segi,:,:); 80 | dists(segi) = sqrt( sum((s(:)-meanS(:)).^2) ); 81 | end 82 | S = squeeze(mean( S(zscore(dists)<3,:,:) ,1)); 83 | 84 | % global variance normalize 85 | S = S / (std(S(:))/std(R(:))); 86 | 87 | 88 | % GED 89 | [W,L] = eig(S,Rr); 90 | [evals(fi,:),sidx] = sort(diag(L),'descend'); 91 | W = W(:,sidx); 92 | 93 | % store top component map and eigenvector 94 | maps(fi,:) = W(:,1)'*S; 95 | evecs(fi,:) = W(:,1); 96 | end 97 | 98 | %% correlation matrices for clustering 99 | 100 | E = zscore(evecs,[],2); 101 | evecCorMat = (E*E'/(EEG.nbchan-1)).^2; 102 | 103 | %% determine the optimal epsilon value 104 | 105 | % range of epsilon parameter values 106 | nepsis = 50; 107 | epsis = linspace(.001,.05,nepsis); 108 | qvec = nan(nepsis,1); 109 | 110 | for epi=1:length(epsis) 111 | 112 | % scan 113 | freqbands = dbscan(evecCorMat,epsis(epi),3,'Distance','Correlation'); 114 | if max(freqbands)<4, continue; end 115 | 116 | % compute q 117 | qtmp = zeros(max(freqbands),1); 118 | MA = false(size(evecCorMat)); 119 | for i=1:max(freqbands) 120 | M = false(size(evecCorMat)); 121 | M(freqbands==i,freqbands==i) = 1; 122 | qtmp(i) = mean(mean(evecCorMat(M))) / mean(mean(evecCorMat(~M))); 123 | MA = MA+M; 124 | end 125 | qvec(epi) = mean(qtmp) + log(mean(MA(:))); 126 | end 127 | 128 | % run it again on the best epsilon value 129 | [~,epsiidx] = findpeaks(qvec,'NPeaks',1,'SortStr','descend'); 130 | if isempty(epsiidx), epsiidx = round(nepsis/2); end 131 | freqbands = dbscan(evecCorMat,epsis(epsiidx),3,'Distance','Correlation'); 132 | 133 | % dissolve tiny clusters, and renumber all clusters consecutively 134 | newc = cell(4,1); n=1; 135 | for i=1:max(freqbands) 136 | cc = bwconncomp(freqbands==i); 137 | for ci=1:cc.NumObjects 138 | if length(cc.PixelIdxList{ci})>2 139 | newc{n} = cc.PixelIdxList{ci}; 140 | n = n+1; 141 | end 142 | end 143 | end 144 | freqbands = -ones(size(frex)); 145 | for ni=1:n-1 146 | freqbands(newc{ni}) = ni; 147 | end 148 | 149 | %% average correlation coefficient within each cluster 150 | 151 | avecorcoef = zeros(max(freqbands),2); 152 | for i=1:max(freqbands) 153 | submat = evecCorMat(freqbands==i,freqbands==i); 154 | avecorcoef(i,1) = mean(nonzeros(tril(submat,-1))); 155 | avecorcoef(i,2) = mean(frex(freqbands==i)); 156 | end 157 | 158 | %% save outputs 159 | 160 | chanlocs = EEG.chanlocs; 161 | save(outfilename,'maps','evecs','evals','frex','evecCorMat','evecCorMat','groupidx','chanlocs','avecorcoef','epsis','qvec','epsiidx') 162 | 163 | 164 | %% some plotting 165 | % 166 | 167 | %% correlation matrix and band boundaries 168 | 169 | figure(1), clf, colormap bone 170 | 171 | imagesc(1-evecCorMat), hold on 172 | f2u = round(linspace(1,length(frex),10)); 173 | set(gca,'clim',[.2 1],'xtick',f2u,'xticklabel',round(frex(f2u),1),'ytick',f2u,'yticklabel',round(frex(f2u),1)) 174 | axis square, axis xy 175 | 176 | for i=1:max(freqbands) 177 | 178 | tbnds = frex(freqbands==i); 179 | tbnds = dsearchn(frex',tbnds([1 end])'); 180 | 181 | % box 182 | plot(tbnds,[1 1]*tbnds(1),'m','linew',2) 183 | plot(tbnds,[1 1]*tbnds(2),'m','linew',2) 184 | plot([1 1]*tbnds(1),tbnds,'m','linew',2) 185 | plot([1 1]*tbnds(2),tbnds,'m','linew',2) 186 | end 187 | 188 | %% plot the average maps 189 | 190 | figure(2), clf 191 | for i=1:max(freqbands) 192 | subplot(3,3,i) 193 | m = pca(maps(freqbands==i,:)); 194 | topoplotIndie(m(:,1),chanlocs,'numcontour',0,'electrodes','off','plotrad',.6); 195 | title([ num2str(round(mean(frex(freqbands==i)),2)) ' Hz' ]) 196 | end 197 | 198 | %% 199 | 200 | -------------------------------------------------------------------------------- /gedBounds_simulation.m: -------------------------------------------------------------------------------- 1 | %% gedBounds method to obtain empirical frequency boundaries based on spatial correlations of eigenvectors. 2 | % You can run the entire script to produce figures, and modify the 3 | % simulation as you like. 4 | % 5 | % Questions? -> mikexcohen@gmail.com 6 | % 7 | 8 | clear 9 | 10 | %% preliminaries 11 | 12 | % mat file containing EEG, leadfield and channel locations 13 | load emptyEEG 14 | 15 | %% simulation parameters 16 | 17 | % boundaries of frequency bands 18 | dipfrex{1} = [ 4 7 ]; 19 | dipfrex{2} = [ 9 11 ]; 20 | dipfrex{3} = [ 11 13 ]; 21 | 22 | % indices of dipole locations 23 | dipoleLoc(1) = 1350; 24 | dipoleLoc(2) = 94; 25 | dipoleLoc(3) = 205; 26 | 27 | %% create the dipole time series 28 | 29 | % create narrowband nonstationary time series 30 | hz = linspace(0,EEG.srate,EEG.pnts); 31 | 32 | % brain of white noise 33 | dipdat = randn(size(EEG.lf.Gain,3),EEG.pnts)*3; 34 | 35 | %% optional: brain of pink noise (1/f) 36 | % The method does not depend on the color of the background noise spectrum. 37 | % This is illustrated by using pink noise instead of white noise. 38 | 39 | % ed = 2000; % exponential decay parameter 40 | % for di=1:size(EEG.lf.Gain,3) 41 | % as = rand(1,EEG.pnts) .* exp(-(0:EEG.pnts-1)/ed); 42 | % data = real(ifft(as .* exp(1i*2*pi*rand(size(as))))); 43 | % dipdat(di,:) = (data-mean(data))/std(data); 44 | % end 45 | 46 | %% 47 | 48 | figure(1), clf 49 | 50 | 51 | for di=1:length(dipoleLoc) 52 | 53 | % FIR1 filtered noise 54 | ford = round( 30*(EEG.srate/3) ); 55 | fkern = fir1(ford,dipfrex{di}./(EEG.srate/2)); 56 | filtdat = filter(fkern,1,randn(EEG.pnts,1)) * 200; 57 | 58 | % put in dipole time series 59 | dipdat(dipoleLoc(di),:) = filtdat; 60 | 61 | subplot(3,1,di), hold on 62 | plot(linspace(0,EEG.srate,length(fkern)),abs(fft(fkern)),'ko-') 63 | plot([0 dipfrex{di}(1) dipfrex{di} dipfrex{di}(2) EEG.srate/2],[0 0 1 1 0 0],'r') 64 | sigX = abs(fft(filtdat)); 65 | plot(linspace(0,EEG.srate,EEG.pnts),sigX/max(sigX),'m') 66 | set(gca,'xlim',[0 20]) 67 | xlabel('Frequency (Hz)'), ylabel('Amplitude') 68 | title([ 'Spectrum, dipole ' num2str(di) ]) 69 | end 70 | 71 | EEG.data = squeeze(EEG.lf.Gain(:,1,:))*dipdat; 72 | 73 | %% now for the analysis 74 | 75 | 76 | %% frex params 77 | 78 | numfrex = 80; 79 | frex = logspace(log10(2),log10(30),numfrex); 80 | stds = linspace(1,1,numfrex); 81 | 82 | onsets = EEG.srate*2:2*EEG.srate:EEG.pnts-EEG.srate*2; 83 | snipn = 2*EEG.srate; 84 | 85 | % initialize 86 | [evals,evecs,maps] = deal(zeros(numfrex,EEG.nbchan)); 87 | 88 | %% create R 89 | 90 | % full R 91 | R = zeros(EEG.nbchan,EEG.nbchan); 92 | for segi=1:length(onsets) 93 | snipdat = EEG.data(:,onsets(segi):onsets(segi)+snipn); 94 | snipdat = bsxfun(@minus,snipdat,mean(snipdat,2)); 95 | R = R + snipdat*snipdat'/snipn; 96 | end 97 | R = R/segi; 98 | 99 | % regularized R 100 | gamma = .01; 101 | Rr = R*(1-gamma) + eye(EEG.nbchan)*gamma*mean(eig(R)); 102 | 103 | %% loop over frequencies 104 | 105 | for fi=1:numfrex 106 | 107 | % filter data 108 | fdat = filterFGx(EEG.data,EEG.srate,frex(fi),stds(fi)); 109 | 110 | %%% compute S 111 | % full S 112 | S = zeros(EEG.nbchan,EEG.nbchan); 113 | for segi=1:length(onsets) 114 | snipdat = fdat(:,onsets(segi):onsets(segi)+snipn); 115 | snipdat = bsxfun(@minus,snipdat,mean(snipdat,2)); 116 | S = S + snipdat*snipdat'/snipn; 117 | end 118 | % global variance normalize (optional; this scales the eigenspectrum) 119 | S = S / (std(S(:))/std(R(:))); 120 | 121 | % GED 122 | [W,L] = eig(S,Rr); 123 | [evals(fi,:),sidx] = sort(diag(L),'descend'); 124 | W = W(:,sidx); 125 | 126 | % store top component map and eigenvector 127 | maps(fi,:) = W(:,1)'*S; 128 | evecs(fi,:) = W(:,1); 129 | 130 | end 131 | 132 | %% correlation matrix for clustering 133 | 134 | E = zscore(evecs,[],2); 135 | evecCorMat = (E*E'/(EEG.nbchan-1)).^2; 136 | 137 | %% 138 | 139 | figure(2), clf, set(gcf,'color','w') 140 | contourf(frex,frex,1-evecCorMat,40,'linecolor','none'), hold on 141 | set(gca,'clim',[0 .8],'xscale','log','yscale','log','xtick',round(logspace(log10(1),log10(numfrex),14),1),'ytick',round(logspace(log10(1),log10(numfrex),14),1),'fontsize',15) 142 | xlabel('Frequency (Hz)'), ylabel('Frequency (Hz)') 143 | axis square, axis xy, colormap bone 144 | title('Eigenvectors correlation matrix') 145 | 146 | % box 147 | for i=1:length(dipfrex) 148 | tbnds = dipfrex{i}; dsearchn(frex',dipfrex{i}'); 149 | plot(tbnds,[1 1]*tbnds(1),'r--','linew',2) 150 | plot(tbnds,[1 1]*tbnds(2),'r--','linew',2) 151 | plot([1 1]*tbnds(1),tbnds,'r--','linew',2) 152 | plot([1 1]*tbnds(2),tbnds,'r--','linew',2) 153 | end 154 | 155 | 156 | e = evals(:,1); 157 | e = e-min(e); e=e./max(e); 158 | plot(frex,1.5*e+frex(1),'b','linew',2) 159 | 160 | h = colorbar; 161 | set(h,'ticklabels',{'1','.8','.6','.4','.2'},'Ticks',0:.2:.8,'fontsize',15) 162 | 163 | %% determine the optimal epsilon value 164 | 165 | % range of epsilon parameters 166 | nepsis = 50; 167 | epsis = linspace(.0001,.05,nepsis); 168 | qvec = nan(nepsis,1); 169 | 170 | for thi=1:length(epsis) 171 | 172 | % scan 173 | freqbands = dbscan(evecCorMat,epsis(thi),3,'Distance','Correlation'); 174 | 175 | % compute q 176 | qtmp = zeros(max(freqbands),1); 177 | MA = false(size(evecCorMat)); 178 | for i=1:max(freqbands) 179 | M = false(size(evecCorMat)); 180 | M(freqbands==i,freqbands==i) = 1; 181 | qtmp(i) = mean(mean(evecCorMat(M))) / mean(mean(evecCorMat(~M))); 182 | MA = MA+M; 183 | end 184 | qvec(thi) = mean(qtmp) + log(mean(MA(:))); 185 | end 186 | 187 | % run it again on the best epsilon value 188 | [~,epsiidx] = findpeaks(qvec,'NPeaks',1,'SortStr','descend'); 189 | if isempty(epsiidx), epsiidx = round(nepsis/2); end 190 | freqbands = dbscan(evecCorMat,epsis(epsiidx),3,'Distance','Correlation'); 191 | 192 | %% draw empirical bounds on correlation map 193 | 194 | for i=1:max(freqbands) 195 | 196 | tbnds = frex(freqbands==i); 197 | tbnds = tbnds([1 end]); 198 | 199 | % box 200 | plot(tbnds,[1 1]*tbnds(1),'m','linew',2) 201 | plot(tbnds,[1 1]*tbnds(2),'m','linew',2) 202 | plot([1 1]*tbnds(1),tbnds,'m','linew',2) 203 | plot([1 1]*tbnds(2),tbnds,'m','linew',2) 204 | end 205 | 206 | %% plot the average maps 207 | 208 | figure(3), clf, set(gcf,'color','w') 209 | for i=1:3 210 | 211 | groundTruth = -squeeze(EEG.lf.Gain(:,1,dipoleLoc(i))); 212 | 213 | % ground truth 214 | subplot(3,3,i) 215 | topoplotIndie(groundTruth,EEG.chanlocs,'numcontour',0,'electrodes','off'); 216 | title([ 'GT: ' num2str(mean(dipfrex{i})) ' Hz' ]) 217 | 218 | % maps 219 | subplot(3,3,i+3) 220 | m = pca(maps(freqbands==i,:)); 221 | m = m(:,1)*sign(corr(m(:,1),groundTruth)); 222 | topoplotIndie(m,EEG.chanlocs,'numcontour',0,'electrodes','off'); 223 | title([ 'Maps: ' num2str(round(mean(frex(freqbands==i)),2)) ' Hz' ]) 224 | 225 | % eigenvectors 226 | subplot(3,3,i+3+3) 227 | m = pca(evecs(freqbands==i,:)); 228 | m = m(:,1)*sign(corr(m(:,1),groundTruth)); 229 | topoplotIndie(m,EEG.chanlocs,'numcontour',0,'electrodes','off'); 230 | title([ 'E-vecs: ' num2str(round(mean(frex(freqbands==i)),2)) ' Hz' ]) 231 | end 232 | 233 | set(findall(gcf,'type','axes'),'fontsize',12) 234 | 235 | %% done. 236 | -------------------------------------------------------------------------------- /topoplotIndie.m: -------------------------------------------------------------------------------- 1 | function [handle,pltchans,epos,Zi] = topoplotIndie(Values,chanlocs,varargin) 2 | 3 | %% Set defaults 4 | 5 | headrad = 0.5; % actual head radius - Don't change this! 6 | GRID_SCALE = 67; % plot map on a 67X67 grid 7 | CIRCGRID = 201; % number of angles to use in drawing circles 8 | HEADCOLOR = [0 0 0]; % default head color (black) 9 | HLINEWIDTH = 1.7; % default linewidth for head, nose, ears 10 | BLANKINGRINGWIDTH = .035;% width of the blanking ring 11 | HEADRINGWIDTH = .007;% width of the cartoon head ring 12 | plotrad = .6; 13 | Values = double(Values); 14 | SHADING = 'interp'; 15 | CONTOURNUM = 6; 16 | ELECTRODES = 'on'; 17 | 18 | nargs = nargin; 19 | if nargs > 2 20 | for i = 1:2:length(varargin) 21 | Param = lower(varargin{i}); 22 | Value = varargin{i+1}; 23 | switch Param 24 | case 'numcontour' 25 | CONTOURNUM = Value; 26 | case 'electrodes' 27 | ELECTRODES = lower(Value); 28 | case 'plotrad' 29 | plotrad = Value; 30 | case 'shading' 31 | SHADING = lower(Value); 32 | if ~any(strcmp(SHADING,{'flat','interp'})) 33 | error('Invalid shading parameter') 34 | end 35 | end 36 | end 37 | end 38 | 39 | Values = Values(:); % make Values a column vector 40 | 41 | %% Read channel location 42 | 43 | labels = {chanlocs.labels}; 44 | Th = [chanlocs.theta]; 45 | Rd = [chanlocs.radius]; 46 | 47 | Th = pi/180*Th; % convert degrees to radians 48 | allchansind = 1:length(Th); 49 | plotchans = 1:length(chanlocs); 50 | [x,y] = pol2cart(Th,Rd); % transform electrode locations from polar to cartesian coordinates 51 | 52 | %% remove infinite and NaN values 53 | 54 | inds = union(find(isnan(Values)), find(isinf(Values))); % NaN and Inf values 55 | for chani=1:length(chanlocs) 56 | if isempty(chanlocs(chani).X); inds = [inds chani]; end 57 | end 58 | 59 | plotchans = setdiff(plotchans,inds); 60 | 61 | plotchans = abs(plotchans); % reverse indicated channel polarities 62 | allchansind = allchansind(plotchans); 63 | Th = Th(plotchans); 64 | Rd = Rd(plotchans); 65 | x = x(plotchans); 66 | y = y(plotchans); 67 | labels = char(labels(plotchans)); % remove labels for electrodes without locations 68 | Values = Values(plotchans); 69 | intrad = min(1.0,max(Rd)*1.02); % default: just outside the outermost electrode location 70 | 71 | %% Find plotting channels 72 | 73 | pltchans = find(Rd <= plotrad); % plot channels inside plotting circle 74 | intchans = find(x <= intrad & y <= intrad); % interpolate and plot channels inside interpolation square 75 | 76 | %% Eliminate channels not plotted 77 | 78 | allx = x; 79 | ally = y; 80 | allchansind = allchansind(pltchans); 81 | intTh = Th(intchans); % eliminate channels outside the interpolation area 82 | intRd = Rd(intchans); 83 | intx = x(intchans); 84 | inty = y(intchans); 85 | Th = Th(pltchans); % eliminate channels outside the plotting area 86 | Rd = Rd(pltchans); 87 | x = x(pltchans); 88 | y = y(pltchans); 89 | 90 | intValues = Values(intchans); 91 | Values = Values(pltchans); 92 | 93 | labels= labels(pltchans,:); 94 | 95 | %% Squeeze channel locations to <= headrad 96 | squeezefac = headrad/plotrad; 97 | intRd = intRd*squeezefac; % squeeze electrode arc_lengths towards the vertex 98 | Rd = Rd*squeezefac; % squeeze electrode arc_lengths towards the vertex 99 | % to plot all inside the head cartoon 100 | intx = intx*squeezefac; 101 | inty = inty*squeezefac; 102 | x = x*squeezefac; 103 | y = y*squeezefac; 104 | allx = allx*squeezefac; 105 | ally = ally*squeezefac; 106 | 107 | %% create grid 108 | xmin = min(-headrad,min(intx)); xmax = max(headrad,max(intx)); 109 | ymin = min(-headrad,min(inty)); ymax = max(headrad,max(inty)); 110 | xi = linspace(xmin,xmax,GRID_SCALE); % x-axis description (row vector) 111 | yi = linspace(ymin,ymax,GRID_SCALE); % y-axis description (row vector) 112 | 113 | [Xi,Yi,Zi] = griddata(inty,intx,intValues,yi',xi,'v4'); % interpolate data 114 | 115 | %% Mask out data outside the head 116 | 117 | mask = (sqrt(Xi.^2 + Yi.^2) <= headrad); % mask outside the plotting circle 118 | Zi(mask == 0) = NaN; % mask non-plotting voxels with NaNs 119 | grid = plotrad; % unless 'noplot', then 3rd output arg is plotrad 120 | delta = xi(2)-xi(1); % length of grid entry 121 | 122 | %% Scale the axes and make the plot 123 | cla % clear current axis 124 | hold on 125 | h = gca; % uses current axes 126 | AXHEADFAC = 1.05; % do not leave room for external ears if head cartoon 127 | set(gca,'Xlim',[-headrad headrad]*AXHEADFAC,'Ylim',[-headrad headrad]*AXHEADFAC); 128 | unsh = (GRID_SCALE+1)/GRID_SCALE; % un-shrink the effects of 'interp' SHADING 129 | 130 | if strcmp(SHADING,'interp') 131 | handle = surface(Xi*unsh,Yi*unsh,zeros(size(Zi)),Zi,'EdgeColor','none','FaceColor',SHADING); 132 | else 133 | handle = surface(Xi-delta/2,Yi-delta/2,zeros(size(Zi)),Zi,'EdgeColor','none','FaceColor',SHADING); 134 | end 135 | contour(Xi,Yi,Zi,CONTOURNUM,'k','hittest','off'); 136 | 137 | %% Plot filled ring to mask jagged grid boundary 138 | hwidth = HEADRINGWIDTH; % width of head ring 139 | hin = squeezefac*headrad*(1- hwidth/2); % inner head ring radius 140 | 141 | if strcmp(SHADING,'interp') 142 | rwidth = BLANKINGRINGWIDTH*1.3; % width of blanking outer ring 143 | else 144 | rwidth = BLANKINGRINGWIDTH; % width of blanking outer ring 145 | end 146 | rin = headrad*(1-rwidth/2); % inner ring radius 147 | if hin>rin 148 | rin = hin; % dont blank inside the head ring 149 | end 150 | 151 | circ = linspace(0,2*pi,CIRCGRID); 152 | rx = sin(circ); 153 | ry = cos(circ); 154 | ringx = [[rx(:)' rx(1) ]*(rin+rwidth) [rx(:)' rx(1)]*rin]; 155 | ringy = [[ry(:)' ry(1) ]*(rin+rwidth) [ry(:)' ry(1)]*rin]; 156 | ringh = patch(ringx,ringy,0.01*ones(size(ringx)),get(gcf,'color'),'edgecolor','none','hittest','off'); hold on 157 | 158 | %% Plot cartoon head, ears, nose 159 | headx = [[rx(:)' rx(1) ]*(hin+hwidth) [rx(:)' rx(1)]*hin]; 160 | heady = [[ry(:)' ry(1) ]*(hin+hwidth) [ry(:)' ry(1)]*hin]; 161 | ringh = patch(headx,heady,ones(size(headx)),HEADCOLOR,'edgecolor',HEADCOLOR,'hittest','off'); hold on 162 | 163 | % Plot ears and nose 164 | base = headrad-.0046; 165 | basex = 0.18*headrad; % nose width 166 | tip = 1.15*headrad; 167 | tiphw = .04*headrad; % nose tip half width 168 | tipr = .01*headrad; % nose tip rounding 169 | q = .04; % ear lengthening 170 | EarX = [.497-.005 .510 .518 .5299 .5419 .54 .547 .532 .510 .489-.005]; % headrad = 0.5 171 | EarY = [q+.0555 q+.0775 q+.0783 q+.0746 q+.0555 -.0055 -.0932 -.1313 -.1384 -.1199]; 172 | sf = headrad/plotrad; % squeeze the model ears and nose 173 | % by this factor 174 | plot3([basex;tiphw;0;-tiphw;-basex]*sf,[base;tip-tipr;tip;tip-tipr;base]*sf,2*ones(size([basex;tiphw;0;-tiphw;-basex])),'Color',HEADCOLOR,'LineWidth',HLINEWIDTH,'hittest','off'); % plot nose 175 | plot3(EarX*sf,EarY*sf,2*ones(size(EarX)),'color',HEADCOLOR,'LineWidth',HLINEWIDTH,'hittest','off') % plot left ear 176 | plot3(-EarX*sf,EarY*sf,2*ones(size(EarY)),'color',HEADCOLOR,'LineWidth',HLINEWIDTH,'hittest','off') % plot right ear 177 | 178 | %% Mark electrode locations 179 | if strcmp(ELECTRODES,'on') % plot electrodes as spots 180 | hp2 = plot3(y,x,ones(size(x)),'.','Color',[0 0 0],'markersize',5,'linewidth',.5,'hittest','off'); 181 | elseif strcmp(ELECTRODES,'labels') % print electrode names (labels) 182 | for i = 1:size(labels,1) 183 | text(double(y(i)),double(x(i)),1,labels(i,:),'HorizontalAlignment','center','VerticalAlignment','middle','Color',[0 0 0],'hittest','off') 184 | end 185 | elseif strcmp(ELECTRODES,'numbers') 186 | for i = 1:size(labels,1) 187 | text(double(y(i)),double(x(i)),1,int2str(allchansind(i)),'HorizontalAlignment','center','VerticalAlignment','middle','Color',[0 0 0],'hittest','off') 188 | end 189 | end 190 | 191 | epos=[x; y]; 192 | axis off 193 | axis equal 194 | 195 | --------------------------------------------------------------------------------