Browse Source

Merge pull request #46 from jimrybarski/37-python3-support

resolves #37: Python 3 support!
zolfa-add_slices_loading
Jim Rybarski 10 years ago
parent
commit
b8c75b2aba
4 changed files with 44 additions and 34 deletions
  1. +7
    -6
      nd2reader/__init__.py
  2. +29
    -24
      nd2reader/parser.py
  3. +2
    -1
      requirements.txt
  4. +6
    -3
      setup.py

+ 7
- 6
nd2reader/__init__.py View File

@ -2,6 +2,7 @@
from nd2reader.model import Image, ImageSet from nd2reader.model import Image, ImageSet
from nd2reader.parser import Nd2Parser from nd2reader.parser import Nd2Parser
import six
class Nd2(Nd2Parser): class Nd2(Nd2Parser):
@ -18,7 +19,7 @@ class Nd2(Nd2Parser):
"Created: %s" % self._absolute_start.strftime("%Y-%m-%d %H:%M:%S"), "Created: %s" % self._absolute_start.strftime("%Y-%m-%d %H:%M:%S"),
"Image size: %sx%s (HxW)" % (self.height, self.width), "Image size: %sx%s (HxW)" % (self.height, self.width),
"Image cycles: %s" % self._time_index_count, "Image cycles: %s" % self._time_index_count,
"Channels: %s" % ", ".join(["'%s'" % channel for channel in self._channels]),
"Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self._channels]),
"Fields of View: %s" % self._field_of_view_count, "Fields of View: %s" % self._field_of_view_count,
"Z-Levels: %s" % self._z_level_count "Z-Levels: %s" % self._z_level_count
]) ])
@ -30,7 +31,7 @@ class Nd2(Nd2Parser):
:rtype: int :rtype: int
""" """
return self.metadata['ImageAttributes']['SLxImageAttributes']['uiHeight']
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiHeight')]
@property @property
def width(self): def width(self):
@ -39,7 +40,7 @@ class Nd2(Nd2Parser):
:rtype: int :rtype: int
""" """
return self.metadata['ImageAttributes']['SLxImageAttributes']['uiWidth']
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiWidth')]
def __iter__(self): def __iter__(self):
""" """
@ -67,11 +68,11 @@ class Nd2(Nd2Parser):
:return: model.ImageSet() :return: model.ImageSet()
""" """
for time_index in xrange(self._time_index_count):
for time_index in range(self._time_index_count):
image_set = ImageSet() image_set = ImageSet()
for fov in range(self._field_of_view_count): for fov in range(self._field_of_view_count):
for channel_name in self._channels: for channel_name in self._channels:
for z_level in xrange(self._z_level_count):
for z_level in range(self._z_level_count):
image = self.get_image(time_index, fov, channel_name, z_level) image = self.get_image(time_index, fov, channel_name, z_level)
if image is not None: if image is not None:
image_set.add(image) image_set.add(image)
@ -101,4 +102,4 @@ class Nd2(Nd2Parser):
except TypeError: except TypeError:
return None return None
else: else:
return image
return image

+ 29
- 24
nd2reader/parser.py View File

