Browse Source

Merge pull request #126 from jimrybarski/125-more-metadata

125 more metadata
zolfa-add_slices_loading
Jim Rybarski 9 years ago
parent
commit
bda7af4bdc
12 changed files with 430 additions and 186 deletions
  1. +5
    -3
      Dockerfile
  2. +12
    -9
      Makefile
  3. +31
    -0
      README.md
  4. +0
    -0
      ftest.py
  5. +37
    -6
      functional_tests/FYLM141111001.py
  6. +108
    -0
      nd2reader/common/v3.py
  7. +0
    -1
      nd2reader/driver/v3.py
  8. +29
    -15
      nd2reader/interface.py
  9. +5
    -0
      nd2reader/model/label.py
  10. +23
    -0
      nd2reader/model/metadata.py
  11. +180
    -152
      nd2reader/parser/v3.py
  12. +0
    -0
      test.py

+ 5
- 3
Dockerfile View File

@ -19,20 +19,22 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python-numpy \ python-numpy \
python3-numpy \ python3-numpy \
libfreetype6-dev \ libfreetype6-dev \
python-matplotlib \
python3-matplotlib \ python3-matplotlib \
libfreetype6-dev \ libfreetype6-dev \
libpng-dev \ libpng-dev \
libjpeg-dev \ libjpeg-dev \
pkg-config \ pkg-config \
python-skimage \
python3-skimage \ python3-skimage \
tk \ tk \
tk-dev \ tk-dev \
python-tk \
python3-tk \ python3-tk \
&& pip install -U cython \
scikit-image \
&& pip3 install -U cython \ && pip3 install -U cython \
scikit-image \ scikit-image \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY . /opt/nd2reader
WORKDIR /opt/nd2reader WORKDIR /opt/nd2reader
RUN python setup.py install
RUN python3 setup.py install

+ 12
- 9
Makefile View File

@ -1,13 +1,16 @@
.PHONY: info build shell py2 py3 test
.PHONY: info build shell py2 py3 test ftest publish
info: info:
@echo "" @echo ""
@echo "Available Make Commands" @echo "Available Make Commands"
@echo "" @echo ""
@echo "build: builds the image"
@echo "build: builds the image"
@echo "shell: starts a bash shell in the container
@echo "py2: maps ~/Documents/nd2s to /var/nd2s and runs a Python 2.7 interpreter" @echo "py2: maps ~/Documents/nd2s to /var/nd2s and runs a Python 2.7 interpreter"
@echo "py3: maps ~/Documents/nd2s to /var/nd2s and runs a Python 3.4 interpreter" @echo "py3: maps ~/Documents/nd2s to /var/nd2s and runs a Python 3.4 interpreter"
@echo "test: runs all unit tests (in Python 3.4)"
@echo "test: runs all unit tests (in Python 3.4)"
@echo "ftest: runs all functional tests (requires specific ND2 files that are not publicly available"
@echo "publish: publishes the code base to PyPI (maintainers only)"
@echo "" @echo ""
build: build:
@ -17,18 +20,18 @@ shell:
xhost local:root; docker run --rm -v ~/nd2s:/var/nd2s -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -it jimrybarski/nd2reader bash xhost local:root; docker run --rm -v ~/nd2s:/var/nd2s -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -it jimrybarski/nd2reader bash
py2: py2:
xhost local:root; docker run --rm -v ~/nd2s:/var/nd2s -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -it jimrybarski/nd2reader python2.7
xhost local:root; docker run --rm -v $(CURDIR):/opt/nd2reader -v ~/nd2s:/var/nd2s -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -it jimrybarski/nd2reader python2.7
py3: py3:
xhost local:root; docker run --rm -v ~/nd2s:/var/nd2s -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -it jimrybarski/nd2reader python3.4
xhost local:root; docker run --rm -v $(CURDIR):/opt/nd2reader -v ~/nd2s:/var/nd2s -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -it jimrybarski/nd2reader python3.4
test: build test: build
docker run --rm -it jimrybarski/nd2reader python3.4 /opt/nd2reader/tests.py
docker run --rm -it jimrybarski/nd2reader python2.7 /opt/nd2reader/tests.py
docker run --rm -v $(CURDIR):/opt/nd2reader -it jimrybarski/nd2reader python3.4 test.py
docker run --rm -v $(CURDIR):/opt/nd2reader -it jimrybarski/nd2reader python2.7 test.py
ftest: build ftest: build
docker run --rm -v ~/nd2s:/var/nd2s -it jimrybarski/nd2reader python3.4 /opt/nd2reader/ftests.py
docker run --rm -v ~/nd2s:/var/nd2s -it jimrybarski/nd2reader python2.7 /opt/nd2reader/ftests.py
docker run --rm -v $(CURDIR):/opt/nd2reader -v ~/nd2s:/var/nd2s -it jimrybarski/nd2reader python3.4 /opt/nd2reader/ftest.py
docker run --rm -v $(CURDIR):/opt/nd2reader -v ~/nd2s:/var/nd2s -it jimrybarski/nd2reader python2.7 /opt/nd2reader/ftest.py
publish: publish:
python setup.py sdist upload -r pypi python setup.py sdist upload -r pypi

