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.
- {rhapso-0.3.2 → rhapso-0.3.3}/PKG-INFO +1 -17
- {rhapso-0.3.2 → rhapso-0.3.3}/README.md +0 -16
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/local/alignment_pipeline.py +7 -7
- rhapso-0.3.3/Rhapso/solver/global_optimization.py +532 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/pre_align_tiles.py +1 -1
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso.egg-info/PKG-INFO +1 -17
- {rhapso-0.3.2 → rhapso-0.3.3}/setup.py +1 -1
- rhapso-0.3.2/Rhapso/solver/global_optimization.py +0 -410
- {rhapso-0.3.2 → rhapso-0.3.3}/LICENSE +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/compute_bbox.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/compute_grid.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/fuse_cell.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/generate_fusion_instructions.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/initialize_output_zarr.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/overlapping_blocks.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/affine_fusion/overlapping_views.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/data_prep/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/data_prep/n5_reader.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/data_prep/s3_big_stitcher_reader.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/data_prep/xml_to_dataframe.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/advanced_refinement.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/difference_of_gaussian.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/image_reader.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/metadata_builder.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/overlap_detection.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/points_validation.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/save_interest_points.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/detection/view_transform_models.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/evaluation/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/evaluation/detection_visualization.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/evaluation/matching_visualization.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/evaluation/ph_corr_qc.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/matching/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/matching/load_and_transform_points.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/matching/ransac_matching.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/matching/save_matches.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/matching/xml_parser.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/multiscale/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/multiscale/array_and_chunk_prep.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/multiscale/block_planner.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/multiscale/ome_metadata.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/multiscale/pyramid_executor.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/affine_fusion.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/aws/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/aws/alignment_pipeline.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/aws/config/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/aws/config/dev/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/aws/fusion_pipeline.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/interest_point_detection.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/interest_point_matching.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/local/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/local/fusion_pipeline.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/multiscale.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/param/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/param/alignment/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/param/fusion/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/param/template/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/solver.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/split_dataset.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/util/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/pipelines/ray/util/fusion_progress.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/compute_tiles.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/concatenate_models.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/connected_graphs.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/data_prep.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/model_and_tile_setup.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/save_results.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/view_transforms.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/solver/xml_to_dataframe_solver.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/compute_grid_rules.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/save_points.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/save_xml.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/split_images.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/split_dataset/xml_to_dataframe_split.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/bdv_checkerboard_tile_colors.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/bdv_split_checkerboard.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/check_intensity_levels.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/check_multiscale.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/filter_tiles_from_split_xml.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/filter_tiles_from_xml.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/generate_processing_metadata.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/hcr_upload_hpc.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/increase_overlap.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/ng_link_gen_z1.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/ng_tile_viewer_proteomics.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/single_channel_xml.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/sort+rename_xml_views.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso/util/tile_analyzer.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso.egg-info/SOURCES.txt +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso.egg-info/dependency_links.txt +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso.egg-info/requires.txt +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/Rhapso.egg-info/top_level.txt +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/pyproject.toml +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/setup.cfg +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/tests/__init__.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/tests/test_detection.py +0 -0
- {rhapso-0.3.2 → rhapso-0.3.3}/tests/test_matching.py +0 -0
- {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.
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Rhapso
|
|
3
|
-
Version: 0.3.
|
|
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.
|
|
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',
|