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,375 @@
1
+ """Coordinate and anatomical summaries for drawing objects."""
2
+
3
+ import csv
4
+
5
+ import numpy as np
6
+
7
+ from .probe_reconstruction import (
8
+ allen_ccf_to_estimated_bregma_mm,
9
+ herbs_vox_to_source_vox,
10
+ is_allen_ccf_2017,
11
+ normalize_axis_info,
12
+ )
13
+
14
+
15
+ ROI_CSV_FIELDS = (
16
+ "piece",
17
+ "point",
18
+ "herbs_ML_vox_from_bregma",
19
+ "herbs_AP_vox_from_bregma",
20
+ "herbs_DV_dorsal_vox_from_bregma",
21
+ "configured_AP_mm",
22
+ "configured_ML_mm",
23
+ "configured_DV_ventral_mm",
24
+ "allen_AP_vox",
25
+ "allen_DV_vox",
26
+ "allen_ML_vox",
27
+ "estimated_AP_mm",
28
+ "estimated_ML_mm",
29
+ "affine_DV_mm_not_for_targeting",
30
+ "surface_depth_mm",
31
+ "structure_id",
32
+ "structure_acronym",
33
+ "structure_name",
34
+ )
35
+
36
+
37
+ def _drawing_pieces(pieces):
38
+ result = []
39
+ for piece in pieces:
40
+ array = np.asarray(piece, dtype=float)
41
+ if array.ndim != 2 or array.shape[1:] != (3,):
42
+ raise ValueError("Drawing pieces must be arrays with shape (N, 3).")
43
+ if not np.all(np.isfinite(array)):
44
+ raise ValueError("Drawing coordinates must contain only finite values.")
45
+ result.append(array)
46
+ if not result or not any(len(piece) for piece in result):
47
+ raise ValueError("A drawing must contain at least one coordinate.")
48
+ return result
49
+
50
+
51
+ def _axis_statistics(values):
52
+ values = np.asarray(values, dtype=float)
53
+ result = {}
54
+ for index, axis in enumerate(("AP", "ML", "DV")):
55
+ column = values[:, index]
56
+ finite = column[np.isfinite(column)]
57
+ if finite.size:
58
+ result[axis] = {
59
+ "centroid": float(np.mean(finite)),
60
+ "min": float(np.min(finite)),
61
+ "max": float(np.max(finite)),
62
+ }
63
+ return result
64
+
65
+
66
+ def _surface_depth_mm(points, label_volume, voxel_size_um):
67
+ points = np.asarray(points, dtype=float)
68
+ indexes = points.astype(int)
69
+ depths = np.full(len(points), np.nan, dtype=float)
70
+ if label_volume is None:
71
+ return depths
72
+
73
+ labels = np.asarray(label_volume)
74
+ if labels.ndim != 3:
75
+ raise ValueError("The atlas label volume must be three-dimensional.")
76
+
77
+ valid_xy = (
78
+ (indexes[:, 0] >= 0)
79
+ & (indexes[:, 0] < labels.shape[0])
80
+ & (indexes[:, 1] >= 0)
81
+ & (indexes[:, 1] < labels.shape[1])
82
+ )
83
+ point_indexes = np.flatnonzero(valid_xy)
84
+ if not point_indexes.size:
85
+ return depths
86
+
87
+ pairs, inverse = np.unique(indexes[point_indexes, :2], axis=0, return_inverse=True)
88
+ lower = np.full(len(pairs), -1, dtype=int)
89
+ upper = np.full(len(pairs), -1, dtype=int)
90
+ chunk_size = 4096
91
+ for start in range(0, len(pairs), chunk_size):
92
+ stop = min(start + chunk_size, len(pairs))
93
+ chunk = pairs[start:stop]
94
+ occupied = labels[chunk[:, 0], chunk[:, 1], :] != 0
95
+ has_brain = np.any(occupied, axis=1)
96
+ if not np.any(has_brain):
97
+ continue
98
+ valid_rows = np.flatnonzero(has_brain)
99
+ lower[start + valid_rows] = np.argmax(occupied[valid_rows], axis=1)
100
+ upper[start + valid_rows] = (
101
+ labels.shape[2]
102
+ - 1
103
+ - np.argmax(occupied[valid_rows, ::-1], axis=1)
104
+ )
105
+
106
+ point_lower = lower[inverse]
107
+ point_upper = upper[inverse]
108
+ point_z = points[point_indexes, 2]
109
+ inside_extent = (
110
+ (point_lower >= 0)
111
+ & (point_z >= point_lower)
112
+ & (point_z <= point_upper)
113
+ )
114
+ selected = point_indexes[inside_extent]
115
+ depths[selected] = (
116
+ (point_upper[inside_extent] - point_z[inside_extent])
117
+ * float(voxel_size_um)
118
+ / 1000.0
119
+ )
120
+ return depths
121
+
122
+
123
+ def _structure_details(structure_ids, label_info):
124
+ unique_ids, inverse = np.unique(structure_ids, return_inverse=True)
125
+ unique_names = np.full(len(unique_ids), "", dtype=object)
126
+ unique_acronyms = np.full(len(unique_ids), "", dtype=object)
127
+ colors = {}
128
+ if label_info is None:
129
+ return unique_names[inverse], unique_acronyms[inverse], colors
130
+
131
+ indexes = np.asarray(label_info.get("index", []), dtype=int).ravel()
132
+ labels = np.asarray(label_info.get("label", []), dtype=object).ravel()
133
+ abbrevs = np.asarray(label_info.get("abbrev", []), dtype=object).ravel()
134
+ label_colors = np.asarray(label_info.get("color", []))
135
+ lookup = {int(label_id): index for index, label_id in enumerate(indexes)}
136
+ for row, label_id in enumerate(unique_ids):
137
+ label_index = lookup.get(int(label_id))
138
+ if label_index is None:
139
+ continue
140
+ if label_index < len(labels):
141
+ unique_names[row] = str(labels[label_index])
142
+ if label_index < len(abbrevs):
143
+ unique_acronyms[row] = str(abbrevs[label_index])
144
+ if label_index < len(label_colors):
145
+ colors[int(label_id)] = tuple(
146
+ int(value) for value in np.ravel(label_colors[label_index])[:3]
147
+ )
148
+ return unique_names[inverse], unique_acronyms[inverse], colors
149
+
150
+
151
+ def _region_summary(structure_ids, names, acronyms, colors):
152
+ unique_ids, first_indexes, counts = np.unique(
153
+ structure_ids, return_index=True, return_counts=True
154
+ )
155
+ total = max(1, len(structure_ids))
156
+ rows = []
157
+ for label_id, sample, count in zip(unique_ids, first_indexes, counts):
158
+ if int(label_id) == 0:
159
+ name = names[sample] or "Outside atlas / unlabeled"
160
+ acronym = acronyms[sample] or "—"
161
+ color = (128, 128, 128)
162
+ else:
163
+ name = names[sample] or "Unknown structure"
164
+ acronym = acronyms[sample] or "—"
165
+ color = colors.get(int(label_id), (128, 128, 128))
166
+ rows.append(
167
+ {
168
+ "label_id": int(label_id),
169
+ "name": name,
170
+ "acronym": acronym,
171
+ "color": color,
172
+ "count": int(count),
173
+ "percentage": float(count) * 100.0 / total,
174
+ }
175
+ )
176
+ rows.sort(key=lambda row: (-row["count"], row["label_id"]))
177
+ return rows
178
+
179
+
180
+ def _drawing_metric(pieces, plot_mode, voxel_size_um):
181
+ scale_mm = float(voxel_size_um) / 1000.0
182
+ if plot_mode == "area":
183
+ return "sampled_area_mm2", sum(len(piece) for piece in pieces) * scale_mm**2
184
+
185
+ length_vox = 0.0
186
+ for piece in pieces:
187
+ if len(piece) > 1:
188
+ length_vox += float(np.linalg.norm(np.diff(piece, axis=0), axis=1).sum())
189
+ return "line_length_mm", length_vox * scale_mm
190
+
191
+
192
+ def build_drawing_roi_info(
193
+ pieces,
194
+ piece_names,
195
+ *,
196
+ bregma_herbs_vox,
197
+ voxel_size_um,
198
+ herbs_shape,
199
+ axis_info=None,
200
+ label_volume=None,
201
+ label_info=None,
202
+ ):
203
+ """Build an ROI report for one drawing piece or a merged drawing.
204
+
205
+ Drawing coordinates use HERBS' internal ``(ML, AP, dorsal DV)`` axes and
206
+ are relative to the configured Bregma voxel.
207
+ """
208
+ pieces = _drawing_pieces(pieces)
209
+ voxel_size_um = float(voxel_size_um)
210
+ if not np.isfinite(voxel_size_um) or voxel_size_um <= 0:
211
+ raise ValueError("Voxel size must be a positive finite value.")
212
+
213
+ herbs_shape = tuple(int(value) for value in herbs_shape)
214
+ if len(herbs_shape) != 3 or any(value <= 0 for value in herbs_shape):
215
+ raise ValueError("HERBS atlas shape must contain three positive dimensions.")
216
+ bregma = np.asarray(bregma_herbs_vox, dtype=float)
217
+ if bregma.shape != (3,):
218
+ raise ValueError("Bregma must contain three HERBS voxel coordinates.")
219
+
220
+ names = [str(name) for name in piece_names]
221
+ if len(names) != len(pieces):
222
+ raise ValueError("Each drawing piece must have one name.")
223
+
224
+ points = np.vstack(pieces)
225
+ piece_index = np.concatenate(
226
+ [np.full(len(piece), index + 1, dtype=int) for index, piece in enumerate(pieces)]
227
+ )
228
+ point_index = np.concatenate(
229
+ [np.arange(1, len(piece) + 1, dtype=int) for piece in pieces]
230
+ )
231
+ absolute_points = points + bregma
232
+ configured_mm = np.stack(
233
+ (points[:, 1], points[:, 0], -points[:, 2]), axis=1
234
+ ) * (voxel_size_um / 1000.0)
235
+
236
+ structure_ids = np.zeros(len(points), dtype=int)
237
+ label_array = None
238
+ if label_volume is not None:
239
+ label_array = np.asarray(label_volume)
240
+ if label_array.shape != herbs_shape:
241
+ raise ValueError("Atlas labels do not match the HERBS atlas shape.")
242
+ indexes = absolute_points.astype(int)
243
+ valid = np.all(
244
+ (indexes >= 0) & (indexes < np.asarray(herbs_shape, dtype=int)), axis=1
245
+ )
246
+ valid_indexes = indexes[valid]
247
+ structure_ids[valid] = label_array[
248
+ valid_indexes[:, 0], valid_indexes[:, 1], valid_indexes[:, 2]
249
+ ].astype(int)
250
+
251
+ structure_names, structure_acronyms, colors = _structure_details(
252
+ structure_ids, label_info
253
+ )
254
+ surface_depth = _surface_depth_mm(
255
+ absolute_points, label_array, voxel_size_um
256
+ )
257
+
258
+ normalized_axis_info = None
259
+ source_vox = None
260
+ estimated_mm = None
261
+ try:
262
+ normalized_axis_info = normalize_axis_info(axis_info, herbs_shape)
263
+ except (KeyError, TypeError, ValueError):
264
+ normalized_axis_info = None
265
+ if normalized_axis_info is not None and is_allen_ccf_2017(
266
+ normalized_axis_info, voxel_size_um
267
+ ):
268
+ source_vox = herbs_vox_to_source_vox(
269
+ absolute_points, normalized_axis_info
270
+ )
271
+ estimated_mm = allen_ccf_to_estimated_bregma_mm(
272
+ source_vox * voxel_size_um
273
+ )
274
+ reported_mm = estimated_mm[:, (0, 2, 1)]
275
+ coordinate_basis = "Estimated Allen Bregma"
276
+ ground_truth = False
277
+ else:
278
+ reported_mm = configured_mm
279
+ coordinate_basis = "Configured atlas Bregma"
280
+ ground_truth = None
281
+
282
+ finite_depth = surface_depth[np.isfinite(surface_depth)]
283
+ depth_summary = None
284
+ if finite_depth.size:
285
+ depth_summary = {
286
+ "mean": float(np.mean(finite_depth)),
287
+ "min": float(np.min(finite_depth)),
288
+ "max": float(np.max(finite_depth)),
289
+ "count": int(finite_depth.size),
290
+ }
291
+
292
+ plot_mode = "area" if names and "area" in names[0].lower() else "line"
293
+ metric_name, metric_value = _drawing_metric(
294
+ pieces, plot_mode, voxel_size_um
295
+ )
296
+ coordinates = {
297
+ "relative_herbs_vox": points,
298
+ "absolute_herbs_vox": absolute_points,
299
+ "configured_bregma_mm": configured_mm,
300
+ "surface_depth_mm": surface_depth,
301
+ }
302
+ if source_vox is not None:
303
+ coordinates["allen_ccf_vox"] = source_vox
304
+ coordinates["estimated_stereotaxic_bregma_mm"] = estimated_mm
305
+
306
+ return {
307
+ "plot_mode": plot_mode,
308
+ "pieces_names": names,
309
+ "point_count": int(len(points)),
310
+ "piece_index": piece_index,
311
+ "point_index": point_index,
312
+ "metric_name": metric_name,
313
+ "metric_value": float(metric_value),
314
+ "coordinate_basis": coordinate_basis,
315
+ "ground_truth": ground_truth,
316
+ "coordinate_summary": _axis_statistics(reported_mm),
317
+ "surface_depth_summary": depth_summary,
318
+ "coordinates": coordinates,
319
+ "structure_ids": structure_ids,
320
+ "structure_names": structure_names,
321
+ "structure_acronyms": structure_acronyms,
322
+ "regions": _region_summary(
323
+ structure_ids, structure_names, structure_acronyms, colors
324
+ ),
325
+ }
326
+
327
+
328
+ def iter_roi_csv_rows(info):
329
+ """Yield dictionaries suitable for a coordinate CSV export."""
330
+ coordinates = info["coordinates"]
331
+ relative = coordinates["relative_herbs_vox"]
332
+ configured = coordinates["configured_bregma_mm"]
333
+ surface_depth = coordinates["surface_depth_mm"]
334
+ source = coordinates.get("allen_ccf_vox")
335
+ estimated = coordinates.get("estimated_stereotaxic_bregma_mm")
336
+
337
+ for index in range(info["point_count"]):
338
+ row = {
339
+ "piece": int(info["piece_index"][index]),
340
+ "point": int(info["point_index"][index]),
341
+ "herbs_ML_vox_from_bregma": float(relative[index, 0]),
342
+ "herbs_AP_vox_from_bregma": float(relative[index, 1]),
343
+ "herbs_DV_dorsal_vox_from_bregma": float(relative[index, 2]),
344
+ "configured_AP_mm": float(configured[index, 0]),
345
+ "configured_ML_mm": float(configured[index, 1]),
346
+ "configured_DV_ventral_mm": float(configured[index, 2]),
347
+ "surface_depth_mm": (
348
+ float(surface_depth[index])
349
+ if np.isfinite(surface_depth[index])
350
+ else ""
351
+ ),
352
+ "structure_id": int(info["structure_ids"][index]),
353
+ "structure_acronym": str(info["structure_acronyms"][index]),
354
+ "structure_name": str(info["structure_names"][index]),
355
+ }
356
+ if source is not None:
357
+ row.update(
358
+ {
359
+ "allen_AP_vox": float(source[index, 0]),
360
+ "allen_DV_vox": float(source[index, 1]),
361
+ "allen_ML_vox": float(source[index, 2]),
362
+ "estimated_AP_mm": float(estimated[index, 0]),
363
+ "estimated_ML_mm": float(estimated[index, 2]),
364
+ "affine_DV_mm_not_for_targeting": float(estimated[index, 1]),
365
+ }
366
+ )
367
+ yield row
368
+
369
+
370
+ def write_roi_csv(path, info):
371
+ """Write every sampled drawing coordinate and its annotation to CSV."""
372
+ with open(path, "w", newline="", encoding="utf-8") as csv_file:
373
+ writer = csv.DictWriter(csv_file, fieldnames=ROI_CSV_FIELDS)
374
+ writer.writeheader()
375
+ writer.writerows(iter_roi_csv_rows(info))
@@ -0,0 +1,4 @@
1
+ def run():
2
+ from .app import main
3
+
4
+ return main()
@@ -0,0 +1,268 @@
1
+ import os
2
+ import sys
3
+ import numpy as np
4
+ import pyqtgraph as pg
5
+ from PyQt6.QtCore import *
6
+ from PyQt6.QtGui import *
7
+
8
+ from .coordinate_validation import coordinates_in_bounds
9
+ from .movable_points import TriangulationPoints
10
+
11
+
12
+ def image_position_in_bounds(position, image):
13
+ """Return whether a Qt/tuple image position can be indexed safely."""
14
+ if image is None:
15
+ return False
16
+ try:
17
+ if hasattr(position, "x") and hasattr(position, "y"):
18
+ x, y = position.x(), position.y()
19
+ else:
20
+ x, y = position
21
+ except (TypeError, ValueError, AttributeError):
22
+ return False
23
+ return coordinates_in_bounds((y, x), np.shape(image)[:2])
24
+
25
+
26
+ class ClickableSlice(pg.ImageItem):
27
+ class SignalProxy(QObject):
28
+ mouseHovered = pyqtSignal(object) # id
29
+ mouseClicked = pyqtSignal(object) # id
30
+
31
+ def __init__(self):
32
+ self._sigprox = ClickableSlice.SignalProxy()
33
+ self.mouseHovered = self._sigprox.mouseHovered
34
+ self.mouseClicked = self._sigprox.mouseClicked
35
+
36
+ pg.ImageItem.__init__(self)
37
+
38
+ self.setAcceptHoverEvents(True)
39
+ self.setLevels((0, 1))
40
+ self.setOpts(axisOrder='row-major')
41
+
42
+ def set_data(self, atlas_data, scale=None):
43
+ # self.label_data = label
44
+ # self.atlas_data = atlas_data
45
+ if scale is not None:
46
+ self.resetTransform()
47
+ self.scale(*scale)
48
+ self.setImage(atlas_data, autoLevels=False)
49
+ # self.label_img.setImage(self.label_data, autoLevels=False)
50
+
51
+ def hoverEvent(self, event):
52
+ if event.isExit():
53
+ return
54
+ try:
55
+ pos = (event.pos())
56
+ except (IndexError, AttributeError):
57
+ return
58
+ self.mouseHovered.emit(event)
59
+
60
+ def mouseClickEvent(self, event):
61
+ pos = (event.pos().x(), event.pos().y())
62
+ # Stop GraphicsScene from dispatching this click to stale curve
63
+ # candidates after the application callback changes the overlay.
64
+ event.accept()
65
+ # id = self.label_data[int(event.pos().x()), int(event.pos().y())]
66
+ self.mouseClicked.emit(pos)
67
+ # self.mouseClicked.emit([event, id])
68
+
69
+
70
+ class SliceStacks(pg.GraphicsLayoutWidget):
71
+ class SignalProxy(QObject):
72
+ mouseHovered = pyqtSignal(object) # id
73
+ mouseClicked = pyqtSignal(object) # id
74
+ pageStepRequested = pyqtSignal(int)
75
+
76
+ def __init__(self):
77
+ self._sigprox = SliceStacks.SignalProxy()
78
+ self.sig_mouse_hovered = self._sigprox.mouseHovered
79
+ self.sig_mouse_clicked = self._sigprox.mouseClicked
80
+ self.sig_page_step_requested = self._sigprox.pageStepRequested
81
+
82
+ pg.GraphicsLayoutWidget.__init__(self)
83
+ self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
84
+ self.vb = self.addViewBox()
85
+ self.setBackground('k')
86
+ self.vb.setAspectLocked()
87
+ self.vb.invertY(True)
88
+ self.vb.setAcceptHoverEvents(True)
89
+
90
+ # self.img = SliceStack()
91
+ self.img = ClickableSlice()
92
+ self.img.mouseClicked.connect(self.mouse_clicked)
93
+ self.img.mouseHovered.connect(self.mouse_hovered)
94
+ self.label_img = pg.ImageItem()
95
+ self.label_data = None
96
+ self.label_img.setCompositionMode(QPainter.CompositionMode.CompositionMode_Multiply)
97
+ self.boundary = pg.ImageItem()
98
+ self.boundary.setCompositionMode(QPainter.CompositionMode.CompositionMode_Plus)
99
+ self.boundary.setVisible(False)
100
+
101
+ self.display_objects = []
102
+
103
+ tri_pnts = TriangulationPoints()
104
+ self.tri_lines_list = []
105
+
106
+ circle_follow = pg.PlotDataItem(pen=pg.mkPen('r', width=2, style=Qt.PenStyle.DashLine))
107
+ lasso_path = pg.PlotDataItem(pen=pg.mkPen(color=(0, 255, 255), width=3, style=Qt.PenStyle.DashLine),
108
+ symbolPen=(0, 255, 255), symbol='o', symbolSize=4)
109
+ ruler_path = pg.PlotDataItem(pen=pg.mkPen(color='y', width=3, style=Qt.PenStyle.DashLine),
110
+ symbolPen='y', symbolBrush='y', symbol='s', symbolSize=3)
111
+
112
+ overlay_img = pg.ImageItem()
113
+
114
+ overlay_contour = pg.ImageItem()
115
+ overlay_contour.setLevels(levels=(0, 1))
116
+
117
+ mask_img = pg.ImageItem()
118
+ mask_img.setLevels(levels=(0, 1))
119
+
120
+ grid_lines = pg.GridItem(pen=(128, 128, 128))
121
+ grid_lines.setVisible(False)
122
+
123
+ virus_pnts = pg.ScatterPlotItem(pen=(133, 255, 117), brush=(133, 255, 117), symbol='s', size=5, hoverSize=8)
124
+ probe_pnts = pg.ScatterPlotItem(pen=(0, 0, 255), brush=(0, 0, 255), symbol='s', size=5, hoverSize=8)
125
+ cell_pnts = pg.ScatterPlotItem(pen=(0, 255, 0), brush=(0, 255, 0), size=5, hoverSize=8)
126
+ drawing_pnts = pg.PlotDataItem(pen=pg.mkPen(color=(255, 102, 0), width=2), brush=None)
127
+ contour_pnts = pg.PlotDataItem(pen=pg.mkPen(color=(255, 0, 255), width=3), brush=None)
128
+ probe_trajectory = pg.PlotDataItem(pen=pg.mkPen(color=(0, 0, 255), width=2), brush=None)
129
+
130
+ self.pre_trajectory_list = [pg.PlotDataItem(pen=pg.mkPen(color=(0, 0, 255), width=2), brush=None)]
131
+
132
+ self.image_dict = {'atlas-overlay': overlay_img,
133
+ 'atlas-mask': mask_img,
134
+ 'grid_lines': grid_lines,
135
+ 'tri_pnts': tri_pnts,
136
+ 'circle_follow': circle_follow,
137
+ 'lasso_path': lasso_path,
138
+ 'atlas-virus': virus_pnts,
139
+ 'atlas-contour': contour_pnts,
140
+ 'atlas-probe': probe_pnts,
141
+ 'atlas-cells': cell_pnts,
142
+ 'ruler_path': ruler_path,
143
+ 'atlas-drawing': drawing_pnts,
144
+ 'atlas-trajectory': probe_trajectory}
145
+ self.image_dict_keys = list(self.image_dict.keys())
146
+
147
+ self.v_line = pg.InfiniteLine(angle=90, movable=False)
148
+ self.h_line = pg.InfiniteLine(angle=0, movable=False)
149
+ self.v_line.setVisible(False)
150
+ self.h_line.setVisible(False)
151
+
152
+ self.vb.addItem(self.img)
153
+ self.vb.addItem(self.label_img)
154
+
155
+ for i in range(len(self.image_dict)):
156
+ self.vb.addItem(self.image_dict[self.image_dict_keys[i]])
157
+
158
+ self.vb.addItem(self.pre_trajectory_list[0])
159
+
160
+ self.vb.addItem(self.boundary)
161
+
162
+ # self.vb.addItem(self.after_trajectory)
163
+
164
+ self.vb.addItem(self.v_line, ignoreBounds=True)
165
+ self.vb.addItem(self.h_line, ignoreBounds=True)
166
+
167
+ def set_pre_design_vis_data(self, data):
168
+ n_probe = len(data)
169
+ exist_n_probes = len(self.pre_trajectory_list)
170
+ if n_probe < exist_n_probes:
171
+ del_ind = np.arange(n_probe, exist_n_probes)[::-1]
172
+ for i in del_ind:
173
+ self.vb.removeItem(self.pre_trajectory_list[i])
174
+ self.pre_trajectory_list[i].deleteLater()
175
+ del self.pre_trajectory_list[i]
176
+ elif n_probe > exist_n_probes:
177
+ for i in range(exist_n_probes, n_probe):
178
+ self.pre_trajectory_list.append(pg.PlotDataItem(pen=pg.mkPen(color=(0, 0, 255), width=2), brush=None))
179
+ self.vb.addItem(self.pre_trajectory_list[-1])
180
+
181
+ for i in range(n_probe):
182
+ self.pre_trajectory_list[i].setData(data[i])
183
+
184
+ def pre_trajectory_color_changed(self, col, lw):
185
+ for i in range(len(self.pre_trajectory_list)):
186
+ self.pre_trajectory_list[i].setPen(pg.mkPen(color=col, width=lw))
187
+
188
+ def remove_pre_trajectories_vis_lines(self):
189
+ del_ind = np.arange(len(self.pre_trajectory_list))[::-1]
190
+ for i in del_ind:
191
+ self.vb.removeItem(self.pre_trajectory_list[i])
192
+ self.pre_trajectory_list[i].deleteLater()
193
+ del self.pre_trajectory_list[i]
194
+ # self.pre_trajectory_list[i].updateItems(True)
195
+ self.pre_trajectory_list.clear()
196
+
197
+
198
+ def set_data(self, atlas, label, contour, scale=None, display_label=None):
199
+ self.img.set_data(atlas)
200
+ self.label_data = label
201
+ if display_label is None:
202
+ display_label = label
203
+ self.label_img.setImage(display_label, autoLevels=False)
204
+ if contour is not None:
205
+ self.set_boundary_data(contour)
206
+
207
+ def set_boundary_data(self, contour):
208
+ self.boundary.setImage(contour, autoLevels=False)
209
+
210
+ def add_display_obj_to_view(self, obj_data, obj_color, obj_type):
211
+ if obj_type == 'probe':
212
+ obj_vis = pg.PlotDataItem(pen=pg.mkPen(color=obj_color, width=2),
213
+ symbolPen=obj_color, symbolBrush=None, symbol='s', symbolSize=3)
214
+ obj_vis.setData(np.asarray(obj_data))
215
+ self.display_objects.append(obj_vis)
216
+ self.vb.addItem(self.display_objects[-1])
217
+
218
+ def remove_display_obj_from_view(self, obj_index):
219
+ self.vb.removeItem(self.display_objects[obj_index])
220
+ self.display_objects[obj_index].deleteLater()
221
+ del self.display_objects[obj_index]
222
+
223
+ def boundingRect(self):
224
+ return self.label_img.boundingRect()
225
+
226
+ def set_lut(self, lut):
227
+ self.label_img.setLookupTable(lut)
228
+
229
+ def set_overlay(self, overlay):
230
+ mode = getattr(
231
+ QPainter.CompositionMode, "CompositionMode_" + overlay
232
+ )
233
+ self.label_img.setCompositionMode(mode)
234
+
235
+ def set_label_opacity(self, o):
236
+ self.label_img.setOpacity(o)
237
+
238
+ # def set_label_colors(self, colors):
239
+ # self.label_colors = colors
240
+
241
+ def mouse_hovered(self, event):
242
+ if event.isExit():
243
+ return
244
+ try:
245
+ pos = (event.pos())
246
+ except (IndexError, AttributeError):
247
+ return
248
+ if not image_position_in_bounds(pos, self.img.image):
249
+ return
250
+ self.sig_mouse_hovered.emit(pos)
251
+
252
+ def mouse_clicked(self, pos):
253
+ if not image_position_in_bounds(pos, self.img.image):
254
+ return
255
+ self.setFocus(Qt.FocusReason.MouseFocusReason)
256
+ self.sig_mouse_clicked.emit(pos)
257
+
258
+ def keyPressEvent(self, event):
259
+ if event.modifiers() == Qt.KeyboardModifier.NoModifier:
260
+ if event.key() == Qt.Key.Key_Left:
261
+ self.sig_page_step_requested.emit(-1)
262
+ event.accept()
263
+ return
264
+ if event.key() == Qt.Key.Key_Right:
265
+ self.sig_page_step_requested.emit(1)
266
+ event.accept()
267
+ return
268
+ super().keyPressEvent(event)
@@ -0,0 +1,20 @@
1
+ import numpy as np
2
+
3
+
4
+ def slice_info_is_ready(width, height, distance, bregma):
5
+ """Return whether slice dimensions and spatial registration are usable."""
6
+ try:
7
+ dimensions = np.asarray([width, height], dtype=float)
8
+ distance_value = float(distance)
9
+ origin = np.asarray(bregma, dtype=float).reshape(-1)
10
+ except (TypeError, ValueError):
11
+ return False
12
+
13
+ return bool(
14
+ dimensions.shape == (2,)
15
+ and np.all(np.isfinite(dimensions))
16
+ and np.all(dimensions > 0)
17
+ and np.isfinite(distance_value)
18
+ and origin.shape == (2,)
19
+ and np.all(np.isfinite(origin))
20
+ )