+ 31
- 0
README.md View File

@ -101,6 +101,37 @@ The `Nd2` object has some programmatically-accessible metadata:
30528 30528
``` ```
Each camera has its own settings. If you image multiple wavelengths with one camera, each channel will appear as its
own camera:
```python
>>> nd2.camera_settings
{'GFP': <Camera Settings: GFP>
Camera: Andor Zyla VSC-00461
Camera ID: VSC-00461
Exposure Time (ms): 100.0
Binning: 2x2, 'BF': <Camera Settings: BF>
Camera: Andor Zyla VSC-00461
Camera ID: VSC-00461
Exposure Time (ms): 100.0
Binning: 2x2}
```
Camera information can be accessed programmatically:
```python
>>> nd2.camera_settings['GFP'].id
'VSC-00461'
>>> nd2.camera_settings['GFP'].name
'Andor Zyla VSC-00461'
>>> nd2.camera_settings['GFP'].exposure
100.0
>>> nd2.camera_settings['GFP'].x_binning
2
>>> nd2.camera_settings['GFP'].y_binning
2
```
### Bug Reports and Features ### Bug Reports and Features
If this fails to work exactly as expected, please open a Github issue. If you get an unhandled exception, please If this fails to work exactly as expected, please open a Github issue. If you get an unhandled exception, please


ftests.py → ftest.py View File


+ 37
- 6
functional_tests/FYLM141111001.py View File

@ -22,8 +22,9 @@ class FunctionalTests(unittest.TestCase):
def test_date(self): def test_date(self):
self.assertEqual(self.nd2.date, datetime(2014, 11, 11, 15, 59, 19)) self.assertEqual(self.nd2.date, datetime(2014, 11, 11, 15, 59, 19))
def test_length(self):
self.assertEqual(len(self.nd2), 30528)
# def test_length(self):
# # This will fail until we address issue #59
# self.assertEqual(len(self.nd2), 17808)
def test_frames(self): def test_frames(self):
self.assertEqual(len(self.nd2.frames), 636) self.assertEqual(len(self.nd2.frames), 636)
@ -32,7 +33,7 @@ class FunctionalTests(unittest.TestCase):
self.assertEqual(len(self.nd2.fields_of_view), 8) self.assertEqual(len(self.nd2.fields_of_view), 8)
def test_channels(self): def test_channels(self):
self.assertTupleEqual(tuple(sorted(self.nd2.channels)), ('', 'GFP'))
self.assertTupleEqual(tuple(sorted(self.nd2.channels)), ('BF', 'GFP'))
def test_z_levels(self): def test_z_levels(self):
self.assertTupleEqual(tuple(self.nd2.z_levels), (0, 1, 2)) self.assertTupleEqual(tuple(self.nd2.z_levels), (0, 1, 2))
@ -42,7 +43,7 @@ class FunctionalTests(unittest.TestCase):
self.assertEqual(image.field_of_view, 2) self.assertEqual(image.field_of_view, 2)
self.assertEqual(image.frame_number, 0) self.assertEqual(image.frame_number, 0)
self.assertAlmostEqual(image.timestamp, 19.0340758) self.assertAlmostEqual(image.timestamp, 19.0340758)
self.assertEqual(image.channel, '')
self.assertEqual(image.channel, 'BF')
self.assertEqual(image.z_level, 1) self.assertEqual(image.z_level, 1)
self.assertEqual(image.height, self.nd2.height) self.assertEqual(image.height, self.nd2.height)
self.assertEqual(image.width, self.nd2.width) self.assertEqual(image.width, self.nd2.width)
@ -70,11 +71,41 @@ class FunctionalTests(unittest.TestCase):
def test_get_image_by_attribute_ok(self): def test_get_image_by_attribute_ok(self):
image = self.nd2.get_image(4, 0, "GFP", 1) image = self.nd2.get_image(4, 0, "GFP", 1)
self.assertIsNotNone(image) self.assertIsNotNone(image)
image = self.nd2.get_image(4, 0, "", 0)
image = self.nd2.get_image(4, 0, "BF", 0)
self.assertIsNotNone(image) self.assertIsNotNone(image)
image = self.nd2.get_image(4, 0, "", 1)
image = self.nd2.get_image(4, 0, "BF", 1)
self.assertIsNotNone(image) self.assertIsNotNone(image)
def test_images(self):
self.assertTupleEqual((self.nd2[0].z_level, self.nd2[0].channel), (0, 'BF'))
self.assertIsNone(self.nd2[1])
self.assertTupleEqual((self.nd2[2].z_level, self.nd2[2].channel), (1, 'BF'))
self.assertTupleEqual((self.nd2[3].z_level, self.nd2[3].channel), (1, 'GFP'))
self.assertTupleEqual((self.nd2[4].z_level, self.nd2[4].channel), (2, 'BF'))
self.assertIsNone(self.nd2[5])
self.assertTupleEqual((self.nd2[6].z_level, self.nd2[6].channel), (0, 'BF'))
self.assertIsNone(self.nd2[7])
self.assertTupleEqual((self.nd2[8].z_level, self.nd2[8].channel), (1, 'BF'))
self.assertTupleEqual((self.nd2[9].z_level, self.nd2[9].channel), (1, 'GFP'))
self.assertTupleEqual((self.nd2[10].z_level, self.nd2[10].channel), (2, 'BF'))
self.assertIsNone(self.nd2[11])
self.assertTupleEqual((self.nd2[12].z_level, self.nd2[12].channel), (0, 'BF'))
self.assertIsNone(self.nd2[13])
self.assertTupleEqual((self.nd2[14].z_level, self.nd2[14].channel), (1, 'BF'))
self.assertTupleEqual((self.nd2[15].z_level, self.nd2[15].channel), (1, 'GFP'))
self.assertTupleEqual((self.nd2[16].z_level, self.nd2[16].channel), (2, 'BF'))
self.assertIsNone(self.nd2[17])
self.assertTupleEqual((self.nd2[18].z_level, self.nd2[18].channel), (0, 'BF'))
self.assertIsNone(self.nd2[19])
self.assertIsNone(self.nd2[47])
self.assertTupleEqual((self.nd2[48].z_level, self.nd2[48].channel), (0, 'BF'))
self.assertIsNone(self.nd2[49])
self.assertTupleEqual((self.nd2[50].z_level, self.nd2[50].channel), (1, 'BF'))
self.assertIsNone(self.nd2[51])
self.assertTupleEqual((self.nd2[52].z_level, self.nd2[52].channel), (2, 'BF'))
self.assertIsNone(self.nd2[53])
self.assertTupleEqual((self.nd2[54].z_level, self.nd2[54].channel), (0, 'BF'))
def test_get_image_by_attribute_none(self): def test_get_image_by_attribute_none(self):
image = self.nd2.get_image(4, 0, "GFP", 0) image = self.nd2.get_image(4, 0, "GFP", 0)
self.assertIsNone(image) self.assertIsNone(image)

