itasc 0.2.0.dev0__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.
- itasc/__init__.py +16 -0
- itasc/cellpose/__init__.py +31 -0
- itasc/cellpose/cellpose_runner.py +334 -0
- itasc/cellpose/divergence_maps.py +380 -0
- itasc/cellpose/flow_following.py +283 -0
- itasc/cellpose/joint.py +98 -0
- itasc/cellpose/napari.yaml +10 -0
- itasc/cellpose/native_masks.py +313 -0
- itasc/cellpose/retrack.py +232 -0
- itasc/cellpose/shape.py +51 -0
- itasc/cellpose/track_laptrack.py +279 -0
- itasc/contact_analysis/__init__.py +61 -0
- itasc/contact_analysis/_provenance.py +29 -0
- itasc/contact_analysis/catalog.py +393 -0
- itasc/contact_analysis/config.py +155 -0
- itasc/contact_analysis/contacts/__init__.py +24 -0
- itasc/contact_analysis/contacts/batch.py +158 -0
- itasc/contact_analysis/contacts/build.py +837 -0
- itasc/contact_analysis/contacts/contact_labels.py +136 -0
- itasc/contact_analysis/contacts/neighborhood.py +146 -0
- itasc/contact_analysis/contacts/reader.py +58 -0
- itasc/contact_analysis/contacts/signed_contact_length.py +143 -0
- itasc/contact_analysis/curation.py +185 -0
- itasc/contact_analysis/dynamics/__init__.py +43 -0
- itasc/contact_analysis/dynamics/collective.py +206 -0
- itasc/contact_analysis/dynamics/kinematics.py +278 -0
- itasc/contact_analysis/dynamics/msd.py +165 -0
- itasc/contact_analysis/dynamics/store.py +251 -0
- itasc/contact_analysis/dynamics/trajectories.py +100 -0
- itasc/contact_analysis/frame_interval.py +85 -0
- itasc/contact_analysis/napari.yaml +10 -0
- itasc/contact_analysis/pipeline.py +313 -0
- itasc/contact_analysis/pixel_size.py +166 -0
- itasc/contact_analysis/quantifier.py +204 -0
- itasc/contact_analysis/quantifiers/__init__.py +7 -0
- itasc/contact_analysis/quantifiers/_contacts_derived.py +42 -0
- itasc/contact_analysis/quantifiers/cell_density.py +65 -0
- itasc/contact_analysis/quantifiers/cell_dynamics.py +81 -0
- itasc/contact_analysis/quantifiers/cell_shape.py +32 -0
- itasc/contact_analysis/quantifiers/contacts.py +72 -0
- itasc/contact_analysis/quantifiers/neighbor_count.py +26 -0
- itasc/contact_analysis/quantifiers/nucleus_dynamics.py +77 -0
- itasc/contact_analysis/quantifiers/nucleus_shape.py +35 -0
- itasc/contact_analysis/quantifiers/shape_relational.py +34 -0
- itasc/contact_analysis/quantifiers/signed_contact_length.py +44 -0
- itasc/contact_analysis/records.py +129 -0
- itasc/contact_analysis/shape/__init__.py +19 -0
- itasc/contact_analysis/shape/core.py +141 -0
- itasc/contact_analysis/shape/relational.py +134 -0
- itasc/contact_analysis/shape_tables.py +387 -0
- itasc/core/__init__.py +0 -0
- itasc/core/cancellation.py +15 -0
- itasc/core/commit.py +61 -0
- itasc/core/imageops.py +34 -0
- itasc/core/label_store.py +53 -0
- itasc/core/lineage.py +113 -0
- itasc/core/logging.py +51 -0
- itasc/core/paths.py +34 -0
- itasc/core/tiff.py +13 -0
- itasc/correction/__init__.py +25 -0
- itasc/correction/labels.py +983 -0
- itasc/napari/__init__.py +7 -0
- itasc/napari/_experiments_panel.py +1091 -0
- itasc/napari/_flow_layout.py +98 -0
- itasc/napari/_icons.py +126 -0
- itasc/napari/_napari_compat.py +118 -0
- itasc/napari/_paths.py +231 -0
- itasc/napari/_preview_cache.py +60 -0
- itasc/napari/_spotlight.py +46 -0
- itasc/napari/_stage_loader.py +146 -0
- itasc/napari/_stage_status.py +110 -0
- itasc/napari/_standalone_paths.py +89 -0
- itasc/napari/_state.py +146 -0
- itasc/napari/_status_rail.py +128 -0
- itasc/napari/_track_render.py +114 -0
- itasc/napari/_widget_helpers.py +236 -0
- itasc/napari/aggregate_widget.py +339 -0
- itasc/napari/candidate_gallery_controller.py +285 -0
- itasc/napari/cell_workflow_widget.py +1405 -0
- itasc/napari/cellpose_segment_track_widget.py +1253 -0
- itasc/napari/cellpose_widget.py +1034 -0
- itasc/napari/contact_analysis/__init__.py +6 -0
- itasc/napari/contact_analysis/curation_controller.py +94 -0
- itasc/napari/contact_analysis/plugins/__init__.py +102 -0
- itasc/napari/contact_analysis/plugins/catalog_summary.py +56 -0
- itasc/napari/contact_analysis/plugins/curation.py +247 -0
- itasc/napari/contact_analysis/plugins/visualize_contacts.py +64 -0
- itasc/napari/contact_analysis_widget.py +951 -0
- itasc/napari/contact_visualization.py +895 -0
- itasc/napari/correction/__init__.py +8 -0
- itasc/napari/correction/_correction_anchor.py +54 -0
- itasc/napari/correction/_correction_candidate_panel.py +259 -0
- itasc/napari/correction/_correction_candidates.py +169 -0
- itasc/napari/correction/_correction_centroids.py +132 -0
- itasc/napari/correction/_correction_commit.py +71 -0
- itasc/napari/correction/_correction_events.py +39 -0
- itasc/napari/correction/_correction_keymap.py +80 -0
- itasc/napari/correction/_correction_layer_lifecycle.py +137 -0
- itasc/napari/correction/_correction_layer_loader.py +86 -0
- itasc/napari/correction/_correction_navigation.py +97 -0
- itasc/napari/correction/_correction_paint.py +46 -0
- itasc/napari/correction/_correction_playback.py +68 -0
- itasc/napari/correction/_correction_protection.py +44 -0
- itasc/napari/correction/_correction_takeover.py +64 -0
- itasc/napari/correction/_correction_track_accordion.py +566 -0
- itasc/napari/correction/_correction_track_path.py +388 -0
- itasc/napari/correction/_correction_ui.py +367 -0
- itasc/napari/correction/_correction_ui_nucleus.py +393 -0
- itasc/napari/correction/_correction_utils.py +204 -0
- itasc/napari/correction/_correction_validation.py +83 -0
- itasc/napari/correction/cell_correction_widget.py +1190 -0
- itasc/napari/correction/correction_widget.py +1363 -0
- itasc/napari/correction/nucleus_correction_widget.py +2185 -0
- itasc/napari/divergence_maps_widget.py +795 -0
- itasc/napari/lineage_canvas_controller.py +351 -0
- itasc/napari/main_widget.py +1184 -0
- itasc/napari/nucleus_atom_extraction_widget.py +648 -0
- itasc/napari/nucleus_db_browser_widget.py +1442 -0
- itasc/napari/nucleus_pipeline_widget.py +579 -0
- itasc/napari/nucleus_segmentation_inputs_widget.py +21 -0
- itasc/napari/nucleus_tracking_inputs_widget.py +248 -0
- itasc/napari/nucleus_workflow_widget.py +963 -0
- itasc/napari/track_path_controller.py +387 -0
- itasc/napari/ui_gate.py +266 -0
- itasc/napari/ui_style.py +503 -0
- itasc/napari/validated_overlay_controller.py +254 -0
- itasc/napari/widgets.py +616 -0
- itasc/napari.yaml +45 -0
- itasc/segmentation/__init__.py +60 -0
- itasc/segmentation/cell_divergence_segmentation.py +433 -0
- itasc/segmentation/cell_label_icm.py +637 -0
- itasc/segmentation/contour_filtering.py +128 -0
- itasc/segmentation/lineage.py +18 -0
- itasc/segmentation/nucleus_segmentation.py +35 -0
- itasc/tracking_ultrack/__init__.py +12 -0
- itasc/tracking_ultrack/_node_geometry.py +191 -0
- itasc/tracking_ultrack/atoms.py +470 -0
- itasc/tracking_ultrack/config.py +76 -0
- itasc/tracking_ultrack/corrections.py +1213 -0
- itasc/tracking_ultrack/db_build.py +492 -0
- itasc/tracking_ultrack/db_query.py +952 -0
- itasc/tracking_ultrack/export.py +133 -0
- itasc/tracking_ultrack/extend.py +243 -0
- itasc/tracking_ultrack/ingest.py +83 -0
- itasc/tracking_ultrack/linking.py +286 -0
- itasc/tracking_ultrack/multi_threshold.py +46 -0
- itasc/tracking_ultrack/napari.yaml +10 -0
- itasc/tracking_ultrack/reseed.py +456 -0
- itasc/tracking_ultrack/retracker.py +153 -0
- itasc/tracking_ultrack/scoring.py +74 -0
- itasc/tracking_ultrack/seed_prior.py +175 -0
- itasc/tracking_ultrack/solve.py +52 -0
- itasc/tracking_ultrack/swap_candidate.py +171 -0
- itasc/tracking_ultrack/track_quality.py +142 -0
- itasc/tracking_ultrack/validation_nodes.py +315 -0
- itasc/tracking_ultrack/validation_state.py +430 -0
- itasc-0.2.0.dev0.data/data/itasc/napari.yaml +45 -0
- itasc-0.2.0.dev0.dist-info/METADATA +186 -0
- itasc-0.2.0.dev0.dist-info/RECORD +162 -0
- itasc-0.2.0.dev0.dist-info/WHEEL +4 -0
- itasc-0.2.0.dev0.dist-info/entry_points.txt +2 -0
- itasc-0.2.0.dev0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Inject validated tracked labels as annotated Ultrack nodes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from itasc.tracking_ultrack.config import TrackingConfig
|
|
10
|
+
from itasc.tracking_ultrack._node_geometry import (
|
|
11
|
+
intersects,
|
|
12
|
+
make_node_pickle,
|
|
13
|
+
node_bbox_and_mask,
|
|
14
|
+
node_pickle_ndim,
|
|
15
|
+
raw_iou,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ValidationInjectionReport:
|
|
21
|
+
inserted: int
|
|
22
|
+
skipped_missing: int
|
|
23
|
+
skipped: list[tuple[int, int]]
|
|
24
|
+
faked: int
|
|
25
|
+
overlaps_added: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class _MaskRecord:
|
|
30
|
+
cell_id: int
|
|
31
|
+
t: int
|
|
32
|
+
bbox: tuple[int, int, int, int]
|
|
33
|
+
mask: np.ndarray
|
|
34
|
+
area: int
|
|
35
|
+
y: float
|
|
36
|
+
x: float
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _frame_mask_for_cell(tracked_labels: np.ndarray, t: int, cell_id: int) -> np.ndarray:
|
|
40
|
+
frame = np.asarray(tracked_labels)[t]
|
|
41
|
+
if frame.ndim == 2:
|
|
42
|
+
return np.asarray(frame == cell_id)
|
|
43
|
+
if frame.ndim == 3:
|
|
44
|
+
return np.asarray(frame == cell_id).any(axis=0)
|
|
45
|
+
raise ValueError(f"Expected tracked frame to be 2D or 3D, got shape {frame.shape}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validated_mask_records(
|
|
49
|
+
validated_tracks: dict[int, set[int]],
|
|
50
|
+
tracked_labels: np.ndarray,
|
|
51
|
+
) -> tuple[list[_MaskRecord], list[tuple[int, int]]]:
|
|
52
|
+
records: list[_MaskRecord] = []
|
|
53
|
+
skipped: list[tuple[int, int]] = []
|
|
54
|
+
labels = np.asarray(tracked_labels)
|
|
55
|
+
n_frames = int(labels.shape[0])
|
|
56
|
+
|
|
57
|
+
for cell_id, frames in sorted(validated_tracks.items()):
|
|
58
|
+
for raw_t in sorted(frames):
|
|
59
|
+
t = int(raw_t)
|
|
60
|
+
if t < 0 or t >= n_frames:
|
|
61
|
+
skipped.append((int(cell_id), t))
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
mask_2d = _frame_mask_for_cell(labels, t, int(cell_id))
|
|
65
|
+
if not mask_2d.any():
|
|
66
|
+
skipped.append((int(cell_id), t))
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
rows = np.flatnonzero(mask_2d.any(axis=1))
|
|
70
|
+
cols = np.flatnonzero(mask_2d.any(axis=0))
|
|
71
|
+
y0, y1 = int(rows[0]), int(rows[-1]) + 1
|
|
72
|
+
x0, x1 = int(cols[0]), int(cols[-1]) + 1
|
|
73
|
+
crop = np.ascontiguousarray(mask_2d[y0:y1, x0:x1], dtype=bool)
|
|
74
|
+
ys, xs = np.nonzero(crop)
|
|
75
|
+
records.append(
|
|
76
|
+
_MaskRecord(
|
|
77
|
+
cell_id=int(cell_id),
|
|
78
|
+
t=t,
|
|
79
|
+
bbox=(y0, x0, y1, x1),
|
|
80
|
+
mask=crop,
|
|
81
|
+
area=int(crop.sum()),
|
|
82
|
+
y=float(y0 + ys.mean()),
|
|
83
|
+
x=float(x0 + xs.mean()),
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return records, skipped
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _overlap_pair(lhs_id: int, rhs_id: int) -> tuple[int, int]:
|
|
91
|
+
return (max(lhs_id, rhs_id), min(lhs_id, rhs_id))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _generate_node_id(index: int, time: int, max_segments: int) -> int:
|
|
95
|
+
return index + (time + 1) * max_segments
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _best_iou_assignments(
|
|
99
|
+
records: list[_MaskRecord],
|
|
100
|
+
candidates: dict[int, tuple[tuple[int, int, int, int], np.ndarray, int]],
|
|
101
|
+
) -> dict[int, int]:
|
|
102
|
+
pairs: list[tuple[float, int, int, int, int]] = []
|
|
103
|
+
for record_index, record in enumerate(records):
|
|
104
|
+
for candidate_id, (candidate_bbox, candidate_mask, _ndim) in candidates.items():
|
|
105
|
+
iou = raw_iou(record.bbox, record.mask, candidate_bbox, candidate_mask)
|
|
106
|
+
pairs.append((-iou, record_index, int(candidate_id), record.cell_id, record.t))
|
|
107
|
+
|
|
108
|
+
pairs.sort()
|
|
109
|
+
assigned_records: set[int] = set()
|
|
110
|
+
assigned_candidates: set[int] = set()
|
|
111
|
+
assignments: dict[int, int] = {}
|
|
112
|
+
for _neg_iou, record_index, candidate_id, _cell_id, _t in pairs:
|
|
113
|
+
if record_index in assigned_records or candidate_id in assigned_candidates:
|
|
114
|
+
continue
|
|
115
|
+
assignments[record_index] = candidate_id
|
|
116
|
+
assigned_records.add(record_index)
|
|
117
|
+
assigned_candidates.add(candidate_id)
|
|
118
|
+
if len(assigned_records) == min(len(records), len(candidates)):
|
|
119
|
+
break
|
|
120
|
+
return assignments
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def inject_validated_nodes(
|
|
124
|
+
working_dir: str | Path,
|
|
125
|
+
validated_tracks: dict[int, set[int]],
|
|
126
|
+
tracked_labels: np.ndarray,
|
|
127
|
+
cfg: TrackingConfig,
|
|
128
|
+
) -> ValidationInjectionReport:
|
|
129
|
+
"""Replace best-matching candidates with validated masks as fixed REAL nodes.
|
|
130
|
+
|
|
131
|
+
The best same-frame candidate by raw IoU is updated in place so its
|
|
132
|
+
hierarchy placement and temporal links are preserved. If no candidate is
|
|
133
|
+
available for a validated mask, a reserved REAL node is inserted instead.
|
|
134
|
+
Other candidates in the same frame that overlap a validated mask are marked
|
|
135
|
+
FAKE and paired with the REAL node in OverlapDB.
|
|
136
|
+
"""
|
|
137
|
+
import sqlalchemy as sqla
|
|
138
|
+
from sqlalchemy.orm import Session
|
|
139
|
+
from ultrack.core.database import NodeDB, OverlapDB, VarAnnotation
|
|
140
|
+
|
|
141
|
+
records, skipped = _validated_mask_records(validated_tracks, tracked_labels)
|
|
142
|
+
if not records:
|
|
143
|
+
return ValidationInjectionReport(
|
|
144
|
+
inserted=0,
|
|
145
|
+
skipped_missing=len(skipped),
|
|
146
|
+
skipped=skipped,
|
|
147
|
+
faked=0,
|
|
148
|
+
overlaps_added=0,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
working_dir = Path(working_dir)
|
|
152
|
+
engine = sqla.create_engine(f"sqlite:///{working_dir / 'data.db'}")
|
|
153
|
+
inserted = 0
|
|
154
|
+
faked_ids: set[int] = set()
|
|
155
|
+
overlap_pairs: set[tuple[int, int]] = set()
|
|
156
|
+
real_node_ids: set[int] = set()
|
|
157
|
+
|
|
158
|
+
with Session(engine) as session:
|
|
159
|
+
sample_node = session.query(NodeDB.pickle).limit(1).scalar()
|
|
160
|
+
fallback_ndim = node_pickle_ndim(sample_node) if sample_node is not None else 2
|
|
161
|
+
next_t_node_id: dict[int, int] = {}
|
|
162
|
+
for t in {record.t for record in records}:
|
|
163
|
+
max_t_node_id = (
|
|
164
|
+
session.query(sqla.func.max(NodeDB.t_node_id))
|
|
165
|
+
.where(NodeDB.t == t)
|
|
166
|
+
.scalar()
|
|
167
|
+
)
|
|
168
|
+
next_t_node_id[t] = int(max_t_node_id or 0) + 1
|
|
169
|
+
|
|
170
|
+
records_by_t: dict[int, list[tuple[int, _MaskRecord]]] = {}
|
|
171
|
+
for index, record in enumerate(records):
|
|
172
|
+
records_by_t.setdefault(record.t, []).append((index, record))
|
|
173
|
+
|
|
174
|
+
for t, indexed_records in records_by_t.items():
|
|
175
|
+
candidate_rows = (
|
|
176
|
+
session.query(NodeDB.id, NodeDB.pickle)
|
|
177
|
+
.where(NodeDB.t == t)
|
|
178
|
+
.where(NodeDB.t_hier_id != 0)
|
|
179
|
+
.all()
|
|
180
|
+
)
|
|
181
|
+
candidates: dict[int, tuple[tuple[int, int, int, int], np.ndarray, int]] = {}
|
|
182
|
+
for candidate_id, candidate_node in candidate_rows:
|
|
183
|
+
candidate_bbox, candidate_mask = node_bbox_and_mask(
|
|
184
|
+
int(candidate_id), candidate_node
|
|
185
|
+
)
|
|
186
|
+
candidates[int(candidate_id)] = (
|
|
187
|
+
candidate_bbox,
|
|
188
|
+
candidate_mask,
|
|
189
|
+
node_pickle_ndim(candidate_node),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
frame_records = [record for _index, record in indexed_records]
|
|
193
|
+
local_assignments = _best_iou_assignments(frame_records, candidates)
|
|
194
|
+
real_node_by_local_index: dict[int, int] = {}
|
|
195
|
+
matched_candidate_ids: set[int] = set()
|
|
196
|
+
|
|
197
|
+
for local_index, record in enumerate(frame_records):
|
|
198
|
+
bbox_arr = np.asarray(record.bbox, dtype=np.int32)
|
|
199
|
+
candidate_id = local_assignments.get(local_index)
|
|
200
|
+
if candidate_id is None:
|
|
201
|
+
node_ndim = next(
|
|
202
|
+
(ndim for _bbox, _mask, ndim in candidates.values()),
|
|
203
|
+
fallback_ndim,
|
|
204
|
+
)
|
|
205
|
+
t_node_id = next_t_node_id[record.t]
|
|
206
|
+
next_t_node_id[record.t] += 1
|
|
207
|
+
node_id = _generate_node_id(
|
|
208
|
+
t_node_id, record.t, cfg.max_segments_per_time
|
|
209
|
+
)
|
|
210
|
+
session.add(
|
|
211
|
+
NodeDB(
|
|
212
|
+
id=node_id,
|
|
213
|
+
t=record.t,
|
|
214
|
+
t_node_id=t_node_id,
|
|
215
|
+
t_hier_id=0,
|
|
216
|
+
z=0,
|
|
217
|
+
y=record.y,
|
|
218
|
+
x=record.x,
|
|
219
|
+
area=record.area,
|
|
220
|
+
pickle=make_node_pickle(
|
|
221
|
+
record.t,
|
|
222
|
+
record.mask,
|
|
223
|
+
bbox_arr,
|
|
224
|
+
node_id,
|
|
225
|
+
ndim=node_ndim,
|
|
226
|
+
),
|
|
227
|
+
node_prob=1.0,
|
|
228
|
+
node_annot=VarAnnotation.REAL,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
inserted += 1
|
|
232
|
+
real_node_ids.add(node_id)
|
|
233
|
+
real_node_by_local_index[local_index] = node_id
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
node_ndim = candidates[candidate_id][2]
|
|
237
|
+
session.query(NodeDB).where(NodeDB.id == candidate_id).update(
|
|
238
|
+
{
|
|
239
|
+
NodeDB.y: record.y,
|
|
240
|
+
NodeDB.x: record.x,
|
|
241
|
+
NodeDB.area: record.area,
|
|
242
|
+
NodeDB.pickle: make_node_pickle(
|
|
243
|
+
record.t,
|
|
244
|
+
record.mask,
|
|
245
|
+
bbox_arr,
|
|
246
|
+
candidate_id,
|
|
247
|
+
ndim=node_ndim,
|
|
248
|
+
),
|
|
249
|
+
NodeDB.node_prob: 1.0,
|
|
250
|
+
NodeDB.node_annot: VarAnnotation.REAL,
|
|
251
|
+
},
|
|
252
|
+
synchronize_session=False,
|
|
253
|
+
)
|
|
254
|
+
inserted += 1
|
|
255
|
+
real_node_ids.add(candidate_id)
|
|
256
|
+
matched_candidate_ids.add(candidate_id)
|
|
257
|
+
real_node_by_local_index[local_index] = candidate_id
|
|
258
|
+
|
|
259
|
+
for local_index, record in enumerate(frame_records):
|
|
260
|
+
real_node_id = real_node_by_local_index[local_index]
|
|
261
|
+
for candidate_id, (
|
|
262
|
+
candidate_bbox,
|
|
263
|
+
candidate_mask,
|
|
264
|
+
_ndim,
|
|
265
|
+
) in candidates.items():
|
|
266
|
+
if candidate_id in matched_candidate_ids:
|
|
267
|
+
continue
|
|
268
|
+
if not intersects(
|
|
269
|
+
record.bbox, record.mask, candidate_bbox, candidate_mask
|
|
270
|
+
):
|
|
271
|
+
continue
|
|
272
|
+
faked_ids.add(candidate_id)
|
|
273
|
+
overlap_pairs.add(_overlap_pair(real_node_id, candidate_id))
|
|
274
|
+
|
|
275
|
+
if faked_ids:
|
|
276
|
+
session.query(NodeDB).where(NodeDB.id.in_(faked_ids)).update(
|
|
277
|
+
{NodeDB.node_annot: VarAnnotation.FAKE},
|
|
278
|
+
synchronize_session=False,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
existing_pairs: set[tuple[int, int]] = set()
|
|
282
|
+
if real_node_ids:
|
|
283
|
+
session.query(OverlapDB).where(
|
|
284
|
+
sqla.or_(
|
|
285
|
+
OverlapDB.node_id.in_(real_node_ids),
|
|
286
|
+
OverlapDB.ancestor_id.in_(real_node_ids),
|
|
287
|
+
)
|
|
288
|
+
).delete(synchronize_session=False)
|
|
289
|
+
if overlap_pairs:
|
|
290
|
+
node_ids = {pair[0] for pair in overlap_pairs} | {pair[1] for pair in overlap_pairs}
|
|
291
|
+
existing_pairs = {
|
|
292
|
+
(int(row.node_id), int(row.ancestor_id))
|
|
293
|
+
for row in session.query(OverlapDB)
|
|
294
|
+
.where(
|
|
295
|
+
sqla.or_(
|
|
296
|
+
OverlapDB.node_id.in_(node_ids),
|
|
297
|
+
OverlapDB.ancestor_id.in_(node_ids),
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
.all()
|
|
301
|
+
}
|
|
302
|
+
for node_id, ancestor_id in sorted(overlap_pairs - existing_pairs):
|
|
303
|
+
session.add(OverlapDB(node_id=node_id, ancestor_id=ancestor_id))
|
|
304
|
+
|
|
305
|
+
session.commit()
|
|
306
|
+
|
|
307
|
+
engine.dispose()
|
|
308
|
+
added_pairs = len(overlap_pairs - existing_pairs)
|
|
309
|
+
return ValidationInjectionReport(
|
|
310
|
+
inserted=inserted,
|
|
311
|
+
skipped_missing=len(skipped),
|
|
312
|
+
skipped=skipped,
|
|
313
|
+
faked=len(faked_ids),
|
|
314
|
+
overlaps_added=added_pairs,
|
|
315
|
+
)
|