Commit 9460b9cdbce8edd6a14117565a551762c67a12ab

Authored by Andrey Zhdanov
Committed by GitHub
1 parent db6e8b0d
Exists in master

Added very basic support for sessions (#374)

Markers code made more robust
Minor fixes
UI for sessions is quite messy, needs polishing
Showing 1 changed file with 82 additions and 30 deletions   Show diff stats
invesalius/gui/task_navigator.py
@@ -20,7 +20,6 @@ @@ -20,7 +20,6 @@
20 import dataclasses 20 import dataclasses
21 from functools import partial 21 from functools import partial
22 import itertools 22 import itertools
23 -import csv  
24 import time 23 import time
25 24
26 import nibabel as nb 25 import nibabel as nb
@@ -208,6 +207,13 @@ class InnerFoldPanel(wx.Panel): @@ -208,6 +207,13 @@ class InnerFoldPanel(wx.Panel):
208 leftSpacing=0, rightSpacing=0) 207 leftSpacing=0, rightSpacing=0)
209 self.dbs_item.Hide() 208 self.dbs_item.Hide()
210 209
  210 + # Fold 6 - Sessions
  211 + item = fold_panel.AddFoldPanel(_("Sessions"), collapsed=False)
  212 + stw = SessionPanel(item)
  213 + fold_panel.ApplyCaptionStyle(item, style)
  214 + fold_panel.AddFoldPanelWindow(item, stw, spacing= 0,
  215 + leftSpacing=0, rightSpacing=0)
  216 +
211 # Check box for camera update in volume rendering during navigation 217 # Check box for camera update in volume rendering during navigation
212 tooltip = wx.ToolTip(_("Update camera in volume")) 218 tooltip = wx.ToolTip(_("Update camera in volume"))
213 checkcamera = wx.CheckBox(self, -1, _('Vol. camera')) 219 checkcamera = wx.CheckBox(self, -1, _('Vol. camera'))
@@ -1104,8 +1110,8 @@ class MarkersPanel(wx.Panel): @@ -1104,8 +1110,8 @@ class MarkersPanel(wx.Panel):
1104 x_seed : float = 0 1110 x_seed : float = 0
1105 y_seed : float = 0 1111 y_seed : float = 0
1106 z_seed : float = 0 1112 z_seed : float = 0
1107 - is_target : int = 0 # is_target is int instead of boolean to avoid  
1108 - # problems with CSV export 1113 + is_target : bool = False
  1114 + session_id : int = 1
1109 1115
1110 # x, y, z, alpha, beta, gamma can be jointly accessed as coord 1116 # x, y, z, alpha, beta, gamma can be jointly accessed as coord
1111 @property 1117 @property
@@ -1135,27 +1141,44 @@ class MarkersPanel(wx.Panel): @@ -1135,27 +1141,44 @@ class MarkersPanel(wx.Panel):
1135 self.x_seed, self.y_seed, self.z_seed = new_seed 1141 self.x_seed, self.y_seed, self.z_seed = new_seed
1136 1142
1137 @classmethod 1143 @classmethod
1138 - def get_headers(cls):  
1139 - """Return the list of field names (headers) for exporting to csv.""" 1144 + def to_string_headers(cls):
  1145 + """Return the string containing tab-separated list of field names (headers)."""
1140 res = [field.name for field in dataclasses.fields(cls)] 1146 res = [field.name for field in dataclasses.fields(cls)]
1141 res.extend(['x_world', 'y_world', 'z_world', 'alpha_world', 'beta_world', 'gamma_world']) 1147 res.extend(['x_world', 'y_world', 'z_world', 'alpha_world', 'beta_world', 'gamma_world'])
1142 - return res 1148 + return '\t'.join(map(lambda x: '\"%s\"' % x, res))
1143 1149
1144 - def get_values(self):  
1145 - """Return the list of values for exporting to csv."""  
1146 - res = []  
1147 - res.extend(dataclasses.astuple(self)) 1150 + def to_string(self):
  1151 + """Serialize to excel-friendly tab-separated string"""
  1152 + res = ''
  1153 + for field in dataclasses.fields(self.__class__):
  1154 + if field.type is str:
  1155 + res += ('\"%s\"\t' % getattr(self, field.name))
  1156 + else:
  1157 + res += ('%s\t' % str(getattr(self, field.name)))
