Rhapso 0.1.99__py3-none-any.whl → 0.1.993__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.
- Rhapso/matching/ransac_matching.py +32 -36
- Rhapso/pipelines/ray/aws/alignment_pipeline.py +17 -14
- Rhapso/pipelines/ray/interest_point_matching.py +9 -9
- Rhapso/pipelines/ray/local/alignment_pipeline.py +20 -17
- Rhapso/pipelines/ray/solver.py +7 -3
- Rhapso/solver/global_optimization.py +5 -5
- Rhapso/split_dataset/save_xml.py +490 -93
- {rhapso-0.1.99.dist-info → rhapso-0.1.993.dist-info}/METADATA +38 -26
- {rhapso-0.1.99.dist-info → rhapso-0.1.993.dist-info}/RECORD +12 -12
- {rhapso-0.1.99.dist-info → rhapso-0.1.993.dist-info}/WHEEL +0 -0
- {rhapso-0.1.99.dist-info → rhapso-0.1.993.dist-info}/licenses/LICENSE +0 -0
- {rhapso-0.1.99.dist-info → rhapso-0.1.993.dist-info}/top_level.txt +0 -0
Rhapso/split_dataset/save_xml.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import boto3
|
|
3
3
|
import re
|
|
4
|
+
import copy
|
|
4
5
|
from xml.etree import ElementTree as ET
|
|
5
6
|
|
|
6
7
|
class SaveXML:
|
|
@@ -11,44 +12,357 @@ class SaveXML:
|
|
|
11
12
|
self.xml_file = xml_file
|
|
12
13
|
self.xml_output_path = xml_output_path
|
|
13
14
|
|
|
15
|
+
def save_tile_attributes_to_xml(self, xml):
|
|
16
|
+
"""
|
|
17
|
+
Ensure the *last* <ViewSetups> (the outer split one) has:
|
|
18
|
+
- <Attributes name="illumination"> old_tile_0..N </Attributes>
|
|
19
|
+
- <Attributes name="channel"> ... </Attributes>
|
|
20
|
+
- <Attributes name="tile"> with locations from Image Splitting </Attributes>
|
|
21
|
+
- <Attributes name="angle"><Angle id=0 name=0/></Attributes>
|
|
22
|
+
"""
|
|
23
|
+
root = ET.fromstring(xml)
|
|
24
|
+
|
|
25
|
+
def tagname(el):
|
|
26
|
+
return el.tag.split('}')[-1]
|
|
27
|
+
|
|
28
|
+
def find_one(tag):
|
|
29
|
+
el = root.find(f'.//{{*}}{tag}')
|
|
30
|
+
if el is None:
|
|
31
|
+
el = root.find(tag)
|
|
32
|
+
return el
|
|
33
|
+
|
|
34
|
+
def _norm_id(raw):
|
|
35
|
+
if isinstance(raw, (tuple, list)):
|
|
36
|
+
return int(raw[1] if len(raw) > 1 else raw[0])
|
|
37
|
+
return int(raw)
|
|
38
|
+
|
|
39
|
+
# --- find ALL ViewSetups blocks ---
|
|
40
|
+
view_setups_all = root.findall('.//{*}ViewSetups')
|
|
41
|
+
if not view_setups_all:
|
|
42
|
+
return xml # nothing to do
|
|
43
|
+
|
|
44
|
+
# Outer split ViewSetups = last, inner original = first non-outer
|
|
45
|
+
outer_vs = view_setups_all[-1]
|
|
46
|
+
inner_vs = None
|
|
47
|
+
for vs in view_setups_all:
|
|
48
|
+
if vs is not outer_vs:
|
|
49
|
+
inner_vs = vs
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
# --- collect existing Attributes on OUTER ---
|
|
53
|
+
children = list(outer_vs)
|
|
54
|
+
attr_by_name = {}
|
|
55
|
+
for ch in children:
|
|
56
|
+
if tagname(ch) == 'Attributes':
|
|
57
|
+
nm = ch.get('name')
|
|
58
|
+
if nm:
|
|
59
|
+
attr_by_name[nm] = ch
|
|
60
|
+
|
|
61
|
+
# --- ensure CHANNEL attributes (can still be cloned from inner) ---
|
|
62
|
+
if 'channel' not in attr_by_name and inner_vs is not None:
|
|
63
|
+
for ch in list(inner_vs):
|
|
64
|
+
if tagname(ch) != 'Attributes':
|
|
65
|
+
continue
|
|
66
|
+
nm = ch.get('name')
|
|
67
|
+
if nm == 'channel':
|
|
68
|
+
cloned = copy.deepcopy(ch)
|
|
69
|
+
outer_vs.append(cloned)
|
|
70
|
+
attr_by_name['channel'] = cloned
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
# --- build/overwrite ILLUMINATION attributes: old_tile_0..N ---
|
|
74
|
+
illum_attrs = attr_by_name.get('illumination')
|
|
75
|
+
if illum_attrs is None:
|
|
76
|
+
illum_attrs = ET.Element('Attributes', {'name': 'illumination'})
|
|
77
|
+
outer_vs.append(illum_attrs)
|
|
78
|
+
attr_by_name['illumination'] = illum_attrs
|
|
79
|
+
else:
|
|
80
|
+
# clear existing <Illumination> entries
|
|
81
|
+
for ch in list(illum_attrs):
|
|
82
|
+
illum_attrs.remove(ch)
|
|
83
|
+
|
|
84
|
+
# unique original tile ids from old_view
|
|
85
|
+
orig_tile_ids = sorted({_norm_id(v['old_view']) for v in self.self_definition})
|
|
86
|
+
|
|
87
|
+
for tid in orig_tile_ids:
|
|
88
|
+
illum_el = ET.SubElement(illum_attrs, 'Illumination')
|
|
89
|
+
ET.SubElement(illum_el, 'id').text = str(tid)
|
|
90
|
+
ET.SubElement(illum_el, 'name').text = f"old_tile_{tid}"
|
|
91
|
+
|
|
92
|
+
# --- ensure ANGLE attributes: a single Angle id=0/name=0 ---
|
|
93
|
+
angle_attrs = attr_by_name.get('angle')
|
|
94
|
+
if angle_attrs is None:
|
|
95
|
+
# try clone from inner if it exists
|
|
96
|
+
if inner_vs is not None:
|
|
97
|
+
for ch in list(inner_vs):
|
|
98
|
+
if tagname(ch) == 'Attributes' and ch.get('name') == 'angle':
|
|
99
|
+
angle_attrs = copy.deepcopy(ch)
|
|
100
|
+
outer_vs.append(angle_attrs)
|
|
101
|
+
break
|
|
102
|
+
# if no inner angle, synthesize default
|
|
103
|
+
if angle_attrs is None:
|
|
104
|
+
angle_attrs = ET.Element('Attributes', {'name': 'angle'})
|
|
105
|
+
angle_el = ET.SubElement(angle_attrs, 'Angle')
|
|
106
|
+
ET.SubElement(angle_el, 'id').text = "0"
|
|
107
|
+
ET.SubElement(angle_el, 'name').text = "0"
|
|
108
|
+
outer_vs.append(angle_attrs)
|
|
109
|
+
else:
|
|
110
|
+
# if it exists but has no <Angle>, make one
|
|
111
|
+
has_angle = any(tagname(ch) == 'Angle' for ch in angle_attrs)
|
|
112
|
+
if not has_angle:
|
|
113
|
+
angle_el = ET.SubElement(angle_attrs, 'Angle')
|
|
114
|
+
ET.SubElement(angle_el, 'id').text = "0"
|
|
115
|
+
ET.SubElement(angle_el, 'name').text = "0"
|
|
116
|
+
|
|
117
|
+
attr_by_name['angle'] = angle_attrs
|
|
118
|
+
|
|
119
|
+
# ---- find or create <Attributes name="tile"> under OUTER <ViewSetups> ----
|
|
120
|
+
children = list(outer_vs)
|
|
121
|
+
tile_attrs = None
|
|
122
|
+
insert_idx = len(children) # default: append at end
|
|
123
|
+
|
|
124
|
+
for i, ch in enumerate(children):
|
|
125
|
+
if tagname(ch) == 'Attributes':
|
|
126
|
+
name_attr = ch.get('name')
|
|
127
|
+
# remember existing tile attributes if present
|
|
128
|
+
if name_attr == 'tile':
|
|
129
|
+
tile_attrs = ch
|
|
130
|
+
# prefer to insert tile after channel attributes if we create it
|
|
131
|
+
if name_attr == 'channel':
|
|
132
|
+
insert_idx = i + 1
|
|
133
|
+
|
|
134
|
+
if tile_attrs is None:
|
|
135
|
+
tile_attrs = ET.Element('Attributes', {'name': 'tile'})
|
|
136
|
+
outer_vs.insert(insert_idx, tile_attrs)
|
|
137
|
+
|
|
138
|
+
# ---- figure out which tile ids (new_view ids) we care about ----
|
|
139
|
+
target_ids = {_norm_id(v['new_view']) for v in self.self_definition}
|
|
140
|
+
|
|
141
|
+
# Remove existing Tile entries for those ids (so we can rewrite cleanly)
|
|
142
|
+
for child in list(tile_attrs):
|
|
143
|
+
if tagname(child) != 'Tile':
|
|
144
|
+
continue
|
|
145
|
+
id_el = child.find('id') or child.find('{*}id')
|
|
146
|
+
if id_el is None or not id_el.text:
|
|
147
|
+
continue
|
|
148
|
+
try:
|
|
149
|
+
if int(id_el.text.strip()) in target_ids:
|
|
150
|
+
tile_attrs.remove(child)
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# ---- build a map: setup_id -> (tx, ty, tz) from 'Image Splitting' ----
|
|
155
|
+
view_regs = find_one('ViewRegistrations')
|
|
156
|
+
tile_locations = {}
|
|
157
|
+
|
|
158
|
+
if view_regs is not None:
|
|
159
|
+
# iterate over ViewRegistration elements, namespace-agnostic
|
|
160
|
+
for vr in view_regs.findall('.//{*}ViewRegistration'):
|
|
161
|
+
setup_attr = vr.get('setup')
|
|
162
|
+
if setup_attr is None:
|
|
163
|
+
continue
|
|
164
|
+
try:
|
|
165
|
+
setup_id = int(setup_attr)
|
|
166
|
+
except ValueError:
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
if setup_id not in target_ids:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# find the Image Splitting transform
|
|
173
|
+
for vt in vr.findall('./{*}ViewTransform'):
|
|
174
|
+
name_el = vt.find('Name') or vt.find('{*}Name')
|
|
175
|
+
if name_el is None:
|
|
176
|
+
continue
|
|
177
|
+
if (name_el.text or '').strip().lower() != 'image splitting':
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
aff_el = vt.find('affine') or vt.find('{*}affine')
|
|
181
|
+
if aff_el is None or not aff_el.text:
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
nums = re.findall(
|
|
185
|
+
r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?',
|
|
186
|
+
aff_el.text
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# 3x4 affine: we expect at least 12 numbers
|
|
190
|
+
if len(nums) >= 12:
|
|
191
|
+
tx, ty, tz = map(float, (nums[3], nums[7], nums[11]))
|
|
192
|
+
elif len(nums) >= 3:
|
|
193
|
+
# fallback: last 3 numbers
|
|
194
|
+
tx, ty, tz = map(float, nums[-3:])
|
|
195
|
+
else:
|
|
196
|
+
tx = ty = tz = 0.0
|
|
197
|
+
|
|
198
|
+
tile_locations[setup_id] = (tx, ty, tz)
|
|
199
|
+
break # stop after first 'Image Splitting' for this VR
|
|
200
|
+
|
|
201
|
+
# ---- create Tile entries for each new_view ----
|
|
202
|
+
for view in self.self_definition:
|
|
203
|
+
new_id = _norm_id(view['new_view'])
|
|
204
|
+
|
|
205
|
+
if new_id in tile_locations:
|
|
206
|
+
loc = tile_locations[new_id]
|
|
207
|
+
else:
|
|
208
|
+
# Fallback: use min bound of interval if we didn't find an image splitting transform
|
|
209
|
+
mins = np.array(view['interval'][0], dtype=float)
|
|
210
|
+
loc = (float(mins[0]), float(mins[1]), float(mins[2]))
|
|
211
|
+
|
|
212
|
+
tile_el = ET.SubElement(tile_attrs, 'Tile')
|
|
213
|
+
ET.SubElement(tile_el, 'id').text = str(new_id)
|
|
214
|
+
ET.SubElement(tile_el, 'name').text = str(new_id)
|
|
215
|
+
ET.SubElement(tile_el, 'location').text = f"{loc[0]:.1f} {loc[1]:.1f} {loc[2]:.1f}"
|
|
216
|
+
|
|
217
|
+
# ---- reorder children in OUTER <ViewSetups>:
|
|
218
|
+
# all <ViewSetup> first, then <Attributes> in illumination, channel, tile, angle order ----
|
|
219
|
+
children = list(outer_vs)
|
|
220
|
+
|
|
221
|
+
viewsetup_children = [ch for ch in children if tagname(ch) == 'ViewSetup']
|
|
222
|
+
attr_children = [ch for ch in children if tagname(ch) == 'Attributes']
|
|
223
|
+
other_children = [ch for ch in children if tagname(ch) not in ('ViewSetup', 'Attributes')]
|
|
224
|
+
|
|
225
|
+
# desired attributes order
|
|
226
|
+
attr_order = {'illumination': 0, 'channel': 1, 'tile': 2, 'angle': 3}
|
|
227
|
+
|
|
228
|
+
def _attr_sort_key(el):
|
|
229
|
+
name = el.get('name', '')
|
|
230
|
+
return attr_order.get(name, 99)
|
|
231
|
+
|
|
232
|
+
attr_children.sort(key=_attr_sort_key)
|
|
233
|
+
|
|
234
|
+
# Clear existing children and re-append in desired order
|
|
235
|
+
for ch in children:
|
|
236
|
+
outer_vs.remove(ch)
|
|
237
|
+
|
|
238
|
+
for ch in viewsetup_children + attr_children + other_children:
|
|
239
|
+
outer_vs.append(ch)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
ET.indent(root, space=" ")
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
return ET.tostring(root, encoding='unicode')
|
|
247
|
+
|
|
14
248
|
def wrap_image_loader_for_split(self, xml: str) -> str:
|
|
249
|
+
"""
|
|
250
|
+
Wrap the top-level ImageLoader in <ImageLoader format="split.viewerimgloader">
|
|
251
|
+
and move the ORIGINAL ViewSetups/Timepoints/MissingViews into an inner
|
|
252
|
+
<SequenceDescription> inside that wrapper.
|
|
253
|
+
|
|
254
|
+
Resulting structure:
|
|
255
|
+
|
|
256
|
+
<SpimData>
|
|
257
|
+
<BasePath/>
|
|
258
|
+
<SequenceDescription>
|
|
259
|
+
<ImageLoader format="split.viewerimgloader">
|
|
260
|
+
<ImageLoader format="bdv.multimg.zarr"> ... </ImageLoader>
|
|
261
|
+
<SequenceDescription>
|
|
262
|
+
<ViewSetups> (ORIGINAL) </ViewSetups>
|
|
263
|
+
<Timepoints> (ORIGINAL) </Timepoints>
|
|
264
|
+
<MissingViews/>
|
|
265
|
+
</SequenceDescription>
|
|
266
|
+
<!-- SetupIds (for split tiles) will be added later -->
|
|
267
|
+
</ImageLoader>
|
|
268
|
+
<!-- NEW ViewSetups/Timepoints/MissingViews for split views are added later -->
|
|
269
|
+
</SequenceDescription>
|
|
270
|
+
<ViewRegistrations> ... </ViewRegistrations>
|
|
271
|
+
</SpimData>
|
|
272
|
+
"""
|
|
15
273
|
root = ET.fromstring(xml)
|
|
16
274
|
|
|
17
|
-
def tn(el):
|
|
275
|
+
def tn(el):
|
|
276
|
+
return el.tag.split('}')[-1]
|
|
277
|
+
|
|
18
278
|
def find_one(tag):
|
|
19
279
|
el = root.find(f'.//{{*}}{tag}')
|
|
20
280
|
return el if el is not None else root.find(tag)
|
|
21
281
|
|
|
22
|
-
seq =
|
|
282
|
+
seq = None
|
|
283
|
+
# Prefer the top-level SequenceDescription (direct child of root)
|
|
284
|
+
for ch in list(root):
|
|
285
|
+
if tn(ch) == 'SequenceDescription':
|
|
286
|
+
seq = ch
|
|
287
|
+
break
|
|
288
|
+
if seq is None:
|
|
289
|
+
seq = find_one('SequenceDescription')
|
|
23
290
|
if seq is None:
|
|
24
|
-
return xml
|
|
25
|
-
|
|
26
|
-
# find the first immediate ImageLoader under SequenceDescription
|
|
27
|
-
loaders = [ch for ch in list(seq) if tn(ch) == 'ImageLoader']
|
|
28
|
-
if not loaders:
|
|
29
291
|
return xml
|
|
30
292
|
|
|
31
|
-
|
|
293
|
+
children = list(seq)
|
|
32
294
|
|
|
33
|
-
|
|
34
|
-
|
|
295
|
+
# Find the first immediate ImageLoader under SequenceDescription
|
|
296
|
+
base_loader = None
|
|
297
|
+
base_loader_idx = None
|
|
298
|
+
for i, ch in enumerate(children):
|
|
299
|
+
if tn(ch) == 'ImageLoader':
|
|
300
|
+
base_loader = ch
|
|
301
|
+
base_loader_idx = i
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
if base_loader is None:
|
|
35
305
|
return xml
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
306
|
+
|
|
307
|
+
fmt = (base_loader.get('format') or '').lower()
|
|
308
|
+
# Already wrapped; assume layout is correct and do nothing
|
|
309
|
+
if fmt == 'split.viewerimgloader':
|
|
39
310
|
return xml
|
|
40
311
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
312
|
+
# Collect any other ImageLoader siblings (other sources)
|
|
313
|
+
other_imageloaders = []
|
|
314
|
+
# Collect ORIGINAL ViewSetups / Timepoints / MissingViews that are siblings
|
|
315
|
+
orig_viewsetups = None
|
|
316
|
+
orig_timepoints = None
|
|
317
|
+
orig_missingviews = None
|
|
318
|
+
|
|
319
|
+
for ch in children[base_loader_idx + 1:]:
|
|
320
|
+
name = tn(ch)
|
|
321
|
+
if name == 'ImageLoader':
|
|
322
|
+
other_imageloaders.append(ch)
|
|
323
|
+
elif name == 'ViewSetups':
|
|
324
|
+
orig_viewsetups = ch
|
|
325
|
+
elif name == 'Timepoints':
|
|
326
|
+
orig_timepoints = ch
|
|
327
|
+
elif name == 'MissingViews':
|
|
328
|
+
orig_missingviews = ch
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# Remove them from the outer SequenceDescription
|
|
332
|
+
for node in (orig_viewsetups, orig_timepoints, orig_missingviews, *other_imageloaders):
|
|
333
|
+
if node is not None and node in seq:
|
|
334
|
+
seq.remove(node)
|
|
335
|
+
|
|
336
|
+
# Remove the original loader from seq
|
|
337
|
+
seq.remove(base_loader)
|
|
338
|
+
|
|
339
|
+
# Build wrapper <ImageLoader format="split.viewerimgloader">
|
|
44
340
|
wrapper = ET.Element('ImageLoader', {'format': 'split.viewerimgloader'})
|
|
45
|
-
|
|
46
|
-
|
|
341
|
+
# First child: original loader
|
|
342
|
+
wrapper.append(base_loader)
|
|
343
|
+
for other_loader in other_imageloaders:
|
|
344
|
+
wrapper.append(other_loader)
|
|
345
|
+
|
|
346
|
+
# Inner <SequenceDescription> that holds the original ViewSetups/Timepoints/MissingViews
|
|
347
|
+
inner_seq = ET.Element('SequenceDescription')
|
|
348
|
+
|
|
349
|
+
if orig_viewsetups is not None:
|
|
350
|
+
inner_seq.append(orig_viewsetups)
|
|
351
|
+
if orig_timepoints is not None:
|
|
352
|
+
inner_seq.append(orig_timepoints)
|
|
353
|
+
if orig_missingviews is not None:
|
|
354
|
+
inner_seq.append(orig_missingviews)
|
|
355
|
+
|
|
356
|
+
wrapper.append(inner_seq)
|
|
357
|
+
|
|
358
|
+
# Insert wrapper where the original loader was
|
|
359
|
+
seq.insert(base_loader_idx, wrapper)
|
|
47
360
|
|
|
48
361
|
try:
|
|
49
362
|
ET.indent(root, space=" ")
|
|
50
363
|
except Exception:
|
|
51
364
|
pass
|
|
365
|
+
|
|
52
366
|
return ET.tostring(root, encoding='unicode')
|
|
53
367
|
|
|
54
368
|
def save_view_interest_points(self, xml):
|
|
@@ -202,46 +516,66 @@ class SaveXML:
|
|
|
202
516
|
return ET.tostring(root, encoding='unicode')
|
|
203
517
|
|
|
204
518
|
def save_setup_id_to_xml(self, xml):
|
|
519
|
+
"""
|
|
520
|
+
Create/overwrite the OUTER <ViewSetups> (split tiles) and ensure outer
|
|
521
|
+
<Timepoints> and <MissingViews> exist under the top-level SequenceDescription.
|
|
522
|
+
|
|
523
|
+
Outer layout target:
|
|
524
|
+
|
|
525
|
+
<SpimData>
|
|
526
|
+
<BasePath/>
|
|
527
|
+
<SequenceDescription>
|
|
528
|
+
<ImageLoader format="split.viewerimgloader">
|
|
529
|
+
...
|
|
530
|
+
<SequenceDescription> (original) </SequenceDescription>
|
|
531
|
+
<SetupIds> ... </SetupIds> (from save_setup_id_definition_to_xml)
|
|
532
|
+
</ImageLoader>
|
|
533
|
+
<ViewSetups> <-- created here (ids 0..499)
|
|
534
|
+
<ViewSetup>...</ViewSetup>
|
|
535
|
+
...
|
|
536
|
+
<Attributes ...>...</Attributes>
|
|
537
|
+
</ViewSetups>
|
|
538
|
+
<Timepoints type="pattern">
|
|
539
|
+
<integerpattern>0</integerpattern>
|
|
540
|
+
</Timepoints>
|
|
541
|
+
<MissingViews/>
|
|
542
|
+
</SequenceDescription>
|
|
543
|
+
<ViewRegistrations>...</ViewRegistrations>
|
|
544
|
+
</SpimData>
|
|
545
|
+
"""
|
|
205
546
|
root = ET.fromstring(xml)
|
|
206
547
|
|
|
207
|
-
def
|
|
548
|
+
def tn(el):
|
|
208
549
|
return el.tag.split('}')[-1]
|
|
209
550
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
setup_ids = ET.Element('SetupIds')
|
|
221
|
-
kids = list(root)
|
|
222
|
-
insert_idx = len(kids)
|
|
223
|
-
if regs is not None and regs in kids:
|
|
224
|
-
insert_idx = kids.index(regs)
|
|
225
|
-
elif seq is not None and seq in kids:
|
|
226
|
-
insert_idx = kids.index(seq) + 1
|
|
227
|
-
root.insert(insert_idx, setup_ids)
|
|
551
|
+
# Find top-level SequenceDescription
|
|
552
|
+
outer_seq = None
|
|
553
|
+
for ch in list(root):
|
|
554
|
+
if tn(ch) == 'SequenceDescription':
|
|
555
|
+
outer_seq = ch
|
|
556
|
+
break
|
|
557
|
+
if outer_seq is None:
|
|
558
|
+
outer_seq = root.find('.//{*}SequenceDescription')
|
|
559
|
+
if outer_seq is None:
|
|
560
|
+
return xml
|
|
228
561
|
|
|
562
|
+
# Find or create OUTER <ViewSetups> under SequenceDescription
|
|
229
563
|
view_setups = None
|
|
230
|
-
for ch in list(
|
|
231
|
-
if
|
|
564
|
+
for ch in list(outer_seq):
|
|
565
|
+
if tn(ch) == 'ViewSetups':
|
|
232
566
|
view_setups = ch
|
|
233
567
|
break
|
|
234
|
-
|
|
235
|
-
view_setups = find_one('ViewSetups')
|
|
568
|
+
|
|
236
569
|
if view_setups is None:
|
|
237
570
|
view_setups = ET.Element('ViewSetups')
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
for i, ch in enumerate(
|
|
241
|
-
if
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
571
|
+
children = list(outer_seq)
|
|
572
|
+
insert_idx = len(children)
|
|
573
|
+
for i, ch in enumerate(children):
|
|
574
|
+
if tn(ch) == 'ImageLoader':
|
|
575
|
+
insert_idx = i + 1
|
|
576
|
+
outer_seq.insert(insert_idx, view_setups)
|
|
577
|
+
|
|
578
|
+
# Helper to normalize ids
|
|
245
579
|
def _norm_id(raw):
|
|
246
580
|
if isinstance(raw, (tuple, list)):
|
|
247
581
|
if len(raw) >= 2:
|
|
@@ -249,11 +583,11 @@ class SaveXML:
|
|
|
249
583
|
return int(raw[0])
|
|
250
584
|
return int(raw)
|
|
251
585
|
|
|
252
|
-
target_ids =
|
|
586
|
+
target_ids = {_norm_id(v['new_view']) for v in self.self_definition}
|
|
253
587
|
|
|
254
|
-
# ViewSetup
|
|
588
|
+
# Remove any existing ViewSetup with those ids (outer only)
|
|
255
589
|
for child in list(view_setups):
|
|
256
|
-
if
|
|
590
|
+
if tn(child) != 'ViewSetup':
|
|
257
591
|
continue
|
|
258
592
|
id_el = child.find('id') or child.find('{*}id')
|
|
259
593
|
if id_el is not None and id_el.text:
|
|
@@ -263,40 +597,22 @@ class SaveXML:
|
|
|
263
597
|
except Exception:
|
|
264
598
|
pass
|
|
265
599
|
|
|
266
|
-
#
|
|
267
|
-
for sid in list(setup_ids):
|
|
268
|
-
if tagname(sid) != 'SetupIdDefinition':
|
|
269
|
-
continue
|
|
270
|
-
nid_el = sid.find('NewId') or sid.find('{*}NewId')
|
|
271
|
-
if nid_el is not None and nid_el.text:
|
|
272
|
-
try:
|
|
273
|
-
if int(nid_el.text.strip()) in target_ids:
|
|
274
|
-
setup_ids.remove(sid)
|
|
275
|
-
except Exception:
|
|
276
|
-
pass
|
|
277
|
-
|
|
600
|
+
# (Re)build ViewSetups for each new split view
|
|
278
601
|
for view in self.self_definition:
|
|
279
602
|
new_id = _norm_id(view['new_view'])
|
|
280
|
-
old_id = _norm_id(view['old_view'])
|
|
281
|
-
|
|
282
|
-
|
|
603
|
+
# old_id = _norm_id(view['old_view']) # not strictly needed here
|
|
604
|
+
|
|
605
|
+
angle = view['angle']
|
|
606
|
+
channel = view['channel']
|
|
283
607
|
illumination = view['illumination']
|
|
284
|
-
tile
|
|
285
|
-
voxel_unit
|
|
286
|
-
voxel_size
|
|
608
|
+
tile = new_id
|
|
609
|
+
voxel_unit = view['voxel_unit']
|
|
610
|
+
voxel_size = view['voxel_dim']
|
|
287
611
|
|
|
288
612
|
mins = np.array(view["interval"][0], dtype=np.int64)
|
|
289
613
|
maxs = np.array(view["interval"][1], dtype=np.int64)
|
|
290
614
|
size = (maxs - mins + 1).tolist()
|
|
291
615
|
|
|
292
|
-
# <SetupIds>/<SetupIdDefinition>
|
|
293
|
-
def_el = ET.SubElement(setup_ids, 'SetupIdDefinition')
|
|
294
|
-
ET.SubElement(def_el, 'NewId').text = str(new_id)
|
|
295
|
-
ET.SubElement(def_el, 'OldId').text = str(old_id)
|
|
296
|
-
ET.SubElement(def_el, 'min').text = f"{int(mins[0])} {int(mins[1])} {int(mins[2])}"
|
|
297
|
-
ET.SubElement(def_el, 'max').text = f"{int(maxs[0])} {int(maxs[1])} {int(maxs[2])}"
|
|
298
|
-
|
|
299
|
-
# <ViewSetups>/<ViewSetup>
|
|
300
616
|
vs = ET.SubElement(view_setups, 'ViewSetup')
|
|
301
617
|
ET.SubElement(vs, 'id').text = str(new_id)
|
|
302
618
|
ET.SubElement(vs, 'size').text = f"{int(size[0])} {int(size[1])} {int(size[2])}"
|
|
@@ -314,6 +630,31 @@ class SaveXML:
|
|
|
314
630
|
ET.SubElement(attrs, 'tile').text = str(int(tile))
|
|
315
631
|
ET.SubElement(attrs, 'angle').text = str(int(angle))
|
|
316
632
|
|
|
633
|
+
# Ensure outer <Timepoints> exists
|
|
634
|
+
outer_timepoints = None
|
|
635
|
+
for ch in list(outer_seq):
|
|
636
|
+
if tn(ch) == 'Timepoints':
|
|
637
|
+
outer_timepoints = ch
|
|
638
|
+
break
|
|
639
|
+
if outer_timepoints is None:
|
|
640
|
+
outer_timepoints = ET.Element('Timepoints', {'type': 'pattern'})
|
|
641
|
+
ip = ET.SubElement(outer_timepoints, 'integerpattern')
|
|
642
|
+
ip.text = "0"
|
|
643
|
+
# place right after ViewSetups
|
|
644
|
+
children = list(outer_seq)
|
|
645
|
+
insert_idx = children.index(view_setups) + 1 if view_setups in children else len(children)
|
|
646
|
+
outer_seq.insert(insert_idx, outer_timepoints)
|
|
647
|
+
|
|
648
|
+
# Ensure outer <MissingViews> exists
|
|
649
|
+
outer_missing = None
|
|
650
|
+
for ch in list(outer_seq):
|
|
651
|
+
if tn(ch) == 'MissingViews':
|
|
652
|
+
outer_missing = ch
|
|
653
|
+
break
|
|
654
|
+
if outer_missing is None:
|
|
655
|
+
outer_missing = ET.Element('MissingViews')
|
|
656
|
+
outer_seq.append(outer_missing)
|
|
657
|
+
|
|
317
658
|
try:
|
|
318
659
|
ET.indent(root, space=" ")
|
|
319
660
|
except Exception:
|
|
@@ -322,38 +663,92 @@ class SaveXML:
|
|
|
322
663
|
return ET.tostring(root, encoding='unicode')
|
|
323
664
|
|
|
324
665
|
def save_setup_id_definition_to_xml(self, xml):
|
|
666
|
+
"""
|
|
667
|
+
Create/overwrite <SetupIds> for the split views.
|
|
668
|
+
|
|
669
|
+
In the desired final layout, SetupIds lives inside:
|
|
670
|
+
<SequenceDescription>
|
|
671
|
+
<ImageLoader format="split.viewerimgloader">
|
|
672
|
+
...
|
|
673
|
+
<SequenceDescription> ... </SequenceDescription>
|
|
674
|
+
<SetupIds> ... </SetupIds> <-- here
|
|
675
|
+
</ImageLoader>
|
|
676
|
+
...
|
|
677
|
+
</SequenceDescription>
|
|
678
|
+
"""
|
|
325
679
|
root = ET.fromstring(xml)
|
|
326
680
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
681
|
+
def tn(el):
|
|
682
|
+
return el.tag.split('}')[-1]
|
|
683
|
+
|
|
684
|
+
# Find top-level SequenceDescription
|
|
685
|
+
outer_seq = None
|
|
686
|
+
for ch in list(root):
|
|
687
|
+
if tn(ch) == 'SequenceDescription':
|
|
688
|
+
outer_seq = ch
|
|
689
|
+
break
|
|
690
|
+
if outer_seq is None:
|
|
691
|
+
outer_seq = root.find('.//{*}SequenceDescription')
|
|
692
|
+
if outer_seq is None:
|
|
693
|
+
return xml
|
|
694
|
+
|
|
695
|
+
# Find the wrapper ImageLoader format="split.viewerimgloader"
|
|
696
|
+
wrapper = None
|
|
697
|
+
for ch in list(outer_seq):
|
|
698
|
+
if tn(ch) == 'ImageLoader' and (ch.get('format') or '').lower() == 'split.viewerimgloader':
|
|
699
|
+
wrapper = ch
|
|
700
|
+
break
|
|
701
|
+
|
|
702
|
+
# If wrapper not found, fall back to old behavior (root-level SetupIds)
|
|
703
|
+
parent_for_setupids = wrapper if wrapper is not None else root
|
|
704
|
+
children = list(parent_for_setupids)
|
|
705
|
+
|
|
706
|
+
# Locate existing <SetupIds> under the chosen parent
|
|
707
|
+
setup_ids = None
|
|
708
|
+
for ch in children:
|
|
709
|
+
if tn(ch) == 'SetupIds':
|
|
710
|
+
setup_ids = ch
|
|
711
|
+
break
|
|
333
712
|
|
|
334
|
-
# create/position <SetupIds>
|
|
335
713
|
if setup_ids is None:
|
|
336
714
|
setup_ids = ET.Element('SetupIds')
|
|
337
|
-
|
|
338
|
-
|
|
715
|
+
if wrapper is not None:
|
|
716
|
+
# Under wrapper: insert after inner SequenceDescription if present
|
|
717
|
+
inner_children = list(wrapper)
|
|
718
|
+
inner_seq = None
|
|
719
|
+
for ich in inner_children:
|
|
720
|
+
if tn(ich) == 'SequenceDescription':
|
|
721
|
+
inner_seq = ich
|
|
722
|
+
break
|
|
723
|
+
insert_idx = inner_children.index(inner_seq) + 1 if inner_seq is not None else len(inner_children)
|
|
724
|
+
wrapper.insert(insert_idx, setup_ids)
|
|
725
|
+
else:
|
|
726
|
+
# Root-level fallback: put before <ViewRegistrations> if present
|
|
727
|
+
root_children = list(root)
|
|
728
|
+
regs_idx = next((i for i, ch in enumerate(root_children) if tn(ch) == 'ViewRegistrations'), None)
|
|
729
|
+
insert_idx = regs_idx if regs_idx is not None else len(root_children)
|
|
730
|
+
root.insert(insert_idx, setup_ids)
|
|
339
731
|
else:
|
|
340
|
-
|
|
732
|
+
# Clear existing definitions so we can rewrite
|
|
733
|
+
setup_ids.clear()
|
|
341
734
|
|
|
735
|
+
# Now populate SetupIdDefinition from self.self_definition
|
|
342
736
|
for view in self.self_definition:
|
|
343
737
|
new_id = view['new_view']
|
|
344
738
|
old_id = view['old_view']
|
|
345
739
|
min_bound = view['interval'][0]
|
|
346
740
|
max_bound = view['interval'][1]
|
|
347
|
-
|
|
741
|
+
|
|
742
|
+
# Normalize IDs (can be int or (tp, setup))
|
|
348
743
|
nid = int(new_id[1] if isinstance(new_id, (tuple, list)) else new_id)
|
|
349
744
|
oid = int(old_id[1] if isinstance(old_id, (tuple, list)) else old_id)
|
|
350
745
|
|
|
351
746
|
def_el = ET.SubElement(setup_ids, 'SetupIdDefinition')
|
|
352
747
|
ET.SubElement(def_el, 'NewId').text = str(nid)
|
|
353
748
|
ET.SubElement(def_el, 'OldId').text = str(oid)
|
|
354
|
-
ET.SubElement(def_el, 'min').text
|
|
355
|
-
ET.SubElement(def_el, 'max').text
|
|
356
|
-
|
|
749
|
+
ET.SubElement(def_el, 'min').text = f"{int(min_bound[0])} {int(min_bound[1])} {int(min_bound[2])}"
|
|
750
|
+
ET.SubElement(def_el, 'max').text = f"{int(max_bound[0])} {int(max_bound[1])} {int(max_bound[2])}"
|
|
751
|
+
|
|
357
752
|
try:
|
|
358
753
|
ET.indent(root, space=" ")
|
|
359
754
|
except Exception:
|
|
@@ -362,11 +757,13 @@ class SaveXML:
|
|
|
362
757
|
return ET.tostring(root, encoding='unicode')
|
|
363
758
|
|
|
364
759
|
def run(self):
|
|
365
|
-
xml = self.
|
|
760
|
+
xml = self.xml_file
|
|
761
|
+
xml = self.wrap_image_loader_for_split(xml)
|
|
762
|
+
xml = self.save_setup_id_definition_to_xml(xml)
|
|
366
763
|
xml = self.save_setup_id_to_xml(xml)
|
|
367
764
|
xml = self.save_view_registrations_to_xml(xml)
|
|
765
|
+
xml = self.save_tile_attributes_to_xml(xml)
|
|
368
766
|
xml = self.save_view_interest_points(xml)
|
|
369
|
-
xml = self.wrap_image_loader_for_split(xml)
|
|
370
767
|
|
|
371
768
|
if self.xml_output_path:
|
|
372
769
|
if self.xml_output_path.startswith("s3://"):
|