|
|
@@ -46,7 +46,7 @@ from PySide6.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleVa
|
|
|
|
from PySide6.QtSvgWidgets import QSvgWidget # needed to show svgs when app is not frozen
|
|
|
|
from PySide6.QtSvgWidgets import QSvgWidget # needed to show svgs when app is not frozen
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CURRENT_VERSION = "1.4.1"
|
|
|
|
CURRENT_VERSION = "1.4.3"
|
|
|
|
APP_NAME = "flares"
|
|
|
|
APP_NAME = "flares"
|
|
|
|
API_URL = f"https://git.research.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
|
|
|
API_URL = f"https://git.research.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
|
|
|
API_URL_SECONDARY = f"https://git.research2.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
|
|
|
API_URL_SECONDARY = f"https://git.research2.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
|
|
@@ -1413,44 +1413,44 @@ class UpdateEventsBlazesWindow(QWidget):
|
|
|
|
self.combo_snirf_events.setEnabled(False)
|
|
|
|
self.combo_snirf_events.setEnabled(False)
|
|
|
|
|
|
|
|
|
|
|
|
def browse_file_b(self):
|
|
|
|
def browse_file_b(self):
|
|
|
|
file_path, _ = QFileDialog.getOpenFileName(self, "Select BLAZES File", "", "BLAZES project Files (*.blaze)")
|
|
|
|
file_path, _ = QFileDialog.getOpenFileName(self, "Select JSON Timeline File", "", "JSON Files (*.json)")
|
|
|
|
if file_path:
|
|
|
|
if file_path:
|
|
|
|
self.line_edit_file_b.setText(file_path)
|
|
|
|
self.line_edit_file_b.setText(file_path)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
|
|
data = json.load(f)
|
|
|
|
data = json.load(f)
|
|
|
|
self.blazes_data = data
|
|
|
|
self.json_data = data
|
|
|
|
|
|
|
|
|
|
|
|
obs_keys = self.extract_blazes_observation_strings(data)
|
|
|
|
obs_keys = self.extract_json_observation_strings(data)
|
|
|
|
self.combo_events.clear()
|
|
|
|
self.combo_events.clear()
|
|
|
|
if obs_keys:
|
|
|
|
if obs_keys:
|
|
|
|
self.combo_events.addItems(obs_keys)
|
|
|
|
self.combo_events.addItems(obs_keys)
|
|
|
|
self.combo_events.setEnabled(True)
|
|
|
|
self.combo_events.setEnabled(True)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
QMessageBox.information(self, "No Events", "No observation keys found in BLAZES file.")
|
|
|
|
QMessageBox.information(self, "No Events", "No events found in JSON file.")
|
|
|
|
self.combo_events.setEnabled(False)
|
|
|
|
self.combo_events.setEnabled(False)
|
|
|
|
|
|
|
|
|
|
|
|
except (json.JSONDecodeError, FileNotFoundError, KeyError, TypeError) as e:
|
|
|
|
except (json.JSONDecodeError, FileNotFoundError, KeyError, TypeError) as e:
|
|
|
|
QMessageBox.warning(self, "Error", f"Failed to parse BLAZES file:\n{e}")
|
|
|
|
QMessageBox.warning(self, "Error", f"Failed to parse JSON file:\n{e}")
|
|
|
|
self.combo_events.clear()
|
|
|
|
self.combo_events.clear()
|
|
|
|
self.combo_events.setEnabled(False)
|
|
|
|
self.combo_events.setEnabled(False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_blazes_observation_strings(self, data):
|
|
|
|
def extract_json_observation_strings(self, data):
|
|
|
|
if "obs" not in data:
|
|
|
|
if "events" not in data:
|
|
|
|
raise KeyError("Missing 'obs' key in BLAZES file.")
|
|
|
|
raise KeyError("Missing 'events' key in JSON file.")
|
|
|
|
|
|
|
|
|
|
|
|
obs = data["obs"]
|
|
|
|
|
|
|
|
event_strings = []
|
|
|
|
event_strings = []
|
|
|
|
|
|
|
|
|
|
|
|
for event_name, occurrences in obs.items():
|
|
|
|
# The new format is a flat list chronologically ordered
|
|
|
|
# occurrences is a list of dicts: [{"start_frame": 642, "start_time_sec": 26.777, ...}]
|
|
|
|
for event in data["events"]:
|
|
|
|
for entry in occurrences:
|
|
|
|
track_name = event.get("track_name", "Unknown")
|
|
|
|
onset = entry.get("start_time_sec", 0.0)
|
|
|
|
onset = event.get("start_sec", 0.0)
|
|
|
|
# Formatting to match your SNIRF style: "Event Name @ 0.000s"
|
|
|
|
|
|
|
|
display_str = f"{event_name} @ {onset:.3f}s"
|
|
|
|
# Formatting to match your SNIRF style: "Event Name @ 0.000s"
|
|
|
|
event_strings.append(display_str)
|
|
|
|
display_str = f"{track_name} @ {onset:.3f}s"
|
|
|
|
|
|
|
|
event_strings.append(display_str)
|
|
|
|
|
|
|
|
|
|
|
|
return event_strings
|
|
|
|
return event_strings
|
|
|
|
|
|
|
|
|
|
|
@@ -1465,16 +1465,16 @@ class UpdateEventsBlazesWindow(QWidget):
|
|
|
|
file_b = self.line_edit_file_b.text()
|
|
|
|
file_b = self.line_edit_file_b.text()
|
|
|
|
suffix = APP_NAME
|
|
|
|
suffix = APP_NAME
|
|
|
|
|
|
|
|
|
|
|
|
if not hasattr(self, "blazes_data") or self.combo_events.count() == 0 or self.combo_snirf_events.count() == 0:
|
|
|
|
if not hasattr(self, "json_data") or self.combo_events.count() == 0 or self.combo_snirf_events.count() == 0:
|
|
|
|
QMessageBox.warning(self, "Missing data", "Please make sure a BLAZES and SNIRF event are selected.")
|
|
|
|
QMessageBox.warning(self, "Missing data", "Please make sure a JSON and SNIRF event are selected.")
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
blaze_text = self.combo_events.currentText()
|
|
|
|
json_text = self.combo_events.currentText()
|
|
|
|
_, blaze_time_str = blaze_text.split(" @ ")
|
|
|
|
_, json_time_str = json_text.split(" @ ")
|
|
|
|
blaze_anchor_time = float(blaze_time_str.replace("s", "").strip())
|
|
|
|
json_anchor_time = float(json_time_str.replace("s", "").strip())
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
QMessageBox.critical(self, "BLAZE Event Error", f"Could not parse BLAZE anchor:\n{e}")
|
|
|
|
QMessageBox.critical(self, "JSON Event Error", f"Could not parse JSON anchor:\n{e}")
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
@@ -1485,33 +1485,33 @@ class UpdateEventsBlazesWindow(QWidget):
|
|
|
|
QMessageBox.critical(self, "SNIRF Event Error", f"Could not parse SNIRF anchor:\n{e}")
|
|
|
|
QMessageBox.critical(self, "SNIRF Event Error", f"Could not parse SNIRF anchor:\n{e}")
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
time_shift = snirf_anchor_time - blaze_anchor_time
|
|
|
|
time_shift = snirf_anchor_time - json_anchor_time
|
|
|
|
|
|
|
|
|
|
|
|
onsets, durations, descriptions = [], [], []
|
|
|
|
onsets, durations, descriptions = [], [], []
|
|
|
|
skipped_count = 0
|
|
|
|
skipped_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
ai_data = self.blazes_data.get("ai_tracks", {})
|
|
|
|
events_list = self.json_data.get("events", [])
|
|
|
|
|
|
|
|
|
|
|
|
for track_name, events in ai_data.items():
|
|
|
|
for event in events_list:
|
|
|
|
|
|
|
|
track_name = event.get("track_name", "Unknown")
|
|
|
|
clean_name = track_name.replace("AI: ", "").strip()
|
|
|
|
clean_name = track_name.replace("AI: ", "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
for event in events:
|
|
|
|
original_start = event.get("start_sec", 0.0)
|
|
|
|
original_start = event.get("start_time_sec", 0.0)
|
|
|
|
original_end = event.get("end_sec", original_start)
|
|
|
|
original_end = event.get("end_time_sec", original_start)
|
|
|
|
duration = original_end - original_start
|
|
|
|
duration = original_end - original_start
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# FILTER: Minimum 0.1s duration
|
|
|
|
# FILTER: Minimum 0.1s duration
|
|
|
|
if duration < 0.1:
|
|
|
|
if duration < 0.1:
|
|
|
|
skipped_count += 1
|
|
|
|
skipped_count += 1
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Apply shift
|
|
|
|
# Apply shift
|
|
|
|
adjusted_onset = original_start + time_shift
|
|
|
|
adjusted_onset = original_start + time_shift
|
|
|
|
|
|
|
|
|
|
|
|
onsets.append(round(adjusted_onset, 6))
|
|
|
|
onsets.append(round(adjusted_onset, 6))
|
|
|
|
durations.append(round(duration, 6))
|
|
|
|
durations.append(round(duration, 6))
|
|
|
|
descriptions.append(clean_name)
|
|
|
|
descriptions.append(clean_name)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
QMessageBox.critical(self, "Track Error", f"Failed to process tracks: {e}")
|
|
|
|
QMessageBox.critical(self, "Track Error", f"Failed to process tracks: {e}")
|
|
|
@@ -1538,7 +1538,7 @@ class UpdateEventsBlazesWindow(QWidget):
|
|
|
|
description=descriptions
|
|
|
|
description=descriptions
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Replace existing annotations with the new aligned AI tracks
|
|
|
|
# Replace existing annotations with the new aligned JSON tracks
|
|
|
|
raw.set_annotations(new_annotations)
|
|
|
|
raw.set_annotations(new_annotations)
|
|
|
|
|
|
|
|
|
|
|
|
write_raw_snirf(raw, save_path)
|
|
|
|
write_raw_snirf(raw, save_path)
|
|
|
@@ -1548,75 +1548,6 @@ class UpdateEventsBlazesWindow(QWidget):
|
|
|
|
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
|
|
|
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_optode_positions(self, file_a, file_b, save_path):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fiducials = {}
|
|
|
|
|
|
|
|
ch_positions = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Read the SNIRF file, set the montage, and write it back
|
|
|
|
|
|
|
|
# TODO: Bad! read_raw_snirf doesnt release memory properly! Should be spawned in a seperate process and killed once completed
|
|
|
|
|
|
|
|
raw = read_raw_snirf(file_a, preload=True)
|
|
|
|
|
|
|
|
raw.set_montage(initial_montage)
|
|
|
|
|
|
|
|
write_raw_snirf(raw, save_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_events_to_snirf(self, raw, new_annotations, save_path):
|
|
|
|
|
|
|
|
raw.set_annotations(new_annotations)
|
|
|
|
|
|
|
|
write_raw_snirf(raw, save_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_event_mapping_json(
|
|
|
|
|
|
|
|
self,
|
|
|
|
|
|
|
|
file_a,
|
|
|
|
|
|
|
|
file_b,
|
|
|
|
|
|
|
|
selected_obs,
|
|
|
|
|
|
|
|
snirf_anchor,
|
|
|
|
|
|
|
|
boris_anchor,
|
|
|
|
|
|
|
|
time_shift,
|
|
|
|
|
|
|
|
mapped_events,
|
|
|
|
|
|
|
|
save_path
|
|
|
|
|
|
|
|
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
|
|
"source": {
|
|
|
|
|
|
|
|
"called_from": self.caller,
|
|
|
|
|
|
|
|
"snirf_file": os.path.basename(file_a),
|
|
|
|
|
|
|
|
"boris_file": os.path.basename(file_b),
|
|
|
|
|
|
|
|
"observation": selected_obs
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
"alignment": {
|
|
|
|
|
|
|
|
"snirf_anchor": snirf_anchor,
|
|
|
|
|
|
|
|
"boris_anchor": boris_anchor,
|
|
|
|
|
|
|
|
"time_shift_seconds": time_shift
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
"events": mapped_events,
|
|
|
|
|
|
|
|
"created_at": datetime.utcnow().isoformat() + "Z"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with open(save_path, "w", encoding="utf-8") as f:
|
|
|
|
|
|
|
|
json.dump(payload, f, indent=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return save_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ProgressBubble(QWidget):
|
|
|
|
class ProgressBubble(QWidget):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|