Browse Source

Merge branch 'master' of github.com:rbnvrw/nd2reader

master
Ruben Verweij 6 years ago
parent
commit
5c02923ea5
10 changed files with 196 additions and 212 deletions
  1. +1
    -1
      docs
  2. +1
    -1
      nd2reader/__init__.py
  3. +58
    -85
      nd2reader/artificial.py
  4. +101
    -0
      nd2reader/common_raw_metadata.py
  5. +5
    -2
      nd2reader/parser.py
  6. +12
    -94
      nd2reader/raw_metadata.py
  7. +2
    -19
      nd2reader/reader.py
  8. +1
    -1
      setup.py
  9. +2
    -2
      sphinx/conf.py
  10. +13
    -7
      tests/test_raw_metadata.py

+ 1
- 1
docs

@ -1 +1 @@
Subproject commit 1b43456084689bad958984cf8a0cad7f63b52286
Subproject commit 6a12543461144fc164942c79a0b1457fd6284779

+ 1
- 1
nd2reader/__init__.py View File

@ -1,4 +1,4 @@
from nd2reader.reader import ND2Reader from nd2reader.reader import ND2Reader
from nd2reader.legacy import Nd2 from nd2reader.legacy import Nd2
__version__ = '3.0.9'
__version__ = '3.1.0'

+ 58
- 85
nd2reader/artificial.py View File