+ 108
- 0
nd2reader/common/v3.py View File

@ -1,4 +1,6 @@
import struct import struct
import array
import six
def read_chunk(fh, chunk_location): def read_chunk(fh, chunk_location):
@ -12,6 +14,8 @@ def read_chunk(fh, chunk_location):
:rtype: bytes :rtype: bytes
""" """
if chunk_location is None:
return None
fh.seek(chunk_location) fh.seek(chunk_location)
# The chunk metadata is always 16 bytes long # The chunk metadata is always 16 bytes long
chunk_metadata = fh.read(16) chunk_metadata = fh.read(16)
@ -22,3 +26,107 @@ def read_chunk(fh, chunk_location):
# start of the actual data field, which is at some arbitrary place after the metadata. # start of the actual data field, which is at some arbitrary place after the metadata.
fh.seek(chunk_location + 16 + relative_offset) fh.seek(chunk_location + 16 + relative_offset)
return fh.read(data_length) return fh.read(data_length)
def read_array(fh, kind, chunk_location):
kinds = {'double': 'd',
'int': 'i',
'float': 'f'}
if kind not in kinds:
raise ValueError('You attempted to read an array of an unknown type.')
raw_data = read_chunk(fh, chunk_location)
if raw_data is None:
return None
return array.array(kinds[kind], raw_data)
def _parse_unsigned_char(data):
return struct.unpack("B", data.read(1))[0]
def _parse_unsigned_int(data):
return struct.unpack("I", data.read(4))[0]
def _parse_unsigned_long(data):
return struct.unpack("Q", data.read(8))[0]
def _parse_double(data):
return struct.unpack("d", data.read(8))[0]
def _parse_string(data):
value = data.read(2)
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")
def _parse_char_array(data):
array_length = struct.unpack("Q", data.read(8))[0]
return array.array("B", data.read(array_length))
def _parse_metadata_item(data, cursor_position):
"""
Reads hierarchical data, analogous to a Python dict.
"""
new_count, length = struct.unpack("<IQ", data.read(12))
length -= data.tell() - cursor_position
next_data_length = data.read(length)
value = read_metadata(next_data_length, new_count)
# Skip some offsets
data.read(new_count * 8)
return value
def _get_value(data, data_type, cursor_position):
"""
ND2s use various codes to indicate different data types, which we translate here.
"""
parser = {1: _parse_unsigned_char,
2: _parse_unsigned_int,
3: _parse_unsigned_int,
5: _parse_unsigned_long,
6: _parse_double,
8: _parse_string,
9: _parse_char_array,
11: _parse_metadata_item}
return parser[data_type](data) if data_type < 11 else parser[data_type](data, cursor_position)
def read_metadata(data, count):
"""
Iterates over each element some section of the metadata and parses it.
"""
if data is None:
return None
data = six.BytesIO(data)
metadata = {}
for _ in range(count):
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 = _get_value(data, data_type, cursor_position)
if name not in metadata.keys():
metadata[name] = value
else:
if not isinstance(metadata[name], list):
# We have encountered this key exactly once before. Since we're seeing it again, we know we
# need to convert it to a list before proceeding.
metadata[name] = [metadata[name]]
# 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

