def __init__(self, path, **kwargs): """ Initialize the tile class. See the base class for other available parameters. :param path: a filesystem path for the tile source. """ super().__init__(path, **kwargs) self._largeImagePath = self._getLargeImagePath() if not os.path.isfile(self._largeImagePath): try: possibleYaml = self._largeImagePath.split('multi://', 1)[-1] self._info = yaml.safe_load(possibleYaml) self._validator.validate(self._info) self._basePath = Path('.') except Exception: raise TileSourceFileNotFoundError(self._largeImagePath) from None else: try: with builtins.open(self._largeImagePath) as fptr: start = fptr.read(1024).strip() if start[:1] not in ('{', '#', '-') and (start[:1] < 'a' or start[:1] > 'z'): raise TileSourceError('File cannot be opened via multi-source reader.') fptr.seek(0) self._info = yaml.safe_load(fptr) except (json.JSONDecodeError, yaml.YAMLError, UnicodeDecodeError): raise TileSourceError('File cannot be opened via multi-source reader.') self._validator.validate(self._info) self._basePath = Path(self._largeImagePath).parent self._basePath /= Path(self._info.get('basePath', '.')) self._collectFrames()
def _loadTileSource(cls, item, **kwargs): if 'largeImage' not in item: raise TileSourceError('No large image file in this item.') if item['largeImage'].get('expected'): raise TileSourceError('The large image file for this item is ' 'still pending creation.') sourceName = item['largeImage']['sourceName'] try: # First try to use the tilesource we recorded as the preferred one. # This is faster than trying to find the best source each time. tileSource = girder_tilesource.AvailableGirderTileSources[sourceName](item, **kwargs) except TileSourceError as exc: # We could try any source # tileSource = girder_tilesource.getGirderTileSource(item, **kwargs) # but, instead, log that the original source no longer works are # reraise the exception logger.warning('The original tile source for item %s is not working' % item['_id']) try: file = File().load(item['largeImage']['fileId'], force=True) localPath = File().getLocalFilePath(file) open(localPath, 'rb').read(1) except IOError: logger.warning( 'Is the original data reachable and readable (it fails via %r)?', localPath) raise IOError(localPath) from None except Exception: pass raise exc return tileSource
def _getLargeImagePath(self): # If self.mayHaveAdjacentFiles is True, we try to use the girder # mount where companion files appear next to each other. largeImageFileId = self.item['largeImage']['fileId'] largeImageFile = File().load(largeImageFileId, force=True) try: largeImagePath = None if (self.mayHaveAdjacentFiles(largeImageFile) and hasattr(File(), 'getGirderMountFilePath')): try: if (largeImageFile.get('imported') and File().getLocalFilePath(largeImageFile) == largeImageFile['path']): largeImagePath = largeImageFile['path'] except Exception: pass if not largeImagePath: try: largeImagePath = File().getGirderMountFilePath( largeImageFile) except FilePathException: pass if not largeImagePath: try: largeImagePath = File().getLocalFilePath(largeImageFile) except AttributeError as e: raise TileSourceError( 'No local file path for this file: %s' % e.args[0]) return largeImagePath except (TileSourceAssetstoreError, FilePathException): raise except (KeyError, ValidationException, TileSourceError) as e: raise TileSourceError('No large image file in this item: %s' % e.args[0])
def __init__(self, path, **kwargs): """ Initialize the tile class. See the base class for other available parameters. :param path: a filesystem path for the tile source. """ super().__init__(path, **kwargs) self._largeImagePath = str(self._getLargeImagePath()) self._pixelInfo = {} try: self._openjpeg = glymur.Jp2k(self._largeImagePath) if not self._openjpeg.shape: if not os.path.isfile(self._largeImagePath): raise FileNotFoundError() raise TileSourceError( 'File cannot be opened via Glymur and OpenJPEG.') except (glymur.jp2box.InvalidJp2kError, struct.error): raise TileSourceError( 'File cannot be opened via Glymur and OpenJPEG.') except FileNotFoundError: if not os.path.isfile(self._largeImagePath): raise TileSourceFileNotFoundError( self._largeImagePath) from None raise glymur.set_option('lib.num_threads', multiprocessing.cpu_count()) self._openjpegHandles = queue.LifoQueue() for _ in range(self._maxOpenHandles - 1): self._openjpegHandles.put(None) self._openjpegHandles.put(self._openjpeg) try: self.sizeY, self.sizeX = self._openjpeg.shape[:2] except IndexError: raise TileSourceError( 'File cannot be opened via Glymur and OpenJPEG.') self.levels = int(self._openjpeg.codestream.segment[2].num_res) + 1 self._minlevel = 0 self.tileWidth = self.tileHeight = 2**int( math.ceil( max( math.log(float(self.sizeX)) / math.log(2) - self.levels + 1, math.log(float(self.sizeY)) / math.log(2) - self.levels + 1))) # Small and large tiles are both inefficient. Large tiles don't work # with some viewers (leaflet and Slide Atlas, for instance) if self.tileWidth < self._minTileSize or self.tileWidth > self._maxTileSize: self.tileWidth = self.tileHeight = min( self._maxTileSize, max(self._minTileSize, self.tileWidth)) self.levels = int( math.ceil( math.log( float(max(self.sizeX, self.sizeY)) / self.tileWidth) / math.log(2))) + 1 self._minlevel = self.levels - self._openjpeg.codestream.segment[ 2].num_res - 1 self._getAssociatedImages()
def __init__(self, path, maxSize=None, **kwargs): """ Initialize the tile class. See the base class for other available parameters. :param path: the associated file path. :param maxSize: either a number or an object with {'width': (width), 'height': height} in pixels. If None, the default max size is used. """ super().__init__(path, **kwargs) self._maxSize = maxSize if isinstance(maxSize, str): try: maxSize = json.loads(maxSize) except Exception: raise TileSourceError( 'maxSize must be None, an integer, a dictionary, or a ' 'JSON string that converts to one of those.') self.maxSize = maxSize largeImagePath = self._getLargeImagePath() # Some formats shouldn't be read this way, even if they could. For # instances, mirax (mrxs) files look like JPEGs, but opening them as # such misses most of the data. self._ignoreSourceNames('pil', largeImagePath) try: self._pilImage = PIL.Image.open(largeImagePath) except OSError: if not os.path.isfile(largeImagePath): raise TileSourceFileNotFoundError(largeImagePath) from None raise TileSourceError('File cannot be opened via PIL.') # If this is encoded as a 32-bit integer or a 32-bit float, convert it # to an 8-bit integer. This expects the source value to either have a # maximum of 1, 2^8-1, 2^16-1, 2^24-1, or 2^32-1, and scales it to # [0, 255] pilImageMode = self._pilImage.mode.split(';')[0] if pilImageMode in ('I', 'F'): imgdata = numpy.asarray(self._pilImage) maxval = 256**math.ceil(math.log(numpy.max(imgdata) + 1, 256)) - 1 self._pilImage = PIL.Image.fromarray( numpy.uint8(numpy.multiply(imgdata, 255.0 / maxval))) self.sizeX = self._pilImage.width self.sizeY = self._pilImage.height # We have just one tile which is the entire image. self.tileWidth = self.sizeX self.tileHeight = self.sizeY self.levels = 1 # Throw an exception if too big if self.tileWidth <= 0 or self.tileHeight <= 0: raise TileSourceError('PIL tile size is invalid.') maxWidth, maxHeight = getMaxSize(maxSize, self.defaultMaxSize()) if self.tileWidth > maxWidth or self.tileHeight > maxHeight: raise TileSourceError('PIL tile size is too large.')
def getTileIOTiffError(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, sparseFallback=False, exception=None, **kwargs): if sparseFallback: if z: noedge = kwargs.copy() noedge.pop('edge', None) noedge['inSparseFallback'] = True image = self.getTile( x // 2, y // 2, z - 1, pilImageAllowed=True, numpyAllowed=False, sparseFallback=sparseFallback, edge=False, **noedge) if not isinstance(image, PIL.Image.Image): image = PIL.Image.open(io.BytesIO(image)) image = image.crop(( self.tileWidth / 2 if x % 2 else 0, self.tileHeight / 2 if y % 2 else 0, self.tileWidth if x % 2 else self.tileWidth / 2, self.tileHeight if y % 2 else self.tileHeight / 2)) image = image.resize((self.tileWidth, self.tileHeight)) else: image = PIL.Image.new('RGBA', (self.tileWidth, self.tileHeight)) return self._outputTile(image, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, applyStyle=False, **kwargs) raise TileSourceError('Internal I/O failure: %s' % exception.args[0])
def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): self._xyzInRange(x, y, z) svslevel = self._svslevels[z] # When we read a region from the SVS, we have to ask for it in the # SVS level 0 coordinate system. Our x and y is in tile space at the # specified z level, so the offset in SVS level 0 coordinates has to be # scaled by the tile size and by the z level. scale = 2**(self.levels - 1 - z) offsetx = x * self.tileWidth * scale offsety = y * self.tileHeight * scale # We ask to read an area that will cover the tile at the z level. The # scale we computed in the __init__ process for this svs level tells # how much larger a region we need to read. try: tile = self._openslide.read_region( (offsetx, offsety), svslevel['svslevel'], (self.tileWidth * svslevel['scale'], self.tileHeight * svslevel['scale'])) except openslide.lowlevel.OpenSlideError as exc: raise TileSourceError('Failed to get OpenSlide region (%r).' % exc) # Always scale to the svs level 0 tile size. if svslevel['scale'] != 1: tile = tile.resize((self.tileWidth, self.tileHeight), getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS) return self._outputTile(tile, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, **kwargs)
def _getAssociatedImage(self, imageKey): """ Get an associated image in PIL format. :param imageKey: the key of the associated image. :return: the image in PIL format or None. """ info = self._metadata['seriesAssociatedImages'].get(imageKey) if info is None: return series = info['seriesNum'] with self._tileLock: try: javabridge.attach() image = self._bioimage.read( series=series, rescale=False, # return internal data types XYWH=(0, 0, info['sizeX'], info['sizeY'])) except javabridge.JavaException as exc: es = javabridge.to_string(exc.throwable) raise TileSourceError( 'Failed to get Bioformat series (%s, %r).' % (es, (series, info['sizeX'], info['sizeY']))) finally: if javabridge.get_env(): javabridge.detach() return large_image.tilesource.base._imageToPIL(image)
def _lazyImport(): """ Import the tifffile module. This is done when needed rather than in the module initialization because it is slow. """ global tifffile if tifffile is None: try: import tifffile except ImportError: raise TileSourceError('tifffile module not found.') if not hasattr(tifffile.TiffTag, 'dtype_name') or not hasattr( tifffile.TiffPage, 'aszarr'): tifffile = None raise TileSourceError('tifffile module is too old.')
def _levelFromIfd(self, ifd, baseifd): """ Get the level based on information in an ifd and on the full-resolution 0-frame ifd. An exception is raised if the ifd does not seem to represent a possible level. :param ifd: an ifd record returned from tifftools. :param baseifd: the ifd record of the full-resolution frame 0. :returns: the level, where self.levels - 1 is full resolution and 0 is the lowest resolution. """ sizeX = ifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0] sizeY = ifd['tags'][tifftools.Tag.ImageLength.value]['data'][0] tileWidth = baseifd['tags'][tifftools.Tag.TileWidth.value]['data'][0] tileHeight = baseifd['tags'][tifftools.Tag.TileLength.value]['data'][0] for tag in { tifftools.Tag.SamplesPerPixel.value, tifftools.Tag.BitsPerSample.value, tifftools.Tag.PlanarConfig.value, tifftools.Tag.Photometric.value, tifftools.Tag.Orientation.value, tifftools.Tag.Compression.value, tifftools.Tag.TileWidth.value, tifftools.Tag.TileLength.value, }: if ((tag in ifd['tags'] and tag not in baseifd['tags']) or (tag not in ifd['tags'] and tag in baseifd['tags']) or (tag in ifd['tags'] and ifd['tags'][tag]['data'] != baseifd['tags'][tag]['data'])): raise TileSourceError('IFD does not match first IFD.') sizes = [(self.sizeX, self.sizeY)] for level in range(self.levels - 1, -1, -1): if (sizeX, sizeY) in sizes: return level altsizes = [] for w, h in sizes: w2f = int(math.floor(w / 2)) h2f = int(math.floor(h / 2)) w2c = int(math.ceil(w / 2)) h2c = int(math.ceil(h / 2)) w2t = int(math.floor((w / 2 + tileWidth - 1) / tileWidth)) * tileWidth h2t = int(math.floor((h / 2 + tileHeight - 1) / tileHeight)) * tileHeight for w2, h2 in [(w2f, h2f), (w2f, h2c), (w2c, h2f), (w2c, h2c), (w2t, h2t)]: if (w2, h2) not in altsizes: altsizes.append((w2, h2)) sizes = altsizes raise TileSourceError('IFD size is not a power of two smaller than first IFD.')
def getTile(self, x, y, z, *args, **kwargs): frame = self._getFrame(**kwargs) self._xyzInRange( x, y, z, frame, len(self._frames) if hasattr(self, '_frames') else None) if not (self.minLevel <= z <= self.maxLevel): raise TileSourceError('z layer does not exist') xFraction = (x + 0.5) * self.tileWidth * 2**(self.levels - 1 - z) / self.sizeX yFraction = (y + 0.5) * self.tileHeight * 2**(self.levels - 1 - z) / self.sizeY fFraction = yFraction if hasattr(self, '_frames'): fFraction = float(frame) / (len(self._frames) - 1) backgroundColor = colorsys.hsv_to_rgb( h=xFraction, s=(0.3 + (0.7 * fFraction)), v=(0.3 + (0.7 * yFraction)), ) rgbColor = tuple(int(val * 255) for val in backgroundColor) image = Image.new(mode='RGB', size=(self.tileWidth, self.tileHeight), color=(rgbColor if not self.fractal else (255, 255, 255))) imageDraw = ImageDraw.Draw(image) if self.fractal: self.fractalTile(image, x, y, 2**z, rgbColor) fontsize = 0.15 text = 'x=%d\ny=%d\nz=%d' % (x, y, z) if hasattr(self, '_frames'): if self._framesParts == 1: text += '\nf=%d' % frame else: for k1, k2 in [('C', 'IndexC'), ('Z', 'IndexZ'), ('T', 'IndexT'), ('XY', 'IndexXY')]: if k2 in self._frames[frame]: text += '\n%s=%d' % (k1, self._frames[frame][k2]) fontsize = min(fontsize, 0.8 / len(text.split('\n'))) try: # the font size should fill the whole tile imageDrawFont = ImageFont.truetype( font='/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf', size=int(fontsize * min(self.tileWidth, self.tileHeight))) except OSError: imageDrawFont = ImageFont.load_default() imageDraw.multiline_text(xy=(10, 10), text=text, fill=(0, 0, 0), font=imageDrawFont) _counters['tiles'] += 1 if self.monochrome: image = image.convert('L') return self._outputTile(image, TILE_FORMAT_PIL, x, y, z, **kwargs)
def mm_y(self, value): self._checkEditable() value = float(value) if value is not None else None if value is not None and value <= 0: raise TileSourceError('mm_y must be positive or None') if value != getattr(self, '_minHeight', None): self._mm_y = value self._invalidateImage()
def minWidth(self, value): self._checkEditable() value = int(value) if value is not None else None if value is not None and value <= 0: raise TileSourceError('minWidth must be positive or None') if value != getattr(self, '_minWidth', None): self._minWidth = value self._invalidateImage()
def _colorizerFromStyle(self, style): """ Add a specified style to a mapnik raster symbolizer. :param style: a style object. :returns: a mapnik raster colorizer. """ try: scheme = style.get('scheme', 'linear') mapnik_scheme = getattr(mapnik, f'COLORIZER_{scheme.upper()}') except AttributeError: mapnik_scheme = mapnik.COLORIZER_DISCRETE raise TileSourceError( 'Scheme has to be either "discrete" or "linear".') colorizer = mapnik.RasterColorizer(mapnik_scheme, mapnik.Color(0, 0, 0, 0)) bandInfo = self.getOneBandInformation(style['band']) minimum = style.get('min', 0) maximum = style.get('max', 255) minimum = bandInfo[minimum] if minimum in ('min', 'max') else minimum maximum = bandInfo[maximum] if maximum in ('min', 'max') else maximum if minimum == 'auto': if not (0 <= bandInfo['min'] <= 255 and 1 <= bandInfo['max'] <= 255): minimum = bandInfo['min'] else: minimum = 0 if maximum == 'auto': if not (0 <= bandInfo['min'] <= 255 and 1 <= bandInfo['max'] <= 255): maximum = bandInfo['max'] else: maximum = 255 if style.get('palette') == 'colortable': for value, color in enumerate(bandInfo['colortable']): colorizer.add_stop(value, mapnik.Color(*color)) else: colors = self.getHexColors( style.get('palette', ['#000000', '#ffffff'])) if len(colors) < 2: raise TileSourceError('A palette must have at least 2 colors.') values = self.interpolateMinMax(minimum, maximum, len(colors)) for value, color in sorted(zip(values, colors)): colorizer.add_stop(value, mapnik.Color(color)) return colorizer
def __init__(self, path, **kwargs): """ Initialize the tile class. See the base class for other available parameters. :param path: a filesystem path for the tile source. """ super().__init__(path, **kwargs) self._largeImagePath = str(self._getLargeImagePath()) _lazyImport() try: self._tf = tifffile.TiffFile(self._largeImagePath) except Exception: if not os.path.isfile(self._largeImagePath): raise TileSourceFileNotFoundError( self._largeImagePath) from None raise TileSourceError('File cannot be opened via tifffile.') maxseries, maxsamples = self._biggestSeries() self.tileWidth = self.tileHeight = self._tileSize s = self._tf.series[maxseries] self._baseSeries = s page = s.pages[0] if ('TileWidth' in page.tags and self._minTileSize <= page.tags['TileWidth'].value <= self._maxTileSize): self.tileWidth = page.tags['TileWidth'].value if ('TileLength' in page.tags and self._minTileSize <= page.tags['TileLength'].value <= self._maxTileSize): self.tileHeight = page.tags['TileLength'].value self.sizeX = s.shape[s.axes.index('X')] self.sizeY = s.shape[s.axes.index('Y')] try: unit = {2: 25.4, 3: 10}[page.tags['ResolutionUnit'].value.real] self._mm_x = (unit * page.tags['XResolution'].value[1] / page.tags['XResolution'].value[0]) self._mm_y = (unit * page.tags['YResolution'].value[1] / page.tags['YResolution'].value[0]) except Exception: self._mm_x = self._mm_y = None self._findMatchingSeries() self.levels = int( max( 1, math.ceil( math.log( float(max(self.sizeX, self.sizeY)) / self.tileWidth) / math.log(2)) + 1)) self._findAssociatedImages() for key in dir(self._tf): if (key.startswith('is_') and hasattr(self, '_handle_' + key[3:]) and getattr(self._tf, key)): getattr(self, '_handle_' + key[3:])()
def _lazyImport(): """ Import the nd2 module. This is done when needed rather than in the module initialization because it is slow. """ global nd2 if nd2 is None: try: import nd2 except ImportError: raise TileSourceError('nd2 module not found.')
def __init__(self, path, **kwargs): """ Initialize the tile class. See the base class for other available parameters. :param path: a filesystem path for the tile source. """ super().__init__(path, **kwargs) if str(path).startswith(NEW_IMAGE_PATH_FLAG): return self._initNew(**kwargs) self._largeImagePath = str(self._getLargeImagePath()) self._editable = False self._ignoreSourceNames('vips', self._largeImagePath) try: self._image = pyvips.Image.new_from_file(self._largeImagePath) except pyvips.error.Error: if not os.path.isfile(self._largeImagePath): raise TileSourceFileNotFoundError( self._largeImagePath) from None raise TileSourceError('File cannot be opened via pyvips') self.sizeX = self._image.width self.sizeY = self._image.height self.tileWidth = self.tileHeight = self._tileSize pages = 1 if 'n-pages' in self._image.get_fields(): pages = self._image.get('n-pages') self._frames = [0] for page in range(1, pages): subInputPath = self._largeImagePath + '[page=%d]' % page subImage = pyvips.Image.new_from_file(subInputPath) if subImage.width == self.sizeX and subImage.height == self.sizeY: self._frames.append(page) continue if subImage.width * subImage.height < self.sizeX * self.sizeY: continue self._frames = [page] self.sizeX = subImage.width self.sizeY = subImage.height self._image = subImage self.levels = int( max( 1, math.ceil( math.log( float(max(self.sizeX, self.sizeY)) / self.tileWidth) / math.log(2)) + 1)) if len(self._frames) > 1: self._recentFrames = cachetools.LRUCache(maxsize=6) self._frameLock = threading.RLock()
def interpolateMinMax(start, stop, count): """ Returns interpolated values for a given start, stop and count :returns: List of interpolated values """ try: step = (float(stop) - float(start)) / (float(count) - 1) except ValueError: raise TileSourceError( 'Minimum and maximum values should be numbers, "auto", "min", or "max".' ) return [float(start + i * step) for i in range(count)]
def crop(self, value): self._checkEditable() if value is None: self._crop = None return x, y, w, h = value x = int(x) y = int(y) w = int(w) h = int(h) if x < 0 or y < 0 or w <= 0 or h <= 0: raise TileSourceError( 'Crop must have non-negative x, y and positive w, h') self._crop = (x, y, w, h)
def _sourceBoundingBox(self, source, width, height): """ Given a source with a possible transform and an image width and height, compute the bounding box for the source. If a crop is used, it is included in the results. If a non-identify transform is used, both it and its inverse are included in the results. :param source: a dictionary that may have a position record. :param width: the width of the source to transform. :param height: the height of the source to transform. :returns: a dictionary with left, top, right, bottom of the bounding box in the final coordinate space. """ pos = source.get('position') bbox = {'left': 0, 'top': 0, 'right': width, 'bottom': height} if not pos: return bbox x0, y0, x1, y1 = 0, 0, width, height if 'crop' in pos: x0 = min(max(pos['crop'].get('left', x0), 0), width) y0 = min(max(pos['crop'].get('top', y0), 0), height) x1 = min(max(pos['crop'].get('right', x1), x0), width) y1 = min(max(pos['crop'].get('bottom', y1), y0), height) bbox['crop'] = {'left': x0, 'top': y0, 'right': x1, 'bottom': y1} corners = numpy.array([[x0, y0, 1], [x1, y0, 1], [x0, y1, 1], [x1, y1, 1]]) m = numpy.identity(3) m[0][0] = pos.get('s11', 1) * pos.get('scale', 1) m[0][1] = pos.get('s12', 0) * pos.get('scale', 1) m[0][2] = pos.get('x', 0) m[1][0] = pos.get('s21', 0) * pos.get('scale', 1) m[1][1] = pos.get('s22', 1) * pos.get('scale', 1) m[1][2] = pos.get('y', 0) if not numpy.array_equal(m, numpy.identity(3)): bbox['transform'] = m try: bbox['inverse'] = numpy.linalg.inv(m) except numpy.linalg.LinAlgError: raise TileSourceError('The position for a source is not invertable (%r)', pos) transcorners = numpy.dot(m, corners.T) bbox['left'] = min(transcorners[0]) bbox['top'] = min(transcorners[1]) bbox['right'] = max(transcorners[0]) bbox['bottom'] = max(transcorners[1]) return bbox
def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, sparseFallback=False, **kwargs): frame = self._getFrame(**kwargs) self._xyzInRange(x, y, z, frame, len(self._frames) if hasattr(self, '_frames') else None) if frame > 0: if self._frames[frame]['dirs'][z] is not None: dir = self._getDirFromCache(*self._frames[frame]['dirs'][z]) else: dir = None else: dir = self._tiffDirectories[z] try: allowStyle = True if dir is None: try: if not kwargs.get('inSparseFallback'): tile = self.getTileFromEmptyDirectory(x, y, z, **kwargs) else: raise IOTiffException('Missing z level %d' % z) except Exception: if sparseFallback: raise IOTiffException('Missing z level %d' % z) else: raise allowStyle = False format = TILE_FORMAT_PIL else: tile = dir.getTile(x, y) format = 'JPEG' if isinstance(tile, PIL.Image.Image): format = TILE_FORMAT_PIL if isinstance(tile, numpy.ndarray): format = TILE_FORMAT_NUMPY return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, applyStyle=allowStyle, **kwargs) except InvalidOperationTiffException as e: raise TileSourceError(e.args[0]) except IOTiffException as e: return self.getTileIOTiffError( x, y, z, pilImageAllowed=pilImageAllowed, numpyAllowed=numpyAllowed, sparseFallback=sparseFallback, exception=e, **kwargs)
def _biggestSeries(self): """ Find the series with the most pixels. Use all series that have the same dimensionality and resolution. They can differ in X, Y size. :returns: index of the largest series, number of pixels in a frame in that series. """ maxseries = None maxsamples = 0 try: for idx, s in enumerate(self._tf.series): samples = numpy.prod(s.shape) if samples > maxsamples and 'X' in s.axes and 'Y' in s.axes: maxseries = idx maxsamples = samples except Exception as exc: self.logger.debug('Cannot use tifffile: %r', exc) maxseries = None if maxseries is None: raise TileSourceError('File cannot be opened via tifffile source.') return maxseries, maxsamples
def _addSourceToTile(self, tile, sourceEntry, corners, scale): """ Add a source to the current tile. :param tile: a numpy array with the tile, or None if there is no data yet. :param sourceEntry: the current record from the sourceList. This contains the sourcenum, kwargs to apply when opening the source, and the frame within the source to fetch. :param corners: the four corners of the tile in the main image space coordinates. :param scale: power of 2 scale of the output; this is tne number of pixels that are conceptually aggregated from the source for one output pixel. :returns: a numpy array of the tile. """ source = self._sources[sourceEntry['sourcenum']] ts = self._openSource(source, sourceEntry['kwargs']) # If tile is outside of bounding box, skip it bbox = source['bbox'] if (corners[2][0] <= bbox['left'] or corners[0][0] >= bbox['right'] or corners[2][1] <= bbox['top'] or corners[0][1] >= bbox['bottom']): return tile transform = bbox.get('transform') srccorners = ( list(numpy.dot(bbox['inverse'], numpy.array(corners).T).T) if transform is not None else corners) x = y = 0 # If there is no transform or the diagonals are positive and there is # no sheer, use getRegion with an appropriate size (be wary of edges) if (transform is None or transform[0][0] > 0 and transform[0][1] == 0 and transform[1][0] == 0 and transform[1][1] > 0): scaleX = transform[0][0] if transform is not None else 1 scaleY = transform[1][1] if transform is not None else 1 region = { 'left': srccorners[0][0], 'top': srccorners[0][1], 'right': srccorners[2][0], 'bottom': srccorners[2][1] } output = { 'maxWidth': (corners[2][0] - corners[0][0]) // scale, 'maxHeight': (corners[2][1] - corners[0][1]) // scale, } if region['left'] < 0: x -= region['left'] * scaleX // scale output['maxWidth'] += int(region['left'] * scaleX // scale) region['left'] = 0 if region['top'] < 0: y -= region['top'] * scaleY // scale output['maxHeight'] += int(region['top'] * scaleY // scale) region['top'] = 0 if region['right'] > source['metadata']['sizeX']: output['maxWidth'] -= int( (region['right'] - source['metadata']['sizeX']) * scaleX // scale) region['right'] = source['metadata']['sizeX'] if region['bottom'] > source['metadata']['sizeY']: output['maxHeight'] -= int( (region['bottom'] - source['metadata']['sizeY']) * scaleY // scale) region['bottom'] = source['metadata']['sizeY'] for key in region: region[key] = int(round(region[key])) self.logger.debug('getRegion: ts: %r, region: %r, output: %r', ts, region, output) sourceTile, _ = ts.getRegion( region=region, output=output, frame=sourceEntry.get('frame', 0), format=TILE_FORMAT_NUMPY) # Otherwise, get an area twice as big as needed and use # scipy.ndimage.affine_transform to transform it else: # TODO raise TileSourceError('Not implemented') # Crop # TODO tile = self._mergeTiles(tile, sourceTile, x, y) return tile
def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, sparseFallback=False, **kwargs): if (z < 0 or z >= len(self._omeLevels) or (self._omeLevels[z] is not None and kwargs.get('frame') in (None, 0, '0', ''))): return super().getTile(x, y, z, pilImageAllowed=pilImageAllowed, numpyAllowed=numpyAllowed, sparseFallback=sparseFallback, **kwargs) frame = self._getFrame(**kwargs) if frame < 0 or frame >= len(self._omebase['TiffData']): raise TileSourceError('Frame does not exist') subdir = None if self._omeLevels[z] is not None: dirnum = int(self._omeLevels[z]['TiffData'][frame].get( 'IFD', frame)) else: dirnum = int(self._omeLevels[-1]['TiffData'][frame].get( 'IFD', frame)) subdir = self.levels - 1 - z dir = self._getDirFromCache(dirnum, subdir) if subdir: scale = int(2**subdir) if (dir is None or (dir.tileWidth != self.tileWidth and dir.tileWidth != dir.imageWidth) or (dir.tileHeight != self.tileHeight and dir.tileHeight != dir.imageHeight) or abs(dir.imageWidth * scale - self.sizeX) > scale or abs(dir.imageHeight * scale - self.sizeY) > scale): return super().getTile(x, y, z, pilImageAllowed=pilImageAllowed, numpyAllowed=numpyAllowed, sparseFallback=sparseFallback, **kwargs) try: tile = dir.getTile(x, y) format = 'JPEG' if isinstance(tile, PIL.Image.Image): format = TILE_FORMAT_PIL if isinstance(tile, numpy.ndarray): format = TILE_FORMAT_NUMPY return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) except InvalidOperationTiffException as e: raise TileSourceError(e.args[0]) except IOTiffException as e: return self.getTileIOTiffError(x, y, z, pilImageAllowed=pilImageAllowed, numpyAllowed=numpyAllowed, sparseFallback=sparseFallback, exception=e, **kwargs)
def _parseOMEInfo(self): # noqa if isinstance(self._omeinfo['Image'], dict): self._omeinfo['Image'] = [self._omeinfo['Image']] for img in self._omeinfo['Image']: if isinstance(img['Pixels'].get('TiffData'), dict): img['Pixels']['TiffData'] = [img['Pixels']['TiffData']] if isinstance(img['Pixels'].get('Plane'), dict): img['Pixels']['Plane'] = [img['Pixels']['Plane']] if isinstance(img['Pixels'].get('Channels'), dict): img['Pixels']['Channels'] = [img['Pixels']['Channels']] try: self._omebase = self._omeinfo['Image'][0]['Pixels'] if isinstance(self._omebase.get('Plane'), dict): self._omebase['Plane'] = [self._omebase['Plane']] if ((not len(self._omebase['TiffData']) or len(self._omebase['TiffData']) == 1) and (len(self._omebase.get('Plane', [])) or len(self._omebase.get('Channel', [])))): if (not len(self._omebase['TiffData']) or self._omebase['TiffData'][0] == {} or int(self._omebase['TiffData'][0].get( 'PlaneCount', 0)) == 1): planes = copy.deepcopy( self._omebase.get('Plane', self._omebase.get('Channel'))) if isinstance(planes, dict): planes = [planes] self._omebase['SizeC'] = 1 for idx, plane in enumerate(planes): plane['IndexC'] = idx self._omebase['TiffData'] = planes elif (int(self._omebase['TiffData'][0].get( 'PlaneCount', 0)) == len( self._omebase.get('Plane', self._omebase.get('Channel', [])))): planes = copy.deepcopy( self._omebase.get('Plane', self._omebase.get('Channel'))) for idx, plane in enumerate(planes): plane['IFD'] = plane.get( 'IFD', int(self._omebase['TiffData'][0].get('IFD', 0)) + idx) self._omebase['TiffData'] = planes if isinstance(self._omebase['TiffData'], dict): self._omebase['TiffData'] = [self._omebase['TiffData']] if len({ entry.get('UUID', {}).get('FileName', '') for entry in self._omebase['TiffData'] }) > 1: raise TileSourceError('OME Tiff references multiple files') if (len(self._omebase['TiffData']) != int(self._omebase['SizeC']) * int(self._omebase['SizeT']) * int(self._omebase['SizeZ']) or len(self._omebase['TiffData']) != len( self._omebase.get('Plane', self._omebase['TiffData']))): raise TileSourceError( 'OME Tiff contains frames that contain multiple planes') except (KeyError, ValueError, IndexError, TypeError): raise TileSourceError( 'OME Tiff does not contain an expected record')
def __init__(self, path, **kwargs): """ Initialize the tile class. See the base class for other available parameters. :param path: a filesystem path for the tile source. """ # Note this is the super of the parent class, not of this class. super(TiffFileTileSource, self).__init__(path, **kwargs) self._largeImagePath = str(self._getLargeImagePath()) try: base = TiledTiffDirectory(self._largeImagePath, 0, mustBeTiled=None) except TiffException: if not os.path.isfile(self._largeImagePath): raise TileSourceFileNotFoundError( self._largeImagePath) from None raise TileSourceError('Not a recognized OME Tiff') info = getattr(base, '_description_record', None) if not info or not info.get('OME'): raise TileSourceError('Not an OME Tiff') self._omeinfo = info['OME'] self._checkForOMEZLoop(self._largeImagePath) self._parseOMEInfo() omeimages = [ entry['Pixels'] for entry in self._omeinfo['Image'] if len( entry['Pixels']['TiffData']) == len(self._omebase['TiffData']) ] levels = [ max( 0, int( math.ceil( math.log( max( float(entry['SizeX']) / base.tileWidth, float(entry['SizeY']) / base.tileHeight)) / math.log(2)))) for entry in omeimages ] omebylevel = dict(zip(levels, omeimages)) self._omeLevels = [ omebylevel.get(key) for key in range(max(omebylevel.keys()) + 1) ] if base._tiffInfo.get('istiled'): self._tiffDirectories = [ TiledTiffDirectory(self._largeImagePath, int(entry['TiffData'][0].get('IFD', 0))) if entry else None for entry in self._omeLevels ] else: self._tiffDirectories = [ TiledTiffDirectory(self._largeImagePath, 0, mustBeTiled=None) if entry else None for entry in self._omeLevels ] self._checkForInefficientDirectories(warn=False) _maxChunk = min(base.imageWidth, base.tileWidth * self._skippedLevels ** 2) * \ min(base.imageHeight, base.tileHeight * self._skippedLevels ** 2) if _maxChunk > self._maxUntiledChunk: raise TileSourceError( 'Untiled image is too large to access with the OME Tiff source' ) self.tileWidth = base.tileWidth self.tileHeight = base.tileHeight self.levels = len(self._tiffDirectories) self.sizeX = base.imageWidth self.sizeY = base.imageHeight # We can get the embedded images, but we don't currently use non-tiled # images as associated images. This would require enumerating tiff # directories not mentioned by the ome list. self._associatedImages = {} self._checkForInefficientDirectories()
def __init__(self, path, **kwargs): # noqa """ Initialize the tile class. See the base class for other available parameters. :param path: the associated file path. """ super().__init__(path, **kwargs) largeImagePath = str(self._getLargeImagePath()) self._ignoreSourceNames('bioformats', largeImagePath, r'\.png$') if not _startJavabridge(self.logger): raise TileSourceError( 'File cannot be opened by bioformats reader because javabridge failed to start' ) self._tileLock = threading.RLock() try: javabridge.attach() try: self._bioimage = bioformats.ImageReader(largeImagePath) except (AttributeError, OSError) as exc: if not os.path.isfile(largeImagePath): raise TileSourceFileNotFoundError(largeImagePath) from None self.logger.debug( 'File cannot be opened via Bioformats. (%r)' % exc) raise TileSourceError( 'File cannot be opened via Bioformats. (%r)' % exc) _openImages.append(self) rdr = self._bioimage.rdr # Bind additional functions not done by bioformats module. # Functions are listed at https://downloads.openmicroscopy.org # //bio-formats/5.1.5/api/loci/formats/IFormatReader.html for (name, params, desc) in [ ('getBitsPerPixel', '()I', 'Get the number of bits per pixel'), ('getEffectiveSizeC', '()I', 'effectiveC * Z * T = imageCount'), ('isNormalized', '()Z', 'Is float data normalized'), ('isMetadataComplete', '()Z', 'True if metadata is completely parsed'), ('getDomains', '()[Ljava/lang/String;', 'Get a list of domains'), ('getZCTCoords', '(I)[I', 'Gets the Z, C and T coordinates ' '(real sizes) corresponding to the given rasterized index value.' ), ('getOptimalTileWidth', '()I', 'the optimal sub-image width ' 'for use with openBytes'), ('getOptimalTileHeight', '()I', 'the optimal sub-image height ' 'for use with openBytes'), ('getResolutionCount', '()I', 'The number of resolutions for ' 'the current series'), ('setResolution', '(I)V', 'Set the resolution level'), ('getResolution', '()I', 'The current resolution level'), ('hasFlattenedResolutions', '()Z', 'True if resolutions have been flattened'), ('setFlattenedResolutions', '(Z)V', 'Set if resolution should be flattened'), ]: setattr( rdr, name, types.MethodType( javabridge.jutil.make_method(name, params, desc), rdr)) # rdr.setFlattenedResolutions(False) self._metadata = { 'dimensionOrder': rdr.getDimensionOrder(), 'metadata': javabridge.jdictionary_to_string_dictionary(rdr.getMetadata()), 'seriesMetadata': javabridge.jdictionary_to_string_dictionary( rdr.getSeriesMetadata()), 'seriesCount': rdr.getSeriesCount(), 'imageCount': rdr.getImageCount(), 'rgbChannelCount': rdr.getRGBChannelCount(), 'sizeColorPlanes': rdr.getSizeC(), 'sizeT': rdr.getSizeT(), 'sizeZ': rdr.getSizeZ(), 'sizeX': rdr.getSizeX(), 'sizeY': rdr.getSizeY(), 'pixelType': rdr.getPixelType(), 'isLittleEndian': rdr.isLittleEndian(), 'isRGB': rdr.isRGB(), 'isInterleaved': rdr.isInterleaved(), 'isIndexed': rdr.isIndexed(), 'bitsPerPixel': rdr.getBitsPerPixel(), 'sizeC': rdr.getEffectiveSizeC(), 'normalized': rdr.isNormalized(), 'metadataComplete': rdr.isMetadataComplete(), # 'domains': rdr.getDomains(), 'optimalTileWidth': rdr.getOptimalTileWidth(), 'optimalTileHeight': rdr.getOptimalTileHeight(), 'resolutionCount': rdr.getResolutionCount(), 'flattenedResolutions': rdr.hasFlattenedResolutions(), } self._checkSeries(rdr) bmd = bioformats.metadatatools.MetadataRetrieve( self._bioimage.metadata) try: self._metadata['channelNames'] = [ bmd.getChannelName(0, c) or bmd.getChannelID(0, c) for c in range(self._metadata['sizeColorPlanes']) ] except Exception: self._metadata['channelNames'] = [] for key in ['sizeXY', 'sizeC', 'sizeZ', 'sizeT']: if not isinstance(self._metadata[key], int) or self._metadata[key] < 1: self._metadata[key] = 1 self.sizeX = self._metadata['sizeX'] self.sizeY = self._metadata['sizeY'] self._computeTiles() self._computeLevels() self._computeMagnification() except javabridge.JavaException as exc: es = javabridge.to_string(exc.throwable) self.logger.debug('File cannot be opened via Bioformats. (%s)' % es) raise TileSourceError( 'File cannot be opened via Bioformats. (%s)' % es) except (AttributeError, UnicodeDecodeError): self.logger.exception( 'The bioformats reader threw an unhandled exception.') raise TileSourceError( 'The bioformats reader threw an unhandled exception.') finally: if javabridge.get_env(): javabridge.detach() if self.levels < 1: raise TileSourceError( 'OpenSlide image must have at least one level.') if self.sizeX <= 0 or self.sizeY <= 0: raise TileSourceError('Bioformats tile size is invalid.')
def __init__(self, path, **kwargs): """ Initialize the tile class. See the base class for other available parameters. :param path: a filesystem path for the tile source. """ super().__init__(path, **kwargs) self._largeImagePath = str(self._getLargeImagePath()) self._pixelInfo = {} _lazyImport() try: self._nd2 = nd2.ND2File(self._largeImagePath, validate_frames=True) except Exception: if not os.path.isfile(self._largeImagePath): raise TileSourceFileNotFoundError( self._largeImagePath) from None raise TileSourceError('File cannot be opened via nd2reader.') # We use dask to allow lazy reading of large images self._nd2array = self._nd2.to_dask(copy=False) arrayOrder = list(self._nd2.sizes) # Reorder this so that it is XY (P), T, Z, C, Y, X, S (or at least end # in Y, X[, S]). newOrder = [k for k in arrayOrder if k not in {'C', 'X', 'Y', 'S'} ] + (['C'] if 'C' in arrayOrder else []) + ['Y', 'X'] + ( ['S'] if 'S' in arrayOrder else []) if newOrder != arrayOrder: self._nd2array = numpy.moveaxis( self._nd2array, list(range(len(arrayOrder))), [newOrder.index(k) for k in arrayOrder]) self._nd2order = newOrder self._nd2origindex = {} basis = 1 for k in arrayOrder: if k not in {'C', 'X', 'Y', 'S'}: self._nd2origindex[k] = basis basis *= self._nd2.sizes[k] self.sizeX = self._nd2.sizes['X'] self.sizeY = self._nd2.sizes['Y'] self.tileWidth = self.tileHeight = self._tileSize if self.sizeX <= self._singleTileThreshold and self.sizeY <= self._singleTileThreshold: self.tileWidth = self.sizeX self.tileHeight = self.sizeY self.levels = int( max( 1, math.ceil( math.log( float(max(self.sizeX, self.sizeY)) / self.tileWidth) / math.log(2)) + 1)) self._framecount = (self._nd2.metadata.contents.channelCount * self._nd2.metadata.contents.frameCount) self._bandnames = { chan.channel.name.lower(): idx for idx, chan in enumerate(self._nd2.metadata.channels) } self._channels = [ chan.channel.name for chan in self._nd2.metadata.channels ] self._tileLock = threading.RLock()
def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): self._xyzInRange(x, y, z) ft = fc = fz = 0 fseries = self._metadata['frameSeries'][0] if kwargs.get('frame') is not None: frame = self._getFrame(**kwargs) fc = frame % self._metadata['sizeC'] fz = (frame // self._metadata['sizeC']) % self._metadata['sizeZ'] ft = (frame // self._metadata['sizeC'] // self._metadata['sizeZ']) % self._metadata['sizeT'] fxy = (frame // self._metadata['sizeC'] // self._metadata['sizeZ'] // self._metadata['sizeT']) if frame < 0 or fxy > self._metadata['sizeXY']: raise TileSourceError('Frame does not exist') fseries = self._metadata['frameSeries'][fxy] seriesLevel = self.levels - 1 - z scale = 1 while seriesLevel >= len(fseries['series']): seriesLevel -= 1 scale *= 2 offsetx = x * self.tileWidth * scale offsety = y * self.tileHeight * scale width = min(self.tileWidth * scale, self.sizeX // 2**seriesLevel - offsetx) height = min(self.tileHeight * scale, self.sizeY // 2**seriesLevel - offsety) sizeXAtScale = fseries['sizeX'] // (2**seriesLevel) sizeYAtScale = fseries['sizeY'] // (2**seriesLevel) finalWidth = width // scale finalHeight = height // scale width = min(width, sizeXAtScale - offsetx) height = min(height, sizeYAtScale - offsety) with self._tileLock: try: javabridge.attach() if width > 0 and height > 0: tile = self._bioimage.read( c=fc, z=fz, t=ft, series=fseries['series'][seriesLevel], rescale=False, # return internal data types XYWH=(offsetx, offsety, width, height)) else: # We need the same dtype, so read 1x1 at 0x0 tile = self._bioimage.read( c=fc, z=fz, t=ft, series=fseries['series'][seriesLevel], rescale=False, # return internal data types XYWH=(0, 0, 1, 1)) tile = numpy.zeros(tuple([0, 0] + list(tile.shape[2:])), dtype=tile.dtype) format = TILE_FORMAT_NUMPY except javabridge.JavaException as exc: es = javabridge.to_string(exc.throwable) raise TileSourceError( 'Failed to get Bioformat region (%s, %r).' % (es, (fc, fz, ft, fseries, self.sizeX, self.sizeY, offsetx, offsety, width, height))) finally: if javabridge.get_env(): javabridge.detach() if scale > 1: tile = tile[::scale, ::scale] if tile.shape[:2] != (finalHeight, finalWidth): fillValue = 0 if tile.dtype == numpy.uint16: fillValue = 65535 elif tile.dtype == numpy.uint8: fillValue = 255 elif tile.dtype.kind == 'f': fillValue = 1 retile = numpy.full(tuple([finalHeight, finalWidth] + list(tile.shape[2:])), fillValue, dtype=tile.dtype) retile[0:min(tile.shape[0], finalHeight), 0:min(tile.shape[1], finalWidth)] = tile[ 0:min(tile.shape[0], finalHeight), 0:min(tile.shape[1], finalWidth)] tile = retile return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, **kwargs)
def __init__(self, path, **kwargs): """ Initialize the tile class. See the base class for other available parameters. :param path: a filesystem path for the tile source. """ super().__init__(path, **kwargs) self._largeImagePath = self._getLargeImagePath() # Read the root dzi file and check that the expected image files exist try: with builtins.open(self._largeImagePath) as fptr: if fptr.read(1024).strip()[:5] != '<?xml': raise TileSourceError( 'File cannot be opened via deepzoom reader.') fptr.seek(0) xml = ElementTree.parse(self._largeImagePath).getroot() self._info = etreeToDict(xml)['Image'] except (ElementTree.ParseError, KeyError, UnicodeDecodeError): raise TileSourceError('File cannot be opened via Deepzoom reader.') except FileNotFoundError: if not os.path.isfile(self._largeImagePath): raise TileSourceFileNotFoundError( self._largeImagePath) from None raise # We should now have a dictionary like # {'Format': 'png', # or 'jpeg' # 'Overlap': '1', # 'Size': {'Height': '41784', 'Width': '44998'}, # 'TileSize': '254'} # and a file structure like # <rootname>_files/<level>/<x>_<y>.<format> # images will be TileSize+Overlap square; final images will be # truncated. Base level is either 0 or probably 8 (level 0 is a 1x1 # pixel tile) self.sizeX = int(self._info['Size']['Width']) self.sizeY = int(self._info['Size']['Height']) self.tileWidth = self.tileHeight = int(self._info['TileSize']) maxXY = max(self.sizeX, self.sizeY) self.levels = int( math.ceil(math.log(maxXY / self.tileWidth) / math.log(2))) + 1 tiledirName = os.path.splitext(os.path.basename( self._largeImagePath))[0] + '_files' rootdir = os.path.dirname(self._largeImagePath) self._tiledir = os.path.join(rootdir, tiledirName) if not os.path.isdir(self._tiledir): rootdir = os.path.dirname(rootdir) self._tiledir = os.path.join(rootdir, tiledirName) zeroname = '0_0.%s' % self._info['Format'] self._nested = os.path.isdir(os.path.join(self._tiledir, '0', zeroname)) zeroimg = PIL.Image.open( os.path.join(self._tiledir, '0', zeroname) if not self._nested else os.path.join(self._tiledir, '0', zeroname, zeroname)) if zeroimg.size == (1, 1): self._baselevel = int( math.ceil(math.log(maxXY) / math.log(2)) - math.ceil(math.log(maxXY / self.tileWidth) / math.log(2))) else: self._baselevel = 0