diff --git a/main.py b/main.py
index 4f7a4be..af53310 100644
--- a/main.py
+++ b/main.py
@@ -550,10 +550,10 @@ class UpdateOptodesWindow(QWidget):
self.btn_browse_a = QPushButton("Browse .snirf")
self.btn_browse_a.clicked.connect(self.browse_file_a)
- self.label_file_b = QLabel("TXT file:")
+ self.label_file_b = QLabel("Text file:")
self.line_edit_file_b = QLineEdit()
self.line_edit_file_b.setReadOnly(True)
- self.btn_browse_b = QPushButton("Browse .txt")
+ self.btn_browse_b = QPushButton("Browse .txt/.xlsx")
self.btn_browse_b.clicked.connect(self.browse_file_b)
self.label_suffix = QLabel("Suffix to append to filename:")
@@ -574,9 +574,10 @@ class UpdateOptodesWindow(QWidget):
self.description.setText("Some software when creating snirf files will insert a template of optode positions as the correct position of the optodes for the participant.
"
"This is rarely correct as each head differs slightly in shape or size, and a lot of calculations require the optodes to be in the correct location.
"
- "Using a .txt file, we can update the positions in the snirf file to match those of a digitization system such as one from Polhemus or elsewhere.
"
+ "Using a .txt or .xlsx file, we can update the positions in the snirf file to match those of a digitization system such as one from Polhemus or elsewhere.
"
"The .txt file should have the fiducials, detectors, and sources clearly labeled, followed by the x, y, and z coordinates seperated by a space.
"
- "An example format of what a digitization text file should look like can be found by clicking here.")
+ "An example format of what a digitization text file should look like can be found by clicking here. Currently only .xlsx files directly exported from a
"
+ "Polhemus system are supported.")
self.description.linkActivated.connect(self.handle_link_click)
layout.addWidget(self.description)
@@ -605,8 +606,7 @@ class UpdateOptodesWindow(QWidget):
file_a_layout.addWidget(file_a_container)
layout.addLayout(file_a_layout)
-
- help_text_b = "Provide a .txt file with labeled optodes (e.g., nz, rpa, lpa, d1, s1) and their x, y, z coordinates."
+ help_text_b = "Provide a .txt file with labeled optodes (e.g., nz, rpa, lpa, d1, s1) and their x, y, z coordinates, or a .xlsx file from a Polhemius system."
file_b_layout = QHBoxLayout()
@@ -689,7 +689,7 @@ class UpdateOptodesWindow(QWidget):
self.line_edit_file_a.setText(file_path)
def browse_file_b(self):
- file_path, _ = QFileDialog.getOpenFileName(self, "Select TXT File", "", "Text Files (*.txt)")
+ file_path, _ = QFileDialog.getOpenFileName(self, "Select File", "", "Text Files (*.txt), Excel Files (*.xlsx)")
if file_path:
self.line_edit_file_b.setText(file_path)
@@ -743,21 +743,67 @@ class UpdateOptodesWindow(QWidget):
fiducials = {}
ch_positions = {}
+ extension = Path(file_b).suffix
+
# Read the lines from the optode file
- with open(file_b, 'r') as f:
- for line in f:
- if line.strip():
- # Split by the semicolon and convert to meters
- ch_name, coords_str = line.split(":")
- coords = np.array(list(map(float, coords_str.strip().split()))) * 0.001
+ if extension == '.txt':
+ with open(file_b, 'r') as f:
+ for line in f:
+ if line.strip():
+ # Split by the semicolon and convert to meters
+ ch_name, coords_str = line.split(":")
+ coords = np.array(list(map(float, coords_str.strip().split()))) * 0.001
- # The key we have is a fiducial
- if ch_name.lower() in ['lpa', 'nz', 'rpa']:
- fiducials[ch_name.lower()] = coords
+ # The key we have is a fiducial
+ if ch_name.lower() in ['lpa', 'nz', 'rpa']:
+ fiducials[ch_name.lower()] = coords
- # The key we have is a source or detector
- else:
- ch_positions[ch_name.upper()] = coords
+ # The key we have is a source or detector
+ else:
+ ch_positions[ch_name.upper()] = coords
+
+ elif extension == '.xlsx':
+
+ df = pd.read_excel(file_b, sheet_name='Sheet1')
+
+ def _get_block_data(df, block_id, row_mapping, scale=0.001):
+ """Isolates a block, cleans numeric data, and returns a scaled dictionary."""
+ # 1. Isolate and clean
+ block = df[df['block_id'] == block_id].iloc[:, [1, 2, 3]].copy()
+ block = block.apply(pd.to_numeric, errors='coerce')
+
+ # 2. Extract into dictionary based on mapping
+ result = {}
+
+ # If row_mapping is a dict (like {0: 'nz'}), use it directly
+ if isinstance(row_mapping, dict):
+ for row_idx, key in row_mapping.items():
+ if row_idx < len(block):
+ result[key] = block.iloc[row_idx].to_numpy(dtype=float) * scale
+
+ # If row_mapping is a string prefix (like 'D' or 'S'), auto-generate keys
+ elif isinstance(row_mapping, str):
+ for i in range(len(block)):
+ result[f"{row_mapping}{i+1}"] = block.iloc[i].to_numpy(dtype=float) * scale
+
+ return result
+
+
+ # Identify blocks
+ is_empty = df.isnull().all(axis=1)
+ df['block_id'] = is_empty.cumsum()
+ clean_df = df[~is_empty].copy()
+
+ # Process Block 2: Landmarks
+ fiducials = _get_block_data(clean_df, 2, {0: 'nz', 2: 'rpa', 3: 'lpa'})
+
+ # Process Block 3: D-Points
+ d_points = _get_block_data(clean_df, 3, 'D')
+
+ # Process Block 4: S-Points
+ s_points = _get_block_data(clean_df, 4, 'S')
+
+ ch_positions = {**d_points, **s_points}
# Create montage with updated coords in head space
initial_montage = make_dig_montage(ch_pos=ch_positions, nasion=fiducials.get('nz'), lpa=fiducials.get('lpa'), rpa=fiducials.get('rpa'), coord_frame='head') # type: ignore