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