1148 1158
1149 # Add world coordinates (in addition to the internal ones). 1159 # Add world coordinates (in addition to the internal ones).
1150 position_world, orientation_world = imagedata_utils.convert_invesalius_to_world( 1160 position_world, orientation_world = imagedata_utils.convert_invesalius_to_world(
1151 position=[self.x, self.y, self.z], 1161 position=[self.x, self.y, self.z],
1152 orientation=[self.alpha, self.beta, self.gamma], 1162 orientation=[self.alpha, self.beta, self.gamma],
1153 ) 1163 )
1154 - res.extend(position_world)  
1155 - res.extend(orientation_world)  
1156 1164
  1165 + res += '\t'.join(map(lambda x: 'N/A' if x is None else str(x), (*position_world, *orientation_world)))
1157 return res 1166 return res
1158 1167
  1168 + def from_string(self, str):
  1169 + """Deserialize from a tab-separated string. If the string is not
  1170 + properly formatted, might throw an exception and leave the object
  1171 + in an inconsistent state."""
  1172 + for field, str_val in zip(dataclasses.fields(self.__class__), str.split('\t')):
  1173 + if field.type is float:
  1174 + setattr(self, field.name, float(str_val))
  1175 + if field.type is int:
  1176 + setattr(self, field.name, int(str_val))
  1177 + if field.type is str:
  1178 + setattr(self, field.name, str_val[1:-1]) # remove the quotation marks
  1179 + if field.type is bool:
  1180 + setattr(self, field.name, str_val=='True')
  1181 +
1159 def __init__(self, parent): 1182 def __init__(self, parent):
1160 wx.Panel.__init__(self, parent) 1183 wx.Panel.__init__(self, parent)
1161 try: 1184 try:
@@ -1176,9 +1199,7 @@ class MarkersPanel(wx.Panel): @@ -1176,9 +1199,7 @@ class MarkersPanel(wx.Panel):
1176 1199
1177 self.marker_colour = const.MARKER_COLOUR 1200 self.marker_colour = const.MARKER_COLOUR
1178 self.marker_size = const.MARKER_SIZE 1201 self.marker_size = const.MARKER_SIZE
1179 -  
1180 - # Define CSV dialect for saving/loading markers  
1181 - csv.register_dialect('markers_dialect', delimiter='\t', quoting=csv.QUOTE_NONNUMERIC) 1202 + self.current_session = 1
1182 1203
1183 # Change marker size 1204 # Change marker size
1184 spin_size = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23)) 1205 spin_size = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23))
@@ -1233,6 +1254,7 @@ class MarkersPanel(wx.Panel): @@ -1233,6 +1254,7 @@ class MarkersPanel(wx.Panel):
1233 self.lc.InsertColumn(3, 'Z') 1254 self.lc.InsertColumn(3, 'Z')
1234 self.lc.InsertColumn(4, 'ID') 1255 self.lc.InsertColumn(4, 'ID')
1235 self.lc.InsertColumn(5, 'Target') 1256 self.lc.InsertColumn(5, 'Target')
  1257 + self.lc.InsertColumn(6, 'Session')
