Commit 9460b9cdbce8edd6a14117565a551762c67a12ab
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): | ... | ... |