diff --git a/integration_tests/external/test_image_open.py b/integration_tests/external/test_image_open.py new file mode 100644 index 00000000..b8f18cc5 --- /dev/null +++ b/integration_tests/external/test_image_open.py @@ -0,0 +1,25 @@ +import renderapi +from renderapi.image_open import read_mipmap_image, read_mipmap_mask +from test_data import render_params, test_2_channels_d +import pytest +import numpy as np + +@pytest.fixture(scope='module') +def test_mml(): + tilespecs = [renderapi.tilespec.TileSpec(json=d) for d in test_2_channels_d] + mml = tilespecs[0].channels[0].ip[0] + return mml + +def test_image_open(test_mml): + img = read_mipmap_image(test_mml) + assert (img.shape == (2048,2048)) + assert (img.dtype == np.uint16) + assert (img.max() == 23059) + +def test_mask_open(test_mml): + mask = read_mipmap_mask(test_mml) + assert (mask.shape == (2048,2048)) + assert (mask.dtype == np.uint8) + assert (mask.max() == np.iinfo(mask.dtype).max) + + \ No newline at end of file diff --git a/renderapi/channel.py b/renderapi/channel.py index 267c19d8..b7ebac36 100644 --- a/renderapi/channel.py +++ b/renderapi/channel.py @@ -1,9 +1,10 @@ from .image_pyramid import ImagePyramid, MipMapLevel + class Channel: '''class for storing channels of different mipmapsources''' - def __init__(self,name=None,maxIntensity=None,minIntensity=None,ip=None,json=None): + def __init__(self, name=None, maxIntensity=None, minIntensity=None, ip=None, json=None): ''' Parameters ========== @@ -22,23 +23,23 @@ def __init__(self,name=None,maxIntensity=None,minIntensity=None,ip=None,json=Non if json is not None: self.from_dict(json) else: - self.name=name - self.maxIntensity=maxIntensity - self.minIntensity=minIntensity + self.name = name + self.maxIntensity = maxIntensity + self.minIntensity = minIntensity self.ip = ip def to_dict(self): ''' method for serializing this class to a json compatible dictionary''' d = {} - d['name']=self.name + d['name'] = self.name if self.minIntensity is not None: - d['minIntensity']=self.minIntensity + d['minIntensity'] = self.minIntensity if self.maxIntensity is not None: - d['maxIntensity']=self.maxIntensity - d['mipmapLevels'] = self.ip.to_ordered_dict() + d['maxIntensity'] = self.maxIntensity + d['mipmapLevels'] = dict(self.ip) return d - - def from_dict(self,d): + + def from_dict(self, d): ''' method for deserializing this class from a json compatible dictionary Parameters @@ -46,11 +47,9 @@ def from_dict(self,d): d: dict json compatible dictionary representation of this channel ''' - self.name=d['name'] - self.minIntensity=d['minIntensity'] - self.maxIntensity=d['maxIntensity'] - self.ip = ImagePyramid(mipMapLevels=[ - MipMapLevel( - int(l), imageUrl=v.get('imageUrl'), maskUrl=v.get('maskUrl')) - for l, v in d['mipmapLevels'].items()]) - + self.name = d['name'] + self.minIntensity = d['minIntensity'] + self.maxIntensity = d['maxIntensity'] + self.ip = ImagePyramid({l: MipMapLevel( + int(l), imageUrl=v.get('imageUrl'), maskUrl=v.get('maskUrl')) + for l, v in d['mipmapLevels'].items()}) diff --git a/renderapi/external/image_open/image_open.py b/renderapi/external/image_open/image_open.py new file mode 100644 index 00000000..ea52fe3c --- /dev/null +++ b/renderapi/external/image_open/image_open.py @@ -0,0 +1,97 @@ + +try: + from urlparse import urlparse + from urllib import unquote, urlopen +except ImportError as e: + from urllib.parse import urlparse, unquote + from urllib.request import urlopen +import tifffile +import numpy as np +import os +from PIL import Image +import boto3 +import tempfile +import io + + +def read_mipmap_image(mml, apply_mask=False): + """function to read a raw image from a mipmaplevel + + Parameters + ========== + mml : renderapi.image_pyramid.MipMapLevel + mip map level to read image of + apply_mask: bool + whether to apply mask to image (default=False) + + Returns + ======= + numpy.array + numpy array of image + """ + img = read_image_url(mml.imageUrl) + if apply_mask: + mask = read_mipmap_mask(mml) + mask = np.array(mask, dtype=np.float) / np.max(mask) + img = np.array(img * mask, dtype=img.dtype) + return img + + +def read_mipmap_mask(mml): + """function to read mask from a mipmaplevel + + Parameters + ========== + mml : renderapi.image_pyramid.MipMapLevel + mipmaplevel to read image from + + Returns + ======= + numpy.array + numpy array of mask + """ + return read_image_url(mml.maskUrl) + + +def read_image_url(url): + """function to read in a render url to an image to a numpy array + + Parameters + ========== + url: str or unicode + url to image to read + dtype: numpy.dtype or None + dtype to convert image to (default to our judgement) + + Returns + ======= + numpy.array + an numpy array containing the image data + """ + (scheme, netloc, path, params, query, fragment) = urlparse(url) + filepath = unquote(path) + filetype = os.path.splitext(filepath)[1][1:] + + if (scheme == 'file') or (scheme == ''): + if filetype == 'tif': + array = tifffile.imread(filepath) + else: + with open(filepath, 'r') as fp: + array = np.asarray(Image.open(fp)) + + if (scheme == 'http') or (scheme == 'https'): + with urlopen(url) as fp: + array = np.asarray(Image.open(fp)) + if (scheme == 's3'): + client = boto3.client('s3') + path = path[1:] + if filetype == 'tif': + fp, tfile = tempfile.mkstemp() + client.download_file(netloc, path, tfile) + array = tifffile.imread(tfile) + os.remove(tfile) + else: + obj = client.get_object(Bucket=netloc, Key=path) + array = np.asarray(Image.open(io.BytesIO(obj['Body'].read()))) + + return array diff --git a/renderapi/image_pyramid.py b/renderapi/image_pyramid.py index 04ab4678..2501a9c6 100644 --- a/renderapi/image_pyramid.py +++ b/renderapi/image_pyramid.py @@ -1,4 +1,5 @@ -from collections import OrderedDict +from collections import MutableMapping +from .errors import RenderError class MipMapLevel: """MipMapLevel class to represent a level of an image pyramid. @@ -26,7 +27,7 @@ def to_dict(self): dict json compatible dictionary representaton """ - return dict(self.__iter__()) + return self._formatUrls() def _formatUrls(self): d = {} @@ -36,90 +37,70 @@ def _formatUrls(self): d.update({'maskUrl': self.maskUrl}) return d + def __getitem__(self,key): + if key=='imageUrl': + return self.imageUrl + if key=='maskUrl': + return self.maskUrl + else: + raise RenderError('{} is not a valid attribute of a mipmapLevel'.format(key)) + def __iter__(self): return iter([(self.level, self._formatUrls())]) + def __eq__(self,b): + try: + return all([self.imageUrl == b.imageUrl, self.maskUrl==b.maskUrl]) + except AttributeError as e: + return all([self.imageUrl == b.get('imageUrl'), self.maskUrl==b.get('maskUrl')]) -class ImagePyramid: - '''Image Pyramid class representing a set of MipMapLevels which correspond - to mipmapped (continuously downsmapled by 2x) representations - of an image at level 0 - Can be put into dictionary formatting using dict(ip) or OrderedDict(ip) - - Attributes - ---------- - mipMapLevels : :obj:`list` of :class:`MipMapLevel` - list of :class:`MipMapLevel` objects defining image pyramid - - ''' - def __init__(self, mipMapLevels=[]): - self.mipMapLevels = mipMapLevels - - def to_dict(self): - """return dictionary representation of this object""" - return dict(self.__iter__()) - - def to_ordered_dict(self, key=None): - """returns :class:`OrderedDict` represention of this object, - ordered by mipmapLevel - - Parameters - ---------- - key : func - function to sort ordered dict of - :class:`mipMapLevel` dicts (default is by level) - - Returns - ------- - OrderedDict - sorted dictionary of :class:`mipMapLevels` in ImagePyramid +class TransformedDict(MutableMapping): + """A dictionary that applies an arbitrary key-altering + function before accessing the keys""" - """ - return OrderedDict(sorted( - self.__iter__(), key=((lambda x: x[0]) if key - is None else key))) + def __init__(self, *args, **kwargs): + self.store = dict() + self.update(dict(*args, **kwargs)) # use the free update to set keys - def append(self, mmL): - """appends a MipMapLevel to this ImagePyramid + def __getitem__(self, key): + return self.store[self.__keytransform__(key)] - Parameters - ---------- - mml : :class:`MipMapLevel` - :class:`MipMapLevel` to append - """ - self.mipMapLevels.append(mmL) + def __setitem__(self, key, value): + self.store[self.__keytransform__(key)] = value - def update(self, mmL): - """updates the ImagePyramid with this MipMapLevel. - will overwrite existing mipMapLevels with same level + def __delitem__(self, key): + del self.store[self.__keytransform__(key)] - Args: - mml (MipMapLevel): mipmap level to update in pyramid - """ - self.mipMapLevels = [ - l for l in self.mipMapLevels if l.level != mmL.level] - self.append(mmL) + def __iter__(self): + return iter(self.store) - def get(self, to_get): - """gets a specific mipmap level in dictionary form + def __len__(self): + return len(self.store) - Parameters - ---------- - to_get : int - level to get + def __keytransform__(self, key): + return key - Returns - ------- - dict - representation of requested MipMapLevel - """ - return self.to_dict()[to_get] # TODO should this default +class ImagePyramid(TransformedDict): + '''Image Pyramid class representing a set of MipMapLevels which correspond + to mipmapped (continuously downsmapled by 2x) representations + of an image at level 0 + Can be put into dictionary formatting using dict(ip) or OrderedDict(ip) + ''' + def __keytransform__(self,key): + try: + level = int(key) + except ValueError as e: + raise RenderError("{} is not a valid mipmap level".format(key)) + if level<0: + raise RenderError("{} is not a valid mipmap level (less than 0)".format(key)) + return "{}".format(level) + + def __iter__(self): + return iter(sorted(self.store)) + @property def levels(self): """list of MipMapLevels in this ImagePyramid""" - return [int(i.level) for i in self.mipMapLevels] + return [int(i.level) for i in self.__iter__()] - def __iter__(self): - return iter([ - l for sl in [list(mmL) for mmL in self.mipMapLevels] for l in sl]) \ No newline at end of file diff --git a/renderapi/tilespec.py b/renderapi/tilespec.py index fcf24cce..9b82f75f 100644 --- a/renderapi/tilespec.py +++ b/renderapi/tilespec.py @@ -92,7 +92,7 @@ def __init__(self, tileId=None, z=None, width=None, height=None, self.inputfilters = inputfilters self.layout = Layout(**kwargs) if layout is None else layout - self.ip = ImagePyramid(mipMapLevels=mipMapLevels) + self.ip = ImagePyramid({mml.level:mml for mml in mipMapLevels}) # legacy scaleXUrl self.maskUrl = maskUrl self.imageUrl = imageUrl @@ -175,7 +175,7 @@ def to_dict(self): thedict['maxIntensity'] = self.maxint if self.layout is not None: thedict['layout'] = self.layout.to_dict() - thedict['mipmapLevels'] = self.ip.to_ordered_dict() + thedict['mipmapLevels'] = dict(self.ip) thedict['transforms'] = {} thedict['transforms']['type'] = 'list' # thedict['transforms']['specList']=[t.to_dict() for t in self.tforms] @@ -227,10 +227,9 @@ def from_dict(self, d): self.maxY = d.get('maxY', None) self.minY = d.get('minY', None) mmld = d.get('mipmapLevels',{}) - self.ip = ImagePyramid(mipMapLevels=[ - MipMapLevel( + self.ip = ImagePyramid({l:MipMapLevel( int(l), imageUrl=v.get('imageUrl'), maskUrl=v.get('maskUrl')) - for l, v in mmld.items()]) + for l, v in mmld.items()}) tfl = TransformList(json=d['transforms']) self.tforms = tfl.tforms diff --git a/requirements.txt b/requirements.txt index 1ec45f7d..3f22a4bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ numpy pillow sphinxcontrib-napoleon decorator -six \ No newline at end of file +six diff --git a/test/test_channel.py b/test/test_channel.py index 3c536e80..963babe9 100644 --- a/test/test_channel.py +++ b/test/test_channel.py @@ -18,7 +18,8 @@ def test_channel(): mml = renderapi.image_pyramid.MipMapLevel(0, imageUrl='file:///not/a/path', maskUrl='file:///not/a/mask') - ip = renderapi.image_pyramid.ImagePyramid(mipMapLevels=[mml]) + ip = renderapi.image_pyramid.ImagePyramid() + ip[0]=mml channel = renderapi.channel.Channel(name='DAPI', maxIntensity=255, minIntensity=0, diff --git a/test/test_resolvedtiles.py b/test/test_resolvedtiles.py index f932eb84..2646f5ce 100644 --- a/test/test_resolvedtiles.py +++ b/test/test_resolvedtiles.py @@ -24,7 +24,7 @@ def resolvedtiles_object(referenced_tilespecs_and_transforms): def test_resolvedtiles_from_dict(resolvedtiles_object,referenced_tilespecs_and_transforms): tilespecs,transforms = referenced_tilespecs_and_transforms - d=resolvedtiles_object.to_dict() + d=json.loads(renderapi.utils.renderdumps(resolvedtiles_object)) resolved_tiles = renderapi.resolvedtiles.ResolvedTiles(json=d) assert(len(tilespecs)==len(resolved_tiles.tilespecs)) assert(len(transforms)==len(resolved_tiles.transforms))