diff --git a/LICENSE b/LICENSE index 8b59f88..beceabe 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,12 @@ -The MIT License (MIT) - -Copyright (c) 2014 Jim Rybarski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/nd2reader/__init__.py b/nd2reader/__init__.py index 5253eb7..b415879 100644 --- a/nd2reader/__init__.py +++ b/nd2reader/__init__.py @@ -1,5 +1,6 @@ import logging from nd2reader.service import BaseNd2 +from nd2reader.model import Image log = logging.getLogger("nd2reader") log.addHandler(logging.StreamHandler()) @@ -8,4 +9,13 @@ log.setLevel(logging.DEBUG) class Nd2(BaseNd2): def __init__(self, filename): - super(Nd2, self).__init__(filename) \ No newline at end of file + super(Nd2, self).__init__(filename) + + def get_image(self, timepoint, fov, channel_name, z_level): + """ + Everything here is zero-indexed. + + """ + image_set_number = self._calculate_image_set_number(timepoint, fov, z_level) + timestamp, raw_image_data = self._reader.get_raw_image_data(image_set_number, self.channel_offset[channel_name]) + return Image(timestamp, raw_image_data, self.height, self.width) \ No newline at end of file diff --git a/nd2reader/model/__init__.py b/nd2reader/model/__init__.py index e14a54e..fff975d 100644 --- a/nd2reader/model/__init__.py +++ b/nd2reader/model/__init__.py @@ -10,9 +10,7 @@ class Channel(object): @property def name(self): - if self._name is not None and self._name != "": - return self._name - return "UnnamedChannel" + return self._name @property def camera(self): @@ -48,8 +46,7 @@ class Image(object): def __init__(self, timestamp, raw_array, height, width): self._timestamp = timestamp self._raw_data = raw_array - self._height = height - self._width = width + self._data = np.reshape(self._raw_data, (height, width)) @property def timestamp(self): @@ -58,7 +55,7 @@ class Image(object): @property def data(self): - return np.reshape(self._raw_data, (self._height, self._width)) + return self._data def show(self): skimage.io.imshow(self.data) diff --git a/nd2reader/service/__init__.py b/nd2reader/service/__init__.py index af115a8..c4e401f 100644 --- a/nd2reader/service/__init__.py +++ b/nd2reader/service/__init__.py @@ -7,49 +7,64 @@ import logging from nd2reader.model import Channel, ImageSet, Image log = logging.getLogger("nd2reader") +log.setLevel(logging.DEBUG) chunk = namedtuple('Chunk', ['location', 'length']) field_of_view = namedtuple('FOV', ['number', 'x', 'y', 'z', 'pfs_offset']) class BaseNd2(object): def __init__(self, filename): - self._parser = Nd2Reader(filename) + self._reader = Nd2Reader(filename) + self._channel_offset = None @property def height(self): - return self._parser.metadata['ImageAttributes']['SLxImageAttributes']['uiHeight'] + return self._metadata['ImageAttributes']['SLxImageAttributes']['uiHeight'] @property def width(self): - return self._parser.metadata['ImageAttributes']['SLxImageAttributes']['uiWidth'] + return self._metadata['ImageAttributes']['SLxImageAttributes']['uiWidth'] - def _get_timepoint_count(self): - return len(self._parser.metadata['ImageEvents']['RLxExperimentRecord']['pEvents']['']) + @property + def _image_count(self): + return self._metadata['ImageAttributes']['SLxImageAttributes']['uiSequenceCount'] @property - def _fields_of_view(self): - """ - Fields of view are the various places in the xy-plane where images were taken. + def _sequence_count(self): + return self._metadata['ImageEvents']['RLxExperimentRecord']['uiCount'] - """ - # Grab all the metadata about fields of view - fov_metadata = self._metadata['ImageMetadata']['SLxExperiment']['ppNextLevelEx'][''] - # The attributes include x, y, and z coordinates, and perfect focus (PFS) offset - fov_attributes = fov_metadata['uLoopPars']['Points'][''] - # If you crop fields of view from your ND2 file, the metadata is retained and only this list is - # updated to indicate that the fields of view have been deleted. - fov_validity = fov_metadata['pItemValid'] - # We only yield valid (i.e. uncropped) fields of view - for number, (fov, valid) in enumerate(zip(fov_attributes, fov_validity)): - if valid: - yield field_of_view(number=number + 1, - x=fov['dPosX'], - y=fov['dPosY'], - z=fov['dPosZ'], - pfs_offset=fov['dPFSOffset']) + @property + def _timepoint_count(self): + return self._image_count / self._field_of_view_count / self._z_level_count + + # @property + # def _fields_of_view(self): + # """ + # Fields of view are the various places in the xy-plane where images were taken. + # + # """ + # # Grab all the metadata about fields of view + # fov_metadata = self._metadata['ImageMetadata']['SLxExperiment']['ppNextLevelEx'][''] + # # The attributes include x, y, and z coordinates, and perfect focus (PFS) offset + # fov_attributes = fov_metadata['uLoopPars']['Points'][''] + # # If you crop fields of view from your ND2 file, the metadata is retained and only this list is + # # updated to indicate that the fields of view have been deleted. + # fov_validity = fov_metadata['pItemValid'] + # # We only yield valid (i.e. uncropped) fields of view + # for number, (fov, valid) in enumerate(zip(fov_attributes, fov_validity)): + # if valid: + # yield field_of_view(number=number + 1, + # x=fov['dPosX'], + # y=fov['dPosY'], + # z=fov['dPosZ'], + # pfs_offset=fov['dPFSOffset']) + + @property + def _z_level_count(self): + return self._image_count / self._sequence_count @property - def _fov_count(self): + def _field_of_view_count(self): """ 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 @@ -60,9 +75,13 @@ class BaseNd2(object): return sum(self._metadata['ImageMetadata']['SLxExperiment']['ppNextLevelEx']['']['pItemValid']) @property - def _channels(self): + def channels(self): metadata = self._metadata['ImageMetadataSeq']['SLxPictureMetadata']['sPicturePlanes'] - validity = self._metadata['ImageMetadata']['SLxExperiment']['ppNextLevelEx']['']['ppNextLevelEx']['']['pItemValid'] + try: + validity = self._metadata['ImageMetadata']['SLxExperiment']['ppNextLevelEx']['']['ppNextLevelEx']['']['pItemValid'] + except KeyError: + # If none of the channels have been deleted, there is no validity list, so we just make one + validity = [True for i 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. @@ -75,37 +94,19 @@ class BaseNd2(object): yield Channel(name, camera, exposure_time) @property - def _channel_count(self): - return self._metadata['ImageAttributes']["SLxImageAttributes"]["uiComp"] - - # @property - # def _z_levels(self): - # for i in self._metadata['ImageMetadata']['SLxExperiment']['ppNextLevelEx']['']['ppNextLevelEx'][''].items(): - # yield i - - # @property - # def _z_level_count(self): - # """ - # The number of different z-axis levels. - # - # """ - # return 1 + def channel_offset(self): + if self._channel_offset is None: + self._channel_offset = {} + for n, channel in enumerate(self.channels): + self._channel_offset[channel.name] = n + return self._channel_offset @property def _metadata(self): - return self._parser.metadata + return self._reader.metadata - def _get_image_set(self, nr): - chunk = self._parser._label_map["ImageDataSeq|%d!" % nr] - d = self._parser._read_chunk(chunk.location) - timestamp = struct.unpack("d", d[:8])[0] - image_set = ImageSet() - # The images for the various channels are interleaved within each other. - for i in range(self._channel_count): - image_data = array.array("H", d) - image = Image(timestamp, image_data[4+i::self._channel_count], self.height, self.width) - image_set.add(image) - return image_set + def _calculate_image_set_number(self, timepoint, fov, z_level): + return timepoint * self._field_of_view_count * self._z_level_count + (fov * self._z_level_count + z_level) class Nd2Reader(object): @@ -128,6 +129,20 @@ class Nd2Reader(object): self._file_handler = open(self._filename, "rb") return self._file_handler + @property + def channel_count(self): + return self._metadata['ImageAttributes']["SLxImageAttributes"]["uiComp"] + + def get_raw_image_data(self, image_set_number, channel_offset): + chunk = self._label_map["ImageDataSeq|%d!" % image_set_number] + data = self._read_chunk(chunk.location) + timestamp = struct.unpack("d", data[:8])[0] + # The images for the various channels are interleaved within each other. Yes, this is an incredibly unintuitive and nonsensical way + # to store data. + image_data = array.array("H", data) + image_data_start = 4 + channel_offset + return timestamp, image_data[image_data_start::self.channel_count] + def _parse_dict_data(self): # TODO: Don't like this name for label in self._top_level_dict_labels: @@ -260,6 +275,15 @@ class Nd2Reader(object): def as_numpy_array(arr): return np.frombuffer(arr) + def _z_level_count(self): + """read the microscope coordinates and temperatures + Missing: get chunknames and types from xml metadata""" + res = {} + name = "CustomData|Z!" + st = self._read_chunk(self._label_map[name].location) + res = array.array("d", st) + return len(res) + def read_lv_encoding(self, data, count): data = StringIO(data) res = {} @@ -267,14 +291,12 @@ class Nd2Reader(object): for c in range(count): lastpos = data.tell() total_count += 1 - # log.debug("%s: %s" % (total_count, lastpos)) hdr = data.read(2) if not hdr: break typ = ord(hdr[0]) bname = data.read(2*ord(hdr[1])) name = bname.decode("utf16")[:-1].encode("utf8") - log.debug(name) if typ == 1: value, = struct.unpack("B", data.read(1)) elif typ in [2, 3]: diff --git a/run.py b/run.py index 241e480..6c2ca53 100644 --- a/run.py +++ b/run.py @@ -3,8 +3,17 @@ from pprint import pprint import numpy as np from skimage import io -# n = Nd2("/home/jim/Desktop/nd2hacking/test-141111.nd2") + n = Nd2("/home/jim/Desktop/nd2hacking/YFP-dsRed-GFP-BF.nd2") -image_set = n._get_image_set(3) -for image in image_set: - image.show() \ No newline at end of file +# n = Nd2("/home/jim/Desktop/nd2hacking/test-141111.nd2") +# for chan in n.channels: +# print(chan.name) +print(n._reader.channel_count) +print(n._z_level_count) +print(n._field_of_view_count) +print(n._timepoint_count) + +# z = n._reader.read_coordinates() +# print(z) +# pprint(n._metadata) +image = n._reader.get_raw_image_data(71, 0)