Browse Source

merged changes from dev

feature/load_slices
Jim Rybarski 10 years ago
parent
commit
d1b4b0bc03
4 changed files with 55 additions and 34 deletions
  1. +17
    -6
      nd2reader/__init__.py
  2. +30
    -24
      nd2reader/parser.py
  3. +2
    -1
      requirements.txt
  4. +6
    -3
      setup.py

+ 17
- 6
nd2reader/__init__.py View File

@ -2,6 +2,7 @@
from nd2reader.model import Image, ImageSet
from nd2reader.parser import Nd2Parser
import six
class Nd2(Nd2Parser):
@ -18,11 +19,21 @@ class Nd2(Nd2Parser):
"Created: %s" % self._absolute_start.strftime("%Y-%m-%d %H:%M:%S"),
"Image size: %sx%s (HxW)" % (self.height, self.width),
"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,
"Z-Levels: %s" % self._z_level_count
])
def __len__(self):
"""
This should be the total number of images in the ND2, but it may be inaccurate. If the ND2 contains a
different number of images in a cycle (i.e. there are "gap" images) it will be higher than reality.
:rtype: int
"""
return self._image_count * self._channel_count
@property
def height(self):
"""
@ -30,7 +41,7 @@ class Nd2(Nd2Parser):
:rtype: int
"""
return self.metadata['ImageAttributes']['SLxImageAttributes']['uiHeight']
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiHeight')]
@property
def width(self):
@ -39,7 +50,7 @@ class Nd2(Nd2Parser):
:rtype: int
"""
return self.metadata['ImageAttributes']['SLxImageAttributes']['uiWidth']
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiWidth')]
def __iter__(self):
"""
@ -92,11 +103,11 @@ class Nd2(Nd2Parser):
:return: model.ImageSet()
"""
for time_index in xrange(self._time_index_count):
for time_index in range(self._time_index_count):
image_set = ImageSet()
for fov in range(self._field_of_view_count):
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)
if image is not None:
image_set.add(image)
@ -126,4 +137,4 @@ class Nd2(Nd2Parser):
except TypeError:
return None
else:
return image
return image

+ 30
- 24
nd2reader/parser.py View File

@ -5,7 +5,7 @@ from datetime import datetime
import numpy as np
import re
import struct
from StringIO import StringIO
import six
class Nd2Parser(object):
@ -15,9 +15,9 @@ class Nd2Parser(object):
"""
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):
self._filename = filename
self._fh = None
@ -52,7 +52,7 @@ class Nd2Parser(object):
: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)
# 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
@ -83,18 +83,20 @@ class Nd2Parser(object):
"""
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
break
else:
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
break
else:
raise ValueError("Could not parse metadata dimensions!")
if six.PY3:
return self._dimension_text.decode("utf8")
return self._dimension_text
@property
@ -107,19 +109,20 @@ class Nd2Parser(object):
"""
if not self._channels:
metadata = self.metadata['ImageMetadataSeq']['SLxPictureMetadata']['sPicturePlanes']
self._channels = []
metadata = self.metadata[six.b('ImageMetadataSeq')][six.b('SLxPictureMetadata')][six.b('sPicturePlanes')]
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:
# If none of the channels have been deleted, there is no validity list, so we just make one
validity = [True for _ in metadata]
# 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
# 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:
continue
self._channels.append(chan['sDescription'])
self._channels.append(chan[six.b('sDescription')].decode("utf8"))
return self._channels
def _calculate_image_group_number(self, time_index, fov, z_level):
@ -157,17 +160,18 @@ class Nd2Parser(object):
: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_24 = None
# ND2s seem to randomly switch between 12- and 24-hour representations.
try:
absolute_start_24 = datetime.strptime(line, "%m/%d/%Y %H:%M:%S")
except ValueError:
except (TypeError, ValueError):
pass
try:
absolute_start_12 = datetime.strptime(line, "%m/%d/%Y %I:%M:%S %p")
except ValueError:
except (TypeError, ValueError):
pass
if not absolute_start_12 and not absolute_start_24:
continue
@ -241,7 +245,7 @@ class Nd2Parser(object):
:rtype: int
"""
return self.metadata['ImageAttributes']['SLxImageAttributes']['uiSequenceCount']
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiSequenceCount')]
def _parse_metadata(self):
"""
@ -249,9 +253,9 @@ class Nd2Parser(object):
"""
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])
stop = label.index("LV")
stop = label.index(six.b("LV"))
self.metadata[label[:stop]] = self._read_metadata(data, 1)
def _read_map(self):
@ -268,7 +272,7 @@ class Nd2Parser(object):
label_start = raw_text.index(Nd2Parser.CHUNK_MAP_START) + 32
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]
location, length = struct.unpack("QQ", raw_text[data_start: data_start + 16])
if key == Nd2Parser.CHUNK_MAP_END:
@ -307,7 +311,7 @@ class Nd2Parser(object):
def _parse_string(self, data):
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
value += data.read(2)
return value.decode("utf16")[:-1].encode("utf8")
@ -349,14 +353,16 @@ class Nd2Parser(object):
Iterates over each element some section of the metadata and parses it.
"""
data = StringIO(data)
data = six.BytesIO(data)
metadata = {}
for _ in xrange(count):
for _ in range(count):
self._cursor_position = data.tell()
header = data.read(2)
if not header:
# We've reached the end of some hierarchy of data
break
if six.PY3:
header = header.decode("utf8")
data_type, name_length = map(ord, header)
name = data.read(name_length * 2).decode("utf16")[:-1].encode("utf8")
value = self._get_value(data, data_type)
@ -370,4 +376,4 @@ class Nd2Parser(object):
# We've encountered this key before so we're guaranteed to be dealing with a list. Thus we append
# the value to the already-existing list.
metadata[name].append(value)
return metadata
return metadata

+ 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
VERSION = "1.0.1"
setup(
name="nd2reader",
packages=['nd2reader', 'nd2reader.model'],
version="1.0.0",
version=VERSION,
description='A tool for reading ND2 files produced by NIS Elements',
author='Jim Rybarski',
author_email='jim@rybarski.com',
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'],
classifiers=['Development Status :: 5 - Production/Stable',
'Intended Audience :: Science/Research',
@ -16,6 +18,7 @@ setup(
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Topic :: Scientific/Engineering',
]
)
)

Loading…
Cancel
Save