1236 1258
1237 self.lc.SetColumnWidth(0, 28) 1259 self.lc.SetColumnWidth(0, 28)
1238 self.lc.SetColumnWidth(1, 50) 1260 self.lc.SetColumnWidth(1, 50)
@@ -1240,6 +1262,7 @@ class MarkersPanel(wx.Panel): @@ -1240,6 +1262,7 @@ class MarkersPanel(wx.Panel):
1240 self.lc.SetColumnWidth(3, 50) 1262 self.lc.SetColumnWidth(3, 50)
1241 self.lc.SetColumnWidth(4, 60) 1263 self.lc.SetColumnWidth(4, 60)
1242 self.lc.SetColumnWidth(5, 60) 1264 self.lc.SetColumnWidth(5, 60)
  1265 + self.lc.SetColumnWidth(5, 50)
1243 1266
1244 self.lc.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnMouseRightDown) 1267 self.lc.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnMouseRightDown)
1245 self.lc.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemBlink) 1268 self.lc.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemBlink)
@@ -1264,6 +1287,7 @@ class MarkersPanel(wx.Panel): @@ -1264,6 +1287,7 @@ class MarkersPanel(wx.Panel):
1264 Publisher.subscribe(self.CreateMarker, 'Create marker') 1287 Publisher.subscribe(self.CreateMarker, 'Create marker')
1265 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') 1288 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
1266 Publisher.subscribe(self.UpdateSeedCoordinates, 'Update tracts') 1289 Publisher.subscribe(self.UpdateSeedCoordinates, 'Update tracts')
  1290 + Publisher.subscribe(self.OnChangeCurrentSession, 'Current session changed')
1267 1291
1268 def __find_target_marker(self): 1292 def __find_target_marker(self):
1269 """Return the index of the marker currently selected as target (there 1293 """Return the index of the marker currently selected as target (there
@@ -1310,13 +1334,13 @@ class MarkersPanel(wx.Panel): @@ -1310,13 +1334,13 @@ class MarkersPanel(wx.Panel):
1310 1334
1311 # Unset the previous target 1335 # Unset the previous target
1312 if prev_idx != -1: 1336 if prev_idx != -1:
1313 - self.markers[prev_idx].is_target = 0 1337 + self.markers[prev_idx].is_target = False
1314 self.lc.SetItemBackgroundColour(prev_idx, 'white') 1338 self.lc.SetItemBackgroundColour(prev_idx, 'white')
1315 Publisher.sendMessage('Set target transparency', status=False, index=prev_idx) 1339 Publisher.sendMessage('Set target transparency', status=False, index=prev_idx)
1316 self.lc.SetItem(prev_idx, 5, "") 1340 self.lc.SetItem(prev_idx, 5, "")
1317 1341
1318 # Set the new target 1342 # Set the new target
1319 - self.markers[idx].is_target = 1 1343 + self.markers[idx].is_target = True
1320 self.lc.SetItemBackgroundColour(idx, 'RED') 1344 self.lc.SetItemBackgroundColour(idx, 'RED')
1321 self.lc.SetItem(idx, 5, _("Yes")) 1345 self.lc.SetItem(idx, 5, _("Yes"))
1322 1346
@@ -1347,7 +1371,7 @@ class MarkersPanel(wx.Panel): @@ -1347,7 +1371,7 @@ class MarkersPanel(wx.Panel):
1347 def OnMouseRightDown(self, evt): 1371 def OnMouseRightDown(self, evt):
1348 # TODO: Enable the "Set as target" only when target is created with registered object 1372 # TODO: Enable the "Set as target" only when target is created with registered object
1349 menu_id = wx.Menu() 1373 menu_id = wx.Menu()
1350 - edit_id = menu_id.Append(0, _('Edit ID')) 1374 + edit_id = menu_id.Append(0, _('Edit label'))
1351 menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerLabel, edit_id) 1375 menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerLabel, edit_id)
1352 color_id = menu_id.Append(2, _('Edit color')) 1376 color_id = menu_id.Append(2, _('Edit color'))
1353 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetColor, color_id) 1377 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetColor, color_id)
@@ -1472,20 +1496,20 @@ class MarkersPanel(wx.Panel): @@ -1472,20 +1496,20 @@ class MarkersPanel(wx.Panel):
1472 wx.MessageBox(_("Unknown version of the markers file."), _("InVesalius 3")) 1496 wx.MessageBox(_("Unknown version of the markers file."), _("InVesalius 3"))
1473 return 1497 return
1474 1498
1475 - reader = csv.reader(file, dialect='markers_dialect')  
1476 - next(reader) # skip the header line 1499 + file.readline() # skip the header line
1477 1500
1478 # Read the data lines and create markers 1501 # Read the data lines and create markers
1479 - for line in reader:  
1480 - marker = self.Marker(*line[:-6]) # Discard the last 6 fields (the world coordinates) 1502 + for line in file.readlines():
  1503 + marker = self.Marker()
  1504 + marker.from_string(line)