+ 0
- 1
nd2reader/driver/v3.py View File

@ -3,7 +3,6 @@
import array import array
import numpy as np import numpy as np
import struct import struct
import six
from nd2reader.model.image import Image from nd2reader.model.image import Image
from nd2reader.common.v3 import read_chunk from nd2reader.common.v3 import read_chunk
from nd2reader.exc import NoImageError from nd2reader.exc import NoImageError


+ 29
- 15
nd2reader/interface.py View File

@ -11,10 +11,9 @@ class Nd2(object):
self._filename = filename self._filename = filename
self._fh = open(filename, "rb") self._fh = open(filename, "rb")
major_version, minor_version = get_version(self._fh) major_version, minor_version = get_version(self._fh)
parser = get_parser(self._fh, major_version, minor_version)
self._driver = parser.driver
self._metadata = parser.metadata
self._parser = get_parser(self._fh, major_version, minor_version)
self._metadata = self._parser.metadata
def __enter__(self): def __enter__(self):
return self return self
@ -27,7 +26,7 @@ class Nd2(object):
"Created: %s" % (self.date if self.date is not None else "Unknown"), "Created: %s" % (self.date if self.date is not None else "Unknown"),
"Image size: %sx%s (HxW)" % (self.height, self.width), "Image size: %sx%s (HxW)" % (self.height, self.width),
"Frames: %s" % len(self.frames), "Frames: %s" % len(self.frames),
"Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self.channels]),
"Channels: %s" % ", ".join(["%s" % str(channel) for channel in self.channels]),
"Fields of View: %s" % len(self.fields_of_view), "Fields of View: %s" % len(self.fields_of_view),
"Z-Levels: %s" % len(self.z_levels) "Z-Levels: %s" % len(self.z_levels)
]) ])
@ -52,7 +51,7 @@ class Nd2(object):
""" """
if isinstance(item, int): if isinstance(item, int):
try: try:
image = self._driver.get_image(item)
image = self._parser.driver.get_image(item)
except KeyError: except KeyError:
raise IndexError raise IndexError
else: else:
@ -78,6 +77,10 @@ class Nd2(object):
for i in range(start, stop)[::step]: for i in range(start, stop)[::step]:
yield self[i] yield self[i]
@property
def camera_settings(self):
return self._parser.camera_metadata
@property @property
def date(self): def date(self):
""" """
@ -91,9 +94,11 @@ class Nd2(object):
@property @property
def z_levels(self): def z_levels(self):
""" """
A list of integers that represent the different levels on the Z-axis that images were taken. Currently this is just a list of numbers from 0 to N.
For example, an ND2 where images were taken at -3µm, 0µm, and +5µm from a set position would be represented by 0, 1 and 2, respectively. ND2s do store the actual
offset of each image in micrometers and in the future this will hopefully be available. For now, however, you will have to match up the order yourself.
A list of integers that represent the different levels on the Z-axis that images were taken. Currently this is
just a list of numbers from 0 to N. For example, an ND2 where images were taken at -3µm, 0µm, and +5µm from a
set position would be represented by 0, 1 and 2, respectively. ND2s do store the actual offset of each image
in micrometers and in the future this will hopefully be available. For now, however, you will have to match up
the order yourself.
:return: list of int :return: list of int
@ -103,7 +108,8 @@ class Nd2(object):
@property @property
def fields_of_view(self): def fields_of_view(self):
""" """
A list of integers representing the various stage locations, in the order they were taken in the first round of acquisition.
A list of integers representing the various stage locations, in the order they were taken in the first round
of acquisition.
:return: list of int :return: list of int
@ -123,8 +129,9 @@ class Nd2(object):
@property @property
def frames(self): def frames(self):
""" """
A list of integers representing groups of images. ND2s consider images to be part of the same frame if they are in the same field of view and don't have the same channel.
So if you take a bright field and GFP image at four different fields of view over and over again, you'll have 8 images and 4 frames per cycle.
A list of integers representing groups of images. ND2s consider images to be part of the same frame if they
are in the same field of view and don't have the same channel. So if you take a bright field and GFP image at
four different fields of view over and over again, you'll have 8 images and 4 frames per cycle.
:return: list of int :return: list of int
@ -153,7 +160,8 @@ class Nd2(object):
def get_image(self, frame_number, field_of_view, channel_name, z_level): def get_image(self, frame_number, field_of_view, channel_name, z_level):
""" """
Attempts to return the image with the unique combination of given attributes. None will be returned if a match is not found.
Attempts to return the image with the unique combination of given attributes. None will be returned if a match
is not found.
:type frame_number: int :type frame_number: int
:param field_of_view: the label for the place in the XY-plane where this image was taken. :param field_of_view: the label for the place in the XY-plane where this image was taken.
@ -166,11 +174,17 @@ class Nd2(object):
:rtype: nd2reader.model.Image() or None :rtype: nd2reader.model.Image() or None
""" """
return self._driver.get_image_by_attributes(frame_number, field_of_view, channel_name, z_level, self.height, self.width)
return self._parser.driver.get_image_by_attributes(frame_number,
field_of_view,
channel_name,
z_level,
self.height,
self.width)
def close(self): def close(self):
""" """
Closes the file handle to the image. This actually sometimes will prevent problems so it's good to do this or use Nd2 as a context manager.
Closes the file handle to the image. This actually sometimes will prevent problems so it's good to do this or
use Nd2 as a context manager.
""" """
self._fh.close() self._fh.close()

