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,409 @@
1
+ """Build self-contained coordinate metadata for exported probe objects."""
2
+
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+
7
+
8
+ PROBE_RECONSTRUCTION_SCHEMA_VERSION = 1
9
+ HERBS_AXES = ("LR", "AP", "DV")
10
+ HERBS_AXIS_DIRECTIONS = ("right", "anterior", "superior")
11
+ _OPPOSITE_DIRECTION = {
12
+ "right": "left",
13
+ "left": "right",
14
+ "anterior": "posterior",
15
+ "posterior": "anterior",
16
+ "superior": "inferior",
17
+ "inferior": "superior",
18
+ }
19
+ _ALLEN_CCF_2017_SHAPES = {
20
+ 10.0: (1320, 800, 1140),
21
+ 25.0: (528, 320, 456),
22
+ 50.0: (264, 160, 228),
23
+ 100.0: (132, 80, 114),
24
+ }
25
+ ALLEN_CCF_ESTIMATED_BREGMA_UM = np.array([5400.0, 440.0, 5700.0])
26
+ ALLEN_CCF_SAGITTAL_TILT_DEG = 5.0
27
+ ALLEN_CCF_DV_SCALE = 0.9434
28
+
29
+
30
+ def allen_ccf_estimated_bregma_vox(voxel_size_um):
31
+ """Return the nearest source voxel to the estimated Allen CCF Bregma."""
32
+ voxel_size_um = float(voxel_size_um)
33
+ if not np.isfinite(voxel_size_um) or voxel_size_um <= 0:
34
+ raise ValueError("Voxel size must be a positive finite value.")
35
+ return np.floor(
36
+ ALLEN_CCF_ESTIMATED_BREGMA_UM / voxel_size_um + 0.5
37
+ ).astype(int)
38
+
39
+
40
+ def normalize_axis_info(axis_info, herbs_shape):
41
+ """Validate an atlas transform and fill fields required for inversion."""
42
+ herbs_shape = tuple(int(value) for value in herbs_shape)
43
+ if len(herbs_shape) != 3 or any(value <= 0 for value in herbs_shape):
44
+ raise ValueError("HERBS atlas shape must contain three positive dimensions.")
45
+
46
+ supplied = axis_info is not None
47
+ if axis_info is None:
48
+ axis_info = {
49
+ "to_HERBS": (0, 1, 2),
50
+ "from_HERBS": (0, 1, 2),
51
+ "direction_change": (False, False, False),
52
+ "size": herbs_shape,
53
+ }
54
+
55
+ to_herbs = tuple(int(value) for value in axis_info["to_HERBS"])
56
+ if len(to_herbs) != 3 or sorted(to_herbs) != [0, 1, 2]:
57
+ raise ValueError("to_HERBS must be a permutation of three atlas axes.")
58
+
59
+ inverse = tuple(int(value) for value in np.argsort(to_herbs))
60
+ from_herbs = tuple(
61
+ int(value) for value in axis_info.get("from_HERBS", inverse)
62
+ )
63
+ if from_herbs != inverse:
64
+ raise ValueError("from_HERBS is not the inverse of to_HERBS.")
65
+
66
+ direction_change = tuple(bool(value) for value in axis_info["direction_change"])
67
+ if len(direction_change) != 3:
68
+ raise ValueError("direction_change must describe exactly three source axes.")
69
+
70
+ if "size" in axis_info:
71
+ source_shape = tuple(int(value) for value in axis_info["size"])
72
+ else:
73
+ source_shape_array = np.empty(3, dtype=int)
74
+ source_shape_array[np.asarray(to_herbs)] = np.asarray(herbs_shape)
75
+ source_shape = tuple(source_shape_array.tolist())
76
+ if len(source_shape) != 3 or any(value <= 0 for value in source_shape):
77
+ raise ValueError("Source atlas shape must contain three positive dimensions.")
78
+
79
+ expected_herbs_shape = tuple(np.asarray(source_shape)[np.asarray(to_herbs)])
80
+ if expected_herbs_shape != herbs_shape:
81
+ raise ValueError(
82
+ "Atlas axis metadata does not match the atlas used for the probe."
83
+ )
84
+
85
+ return {
86
+ "to_HERBS": to_herbs,
87
+ "from_HERBS": from_herbs,
88
+ "direction_change": direction_change,
89
+ "size": source_shape,
90
+ "available_from_atlas": supplied,
91
+ }
92
+
93
+
94
+ def herbs_vox_to_source_vox(points, axis_info):
95
+ """Convert one or more continuous HERBS voxels into source-atlas voxels."""
96
+ points = np.asarray(points, dtype=float)
97
+ if points.shape[-1:] != (3,):
98
+ raise ValueError("Coordinate arrays must end with three values.")
99
+
100
+ source_points = points[..., np.asarray(axis_info["from_HERBS"])].copy()
101
+ source_shape = np.asarray(axis_info["size"], dtype=float)
102
+ for axis, should_flip in enumerate(axis_info["direction_change"]):
103
+ if should_flip:
104
+ source_points[..., axis] = (
105
+ source_shape[axis] - 1 - source_points[..., axis]
106
+ )
107
+ return source_points
108
+
109
+
110
+ def volume_view_vox_to_source_vox(points, view_shape, axis_info):
111
+ """Convert AtlasView ``(DV, ML, AP-view)`` voxels to source-atlas voxels."""
112
+ points = np.asarray(points, dtype=float)
113
+ view_shape = tuple(int(value) for value in view_shape)
114
+ if points.shape[-1:] != (3,):
115
+ raise ValueError("Coordinate arrays must end with three values.")
116
+ if len(view_shape) != 3 or any(value <= 0 for value in view_shape):
117
+ raise ValueError("Atlas view shape must contain three positive dimensions.")
118
+
119
+ herbs_points = np.stack(
120
+ (
121
+ points[..., 1],
122
+ points[..., 2],
123
+ view_shape[0] - 1 - points[..., 0],
124
+ ),
125
+ axis=-1,
126
+ )
127
+ herbs_shape = (view_shape[1], view_shape[2], view_shape[0])
128
+ normalized_axis_info = normalize_axis_info(axis_info, herbs_shape)
129
+ return herbs_vox_to_source_vox(herbs_points, normalized_axis_info)
130
+
131
+
132
+ def allen_ccf_to_estimated_bregma_mm(ccf_um):
133
+ """Approximate Allen CCF ``(AP, DV, ML)`` µm as stereotaxic millimeters.
134
+
135
+ The returned axes are ``(AP, DV, ML)`` with positive AP anterior, positive
136
+ DV ventral, and positive ML right. This is a community-derived
137
+ approximation, not a ground-truth targeting transform.
138
+ """
139
+ ccf_um = np.asarray(ccf_um, dtype=float)
140
+ if ccf_um.shape[-1:] != (3,):
141
+ raise ValueError("Allen CCF coordinates must end with AP, DV, and ML.")
142
+
143
+ centered = ccf_um - ALLEN_CCF_ESTIMATED_BREGMA_UM
144
+ ap = centered[..., 0]
145
+ dv = centered[..., 1]
146
+ ml = centered[..., 2]
147
+ angle = np.deg2rad(ALLEN_CCF_SAGITTAL_TILT_DEG)
148
+ rotated_ap = ap * np.cos(angle) - dv * np.sin(angle)
149
+ rotated_dv = (
150
+ ap * np.sin(angle) + dv * np.cos(angle)
151
+ ) * ALLEN_CCF_DV_SCALE
152
+ return np.stack((-rotated_ap, rotated_dv, ml), axis=-1) / 1000.0
153
+
154
+
155
+ def format_estimated_bregma_report(estimated_mm, surface_depth_um, region=""):
156
+ """Format the concise Allen coordinate report used by the status bar."""
157
+ estimated_mm = np.asarray(estimated_mm, dtype=float)
158
+ if estimated_mm.shape != (3,):
159
+ raise ValueError("Estimated coordinates must contain AP, DV, and ML.")
160
+ report = (
161
+ "Bregma est.: AP {:+.2f} mm | ML {:+.2f} mm | "
162
+ "Depth {:.2f} mm from surface"
163
+ ).format(
164
+ estimated_mm[0],
165
+ estimated_mm[2],
166
+ float(surface_depth_um) / 1000.0,
167
+ )
168
+ if region:
169
+ report = "{} | {}".format(report, region)
170
+ return report
171
+
172
+
173
+ def _source_axis_metadata(axis_info):
174
+ source_axes = [None, None, None]
175
+ source_directions = [None, None, None]
176
+ for herbs_axis, source_axis in enumerate(axis_info["to_HERBS"]):
177
+ source_axes[source_axis] = HERBS_AXES[herbs_axis]
178
+ direction = HERBS_AXIS_DIRECTIONS[herbs_axis]
179
+ if axis_info["direction_change"][source_axis]:
180
+ direction = _OPPOSITE_DIRECTION[direction]
181
+ source_directions[source_axis] = direction
182
+ return source_axes, source_directions
183
+
184
+
185
+ def is_allen_ccf_2017(axis_info, voxel_size_um):
186
+ expected_shape = _ALLEN_CCF_2017_SHAPES.get(float(voxel_size_um))
187
+ return (
188
+ expected_shape == tuple(axis_info["size"])
189
+ and tuple(axis_info["to_HERBS"]) == (2, 0, 1)
190
+ and tuple(axis_info["direction_change"]) == (True, True, False)
191
+ )
192
+
193
+
194
+ def _flatten_groups(groups, dtype=None):
195
+ arrays = [np.asarray(group, dtype=dtype) for group in groups]
196
+ if not arrays:
197
+ return np.empty((0,), dtype=dtype)
198
+ return np.concatenate(arrays, axis=0)
199
+
200
+
201
+ def _structure_text(structure_ids, label_info, key, default=""):
202
+ lookup = {
203
+ int(label_id): str(value)
204
+ for label_id, value in zip(
205
+ np.ravel(label_info["index"]), np.ravel(label_info[key])
206
+ )
207
+ }
208
+ return [lookup.get(int(label_id), default) for label_id in structure_ids]
209
+
210
+
211
+ def _coordinate_record(relative_bregma_vox, bregma_herbs_vox, voxel_size_um,
212
+ axis_info, voxel_index=None, allen_ccf=False):
213
+ relative_bregma_vox = np.asarray(relative_bregma_vox, dtype=float)
214
+ herbs_vox = relative_bregma_vox + bregma_herbs_vox
215
+ source_vox = herbs_vox_to_source_vox(herbs_vox, axis_info)
216
+ record = {
217
+ "herbs_vox": herbs_vox,
218
+ "herbs_vox_index": (
219
+ np.asarray(voxel_index, dtype=int)
220
+ if voxel_index is not None
221
+ else herbs_vox.astype(int)
222
+ ),
223
+ "bregma_um": relative_bregma_vox * voxel_size_um,
224
+ "source_vox": source_vox,
225
+ "source_um": source_vox * voxel_size_um,
226
+ }
227
+ if allen_ccf:
228
+ record["allen_ccf_vox"] = record["source_vox"].copy()
229
+ record["allen_ccf_um"] = record["source_um"].copy()
230
+ record["estimated_stereotaxic_bregma_mm"] = (
231
+ allen_ccf_to_estimated_bregma_mm(record["allen_ccf_um"])
232
+ )
233
+ return record
234
+
235
+
236
+ def build_probe_reconstruction(
237
+ *,
238
+ insertion_bregma_vox,
239
+ terminus_bregma_vox,
240
+ insertion_vox_index,
241
+ terminus_vox_index,
242
+ contact_bregma_vox,
243
+ contact_vox_index,
244
+ contact_structure_ids,
245
+ contact_local_from_tip_base_um,
246
+ probe_length_um,
247
+ probe_settings,
248
+ site_face,
249
+ voxel_size_um,
250
+ bregma_herbs_vox,
251
+ herbs_atlas_shape,
252
+ label_info,
253
+ axis_info=None,
254
+ atlas_identifier=None,
255
+ atlas_path=None,
256
+ software_version=None,
257
+ trajectory_fit=None,
258
+ ):
259
+ """Create the reconstruction payload embedded in each merged probe.
260
+
261
+ Contacts are flattened column-major. Within a column, index zero is the
262
+ contact nearest the geometric tip for probe geometries generated by HERBS.
263
+ """
264
+ voxel_size_um = float(voxel_size_um)
265
+ if not np.isfinite(voxel_size_um) or voxel_size_um <= 0:
266
+ raise ValueError("Atlas voxel size must be a positive number.")
267
+ bregma_herbs_vox = np.asarray(bregma_herbs_vox, dtype=float)
268
+ if bregma_herbs_vox.shape != (3,):
269
+ raise ValueError("Bregma must contain three HERBS voxel coordinates.")
270
+
271
+ normalized_axis_info = normalize_axis_info(axis_info, herbs_atlas_shape)
272
+ source_axes, source_directions = _source_axis_metadata(normalized_axis_info)
273
+ allen_ccf = is_allen_ccf_2017(normalized_axis_info, voxel_size_um)
274
+
275
+ contact_counts = [len(group) for group in contact_bregma_vox]
276
+ if not (
277
+ contact_counts == [len(group) for group in contact_vox_index]
278
+ == [len(group) for group in contact_structure_ids]
279
+ == [len(group) for group in contact_local_from_tip_base_um]
280
+ ):
281
+ raise ValueError("Probe contact coordinate groups do not have matching sizes.")
282
+
283
+ contact_bregma_vox_flat = _flatten_groups(contact_bregma_vox, dtype=float)
284
+ contact_vox_index_flat = _flatten_groups(contact_vox_index, dtype=int)
285
+ structure_ids = _flatten_groups(contact_structure_ids, dtype=int)
286
+ local_from_tip_base_um = _flatten_groups(
287
+ contact_local_from_tip_base_um, dtype=float
288
+ )
289
+ column_index = np.concatenate(
290
+ [np.full(count, column, dtype=int) for column, count in enumerate(contact_counts)]
291
+ ) if contact_counts else np.empty((0,), dtype=int)
292
+ index_in_column = np.concatenate(
293
+ [np.arange(count, dtype=int) for count in contact_counts]
294
+ ) if contact_counts else np.empty((0,), dtype=int)
295
+
296
+ contact_herbs_vox = contact_bregma_vox_flat + bregma_herbs_vox
297
+ contact_source_vox = herbs_vox_to_source_vox(
298
+ contact_herbs_vox, normalized_axis_info
299
+ )
300
+ tip_length_um = float(probe_settings.get("tip_length") or 0)
301
+ distance_from_tip_um = local_from_tip_base_um[:, 0] + tip_length_um
302
+ contact_local_um = local_from_tip_base_um.copy()
303
+ contact_local_um[:, 0] = distance_from_tip_um
304
+
305
+ contacts = {
306
+ "count": int(len(structure_ids)),
307
+ "site_index": np.arange(len(structure_ids), dtype=int),
308
+ "column_index": column_index,
309
+ "index_in_column": index_in_column,
310
+ "column_contact_counts": np.asarray(contact_counts, dtype=int),
311
+ "ordering": "column-major; index_in_column 0 is tip-nearest",
312
+ "probe_local_axes": ["distance_from_tip", "lateral", "surface_normal"],
313
+ "probe_local_um": contact_local_um,
314
+ "distance_from_tip_um": distance_from_tip_um,
315
+ "distance_from_insertion_um": float(probe_length_um) - distance_from_tip_um,
316
+ "axial_distance_up_from_tip_um": distance_from_tip_um,
317
+ "axial_depth_from_insertion_um": (
318
+ float(probe_length_um) - distance_from_tip_um
319
+ ),
320
+ "herbs_vox": contact_herbs_vox,
321
+ "herbs_vox_index": contact_vox_index_flat,
322
+ "bregma_um": contact_bregma_vox_flat * voxel_size_um,
323
+ "source_vox": contact_source_vox,
324
+ "source_um": contact_source_vox * voxel_size_um,
325
+ "structure_id": structure_ids,
326
+ "structure_acronym": _structure_text(
327
+ structure_ids, label_info, "abbrev"
328
+ ),
329
+ "structure_name": _structure_text(structure_ids, label_info, "label"),
330
+ }
331
+ if allen_ccf:
332
+ contacts["allen_ccf_vox"] = contacts["source_vox"].copy()
333
+ contacts["allen_ccf_um"] = contacts["source_um"].copy()
334
+ contacts["estimated_stereotaxic_bregma_mm"] = (
335
+ allen_ccf_to_estimated_bregma_mm(contacts["allen_ccf_um"])
336
+ )
337
+
338
+ source_name = "Allen Mouse Common Coordinate Framework"
339
+ source_version = "CCFv3 2017"
340
+ if not allen_ccf:
341
+ source_name = atlas_identifier or "Source atlas"
342
+ source_version = None
343
+
344
+ atlas = {
345
+ "identifier": atlas_identifier,
346
+ "path_at_export": str(Path(atlas_path).resolve()) if atlas_path else None,
347
+ "voxel_size_um": voxel_size_um,
348
+ "herbs_shape_vox": tuple(int(value) for value in herbs_atlas_shape),
349
+ "bregma_herbs_vox": bregma_herbs_vox,
350
+ "herbs_axes": list(HERBS_AXES),
351
+ "herbs_axis_directions": list(HERBS_AXIS_DIRECTIONS),
352
+ "source_name": source_name,
353
+ "source_version": source_version,
354
+ "source_shape_vox": tuple(normalized_axis_info["size"]),
355
+ "source_axes": source_axes,
356
+ "source_axis_directions": source_directions,
357
+ "bregma_source_vox": herbs_vox_to_source_vox(
358
+ bregma_herbs_vox, normalized_axis_info
359
+ ),
360
+ "axis_transform": normalized_axis_info,
361
+ "label_lookup": label_info,
362
+ }
363
+ atlas["bregma_source_um"] = atlas["bregma_source_vox"] * voxel_size_um
364
+ if allen_ccf:
365
+ atlas["estimated_stereotaxic_transform"] = {
366
+ "name": "Community-estimated Allen CCF to Bregma",
367
+ "coordinate_order": ["AP", "DV", "ML"],
368
+ "units": "mm",
369
+ "positive_directions": ["anterior", "ventral", "right"],
370
+ "ccf_bregma_um": ALLEN_CCF_ESTIMATED_BREGMA_UM.copy(),
371
+ "sagittal_tilt_deg": ALLEN_CCF_SAGITTAL_TILT_DEG,
372
+ "dv_scale": ALLEN_CCF_DV_SCALE,
373
+ "ground_truth": False,
374
+ "targeting_note": (
375
+ "Approximate only; use measured depth from brain surface for "
376
+ "surgical targeting rather than transformed DV."
377
+ ),
378
+ }
379
+
380
+ return {
381
+ "schema_version": PROBE_RECONSTRUCTION_SCHEMA_VERSION,
382
+ "software": {"name": "DriftlessMap", "version": software_version},
383
+ "atlas": atlas,
384
+ "probe": {
385
+ "settings": probe_settings,
386
+ "site_face": site_face,
387
+ "contact_ordering": contacts["ordering"],
388
+ "trajectory_fit": trajectory_fit,
389
+ },
390
+ "coordinates": {
391
+ "tip": _coordinate_record(
392
+ terminus_bregma_vox,
393
+ bregma_herbs_vox,
394
+ voxel_size_um,
395
+ normalized_axis_info,
396
+ voxel_index=terminus_vox_index,
397
+ allen_ccf=allen_ccf,
398
+ ),
399
+ "insertion": _coordinate_record(
400
+ insertion_bregma_vox,
401
+ bregma_herbs_vox,
402
+ voxel_size_um,
403
+ normalized_axis_info,
404
+ voxel_index=insertion_vox_index,
405
+ allen_ccf=allen_ccf,
406
+ ),
407
+ "contacts": contacts,
408
+ },
409
+ }