11import pathlib
2+ from pathlib import Path
23import xml .etree .ElementTree as ET
34from datetime import datetime
4-
55import numpy as np
66
77
8- def get_prairieview_metadata (ome_tif_filepath : str ) -> dict :
9- """Extract metadata for scans generated by Prairie View acquisition software.
8+ class PrairieViewMeta :
109
11- The Prairie View software generates one `.ome.tif` imaging file per frame
12- acquired. The metadata for all frames is contained in one .xml file. This
13- function locates the .xml file and generates a dictionary necessary to
14- populate the DataJoint `ScanInfo` and `Field` tables. Prairie View works
15- with resonance scanners with a single field. Prairie View does not support
16- bidirectional x and y scanning. ROI information is not contained in the
17- `.xml` file. All images generated using Prairie View have square dimensions(e.g. 512x512).
10+ def __init__ (self , prairieview_dir : str ):
11+ """Initialize PrairieViewMeta loader class
1812
19- Args:
20- ome_tif_filepath: An absolute path to the .ome.tif image file.
13+ Args:
14+ prairieview_dir (str): string, absolute file path to directory containing PrairieView dataset
15+ """
16+ # ---- Search and verify CaImAn output file exists ----
17+ # May return multiple xml files. Only need one that contains scan metadata.
18+ self .prairieview_dir = Path (prairieview_dir )
2119
22- Raises:
23- FileNotFoundError: No .xml file containing information about the acquired scan
24- was found at path in parent directory at `ome_tif_filepath`.
20+ for file in self .prairieview_dir .glob ("*.xml" ):
21+ xml_tree = ET .parse (file )
22+ xml_root = xml_tree .getroot ()
23+ if xml_root .find (".//Sequence" ):
24+ self .xml_file = file
25+ self ._xml_root = xml_root
26+ break
27+ else :
28+ raise FileNotFoundError (
29+ f"No PrarieView metadata .xml file found at { prairieview_dir } "
30+ )
2531
26- Returns:
27- metainfo: A dict mapping keys to corresponding metadata values fetched from the
28- .xml file.
29- """
32+ self ._meta = None
3033
31- # May return multiple xml files. Only need one that contains scan metadata.
32- xml_files_list = pathlib .Path (ome_tif_filepath ).parent .glob ("*.xml" )
34+ @property
35+ def meta (self ):
36+ if self ._meta is None :
37+ self ._meta = _extract_prairieview_metadata (self .xml_file )
38+ return self ._meta
3339
34- for file in xml_files_list :
35- xml_tree = ET .parse (file )
36- xml_file = xml_tree .getroot ()
37- if xml_file .find (".//Sequence" ):
38- break
39- else :
40- raise FileNotFoundError (
41- f"No PrarieView metadata .xml file found at { pathlib .Path (ome_tif_filepath ).parent } "
42- )
40+ def get_prairieview_files (self , plane_idx = None , channel = None ):
41+ if plane_idx is None :
42+ if self .meta ['num_planes' ] > 1 :
43+ raise ValueError (f"Please specify 'plane_idx' - Plane indices: { self .meta ['plane_indices' ]} " )
44+ else :
45+ plane_idx = self .meta ['plane_indices' ][0 ]
46+ else :
47+ assert plane_idx in self .meta ['plane_indices' ], f"Invalid 'plane_idx' - Plane indices: { self .meta ['plane_indices' ]} "
48+
49+ if channel is None :
50+ if self .meta ['num_channels' ] > 1 :
51+ raise ValueError (f"Please specify 'channel' - Channels: { self .meta ['channels' ]} " )
52+ else :
53+ plane_idx = self .meta ['channels' ][0 ]
54+ else :
55+ assert channel in self .meta ['channels' ], f"Invalid 'channel' - Channels: { self .meta ['channels' ]} "
56+
57+ frames = self ._xml_root .findall (f".//Sequence/Frame/[@index='{ plane_idx } ']/File/[@channel='{ channel } ']" )
58+ return [f .attrib ['filename' ] for f in frames ]
59+
60+
61+ def _extract_prairieview_metadata (xml_filepath : str ):
62+ xml_filepath = Path (xml_filepath )
63+ if not xml_filepath .exists ():
64+ raise FileNotFoundError (f"{ xml_filepath } does not exist" )
65+ xml_tree = ET .parse (xml_filepath )
66+ xml_root = xml_tree .getroot ()
4367
4468 bidirectional_scan = False # Does not support bidirectional
4569 roi = 0
4670 n_fields = 1 # Always contains 1 field
47- recording_start_time = xml_file .find (".//Sequence/[@cycle='1']" ).attrib .get ("time" )
71+ recording_start_time = xml_root .find (".//Sequence/[@cycle='1']" ).attrib .get ("time" )
4872
4973 # Get all channels and find unique values
5074 channel_list = [
5175 int (channel .attrib .get ("channel" ))
52- for channel in xml_file .iterfind (".//Sequence/Frame/File/[@channel]" )
76+ for channel in xml_root .iterfind (".//Sequence/Frame/File/[@channel]" )
5377 ]
54- n_channels = len (set (channel_list ))
55- n_frames = len (xml_file .findall (".//Sequence/Frame" ))
78+ channels = set (channel_list )
79+ n_channels = len (channels )
80+ n_frames = len (xml_root .findall (".//Sequence/Frame" ))
5681 framerate = 1 / float (
57- xml_file .findall ('.//PVStateValue/[@key="framePeriod"]' )[0 ].attrib .get ("value" )
82+ xml_root .findall ('.//PVStateValue/[@key="framePeriod"]' )[0 ].attrib .get ("value" )
5883 ) # rate = 1/framePeriod
5984
6085 usec_per_line = (
6186 float (
62- xml_file .findall (".//PVStateValue/[@key='scanLinePeriod']" )[0 ].attrib .get (
87+ xml_root .findall (".//PVStateValue/[@key='scanLinePeriod']" )[0 ].attrib .get (
6388 "value"
6489 )
6590 )
6691 * 1e6
6792 ) # Convert from seconds to microseconds
6893
6994 scan_datetime = datetime .strptime (
70- xml_file .attrib .get ("date" ), "%m/%d/%Y %I:%M:%S %p"
95+ xml_root .attrib .get ("date" ), "%m/%d/%Y %I:%M:%S %p"
7196 )
7297
7398 total_scan_duration = float (
74- xml_file .findall (".//Sequence/Frame" )[- 1 ].attrib .get ("relativeTime" )
99+ xml_root .findall (".//Sequence/Frame" )[- 1 ].attrib .get ("relativeTime" )
75100 )
76101
77102 pixel_height = int (
78- xml_file .findall (".//PVStateValue/[@key='pixelsPerLine']" )[0 ].attrib .get (
103+ xml_root .findall (".//PVStateValue/[@key='pixelsPerLine']" )[0 ].attrib .get (
79104 "value"
80105 )
81106 )
82107 # All PrairieView-acquired images have square dimensions (512 x 512; 1024 x 1024)
83108 pixel_width = pixel_height
84109
85110 um_per_pixel = float (
86- xml_file .find (
111+ xml_root .find (
87112 ".//PVStateValue/[@key='micronsPerPixel']/IndexedValue/[@index='XAxis']"
88113 ).attrib .get ("value" )
89114 )
@@ -92,43 +117,45 @@ def get_prairieview_metadata(ome_tif_filepath: str) -> dict:
92117
93118 # x and y coordinate values for the center of the field
94119 x_field = float (
95- xml_file .find (
120+ xml_root .find (
96121 ".//PVStateValue/[@key='currentScanCenter']/IndexedValue/[@index='XAxis']"
97122 ).attrib .get ("value" )
98123 )
99124 y_field = float (
100- xml_file .find (
125+ xml_root .find (
101126 ".//PVStateValue/[@key='currentScanCenter']/IndexedValue/[@index='YAxis']"
102127 ).attrib .get ("value" )
103128 )
129+
104130 if (
105- xml_file .find (
131+ xml_root .find (
106132 ".//Sequence/[@cycle='1']/Frame/PVStateShard/PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']"
107133 )
108134 is None
109135 ):
110136 z_fields = np .float64 (
111- xml_file .find (
137+ xml_root .find (
112138 ".//PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']/SubindexedValue"
113139 ).attrib .get ("value" )
114140 )
115141 n_depths = 1
142+ plane_indices = {0 }
116143 assert z_fields .size == n_depths
117144 bidirection_z = False
118-
119145 else :
120146 bidirection_z = (
121- xml_file .find (".//Sequence" ).attrib .get ("bidirectionalZ" ) == "True"
147+ xml_root .find (".//Sequence" ).attrib .get ("bidirectionalZ" ) == "True"
122148 )
123149
124150 # One "Frame" per depth in the .xml file. Gets number of frames in first sequence
125151 planes = [
126152 int (plane .attrib .get ("index" ))
127- for plane in xml_file .findall (".//Sequence/[@cycle='1']/Frame" )
153+ for plane in xml_root .findall (".//Sequence/[@cycle='1']/Frame" )
128154 ]
129- n_depths = len (set (planes ))
155+ plane_indices = set (planes )
156+ n_depths = len (plane_indices )
130157
131- z_controllers = xml_file .findall (
158+ z_controllers = xml_root .findall (
132159 ".//Sequence/[@cycle='1']/Frame/[@index='1']/PVStateShard/PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']/SubindexedValue"
133160 )
134161
@@ -137,13 +164,13 @@ def get_prairieview_metadata(ome_tif_filepath: str) -> dict:
137164 # must change depths.
138165 if len (z_controllers ) > 1 :
139166 z_repeats = []
140- for controller in xml_file .findall (
167+ for controller in xml_root .findall (
141168 ".//Sequence/[@cycle='1']/Frame/[@index='1']/PVStateShard/PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']/"
142169 ):
143170 z_repeats .append (
144171 [
145172 float (z .attrib .get ("value" ))
146- for z in xml_file .findall (
173+ for z in xml_root .findall (
147174 ".//Sequence/[@cycle='1']/Frame/PVStateShard/PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']/SubindexedValue/[@subindex='{0}']" .format (
148175 controller .attrib .get ("subindex" )
149176 )
@@ -163,7 +190,7 @@ def get_prairieview_metadata(ome_tif_filepath: str) -> dict:
163190 else :
164191 z_fields = [
165192 z .attrib .get ("value" )
166- for z in xml_file .findall (
193+ for z in xml_root .findall (
167194 ".//Sequence/[@cycle='1']/Frame/PVStateShard/PVStateValue/[@key='positionCurrent']/SubindexedValues/[@index='ZAxis']/SubindexedValue/[@subindex='0']"
168195 )
169196 ]
@@ -195,6 +222,47 @@ def get_prairieview_metadata(ome_tif_filepath: str) -> dict:
195222 fieldY = y_field ,
196223 fieldZ = z_fields ,
197224 recording_time = recording_start_time ,
225+ channels = list (channels ),
226+ plane_indices = list (plane_indices ),
198227 )
199228
200229 return metainfo
230+
231+
232+ def get_prairieview_metadata (ome_tif_filepath : str ) -> dict :
233+ """Extract metadata for scans generated by Prairie View acquisition software.
234+
235+ The Prairie View software generates one `.ome.tif` imaging file per frame
236+ acquired. The metadata for all frames is contained in one .xml file. This
237+ function locates the .xml file and generates a dictionary necessary to
238+ populate the DataJoint `ScanInfo` and `Field` tables. Prairie View works
239+ with resonance scanners with a single field. Prairie View does not support
240+ bidirectional x and y scanning. ROI information is not contained in the
241+ `.xml` file. All images generated using Prairie View have square dimensions(e.g. 512x512).
242+
243+ Args:
244+ ome_tif_filepath: An absolute path to the .ome.tif image file.
245+
246+ Raises:
247+ FileNotFoundError: No .xml file containing information about the acquired scan
248+ was found at path in parent directory at `ome_tif_filepath`.
249+
250+ Returns:
251+ metainfo: A dict mapping keys to corresponding metadata values fetched from the
252+ .xml file.
253+ """
254+
255+ # May return multiple xml files. Only need one that contains scan metadata.
256+ xml_files_list = pathlib .Path (ome_tif_filepath ).parent .glob ("*.xml" )
257+
258+ for file in xml_files_list :
259+ xml_tree = ET .parse (file )
260+ xml_file = xml_tree .getroot ()
261+ if xml_file .find (".//Sequence" ):
262+ break
263+ else :
264+ raise FileNotFoundError (
265+ f"No PrarieView metadata .xml file found at { pathlib .Path (ome_tif_filepath ).parent } "
266+ )
267+
268+ return _extract_prairieview_metadata (file )
0 commit comments