+ 5
- 0
nd2reader/model/label.py View File

@ -5,11 +5,16 @@ import re
class LabelMap(object): class LabelMap(object):
""" """
Contains pointers to metadata. This might only be valid for V3 files.
""" """
def __init__(self, raw_binary_data): def __init__(self, raw_binary_data):
self._data = raw_binary_data self._data = raw_binary_data
self._image_data = {} self._image_data = {}
def image_attributes(self):
return self._get_location(six.b("ImageAttributesLV!"))
def _get_location(self, label): def _get_location(self, label):
try: try:
label_location = self._data.index(label) + len(label) label_location = self._data.index(label) + len(label)


+ 23
- 0
nd2reader/model/metadata.py View File

@ -1,3 +1,6 @@
import six
class Metadata(object): class Metadata(object):
""" A simple container for ND2 metadata. """ """ A simple container for ND2 metadata. """
def __init__(self, height, width, channels, date, fields_of_view, frames, z_levels, total_images_per_channel): def __init__(self, height, width, channels, date, fields_of_view, frames, z_levels, total_images_per_channel):
@ -93,3 +96,23 @@ class Metadata(object):
""" """
return self._total_images_per_channel return self._total_images_per_channel
class CameraSettings(object):
def __init__(self, name, id, exposure, x_binning, y_binning, channel_name):
self.name = name.decode("utf8")
self.id = id.decode("utf8")
self.exposure = exposure
self.x_binning = int(x_binning)
self.y_binning = int(y_binning)
self.channel_name = channel_name
if six.PY3:
self.channel_name = self.channel_name.decode("utf8") if channel_name is not None else None
def __repr__(self):
return "\n".join(["<Camera Settings: %s>" % self.channel_name,
"Camera: %s" % self.name,
"Camera ID: %s" % self.id,
"Exposure Time (ms): %s" % self.exposure,
"Binning: %sx%s" % (self.x_binning, self.y_binning)
])

+ 180
- 152
nd2reader/parser/v3.py View File