@ -6,6 +6,27 @@ import struct
from nd2reader.common import check_or_make_dir from nd2reader.common import check_or_make_dir
from os import path from os import path
global_labels = ['image_attributes', 'image_text_info', 'image_metadata',
'image_metadata_sequence', 'image_calibration', 'x_data',
'y_data', 'z_data', 'roi_metadata', 'pfs_status', 'pfs_offset',
'guid', 'description', 'camera_exposure_time', 'camera_temp',
'acquisition_times', 'acquisition_times_2',
'acquisition_frames', 'lut_data', 'grabber_settings',
'custom_data', 'app_info', 'image_frame_0']
global_file_labels = ["ImageAttributesLV!", "ImageTextInfoLV!",
"ImageMetadataLV!", "ImageMetadataSeqLV|0!",
"ImageCalibrationLV|0!", "CustomData|X!", "CustomData|Y!",
"CustomData|Z!", "CustomData|RoiMetadata_v1!",
"CustomData|PFS_STATUS!", "CustomData|PFS_OFFSET!",
"CustomData|GUIDStore!", "CustomData|CustomDescriptionV1_0!",
"CustomData|Camera_ExposureTime1!", "CustomData|CameraTemp1!",
"CustomData|AcqTimesCache!", "CustomData|AcqTimes2Cache!",
"CustomData|AcqFramesCache!", "CustomDataVar|LUTDataV1_0!",
"CustomDataVar|GrabberCameraSettingsV1_0!",
"CustomDataVar|CustomDataV2_0!", "CustomDataVar|AppInfo_V1_0!",
"ImageDataSeq|0!"]
class ArtificialND2(object): class ArtificialND2(object):
"""Artificial ND2 class (for testing purposes) """Artificial ND2 class (for testing purposes)
@ -105,56 +126,8 @@ class ArtificialND2(object):
""" """
raw_text = six.b('') raw_text = six.b('')
labels = [
'image_attributes',
'image_text_info',
'image_metadata',
'image_metadata_sequence',
'image_calibration',
'x_data',
'y_data',
'z_data',
'roi_metadata',
'pfs_status',
'pfs_offset',
'guid',
'description',
'camera_exposure_time',
'camera_temp',
'acquisition_times',
'acquisition_times_2',
'acquisition_frames',
'lut_data',
'grabber_settings',
'custom_data',
'app_info',
'image_frame_0'
]
file_labels = [
"ImageAttributesLV!",
"ImageTextInfoLV!",
"ImageMetadataLV!",
"ImageMetadataSeqLV|0!",
"ImageCalibrationLV|0!",
"CustomData|X!",
"CustomData|Y!",
"CustomData|Z!",
"CustomData|RoiMetadata_v1!",
"CustomData|PFS_STATUS!",
"CustomData|PFS_OFFSET!",
"CustomData|GUIDStore!",
"CustomData|CustomDescriptionV1_0!",
"CustomData|Camera_ExposureTime1!",
"CustomData|CameraTemp1!",
"CustomData|AcqTimesCache!",
"CustomData|AcqTimes2Cache!",
"CustomData|AcqFramesCache!",
"CustomDataVar|LUTDataV1_0!",
"CustomDataVar|GrabberCameraSettingsV1_0!",
"CustomDataVar|CustomDataV2_0!",
"CustomDataVar|AppInfo_V1_0!",
"ImageDataSeq|0!"
]
labels = global_labels
file_labels = global_file_labels
file_data, file_data_dict = self._get_file_data(labels) file_data, file_data_dict = self._get_file_data(labels)
@ -242,42 +215,42 @@ class ArtificialND2(object):
return raw_data return raw_data
@staticmethod
def _get_slx_img_attrib():
return {'uiWidth': 128,
'uiWidthBytes': 256,
'uiHeight': 128,
'uiComp': 1,
'uiBpcInMemory': 16,
'uiBpcSignificant': 12,
'uiSequenceCount': 70,
'uiTileWidth': 128,
'uiTileHeight': 128,
'eCompression': 2,
'dCompressionParam': -1.0,
'ePixelType': 1,
'uiVirtualComponents': 1
}
@staticmethod
def _get_slx_picture_metadata():
return {'sPicturePlanes':
{
'sPlaneNew': {
# channels are numbered a0, a1, ..., aN
'a0': {
'sDescription': 'TRITC'
}
}
}
}
def _get_file_data(self, labels): def _get_file_data(self, labels):
file_data = [ file_data = [
{
'SLxImageAttributes':
{
'uiWidth': 128,
'uiWidthBytes': 256,
'uiHeight': 128,
'uiComp': 1,
'uiBpcInMemory': 16,
'uiBpcSignificant': 12,
'uiSequenceCount': 70,
'uiTileWidth': 128,
'uiTileHeight': 128,
'eCompression': 2,
'dCompressionParam': -1.0,
'ePixelType': 1,
'uiVirtualComponents': 1
}
}, # ImageAttributesLV!",
{'SLxImageAttributes': self._get_slx_img_attrib()}, # ImageAttributesLV!",
7, # ImageTextInfoLV!", 7, # ImageTextInfoLV!",
7, # ImageMetadataLV!", 7, # ImageMetadataLV!",
{
'SLxPictureMetadata':
{
'sPicturePlanes':
{
'sPlaneNew': {
# channels are numbered a0, a1, ..., aN
'a0': {
'sDescription': 'TRITC'
}
}
}
}
}, # ImageMetadataSeqLV|0!",
{'SLxPictureMetadata': self._get_slx_picture_metadata()}, # ImageMetadataSeqLV|0!",
7, # ImageCalibrationLV|0!", 7, # ImageCalibrationLV|0!",
7, # CustomData|X!", 7, # CustomData|X!",
7, # CustomData|Y!", 7, # CustomData|Y!",
@ -289,9 +262,9 @@ class ArtificialND2(object):
7, # CustomData|CustomDescriptionV1_0!", 7, # CustomData|CustomDescriptionV1_0!",
7, # CustomData|Camera_ExposureTime1!", 7, # CustomData|Camera_ExposureTime1!",
7, # CustomData|CameraTemp1!", 7, # CustomData|CameraTemp1!",
7, # CustomData|AcqTimesCache!",
7, # CustomData|AcqTimes2Cache!",
7, # CustomData|AcqFramesCache!",
[0], # CustomData|AcqTimesCache!",
[0], # CustomData|AcqTimes2Cache!",
[0], # CustomData|AcqFramesCache!",
7, # CustomDataVar|LUTDataV1_0!", 7, # CustomDataVar|LUTDataV1_0!",
7, # CustomDataVar|GrabberCameraSettingsV1_0!", 7, # CustomDataVar|GrabberCameraSettingsV1_0!",
7, # CustomDataVar|CustomDataV2_0!", 7, # CustomDataVar|CustomDataV2_0!",


+ 101
- 0
nd2reader/common_raw_metadata.py View File

@ -0,0 +1,101 @@
import six
import warnings
from nd2reader.common import get_from_dict_if_exists
def parse_if_not_none(to_check, callback):
if to_check is not None:
return callback()
return None
def parse_dimension_text_line(line):
if six.b("Dimensions:") in line:
entries = line.split(six.b("\r\n"))
for entry in entries:
if entry.startswith(six.b("Dimensions:")):
return entry
return None
def parse_roi_shape(shape):
if shape == 3:
return 'rectangle'
elif shape == 9:
return 'circle'
return None
def parse_roi_type(type_no):
if type_no == 4:
return 'stimulation'
elif type_no == 3:
return 'reference'
elif type_no == 2:
return 'background'
return None
def get_loops_from_data(loop_data):
loops = [loop_data]
if six.b('uiPeriodCount') in loop_data and loop_data[six.b('uiPeriodCount')] > 0:
# special ND experiment
if six.b('pPeriod') not in loop_data:
return []
# take the first dictionary element, it contains all loop data
loops = loop_data[six.b('pPeriod')][list(loop_data[six.b('pPeriod')].keys())[0]]
return loops
def guess_sampling_from_loops(duration, loop):
""" In some cases, both keys are not saved. Then try to calculate it.
Args:
duration: the total duration of the loop
loop: the raw loop data
Returns:
float: the guessed sampling interval in milliseconds
"""
number_of_loops = get_from_dict_if_exists('uiCount', loop)
number_of_loops = number_of_loops if number_of_loops is not None and number_of_loops > 0 else 1
interval = duration / number_of_loops
return interval
def determine_sampling_interval(duration, loop):
"""Determines the loop sampling interval in milliseconds
Args:
duration: loop duration in milliseconds
loop: loop dictionary
Returns:
float: the sampling interval in milliseconds
"""
interval = get_from_dict_if_exists('dPeriod', loop)
avg_interval = get_from_dict_if_exists('dAvgPeriodDiff', loop)
if interval is None or interval <= 0:
interval = avg_interval
else:
avg_interval_set = avg_interval is not None and avg_interval > 0
if round(avg_interval) != round(interval) and avg_interval_set:
message = ("Reported average frame interval (%.1f ms) doesn't"
" match the set interval (%.1f ms). Using the average"
" now.")
warnings.warn(message % (avg_interval, interval), RuntimeWarning)
interval = avg_interval
if interval is None or interval <= 0:
# In some cases, both keys are not saved. Then try to calculate it.
interval = guess_sampling_from_loops(duration, loop)
return interval

+ 5
- 2
nd2reader/parser.py View File

@ -265,8 +265,11 @@ class Parser(object):
# The images for the various channels are interleaved within the same array. For example, the second image # The images for the various channels are interleaved within the same array. For example, the second image
# of a four image group will be composed of bytes 2, 6, 10, etc. If you understand why someone would design # of a four image group will be composed of bytes 2, 6, 10, etc. If you understand why someone would design
# a data structure that way, please send the author of this library a message. # a data structure that way, please send the author of this library a message.
number_of_true_channels = int((len(image_group_data) - 4) / (height * width))
image_data = np.reshape(image_group_data[image_data_start::number_of_true_channels], (height, width))
number_of_true_channels = int(len(image_group_data[4:]) / (height * width))
try:
image_data = np.reshape(image_group_data[image_data_start::number_of_true_channels], (height, width))
except ValueError:
image_data = np.reshape(image_group_data[image_data_start::number_of_true_channels], (height, int(round(len(image_group_data[image_data_start::number_of_true_channels])/height))))
# Skip images that are all zeros! This is important, since NIS Elements creates blank "gap" images if you # Skip images that are all zeros! This is important, since NIS Elements creates blank "gap" images if you
# don't have the same number of images each cycle. We discovered this because we only took GFP images every # don't have the same number of images each cycle. We discovered this because we only took GFP images every


+ 12
- 94
nd2reader/raw_metadata.py View File

@ -1,9 +1,11 @@
import re import re
from nd2reader.common import read_chunk, read_array, read_metadata, parse_date, get_from_dict_if_exists
import xmltodict import xmltodict
import six import six
import numpy as np import numpy as np
from nd2reader.common import read_chunk, read_array, read_metadata, parse_date, get_from_dict_if_exists
from nd2reader.common_raw_metadata import parse_dimension_text_line, parse_if_not_none, parse_roi_shape, parse_roi_type, get_loops_from_data, determine_sampling_interval
class RawMetadata(object): class RawMetadata(object):
"""RawMetadata class parses and stores the raw metadata that is read from the binary file in dict format. """RawMetadata class parses and stores the raw metadata that is read from the binary file in dict format.
@ -35,15 +37,15 @@ class RawMetadata(object):
frames_per_channel = self._parse_total_images_per_channel() frames_per_channel = self._parse_total_images_per_channel()
self._metadata_parsed = { self._metadata_parsed = {
"height": self._parse_if_not_none(self.image_attributes, self._parse_height),
"width": self._parse_if_not_none(self.image_attributes, self._parse_width),
"date": self._parse_if_not_none(self.image_text_info, self._parse_date),
"height": parse_if_not_none(self.image_attributes, self._parse_height),
"width": parse_if_not_none(self.image_attributes, self._parse_width),
"date": parse_if_not_none(self.image_text_info, self._parse_date),
"fields_of_view": self._parse_fields_of_view(), "fields_of_view": self._parse_fields_of_view(),
"frames": self._parse_frames(), "frames": self._parse_frames(),
"z_levels": self._parse_z_levels(), "z_levels": self._parse_z_levels(),
"total_images_per_channel": frames_per_channel, "total_images_per_channel": frames_per_channel,
"channels": self._parse_channels(), "channels": self._parse_channels(),
"pixel_microns": self._parse_if_not_none(self.image_calibration, self._parse_calibration),
"pixel_microns": parse_if_not_none(self.image_calibration, self._parse_calibration),
} }
self._set_default_if_not_empty('fields_of_view') self._set_default_if_not_empty('fields_of_view')
@ -63,12 +65,6 @@ class RawMetadata(object):
# if the file is not empty, we always have one of this entry # if the file is not empty, we always have one of this entry
self._metadata_parsed[entry] = [0] self._metadata_parsed[entry] = [0]
@staticmethod
def _parse_if_not_none(to_check, callback):
if to_check is not None:
return callback()
return None
def _parse_width_or_height(self, key): def _parse_width_or_height(self, key):
try: try:
length = self.image_attributes[six.b('SLxImageAttributes')][six.b(key)] length = self.image_attributes[six.b('SLxImageAttributes')][six.b(key)]
@ -180,21 +176,12 @@ class RawMetadata(object):
return dimension_text return dimension_text
for line in textinfo: for line in textinfo:
entry = self._parse_dimension_text_line(line)
entry = parse_dimension_text_line(line)
if entry is not None: if entry is not None:
return entry return entry
return dimension_text return dimension_text
@staticmethod
def _parse_dimension_text_line(line):
if six.b("Dimensions:") in line:
entries = line.split(six.b("\r\n"))
for entry in entries:
if entry.startswith(six.b("Dimensions:")):
return entry
return None
def _parse_dimension(self, pattern): def _parse_dimension(self, pattern):
dimension_text = self._parse_dimension_text() dimension_text = self._parse_dimension_text()
if dimension_text is None: if dimension_text is None:
@ -261,8 +248,8 @@ class RawMetadata(object):
"timepoints": [], "timepoints": [],
"positions": [], "positions": [],
"sizes": [], "sizes": [],
"shape": self._parse_roi_shape(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiShapeType')]),
"type": self._parse_roi_type(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiInterpType')])
"shape": parse_roi_shape(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiShapeType')]),
"type": parse_roi_type(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiInterpType')])
} }
for i in range(number_of_timepoints): for i in range(number_of_timepoints):
roi_dict = self._parse_vect_anim(roi_dict, raw_roi_dict[six.b('m_vectAnimParams_%d' % i)]) roi_dict = self._parse_vect_anim(roi_dict, raw_roi_dict[six.b('m_vectAnimParams_%d' % i)])
@ -274,26 +261,6 @@ class RawMetadata(object):
return roi_dict return roi_dict
@staticmethod
def _parse_roi_shape(shape):
if shape == 3:
return 'rectangle'
elif shape == 9:
return 'circle'
return None
@staticmethod
def _parse_roi_type(type_no):
if type_no == 4:
return 'stimulation'
elif type_no == 3:
return 'reference'
elif type_no == 2:
return 'background'
return None
def _parse_vect_anim(self, roi_dict, animation_dict): def _parse_vect_anim(self, roi_dict, animation_dict):
""" """
Parses a ROI vector animation object and adds it to the global list of timepoints and positions. Parses a ROI vector animation object and adds it to the global list of timepoints and positions.
@ -345,18 +312,6 @@ class RawMetadata(object):
if six.b('uLoopPars') in raw_data: if six.b('uLoopPars') in raw_data:
self._metadata_parsed['experiment']['loops'] = self._parse_loop_data(raw_data[six.b('uLoopPars')]) self._metadata_parsed['experiment']['loops'] = self._parse_loop_data(raw_data[six.b('uLoopPars')])
@staticmethod
def _get_loops_from_data(loop_data):
loops = [loop_data]
if six.b('uiPeriodCount') in loop_data and loop_data[six.b('uiPeriodCount')] > 0:
# special ND experiment
if six.b('pPeriod') not in loop_data:
return []
# take the first dictionary element, it contains all loop data
loops = loop_data[six.b('pPeriod')][list(loop_data[six.b('pPeriod')].keys())[0]]
return loops
def _parse_loop_data(self, loop_data): def _parse_loop_data(self, loop_data):
"""Parse the experimental loop data """Parse the experimental loop data
@ -367,7 +322,7 @@ class RawMetadata(object):
list: list of the parsed loops list: list of the parsed loops
""" """
loops = self._get_loops_from_data(loop_data)
loops = get_loops_from_data(loop_data)
# take into account the absolute time in ms # take into account the absolute time in ms
time_offset = 0 time_offset = 0
@ -377,7 +332,7 @@ class RawMetadata(object):
for loop in loops: for loop in loops:
# duration of this loop # duration of this loop
duration = get_from_dict_if_exists('dDuration', loop) or 0 duration = get_from_dict_if_exists('dDuration', loop) or 0
interval = self._determine_sampling_interval(duration, loop)
interval = determine_sampling_interval(duration, loop)
# if duration is not saved, infer it # if duration is not saved, infer it
duration = self.get_duration_from_interval_and_loops(duration, interval, loop) duration = self.get_duration_from_interval_and_loops(duration, interval, loop)
@ -418,43 +373,6 @@ class RawMetadata(object):
return duration return duration
@staticmethod
def _determine_sampling_interval(duration, loop):
"""Determines the loop sampling interval in milliseconds
Args:
duration: loop duration in milliseconds
loop: loop dictionary
Returns:
float: the sampling interval in milliseconds
"""
interval = get_from_dict_if_exists('dPeriod', loop)
if interval is None or interval <= 0:
# Use a fallback if it is still not found
interval = get_from_dict_if_exists('dAvgPeriodDiff', loop)
if interval is None or interval <= 0:
# In some cases, both keys are not saved. Then try to calculate it.
interval = RawMetadata._guess_sampling_from_loops(duration, loop)
return interval
@staticmethod
def _guess_sampling_from_loops(duration, loop):
""" In some cases, both keys are not saved. Then try to calculate it.
Args:
duration: the total duration of the loop
loop: the raw loop data
Returns:
float: the guessed sampling interval in milliseconds
"""
number_of_loops = get_from_dict_if_exists('uiCount', loop)
number_of_loops = number_of_loops if number_of_loops is not None and number_of_loops > 0 else 1
interval = duration / number_of_loops
return interval
@property @property
def image_text_info(self): def image_text_info(self):


+ 2
- 19
nd2reader/reader.py View File

@ -185,26 +185,9 @@ class ND2Reader(FramesSequenceND):
np.ndarray: an array of times in milliseconds. np.ndarray: an array of times in milliseconds.
""" """
if self._timesteps is not None and len(timesteps) > 0:
if self._timesteps is not None and len(self._timesteps) > 0:
return self._timesteps return self._timesteps
timesteps = np.array([])
current_time = 0.0
for loop in self.metadata['experiment']['loops']:
if loop['stimulation']:
continue
if loop['sampling_interval'] == 0:
# This is a loop were no data is acquired
current_time += loop['duration']
continue
timesteps = np.concatenate(
(timesteps, np.arange(current_time, current_time + loop['duration'], loop['sampling_interval'])))
current_time += loop['duration']
# if experiment did not finish, number of timesteps is wrong. Take correct amount of leading timesteps.
self._timesteps = timesteps[:self.metadata['num_frames']]
self._timesteps = np.array(list(self._parser._raw_metadata.acquisition_times), dtype=np.float) * 1000.0
return self._timesteps return self._timesteps

+ 1
- 1
setup.py View File

@ -1,6 +1,6 @@
from setuptools import setup from setuptools import setup
VERSION = '3.0.9'
VERSION = '3.1.0'
if __name__ == '__main__': if __name__ == '__main__':
setup( setup(


+ 2
- 2
sphinx/conf.py View File

@ -44,9 +44,9 @@ author = 'Ruben Verweij'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '3.0.9'
version = '3.1.0'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '3.0.9'
release = '3.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.


+ 13
- 7
tests/test_raw_metadata.py View File

@ -4,6 +4,7 @@ import six
from nd2reader.artificial import ArtificialND2 from nd2reader.artificial import ArtificialND2
from nd2reader.label_map import LabelMap from nd2reader.label_map import LabelMap
from nd2reader.raw_metadata import RawMetadata from nd2reader.raw_metadata import RawMetadata
from nd2reader.common_raw_metadata import parse_roi_shape, parse_roi_type, parse_dimension_text_line
class TestRawMetadata(unittest.TestCase): class TestRawMetadata(unittest.TestCase):
@ -14,15 +15,20 @@ class TestRawMetadata(unittest.TestCase):
self.metadata = RawMetadata(self.nd2.file_handle, self.label_map) self.metadata = RawMetadata(self.nd2.file_handle, self.label_map)
def test_parse_roi_shape(self): def test_parse_roi_shape(self):
self.assertEqual(self.metadata._parse_roi_shape(3), 'rectangle')
self.assertEqual(self.metadata._parse_roi_shape(9), 'circle')
self.assertIsNone(self.metadata._parse_roi_shape(-1))
self.assertEqual(parse_roi_shape(3), 'rectangle')
self.assertEqual(parse_roi_shape(9), 'circle')
self.assertIsNone(parse_roi_shape(-1))
def test_parse_roi_type(self): def test_parse_roi_type(self):
self.assertEqual(self.metadata._parse_roi_type(3), 'reference')
self.assertEqual(self.metadata._parse_roi_type(2), 'background')
self.assertEqual(self.metadata._parse_roi_type(4), 'stimulation')
self.assertIsNone(self.metadata._parse_roi_type(-1))
self.assertEqual(parse_roi_type(3), 'reference')
self.assertEqual(parse_roi_type(2), 'background')
self.assertEqual(parse_roi_type(4), 'stimulation')
self.assertIsNone(parse_roi_type(-1))
def test_parse_dimension_text(self):
line = six.b('Metadata:\r\nDimensions: T(443) x \xce\xbb(1)\r\nCamera Name: Andor Zyla VSC-01537')
self.assertEqual(parse_dimension_text_line(line), six.b('Dimensions: T(443) x \xce\xbb(1)'))
self.assertIsNone(parse_dimension_text_line(six.b('Dim: nothing')))
def test_dict(self): def test_dict(self):
self.assertTrue(type(self.metadata.__dict__) is dict) self.assertTrue(type(self.metadata.__dict__) is dict)


Loading…
Cancel
Save