driftlessmap 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. driftlessmap/__init__.py +16 -0
  2. driftlessmap/__main__.py +5 -0
  3. driftlessmap/about.py +24 -0
  4. driftlessmap/allen_downloader.py +908 -0
  5. driftlessmap/app.py +8085 -0
  6. driftlessmap/atlas_downloader.py +370 -0
  7. driftlessmap/atlas_loader.py +526 -0
  8. driftlessmap/atlas_processor.py +784 -0
  9. driftlessmap/atlas_transform.py +111 -0
  10. driftlessmap/atlas_view.py +1435 -0
  11. driftlessmap/cell_detection.py +27 -0
  12. driftlessmap/coordinate_validation.py +19 -0
  13. driftlessmap/czi_reader.py +236 -0
  14. driftlessmap/data/WHS_atlas_labels.pkl +0 -0
  15. driftlessmap/data/allen_mice_atlas_labels.pkl +0 -0
  16. driftlessmap/data/atlas_labels.pkl +0 -0
  17. driftlessmap/data/query.csv +1328 -0
  18. driftlessmap/download_utils.py +85 -0
  19. driftlessmap/herbs.png +0 -0
  20. driftlessmap/icons/backward.svg +15 -0
  21. driftlessmap/icons/dot.svg +11 -0
  22. driftlessmap/icons/down-arrow.svg +10 -0
  23. driftlessmap/icons/fast_backward.svg +14 -0
  24. driftlessmap/icons/fast_forward.svg +14 -0
  25. driftlessmap/icons/forward.svg +14 -0
  26. driftlessmap/icons/layers/add.png +0 -0
  27. driftlessmap/icons/layers/ai.png +0 -0
  28. driftlessmap/icons/layers/eye_off.png +0 -0
  29. driftlessmap/icons/layers/eye_on.png +0 -0
  30. driftlessmap/icons/layers/eye_white.png +0 -0
  31. driftlessmap/icons/layers/hi.png +0 -0
  32. driftlessmap/icons/layers/trash.png +0 -0
  33. driftlessmap/icons/object.svg +22 -0
  34. driftlessmap/icons/sidebar/add.png +0 -0
  35. driftlessmap/icons/sidebar/atlascontrol.png +0 -0
  36. driftlessmap/icons/sidebar/atlascontrol.svg +32 -0
  37. driftlessmap/icons/sidebar/bnd.svg +14 -0
  38. driftlessmap/icons/sidebar/c_section.png +0 -0
  39. driftlessmap/icons/sidebar/c_section2.png +0 -0
  40. driftlessmap/icons/sidebar/cell.svg +24 -0
  41. driftlessmap/icons/sidebar/check.svg +9 -0
  42. driftlessmap/icons/sidebar/compare.svg +18 -0
  43. driftlessmap/icons/sidebar/contour.svg +14 -0
  44. driftlessmap/icons/sidebar/h_section.png +0 -0
  45. driftlessmap/icons/sidebar/info.svg +14 -0
  46. driftlessmap/icons/sidebar/layers.png +0 -0
  47. driftlessmap/icons/sidebar/layers.svg +23 -0
  48. driftlessmap/icons/sidebar/line.svg +10 -0
  49. driftlessmap/icons/sidebar/link.svg +18 -0
  50. driftlessmap/icons/sidebar/link_off.svg +18 -0
  51. driftlessmap/icons/sidebar/merge.svg +11 -0
  52. driftlessmap/icons/sidebar/object.svg +13 -0
  53. driftlessmap/icons/sidebar/probe.png +0 -0
  54. driftlessmap/icons/sidebar/probe.svg +23 -0
  55. driftlessmap/icons/sidebar/rotation_horizontal.svg +14 -0
  56. driftlessmap/icons/sidebar/rotation_vertical.svg +14 -0
  57. driftlessmap/icons/sidebar/s_section.png +0 -0
  58. driftlessmap/icons/sidebar/tool.png +0 -0
  59. driftlessmap/icons/sidebar/tool.svg +19 -0
  60. driftlessmap/icons/sidebar/trash.png +0 -0
  61. driftlessmap/icons/sidebar/tree_checked.svg +10 -0
  62. driftlessmap/icons/sidebar/treeview.png +0 -0
  63. driftlessmap/icons/sidebar/treeview.svg +10 -0
  64. driftlessmap/icons/sidebar/treeview2.png +0 -0
  65. driftlessmap/icons/sidebar/virus.svg +24 -0
  66. driftlessmap/icons/tdown.svg +16 -0
  67. driftlessmap/icons/toolbar/accept.svg +18 -0
  68. driftlessmap/icons/toolbar/accept2.svg +24 -0
  69. driftlessmap/icons/toolbar/aim.svg +44 -0
  70. driftlessmap/icons/toolbar/aim_not.svg +28 -0
  71. driftlessmap/icons/toolbar/anchor.svg +14 -0
  72. driftlessmap/icons/toolbar/anticlockwise_rotation.svg +12 -0
  73. driftlessmap/icons/toolbar/atlas_icon.png +0 -0
  74. driftlessmap/icons/toolbar/boundary_register.svg +18 -0
  75. driftlessmap/icons/toolbar/cancel.svg +32 -0
  76. driftlessmap/icons/toolbar/cell_select.svg +22 -0
  77. driftlessmap/icons/toolbar/cell_select_not.svg +12 -0
  78. driftlessmap/icons/toolbar/check.svg +9 -0
  79. driftlessmap/icons/toolbar/clockwise_rotation.svg +11 -0
  80. driftlessmap/icons/toolbar/closed_path.svg +16 -0
  81. driftlessmap/icons/toolbar/closed_path2 copy.svg +19 -0
  82. driftlessmap/icons/toolbar/closed_path2.svg +20 -0
  83. driftlessmap/icons/toolbar/eraser.png +0 -0
  84. driftlessmap/icons/toolbar/eraser.svg +19 -0
  85. driftlessmap/icons/toolbar/eye.svg +17 -0
  86. driftlessmap/icons/toolbar/eye_closed.svg +19 -0
  87. driftlessmap/icons/toolbar/fill.svg +12 -0
  88. driftlessmap/icons/toolbar/gps.svg +15 -0
  89. driftlessmap/icons/toolbar/h_flip.png +0 -0
  90. driftlessmap/icons/toolbar/handle.png +0 -0
  91. driftlessmap/icons/toolbar/image_icon.png +0 -0
  92. driftlessmap/icons/toolbar/image_icon.svg +25 -0
  93. driftlessmap/icons/toolbar/info.svg +14 -0
  94. driftlessmap/icons/toolbar/inpart.png +0 -0
  95. driftlessmap/icons/toolbar/inpart.svg +17 -0
  96. driftlessmap/icons/toolbar/lasso.png +0 -0
  97. driftlessmap/icons/toolbar/lasso.svg +14 -0
  98. driftlessmap/icons/toolbar/left90.png +0 -0
  99. driftlessmap/icons/toolbar/line.svg +10 -0
  100. driftlessmap/icons/toolbar/line_sites.svg +11 -0
  101. driftlessmap/icons/toolbar/linear_silicon.png +0 -0
  102. driftlessmap/icons/toolbar/linear_silicon.svg +88 -0
  103. driftlessmap/icons/toolbar/list.svg +14 -0
  104. driftlessmap/icons/toolbar/location.svg +14 -0
  105. driftlessmap/icons/toolbar/magic-wand.svg +16 -0
  106. driftlessmap/icons/toolbar/magic_white.png +0 -0
  107. driftlessmap/icons/toolbar/mask.svg +10 -0
  108. driftlessmap/icons/toolbar/match.svg +20 -0
  109. driftlessmap/icons/toolbar/matchbnd.svg +18 -0
  110. driftlessmap/icons/toolbar/matching.svg +36 -0
  111. driftlessmap/icons/toolbar/merge.svg +11 -0
  112. driftlessmap/icons/toolbar/move_down.png +0 -0
  113. driftlessmap/icons/toolbar/move_left.png +0 -0
  114. driftlessmap/icons/toolbar/move_right.png +0 -0
  115. driftlessmap/icons/toolbar/move_up.png +0 -0
  116. driftlessmap/icons/toolbar/moving.png +0 -0
  117. driftlessmap/icons/toolbar/multi-probe.svg +119 -0
  118. driftlessmap/icons/toolbar/multi_pencil.svg +23 -0
  119. driftlessmap/icons/toolbar/multi_probe.png +0 -0
  120. driftlessmap/icons/toolbar/object.svg +13 -0
  121. driftlessmap/icons/toolbar/open_path.svg +22 -0
  122. driftlessmap/icons/toolbar/outpart.png +0 -0
  123. driftlessmap/icons/toolbar/outpart.svg +26 -0
  124. driftlessmap/icons/toolbar/pencil.png +0 -0
  125. driftlessmap/icons/toolbar/pencil.svg +19 -0
  126. driftlessmap/icons/toolbar/plasso.png +0 -0
  127. driftlessmap/icons/toolbar/probe.svg +19 -0
  128. driftlessmap/icons/toolbar/radar.svg +32 -0
  129. driftlessmap/icons/toolbar/rotation.svg +45 -0
  130. driftlessmap/icons/toolbar/rotation_clockwise.svg +18 -0
  131. driftlessmap/icons/toolbar/rotation_counter_clockwise.svg +18 -0
  132. driftlessmap/icons/toolbar/ruler.svg +27 -0
  133. driftlessmap/icons/toolbar/separate_sites.svg +13 -0
  134. driftlessmap/icons/toolbar/single_pencil.svg +16 -0
  135. driftlessmap/icons/toolbar/toa.svg +11 -0
  136. driftlessmap/icons/toolbar/toa_delete.svg +36 -0
  137. driftlessmap/icons/toolbar/toaa.svg +11 -0
  138. driftlessmap/icons/toolbar/toh.svg +11 -0
  139. driftlessmap/icons/toolbar/toh_delete.svg +36 -0
  140. driftlessmap/icons/toolbar/tohh.svg +11 -0
  141. driftlessmap/icons/toolbar/trans.png +0 -0
  142. driftlessmap/icons/toolbar/trans.svg +16 -0
  143. driftlessmap/icons/toolbar/triangulation.svg +10 -0
  144. driftlessmap/icons/toolbar/two_window.png +0 -0
  145. driftlessmap/icons/toolbar/unmerge.svg +16 -0
  146. driftlessmap/icons/toolbar/v_flip.png +0 -0
  147. driftlessmap/icons/toolbar/virus.svg +24 -0
  148. driftlessmap/icons/toolbar/virus_register.svg +31 -0
  149. driftlessmap/icons/toolbar/vis2d.svg +29 -0
  150. driftlessmap/icons/toolbar/window2.png +0 -0
  151. driftlessmap/icons/toolbar/window3.png +0 -0
  152. driftlessmap/icons/toolbar/window4.png +0 -0
  153. driftlessmap/icons/tree_close.svg +14 -0
  154. driftlessmap/icons/tree_open.svg +14 -0
  155. driftlessmap/icons/up-arrow.svg +10 -0
  156. driftlessmap/image_curves.py +579 -0
  157. driftlessmap/image_reader.py +186 -0
  158. driftlessmap/image_stacks.py +359 -0
  159. driftlessmap/image_view.py +646 -0
  160. driftlessmap/images_reader.py +5 -0
  161. driftlessmap/label_tree.py +278 -0
  162. driftlessmap/layer_validation.py +25 -0
  163. driftlessmap/layers_control.py +554 -0
  164. driftlessmap/main_window.ui +883 -0
  165. driftlessmap/movable_points.py +254 -0
  166. driftlessmap/obj_items.py +122 -0
  167. driftlessmap/object_control.py +1762 -0
  168. driftlessmap/persistence.py +376 -0
  169. driftlessmap/popup_message.py +16 -0
  170. driftlessmap/probe_csv.py +319 -0
  171. driftlessmap/probe_reconstruction.py +409 -0
  172. driftlessmap/probe_utiles.py +1442 -0
  173. driftlessmap/qss/atlas_view_group_box.qss +15 -0
  174. driftlessmap/qss/box_label.qss +7 -0
  175. driftlessmap/qss/channel_selector.qss +19 -0
  176. driftlessmap/qss/color_combo.qss +121 -0
  177. driftlessmap/qss/decor_label.qss +9 -0
  178. driftlessmap/qss/dialogs.qss +11 -0
  179. driftlessmap/qss/hidden_line_edit.qss +5 -0
  180. driftlessmap/qss/label_tree.qss +123 -0
  181. driftlessmap/qss/main_window.qss +243 -0
  182. driftlessmap/qss/menu_bar.qss +42 -0
  183. driftlessmap/qss/multi_handle_slider.qss +14 -0
  184. driftlessmap/qss/obj_ctrl_bottom_button.qss +29 -0
  185. driftlessmap/qss/object_text_button.qss +19 -0
  186. driftlessmap/qss/page_control.qss +36 -0
  187. driftlessmap/qss/rotation_button.qss +9 -0
  188. driftlessmap/qss/side_bar.qss +23 -0
  189. driftlessmap/qss/spinbox.qss +106 -0
  190. driftlessmap/qss/tabs.qss +211 -0
  191. driftlessmap/qss/tool_bar.qss +77 -0
  192. driftlessmap/resources.py +34 -0
  193. driftlessmap/roi_analysis.py +375 -0
  194. driftlessmap/run_driftlessmap.py +4 -0
  195. driftlessmap/slice_stacks.py +268 -0
  196. driftlessmap/slice_validation.py +20 -0
  197. driftlessmap/toolbox.py +569 -0
  198. driftlessmap/triangulation.py +517 -0
  199. driftlessmap/triangulation_points.py +108 -0
  200. driftlessmap/user_settings.py +92 -0
  201. driftlessmap/uuuuuu.py +1168 -0
  202. driftlessmap/version.py +3 -0
  203. driftlessmap/widgets_utils.py +140 -0
  204. driftlessmap/wtiles.py +715 -0
  205. driftlessmap-1.1.0.dist-info/METADATA +186 -0
  206. driftlessmap-1.1.0.dist-info/RECORD +210 -0
  207. driftlessmap-1.1.0.dist-info/WHEEL +5 -0
  208. driftlessmap-1.1.0.dist-info/entry_points.txt +2 -0
  209. driftlessmap-1.1.0.dist-info/licenses/LICENSE.txt +23 -0
  210. driftlessmap-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1762 @@