@ -1,17 +1,128 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import array
from datetime import datetime from datetime import datetime
from nd2reader.model.metadata import Metadata
from nd2reader.model.metadata import Metadata, CameraSettings
from nd2reader.model.label import LabelMap from nd2reader.model.label import LabelMap
from nd2reader.parser.base import BaseParser from nd2reader.parser.base import BaseParser
from nd2reader.driver.v3 import V3Driver from nd2reader.driver.v3 import V3Driver
from nd2reader.common.v3 import read_chunk
from nd2reader.common.v3 import read_chunk, read_array, read_metadata
import re import re
import six import six
import struct import struct
def ignore_missing(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except:
return None
return wrapper
class V3RawMetadata(object):
def __init__(self, fh, label_map):
self._fh = fh
self._label_map = label_map
@property
@ignore_missing
def image_text_info(self):
return read_metadata(read_chunk(self._fh, self._label_map.image_text_info), 1)
@property
@ignore_missing
def image_metadata_sequence(self):
return read_metadata(read_chunk(self._fh, self._label_map.image_metadata_sequence), 1)
@property
@ignore_missing
def image_calibration(self):
return read_metadata(read_chunk(self._fh, self._label_map.image_calibration), 1)
@property
@ignore_missing
def image_attributes(self):
return read_metadata(read_chunk(self._fh, self._label_map.image_attributes), 1)
@property
@ignore_missing
def x_data(self):
return read_array(self._fh, 'double', self._label_map.x_data)
@property
@ignore_missing
def y_data(self):
return read_array(self._fh, 'double', self._label_map.y_data)
@property
@ignore_missing
def z_data(self):
return read_array(self._fh, 'double', self._label_map.z_data)
@property
@ignore_missing
def roi_metadata(self):
return read_metadata(read_chunk(self._fh, self._label_map.roi_metadata), 1)
@property
@ignore_missing
def pfs_status(self):
return read_array(self._fh, 'int', self._label_map.pfs_status)
@property
@ignore_missing
def pfs_offset(self):
return read_array(self._fh, 'int', self._label_map.pfs_offset)
@property
@ignore_missing
def camera_exposure_time(self):
return read_array(self._fh, 'double', self._label_map.camera_exposure_time)
@property
@ignore_missing
def lut_data(self):
return read_chunk(self._fh, self._label_map.lut_data)
@property
@ignore_missing
def grabber_settings(self):
return read_chunk(self._fh, self._label_map.grabber_settings)
@property
@ignore_missing
def custom_data(self):
return read_chunk(self._fh, self._label_map.custom_data)
@property
@ignore_missing
def app_info(self):
return read_chunk(self._fh, self._label_map.app_info)
@property
@ignore_missing
def camera_temp(self):
camera_temp = read_array(self._fh, 'double', self._label_map.camera_temp)
if camera_temp:
for temp in map(lambda x: round(x * 100.0, 2), camera_temp):
yield temp
@property
@ignore_missing
def acquisition_times(self):
acquisition_times = read_array(self._fh, 'double', self._label_map.acquisition_times)
if acquisition_times:
for acquisition_time in map(lambda x: x / 1000.0, acquisition_times):
yield acquisition_time
@property
@ignore_missing
def image_metadata(self):
if self._label_map.image_metadata:
return read_metadata(read_chunk(self._fh, self._label_map.image_metadata), 1)
class V3Parser(BaseParser): class V3Parser(BaseParser):
""" Parses ND2 files and creates a Metadata and driver object. """ """ Parses ND2 files and creates a Metadata and driver object. """
CHUNK_HEADER = 0xabeceda CHUNK_HEADER = 0xabeceda
@ -25,7 +136,10 @@ class V3Parser(BaseParser):
""" """
self._fh = fh self._fh = fh
self._metadata = None self._metadata = None
self._raw_metadata = None
self._label_map = None self._label_map = None
self._camera_metadata = {}
self._parse_metadata()
@property @property
def metadata(self): def metadata(self):
@ -33,69 +147,64 @@ class V3Parser(BaseParser):
:rtype: Metadata :rtype: Metadata
""" """
if not self._metadata:
self._parse_metadata()
return self._metadata return self._metadata
@property
def camera_metadata(self):
return self._camera_metadata
@property @property
def driver(self): def driver(self):
return V3Driver(self.metadata, self._label_map, self._fh) return V3Driver(self.metadata, self._label_map, self._fh)
def _build_metadata_dict(self):
self._label_map = self._build_label_map()
raw_data = {"image_text_info": read_chunk(self._fh, self._label_map.image_text_info),
"image_metadata_sequence": read_chunk(self._fh, self._label_map.image_metadata_sequence),
# "image_data": read_chunk(self._fh, self._label_map.image_data),
"image_calibration": read_chunk(self._fh, self._label_map.image_calibration),
"image_attributes": read_chunk(self._fh, self._label_map.image_attributes),
# "x_data": read_chunk(self._fh, self._label_map.x_data),
# "y_data": read_chunk(self._fh, self._label_map.y_data),
# "z_data": read_chunk(self._fh, self._label_map.z_data),
# "roi_metadata": read_chunk(self._fh, self._label_map.roi_metadata),
# "pfs_status": read_chunk(self._fh, self._label_map.pfs_status),
# "pfs_offset": read_chunk(self._fh, self._label_map.pfs_offset),
# "guid": read_chunk(self._fh, self._label_map.guid),
# "description": read_chunk(self._fh, self._label_map.description),
# "camera_exposure_time": read_chunk(self._fh, self._label_map.camera_exposure_time),
# "camera_temp": read_chunk(self._fh, self._label_map.camera_temp),
# "acquisition_times": read_chunk(self._fh, self._label_map.acquisition_times),
# "acquisition_times_2": read_chunk(self._fh, self._label_map.acquisition_times_2),
# "acquisition_frames": read_chunk(self._fh, self._label_map.acquisition_frames),
# "lut_data": read_chunk(self._fh, self._label_map.lut_data),
# "grabber_settings": read_chunk(self._fh, self._label_map.grabber_settings),
# "custom_data": read_chunk(self._fh, self._label_map.custom_data),
# "app_info": read_chunk(self._fh, self._label_map.app_info)
}
if self._label_map.image_metadata:
raw_data["image_metadata"] = read_chunk(self._fh, self._label_map.image_metadata)
return {key: self._read_metadata(data, 1) for key, data in raw_data.items()}
@property
def raw_metadata(self):
if not self._raw_metadata:
self._label_map = self._build_label_map()
self._raw_metadata = V3RawMetadata(self._fh, self._label_map)
return self._raw_metadata
def _parse_metadata(self): def _parse_metadata(self):
""" """
Reads all metadata and instantiates the Metadata object. Reads all metadata and instantiates the Metadata object.
""" """
metadata_dict = self._build_metadata_dict()
height = metadata_dict['image_attributes'][six.b('SLxImageAttributes')][six.b('uiHeight')]
width = metadata_dict['image_attributes'][six.b('SLxImageAttributes')][six.b('uiWidth')]
channels = self._parse_channels(metadata_dict)
date = self._parse_date(metadata_dict)
fields_of_view = self._parse_fields_of_view(metadata_dict)
frames = self._parse_frames(metadata_dict)
z_levels = self._parse_z_levels(metadata_dict)
total_images_per_channel = self._parse_total_images_per_channel(metadata_dict)
self._metadata = Metadata(height, width, channels, date, fields_of_view, frames, z_levels, total_images_per_channel)
def _parse_date(self, metadata_dict):
height = self.raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiHeight')]
width = self.raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiWidth')]
date = self._parse_date(self.raw_metadata)
fields_of_view = self._parse_fields_of_view(self.raw_metadata)
frames = self._parse_frames(self.raw_metadata)
z_levels = self._parse_z_levels(self.raw_metadata)
total_images_per_channel = self._parse_total_images_per_channel(self.raw_metadata)
channels = []
for camera_setting in self._parse_camera_settings():
channels.append(camera_setting.channel_name)
self._camera_metadata[camera_setting.channel_name] = camera_setting
self._metadata = Metadata(height, width, sorted(list(channels)), date, fields_of_view, frames, z_levels, total_images_per_channel)
def _parse_camera_settings(self):
for camera in self._raw_metadata.image_metadata_sequence[six.b('SLxPictureMetadata')][six.b('sPicturePlanes')][six.b('sSampleSetting')].values():
name = camera[six.b('pCameraSetting')][six.b('CameraUserName')]
id = camera[six.b('pCameraSetting')][six.b('CameraUniqueName')]
exposure = camera[six.b('dExposureTime')]
x_binning = camera[six.b('pCameraSetting')][six.b('FormatFast')][six.b('fmtDesc')][six.b('dBinningX')]
y_binning = camera[six.b('pCameraSetting')][six.b('FormatFast')][six.b('fmtDesc')][six.b('dBinningY')]
optical_configs = camera[six.b('sOpticalConfigs')]
if six.b('') in optical_configs.keys():
channel_name = optical_configs[six.b('')][six.b('sOpticalConfigName')]
else:
channel_name = None
yield CameraSettings(name, id, exposure, x_binning, y_binning, channel_name)
def _parse_date(self, raw_metadata):
""" """
The date and time when acquisition began. The date and time when acquisition began.
:type metadata_dict: dict
:type raw_metadata: V3RawMetadata
:rtype: datetime.datetime() or None :rtype: datetime.datetime() or None
""" """
for line in metadata_dict['image_text_info'][six.b('SLxImageTextInfo')].values():
for line in raw_metadata.image_text_info[six.b('SLxImageTextInfo')].values():
line = line.decode("utf8") line = line.decode("utf8")
absolute_start_12 = None absolute_start_12 = None
absolute_start_24 = None absolute_start_24 = None
@ -113,20 +222,20 @@ class V3Parser(BaseParser):
return absolute_start_12 if absolute_start_12 else absolute_start_24 return absolute_start_12 if absolute_start_12 else absolute_start_24
return None return None
def _parse_channels(self, metadata_dict):
def _parse_channels(self, raw_metadata):
""" """
These are labels created by the NIS Elements user. Typically they may a short description of the filter cube These are labels created by the NIS Elements user. Typically they may a short description of the filter cube
used (e.g. "bright field", "GFP", etc.) used (e.g. "bright field", "GFP", etc.)
:type metadata_dict: dict
:type raw_metadata: V3RawMetadata
:rtype: list :rtype: list
""" """
channels = [] channels = []
metadata = metadata_dict['image_metadata_sequence'][six.b('SLxPictureMetadata')][six.b('sPicturePlanes')]
metadata = raw_metadata.image_metadata_sequence[six.b('SLxPictureMetadata')][six.b('sPicturePlanes')]
try: try:
validity = metadata_dict['image_metadata'][six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')]
except KeyError:
validity = raw_metadata.image_metadata[six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')]
except (KeyError, TypeError):
# 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
@ -138,50 +247,50 @@ class V3Parser(BaseParser):
channels.append(chan[six.b('sDescription')].decode("utf8")) channels.append(chan[six.b('sDescription')].decode("utf8"))
return channels return channels
def _parse_fields_of_view(self, metadata_dict):
def _parse_fields_of_view(self, raw_metadata):
""" """
The metadata contains information about fields of view, but it contains it even if some fields The metadata contains information about fields of view, but it contains it even if some fields
of view were cropped. We can't find anything that states which fields of view are actually of view were cropped. We can't find anything that states which fields of view are actually
in the image data, so we have to calculate it. There probably is something somewhere, since in the image data, so we have to calculate it. There probably is something somewhere, since
NIS Elements can figure it out, but we haven't found it yet. NIS Elements can figure it out, but we haven't found it yet.
:type metadata_dict: dict
:type raw_metadata: V3RawMetadata
:rtype: list :rtype: list
""" """
return self._parse_dimension(r""".*?XY\((\d+)\).*?""", metadata_dict)
return self._parse_dimension(r""".*?XY\((\d+)\).*?""", raw_metadata)
def _parse_frames(self, metadata_dict):
def _parse_frames(self, raw_metadata):
""" """
The number of cycles. The number of cycles.
:type metadata_dict: dict
:type raw_metadata: V3RawMetadata
:rtype: list :rtype: list
""" """
return self._parse_dimension(r""".*?T'?\((\d+)\).*?""", metadata_dict)
return self._parse_dimension(r""".*?T'?\((\d+)\).*?""", raw_metadata)
def _parse_z_levels(self, metadata_dict):
def _parse_z_levels(self, raw_metadata):
""" """
The different levels in the Z-plane. Just a sequence from 0 to n. The different levels in the Z-plane. Just a sequence from 0 to n.
:type metadata_dict: dict
:type raw_metadata: V3RawMetadata
:rtype: list :rtype: list
""" """
return self._parse_dimension(r""".*?Z\((\d+)\).*?""", metadata_dict)
return self._parse_dimension(r""".*?Z\((\d+)\).*?""", raw_metadata)
def _parse_dimension_text(self, metadata_dict):
def _parse_dimension_text(self, raw_metadata):
""" """
While there are metadata values that represent a lot of what we want to capture, they seem to be unreliable. While there are metadata values that represent a lot of what we want to capture, they seem to be unreliable.
Sometimes certain elements don't exist, or change their data type randomly. However, the human-readable text Sometimes certain elements don't exist, or change their data type randomly. However, the human-readable text
is always there and in the same exact format, so we just parse that instead. is always there and in the same exact format, so we just parse that instead.
:type metadata_dict: dict
:type raw_metadata: V3RawMetadata
:rtype: str :rtype: str
""" """
for line in metadata_dict['image_text_info'][six.b('SLxImageTextInfo')].values():
for line in raw_metadata.image_text_info[six.b('SLxImageTextInfo')].values():
if six.b("Dimensions:") in line: if six.b("Dimensions:") in line:
metadata = line metadata = line
break break
@ -195,16 +304,16 @@ class V3Parser(BaseParser):
return six.b("") return six.b("")
return dimension_text return dimension_text
def _parse_dimension(self, pattern, metadata_dict):
def _parse_dimension(self, pattern, raw_metadata):
""" """
:param pattern: a valid regex pattern :param pattern: a valid regex pattern
:type pattern: str :type pattern: str
:type metadata_dict: dict
:type raw_metadata: V3RawMetadata
:rtype: list of int :rtype: list of int
""" """
dimension_text = self._parse_dimension_text(metadata_dict)
dimension_text = self._parse_dimension_text(raw_metadata)
if six.PY3: if six.PY3:
dimension_text = dimension_text.decode("utf8") dimension_text = dimension_text.decode("utf8")
match = re.match(pattern, dimension_text) match = re.match(pattern, dimension_text)
@ -213,15 +322,15 @@ class V3Parser(BaseParser):
count = int(match.group(1)) count = int(match.group(1))
return list(range(count)) return list(range(count))
def _parse_total_images_per_channel(self, metadata_dict):
def _parse_total_images_per_channel(self, raw_metadata):
""" """
The total number of images per channel. Warning: this may be inaccurate as it includes "gap" images. The total number of images per channel. Warning: this may be inaccurate as it includes "gap" images.
:type metadata_dict: dict
:type raw_metadata: V3RawMetadata
:rtype: int :rtype: int
""" """
return metadata_dict['image_attributes'][six.b('SLxImageAttributes')][six.b('uiSequenceCount')]
return raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiSequenceCount')]
def _build_label_map(self): def _build_label_map(self):
""" """
@ -237,84 +346,3 @@ class V3Parser(BaseParser):
self._fh.seek(chunk_map_start_location) self._fh.seek(chunk_map_start_location)
raw_text = self._fh.read(-1) raw_text = self._fh.read(-1)
return LabelMap(raw_text) return LabelMap(raw_text)
def _parse_unsigned_char(self, data):
return struct.unpack("B", data.read(1))[0]
def _parse_unsigned_int(self, data):
return struct.unpack("I", data.read(4))[0]
def _parse_unsigned_long(self, data):
return struct.unpack("Q", data.read(8))[0]
def _parse_double(self, data):
return struct.unpack("d", data.read(8))[0]
def _parse_string(self, data):
value = data.read(2)
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")
def _parse_char_array(self, data):
array_length = struct.unpack("Q", data.read(8))[0]
return array.array("B", data.read(array_length))
def _parse_metadata_item(self, data):
"""
Reads hierarchical data, analogous to a Python dict.
"""
new_count, length = struct.unpack("<IQ", data.read(12))
length -= data.tell() - self._cursor_position
next_data_length = data.read(length)
value = self._read_metadata(next_data_length, new_count)
# Skip some offsets
data.read(new_count * 8)
return value
def _get_value(self, data, data_type):
"""
ND2s use various codes to indicate different data types, which we translate here.
"""
parser = {1: self._parse_unsigned_char,
2: self._parse_unsigned_int,
3: self._parse_unsigned_int,
5: self._parse_unsigned_long,
6: self._parse_double,
8: self._parse_string,
9: self._parse_char_array,
11: self._parse_metadata_item}
return parser[data_type](data)
def _read_metadata(self, data, count):
"""
Iterates over each element some section of the metadata and parses it.
"""
data = six.BytesIO(data)
metadata = {}
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)
if name not in metadata.keys():
metadata[name] = value
else:
if not isinstance(metadata[name], list):
# We have encountered this key exactly once before. Since we're seeing it again, we know we
# need to convert it to a list before proceeding.
metadata[name] = [metadata[name]]
# 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

tests.py → test.py View File


Loading…
Cancel
Save