1481 self.CreateMarker(coord=marker.coord, colour=marker.colour, size=marker.size, 1505 self.CreateMarker(coord=marker.coord, colour=marker.colour, size=marker.size,
1482 - label=marker.label, is_target=0, seed=marker.seed) 1506 + label=marker.label, is_target=False, seed=marker.seed, session_id=marker.session_id)
1483 1507
1484 if marker.label in self.__list_fiducial_labels(): 1508 if marker.label in self.__list_fiducial_labels():
1485 Publisher.sendMessage('Load image fiducials', label=marker.label, coord=marker.coord) 1509 Publisher.sendMessage('Load image fiducials', label=marker.label, coord=marker.coord)
1486 1510
1487 - # If the new marker has is_target=1 (True), we first create  
1488 - # a marker with is_target=0 (False), and then call __set_marker_as_target 1511 + # If the new marker has is_target=True, we first create
  1512 + # a marker with is_target=False, and then call __set_marker_as_target
1489 if marker.is_target: 1513 if marker.is_target:
1490 self.__set_marker_as_target(len(self.markers)-1) 1514 self.__set_marker_as_target(len(self.markers)-1)
1491 1515
@@ -1521,9 +1545,8 @@ class MarkersPanel(wx.Panel): @@ -1521,9 +1545,8 @@ class MarkersPanel(wx.Panel):
1521 try: 1545 try:
1522 with open(filename, 'w', newline='') as file: 1546 with open(filename, 'w', newline='') as file:
1523 file.writelines(['%s%i\n' % (const.MARKER_FILE_MAGICK_STRING, const.CURRENT_MARKER_FILE_VERSION)]) 1547 file.writelines(['%s%i\n' % (const.MARKER_FILE_MAGICK_STRING, const.CURRENT_MARKER_FILE_VERSION)])
1524 - writer = csv.writer(file, dialect='markers_dialect')  
1525 - writer.writerow(self.Marker.get_headers())  
1526 - writer.writerows(marker.get_values() for marker in self.markers) 1548 + file.writelines(['%s\n' % self.Marker.to_string_headers()])
  1549 + file.writelines('%s\n' % marker.to_string() for marker in self.markers)
1527 file.close() 1550 file.close()
1528 except: 1551 except:
1529 wx.MessageBox(_("Error writing markers file."), _("InVesalius 3")) 1552 wx.MessageBox(_("Error writing markers file."), _("InVesalius 3"))
@@ -1535,7 +1558,10 @@ class MarkersPanel(wx.Panel): @@ -1535,7 +1558,10 @@ class MarkersPanel(wx.Panel):
1535 def OnSelectSize(self, evt, ctrl): 1558 def OnSelectSize(self, evt, ctrl):
1536 self.marker_size = ctrl.GetValue() 1559 self.marker_size = ctrl.GetValue()
1537 1560
1538 - def CreateMarker(self, coord=None, colour=None, size=None, label='*', is_target=0, seed=None): 1561 + def OnChangeCurrentSession(self, new_session_id):
  1562 + self.current_session = new_session_id
  1563 +
  1564 + def CreateMarker(self, coord=None, colour=None, size=None, label='*', is_target=False, seed=None, session_id=None):