1
+ import os
2
+ import sys
3
+ import numpy as np
4
+ from random import randint
5
+ import pyqtgraph as pg
6
+ from PyQt6.QtWidgets import *
7
+ from PyQt6.QtGui import *
8
+ from PyQt6.QtCore import *
9
+ from .wtiles import QDoubleButton
10
+ from .probe_csv import write_probe_csv_files
11
+ from .roi_analysis import write_roi_csv
12
+ from .uuuuuu import read_qss_file
13
+ from .resources import resource_path
14
+
15
+
16
+ eye_button_style = """
17
+ border-left: None;
18
+ border-right: 1px solid gray;
19
+ border-top: None;
20
+ border-bottom: None;
21
+ border-radius: 0px;
22
+ background: transparent;
23
+ """
24
+
25
+
26
+ line_edit_style = """
27
+ QLineEdit {
28
+ background: transparent;
29
+ border: 1px solid green;
30
+ border-radius:0px;
31
+ text-align:left;
32
+ color: white;
33
+ padding-left: 5px;
34
+ margin: 0px;
35
+ height: 40px;
36
+ width: 220px;
37
+ }
38
+ """
39
+
40
+
41
+ class ObjectTextButton(QPushButton):
42
+ def __init__(self):
43
+ QPushButton.__init__(self)
44
+ btn_style = read_qss_file("qss/object_text_button.qss")
45
+ self.setStyleSheet(btn_style)
46
+ self.setFixedSize(QSize(150, 40))
47
+
48
+
49
+ class CompareWindow(QDialog):
50
+ def __init__(self, obj_names, obj_data):
51
+ super().__init__()
52
+
53
+ self.setWindowTitle("Compare Information Window")
54
+
55
+ n_object = len(obj_names)
56
+ # print(n_object)
57
+ # print(obj_data)
58
+ all_label_names = []
59
+ all_label_acronym = []
60
+ all_label_color = []
61
+ all_label_channels = []
62
+ for i in range(n_object):
63
+ all_label_names.append(obj_data[i]["label_name"])
64
+ all_label_acronym.append(obj_data[i]["label_acronym"])
65
+ all_label_color.append(obj_data[i]["label_color"])
66
+ all_label_channels.append(obj_data[i]["region_sites"])
67
+ all_label_names = np.concatenate(all_label_names)
68
+ all_label_acronym = np.concatenate(all_label_acronym)
69
+ all_label_color = np.concatenate(all_label_color)
70
+ all_label_channels = np.concatenate(all_label_channels)
71
+
72
+ # print(all_label_names)
73
+ # print(all_label_acronym)
74
+ # print(all_label_color)
75
+ # print(all_label_channels)
76
+
77
+ unique_label_names = np.unique(all_label_names)
78
+ unique_label_acronym = []
79
+ unique_label_color = []
80
+ unique_label_channels = []
81
+ for i in range(len(unique_label_names)):
82
+ c_ind = np.where(all_label_names == unique_label_names[i])[0]
83
+ unique_label_acronym.append(all_label_acronym[c_ind[0]])
84
+ unique_label_color.append(all_label_color[c_ind[0]])
85
+ unique_label_channels.append(np.sum(all_label_channels[c_ind]))
86
+
87
+ layout = QVBoxLayout()
88
+ full_name = obj_names[0]
89
+ for i in range(1, n_object):
90
+ full_name = full_name + ", "
91
+ full_name = full_name + obj_names[i]
92
+
93
+ self.label = QLabel("Compare {}".format(full_name))
94
+ color = QColor(128, 128, 128, 128)
95
+ label_style = "QLabel {background-color: " + color.name() + "; font-size: 20px}"
96
+ self.label.setStyleSheet(label_style)
97
+
98
+ sec_group = QGroupBox()
99
+ slayout = QGridLayout(sec_group)
100
+ lb1 = QLabel("Brain Region")
101
+ lb2 = QLabel("Acronym")
102
+ lb3 = QLabel("Channels")
103
+ lb4 = QLabel("Color")
104
+ slayout.addWidget(lb1, 0, 0, 1, 1)
105
+ slayout.addWidget(lb2, 0, 1, 1, 1)
106
+ slayout.addWidget(lb3, 0, 2, 1, 1)
107
+ slayout.addWidget(lb4, 0, 3, 1, 1)
108
+
109
+ channels = np.ravel(unique_label_channels).astype(int)
110
+
111
+ for i in range(len(unique_label_names)):
112
+ slayout.addWidget(QLabel(unique_label_names[i]), i + 1, 0, 1, 1)
113
+ slayout.addWidget(QLabel(unique_label_acronym[i]), i + 1, 1, 1, 1)
114
+ slayout.addWidget(QLabel(str(channels[i])), i + 1, 2, 1, 1)
115
+ clb = QLabel()
116
+ da_color = QColor(
117
+ unique_label_color[i][0],
118
+ unique_label_color[i][1],
119
+ unique_label_color[i][2],
120
+ 255,
121
+ ).name()
122
+ clb.setStyleSheet(
123
+ "QLabel {background-color: " + da_color + "; width: 20px; height: 20px}"
124
+ )
125
+ slayout.addWidget(clb, i + 1, 3, 1, 1)
126
+
127
+ # make plot data
128
+ plot_frame = QFrame()
129
+ plot_frame.setMaximumWidth(300)
130
+ plot_frame.setMinimumWidth(300)
131
+ view_layout = QHBoxLayout(plot_frame)
132
+ view_layout.setSpacing(0)
133
+ view_layout.setContentsMargins(0, 0, 0, 0)
134
+
135
+ w = pg.GraphicsLayoutWidget()
136
+ view = w.addViewBox()
137
+ view.setAspectLocked()
138
+
139
+ view_layout.addWidget(w)
140
+
141
+ for k in range(n_object):
142
+ vis_data = obj_data[k]["vis_data"]
143
+ n_column = len(vis_data)
144
+
145
+ plot_center_x_val = k * 10 + 0.5 * n_column
146
+
147
+ probe_type_name = obj_data[k]["probe_type_name"]
148
+ # draw tips
149
+ if probe_type_name != "Tetrode":
150
+ da_tip_loc = np.array(
151
+ [[0, 0], [0.5 * n_column, -2 * n_column], [n_column, 0]]
152
+ )
153
+ da_tip_loc = da_tip_loc + np.array([plot_center_x_val, 0])
154
+ tips = pg.PlotDataItem(
155
+ da_tip_loc,
156
+ connect="all",
157
+ fillLevel=0,
158
+ fillBrush=pg.mkBrush(color=(128, 128, 128)),
159
+ )
160
+ view.addItem(tips)
161
+
162
+ # draw area
163
+ region_label = obj_data[k]["region_label"]
164
+
165
+ group_color = obj_data[k]["label_color"]
166
+ sites_label = obj_data[k]["sites_label"]
167
+ for i in range(n_column):
168
+ group_id = vis_data[i]["group_id"].astype(int)
169
+ start_loc = vis_data[i]["start_loc"]
170
+ end_loc = vis_data[i]["end_loc"]
171
+ sites_loc_column = vis_data[i]["sites"]
172
+
173
+ for j in range(len(group_id)):
174
+ area_data = np.array([[i, end_loc[j]], [i + 1, end_loc[j]]])
175
+ area_data = area_data + np.array([plot_center_x_val, 0])
176
+ da_item = pg.PlotDataItem(
177
+ area_data,
178
+ fillLevel=start_loc[j],
179
+ fillBrush=group_color[group_id[j]],
180
+ pen=None,
181
+ )
182
+ view.addItem(da_item)
183
+
184
+ sites_color = []
185
+ for j in range(len(sites_label[i])):
186
+ color_ind = np.where(region_label == sites_label[i][j])[0][0]
187
+ sites_color.append(group_color[color_ind])
188
+
189
+ pnt_data = np.stack(
190
+ [np.repeat(i + 0.5, len(sites_loc_column)), sites_loc_column],
191
+ axis=1,
192
+ )
193
+ pnt_data = pnt_data + np.array([plot_center_x_val, 0])
194
+ da_item = pg.ScatterPlotItem(
195
+ pos=pnt_data,
196
+ pen=(128, 128, 128, 128),
197
+ brush=sites_color,
198
+ symbol="s",
199
+ size=3,
200
+ hoverSize=8,
201
+ )
202
+ view.addItem(da_item)
203
+
204
+ channel_info_frame = QFrame()
205
+ channel_info_layout = QHBoxLayout(channel_info_frame)
206
+ channel_info_layout.setContentsMargins(0, 0, 0, 0)
207
+ channel_info_layout.setSpacing(10)
208
+ channel_info_layout.addWidget(plot_frame)
209
+ channel_info_layout.addWidget(sec_group)
210
+
211
+ # ok button, used to close window
212
+ ok_btn = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
213
+ ok_btn.accepted.connect(self.accept)
214
+
215
+ # add widget to layout
216
+ layout.addWidget(self.label)
217
+ layout.addWidget(channel_info_frame)
218
+ layout.addWidget(ok_btn)
219
+ self.setLayout(layout)
220
+
221
+ def accept(self) -> None:
222
+ self.close()
223
+
224
+
225
+ class CellsInfoWindow(QDialog):
226
+ def __init__(self, name, data):
227
+ super().__init__()
228
+
229
+ self.setWindowTitle("Cell Information Window")
230
+
231
+ layout = QVBoxLayout()
232
+ self.label = QLabel(name)
233
+ color = QColor(
234
+ data["vis_color"][0],
235
+ data["vis_color"][1],
236
+ data["vis_color"][2],
237
+ data["vis_color"][3],
238
+ )
239
+ label_style = "QLabel {background-color: " + color.name() + "; font-size: 20px}"
240
+ self.label.setStyleSheet(label_style)
241
+
242
+ data_mat = data["data"][0]
243
+ for i in range(1, len(data["data"])):
244
+ data_mat = np.vstack([data_mat, data["data"][i]])
245
+
246
+ sec_group = QGroupBox("Total Count: {}".format(len(data_mat)))
247
+ slayout = QGridLayout(sec_group)
248
+ lb1 = QLabel("Brain Region")
249
+ lb2 = QLabel("Acronym")
250
+ lb3 = QLabel("Color")
251
+ lb4 = QLabel("Count")
252
+ slayout.addWidget(lb1, 0, 0, 1, 1)
253
+ slayout.addWidget(lb2, 0, 1, 1, 1)
254
+ slayout.addWidget(lb3, 0, 2, 1, 1)
255
+ slayout.addWidget(lb4, 0, 3, 1, 1)
256
+
257
+ for i in range(len(data["label_name"])):
258
+ slayout.addWidget(QLabel(data["label_name"][i]), i + 1, 0, 1, 1)
259
+ slayout.addWidget(QLabel(data["label_acronym"][i]), i + 1, 1, 1, 1)
260
+ clb = QLabel()
261
+ da_color = QColor(
262
+ data["label_color"][i][0],
263
+ data["label_color"][i][1],
264
+ data["label_color"][i][2],
265
+ 255,
266
+ ).name()
267
+ clb.setStyleSheet(
268
+ "QLabel {background-color: " + da_color + "; width: 20px; height: 20px}"
269
+ )
270
+ slayout.addWidget(clb, i + 1, 2, 1, 1)
271
+ slayout.addWidget(QLabel(str(data["region_count"][i])), i + 1, 3, 1, 1)
272
+
273
+ # ok button, used to close window
274
+ ok_btn = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
275
+ ok_btn.accepted.connect(self.accept)
276
+
277
+ # add widget to layout
278
+ layout.addWidget(self.label)
279
+ layout.addWidget(sec_group)
280
+ layout.addWidget(ok_btn)
281
+ self.setLayout(layout)
282
+
283
+ def accept(self) -> None:
284
+ self.close()
285
+
286
+ def set_probe_color(self, color):
287
+ self.label.setStyleSheet("QLabel {background-color: " + color + ";}")
288
+
289
+
290
+ class DrawingInfoWindow(QDialog):
291
+ def __init__(self, name, data):
292
+ super().__init__()
293
+
294
+ self.object_name = name
295
+ self.roi_info = data
296
+ self.setWindowTitle("Drawing ROI Information")
297
+ self.setMinimumWidth(720)
298
+
299
+ layout = QVBoxLayout()
300
+ title = QLabel(name)
301
+ title.setStyleSheet("QLabel {font-size: 20px; font-weight: bold;}")
302
+ layout.addWidget(title)
303
+
304
+ summary_group = QGroupBox("ROI summary")
305
+ summary_layout = QFormLayout(summary_group)
306
+ mode = str(data["plot_mode"]).capitalize()
307
+ summary_layout.addRow("Drawing:", QLabel(mode))
308
+ summary_layout.addRow(
309
+ "Sampled points:", QLabel("{:,}".format(data["point_count"]))
310
+ )
311
+ if data["metric_name"] == "sampled_area_mm2":
312
+ metric_label = "Sampled area:"
313
+ metric_text = "{:.4f} mm²".format(data["metric_value"])
314
+ else:
315
+ metric_label = "Line length:"
316
+ metric_text = "{:.3f} mm".format(data["metric_value"])
317
+ summary_layout.addRow(metric_label, QLabel(metric_text))
318
+ summary_layout.addRow(
319
+ "Coordinate basis:", QLabel(data["coordinate_basis"])
320
+ )
321
+
322
+ coordinate_summary = data["coordinate_summary"]
323
+ centroid_parts = []
324
+ for axis in ("AP", "ML"):
325
+ if axis in coordinate_summary:
326
+ centroid_parts.append(
327
+ "{} {:+.3f} mm".format(
328
+ axis, coordinate_summary[axis]["centroid"]
329
+ )
330
+ )
331
+ if data["ground_truth"] is None and "DV" in coordinate_summary:
332
+ centroid_parts.append(
333
+ "DV {:+.3f} mm".format(
334
+ coordinate_summary["DV"]["centroid"]
335
+ )
336
+ )
337
+ summary_layout.addRow("Centroid:", QLabel(" | ".join(centroid_parts)))
338
+
339
+ for axis in ("AP", "ML"):
340
+ if axis not in coordinate_summary:
341
+ continue
342
+ axis_data = coordinate_summary[axis]
343
+ summary_layout.addRow(
344
+ "{} range:".format(axis),
345
+ QLabel(
346
+ "{:+.3f} to {:+.3f} mm".format(
347
+ axis_data["min"], axis_data["max"]
348
+ )
349
+ ),
350
+ )
351
+
352
+ depth = data.get("surface_depth_summary")
353
+ if depth is None:
354
+ depth_text = "Unavailable for this ROI"
355
+ else:
356
+ depth_text = (
357
+ "mean {:.3f} mm | range {:.3f}–{:.3f} mm "
358
+ "({:,} points)"
359
+ ).format(
360
+ depth["mean"], depth["min"], depth["max"], depth["count"]
361
+ )
362
+ summary_layout.addRow("Depth from surface:", QLabel(depth_text))
363
+ layout.addWidget(summary_group)
364
+
365
+ note = QLabel()
366
+ note.setWordWrap(True)
367
+ if data["ground_truth"] is False:
368
+ note.setText(
369
+ "Allen Bregma coordinates are estimates, not ground truth. "
370
+ "Depth is measured from the local dorsal brain surface. The "
371
+ "affine DV estimate is included only in the CSV and is marked "
372
+ "as unsuitable for targeting."
373
+ )
374
+ else:
375
+ note.setText(
376
+ "AP, ML, and DV are relative to the Bregma voxel configured "
377
+ "for this atlas. Depth is measured from the local dorsal "
378
+ "brain surface."
379
+ )
380
+ layout.addWidget(note)
381
+
382
+ regions = data.get("regions", [])
383
+ region_group = QGroupBox("Brain-region distribution")
384
+ region_layout = QVBoxLayout(region_group)
385
+ self.region_table = QTableWidget(len(regions), 6)
386
+ self.region_table.setHorizontalHeaderLabels(
387
+ ["ID", "Region", "Acronym", "Samples", "%", "Color"]
388
+ )
389
+ self.region_table.setEditTriggers(
390
+ QAbstractItemView.EditTrigger.NoEditTriggers
391
+ )
392
+ self.region_table.setSelectionBehavior(
393
+ QAbstractItemView.SelectionBehavior.SelectRows
394
+ )
395
+ self.region_table.verticalHeader().setVisible(False)
396
+ for row_index, region in enumerate(regions):
397
+ values = (
398
+ str(region["label_id"]),
399
+ region["name"],
400
+ region["acronym"],
401
+ "{:,}".format(region["count"]),
402
+ "{:.2f}".format(region["percentage"]),
403
+ )
404
+ for column, value in enumerate(values):
405
+ self.region_table.setItem(
406
+ row_index, column, QTableWidgetItem(value)
407
+ )
408
+ color_item = QTableWidgetItem()
409
+ color_item.setBackground(QColor(*region["color"]))
410
+ self.region_table.setItem(row_index, 5, color_item)
411
+ self.region_table.horizontalHeader().setSectionResizeMode(
412
+ 1, QHeaderView.ResizeMode.Stretch
413
+ )
414
+ self.region_table.resizeColumnsToContents()
415
+ self.region_table.setMaximumHeight(280)
416
+ region_layout.addWidget(self.region_table)
417
+ layout.addWidget(region_group)
418
+
419
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
420
+ self.export_btn = buttons.addButton(
421
+ "Export coordinates as CSV",
422
+ QDialogButtonBox.ButtonRole.ActionRole,
423
+ )
424
+ self.export_btn.clicked.connect(self.export_coordinates)
425
+ buttons.accepted.connect(self.accept)
426
+ layout.addWidget(buttons)
427
+ self.setLayout(layout)
428
+
429
+ def export_coordinates(self):
430
+ safe_name = "".join(
431
+ character if character.isalnum() or character in "-_" else "_"
432
+ for character in self.object_name
433
+ )
434
+ file_path, _ = QFileDialog.getSaveFileName(
435
+ self,
436
+ "Export drawing ROI coordinates",
437
+ "{}_coordinates.csv".format(safe_name or "drawing_roi"),
438
+ "CSV files (*.csv)",
439
+ )
440
+ if not file_path:
441
+ return
442
+ try:
443
+ write_roi_csv(file_path, self.roi_info)
444
+ except OSError as exc:
445
+ QMessageBox.warning(
446
+ self,
447
+ "Export failed",
448
+ "Could not save the ROI coordinates:\n{}".format(exc),
449
+ )
450
+
451
+ def accept(self) -> None:
452
+ self.close()
453
+
454
+
455
+ class VirusInfoWindow(QDialog):
456
+ def __init__(self, name, data):
457
+ super().__init__()
458
+
459
+ self.setWindowTitle("Virus Information Window")
460
+
461
+ layout = QVBoxLayout()
462
+ self.label = QLabel(name)
463
+ color = QColor(
464
+ data["vis_color"][0],
465
+ data["vis_color"][1],
466
+ data["vis_color"][2],
467
+ data["vis_color"][3],
468
+ )
469
+ label_style = "QLabel {background-color: " + color.name() + "; font-size: 20px}"
470
+ self.label.setStyleSheet(label_style)
471
+
472
+ region_label = data["label_id"]
473
+ region_name = data["label_name"]
474
+ region_acronym = data["label_acronym"]
475
+ virus_volume = data["virus_volume"]
476
+ region_volume = data["region_volume"]
477
+ region_color = data["label_color"]
478
+ proportion = np.round(np.ravel(virus_volume) / np.ravel(region_volume) * 100, 2)
479
+
480
+ sec_group = QGroupBox()
481
+ sec_layout = QGridLayout(sec_group)
482
+ column_names = [
483
+ QLabel("ID"),
484
+ QLabel("Brain Region"),
485
+ QLabel("Acronym"),
486
+ QLabel("Volume (stk voxel)"),
487
+ QLabel("Proportion (%)"),
488
+ QLabel("Color"),
489
+ ]
490
+ for i in range(len(column_names)):
491
+ sec_layout.addWidget(column_names[i], 0, i, 1, 1)
492
+
493
+ for i in range(len(region_label)):
494
+ clb = QLabel()
495
+ da_color = QColor(
496
+ region_color[i][0], region_color[i][1], region_color[i][2], 255
497
+ ).name()
498
+ clb.setStyleSheet(
499
+ "QLabel {background-color: " + da_color + "; width: 20px; height: 20px}"
500
+ )
501
+
502
+ row_val = [
503
+ QLabel(str(region_label[i])),
504
+ QLabel(region_name[i]),
505
+ QLabel(region_acronym[i]),
506
+ QLabel(str(virus_volume[i])),
507
+ QLabel(str(proportion[i])),
508
+ clb,
509
+ ]
510
+
511
+ for j in range(len(row_val)):
512
+ sec_layout.addWidget(row_val[j], i + 1, j, 1, 1)
513
+
514
+ # ok button, used to close window
515
+ ok_btn = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
516
+ ok_btn.accepted.connect(self.accept)
517
+
518
+ # add widget to layout
519
+ layout.addWidget(self.label)
520
+ layout.addWidget(sec_group)
521
+ layout.addWidget(ok_btn)
522
+ self.setLayout(layout)
523
+
524
+ def accept(self) -> None:
525
+ self.close()
526
+
527
+ def set_probe_color(self, color):
528
+ self.label.setStyleSheet("QLabel {background-color: " + color + ";}")
529
+
530
+
531
+ class ProbeInfoWindow(QDialog):
532
+ def __init__(self, name, data):
533
+ super().__init__()
534
+
535
+ self.setWindowTitle("Probe Information Window")
536
+ self.object_name = name
537
+ self.probe_data = data
538
+
539
+ self.label = QLabel(name)
540
+ # self.label = QLabel("Probe % d " % group_id)
541
+ color = QColor(
542
+ data["vis_color"][0],
543
+ data["vis_color"][1],
544
+ data["vis_color"][2],
545
+ data["vis_color"][3],
546
+ )
547
+ label_style = "QLabel {background-color: " + color.name() + "; font-size: 20px}"
548
+ self.label.setStyleSheet(label_style)
549
+
550
+ ap_angle_label = QLabel("AP tilt from vertical : ")
551
+ ap_angle_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
552
+ ap_angle_label.setToolTip(
553
+ "Angle of the fitted probe from the dorsoventral axis in the "
554
+ "anterior–posterior plane."
555
+ )
556
+ ml_angle_label = QLabel("ML tilt from vertical : ")
557
+ ml_angle_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
558
+ ml_angle_label.setToolTip(
559
+ "Angle of the fitted probe from the dorsoventral axis in the "
560
+ "medial–lateral plane."
561
+ )
562
+ probe_length_label = QLabel("Insertion-to-tip length : ")
563
+ probe_length_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
564
+ dv_label = QLabel("Vertical depth change : ")
565
+ dv_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
566
+ insertion_coords_label = QLabel("Insertion from configured Bregma : ")
567
+ insertion_coords_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
568
+ insertion_voxels_label = QLabel("Insertion atlas voxel (ML, AP, DV) : ")
569
+ insertion_voxels_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
570
+
571
+ terminus_coords_label = QLabel("Tip from configured Bregma : ")
572
+ terminus_coords_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
573
+ terminus_voxels_label = QLabel("Tip atlas voxel (ML, AP, DV) : ")
574
+ terminus_voxels_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
575
+
576
+ ap_angle_value = QLabel(
577
+ "{} \u00B0 {}".format(
578
+ np.round(data["ap_angle"], 2), data.get("ap_tilt", "")
579
+ ).strip()
580
+ )
581
+ ml_angle_value = QLabel(
582
+ "{} \u00B0 {}".format(
583
+ np.round(data["ml_angle"], 2), data.get("ml_tilt", "")
584
+ ).strip()
585
+ )
586
+ probe_length = QLabel("{} \u03BCm".format(np.round(data["probe_length"], 2)))
587
+ dv = QLabel("{} \u03BCm".format(np.round(data["dv"], 2)))
588
+ ic_val = np.round(data["insertion_coords"], 2)
589
+ insertion_coords = QLabel(
590
+ "ML {:+.3f} mm | AP {:+.3f} mm | DV-dorsal {:+.3f} mm".format(
591
+ ic_val[0] / 1000.0,
592
+ ic_val[1] / 1000.0,
593
+ ic_val[2] / 1000.0,
594
+ )
595
+ )
596
+ iv_val = np.asarray(data["insertion_vox"], dtype=int)
597
+ insertion_vox = QLabel(
598
+ "({}, {}, {})".format(iv_val[0], iv_val[1], iv_val[2])
599
+ )
600
+
601
+ tc_val = np.round(data["terminus_coords"], 2)
602
+ terminus_coords = QLabel(
603
+ "ML {:+.3f} mm | AP {:+.3f} mm | DV-dorsal {:+.3f} mm".format(
604
+ tc_val[0] / 1000.0,
605
+ tc_val[1] / 1000.0,
606
+ tc_val[2] / 1000.0,
607
+ )
608
+ )
609
+ tv_val = np.asarray(data["terminus_vox"], dtype=int)
610
+ terminus_vox = QLabel(
611
+ "({}, {}, {})".format(tv_val[0], tv_val[1], tv_val[2])
612
+ )
613
+
614
+ coords_info_group = QGroupBox()
615
+ coords_info_layout = QGridLayout(coords_info_group)
616
+ coords_info_layout.addWidget(ap_angle_label, 0, 0, 1, 1)
617
+ coords_info_layout.addWidget(ap_angle_value, 0, 1, 1, 1)
618
+ coords_info_layout.addWidget(ml_angle_label, 0, 2, 1, 1)
619
+ coords_info_layout.addWidget(ml_angle_value, 0, 3, 1, 1)
620
+
621
+ coords_info_layout.addWidget(probe_length_label, 1, 0, 1, 1)
622
+ coords_info_layout.addWidget(probe_length, 1, 1, 1, 1)
623
+ coords_info_layout.addWidget(dv_label, 1, 2, 1, 1)
624
+ coords_info_layout.addWidget(dv, 1, 3, 1, 1)
625
+
626
+ coords_info_layout.addWidget(insertion_coords_label, 2, 0, 1, 1)
627
+ coords_info_layout.addWidget(insertion_coords, 2, 1, 1, 1)
628
+ coords_info_layout.addWidget(terminus_coords_label, 2, 2, 1, 1)
629
+ coords_info_layout.addWidget(terminus_coords, 2, 3, 1, 1)
630
+
631
+ coords_info_layout.addWidget(insertion_voxels_label, 3, 0, 1, 1)
632
+ coords_info_layout.addWidget(insertion_vox, 3, 1, 1, 1)
633
+ coords_info_layout.addWidget(terminus_voxels_label, 3, 2, 1, 1)
634
+ coords_info_layout.addWidget(terminus_vox, 3, 3, 1, 1)
635
+
636
+ reconstruction = data.get("reconstruction", {})
637
+ reconstructed_coordinates = reconstruction.get("coordinates", {})
638
+ insertion_reconstruction = reconstructed_coordinates.get("insertion", {})
639
+ tip_reconstruction = reconstructed_coordinates.get("tip", {})
640
+ if "estimated_stereotaxic_bregma_mm" in insertion_reconstruction:
641
+ insertion_estimated = insertion_reconstruction[
642
+ "estimated_stereotaxic_bregma_mm"
643
+ ]
644
+ tip_estimated = tip_reconstruction[
645
+ "estimated_stereotaxic_bregma_mm"
646
+ ]
647
+ coords_info_layout.addWidget(
648
+ QLabel("Estimated Allen Bregma insertion : "),
649
+ 4,
650
+ 0,
651
+ 1,
652
+ 1,
653
+ )
654
+ coords_info_layout.addWidget(
655
+ QLabel(
656
+ "AP {:+.3f} mm | ML {:+.3f} mm".format(
657
+ insertion_estimated[0], insertion_estimated[2]
658
+ )
659
+ ),
660
+ 4,
661
+ 1,
662
+ 1,
663
+ 1,
664
+ )
665
+ coords_info_layout.addWidget(
666
+ QLabel("Estimated Allen Bregma tip : "), 4, 2, 1, 1
667
+ )
668
+ coords_info_layout.addWidget(
669
+ QLabel(
670
+ "AP {:+.3f} mm | ML {:+.3f} mm".format(
671
+ tip_estimated[0], tip_estimated[2]
672
+ )
673
+ ),
674
+ 4,
675
+ 3,
676
+ 1,
677
+ 1,
678
+ )
679
+
680
+ fit_group = QGroupBox("Trajectory mapping quality")
681
+ fit_layout = QFormLayout(fit_group)
682
+ fit = data.get("trajectory_fit") or reconstruction.get("probe", {}).get(
683
+ "trajectory_fit"
684
+ )
685
+ if fit:
686
+ voxel_size_um = float(
687
+ reconstruction.get("atlas", {}).get("voxel_size_um", 1.0)
688
+ )
689
+ rms_error = float(fit["rms_error_um"])
690
+ if rms_error <= 1.5 * voxel_size_um:
691
+ quality = "Good"
692
+ elif rms_error <= 3.0 * voxel_size_um:
693
+ quality = "Review"
694
+ else:
695
+ quality = "Poor"
696
+ fit_layout.addRow(
697
+ "Fit:",
698
+ QLabel(
699
+ "{} — {} of {} points retained".format(
700
+ quality, fit["inlier_count"], fit["point_count"]
701
+ )
702
+ ),
703
+ )
704
+ fit_layout.addRow(
705
+ "Deviation from fitted line:",
706
+ QLabel(
707
+ "retained RMS {:.1f} \u03BCm | all-point max {:.1f} "
708
+ "\u03BCm".format(
709
+ rms_error, float(fit["max_error_um"])
710
+ )
711
+ ),
712
+ )
713
+ fit_layout.addRow(
714
+ "Straight-line agreement:",
715
+ QLabel(
716
+ "{:.1f}%".format(
717
+ 100.0 * float(fit["explained_fraction"])
718
+ )
719
+ ),
720
+ )
721
+ fit_layout.addRow(
722
+ "Insertion surface:",
723
+ QLabel(
724
+ fit.get(
725
+ "surface_method",
726
+ "Atlas surface correction method not recorded",
727
+ )
728
+ ),
729
+ )
730
+ else:
731
+ fit_layout.addRow(
732
+ QLabel(
733
+ "Mapping diagnostics are unavailable for this legacy "
734
+ "probe. Re-merge it to calculate them."
735
+ )
736
+ )
737
+
738
+ # group info container
739
+ region_sites_num = np.asarray(
740
+ [
741
+ "{:g}".format(float(value))
742
+ for value in np.ravel(data["region_sites"])
743
+ ]
744
+ )[::-1]
745
+ region_label = np.ravel(data["region_label"]).astype(int).astype(str)[::-1]
746
+ region_color = np.asarray(data["label_color"])[::-1, :]
747
+ region_length = np.round(data["region_length"], 3).astype(str)[::-1]
748
+ region_name = np.ravel(data["label_name"])[::-1]
749
+ region_acronym = np.ravel(data["label_acronym"])[::-1]
750
+
751
+ sec_group = QGroupBox()
752
+ sec_layout = QGridLayout(sec_group)
753
+ column_names = [
754
+ QLabel("ID"),
755
+ QLabel("Brain Region"),
756
+ QLabel("Acronym"),
757
+ QLabel("Contacts"),
758
+ QLabel("Path (\u03BCm)"),
759
+ QLabel("Color"),
760
+ ]
761
+ column_names[3].setToolTip(
762
+ "Number of physical probe contacts assigned to this region."
763
+ )
764
+ column_names[4].setToolTip(
765
+ "Length of the reconstructed probe centerline inside this region."
766
+ )
767
+ for i in range(len(column_names)):
768
+ sec_layout.addWidget(column_names[i], 0, i, 1, 1)
769
+
770
+ for i in range(len(region_label)):
771
+ clb = QLabel()
772
+ da_color = QColor(
773
+ region_color[i][0], region_color[i][1], region_color[i][2], 255
774
+ ).name()
775
+ clb.setStyleSheet(
776
+ "QLabel {background-color: " + da_color + "; width: 20px; height: 20px}"
777
+ )
778
+ clb.setFixedSize(50, 24)
779
+
780
+ row_val = [
781
+ QLabel(region_label[i]),
782
+ QLabel(region_name[i]),
783
+ QLabel(region_acronym[i]),
784
+ QLabel(region_sites_num[i]),
785
+ QLabel(region_length[i]),
786
+ clb,
787
+ ]
788
+
789
+ for j in range(len(row_val)):
790
+ sec_layout.addWidget(row_val[j], i + 1, j, 1, 1)
791
+ sec_layout.setRowStretch(len(region_label) + 1, 1)
792
+
793
+ # plot container
794
+ plot_frame = QFrame()
795
+ plot_frame.setMaximumWidth(300)
796
+ plot_frame.setMinimumWidth(300)
797
+ view_layout = QHBoxLayout(plot_frame)
798
+ view_layout.setSpacing(0)
799
+ view_layout.setContentsMargins(0, 0, 0, 0)
800
+
801
+ w = pg.GraphicsLayoutWidget()
802
+ view = w.addViewBox()
803
+ view.setAspectLocked()
804
+ # view.invertY(True)
805
+
806
+ # load plot data
807
+ vis_data = data["vis_data"]
808
+ n_column = len(vis_data)
809
+
810
+ probe_type_name = data["probe_type_name"]
811
+ # draw tips
812
+ if probe_type_name != "Tetrode":
813
+ da_tip_loc = np.array(
814
+ [[0, 0], [0.5 * n_column, -2 * n_column], [n_column, 0]]
815
+ )
816
+ tips = pg.PlotDataItem(
817
+ da_tip_loc,
818
+ connect="all",
819
+ fillLevel=0,
820
+ fillBrush=pg.mkBrush(color=(128, 128, 128)),
821
+ )
822
+ view.addItem(tips)
823
+
824
+ # draw area
825
+ region_label = data["region_label"]
826
+ region_sites = data["region_sites"]
827
+ region_text_loc = data["text_loc"]
828
+
829
+ group_color = data["label_color"]
830
+ sites_label = data["sites_label"]
831
+ for i in range(n_column):
832
+ group_id = vis_data[i]["group_id"].astype(int)
833
+ start_loc = vis_data[i]["start_loc"]
834
+ end_loc = vis_data[i]["end_loc"]
835
+ sites_loc_column = vis_data[i]["sites"]
836
+
837
+ for j in range(len(group_id)):
838
+ area_data = np.array([[i, end_loc[j]], [i + 1, end_loc[j]]])
839
+ da_item = pg.PlotDataItem(
840
+ area_data,
841
+ fillLevel=start_loc[j],
842
+ fillBrush=group_color[group_id[j]],
843
+ pen=None,
844
+ )
845
+ view.addItem(da_item)
846
+
847
+ sites_color = []
848
+ for j in range(len(sites_label[i])):
849
+ color_ind = np.where(region_label == sites_label[i][j])[0][0]
850
+ sites_color.append(group_color[color_ind])
851
+
852
+ pnt_data = np.stack(
853
+ [np.repeat(i + 0.5, len(sites_loc_column)), sites_loc_column], axis=1
854
+ )
855
+ da_item = pg.ScatterPlotItem(
856
+ pos=pnt_data,
857
+ pen=(128, 128, 128, 128),
858
+ brush=sites_color,
859
+ symbol="s",
860
+ size=3,
861
+ hoverSize=8,
862
+ )
863
+ view.addItem(da_item)
864
+
865
+ # draw text
866
+ plot_region_acronyms = np.ravel(data["label_acronym"])
867
+ self.probe_region_text_items = []
868
+ for i in range(len(region_label)):
869
+ region_text = pg.TextItem(
870
+ "{}: {} contacts".format(
871
+ plot_region_acronyms[i],
872
+ "{:g}".format(float(region_sites[i])),
873
+ )
874
+ )
875
+ region_text.setPos(n_column + 0.5, region_text_loc[i])
876
+ view.addItem(region_text)
877
+ self.probe_region_text_items.append(region_text)
878
+
879
+ view_layout.addWidget(w)
880
+
881
+ channel_info_frame = QFrame()
882
+ channel_info_layout = QHBoxLayout(channel_info_frame)
883
+ channel_info_layout.setContentsMargins(0, 0, 0, 0)
884
+ channel_info_layout.setSpacing(10)
885
+ channel_info_layout.addWidget(plot_frame)
886
+ channel_info_layout.addWidget(sec_group)
887
+
888
+ interpretation_note = QLabel(
889
+ "Angles are measured from the dorsoventral axis. Contacts are "
890
+ "physical recording sites; path is the fitted centerline length "
891
+ "inside each region. Allen Bregma AP/ML values are estimates."
892
+ )
893
+ interpretation_note.setWordWrap(True)
894
+
895
+ # export and close buttons
896
+ ok_btn = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
897
+ self.export_btn = ok_btn.addButton(
898
+ "Export probe CSV files",
899
+ QDialogButtonBox.ButtonRole.ActionRole,
900
+ )
901
+ self.export_btn.clicked.connect(self.export_coordinates)
902
+ ok_btn.accepted.connect(self.accept)
903
+
904
+ # add widget to layout
905
+ layout = QVBoxLayout()
906
+ layout.addWidget(self.label)
907
+ layout.addWidget(coords_info_group)
908
+ layout.addWidget(fit_group)
909
+ layout.addSpacing(10)
910
+ layout.addWidget(channel_info_frame)
911
+ layout.addWidget(interpretation_note)
912
+ layout.addWidget(ok_btn)
913
+ self.setLayout(layout)
914
+
915
+ def export_coordinates(self):
916
+ safe_name = "".join(
917
+ character if character.isalnum() or character in "-_" else "_"
918
+ for character in self.object_name
919
+ )
920
+ file_path, _ = QFileDialog.getSaveFileName(
921
+ self,
922
+ "Choose a base name for the three probe CSV files",
923
+ "{}_probe_export.csv".format(safe_name or "probe"),
924
+ "CSV files (*.csv)",
925
+ )
926
+ if not file_path:
927
+ return
928
+ try:
929
+ paths = write_probe_csv_files(
930
+ file_path, self.object_name, self.probe_data
931
+ )
932
+ except (KeyError, OSError, TypeError, ValueError) as exc:
933
+ QMessageBox.warning(
934
+ self,
935
+ "Export failed",
936
+ "Could not export this probe:\n{}".format(exc),
937
+ )
938
+ return
939
+ QMessageBox.information(
940
+ self,
941
+ "Probe CSV files exported",
942
+ "Created:\n{}".format(
943
+ "\n".join(str(path) for path in paths.values())
944
+ ),
945
+ )
946
+
947
+ def accept(self) -> None:
948
+ self.close()
949
+
950
+ def set_probe_color(self, color):
951
+ self.label.setStyleSheet("QLabel {background-color: " + color + ";}")
952
+
953
+
954
+ class SinglePiece(QWidget):
955
+ sig_clicked = pyqtSignal(object)
956
+ sig_name_changed = pyqtSignal(object)
957
+
958
+ def __init__(
959
+ self,
960
+ parent=None,
961
+ index=0,
962
+ obj_name="",
963
+ obj_type="probe piece",
964
+ object_icon=None,
965
+ ):
966
+ QWidget.__init__(self, parent=parent)
967
+
968
+ self.inactive_style = "QFrame{background-color:rgb(83, 83, 83); border: 1px solid rgb(128, 128, 128);}"
969
+ self.active_style = "QFrame{background-color:rgb(107, 107, 107); border: 1px solid rgb(128, 128, 128);}"
970
+
971
+ self.id = index
972
+ self.object_name = obj_name
973
+ self.object_type = obj_type
974
+ self.active = True
975
+
976
+ self.tbnail = QPushButton()
977
+ self.tbnail.setFixedSize(QSize(40, 40))
978
+ self.tbnail.setStyleSheet(eye_button_style)
979
+ self.tbnail.setIcon(object_icon)
980
+ self.tbnail.setIconSize(QSize(20, 20))
981
+ self.tbnail.clicked.connect(self.on_click)
982
+
983
+ self.text_btn = QDoubleButton()
984
+ self.text_btn.setFixedSize(QSize(240, 40))
985
+ self.text_btn.left_clicked.connect(self.on_click)
986
+ self.text_btn.double_clicked.connect(self.on_doubleclick)
987
+
988
+ self.l_line_edit = QLineEdit()
989
+ self.l_line_edit.setStyleSheet(line_edit_style)
990
+ self.l_line_edit.setFixedWidth(240)
991
+ self.l_line_edit.editingFinished.connect(self.enter_pressed)
992
+ self.l_line_edit.setVisible(False)
993
+
994
+ self.inner_frame = QFrame()
995
+ self.inner_frame.setStyleSheet(self.active_style)
996
+ self.inner_layout = QHBoxLayout(self.inner_frame)
997
+ self.inner_layout.setContentsMargins(0, 0, 0, 0)
998
+ self.inner_layout.setSpacing(0)
999
+ self.inner_layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
1000
+ self.inner_layout.addWidget(self.tbnail)
1001
+ self.inner_layout.addSpacing(5)
1002
+ self.inner_layout.addWidget(self.text_btn)
1003
+ self.inner_layout.addWidget(self.l_line_edit)
1004
+ self.inner_layout.addStretch()
1005
+
1006
+ outer_layout = QHBoxLayout()
1007
+ outer_layout.setContentsMargins(0, 0, 0, 0)
1008
+ outer_layout.setSpacing(0)
1009
+ outer_layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
1010
+ outer_layout.addWidget(self.inner_frame)
1011
+
1012
+ self.setLayout(outer_layout)
1013
+ self.setFixedHeight(40)
1014
+ # self.show()
1015
+
1016
+ @pyqtSlot()
1017
+ def on_click(self):
1018
+ self.set_checked(True)
1019
+ self.sig_clicked.emit(self.id)
1020
+
1021
+ @pyqtSlot()
1022
+ def on_doubleclick(self):
1023
+ self.l_line_edit.setText(self.text_btn.text())
1024
+ self.l_line_edit.setVisible(True)
1025
+ self.text_btn.setVisible(False)
1026
+ self.l_line_edit.setFocus(True)
1027
+ self.set_checked(True)
1028
+ self.sig_clicked.emit(self.id)
1029
+
1030
+ @pyqtSlot()
1031
+ def enter_pressed(self):
1032
+ da_text = self.l_line_edit.text()
1033
+ if "-" not in da_text or "piece" not in da_text:
1034
+ return
1035
+ self.l_line_edit.setVisible(False)
1036
+ self.text_btn.setText(self.l_line_edit.text())
1037
+ self.object_name = self.l_line_edit.text()
1038
+ self.text_btn.setVisible(True)
1039
+ self.sig_name_changed.emit((self.id, self.object_name))
1040
+
1041
+ def is_checked(self):
1042
+ return self.active
1043
+
1044
+ def set_checked(self, check):
1045
+ self.active = check
1046
+ if not self.active:
1047
+ self.inner_frame.setStyleSheet(self.inactive_style)
1048
+ else:
1049
+ self.inner_frame.setStyleSheet(self.active_style)
1050
+
1051
+
1052
+ class RegisteredObject(QWidget):
1053
+ sig_object_color_changed = pyqtSignal(object)
1054
+ eye_clicked = pyqtSignal(object)
1055
+ sig_clicked = pyqtSignal(object)
1056
+ sig_link = pyqtSignal(object)
1057
+ sig_double_clicked = pyqtSignal(object)
1058
+
1059
+ def __init__(
1060
+ self,
1061
+ parent=None,
1062
+ obj_id=0,
1063
+ obj_name="",
1064
+ obj_type="merged probe",
1065
+ object_icon=None,
1066
+ ):
1067
+ QWidget.__init__(self, parent=parent)
1068
+
1069
+ self.inactive_style = "QFrame{background-color:rgb(83, 83, 83); border: 1px solid rgb(128, 128, 128);}"
1070
+ self.active_style = "QFrame{background-color:rgb(107, 107, 107); border: 1px solid rgb(128, 128, 128);}"
1071
+
1072
+ self.color = QColor(randint(0, 255), randint(0, 255), randint(0, 255), 255)
1073
+ self.icon_back = "border:1px solid black; background-color: {}".format(
1074
+ self.color.name()
1075
+ )
1076
+
1077
+ self.id = obj_id
1078
+ self.object_type = obj_type
1079
+ self.object_name = obj_name
1080
+ self.active = True
1081
+ self.vis = True
1082
+
1083
+ self.eye_button = QPushButton()
1084
+ self.eye_button.setFixedSize(QSize(40, 40))
1085
+ self.eye_button.setStyleSheet(eye_button_style)
1086
+ self.eye_button.setCheckable(True)
1087
+ eye_icon = QIcon()
1088
+ eye_icon.addPixmap(QPixmap(resource_path("icons/layers/eye_on.png")), QIcon.Mode.Normal, QIcon.State.Off)
1089
+ eye_icon.addPixmap(QPixmap(resource_path("icons/layers/eye_off.png")), QIcon.Mode.Normal, QIcon.State.On)
1090
+ self.eye_button.setIcon(eye_icon)
1091
+ self.eye_button.setIconSize(QSize(20, 20))
1092
+ self.eye_button.clicked.connect(self.eye_on_click)
1093
+
1094
+ self.tbnail = QPushButton()
1095
+ self.tbnail.setStyleSheet(self.icon_back)
1096
+ self.tbnail.setIcon(object_icon)
1097
+ self.tbnail.setIconSize(QSize(20, 20))
1098
+ self.tbnail.clicked.connect(self.change_object_color)
1099
+
1100
+ self.text_btn = ObjectTextButton()
1101
+ self.text_btn.setText(self.object_name)
1102
+ self.text_btn.clicked.connect(self.text_btn_on_click)
1103
+ # self.text_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Ignored)
1104
+
1105
+ self.link_button = QPushButton()
1106
+ self.link_button.setFixedSize(QSize(25, 40))
1107
+ self.link_button.setCheckable(True)
1108
+ link_icon = QIcon()
1109
+ link_icon.addPixmap(
1110
+ QPixmap(resource_path("icons/sidebar/link_off.svg")), QIcon.Mode.Normal, QIcon.State.Off
1111
+ )
1112
+ link_icon.addPixmap(QPixmap(resource_path("icons/sidebar/link.svg")), QIcon.Mode.Normal, QIcon.State.On)
1113
+ self.link_button.setIcon(link_icon)
1114
+ self.link_button.setIconSize(QSize(20, 20))
1115
+ self.link_button.clicked.connect(self.on_linked)
1116
+
1117
+ self.inner_frame = QFrame()
1118
+ self.inner_frame.setStyleSheet(self.active_style)
1119
+ self.inner_layout = QHBoxLayout(self.inner_frame)
1120
+ self.inner_layout.setContentsMargins(0, 0, 0, 0)
1121
+ self.inner_layout.setSpacing(0)
1122
+ self.inner_layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
1123
+ self.inner_layout.addWidget(self.eye_button)
1124
+ self.inner_layout.addSpacing(5)
1125
+ self.inner_layout.addWidget(self.tbnail)
1126
+ self.inner_layout.addSpacing(5)
1127
+ self.inner_layout.addWidget(self.text_btn)
1128
+ self.inner_layout.addWidget(self.link_button)
1129
+ self.inner_layout.addStretch()
1130
+
1131
+ outer_layout = QHBoxLayout()
1132
+ outer_layout.setContentsMargins(0, 0, 0, 0)
1133
+ outer_layout.setSpacing(0)
1134
+ outer_layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
1135
+ outer_layout.addWidget(self.inner_frame)
1136
+
1137
+ self.setLayout(outer_layout)
1138
+ self.setFixedHeight(40)
1139
+
1140
+ def set_icon_style(self, color):
1141
+ self.color = color
1142
+ self.icon_back = "border:1px solid black; background-color: {}".format(
1143
+ color.name()
1144
+ )
1145
+ self.tbnail.setStyleSheet(self.icon_back)
1146
+
1147
+ def change_object_color(self):
1148
+ self.sig_object_color_changed.emit(self.id)
1149
+
1150
+ def eye_on_click(self):
1151
+ if self.eye_button.isChecked():
1152
+ self.vis = False
1153
+ else:
1154
+ self.vis = True
1155
+ self.set_checked(True)
1156
+ self.tbnail.setEnabled(self.vis)
1157
+ self.text_btn.setEnabled(self.vis)
1158
+ self.eye_clicked.emit((self.id, self.vis))
1159
+
1160
+ def set_checked(self, check):
1161
+ self.active = check
1162
+ if not self.active:
1163
+ self.inner_frame.setStyleSheet(self.inactive_style)
1164
+ else:
1165
+ self.inner_frame.setStyleSheet(self.active_style)
1166
+
1167
+ def text_btn_on_click(self):
1168
+ self.set_checked(True)
1169
+ self.sig_clicked.emit(self.id)
1170
+
1171
+ def is_checked(self):
1172
+ return self.active
1173
+
1174
+ def on_linked(self):
1175
+ self.sig_link.emit(self.id)
1176
+
1177
+
1178
+ class BottomButton(QPushButton):
1179
+ def __init__(self, icon_path):
1180
+ QPushButton.__init__(self)
1181
+ btm_style = read_qss_file("qss/obj_ctrl_bottom_button.qss")
1182
+ self.setFixedSize(24, 24)
1183
+ self.setStyleSheet(btm_style)
1184
+ self.setIcon(QIcon(resource_path(icon_path)))
1185
+ self.setIconSize(QSize(20, 20))
1186
+
1187
+
1188
+ class ObjectControl(QObject):
1189
+ """
1190
+ only pieces' name can be changed, the merged can not, so far
1191
+ """
1192
+
1193
+ class SignalProxy(QObject):
1194
+ sigOpacityChanged = pyqtSignal(object)
1195
+ sigVisChanged = pyqtSignal(object)
1196
+ sigDeleteObject = pyqtSignal(object)
1197
+ sigAddObject = pyqtSignal(object)
1198
+ sigColorChanged = pyqtSignal(object)
1199
+ sigSizeChanged = pyqtSignal(object)
1200
+ sigBlendModeChanged = pyqtSignal(object)
1201
+
1202
+ def __init__(self, parent=None):
1203
+ self._sigprox = ObjectControl.SignalProxy()
1204
+ self.sig_opacity_changed = self._sigprox.sigOpacityChanged
1205
+ self.sig_visible_changed = self._sigprox.sigVisChanged
1206
+ self.sig_delete_object = (
1207
+ self._sigprox.sigDeleteObject
1208
+ ) # for delete obj 3d gl widgets
1209
+ self.sig_add_object = self._sigprox.sigAddObject # for add obj 3d gl widgets
1210
+ self.sig_color_changed = self._sigprox.sigColorChanged
1211
+ self.sig_size_changed = self._sigprox.sigSizeChanged
1212
+ self.sig_blend_mode_changed = self._sigprox.sigBlendModeChanged
1213
+
1214
+ QObject.__init__(self)
1215
+
1216
+ self.default_size_val = 2
1217
+ self.default_opacity_val = 100
1218
+ self.drawing_info_provider = None
1219
+
1220
+ self.valid_obj_type = [
1221
+ "probe piece",
1222
+ "merged probe",
1223
+ "cells piece",
1224
+ "merged cells",
1225
+ "virus piece",
1226
+ "merged virus",
1227
+ "contour piece",
1228
+ "merged contour",
1229
+ "drawing piece",
1230
+ "merged drawing",
1231
+ ]
1232
+
1233
+ self.current_obj_index = None
1234
+
1235
+ self.obj_count = 0
1236
+ self.linked_indexes = []
1237
+
1238
+ self.obj_list = [] # widgets
1239
+ self.obj_id = [] # identity
1240
+ self.obj_name = (
1241
+ []
1242
+ ) # names, initially the same as type, can be changed freely ???
1243
+ self.obj_type = [] # type
1244
+ self.obj_data = [] # data
1245
+ self.obj_size = [] # size
1246
+ self.obj_opacity = []
1247
+ self.obj_comp_mode = []
1248
+ self.obj_visibility = []
1249
+ self.obj_merged = []
1250
+
1251
+ self.probe_icon = QIcon(resource_path("icons/sidebar/probe.svg"))
1252
+ self.virus_icon = QIcon(resource_path("icons/sidebar/virus.svg"))
1253
+ self.drawing_icon = QIcon(resource_path("icons/toolbar/pencil.svg"))
1254
+ self.cell_icon = QIcon(resource_path("icons/toolbar/location.svg"))
1255
+ self.contour_icon = QIcon(resource_path("icons/sidebar/contour.svg"))
1256
+ self.compare_icon = QIcon(resource_path("icons/sidebar/compare.svg"))
1257
+
1258
+ combo_label = QLabel("Composition:")
1259
+ combo_label.setFixedWidth(80)
1260
+ self.obj_blend_combo = QComboBox()
1261
+ self.obj_blend_combo.setEditable(False)
1262
+ combo_value = ["opaque", "translucent", "additive"]
1263
+ self.obj_blend_combo.addItems(combo_value)
1264
+ self.obj_blend_combo.setCurrentText("opaque")
1265
+ self.obj_blend_combo.currentTextChanged.connect(self.blend_mode_changed)
1266
+ combo_wrap = QFrame()
1267
+ combo_wrap_layout = QHBoxLayout(combo_wrap)
1268
+ combo_wrap_layout.setContentsMargins(0, 0, 0, 0)
1269
+ combo_wrap_layout.setSpacing(5)
1270
+ combo_wrap_layout.addWidget(combo_label)
1271
+ combo_wrap_layout.addWidget(self.obj_blend_combo)
1272
+
1273
+ obj_opacity_label = QLabel("Opacity:")
1274
+ obj_opacity_label.setFixedWidth(80)
1275
+ self.obj_opacity_slider = QSlider(Qt.Orientation.Horizontal)
1276
+ self.obj_opacity_slider.setMaximum(100)
1277
+ self.obj_opacity_slider.setMinimum(0)
1278
+ self.obj_opacity_slider.setValue(100)
1279
+ self.obj_opacity_slider.valueChanged.connect(self.change_opacity_label_value)
1280
+ self.obj_opacity_slider.sliderMoved.connect(self.send_opacity_changed_signal)
1281
+ self.obj_opacity_val_label = QLabel("100%")
1282
+ self.obj_opacity_val_label.setFixedWidth(40)
1283
+ opacity_wrap = QFrame()
1284
+ opacity_wrap_layout = QHBoxLayout(opacity_wrap)
1285
+ opacity_wrap_layout.setContentsMargins(0, 0, 0, 0)
1286
+ opacity_wrap_layout.setSpacing(5)
1287
+ opacity_wrap_layout.addWidget(obj_opacity_label)
1288
+ opacity_wrap_layout.addWidget(self.obj_opacity_slider)
1289
+ opacity_wrap_layout.addWidget(self.obj_opacity_val_label)
1290
+
1291
+ obj_size_label = QLabel("Size/Width: ")
1292
+ obj_size_label.setFixedWidth(80)
1293
+ self.obj_size_slider = QSlider(Qt.Orientation.Horizontal)
1294
+ self.obj_size_slider.setValue(2)
1295
+ self.obj_size_slider.setMinimum(1)
1296
+ self.obj_size_slider.setMaximum(10)
1297
+ self.obj_size_slider.valueChanged.connect(self.change_size_label_value)
1298
+ self.obj_size_slider.sliderMoved.connect(self.send_size_changed_signal)
1299
+ self.obj_size_val_label = QLabel("2")
1300
+ self.obj_size_val_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
1301
+ self.obj_size_val_label.setFixedWidth(40)
1302
+ size_wrap = QFrame()
1303
+ size_wrap_layout = QHBoxLayout(size_wrap)
1304
+ size_wrap_layout.setContentsMargins(0, 0, 0, 0)
1305
+ size_wrap_layout.setSpacing(5)
1306
+ size_wrap_layout.addWidget(obj_size_label)
1307
+ size_wrap_layout.addWidget(self.obj_size_slider)
1308
+ size_wrap_layout.addWidget(self.obj_size_val_label)
1309
+
1310
+ top_frame = QFrame()
1311
+ top_layout = QVBoxLayout(top_frame)
1312
+ top_layout.setContentsMargins(0, 0, 0, 0)
1313
+ top_layout.setSpacing(0)
1314
+ top_layout.addWidget(combo_wrap)
1315
+ top_layout.addSpacing(10)
1316
+ top_layout.addWidget(opacity_wrap)
1317
+ top_layout.addSpacing(10)
1318
+ top_layout.addWidget(size_wrap)
1319
+ top_layout.addSpacing(10)
1320
+
1321
+ self.layer_frame = QFrame()
1322
+ self.layer_frame.setStyleSheet("background: transparent; border: 0px;")
1323
+ self.layer_frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
1324
+ self.layer_layout = QBoxLayout(QBoxLayout.Direction.BottomToTop, self.layer_frame)
1325
+ self.layer_layout.setAlignment(Qt.AlignmentFlag.AlignBottom)
1326
+ self.layer_layout.setContentsMargins(0, 0, 0, 0)
1327
+ self.layer_layout.setSpacing(5)
1328
+
1329
+ self.layer_scroll = QScrollArea()
1330
+ self.layer_scroll.setStyleSheet("background: transparent; border: 0px;")
1331
+ self.layer_scroll.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
1332
+ self.layer_scroll.setWidget(self.layer_frame)
1333
+ self.layer_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
1334
+ self.layer_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
1335
+ self.layer_scroll.setWidgetResizable(True)
1336
+
1337
+ mid_frame = QFrame()
1338
+ mid_frame.setStyleSheet(
1339
+ "background: transparent; border: 1px solid rgb(128, 128, 128);"
1340
+ )
1341
+ mid_frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
1342
+ mid_layout = QGridLayout(mid_frame)
1343
+ mid_layout.setContentsMargins(0, 0, 0, 0)
1344
+ mid_layout.setSpacing(0)
1345
+ mid_layout.setAlignment(Qt.AlignmentFlag.AlignBottom)
1346
+ mid_layout.addWidget(self.layer_scroll, 0, 0, 1, 1)
1347
+
1348
+ self.outer_frame = QFrame()
1349
+ self.outer_frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
1350
+ outer_layout = QVBoxLayout(self.outer_frame)
1351
+ outer_layout.setSpacing(0)
1352
+ outer_layout.addWidget(top_frame)
1353
+ outer_layout.addWidget(mid_frame)
1354
+
1355
+ # bottom buttons
1356
+ self.info_btn = BottomButton("icons/sidebar/info.svg")
1357
+ self.info_btn.setToolTip("open information window")
1358
+ self.info_btn.clicked.connect(self.info_btn_clicked)
1359
+
1360
+ self.vis2d_btn = BottomButton("icons/toolbar/vis2d.svg")
1361
+ self.vis2d_btn.setToolTip("find the corresponding 2D plane")
1362
+ self.unmerge_btn = BottomButton("icons/toolbar/unmerge.svg")
1363
+ self.unmerge_btn.setToolTip("un-merge a merged object")
1364
+ self.unmerge_btn.clicked.connect(self.unmerge_objects)
1365
+ self.compare_btn = BottomButton("icons/sidebar/compare.svg")
1366
+ self.compare_btn.setToolTip("compare multiple linked objects")
1367
+
1368
+ self.merge_probe_btn = BottomButton("icons/sidebar/probe.svg")
1369
+ self.merge_probe_btn.setToolTip("merge probes pieces")
1370
+
1371
+ self.merge_drawing_btn = BottomButton("icons/toolbar/pencil.svg")
1372
+ self.merge_drawing_btn.setToolTip("merge drawing pieces")
1373
+
1374
+ self.merge_cell_btn = BottomButton("icons/toolbar/location.svg")
1375
+ self.merge_cell_btn.setToolTip("merge cells pieces")
1376
+
1377
+ self.merge_virus_btn = BottomButton("icons/sidebar/virus.svg")
1378
+ self.merge_virus_btn.setToolTip("merge virus pieces")
1379
+
1380
+ self.merge_contour_btn = BottomButton("icons/sidebar/contour.svg")
1381
+ self.merge_contour_btn.setToolTip("merge contour pieces")
1382
+
1383
+ self.add_object_btn = BottomButton("icons/sidebar/add.png")
1384
+ self.add_object_btn.setToolTip("add a piece")
1385
+
1386
+ self.delete_object_btn = BottomButton("icons/sidebar/trash.png")
1387
+ self.delete_object_btn.setToolTip("delete an object")
1388
+ self.delete_object_btn.clicked.connect(self.delete_object_btn_clicked)
1389
+
1390
+ def blend_mode_changed(self):
1391
+ blend_mode = self.obj_blend_combo.currentText()
1392
+ if self.current_obj_index is None:
1393
+ return
1394
+ if "merged" not in self.obj_type[self.current_obj_index]:
1395
+ return
1396
+ if blend_mode != self.obj_comp_mode[self.current_obj_index]:
1397
+ self.obj_comp_mode[self.current_obj_index] = blend_mode
1398
+ self.sig_blend_mode_changed.emit(blend_mode)
1399
+
1400
+ def change_opacity_label_value(self):
1401
+ da_val = self.obj_opacity_slider.value()
1402
+ self.obj_opacity_val_label.setText("{} %".format(da_val))
1403
+
1404
+ def send_opacity_changed_signal(self):
1405
+ da_val = self.obj_opacity_slider.value()
1406
+ if self.current_obj_index is None:
1407
+ return
1408
+ if "merged" not in self.obj_type[self.current_obj_index]:
1409
+ self.obj_opacity_slider.setValue(da_val)
1410
+ return
1411
+ self.obj_opacity[self.current_obj_index] = da_val
1412
+ self.sig_opacity_changed.emit(da_val)
1413
+
1414
+ def change_size_label_value(self):
1415
+ da_val = self.obj_size_slider.value()
1416
+ self.obj_size_val_label.setText(str(da_val))
1417
+
1418
+ def send_size_changed_signal(self):
1419
+ da_val = self.obj_size_slider.value()
1420
+ if self.current_obj_index is None:
1421
+ return
1422
+ if "merged" not in self.obj_type[self.current_obj_index]:
1423
+ self.obj_size_slider.setValue(da_val)
1424
+ return
1425
+ self.obj_size[self.current_obj_index] = da_val
1426
+ self.sig_size_changed.emit((self.obj_type[self.current_obj_index], da_val))
1427
+
1428
+ def obj_piece_name_changed(self, ev):
1429
+ clicked_id = ev[0]
1430
+ name = ev[1]
1431
+ clicked_index = np.where(np.ravel(self.obj_id) == clicked_id)[0][0]
1432
+ self.current_obj_index = clicked_index
1433
+ self.obj_name[clicked_index] = name
1434
+
1435
+ def delete_object_btn_clicked(self):
1436
+ if self.current_obj_index is None:
1437
+ return
1438
+ self.delete_objects(self.current_obj_index)
1439
+
1440
+ def info_btn_clicked(self):
1441
+ if self.current_obj_index is None:
1442
+ return
1443
+ object_type = self.obj_type[self.current_obj_index]
1444
+ if "merged" in object_type or "drawing" in object_type:
1445
+ self.obj_info_on_click()
1446
+
1447
+ def obj_clicked(self, clicked_id):
1448
+ self.set_active_layer_to_current(clicked_id)
1449
+
1450
+ def obj_info_on_click(self):
1451
+ da_data = self.obj_data[self.current_obj_index]
1452
+ da_name = self.obj_name[self.current_obj_index]
1453
+ object_type = self.obj_type[self.current_obj_index]
1454
+ if "probe" in object_type:
1455
+ self.info_window = ProbeInfoWindow(da_name, da_data)
1456
+ elif "virus" in object_type:
1457
+ self.info_window = VirusInfoWindow(da_name, da_data)
1458
+ elif "cell" in object_type:
1459
+ self.info_window = CellsInfoWindow(da_name, da_data)
1460
+ elif "drawing" in object_type:
1461
+ if self.drawing_info_provider is None:
1462
+ return
1463
+ try:
1464
+ drawing_info = self.drawing_info_provider(
1465
+ da_data, object_type, da_name
1466
+ )
1467
+ except (KeyError, TypeError, ValueError) as exc:
1468
+ QMessageBox.warning(
1469
+ None,
1470
+ "Drawing information unavailable",
1471
+ "Could not analyze this drawing:\n{}".format(exc),
1472
+ )
1473
+ return
1474
+ self.info_window = DrawingInfoWindow(da_name, drawing_info)
1475
+ else:
1476
+ return
1477
+ self.info_window.exec()
1478
+
1479
+ def set_active_layer_to_current(self, clicked_id):
1480
+ previous_obj_id = self.obj_id[self.current_obj_index]
1481
+ if previous_obj_id != clicked_id:
1482
+ active_index = np.where(np.ravel(self.obj_id) == clicked_id)[0][0]
1483
+ self.current_obj_index = active_index
1484
+ # self.obj_list[active_index].set_checked(True)
1485
+ inactive_id = np.where(np.ravel(self.obj_id) == previous_obj_id)[0][0]
1486
+ self.obj_list[inactive_id].set_checked(False)
1487
+ self.set_slider_combo_to_current()
1488
+ else:
1489
+ return
1490
+
1491
+ def obj_link_changed(self, clicked_id):
1492
+ da_index = np.where(np.ravel(self.obj_id) == clicked_id)[0][0]
1493
+ if self.obj_list[da_index].link_button.isChecked():
1494
+ self.linked_indexes.append(da_index)
1495
+ else:
1496
+ if da_index in self.linked_indexes:
1497
+ del_ind = np.where(np.ravel(self.linked_indexes) == da_index)[0][0]
1498
+ self.linked_indexes.pop(del_ind)
1499
+
1500
+ def obj_color_changed(self, clicked_id):
1501
+ self.set_active_layer_to_current(clicked_id)
1502
+ color = QColorDialog.getColor()
1503
+ if color.isValid():
1504
+ self.obj_list[self.current_obj_index].set_icon_style(color)
1505
+ da_color = (color.red(), color.green(), color.blue(), 255)
1506
+ self.sig_color_changed.emit((self.current_obj_index, da_color))
1507
+
1508
+ def obj_eye_clicked(self, ev):
1509
+ clicked_id = ev[0]
1510
+ vis = ev[1]
1511
+ self.set_active_layer_to_current(clicked_id)
1512
+ self.sig_visible_changed.emit((self.current_obj_index, vis))
1513
+
1514
+ # delete a object
1515
+ def delete_objects(self, delete_index):
1516
+ del_ind = np.ravel(delete_index)
1517
+ if len(del_ind) > 1:
1518
+ del_ind = np.sort(delete_index)[::-1]
1519
+
1520
+ current_obj_id = self.obj_id[self.current_obj_index]
1521
+ for da_ind in del_ind:
1522
+ self.layer_layout.removeWidget(self.obj_list[da_ind])
1523
+ self.obj_list[da_ind].deleteLater()
1524
+ del self.obj_list[da_ind]
1525
+ del self.obj_id[da_ind]
1526
+ del self.obj_name[da_ind]
1527
+ del self.obj_type[da_ind]
1528
+ del self.obj_data[da_ind]
1529
+ del self.obj_comp_mode[da_ind]
1530
+ del self.obj_opacity[da_ind]
1531
+ del self.obj_size[da_ind]
1532
+ self.sig_delete_object.emit(da_ind)
1533
+ if self.current_obj_index in del_ind:
1534
+ if self.obj_list:
1535
+ self.obj_list[-1].set_checked(True)
1536
+ self.current_obj_index = len(self.obj_list) - 1
1537
+ else:
1538
+ self.current_obj_index = None
1539
+ else:
1540
+ active_index = np.where(np.ravel(self.obj_id) == current_obj_id)[0][0]
1541
+ self.current_obj_index = active_index
1542
+
1543
+ #
1544
+ def get_object_icon(self, object_type):
1545
+ if "probe" in object_type:
1546
+ object_icon = self.probe_icon
1547
+ elif "virus" in object_type:
1548
+ object_icon = self.virus_icon
1549
+ elif "cell" in object_type:
1550
+ object_icon = self.cell_icon
1551
+ elif "contour" in object_type:
1552
+ object_icon = self.contour_icon
1553
+ elif "drawing" in object_type:
1554
+ object_icon = self.drawing_icon
1555
+ else:
1556
+ object_icon = None
1557
+ return object_icon
1558
+
1559
+ def get_group_count(self, object_type):
1560
+ group_count = len(
1561
+ [da_type for da_type in self.obj_type if da_type == object_type]
1562
+ )
1563
+ return group_count
1564
+
1565
+ def add_object(self, object_name, object_type, object_data, object_mode):
1566
+ object_icon = self.get_object_icon(object_type)
1567
+ # group_count = self.get_group_count(object_type)
1568
+ if object_icon is None:
1569
+ return
1570
+ self.obj_id.append(self.obj_count)
1571
+ self.obj_data.append(object_data)
1572
+ self.obj_name.append(object_name)
1573
+ self.obj_type.append(object_type)
1574
+ if "merged" in object_type:
1575
+ new_layer = RegisteredObject(
1576
+ obj_id=self.obj_count,
1577
+ obj_name=object_name,
1578
+ obj_type=object_type,
1579
+ object_icon=object_icon,
1580
+ )
1581
+ new_layer.eye_clicked.connect(self.obj_eye_clicked)
1582
+ new_layer.sig_object_color_changed.connect(self.obj_color_changed)
1583
+ new_layer.sig_link.connect(self.obj_link_changed)
1584
+ self.obj_opacity.append(self.default_opacity_val)
1585
+ self.obj_size.append(self.default_size_val)
1586
+ self.obj_comp_mode.append(object_mode)
1587
+ da_color = (
1588
+ new_layer.color.red(),
1589
+ new_layer.color.green(),
1590
+ new_layer.color.blue(),
1591
+ 255,
1592
+ )
1593
+ self.obj_data[-1]["vis_color"] = da_color
1594
+ if self.obj_size_slider.value() != self.default_size_val:
1595
+ self.obj_size_slider.setValue(self.default_size_val)
1596
+ if self.obj_opacity_slider.value() != self.default_opacity_val:
1597
+ self.obj_opacity_slider.setValue(self.default_opacity_val)
1598
+ if self.obj_blend_combo.currentText() != object_mode:
1599
+ self.obj_blend_combo.blockSignals(True)
1600
+ self.obj_blend_combo.setCurrentText(object_mode)
1601
+ self.obj_blend_combo.blockSignals(False)
1602
+ else:
1603
+ new_layer = SinglePiece(
1604
+ index=self.obj_count,
1605
+ obj_name=object_name,
1606
+ obj_type=object_type,
1607
+ object_icon=object_icon,
1608
+ )
1609
+ new_layer.sig_name_changed.connect(self.obj_piece_name_changed)
1610
+ self.obj_opacity.append([])
1611
+ self.obj_size.append([])
1612
+ self.obj_comp_mode.append([])
1613
+
1614
+ new_layer.text_btn.setText(self.obj_name[-1])
1615
+ new_layer.set_checked(True)
1616
+ new_layer.sig_clicked.connect(self.obj_clicked)
1617
+ self.obj_list.append(new_layer)
1618
+
1619
+ active_index = np.where(np.ravel(self.obj_id) == self.obj_count)[0][0]
1620
+ if self.current_obj_index is None:
1621
+ self.current_obj_index = active_index
1622
+ else:
1623
+ self.obj_list[self.current_obj_index].set_checked(False)
1624
+ self.current_obj_index = active_index
1625
+
1626
+ self.layer_layout.addWidget(self.obj_list[-1])
1627
+ self.sig_add_object.emit((object_data, object_type))
1628
+ self.obj_count += 1
1629
+
1630
+ # merge object pieces
1631
+ def merge_pieces(self, obj_type="probe piece"):
1632
+ if obj_type not in [
1633
+ "probe piece",
1634
+ "virus piece",
1635
+ "contour piece",
1636
+ "drawing piece",
1637
+ "cells piece",
1638
+ ]:
1639
+ return
1640
+ n_obj = len(self.obj_id)
1641
+ cind = [ind for ind in range(n_obj) if self.obj_type[ind] == obj_type]
1642
+ # print("cind", cind)
1643
+ valid_pieces_names = np.ravel(self.obj_name)[np.array(cind)]
1644
+ n_pieces = len(valid_pieces_names)
1645
+ m_obj_names = []
1646
+ for i in range(n_pieces):
1647
+ da_name = valid_pieces_names[i]
1648
+ # da_name = da_name.replace(" ", "")
1649
+ da_name_split = da_name.split("-")
1650
+ m_obj_name = da_name_split[0]
1651
+ if m_obj_name[-1] == " ":
1652
+ m_obj_name = m_obj_name[:-1]
1653
+ m_obj_names.append(m_obj_name)
1654
+ merging_object_names = np.unique(m_obj_names)
1655
+ n_object = len(merging_object_names)
1656
+ data = [[] for _ in range(n_object)]
1657
+ pieces_names = [[] for _ in range(n_object)]
1658
+ for i in range(n_object):
1659
+ sub_inds = [
1660
+ cind[ind]
1661
+ for ind in range(n_pieces)
1662
+ if m_obj_names[ind] == merging_object_names[i]
1663
+ ]
1664
+ for j in range(len(sub_inds)):
1665
+ data[i].append(self.obj_data[sub_inds[j]])
1666
+ pieces_names[i].append(self.obj_name[sub_inds[j]])
1667
+ # temp = self.obj_data[da_piece_ind_in_obj_order[0]].T
1668
+ # if len(da_piece_ind_in_obj_order) > 1:
1669
+ # for j in range(1, len(da_piece_ind_in_obj_order)):
1670
+ # print(self.obj_data[da_piece_ind_in_obj_order[j]].T)
1671
+ # temp = np.hstack([temp, self.obj_data[da_piece_ind_in_obj_order[j]].T])
1672
+ # data[i] = temp.T
1673
+ self.delete_objects(cind)
1674
+ return data, merging_object_names, pieces_names
1675
+
1676
+ # unmerge object pieces
1677
+ def unmerge_objects(self):
1678
+ current_type = self.obj_type[self.current_obj_index]
1679
+ if "merged" not in current_type:
1680
+ return
1681
+ current_data = self.obj_data[self.current_obj_index]
1682
+ m_obj_type = current_type.split(" ")[1]
1683
+ data_list = current_data["data"]
1684
+ pieces_names = current_data["pieces_names"]
1685
+ self.delete_objects([self.current_obj_index])
1686
+ for i in range(len(data_list)):
1687
+ self.add_object(
1688
+ pieces_names[i], "{} piece".format(m_obj_type), data_list[i], None
1689
+ )
1690
+
1691
+ # get obj data
1692
+ def get_obj_data(self):
1693
+ data = {
1694
+ "obj_name": self.obj_name,
1695
+ "obj_type": self.obj_type,
1696
+ "obj_data": self.obj_data,
1697
+ "obj_size": self.obj_size,
1698
+ "obj_opacity": self.obj_opacity,
1699
+ "obj_comp_mode": self.obj_comp_mode,
1700
+ "current_obj_index": self.current_obj_index,
1701
+ }
1702
+ return data
1703
+
1704
+ def set_obj_data(self, data):
1705
+ for i in range(len(data["obj_type"])):
1706
+ self.add_object(
1707
+ data["obj_name"][i],
1708
+ data["obj_type"][i],
1709
+ data["obj_data"][i],
1710
+ data["obj_comp_mode"][i],
1711
+ )
1712
+
1713
+ self.obj_size = data["obj_size"]
1714
+ self.obj_opacity = data["obj_opacity"]
1715
+ self.obj_comp_mode = data["obj_comp_mode"]
1716
+ self.current_obj_index = data["current_obj_index"]
1717
+
1718
+ self.obj_list[-1].set_checked(False)
1719
+ self.obj_list[self.current_obj_index].set_checked(True)
1720
+
1721
+ # print(self.obj_type)
1722
+
1723
+ def set_slider_combo_to_current(self):
1724
+ if "merged" in self.obj_type[self.current_obj_index]:
1725
+ size_val = self.obj_size_slider.value()
1726
+ opacity_val = self.obj_opacity_slider.value()
1727
+ compo_mode = self.obj_blend_combo.currentText()
1728
+
1729
+ if self.obj_size[self.current_obj_index] != size_val:
1730
+ self.obj_size_slider.setValue(self.obj_size[self.current_obj_index])
1731
+ if self.obj_opacity[self.current_obj_index] != opacity_val:
1732
+ self.obj_opacity_slider.setValue(
1733
+ self.obj_opacity[self.current_obj_index]
1734
+ )
1735
+ if self.obj_comp_mode[self.current_obj_index] != compo_mode:
1736
+ self.obj_blend_combo.setCurrentText(
1737
+ self.obj_comp_mode[self.current_obj_index]
1738
+ )
1739
+ else:
1740
+ return
1741
+
1742
+ def clear_all(self):
1743
+ ind = np.arange(len(self.obj_list))[::-1]
1744
+ for i in ind:
1745
+ self.layer_layout.removeWidget(self.obj_list[i])
1746
+ self.obj_list[i].deleteLater()
1747
+ del self.obj_list[i]
1748
+ self.obj_id = []
1749
+ self.obj_name = []
1750
+ self.obj_type = []
1751
+ self.obj_data = []
1752
+ self.obj_size = []
1753
+ self.obj_opacity = []
1754
+ self.obj_comp_mode = []
1755
+ self.obj_count = 0
1756
+ self.current_obj_index = None
1757
+
1758
+ def compare_obj_called(self):
1759
+ compare_names = [self.obj_name[ind] for ind in self.linked_indexes]
1760
+ compare_data = [self.obj_data[ind] for ind in self.linked_indexes]
1761
+ info_window = CompareWindow(compare_names, compare_data)
1762
+ info_window.exec()