(edited after adding fiducials back when recreating the montage)
@sappelhoff , @mmagnuski
Here is the code I ended up making.
I ignored the Z axis of the electrode coordinates and hope for the best in this regard.
Here is a reproducable example of how I proceeded
import vg
import scipy
from scipy.spatial.transform import Rotation as R
import mne
import logging
import vg
import numpy as np
import scipy
from scipy.spatial.transform import Rotation as R
import mne
import logging
def rotate_montage_with_misplaced_channel(intended_channel_str: str,
actual_channel_str: str,
montage: mne.channels.DigMontage,
exclude=[]) -> mne.channels.DigMontage:
'''
Corrects a montage by artificially rotating all electrode coordinates
'''
log = logging.getLogger(__name__)
# Save all the fiducials
coord_frame = montage.get_positions()['coord_frame']
nasion = montage.get_positions()['nasion']
lpa = montage.get_positions()['lpa']
rpa = montage.get_positions()['rpa']
hsp = montage.get_positions()['hsp']
hpi = montage.get_positions()['hpi']
# retrieve the x,y,z position of the channels
A_intended = montage.get_positions()['ch_pos'][intended_channel_str]
B_actual = montage.get_positions()['ch_pos'][actual_channel_str]
# get the x and y locations for the rotation, ignore Z
angle_between_ch = [vg.signed_angle(A_intended, B_actual, look=vg.basis.x, units='rad'),
vg.signed_angle(A_intended, B_actual, look=vg.basis.y, units='rad'),
0]
# Define the rotation on X and Y
rot = R.from_rotvec(angle_between_ch)
log.warning(f'Rotating coordinates using the angle between {intended_channel_str} to {actual_channel_str} as a reference {angle_between_ch} Rad')
# create an empty matrix for storing new electrode positions
new_pos = np.zeros((len(montage.ch_names), 3), np.float32)
# Loop on all electrode locations
for ch_name, ch_idx in zip(montage.ch_names, list(range(len(montage.ch_names)))):
if ch_name not in exclude:
# rotate them
new_pos[ch_idx,:] = rot.apply(montage.dig[ch_idx]['r'])
old = montage.dig[ch_idx]['r']
new = rot.apply(montage.dig[ch_idx]['r'])
#print(f'old:{old}, new:{new}')
# keep the values
#new_pos[ch_idx,:] = montage.dig[ch_idx]['r']
# apply the values in a new montage
montage_out = mne.channels.make_dig_montage(ch_pos=dict(zip(montage.ch_names, new_pos)),
coord_frame = coord_frame,
nasion = nasion,
lpa = lpa,
rpa = rpa,
hsp = hsp,
hpi = hpi)
# alters and return the montage
return montage_out
Letās apply an example in which the cap was badly positioned such that the electrode that was on FCz was placed on Cz instead
montage = mne.channels.make_standard_montage('standard_1005')
montage.plot()
mnt = rotate_montage_with_misplaced_channel(intended_channel_str='FCz',
actual_channel_str='Cz',
montage= montage)
mnt.plot()
Does it makes sense this way?
My goal after solving this is to apply this rotation to the faulty data hoping it helps Current Source Density algorithms and perhaps inverse methods better.
In this example, if I wanted to have any signal extracted from the ārealā FCz, I would then pick channel labeled āCzā instead (but yet with the correct montage coordinates).
Did I overlook the Z axis?