1539 new_marker = self.Marker() 1565 new_marker = self.Marker()
1540 new_marker.coord = coord or self.current_coord 1566 new_marker.coord = coord or self.current_coord
1541 new_marker.colour = colour or self.marker_colour 1567 new_marker.colour = colour or self.marker_colour
@@ -1543,6 +1569,7 @@ class MarkersPanel(wx.Panel): @@ -1543,6 +1569,7 @@ class MarkersPanel(wx.Panel):
1543 new_marker.label = label 1569 new_marker.label = label
1544 new_marker.is_target = is_target 1570 new_marker.is_target = is_target
1545 new_marker.seed = seed or self.current_seed 1571 new_marker.seed = seed or self.current_seed
  1572 + new_marker.session_id = session_id or self.current_session
1546 1573
1547 # Note that ball_id is zero-based, so we assign it len(self.markers) before the new marker is added 1574 # Note that ball_id is zero-based, so we assign it len(self.markers) before the new marker is added
1548 Publisher.sendMessage('Add marker', ball_id=len(self.markers), 1575 Publisher.sendMessage('Add marker', ball_id=len(self.markers),
@@ -1557,7 +1584,8 @@ class MarkersPanel(wx.Panel): @@ -1557,7 +1584,8 @@ class MarkersPanel(wx.Panel):
1557 self.lc.SetItem(num_items, 1, str(round(new_marker.x, 2))) 1584 self.lc.SetItem(num_items, 1, str(round(new_marker.x, 2)))
1558 self.lc.SetItem(num_items, 2, str(round(new_marker.y, 2))) 1585 self.lc.SetItem(num_items, 2, str(round(new_marker.y, 2)))
1559 self.lc.SetItem(num_items, 3, str(round(new_marker.z, 2))) 1586 self.lc.SetItem(num_items, 3, str(round(new_marker.z, 2)))
1560 - self.lc.SetItem(num_items, 4, str(new_marker.label)) 1587 + self.lc.SetItem(num_items, 4, new_marker.label)
  1588 + self.lc.SetItem(num_items, 6, str(new_marker.session_id))
1561 self.lc.EnsureVisible(num_items) 1589 self.lc.EnsureVisible(num_items)
1562 1590
1563 class DbsPanel(wx.Panel): 1591 class DbsPanel(wx.Panel):
@@ -2011,6 +2039,30 @@ class TractographyPanel(wx.Panel): @@ -2011,6 +2039,30 @@ class TractographyPanel(wx.Panel):
2011 Publisher.sendMessage('Remove tracts') 2039 Publisher.sendMessage('Remove tracts')
2012 2040
2013 2041
  2042 +class SessionPanel(wx.Panel):
  2043 + def __init__(self, parent):
  2044 + wx.Panel.__init__(self, parent)
  2045 + try:
  2046 + default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
  2047 + except AttributeError:
  2048 + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR)
  2049 + self.SetBackgroundColour(default_colour)
  2050 +
  2051 + # session count spinner
  2052 + self.__spin_session = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23))
  2053 + self.__spin_session.SetRange(1, 99)
  2054 + self.__spin_session.SetValue(1)
  2055 +
  2056 + self.__spin_session.Bind(wx.EVT_TEXT, self.OnSessionChanged)
  2057 + self.__spin_session.Bind(wx.EVT_SPINCTRL, self.OnSessionChanged)
  2058 +
  2059 + sizer_create = wx.FlexGridSizer(rows=1, cols=1, hgap=5, vgap=5)
  2060 + sizer_create.AddMany([(self.__spin_session, 1)])
  2061 +
  2062 + def OnSessionChanged(self, evt):
  2063 + Publisher.sendMessage('Current session changed', new_session_id=self.__spin_session.GetValue())
  2064 +
  2065 +
2014 class InputAttributes(object): 2066 class InputAttributes(object):
2015 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python 2067 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python
2016 def __init__(self, *initial_data, **kwargs): 2068 def __init__(self, *initial_data, **kwargs):