Hi everyone,
First of all, thanks to @rbnvrw for all your work π I ran into some issues in the 3.2.0 build and would love to be able to help.
Unfortunately, the current build of nd2reader (3.2.0) breaks functionality that was previously provided. This is due to the current implementation of reader.get_frame_vczyx() which replaces reader.get_frame_2D() by providing specific methods via _register_get_frame().
Problem (1)
For e.g. this ND2-file a multi-FOV, multichannel, z-stack with shape (v=11, z=7, c=5, y=1806, x=1278) setting bundle_axes = 'zcyx' worked fine in 3.1.0. Now:
with ND2Reader(path + '2019-8-14_24W-S3a-Sec1-A1_012.nd2') as images:
images.bundle_axes = 'zcyx'
images.iter_axes = 'v'
print(f"Fields-of-view in ND2 file: {len(images)}")
im = images[0]
images.close()
print(f"Shape of single field-of-view: {im.shape}")
fails in base_frames._transpose() due to axes mismatch:
C:\Anaconda3\envs\ND2tiff_dev\lib\site-packages\pims\base_frames.py in get_frame_T(**ind)
300 transposition = [expected_axes.index(a) for a in desired_axes]
301 def get_frame_T(**ind):
--> 302 return get_frame(**ind).transpose(transposition)
303 return get_frame_T
304
ValueError: axes don't match array
Running with images.bundle_axes = 'czyx' instead avoids this error, but yields an object of incorrect shape:
with ND2Reader(path + '2019-8-14_24W-S3a-Sec1-A1_012.nd2') as images:
images.bundle_axes = 'czyx'
images.iter_axes = 'v'
print(f"Fields-of-view in ND2 file: {len(images)}")
im = images[0]
images.close()
print(f"Shape of single field-of-view: {im.shape}")
Output:
Fields-of-view in ND2 file: 11
Shape of single field-of-view: (35, 1806, 1278)
This should be (5,7,1806,1278).
Bad fix: Manually calling np.reshape(im, (5,7,1806,1278)) corrects this and a possible fix could work by changing get_frames_vczyx():
https://github.com/rbnvrw/nd2reader/blob/b17c9d1c8996cf87bee1f935d9512a371387c640/nd2reader/reader.py#L70
to:
res = np.squeeze(np.array(result, dtype=self._dtype))
return np.reshape(res, ([self.sizes[i] for i in self.bundle_axes]))
With images.bundle_axes = 'czyx' this returns the correct shape, with all frames in the right place. However, with images.bundle_axes = 'zcyx', np.reshape works incorrectly, since the order of frames in res will always follow the order of the loop in get_frame_vczyx(), aka v,c,z. I am not sure how write a fix without changing core loop (more about that at the end of this post).
Problem (2)
When avoiding the axes mismatch error in baseframes._transpose() by either using images.bundle_axes = 'czyx' or with the "bad_fix", metadata is not being propagated to the individual images, as would be the case if in baseframes._make_get_frame() the "first option" fails (no appropriate register_get_frame method was defined for get_frame_dict) and _bundle() is called. As such, calling:
with ND2Reader(path + '2019-8-14_24W-S3a-Sec1-A1_012.nd2') as images:
images.bundle_axes = 'czyx'
images.iter_axes = 'v'
im = images[0]
images.close()
print(im.shape)
print(im.metadata)
yields:
(35, 1806, 1278)
{'axes': ['c', 'z', 'y', 'x'], 'coords': {'v': 0, 't': 0}}
When assembled through base_frames._bundle(), metadata is propagated and print(im.metadata) yields:
{'height': 1806, 'width': 1278, 'date': datetime.datetime(2019, 8, 15, 10, 55, 24), 'fields_of_view': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'frames': [0], 'z_levels': [0, 1, 2, 3, 4, 5, 6], 'total_images_per_channel': 77, 'channels': ['647', '488', '561', '405', 'dic'], 'pixel_microns': 0.108333333333333, 'num_frames': 1, 'experiment': {'description': 'W3D', 'loops': [{'start': 0, 'duration': 0, 'stimulation': False, 'sampling_interval': 0.0}]}, 'events': [{'index': 1, 'time': 3265.7298961281776, 'type': 7, 'name': 'Command Executed'}, {'index': 2, 'time': 4311.860809832811, 'type': 7, 'name': 'Command Executed'}, {'index': 3, 'time': 25922.38347223401, 'type': 7, 'name': 'Command Executed'}, {'index': 4, 'time': 26966.55641874671, 'type': 7, 'name': 'Command Executed'}, {'index': 5, 'time': 48852.96897947788, 'type': 7, 'name': 'Command Executed'}, {'index': 6, 'time': 49896.39902755618, 'type': 7, 'name': 'Command Executed'}, {'index': 7, 'time': 71575.21477815509, 'type': 7, 'name': 'Command Executed'}, {'index': 8, 'time': 72618.643543154, 'type': 7, 'name': 'Command Executed'}, {'index': 9, 'time': 95025.13088130951, 'type': 7, 'name': 'Command Executed'}, {'index': 10, 'time': 96067.59092727304, 'type': 7, 'name': 'Command Executed'}, {'index': 11, 'time': 117582.08049416542, 'type': 7, 'name': 'Command Executed'}, {'index': 12, 'time': 118625.93363505602, 'type': 7, 'name': 'Command Executed'}, {'index': 13, 'time': 140330.72805649042, 'type': 7, 'name': 'Command Executed'}, {'index': 14, 'time': 141398.76003962755, 'type': 7, 'name': 'Command Executed'}, {'index': 15, 'time': 163148.8205845952, 'type': 7, 'name': 'Command Executed'}, {'index': 16, 'time': 164190.54478898644, 'type': 7, 'name': 'Command Executed'}, {'index': 17, 'time': 187320.49074921012, 'type': 7, 'name': 'Command Executed'}, {'index': 18, 'time': 188363.45247617364, 'type': 7, 'name': 'Command Executed'}, {'index': 19, 'time': 211586.02401459217, 'type': 7, 'name': 'Command Executed'}, {'index': 20, 'time': 212629.77386826277, 'type': 7, 'name': 'Command Executed'}, {'index': 21, 'time': 235066.1590194106, 'type': 7, 'name': 'Command Executed'}, {'index': 22, 'time': 236108.4846636057, 'type': 7, 'name': 'Command Executed'}], 'axes': ['c', 'z', 'y', 'x'], 'coords': {'v': 0, 't': 0}}
Problem (3):
Currently, get_frames_2D() is simply forwarding to get_frame_vczyx(), however the only time that get_frames_2D() would actually be called (as far as I can see) would be from within base_frames._bundle() or _drop(). Both functions will call get_frames_2D() iteratively (as they expect the original behavior of get_frames_2D(): they internally loop over the desired axes. Since this would now call the loop inside get_frame_vczyx(), it would iterate over all axes twice, and probably cause issues. I haven't tested this, since due to:
https://github.com/rbnvrw/nd2reader/blob/b17c9d1c8996cf87bee1f935d9512a371387c640/nd2reader/reader.py#L167-L176
base_frames._drop() and base_frames._bundle() will pretty much never be called.
Finally a general comment: basically, in order to fix problems 1 & 2, get_frame_vczyx() must re-implement most (all?) functionality of base_frames._bundle(). I am probably missing something, but right now, it is not clear to me what the benefit of the new get_frame_vczyx() function and associated commits since 1d43617 really is. @rbnvrw is there some added functionality you are hoping to achieve? Again, I'd be happy to help π