@ -5,7 +5,7 @@ from datetime import datetime
import numpy as np import numpy as np
import re import re
import struct import struct
from StringIO import StringIO
import six
class Nd2Parser(object): class Nd2Parser(object):
@ -15,9 +15,9 @@ class Nd2Parser(object):
""" """
CHUNK_HEADER = 0xabeceda CHUNK_HEADER = 0xabeceda
CHUNK_MAP_START = "ND2 FILEMAP SIGNATURE NAME 0001!"
CHUNK_MAP_END = "ND2 CHUNK MAP SIGNATURE 0000001!"
CHUNK_MAP_START = six.b("ND2 FILEMAP SIGNATURE NAME 0001!")
CHUNK_MAP_END = six.b("ND2 CHUNK MAP SIGNATURE 0000001!")
def __init__(self, filename): def __init__(self, filename):
self._filename = filename self._filename = filename
self._fh = None self._fh = None
@ -47,7 +47,7 @@ class Nd2Parser(object):
:return: (int, array.array()) or None :return: (int, array.array()) or None
""" """
chunk = self._label_map["ImageDataSeq|%d!" % image_group_number]
chunk = self._label_map[six.b("ImageDataSeq|%d!" % image_group_number)]
data = self._read_chunk(chunk) data = self._read_chunk(chunk)
# All images in the same image group share the same timestamp! So if you have complicated image data, # All images in the same image group share the same timestamp! So if you have complicated image data,
# your timestamps may not be entirely accurate. Practically speaking though, they'll only be off by a few # your timestamps may not be entirely accurate. Practically speaking though, they'll only be off by a few
@ -78,18 +78,20 @@ class Nd2Parser(object):
""" """
if self._dimension_text is None: if self._dimension_text is None:
for line in self.metadata['ImageTextInfo']['SLxImageTextInfo'].values():
if "Dimensions:" in line:
for line in self.metadata[six.b('ImageTextInfo')][six.b('SLxImageTextInfo')].values():
if six.b("Dimensions:") in line:
metadata = line metadata = line
break break
else: else:
raise ValueError("Could not parse metadata dimensions!") raise ValueError("Could not parse metadata dimensions!")
for line in metadata.split("\r\n"):
if line.startswith("Dimensions:"):
for line in metadata.split(six.b("\r\n")):
if line.startswith(six.b("Dimensions:")):
self._dimension_text = line self._dimension_text = line
break break
else: else:
raise ValueError("Could not parse metadata dimensions!") raise ValueError("Could not parse metadata dimensions!")
if six.PY3:
return self._dimension_text.decode("utf8")
return self._dimension_text return self._dimension_text
@property @property
@ -101,19 +103,19 @@ class Nd2Parser(object):
:rtype: str :rtype: str
""" """
metadata = self.metadata['ImageMetadataSeq']['SLxPictureMetadata']['sPicturePlanes']
metadata = self.metadata[six.b('ImageMetadataSeq')][six.b('SLxPictureMetadata')][six.b('sPicturePlanes')]
try: try:
validity = self.metadata['ImageMetadata']['SLxExperiment']['ppNextLevelEx'][''][0]['ppNextLevelEx'][''][0]['pItemValid']
validity = self.metadata[six.b('ImageMetadata')][six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')]
except KeyError: except KeyError:
# If none of the channels have been deleted, there is no validity list, so we just make one # If none of the channels have been deleted, there is no validity list, so we just make one
validity = [True for _ in metadata] validity = [True for _ in metadata]
# Channel information is contained in dictionaries with the keys a0, a1...an where the number # Channel information is contained in dictionaries with the keys a0, a1...an where the number
# indicates the order in which the channel is stored. So by sorting the dicts alphabetically # indicates the order in which the channel is stored. So by sorting the dicts alphabetically
# we get the correct order. # we get the correct order.
for (label, chan), valid in zip(sorted(metadata['sPlaneNew'].items()), validity):
for (label, chan), valid in zip(sorted(metadata[six.b('sPlaneNew')].items()), validity):
if not valid: if not valid:
continue continue
yield chan['sDescription']
yield chan[six.b('sDescription')].decode("utf8")
def _calculate_image_group_number(self, time_index, fov, z_level): def _calculate_image_group_number(self, time_index, fov, z_level):
""" """
@ -150,17 +152,18 @@ class Nd2Parser(object):
:rtype: datetime.datetime() :rtype: datetime.datetime()
""" """
for line in self.metadata['ImageTextInfo']['SLxImageTextInfo'].values():
for line in self.metadata[six.b('ImageTextInfo')][six.b('SLxImageTextInfo')].values():
line = line.decode("utf8")
absolute_start_12 = None absolute_start_12 = None
absolute_start_24 = None absolute_start_24 = None
# ND2s seem to randomly switch between 12- and 24-hour representations. # ND2s seem to randomly switch between 12- and 24-hour representations.
try: try:
absolute_start_24 = datetime.strptime(line, "%m/%d/%Y %H:%M:%S") absolute_start_24 = datetime.strptime(line, "%m/%d/%Y %H:%M:%S")
except ValueError:
except (TypeError, ValueError):
pass pass
try: try:
absolute_start_12 = datetime.strptime(line, "%m/%d/%Y %I:%M:%S %p") absolute_start_12 = datetime.strptime(line, "%m/%d/%Y %I:%M:%S %p")
except ValueError:
except (TypeError, ValueError):
pass pass
if not absolute_start_12 and not absolute_start_24: if not absolute_start_12 and not absolute_start_24:
continue continue
@ -178,7 +181,7 @@ class Nd2Parser(object):
pattern = r""".*?λ\((\d+)\).*?""" pattern = r""".*?λ\((\d+)\).*?"""
try: try:
count = int(re.match(pattern, self._dimensions).group(1)) count = int(re.match(pattern, self._dimensions).group(1))
except AttributeError:
except AttributeError as e:
return 1 return 1
else: else:
return count return count
@ -242,7 +245,7 @@ class Nd2Parser(object):
:rtype: int :rtype: int
""" """
return self.metadata['ImageAttributes']['SLxImageAttributes']['uiSequenceCount']
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiSequenceCount')]
def _parse_metadata(self): def _parse_metadata(self):
""" """
@ -250,9 +253,9 @@ class Nd2Parser(object):
""" """
for label in self._label_map.keys(): for label in self._label_map.keys():
if label.endswith("LV!") or "LV|" in label:
if label.endswith(six.b("LV!")) or six.b("LV|") in label:
data = self._read_chunk(self._label_map[label]) data = self._read_chunk(self._label_map[label])
stop = label.index("LV")
stop = label.index(six.b("LV"))
self.metadata[label[:stop]] = self._read_metadata(data, 1) self.metadata[label[:stop]] = self._read_metadata(data, 1)
def _read_map(self): def _read_map(self):
@ -269,7 +272,7 @@ class Nd2Parser(object):
label_start = raw_text.index(Nd2Parser.CHUNK_MAP_START) + 32 label_start = raw_text.index(Nd2Parser.CHUNK_MAP_START) + 32
while True: while True:
data_start = raw_text.index("!", label_start) + 1
data_start = raw_text.index(six.b("!"), label_start) + 1
key = raw_text[label_start: data_start] key = raw_text[label_start: data_start]
location, length = struct.unpack("QQ", raw_text[data_start: data_start + 16]) location, length = struct.unpack("QQ", raw_text[data_start: data_start + 16])
if key == Nd2Parser.CHUNK_MAP_END: if key == Nd2Parser.CHUNK_MAP_END:
@ -308,7 +311,7 @@ class Nd2Parser(object):
def _parse_string(self, data): def _parse_string(self, data):
value = data.read(2) value = data.read(2)
while not value.endswith("\x00\x00"):
while not value.endswith(six.b("\x00\x00")):
# the string ends at the first instance of \x00\x00 # the string ends at the first instance of \x00\x00
value += data.read(2) value += data.read(2)
return value.decode("utf16")[:-1].encode("utf8") return value.decode("utf16")[:-1].encode("utf8")
@ -350,14 +353,16 @@ class Nd2Parser(object):
Iterates over each element some section of the metadata and parses it. Iterates over each element some section of the metadata and parses it.
""" """
data = StringIO(data)
data = six.BytesIO(data)
metadata = {} metadata = {}
for _ in xrange(count):
for _ in range(count):
self._cursor_position = data.tell() self._cursor_position = data.tell()
header = data.read(2) header = data.read(2)
if not header: if not header:
# We've reached the end of some hierarchy of data # We've reached the end of some hierarchy of data
break break
if six.PY3:
header = header.decode("utf8")
data_type, name_length = map(ord, header) data_type, name_length = map(ord, header)
name = data.read(name_length * 2).decode("utf16")[:-1].encode("utf8") name = data.read(name_length * 2).decode("utf16")[:-1].encode("utf8")
value = self._get_value(data, data_type) value = self._get_value(data, data_type)


+ 2
- 1
requirements.txt View File

@ -1 +1,2 @@
numpy>=1.9.2
numpy>=1.9.2
six

+ 6
- 3
setup.py View File

@ -1,14 +1,16 @@
from distutils.core import setup from distutils.core import setup
VERSION = "1.0.1"
setup( setup(
name="nd2reader", name="nd2reader",
packages=['nd2reader', 'nd2reader.model'], packages=['nd2reader', 'nd2reader.model'],
version="1.0.0",
version=VERSION,
description='A tool for reading ND2 files produced by NIS Elements', description='A tool for reading ND2 files produced by NIS Elements',
author='Jim Rybarski', author='Jim Rybarski',
author_email='jim@rybarski.com', author_email='jim@rybarski.com',
url='https://github.com/jimrybarski/nd2reader', url='https://github.com/jimrybarski/nd2reader',
download_url='https://github.com/jimrybarski/nd2reader/tarball/1.0.0',
download_url='https://github.com/jimrybarski/nd2reader/tarball/%s' % VERSION,
keywords=['nd2', 'nikon', 'microscopy', 'NIS Elements'], keywords=['nd2', 'nikon', 'microscopy', 'NIS Elements'],
classifiers=['Development Status :: 5 - Production/Stable', classifiers=['Development Status :: 5 - Production/Stable',
'Intended Audience :: Science/Research', 'Intended Audience :: Science/Research',
@ -16,6 +18,7 @@ setup(
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering',
] ]
)
)

Loading…
Cancel
Save