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 20 import dataclasses
21 21 from functools import partial
22 22 import itertools
23   -import csv
24 23 import time
25 24  
26 25 import nibabel as nb
... ... @@ -208,6 +207,13 @@ class InnerFoldPanel(wx.Panel):
208 207 leftSpacing=0, rightSpacing=0)
209 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 217 # Check box for camera update in volume rendering during navigation
212 218 tooltip = wx.ToolTip(_("Update camera in volume"))
213 219 checkcamera = wx.CheckBox(self, -1, _('Vol. camera'))
... ... @@ -1104,8 +1110,8 @@ class MarkersPanel(wx.Panel):
1104 1110 x_seed : float = 0
1105 1111 y_seed : float = 0
1106 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 1116 # x, y, z, alpha, beta, gamma can be jointly accessed as coord
1111 1117 @property
... ... @@ -1135,27 +1141,44 @@ class MarkersPanel(wx.Panel):
1135 1141 self.x_seed, self.y_seed, self.z_seed = new_seed
1136 1142  
1137 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 1146 res = [field.name for field in dataclasses.fields(cls)]
1141 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 1159 # Add world coordinates (in addition to the internal ones).
1150 1160 position_world, orientation_world = imagedata_utils.convert_invesalius_to_world(
1151 1161 position=[self.x, self.y, self.z],
1152 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 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 1182 def __init__(self, parent):
1160 1183 wx.Panel.__init__(self, parent)
1161 1184 try:
... ... @@ -1176,9 +1199,7 @@ class MarkersPanel(wx.Panel):
1176 1199  
1177 1200 self.marker_colour = const.MARKER_COLOUR
1178 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 1204 # Change marker size
1184 1205 spin_size = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23))
... ... @@ -1233,6 +1254,7 @@ class MarkersPanel(wx.Panel):
1233 1254 self.lc.InsertColumn(3, 'Z')
1234 1255 self.lc.InsertColumn(4, 'ID')
1235 1256 self.lc.InsertColumn(5, 'Target')
  1257 + self.lc.InsertColumn(6, 'Session')
1236 1258  
1237 1259 self.lc.SetColumnWidth(0, 28)
1238 1260 self.lc.SetColumnWidth(1, 50)
... ... @@ -1240,6 +1262,7 @@ class MarkersPanel(wx.Panel):
1240 1262 self.lc.SetColumnWidth(3, 50)
1241 1263 self.lc.SetColumnWidth(4, 60)
1242 1264 self.lc.SetColumnWidth(5, 60)
  1265 + self.lc.SetColumnWidth(5, 50)
1243 1266  
1244 1267 self.lc.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnMouseRightDown)
1245 1268 self.lc.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemBlink)
... ... @@ -1264,6 +1287,7 @@ class MarkersPanel(wx.Panel):
1264 1287 Publisher.subscribe(self.CreateMarker, 'Create marker')
1265 1288 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
1266 1289 Publisher.subscribe(self.UpdateSeedCoordinates, 'Update tracts')
  1290 + Publisher.subscribe(self.OnChangeCurrentSession, 'Current session changed')
1267 1291  
1268 1292 def __find_target_marker(self):
1269 1293 """Return the index of the marker currently selected as target (there
... ... @@ -1310,13 +1334,13 @@ class MarkersPanel(wx.Panel):
1310 1334  
1311 1335 # Unset the previous target
1312 1336 if prev_idx != -1:
1313   - self.markers[prev_idx].is_target = 0
  1337 + self.markers[prev_idx].is_target = False
1314 1338 self.lc.SetItemBackgroundColour(prev_idx, 'white')
1315 1339 Publisher.sendMessage('Set target transparency', status=False, index=prev_idx)
1316 1340 self.lc.SetItem(prev_idx, 5, "")
1317 1341  
1318 1342 # Set the new target
1319   - self.markers[idx].is_target = 1
  1343 + self.markers[idx].is_target = True
1320 1344 self.lc.SetItemBackgroundColour(idx, 'RED')
1321 1345 self.lc.SetItem(idx, 5, _("Yes"))
1322 1346  
... ... @@ -1347,7 +1371,7 @@ class MarkersPanel(wx.Panel):
1347 1371 def OnMouseRightDown(self, evt):
1348 1372 # TODO: Enable the "Set as target" only when target is created with registered object
1349 1373 menu_id = wx.Menu()
1350   - edit_id = menu_id.Append(0, _('Edit ID'))
  1374 + edit_id = menu_id.Append(0, _('Edit label'))
1351 1375 menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerLabel, edit_id)
1352 1376 color_id = menu_id.Append(2, _('Edit color'))
1353 1377 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetColor, color_id)
... ... @@ -1472,20 +1496,20 @@ class MarkersPanel(wx.Panel):
1472 1496 wx.MessageBox(_("Unknown version of the markers file."), _("InVesalius 3"))
1473 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 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 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 1508 if marker.label in self.__list_fiducial_labels():
1485 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 1513 if marker.is_target:
1490 1514 self.__set_marker_as_target(len(self.markers)-1)
1491 1515  
... ... @@ -1521,9 +1545,8 @@ class MarkersPanel(wx.Panel):
1521 1545 try:
1522 1546 with open(filename, 'w', newline='') as file:
1523 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 1550 file.close()
1528 1551 except:
1529 1552 wx.MessageBox(_("Error writing markers file."), _("InVesalius 3"))
... ... @@ -1535,7 +1558,10 @@ class MarkersPanel(wx.Panel):
1535 1558 def OnSelectSize(self, evt, ctrl):
1536 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 1565 new_marker = self.Marker()
1540 1566 new_marker.coord = coord or self.current_coord
1541 1567 new_marker.colour = colour or self.marker_colour
... ... @@ -1543,6 +1569,7 @@ class MarkersPanel(wx.Panel):
1543 1569 new_marker.label = label
1544 1570 new_marker.is_target = is_target
1545 1571 new_marker.seed = seed or self.current_seed
  1572 + new_marker.session_id = session_id or self.current_session
1546 1573  
1547 1574 # Note that ball_id is zero-based, so we assign it len(self.markers) before the new marker is added
1548 1575 Publisher.sendMessage('Add marker', ball_id=len(self.markers),
... ... @@ -1557,7 +1584,8 @@ class MarkersPanel(wx.Panel):
1557 1584 self.lc.SetItem(num_items, 1, str(round(new_marker.x, 2)))
1558 1585 self.lc.SetItem(num_items, 2, str(round(new_marker.y, 2)))
1559 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 1589 self.lc.EnsureVisible(num_items)
1562 1590  
1563 1591 class DbsPanel(wx.Panel):
... ... @@ -2011,6 +2039,30 @@ class TractographyPanel(wx.Panel):
2011 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 2066 class InputAttributes(object):
2015 2067 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python
2016 2068 def __init__(self, *initial_data, **kwargs):
... ...