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,517 @@
1
+ """Validated, reproducible piecewise-affine registration for DriftlessMap."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from scipy.spatial import Delaunay, QhullError
8
+
9
+
10
+ TRIANGULATION_SCHEMA_VERSION = 1
11
+ _DUPLICATE_DISTANCE_PX = 0.5
12
+ _MIN_DOUBLE_AREA_PX2 = 1.0
13
+ _BARYCENTRIC_TOLERANCE = 1e-6
14
+ _MAP_TILE_ROWS = 256
15
+
16
+
17
+ class TriangulationError(ValueError):
18
+ """Raised when paired landmarks cannot define a safe registration."""
19
+
20
+
21
+ def _as_points(points, name):
22
+ points = np.asarray(points, dtype=float)
23
+ if points.ndim != 2 or points.shape[1:] != (2,):
24
+ raise TriangulationError(
25
+ "{} landmarks must have shape (N, 2).".format(name)
26
+ )
27
+ if not np.all(np.isfinite(points)):
28
+ raise TriangulationError(
29
+ "{} landmarks contain non-finite coordinates.".format(name)
30
+ )
31
+ return points
32
+
33
+
34
+ def _as_shape(shape, name):
35
+ try:
36
+ shape = tuple(int(value) for value in shape[:2])
37
+ except (TypeError, ValueError, IndexError):
38
+ raise TriangulationError(
39
+ "{} image shape must contain height and width.".format(name)
40
+ ) from None
41
+ if len(shape) != 2 or any(value <= 0 for value in shape):
42
+ raise TriangulationError(
43
+ "{} image shape must contain positive height and width.".format(name)
44
+ )
45
+ return shape
46
+
47
+
48
+ def _validate_bounds(points, shape, name):
49
+ height, width = shape
50
+ valid = (
51
+ (points[:, 0] >= 0)
52
+ & (points[:, 0] <= width - 1)
53
+ & (points[:, 1] >= 0)
54
+ & (points[:, 1] <= height - 1)
55
+ )
56
+ if not np.all(valid):
57
+ bad = int(np.flatnonzero(~valid)[0])
58
+ raise TriangulationError(
59
+ "{} landmark {} is outside the image.".format(name, bad + 1)
60
+ )
61
+
62
+
63
+ def _validate_duplicates(points, name):
64
+ if len(points) < 2:
65
+ return
66
+ deltas = points[:, None, :] - points[None, :, :]
67
+ distances = np.linalg.norm(deltas, axis=2)
68
+ np.fill_diagonal(distances, np.inf)
69
+ duplicate = np.argwhere(distances < _DUPLICATE_DISTANCE_PX)
70
+ if duplicate.size:
71
+ first, second = duplicate[0]
72
+ raise TriangulationError(
73
+ "{} landmarks {} and {} are duplicates or too close together.".format(
74
+ name, int(first) + 1, int(second) + 1
75
+ )
76
+ )
77
+
78
+
79
+ def _canonical_simplices(points):
80
+ try:
81
+ simplices = Delaunay(points).simplices
82
+ except QhullError as exc:
83
+ raise TriangulationError(
84
+ "Landmarks are collinear or cannot form a Delaunay mesh."
85
+ ) from exc
86
+ simplices = np.sort(np.asarray(simplices, dtype=np.int32), axis=1)
87
+ order = np.lexsort(
88
+ (simplices[:, 2], simplices[:, 1], simplices[:, 0])
89
+ )
90
+ return simplices[order]
91
+
92
+
93
+ def _validate_simplices(simplices, point_count):
94
+ simplices = np.asarray(simplices, dtype=np.int32)
95
+ if simplices.ndim != 2 or simplices.shape[1:] != (3,):
96
+ raise TriangulationError("Triangle connectivity must have shape (T, 3).")
97
+ if not len(simplices):
98
+ raise TriangulationError("The landmark mesh contains no triangles.")
99
+ if np.any(simplices < 0) or np.any(simplices >= point_count):
100
+ raise TriangulationError("Triangle connectivity references a missing landmark.")
101
+ if np.any(
102
+ np.diff(np.sort(simplices, axis=1), axis=1) == 0
103
+ ):
104
+ raise TriangulationError("A triangle references the same landmark twice.")
105
+ unique_rows = np.unique(np.sort(simplices, axis=1), axis=0)
106
+ if len(unique_rows) != len(simplices):
107
+ raise TriangulationError("Triangle connectivity contains duplicates.")
108
+ return simplices
109
+
110
+
111
+ def _signed_double_area(triangles):
112
+ edge_1 = triangles[:, 1] - triangles[:, 0]
113
+ edge_2 = triangles[:, 2] - triangles[:, 0]
114
+ return edge_1[:, 0] * edge_2[:, 1] - edge_1[:, 1] * edge_2[:, 0]
115
+
116
+
117
+ def _minimum_angles(triangles):
118
+ result = np.zeros(len(triangles), dtype=float)
119
+ for index, triangle in enumerate(triangles):
120
+ sides = np.array(
121
+ [
122
+ np.linalg.norm(triangle[1] - triangle[2]),
123
+ np.linalg.norm(triangle[0] - triangle[2]),
124
+ np.linalg.norm(triangle[0] - triangle[1]),
125
+ ],
126
+ dtype=float,
127
+ )
128
+ if np.any(sides <= 0):
129
+ result[index] = 0.0
130
+ continue
131
+ cosines = np.array(
132
+ [
133
+ (sides[1] ** 2 + sides[2] ** 2 - sides[0] ** 2)
134
+ / (2 * sides[1] * sides[2]),
135
+ (sides[0] ** 2 + sides[2] ** 2 - sides[1] ** 2)
136
+ / (2 * sides[0] * sides[2]),
137
+ (sides[0] ** 2 + sides[1] ** 2 - sides[2] ** 2)
138
+ / (2 * sides[0] * sides[1]),
139
+ ]
140
+ )
141
+ result[index] = float(
142
+ np.min(np.degrees(np.arccos(np.clip(cosines, -1.0, 1.0))))
143
+ )
144
+ return result
145
+
146
+
147
+ def _affine_condition_numbers(source_triangles, target_triangles):
148
+ conditions = np.full(len(source_triangles), np.inf, dtype=float)
149
+ for index, (source, target) in enumerate(
150
+ zip(source_triangles, target_triangles)
151
+ ):
152
+ source_edges = (source[1:] - source[0]).T
153
+ target_edges = (target[1:] - target[0]).T
154
+ try:
155
+ linear = target_edges @ np.linalg.inv(source_edges)
156
+ except np.linalg.LinAlgError:
157
+ continue
158
+ singular_values = np.linalg.svd(linear, compute_uv=False)
159
+ if singular_values[-1] > np.finfo(float).eps:
160
+ conditions[index] = float(
161
+ singular_values[0] / singular_values[-1]
162
+ )
163
+ return conditions
164
+
165
+
166
+ def _mesh_quality(atlas_points, histology_points, simplices):
167
+ atlas_triangles = atlas_points[simplices]
168
+ histology_triangles = histology_points[simplices]
169
+ atlas_area = _signed_double_area(atlas_triangles)
170
+ histology_area = _signed_double_area(histology_triangles)
171
+ degenerate = (
172
+ (np.abs(atlas_area) < _MIN_DOUBLE_AREA_PX2)
173
+ | (np.abs(histology_area) < _MIN_DOUBLE_AREA_PX2)
174
+ )
175
+ folded = (atlas_area * histology_area < 0) & ~degenerate
176
+
177
+ area_ratio = np.full(len(simplices), np.nan, dtype=float)
178
+ usable = np.abs(histology_area) >= _MIN_DOUBLE_AREA_PX2
179
+ area_ratio[usable] = (
180
+ np.abs(atlas_area[usable]) / np.abs(histology_area[usable])
181
+ )
182
+ finite_ratio = area_ratio[np.isfinite(area_ratio) & (area_ratio > 0)]
183
+ median_ratio = float(np.median(finite_ratio)) if finite_ratio.size else 1.0
184
+ normalized_area_ratio = area_ratio / median_ratio
185
+
186
+ minimum_angle = np.minimum(
187
+ _minimum_angles(atlas_triangles),
188
+ _minimum_angles(histology_triangles),
189
+ )
190
+ condition = _affine_condition_numbers(
191
+ histology_triangles, atlas_triangles
192
+ )
193
+
194
+ severity = np.zeros(len(simplices), dtype=np.uint8)
195
+ warning = (
196
+ (minimum_angle < 15.0)
197
+ | (condition > 6.0)
198
+ | (normalized_area_ratio < 0.25)
199
+ | (normalized_area_ratio > 4.0)
200
+ )
201
+ severe = (
202
+ degenerate
203
+ | folded
204
+ | (minimum_angle < 5.0)
205
+ | (condition > 20.0)
206
+ | (normalized_area_ratio < 0.05)
207
+ | (normalized_area_ratio > 20.0)
208
+ )
209
+ severity[warning] = 1
210
+ severity[severe] = 2
211
+
212
+ finite_condition = condition[np.isfinite(condition)]
213
+ summary = {
214
+ "triangle_count": int(len(simplices)),
215
+ "folded_count": int(np.count_nonzero(folded)),
216
+ "degenerate_count": int(np.count_nonzero(degenerate)),
217
+ "warning_count": int(np.count_nonzero(severity == 1)),
218
+ "severe_count": int(np.count_nonzero(severity == 2)),
219
+ "review_count": int(np.count_nonzero(severity > 0)),
220
+ "minimum_angle_deg": (
221
+ float(np.min(minimum_angle)) if len(minimum_angle) else 0.0
222
+ ),
223
+ "maximum_anisotropy": (
224
+ float(np.max(finite_condition))
225
+ if finite_condition.size
226
+ else float("inf")
227
+ ),
228
+ "minimum_normalized_area": (
229
+ float(np.nanmin(normalized_area_ratio))
230
+ if np.any(np.isfinite(normalized_area_ratio))
231
+ else float("nan")
232
+ ),
233
+ "maximum_normalized_area": (
234
+ float(np.nanmax(normalized_area_ratio))
235
+ if np.any(np.isfinite(normalized_area_ratio))
236
+ else float("nan")
237
+ ),
238
+ }
239
+ return {
240
+ "atlas_signed_double_area": atlas_area,
241
+ "histology_signed_double_area": histology_area,
242
+ "normalized_area_ratio": normalized_area_ratio,
243
+ "minimum_angle_deg": minimum_angle,
244
+ "anisotropy": condition,
245
+ "folded": folded,
246
+ "degenerate": degenerate,
247
+ "severity": severity,
248
+ "summary": summary,
249
+ }
250
+
251
+
252
+ def build_piecewise_affine_registration(
253
+ atlas_points,
254
+ histology_points,
255
+ *,
256
+ atlas_shape,
257
+ histology_shape,
258
+ simplices=None,
259
+ allow_unsafe=False,
260
+ ):
261
+ """Create one topology shared by both registration directions.
262
+
263
+ ``allow_unsafe`` is intended only for live mesh visualization. Transfers
264
+ keep the default strict behavior and reject collapsed or folded triangles.
265
+ """
266
+ atlas_points = _as_points(atlas_points, "Atlas")
267
+ histology_points = _as_points(histology_points, "Histology")
268
+ atlas_shape = _as_shape(atlas_shape, "Atlas")
269
+ histology_shape = _as_shape(histology_shape, "Histology")
270
+
271
+ if len(atlas_points) != len(histology_points):
272
+ raise TriangulationError(
273
+ "Atlas and histology must contain the same number of paired landmarks."
274
+ )
275
+ if len(atlas_points) < 3:
276
+ raise TriangulationError(
277
+ "At least three paired landmarks are required."
278
+ )
279
+ _validate_bounds(atlas_points, atlas_shape, "Atlas")
280
+ _validate_bounds(histology_points, histology_shape, "Histology")
281
+ _validate_duplicates(atlas_points, "Atlas")
282
+ _validate_duplicates(histology_points, "Histology")
283
+
284
+ if simplices is None:
285
+ simplices = _canonical_simplices(atlas_points)
286
+ else:
287
+ simplices = _validate_simplices(simplices, len(atlas_points))
288
+ quality = _mesh_quality(atlas_points, histology_points, simplices)
289
+ summary = quality["summary"]
290
+ errors = []
291
+ if summary["degenerate_count"]:
292
+ errors.append(
293
+ "{} triangle(s) collapse to a line or a point.".format(
294
+ summary["degenerate_count"]
295
+ )
296
+ )
297
+ if summary["folded_count"]:
298
+ errors.append(
299
+ "{} triangle(s) are folded. Move the corresponding landmarks "
300
+ "until the red triangles disappear.".format(summary["folded_count"])
301
+ )
302
+ if errors and not allow_unsafe:
303
+ raise TriangulationError(" ".join(errors))
304
+
305
+ warnings = []
306
+ if summary["warning_count"] or summary["severe_count"]:
307
+ warnings.append(
308
+ "{} triangle(s) have high stretch or narrow angles.".format(
309
+ summary["warning_count"] + summary["severe_count"]
310
+ )
311
+ )
312
+ return {
313
+ "schema_version": TRIANGULATION_SCHEMA_VERSION,
314
+ "method": "shared-atlas-delaunay-piecewise-affine",
315
+ "atlas_points": atlas_points,
316
+ "histology_points": histology_points,
317
+ "atlas_shape": atlas_shape,
318
+ "histology_shape": histology_shape,
319
+ "simplices": np.asarray(simplices, dtype=np.int32),
320
+ "quality": quality,
321
+ "warnings": warnings,
322
+ "errors": errors,
323
+ }
324
+
325
+
326
+ def registration_summary_text(registration):
327
+ summary = registration["quality"]["summary"]
328
+ text = (
329
+ "{triangle_count} triangles | {folded_count} folded | "
330
+ "{review_count} warning/review | min angle {minimum_angle_deg:.1f}° | "
331
+ "max anisotropy {maximum_anisotropy:.1f}×"
332
+ ).format(**summary)
333
+ return text
334
+
335
+
336
+ def triangle_colors(registration):
337
+ """Return RGB colors for healthy, warning, and severe triangles."""
338
+ severity = registration["quality"]["severity"]
339
+ palette = np.array(
340
+ [
341
+ [60, 190, 90],
342
+ [240, 180, 30],
343
+ [225, 65, 65],
344
+ ],
345
+ dtype=np.uint8,
346
+ )
347
+ return palette[severity]
348
+
349
+
350
+ def _registration_spaces(registration, direction):
351
+ if direction == "histology_to_atlas":
352
+ return (
353
+ registration["histology_points"],
354
+ registration["atlas_points"],
355
+ registration["atlas_shape"],
356
+ )
357
+ if direction == "atlas_to_histology":
358
+ return (
359
+ registration["atlas_points"],
360
+ registration["histology_points"],
361
+ registration["histology_shape"],
362
+ )
363
+ raise ValueError(
364
+ "Direction must be 'histology_to_atlas' or 'atlas_to_histology'."
365
+ )
366
+
367
+
368
+ def _barycentric(points, triangle):
369
+ matrix = (triangle[1:] - triangle[0]).T
370
+ try:
371
+ inverse = np.linalg.inv(matrix)
372
+ except np.linalg.LinAlgError:
373
+ return np.full((len(points), 3), np.nan, dtype=float)
374
+ uv = (points - triangle[0]) @ inverse.T
375
+ return np.column_stack((1.0 - uv[:, 0] - uv[:, 1], uv))
376
+
377
+
378
+ def dense_inverse_map(registration, direction):
379
+ """Return OpenCV maps from every destination pixel back to the source."""
380
+ source_points, destination_points, destination_shape = (
381
+ _registration_spaces(registration, direction)
382
+ )
383
+ height, width = destination_shape
384
+ map_x = np.full((height, width), -1.0, dtype=np.float32)
385
+ map_y = np.full((height, width), -1.0, dtype=np.float32)
386
+ assigned = np.zeros((height, width), dtype=bool)
387
+
388
+ for simplex in registration["simplices"]:
389
+ destination_triangle = destination_points[simplex]
390
+ source_triangle = source_points[simplex]
391
+ x_min = max(0, int(np.floor(np.min(destination_triangle[:, 0]))))
392
+ x_max = min(
393
+ width - 1, int(np.ceil(np.max(destination_triangle[:, 0])))
394
+ )
395
+ y_min = max(0, int(np.floor(np.min(destination_triangle[:, 1]))))
396
+ y_max = min(
397
+ height - 1, int(np.ceil(np.max(destination_triangle[:, 1])))
398
+ )
399
+ if x_max < x_min or y_max < y_min:
400
+ continue
401
+
402
+ for tile_y_min in range(y_min, y_max + 1, _MAP_TILE_ROWS):
403
+ tile_y_max = min(y_max, tile_y_min + _MAP_TILE_ROWS - 1)
404
+ grid_y, grid_x = np.mgrid[
405
+ tile_y_min : tile_y_max + 1, x_min : x_max + 1
406
+ ]
407
+ coordinates = np.column_stack((grid_x.ravel(), grid_y.ravel()))
408
+ weights = _barycentric(coordinates, destination_triangle)
409
+ inside = np.all(weights >= -_BARYCENTRIC_TOLERANCE, axis=1)
410
+ if not np.any(inside):
411
+ continue
412
+ destination_y = grid_y.ravel()[inside]
413
+ destination_x = grid_x.ravel()[inside]
414
+ new_pixels = ~assigned[destination_y, destination_x]
415
+ if not np.any(new_pixels):
416
+ continue
417
+ destination_y = destination_y[new_pixels]
418
+ destination_x = destination_x[new_pixels]
419
+ source = weights[inside][new_pixels] @ source_triangle
420
+ map_x[destination_y, destination_x] = source[:, 0]
421
+ map_y[destination_y, destination_x] = source[:, 1]
422
+ assigned[destination_y, destination_x] = True
423
+ return map_x, map_y, assigned
424
+
425
+
426
+ def warp_image_piecewise(
427
+ image,
428
+ registration,
429
+ direction,
430
+ *,
431
+ interpolation=cv2.INTER_LINEAR,
432
+ ):
433
+ """Warp an image with one dense inverse map, avoiding triangle seams."""
434
+ image = np.asarray(image)
435
+ if image.ndim not in (2, 3):
436
+ raise ValueError("Images must be two- or three-dimensional.")
437
+ source_points, _destination_points, _shape = _registration_spaces(
438
+ registration, direction
439
+ )
440
+ expected_shape = (
441
+ registration["histology_shape"]
442
+ if direction == "histology_to_atlas"
443
+ else registration["atlas_shape"]
444
+ )
445
+ if tuple(image.shape[:2]) != tuple(expected_shape):
446
+ raise ValueError("The source image does not match the registration.")
447
+ if not np.all(np.isfinite(source_points)):
448
+ raise ValueError("Registration source points are invalid.")
449
+
450
+ map_x, map_y, valid = dense_inverse_map(registration, direction)
451
+ warped = cv2.remap(
452
+ image,
453
+ map_x,
454
+ map_y,
455
+ interpolation,
456
+ borderMode=cv2.BORDER_CONSTANT,
457
+ borderValue=0,
458
+ )
459
+ if warped.ndim == 3:
460
+ warped[~valid, :] = 0
461
+ else:
462
+ warped[~valid] = 0
463
+ return warped
464
+
465
+
466
+ def transform_points_piecewise(points, registration, direction):
467
+ """Transform points through the persisted mesh.
468
+
469
+ Returns ``(transformed, valid, triangle_index)``. Invalid points retain NaN
470
+ coordinates and can be reported or removed by the caller.
471
+ """
472
+ points = _as_points(points, "Input")
473
+ source_points, destination_points, _destination_shape = _registration_spaces(
474
+ registration, direction
475
+ )
476
+ transformed = np.full_like(points, np.nan, dtype=float)
477
+ triangle_index = np.full(len(points), -1, dtype=np.int32)
478
+ if not len(points):
479
+ return transformed, np.zeros(0, dtype=bool), triangle_index
480
+
481
+ height, width = (
482
+ registration["histology_shape"]
483
+ if direction == "histology_to_atlas"
484
+ else registration["atlas_shape"]
485
+ )
486
+ in_bounds = (
487
+ (points[:, 0] >= 0)
488
+ & (points[:, 0] <= width - 1)
489
+ & (points[:, 1] >= 0)
490
+ & (points[:, 1] <= height - 1)
491
+ )
492
+ unassigned = in_bounds.copy()
493
+ for tri_index, simplex in enumerate(registration["simplices"]):
494
+ if not np.any(unassigned):
495
+ break
496
+ source_triangle = source_points[simplex]
497
+ x_min, y_min = np.min(source_triangle, axis=0)
498
+ x_max, y_max = np.max(source_triangle, axis=0)
499
+ candidates = np.flatnonzero(
500
+ unassigned
501
+ & (points[:, 0] >= x_min - _BARYCENTRIC_TOLERANCE)
502
+ & (points[:, 0] <= x_max + _BARYCENTRIC_TOLERANCE)
503
+ & (points[:, 1] >= y_min - _BARYCENTRIC_TOLERANCE)
504
+ & (points[:, 1] <= y_max + _BARYCENTRIC_TOLERANCE)
505
+ )
506
+ if not len(candidates):
507
+ continue
508
+ weights = _barycentric(points[candidates], source_triangle)
509
+ inside = np.all(weights >= -_BARYCENTRIC_TOLERANCE, axis=1)
510
+ selected = candidates[inside]
511
+ if not len(selected):
512
+ continue
513
+ transformed[selected] = weights[inside] @ destination_points[simplex]
514
+ triangle_index[selected] = tri_index
515
+ unassigned[selected] = False
516
+ valid = triangle_index >= 0
517
+ return transformed, valid, triangle_index
@@ -0,0 +1,108 @@
1
+
2
+ import cv2
3
+ import numpy as np
4
+ import random
5
+
6
+
7
+ # Check if a point is inside a rectangle
8
+ def rect_contains(rect, point):
9
+ if point[0] < rect[0]:
10
+ return False
11
+ elif point[1] < rect[1]:
12
+ return False
13
+ elif point[0] > rect[2]:
14
+ return False
15
+ elif point[1] > rect[3]:
16
+ return False
17
+ return True
18
+
19
+
20
+ # Draw a point
21
+ def draw_point(img, p, color):
22
+ cv2.circle(img, p, 2, color, cv2.FILLED, cv2.LINE_AA, 0)
23
+
24
+
25
+ # Draw delaunay triangles
26
+ def draw_delaunay(img, subdiv, delaunay_color):
27
+ triangleList = subdiv.getTriangleList()
28
+ size = img.shape
29
+ r = (0, 0, size[1], size[0])
30
+ for t in triangleList:
31
+ pt1 = (int(t[0]), int(t[1]))
32
+ pt2 = (int(t[2]), int(t[3]))
33
+ pt3 = (int(t[4]), int(t[5]))
34
+ if rect_contains(r, pt1) and rect_contains(r, pt2) and rect_contains(r, pt3):
35
+ cv2.line(img, pt1, pt2, delaunay_color, 1, cv2.LINE_AA, 0)
36
+ cv2.line(img, pt2, pt3, delaunay_color, 1, cv2.LINE_AA, 0)
37
+ cv2.line(img, pt3, pt1, delaunay_color, 1, cv2.LINE_AA, 0)
38
+
39
+
40
+ # Draw voronoi diagram
41
+ def draw_voronoi(img, subdiv):
42
+ (facets, centers) = subdiv.getVoronoiFacetList([])
43
+
44
+ for i in range(0, len(facets)):
45
+ ifacet_arr = []
46
+ for f in facets[i]:
47
+ ifacet_arr.append(f)
48
+ ifacet = np.array(ifacet_arr, 'i')
49
+ color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
50
+ cv2.fillConvexPoly(img, ifacet, color, cv2.LINE_AA, 0)
51
+ ifacets = np.array([ifacet])
52
+ cv2.polylines(img, ifacets, True, (0, 0, 0), 1, cv2.LINE_AA, 0)
53
+ cv2.circle(img, (int(centers[i][0]), int(centers[i][1])), 3, (0, 0, 0), cv2.FILLED, cv2.LINE_AA, 0)
54
+
55
+
56
+ if __name__ == '__main__':
57
+ # Define window names
58
+ win_delaunay = "Delaunay Triangulation"
59
+ win_voronoi = "Voronoi Diagram"
60
+ # Turn on animation while drawing triangles
61
+ animate = True
62
+ # Define colors for drawing.
63
+ delaunay_color = (255, 255, 255)
64
+ points_color = (0, 0, 255)
65
+ # Read in the image.
66
+ img = cv2.imread("/Users/jingyig/Work/Kavli/PyCode/herrbs/obama.jpg")
67
+ # Keep a copy around
68
+ img_orig = img.copy()
69
+ # Rectangle to be used with Subdiv2D
70
+ size = img.shape
71
+ rect = (0, 0, size[1], size[0])
72
+ # Create an instance of Subdiv2D
73
+ subdiv = cv2.Subdiv2D(rect)
74
+ # Create an array of points.
75
+ points = []
76
+ # Read in the points from a text file
77
+ with open("/Users/jingyig/Work/Kavli/PyCode/herrbs/points.txt") as file:
78
+ for line in file:
79
+ x, y = line.split()
80
+ points.append((int(x), int(y)))
81
+
82
+ # Insert points into subdiv
83
+ for p in points:
84
+ subdiv.insert(p)
85
+ # Show animation
86
+ if animate:
87
+ img_copy = img_orig.copy()
88
+ # Draw delaunay triangles
89
+ draw_delaunay(img_copy, subdiv, (255, 255, 255))
90
+ cv2.imshow(win_delaunay, img_copy)
91
+ cv2.waitKey(100)
92
+
93
+ # Draw delaunay triangles
94
+ draw_delaunay(img, subdiv, (255, 255, 255))
95
+
96
+ # Draw points
97
+
98
+ for p in points:
99
+ draw_point(img, p, (0, 0, 255))
100
+
101
+ # Allocate space for Voronoi Diagram
102
+ img_voronoi = np.zeros(img.shape, dtype=img.dtype)
103
+ # Draw Voronoi diagram
104
+ draw_voronoi(img_voronoi, subdiv)
105
+ # Show results
106
+ cv2.imshow(win_delaunay, img)
107
+ cv2.imshow(win_voronoi, img_voronoi)
108
+ cv2.waitKey(0)