diff --git a/CHANGELOG.md b/CHANGELOG.md index cfce502..bb1b2ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ ## [1.1.2] - 2015-10-09 +### ADDED +- `Image` objects now have a `frame_number` attribute. + ### CHANGED - `Image` objects now directly subclass Numpy arrays. - Refactored code to permit parsing of different versions of ND2s, which will allow us to add support for NIS Elements 3.x. diff --git a/nd2reader/driver/v3.py b/nd2reader/driver/v3.py index ea00527..e7a3484 100644 --- a/nd2reader/driver/v3.py +++ b/nd2reader/driver/v3.py @@ -5,11 +5,10 @@ import numpy as np import struct import six from nd2reader.model.image import Image +from nd2reader.parser.v3 import read_chunk class V3Driver(object): - CHUNK_HEADER = 0xabeceda - def __init__(self, metadata, label_map, file_handle): self._metadata = metadata self._label_map = label_map @@ -79,7 +78,7 @@ class V3Driver(object): """ chunk = self._label_map[six.b("ImageDataSeq|%d!" % image_group_number)] - data = self._read_chunk(chunk) + data = read_chunk(self._file_handle, 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 # seconds unless you're doing something super weird. @@ -97,21 +96,3 @@ class V3Driver(object): if np.any(image_data): return timestamp, Image(image_data) return None - - def _read_chunk(self, chunk_location): - """ - Gets the data for a given chunk pointer - - :rtype: bytes - - """ - self._file_handle.seek(chunk_location) - # The chunk metadata is always 16 bytes long - chunk_metadata = self._file_handle.read(16) - header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata) - if header != V3Driver.CHUNK_HEADER: - raise ValueError("The ND2 file seems to be corrupted.") - # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the - # start of the actual data field, which is at some arbitrary place after the metadata. - self._file_handle.seek(chunk_location + 16 + relative_offset) - return self._file_handle.read(data_length) diff --git a/nd2reader/interface.py b/nd2reader/interface.py index 29813f4..b62e4d7 100644 --- a/nd2reader/interface.py +++ b/nd2reader/interface.py @@ -12,8 +12,9 @@ class Nd2(object): """ def __init__(self, filename): - major_version, minor_version = get_version(filename) - parser = get_parser(filename, major_version, minor_version) + self._fh = open(filename, "rb") + 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._filename = filename @@ -160,3 +161,6 @@ class Nd2(object): """ return self._driver.get_image_by_attributes(frame_number, field_of_view, channel_name, z_level) + + def close(self): + self._fh.close() \ No newline at end of file diff --git a/nd2reader/parser/parser.py b/nd2reader/parser/parser.py index d3e7488..062c835 100644 --- a/nd2reader/parser/parser.py +++ b/nd2reader/parser/parser.py @@ -2,9 +2,9 @@ from nd2reader.parser.v3 import V3Parser from nd2reader.exc import InvalidVersionError -def get_parser(filename, major_version, minor_version): +def get_parser(fh, major_version, minor_version): parsers = {(3, None): V3Parser} parser = parsers.get((major_version, minor_version)) or parsers.get((major_version, None)) if not parser: raise InvalidVersionError("No parser is available for that version.") - return parser(filename) + return parser(fh) diff --git a/nd2reader/parser/v3.py b/nd2reader/parser/v3.py index a7dfd75..ab0bbd1 100644 --- a/nd2reader/parser/v3.py +++ b/nd2reader/parser/v3.py @@ -10,15 +10,33 @@ import six import struct +def read_chunk(fh, chunk_location): + """ + Gets the data for a given chunk pointer + + :rtype: bytes + + """ + fh.seek(chunk_location) + # The chunk metadata is always 16 bytes long + chunk_metadata = fh.read(16) + header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata) + if header != 0xabeceda: + raise ValueError("The ND2 file seems to be corrupted.") + # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the + # start of the actual data field, which is at some arbitrary place after the metadata. + fh.seek(chunk_location + 16 + relative_offset) + return fh.read(data_length) + + class V3Parser(BaseParser): """ Parses ND2 files and creates a Metadata and ImageReader object. """ CHUNK_HEADER = 0xabeceda 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 + def __init__(self, fh): + self._fh = fh self._metadata = None self._label_map = None @@ -30,7 +48,7 @@ class V3Parser(BaseParser): @property def driver(self): - return V3Driver(self.metadata, self._label_map, self._get_file_handle()) + return V3Driver(self.metadata, self._label_map, self._fh) def _parse_metadata(self): """ @@ -41,7 +59,7 @@ class V3Parser(BaseParser): self._label_map = self._build_label_map() for label in self._label_map.keys(): if label.endswith(six.b("LV!")) or six.b("LV|") in label: - data = self._read_chunk(self._label_map[label]) + data = read_chunk(self._fh, self._label_map[label]) stop = label.index(six.b("LV")) metadata_dict[label[:stop]] = self._read_metadata(data, 1) @@ -134,11 +152,6 @@ class V3Parser(BaseParser): """ return self._parse_dimension(r""".*?Z\((\d+)\).*?""", metadata_dict) - def _get_file_handle(self): - if self._fh is None: - self._fh = open(self._filename, "rb") - return self._fh - def _parse_dimension_text(self, metadata_dict): """ While there are metadata values that represent a lot of what we want to capture, they seem to be unreliable. @@ -191,10 +204,10 @@ class V3Parser(BaseParser): """ label_map = {} - self._get_file_handle().seek(-8, 2) - chunk_map_start_location = struct.unpack("Q", self._get_file_handle().read(8))[0] - self._get_file_handle().seek(chunk_map_start_location) - raw_text = self._get_file_handle().read(-1) + self._fh.seek(-8, 2) + chunk_map_start_location = struct.unpack("Q", self._fh.read(8))[0] + self._fh.seek(chunk_map_start_location) + raw_text = self._fh.read(-1) label_start = raw_text.index(V3Parser.CHUNK_MAP_START) + 32 while True: @@ -208,24 +221,6 @@ class V3Parser(BaseParser): label_start = data_start + 16 return label_map - def _read_chunk(self, chunk_location): - """ - Gets the data for a given chunk pointer - - :rtype: bytes - - """ - self._get_file_handle().seek(chunk_location) - # The chunk metadata is always 16 bytes long - chunk_metadata = self._get_file_handle().read(16) - header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata) - if header != V3Parser.CHUNK_HEADER: - raise ValueError("The ND2 file seems to be corrupted.") - # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the - # start of the actual data field, which is at some arbitrary place after the metadata. - self._get_file_handle().seek(chunk_location + 16 + relative_offset) - return self._get_file_handle().read(data_length) - def _parse_unsigned_char(self, data): return struct.unpack("B", data.read(1))[0] diff --git a/nd2reader/version.py b/nd2reader/version.py index 9434d2f..ca50011 100644 --- a/nd2reader/version.py +++ b/nd2reader/version.py @@ -2,7 +2,7 @@ import re from nd2reader.exc import InvalidVersionError -def get_version(filename): +def get_version(fh): """ Determines what version the ND2 is. @@ -10,11 +10,10 @@ def get_version(filename): :type filename: str """ - with open(filename, 'rb') as f: - # the first 16 bytes seem to have no meaning, so we skip them - f.seek(16) - # the next 38 bytes contain the string that we want to parse. Unlike most of the ND2, this is in UTF-8 - data = f.read(38).decode("utf8") + # the first 16 bytes seem to have no meaning, so we skip them + fh.seek(16) + # the next 38 bytes contain the string that we want to parse. Unlike most of the ND2, this is in UTF-8 + data = fh.read(38).decode("utf8") return parse_version(data)