Rhapso 0.3.2__tar.gz → 0.3.3__tar.gz

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 (106) hide show
  1. {rhapso-0.3.2 → rhapso-0.3.3}/PKG-INFO +1 -17
  2. {rhapso-0.3.2 → rhapso-0.3.3}/README.md +0 -16
  3. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/local/alignment_pipeline.py +7 -7
  4. rhapso-0.3.3/Rhapso/solver/global_optimization.py +532 -0
  5. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/pre_align_tiles.py +1 -1
  6. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso.egg-info/PKG-INFO +1 -17
  7. {rhapso-0.3.2 → rhapso-0.3.3}/setup.py +1 -1
  8. rhapso-0.3.2/Rhapso/solver/global_optimization.py +0 -410
  9. {rhapso-0.3.2 → rhapso-0.3.3}/LICENSE +0 -0
  10. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/__init__.py +0 -0
  11. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/__init__.py +0 -0
  12. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/compute_bbox.py +0 -0
  13. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/compute_grid.py +0 -0
  14. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/fuse_cell.py +0 -0
  15. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/generate_fusion_instructions.py +0 -0
  16. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/initialize_output_zarr.py +0 -0
  17. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/overlapping_blocks.py +0 -0
  18. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/overlapping_views.py +0 -0
  19. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/data_prep/__init__.py +0 -0
  20. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/data_prep/n5_reader.py +0 -0
  21. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/data_prep/s3_big_stitcher_reader.py +0 -0
  22. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/data_prep/xml_to_dataframe.py +0 -0
  23. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/__init__.py +0 -0
  24. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/advanced_refinement.py +0 -0
  25. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/difference_of_gaussian.py +0 -0
  26. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/image_reader.py +0 -0
  27. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/metadata_builder.py +0 -0
  28. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/overlap_detection.py +0 -0
  29. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/points_validation.py +0 -0
  30. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/save_interest_points.py +0 -0
  31. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/view_transform_models.py +0 -0
  32. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/evaluation/__init__.py +0 -0
  33. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/evaluation/detection_visualization.py +0 -0
  34. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/evaluation/matching_visualization.py +0 -0
  35. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/evaluation/ph_corr_qc.py +0 -0
  36. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/matching/__init__.py +0 -0
  37. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/matching/load_and_transform_points.py +0 -0
  38. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/matching/ransac_matching.py +0 -0
  39. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/matching/save_matches.py +0 -0
  40. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/matching/xml_parser.py +0 -0
  41. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/multiscale/__init__.py +0 -0
  42. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/multiscale/array_and_chunk_prep.py +0 -0
  43. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/multiscale/block_planner.py +0 -0
  44. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/multiscale/ome_metadata.py +0 -0
  45. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/multiscale/pyramid_executor.py +0 -0
  46. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/__init__.py +0 -0
  47. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/__init__.py +0 -0
  48. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/affine_fusion.py +0 -0
  49. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/aws/__init__.py +0 -0
  50. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/aws/alignment_pipeline.py +0 -0
  51. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/aws/config/__init__.py +0 -0
  52. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/aws/config/dev/__init__.py +0 -0
  53. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/aws/fusion_pipeline.py +0 -0
  54. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/interest_point_detection.py +0 -0
  55. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/interest_point_matching.py +0 -0
  56. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/local/__init__.py +0 -0
  57. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/local/fusion_pipeline.py +0 -0
  58. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/multiscale.py +0 -0
  59. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/param/__init__.py +0 -0
  60. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/param/alignment/__init__.py +0 -0
  61. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/param/fusion/__init__.py +0 -0
  62. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/param/template/__init__.py +0 -0
  63. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/solver.py +0 -0
  64. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/split_dataset.py +0 -0
  65. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/util/__init__.py +0 -0
  66. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/util/fusion_progress.py +0 -0
  67. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/__init__.py +0 -0
  68. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/compute_tiles.py +0 -0
  69. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/concatenate_models.py +0 -0
  70. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/connected_graphs.py +0 -0
  71. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/data_prep.py +0 -0
  72. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/model_and_tile_setup.py +0 -0
  73. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/save_results.py +0 -0
  74. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/view_transforms.py +0 -0
  75. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/xml_to_dataframe_solver.py +0 -0
  76. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/__init__.py +0 -0
  77. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/compute_grid_rules.py +0 -0
  78. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/save_points.py +0 -0
  79. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/save_xml.py +0 -0
  80. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/split_images.py +0 -0
  81. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/xml_to_dataframe_split.py +0 -0
  82. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/__init__.py +0 -0
  83. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/bdv_checkerboard_tile_colors.py +0 -0
  84. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/bdv_split_checkerboard.py +0 -0
  85. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/check_intensity_levels.py +0 -0
  86. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/check_multiscale.py +0 -0
  87. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/filter_tiles_from_split_xml.py +0 -0
  88. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/filter_tiles_from_xml.py +0 -0
  89. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/generate_processing_metadata.py +0 -0
  90. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/hcr_upload_hpc.py +0 -0
  91. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/increase_overlap.py +0 -0
  92. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/ng_link_gen_z1.py +0 -0
  93. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/ng_tile_viewer_proteomics.py +0 -0
  94. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/single_channel_xml.py +0 -0
  95. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/sort+rename_xml_views.py +0 -0
  96. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/tile_analyzer.py +0 -0
  97. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso.egg-info/SOURCES.txt +0 -0
  98. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso.egg-info/dependency_links.txt +0 -0
  99. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso.egg-info/requires.txt +0 -0
  100. {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso.egg-info/top_level.txt +0 -0
  101. {rhapso-0.3.2 → rhapso-0.3.3}/pyproject.toml +0 -0
  102. {rhapso-0.3.2 → rhapso-0.3.3}/setup.cfg +0 -0
  103. {rhapso-0.3.2 → rhapso-0.3.3}/tests/__init__.py +0 -0
  104. {rhapso-0.3.2 → rhapso-0.3.3}/tests/test_detection.py +0 -0
  105. {rhapso-0.3.2 → rhapso-0.3.3}/tests/test_matching.py +0 -0
  106. {rhapso-0.3.2 → rhapso-0.3.3}/tests/test_solving.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Rhapso
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A python package for aligning and stitching light sheet fluorescence microscopy images
5
5
  Author: ND
6
6
  Author-email: sean.fite@alleninstitute.org
@@ -247,22 +247,6 @@ There is a special case in some datasets where the z-stack is very large. In thi
247
247
 
248
248
  <br>
249
249
 
250
- ## Performance
251
-
252
- **Interest Point Detection Performance Example (130TB Zarr dataset)**
253
-
254
- | Environment | Resources | Avg runtime |
255
- |:----------------------|:---------------------|:-----------:|
256
- | Local single machine | 10 CPU, 10 GB RAM | ~120 min |
257
- | AWS Ray cluster | 560 CPU, 4.4 TB RAM | ~10 min |
258
-
259
- <br>
260
- *Actual times vary by pipeline components, dataset size, tiling, and parameter choices.*
261
-
262
- ---
263
-
264
- <br>
265
-
266
250
  ## Ray
267
251
 
268
252
  **Ray** is a Python framework for parallel and distributed computing. It lets you run regular Python functions in parallel on a single machine **or** scale them out to a cluster (e.g., AWS) with minimal code changes. In Rhapso, we use Ray to process large scale datasets.
@@ -203,22 +203,6 @@ There is a special case in some datasets where the z-stack is very large. In thi
203
203
 
204
204
  <br>
205
205
 
206
- ## Performance
207
-
208
- **Interest Point Detection Performance Example (130TB Zarr dataset)**
209
-
210
- | Environment | Resources | Avg runtime |
211
- |:----------------------|:---------------------|:-----------:|
212
- | Local single machine | 10 CPU, 10 GB RAM | ~120 min |
213
- | AWS Ray cluster | 560 CPU, 4.4 TB RAM | ~10 min |
214
-
215
- <br>
216
- *Actual times vary by pipeline components, dataset size, tiling, and parameter choices.*
217
-
218
- ---
219
-
220
- <br>
221
-
222
206
  ## Ray
223
207
 
224
208
  **Ray** is a Python framework for parallel and distributed computing. It lets you run regular Python functions in parallel on a single machine **or** scale them out to a cluster (e.g., AWS) with minimal code changes. In Rhapso, we use Ray to process large scale datasets.
@@ -160,11 +160,11 @@ split_dataset = SplitDataset(
160
160
  )
161
161
 
162
162
  # -- ALIGNMENT PIPELINE --
163
- interest_point_detection.run()
164
- interest_point_matching_rigid.run()
165
- solver_rigid.run()
166
- interest_point_matching_affine.run()
167
- solver_affine.run()
168
- split_dataset.run()
169
- interest_point_matching_split_affine.run()
163
+ # interest_point_detection.run()
164
+ # interest_point_matching_rigid.run()
165
+ # solver_rigid.run()
166
+ # interest_point_matching_affine.run()
167
+ # solver_affine.run()
168
+ # split_dataset.run()
169
+ # interest_point_matching_split_affine.run()
170
170
  solver_split_affine.run()
@@ -0,0 +1,532 @@
1
+ import numpy as np
2
+ import math
3
+
4
+ """
5
+ GlobalOptimization iteratively refines per-tile transforms to achieve sub-pixel alignment
6
+ using matched point correspondences.
7
+ """
8
+
9
+ class GlobalOptimization:
10
+ def __init__(
11
+ self,
12
+ tiles,
13
+ relative_threshold,
14
+ absolute_threshold,
15
+ min_matches,
16
+ damp,
17
+ regularization_weight,
18
+ max_iterations,
19
+ max_allowed_error,
20
+ max_plateauwidth,
21
+ run_type,
22
+ metrics_output_path,
23
+ ):
24
+ self.tiles = tiles
25
+ self.relative_threshold = relative_threshold
26
+ self.absolute_threshold = absolute_threshold
27
+ self.min_matches = min_matches
28
+ self.damp = damp
29
+ self.regularization_weight = regularization_weight
30
+ self.max_iterations = max_iterations
31
+ self.max_allowed_error = max_allowed_error
32
+ self.max_plateauwidth = max_plateauwidth
33
+ self.run_type = run_type
34
+ self.metrics_output_path = metrics_output_path
35
+
36
+ self.validation_stats = {
37
+ "solve_metrics_per_tile": {
38
+ "i": 0,
39
+ "stats": [],
40
+ },
41
+ "solver_metrics_per_tile": {
42
+ "stats": [],
43
+ },
44
+ }
45
+
46
+ self.observer = {
47
+ "max": 0,
48
+ "mean": 0,
49
+ "median": 0,
50
+ "min": float("inf"),
51
+ "slope": [],
52
+ "values": [],
53
+ "square_differences": 0,
54
+ "squares": 0,
55
+ "std": 0,
56
+ "std_0": 0,
57
+ "var": 0,
58
+ "var_0": 0,
59
+ }
60
+
61
+ # --------------------------------------------------
62
+ # Data preparation / synchronization
63
+ # --------------------------------------------------
64
+
65
+ def prepare_tile_arrays(self):
66
+ """
67
+ Convert match dictionaries into NumPy arrays
68
+ _p1_l is static local source points.
69
+ _p1_w is this tile's current optimized world source points.
70
+ _p2_w is only a cache and must be refreshed from live matches.
71
+ _weights is static match weights.
72
+ """
73
+ for tile in self.tiles:
74
+ matches = tile.get("matches", [])
75
+
76
+ if len(matches) == 0:
77
+ tile["_p1_l"] = np.empty((0, 3), dtype=np.float64)
78
+ tile["_p1_w"] = np.empty((0, 3), dtype=np.float64)
79
+ tile["_p2_w"] = np.empty((0, 3), dtype=np.float64)
80
+ tile["_weights"] = np.empty((0,), dtype=np.float64)
81
+ continue
82
+
83
+ tile["_p1_l"] = np.asarray(
84
+ [m["p1"]["l"] for m in matches],
85
+ dtype=np.float64,
86
+ ).reshape(-1, 3)
87
+
88
+ tile["_p1_w"] = np.asarray(
89
+ [m["p1"]["w"] for m in matches],
90
+ dtype=np.float64,
91
+ ).reshape(-1, 3)
92
+
93
+ # This starts as a snapshot, but it is refreshed every iteration.
94
+ tile["_p2_w"] = np.asarray(
95
+ [m["p2"]["w"] for m in matches],
96
+ dtype=np.float64,
97
+ ).reshape(-1, 3)
98
+
99
+ tile["_weights"] = np.asarray(
100
+ [m.get("weight", 1.0) for m in matches],
101
+ dtype=np.float64,
102
+ )
103
+
104
+ def refresh_target_arrays_from_matches(self):
105
+ """
106
+ Refresh cached target-side world coordinates from the live match graph.
107
+ """
108
+ for tile in self.tiles:
109
+ matches = tile.get("matches", [])
110
+
111
+ if len(matches) == 0:
112
+ continue
113
+
114
+ tile["_p2_w"][:] = np.asarray(
115
+ [m["p2"]["w"] for m in matches],
116
+ dtype=np.float64,
117
+ ).reshape(-1, 3)
118
+
119
+ def sync_tile_array_to_matches(self, tile):
120
+ """
121
+ Sync one tile's optimized source-side world coordinates back into matches.
122
+ """
123
+ if "_p1_w" not in tile:
124
+ return
125
+
126
+ p1_w = tile["_p1_w"]
127
+ matches = tile.get("matches", [])
128
+
129
+ for match, w in zip(matches, p1_w):
130
+ match["p1"]["w"][:] = w.tolist()
131
+
132
+ def sync_arrays_to_matches(self):
133
+ """
134
+ Final full sync for downstream code that reads match["p1"]["w"].
135
+ """
136
+ for tile in self.tiles:
137
+ self.sync_tile_array_to_matches(tile)
138
+
139
+ # --------------------------------------------------
140
+ # Observer / convergence
141
+ # --------------------------------------------------
142
+
143
+ def update_observer(self, new_value):
144
+ obs = self.observer
145
+ obs["values"].append(new_value)
146
+
147
+ n = len(obs["values"])
148
+
149
+ if n == 1:
150
+ obs["slope"].append(0.0)
151
+ obs["mean"] = new_value
152
+ obs["var"] = 0.0
153
+ obs["var_0"] = 0.0
154
+ obs["squares"] = new_value * new_value
155
+ else:
156
+ obs["slope"].append(new_value - obs["values"][-2])
157
+
158
+ delta = new_value - obs["mean"]
159
+ obs["mean"] += delta / n
160
+
161
+ obs["square_differences"] += delta * (new_value - obs["mean"])
162
+ obs["var"] = obs["square_differences"] / (n - 1)
163
+
164
+ obs["squares"] += new_value * new_value
165
+ obs["var_0"] = obs["squares"] / n
166
+
167
+ obs["std"] = math.sqrt(max(obs["var"], 0.0))
168
+ obs["std_0"] = math.sqrt(max(obs["var_0"], 0.0))
169
+
170
+ obs["min"] = min(obs["min"], new_value)
171
+ obs["max"] = max(obs["max"], new_value)
172
+
173
+ # This is only metrics. It is not part of the solve.
174
+ obs["median"] = float(np.median(obs["values"])) if n > 0 else 0.0
175
+
176
+ def get_wide_slope(self, values, width):
177
+ width = int(width)
178
+ return (values[-1] - values[-1 - width]) / width
179
+
180
+ def append_iteration_metrics(self, i, error):
181
+ """
182
+ Store compact metrics instead of deepcopying the whole growing observer.
183
+ """
184
+ self.validation_stats.setdefault("solver_metrics_per_tile", {}).setdefault("stats", []).append(
185
+ {
186
+ "iteration": i,
187
+ "error": error,
188
+ "observer": {
189
+ "mean": self.observer["mean"],
190
+ "median": self.observer["median"],
191
+ "min": self.observer["min"],
192
+ "max": self.observer["max"],
193
+ "std": self.observer["std"],
194
+ "slope": self.observer["slope"][-1] if self.observer["slope"] else 0.0,
195
+ },
196
+ }
197
+ )
198
+
199
+ # --------------------------------------------------
200
+ # Model helpers
201
+ # --------------------------------------------------
202
+
203
+ def model_to_matrix_translation(self, model):
204
+ M = np.array(
205
+ [
206
+ [model["m00"], model["m01"], model["m02"]],
207
+ [model["m10"], model["m11"], model["m12"]],
208
+ [model["m20"], model["m21"], model["m22"]],
209
+ ],
210
+ dtype=np.float64,
211
+ )
212
+
213
+ t = np.array(
214
+ [model["m03"], model["m13"], model["m23"]],
215
+ dtype=np.float64,
216
+ )
217
+
218
+ return M, t
219
+
220
+ def apply_model_array(self, points, model):
221
+ M, t = self.model_to_matrix_translation(model)
222
+ return points @ M.T + t
223
+
224
+ def get_active_model(self, tile):
225
+ if self.run_type in ("affine", "split-affine"):
226
+ return tile["model"]["regularized"]
227
+
228
+ if self.run_type == "rigid":
229
+ return tile["model"]["b"]
230
+
231
+ raise ValueError(f"Unknown run_type: {self.run_type}")
232
+
233
+ def regularize_models(self, affine, rigid):
234
+ l1 = 1.0 - self.regularization_weight
235
+
236
+ keys = [
237
+ "m00", "m01", "m02", "m03",
238
+ "m10", "m11", "m12", "m13",
239
+ "m20", "m21", "m22", "m23",
240
+ ]
241
+
242
+ return {
243
+ key: l1 * affine[key] + self.regularization_weight * rigid[key]
244
+ for key in keys
245
+ }
246
+
247
+ # --------------------------------------------------
248
+ # Cost / error scoring
249
+ # --------------------------------------------------
250
+
251
+ def update_cost(self, tile):
252
+ """
253
+ Computes and stores average distance and weighted cost for one tile.
254
+ Assumes _p2_w has already been refreshed from the live match graph.
255
+ """
256
+ p1_w = tile["_p1_w"]
257
+ p2_w = tile["_p2_w"]
258
+ weights = tile["_weights"]
259
+
260
+ if p1_w.shape[0] == 0:
261
+ tile["model"]["cost"] = 0.0
262
+ tile["cost"] = 0.0
263
+ tile["distance"] = 0.0
264
+ return
265
+
266
+ diff = p1_w - p2_w
267
+ distances = np.linalg.norm(diff, axis=1)
268
+
269
+ distance = float(distances.mean())
270
+
271
+ sum_weight = float(weights.sum())
272
+ if sum_weight > 0:
273
+ cost = float(np.sum(distances * distances * weights) / sum_weight)
274
+ else:
275
+ cost = 0.0
276
+
277
+ tile["model"]["cost"] = cost
278
+ tile["cost"] = cost
279
+ tile["distance"] = distance
280
+
281
+ def update_errors(self):
282
+ """
283
+ Refresh target arrays and score the current global state.
284
+ """
285
+ if not self.tiles:
286
+ return 0.0
287
+
288
+ self.refresh_target_arrays_from_matches()
289
+
290
+ total_distance = 0.0
291
+ min_error = float("inf")
292
+ max_error = 0.0
293
+
294
+ for tile in self.tiles:
295
+ self.update_cost(tile)
296
+
297
+ distance = tile["distance"]
298
+ min_error = min(min_error, distance)
299
+ max_error = max(max_error, distance)
300
+ total_distance += distance
301
+
302
+ return total_distance / len(self.tiles)
303
+
304
+ # --------------------------------------------------
305
+ # Model fitting
306
+ # --------------------------------------------------
307
+
308
+ def rigid_fit_model(self, rigid_model, tile):
309
+ """
310
+ Computes best-fit rigid transform using quaternion-based estimation.
311
+ Vectorized over all matches in the tile.
312
+ """
313
+ P = tile["_p1_l"]
314
+ Q = tile["_p2_w"]
315
+
316
+ if P.shape[0] == 0:
317
+ return rigid_model
318
+
319
+ pc = P.mean(axis=0)
320
+ qc = Q.mean(axis=0)
321
+
322
+ X = P - pc
323
+ Y = Q - qc
324
+
325
+ S = X.T @ Y
326
+
327
+ Sxx, Sxy, Sxz = S[0, :]
328
+ Syx, Syy, Syz = S[1, :]
329
+ Szx, Szy, Szz = S[2, :]
330
+
331
+ N = np.array(
332
+ [
333
+ [Sxx + Syy + Szz, Syz - Szy, Szx - Sxz, Sxy - Syx],
334
+ [Syz - Szy, Sxx - Syy - Szz, Sxy + Syx, Szx + Sxz],
335
+ [Szx - Sxz, Sxy + Syx, -Sxx + Syy - Szz, Syz + Szy],
336
+ [Sxy - Syx, Szx + Sxz, Syz + Szy, -Sxx - Syy + Szz],
337
+ ],
338
+ dtype=np.float64,
339
+ )
340
+
341
+ if not np.all(np.isfinite(N)):
342
+ raise ValueError("Matrix N contains NaNs or Infs")
343
+
344
+ eigenvalues, eigenvectors = np.linalg.eigh(N)
345
+ q = eigenvectors[:, np.argmax(eigenvalues)]
346
+
347
+ q_norm = np.linalg.norm(q)
348
+ if q_norm == 0 or not np.isfinite(q_norm):
349
+ raise ValueError("Invalid quaternion norm during rigid fit")
350
+
351
+ q /= q_norm
352
+ q0, qx, qy, qz = q
353
+
354
+ R = np.array(
355
+ [
356
+ [
357
+ q0 * q0 + qx * qx - qy * qy - qz * qz,
358
+ 2 * (qx * qy - q0 * qz),
359
+ 2 * (qx * qz + q0 * qy),
360
+ ],
361
+ [
362
+ 2 * (qy * qx + q0 * qz),
363
+ q0 * q0 - qx * qx + qy * qy - qz * qz,
364
+ 2 * (qy * qz - q0 * qx),
365
+ ],
366
+ [
367
+ 2 * (qz * qx - q0 * qy),
368
+ 2 * (qz * qy + q0 * qx),
369
+ q0 * q0 - qx * qx - qy * qy + qz * qz,
370
+ ],
371
+ ],
372
+ dtype=np.float64,
373
+ )
374
+
375
+ t = qc - R @ pc
376
+
377
+ rigid_model["m00"], rigid_model["m01"], rigid_model["m02"] = R[0, :]
378
+ rigid_model["m10"], rigid_model["m11"], rigid_model["m12"] = R[1, :]
379
+ rigid_model["m20"], rigid_model["m21"], rigid_model["m22"] = R[2, :]
380
+ rigid_model["m03"], rigid_model["m13"], rigid_model["m23"] = t
381
+
382
+ return rigid_model
383
+
384
+ def affine_fit_model(self, affine_model, tile):
385
+ """
386
+ Affine transformation model update.
387
+ Vectorized over all matches in the tile.
388
+ """
389
+ P = tile["_p1_l"]
390
+ Q = tile["_p2_w"]
391
+
392
+ if P.shape[0] < 3:
393
+ raise ValueError("Not enough matches for affine fit")
394
+
395
+ pc = P.mean(axis=0)
396
+ qc = Q.mean(axis=0)
397
+
398
+ X = P - pc
399
+ Y = Q - qc
400
+
401
+ A = X.T @ X
402
+ B = X.T @ Y
403
+
404
+ try:
405
+ # Solves A @ M_t = B.
406
+ # M_t is the transpose of the final affine matrix.
407
+ M_t = np.linalg.solve(A, B)
408
+ except np.linalg.LinAlgError as e:
409
+ raise ValueError("Affine matrix is singular") from e
410
+
411
+ M = M_t.T
412
+ t = qc - M @ pc
413
+
414
+ affine_model["m00"], affine_model["m01"], affine_model["m02"] = M[0, :]
415
+ affine_model["m10"], affine_model["m11"], affine_model["m12"] = M[1, :]
416
+ affine_model["m20"], affine_model["m21"], affine_model["m22"] = M[2, :]
417
+ affine_model["m03"], affine_model["m13"], affine_model["m23"] = t
418
+
419
+ return affine_model
420
+
421
+ def fit(self, tile):
422
+ """
423
+ Fits transformation models to a tile using current target-side world points.
424
+ """
425
+ if tile["_p1_l"].shape[0] == 0:
426
+ return
427
+
428
+ affine = self.affine_fit_model(tile["model"]["a"], tile)
429
+ rigid = self.rigid_fit_model(tile["model"]["b"], tile)
430
+ regularized = self.regularize_models(affine, rigid)
431
+
432
+ tile["model"]["a"] = affine
433
+ tile["model"]["b"] = rigid
434
+ tile["model"]["regularized"] = regularized
435
+
436
+ # --------------------------------------------------
437
+ # Application / dampening
438
+ # --------------------------------------------------
439
+
440
+ def apply_damp(self, tile):
441
+ """
442
+ Damp current p1 world positions toward the tile's model-applied local points.
443
+ """
444
+ if tile["_p1_l"].shape[0] == 0:
445
+ return
446
+
447
+ model = self.get_active_model(tile)
448
+ target = self.apply_model_array(tile["_p1_l"], model)
449
+
450
+ tile["_p1_w"] += self.damp * (target - tile["_p1_w"])
451
+
452
+ def apply(self):
453
+ """
454
+ Apply current model to local points to initialize p1 world positions.
455
+ Also syncs these initialized positions into the live match graph.
456
+ """
457
+ for tile in self.tiles:
458
+ if tile["_p1_l"].shape[0] == 0:
459
+ continue
460
+
461
+ model = self.get_active_model(tile)
462
+ tile["_p1_w"][:] = self.apply_model_array(tile["_p1_l"], model)
463
+
464
+ # Important: make initialized world points visible globally.
465
+ self.sync_tile_array_to_matches(tile)
466
+
467
+ # --------------------------------------------------
468
+ # Optimization loop
469
+ # --------------------------------------------------
470
+
471
+ def optimize_silently(self):
472
+ """
473
+ Iteratively refines tile alignments until convergence or max iterations.
474
+ 1. refresh target arrays from the live match graph,
475
+ 2. fit each tile against current target positions,
476
+ 3. damp source positions,
477
+ 4. sync source positions back into the live match graph,
478
+ 5. average current global error,
479
+ 6. repeat.
480
+ """
481
+ if not self.tiles:
482
+ return
483
+
484
+ self.prepare_tile_arrays()
485
+ self.apply()
486
+
487
+ i = 0
488
+ proceed = i < self.max_iterations
489
+
490
+ while proceed:
491
+ # Pull current neighbor/global positions into _p2_w.
492
+ self.refresh_target_arrays_from_matches()
493
+
494
+ for tile in self.tiles:
495
+ # Fit against the current live global state.
496
+ self.fit(tile)
497
+
498
+ # Update this tile's private optimized world points.
499
+ self.apply_damp(tile)
500
+
501
+ # Make this tile's update visible to the rest of the graph.
502
+ self.sync_tile_array_to_matches(tile)
503
+
504
+ # Score after all tile updates using current global positions.
505
+ error = self.update_errors()
506
+ self.update_observer(error)
507
+ self.append_iteration_metrics(i, error)
508
+
509
+ if i > self.max_plateauwidth:
510
+ proceed = error > self.max_allowed_error
511
+ d = self.max_plateauwidth
512
+
513
+ while not proceed and d >= 1:
514
+ proceed = proceed or abs(self.get_wide_slope(self.observer["values"], d)) > 0.0001
515
+ d /= 2
516
+
517
+ i += 1
518
+
519
+ if i >= self.max_iterations:
520
+ proceed = False
521
+
522
+ self.validation_stats["solve_metrics_per_tile"]["i"] = i
523
+
524
+ # Final downstream sync.
525
+ self.sync_arrays_to_matches()
526
+
527
+ def run(self):
528
+ """
529
+ Executes the entry point of the solver.
530
+ """
531
+ self.optimize_silently()
532
+ return self.tiles, self.validation_stats
@@ -318,6 +318,6 @@ class PreAlignTiles:
318
318
  unaligned_tiles = self.pre_align(tiles)
319
319
 
320
320
  if len(unaligned_tiles) > 0:
321
- print(f"aligned all tiles but: {len(unaligned_tiles)}")
321
+ print(f"Pre-aligned all tiles but: {len(unaligned_tiles)}")
322
322
 
323
323
  return tiles['tiles']
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Rhapso
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A python package for aligning and stitching light sheet fluorescence microscopy images
5
5
  Author: ND
6
6
  Author-email: sean.fite@alleninstitute.org
@@ -247,22 +247,6 @@ There is a special case in some datasets where the z-stack is very large. In thi
247
247
 
248
248
  <br>
249
249
 
250
- ## Performance
251
-
252
- **Interest Point Detection Performance Example (130TB Zarr dataset)**
253
-
254
- | Environment | Resources | Avg runtime |
255
- |:----------------------|:---------------------|:-----------:|
256
- | Local single machine | 10 CPU, 10 GB RAM | ~120 min |
257
- | AWS Ray cluster | 560 CPU, 4.4 TB RAM | ~10 min |
258
-
259
- <br>
260
- *Actual times vary by pipeline components, dataset size, tiling, and parameter choices.*
261
-
262
- ---
263
-
264
- <br>
265
-
266
250
  ## Ray
267
251
 
268
252
  **Ray** is a Python framework for parallel and distributed computing. It lets you run regular Python functions in parallel on a single machine **or** scale them out to a cluster (e.g., AWS) with minimal code changes. In Rhapso, we use Ray to process large scale datasets.
@@ -7,7 +7,7 @@ long_description = (this_directory / "README.md").read_text(encoding="utf-8")
7
7
 
8
8
  setup(
9
9
  name='Rhapso',
10
- version='0.3.2',
10
+ version='0.3.3',
11
11
  author='ND',
12
12
  author_email='sean.fite@alleninstitute.org',
13
13
  description='A python package for aligning and stitching light sheet fluorescence microscopy images',