diff --git a/images_cache/__init__.py b/images_cache/__init__.py index 42fd789..f680ca5 100644 --- a/images_cache/__init__.py +++ b/images_cache/__init__.py @@ -10,22 +10,22 @@ class CacheDriverFactory(object): def __init__(self): self.cache_driver = settings.IMAGES_CACHE_DRIVER - def _get_fake_cache(self): + def _get_fake_cache(self, rendering_engine): from fake_cache import FakeCache return FakeCache() - def _get_redis_driver(self): + def _get_redis_driver(self, rendering_engine): from redis_img_cache import RedisCache return RedisCache(settings.REDIS_HOST, settings.REDIS_PORT, settings.REDIS_DB, - settings.CACHE_EXPIRE_TIME) + settings.CACHE_EXPIRE_TIME, rendering_engine) - def get_cache(self): + def get_cache(self, rendering_engine): if not settings.IMAGES_CACHE_ENABLED: - return self._get_fake_cache() + return self._get_fake_cache(rendering_engine) else: if self.cache_driver == 'redis': - return self._get_redis_driver() + return self._get_redis_driver(rendering_engine) else: raise UnknownCacheDriver('There is no driver for %s' % self.cache_driver) diff --git a/images_cache/cache_interface.py b/images_cache/cache_interface.py index 1305fdd..0c697a1 100644 --- a/images_cache/cache_interface.py +++ b/images_cache/cache_interface.py @@ -1,5 +1,3 @@ -from ome_seadragon import settings - from abc import ABCMeta, abstractmethod @@ -8,19 +6,19 @@ class CacheInterface(object): __metaclass__ = ABCMeta @abstractmethod - def _get_tile_key(self, image_id, level, column, row, tile_size, image_format, + def _get_tile_key(self, engine, image_id, level, column, row, tile_size, image_format, image_quality=None): if image_quality: image_format = '%s%s' % (image_format, image_quality) return 'TILE::IMG_%s|L_%s|C_%s-R_%s|S_%spx|F_%s|E_%s' % (image_id, level, column, row, tile_size, image_format.upper(), - settings.TILES_RENDERING_ENGINE) + engine) @abstractmethod - def _get_thumbnail_key(self, image_id, thumbnail_size, image_format): + def _get_thumbnail_key(self, engine, image_id, thumbnail_size, image_format): return 'THUMB::IMG_%s|S_%spx|F_%s|E_%s' % (image_id, thumbnail_size, image_format.upper(), - settings.THUMBNAILS_RENDERING_ENGINE) + engine) @abstractmethod def tile_to_cache(self, image_id, image_obj, level, column, row, tile_size, image_format, diff --git a/images_cache/fake_cache.py b/images_cache/fake_cache.py index ba9ccfc..6a52e2a 100644 --- a/images_cache/fake_cache.py +++ b/images_cache/fake_cache.py @@ -6,11 +6,11 @@ class FakeCache(CacheInterface): def __init__(self): pass - def _get_tile_key(self, image_id, level, column, row, tile_size, image_format, + def _get_tile_key(self, engine, image_id, level, column, row, tile_size, image_format, image_quality=None): pass - def _get_thumbnail_key(self, image_id, thumbnail_size, image_format): + def _get_thumbnail_key(self, engine, image_id, thumbnail_size, image_format): pass def tile_to_cache(self, image_id, image_obj, level, column, row, tile_size, image_format, diff --git a/images_cache/redis_img_cache.py b/images_cache/redis_img_cache.py index dc936f3..bd57d7b 100644 --- a/images_cache/redis_img_cache.py +++ b/images_cache/redis_img_cache.py @@ -8,32 +8,33 @@ class RedisCache(CacheInterface): - def __init__(self, host, port, database, default_expire): + def __init__(self, host, port, database, default_expire, engine): self.client = redis.StrictRedis(host=host, port=port, db=database) self.default_expire_time = default_expire self.logger = logging.getLogger(__name__) + self.engine = engine - def _get_tile_key(self, image_id, level, column, row, tile_size, image_format, + def _get_tile_key(self, engine, image_id, level, column, row, tile_size, image_format, image_quality=None): - return super(RedisCache, self)._get_tile_key(image_id, level, column, row, - tile_size, image_format, - image_quality) + return super(RedisCache, self)._get_tile_key(engine, image_id, level, column, row, + tile_size, image_format, image_quality) - def _get_thumbnail_key(self, image_id, thumbnail_size, image_format): - return super(RedisCache, self)._get_thumbnail_key(image_id, thumbnail_size, image_format) + def _get_thumbnail_key(self, engine, image_id, thumbnail_size, image_format): + return super(RedisCache, self)._get_thumbnail_key(engine, image_id, thumbnail_size, + image_format) def tile_to_cache(self, image_id, image_obj, level, column, row, tile_size, image_format, image_quality=None): out_buffer = StringIO() image_obj.save(out_buffer, image_format) - tile_key = self._get_tile_key(image_id, level, column, row, tile_size, + tile_key = self._get_tile_key(self.engine, image_id, level, column, row, tile_size, image_format.upper(), image_quality) self.client.set(tile_key, out_buffer.getvalue()) self._set_expire_time(tile_key) def tile_from_cache(self, image_id, level, column, row, tile_size, image_format, image_quality=None): - tile_key = self._get_tile_key(image_id, level, column, row, tile_size, + tile_key = self._get_tile_key(self.engine, image_id, level, column, row, tile_size, image_format.upper(), image_quality) self.logger.info('Tile from cache: %s' % tile_key) tile_str = self.client.get(tile_key) @@ -51,12 +52,12 @@ def tile_from_cache(self, image_id, level, column, row, tile_size, image_format, def thumbnail_to_cache(self, image_id, image_obj, thumbnail_size, image_format): out_buffer = StringIO() image_obj.save(out_buffer, image_format) - th_key = self._get_thumbnail_key(image_id, thumbnail_size, image_format) + th_key = self._get_thumbnail_key(self.engine, image_id, thumbnail_size, image_format) self.client.set(th_key, out_buffer.getvalue()) self._set_expire_time(th_key) def thumbnail_from_cache(self, image_id, thumbnail_size, image_format): - th_key = self._get_thumbnail_key(image_id, thumbnail_size, image_format) + th_key = self._get_thumbnail_key(self.engine, image_id, thumbnail_size, image_format) self.logger.info('Thumbnail from cache: %s' % th_key) img_str = self.client.get(th_key) if img_str: diff --git a/settings.py b/settings.py index 88ee458..69d06f9 100644 --- a/settings.py +++ b/settings.py @@ -21,10 +21,16 @@ def time_dict_to_seconds(value): # /bin/omero config set omero.web.ome_seadragon.repository $(/bin/omero config get omero.data.dir) 'omero.web.ome_seadragon.repository': ['IMGS_REPOSITORY', None, identity, None], 'omero.web.ome_seadragon.images_folder': ['IMGS_FOLDER', 'ManagedRepository', identity, None], - # default rendering engine - 'omero.web.ome_seadragon.tiles.rendering_engine': ['TILES_RENDERING_ENGINE', 'openslide', identity, None], - 'omero.web.ome_seadragon.thumbnails.rendering_engine': ['THUMBNAILS_RENDERING_ENGINE', 'omero', - identity, None], + # default rendering engines + 'omero.web.ome_seadragon.tiles.primary_rendering_engine': ['PRIMARY_TILES_RENDERING_ENGINE', + 'openslide', identity, None], + 'omero.web.ome_seadragon.thumbnails.primary_rendering_engine': ['PRIMARY_THUMBNAILS_RENDERING_ENGINE', + 'omero', identity, None], + # secondary rendering engines + 'omero.web.ome_seadragon.tiles.secondary_rendering_engine': ['SECONDARY_TILES_RENDERING_ENGINE', + 'omero', identity, None], + 'omero.web.ome_seadragon.thumbnails.secondary_rendering_engine': ['SECONDARY_THUMBNAILS_RENDERING_ENGINE', + 'openslide', identity, None], # deepzoom properties 'omero.web.ome_seadragon.deepzoom.overlap': ['DEEPZOOM_OVERLAP', 1, identity, None], 'omero.web.ome_seadragon.deepzoom.format': ['DEEPZOOM_FORMAT', 'jpeg', identity, None], diff --git a/slides_manager/__init__.py b/slides_manager/__init__.py index 406a623..93523c2 100644 --- a/slides_manager/__init__.py +++ b/slides_manager/__init__.py @@ -8,8 +8,16 @@ class UnknownRenderingEngine(Exception): class RenderingEngineFactory(object): def __init__(self): - self.tiles_rendering_engine = settings.TILES_RENDERING_ENGINE - self.thumbnails_rendering_engine = settings.THUMBNAILS_RENDERING_ENGINE + self.primary_tiles_rendering_engine = settings.PRIMARY_TILES_RENDERING_ENGINE + self.primary_thumbnails_rendering_engine = settings.PRIMARY_THUMBNAILS_RENDERING_ENGINE + try: + self.secondary_tiles_rendering_engine = settings.SECONDARY_TILES_RENDERING_ENGINE + except AttributeError: + self.secondary_tiles_rendering_engine = None + try: + self.secondary_thumbnails_rendering_engine = settings.SECONDARY_THUMBNAILS_RENDERING_ENGINE + except AttributeError: + self.secondary_thumbnails_rendering_engine = None def _get_openslide_engine(self, image_id, connection): from openslide_engine import OpenSlideEngine @@ -21,18 +29,28 @@ def _get_omero_engine(self, image_id, connection): return OmeEngine(image_id, connection) - def get_tiles_rendering_engine(self, image_id, connection): - if self.tiles_rendering_engine == 'openslide': + def _get_engine(self, engine, image_id, connection): + if engine == 'openslide': return self._get_openslide_engine(image_id, connection) - if self.tiles_rendering_engine == 'omero': + if engine == 'omero': return self._get_omero_engine(image_id, connection) else: - raise UnknownRenderingEngine('%s is not a valid rendering engine' % self.tiles_rendering_engine) + raise UnknownRenderingEngine('%s is not a valid rendering engine' % self.primary_thumbnails_rendering_engine) - def get_thumbnails_rendering_engine(self, image_id, connection): - if self.thumbnails_rendering_engine == 'openslide': - return self._get_openslide_engine(image_id, connection) - if self.thumbnails_rendering_engine == 'omero': - return self._get_omero_engine(image_id, connection) + def get_primary_tiles_rendering_engine(self, image_id, connection): + return self._get_engine(self.primary_tiles_rendering_engine, image_id, connection) + + def get_primary_thumbnails_rendering_engine(self, image_id, connection): + return self._get_engine(self.primary_thumbnails_rendering_engine, image_id, connection) + + def get_secondary_tiles_rendering_engine(self, image_id, connection): + if self.secondary_tiles_rendering_engine: + return self._get_engine(self.secondary_tiles_rendering_engine, image_id, connection) + else: + return None + + def get_secondary_thumbnails_rendering_engine(self, image_id, connection): + if self.secondary_thumbnails_rendering_engine: + return self._get_engine(self.secondary_thumbnails_rendering_engine, image_id, connection) else: - raise UnknownRenderingEngine('%s is not a valid rendering engine' % self.thumbnails_rendering_engine) + return None diff --git a/slides_manager/ome_engine.py b/slides_manager/ome_engine.py index e02a839..ece766d 100644 --- a/slides_manager/ome_engine.py +++ b/slides_manager/ome_engine.py @@ -152,7 +152,7 @@ def get_dzi_description(self, original_file_source=False, file_mimetype=None): def get_thumbnail(self, size, original_file_source=False, file_mimeype=None): self._check_source_type(original_file_source) - cache = CacheDriverFactory().get_cache() + cache = CacheDriverFactory().get_cache('omero') thumbnail = cache.thumbnail_from_cache(self.image_id, size, settings.DEEPZOOM_FORMAT) if thumbnail is None: @@ -175,7 +175,7 @@ def get_thumbnail(self, size, original_file_source=False, file_mimeype=None): def get_tile(self, level, column, row, original_file_source=False, file_mimetype=None): self._check_source_type(original_file_source) - cache = CacheDriverFactory().get_cache() + cache = CacheDriverFactory().get_cache('omero') cache_params = { 'image_id': self.image_id, 'level': level, diff --git a/slides_manager/openslide_engine.py b/slides_manager/openslide_engine.py index e83b29e..14cf402 100644 --- a/slides_manager/openslide_engine.py +++ b/slides_manager/openslide_engine.py @@ -60,7 +60,7 @@ def get_dzi_description(self, original_file_source=False, file_mimetype=None): return None def get_thumbnail(self, size, original_file_source=False, file_mimeype=None): - cache = CacheDriverFactory().get_cache() + cache = CacheDriverFactory().get_cache('openslide') # get thumbnail from cache thumb = cache.thumbnail_from_cache(self.image_id, size, settings.DEEPZOOM_FORMAT) @@ -78,7 +78,7 @@ def get_thumbnail(self, size, original_file_source=False, file_mimeype=None): return thumb, settings.DEEPZOOM_FORMAT def get_tile(self, level, column, row, original_file_source=False, file_mimetype=None): - cache = CacheDriverFactory().get_cache() + cache = CacheDriverFactory().get_cache('openslide') cache_params = { 'image_id': self.image_id, 'level': level, diff --git a/src/js/shapes.js b/src/js/shapes.js index 53480fc..f7a3c4c 100644 --- a/src/js/shapes.js +++ b/src/js/shapes.js @@ -141,7 +141,7 @@ function Shape(id, transform_matrix) { }; this.getCoveragePercentage = function(shape) { - var shape_area = shape.getArea(); + var shape_area = shape.getArea(1); if ((typeof this.paper_shape !== 'undefined') && (typeof shape_area !== 'undefined')) { var intersection = this._shapeToPath().intersect(shape._shapeToPath()); // passing 1 as pixel size because we only need area in pixels to get the coverage ratio diff --git a/tools/image_tiles_downloader.py b/tools/image_tiles_downloader.py new file mode 100644 index 0000000..ec2523e --- /dev/null +++ b/tools/image_tiles_downloader.py @@ -0,0 +1,147 @@ +from cStringIO import StringIO +from PIL import Image +from requests import Session +import xml.etree.ElementTree as ET +import sys +import os +from urlparse import urljoin +import logging +from argparse import ArgumentParser +import math + + +class ImageTilesDownloader(object): + + def __init__(self, image_id, mirax_image, ome_base_url, output_folder=None, + log_level='INFO', log_file=None): + self.logger = self.get_logger(log_level, log_file) + self.image_id = image_id + if mirax_image: + self.get_dzi_url = urljoin(ome_base_url, 'mirax/deepzoom/get/%s.dzi' % self.image_id) + self.get_tile_pattern = urljoin( + ome_base_url, 'mirax/deepzoom/get/%s_files' % self.image_id + ) + self.get_tile_pattern = urljoin(self.get_tile_pattern, '%s/%s_%s.%s') + self.logger.info(self.get_tile_pattern) + else: + self.get_dzi_url = urljoin(ome_base_url, 'deepzoom/get/%s.dzi' % self.image_id) + self.get_tile_pattern = urljoin( + ome_base_url, 'deepzoom/get/%s_files/' % self.image_id + ) + self.get_tile_pattern = urljoin(self.get_tile_pattern, '%s/%s_%s.%s') + self.logger.info(self.get_tile_pattern) + self.output_folder = output_folder + if self.output_folder: + try: + os.makedirs(self.output_folder) + self.logger.info('Output directory %s created' % self.output_folder) + except OSError: + self.logger.info('Using existing directory %s' % self.output_folder) + self.session = Session() + + def get_logger(self, log_level='INFO', log_file=None, mode='a'): + LOG_FORMAT = '%(asctime)s|%(levelname)-8s|%(message)s' + LOG_DATEFMT = '%Y-%m-%d %H:%M:%S' + + logger = logging.getLogger('tiler_downloader') + if not isinstance(log_level, int): + try: + log_level = getattr(logging, log_level) + except AttributeError: + raise ValueError('Unsupported literal log level: %s' % log_level) + logger.setLevel(log_level) + logger.handlers = [] + if log_file: + handler = logging.FileHandler(log_file, mode=mode) + else: + handler = logging.StreamHandler() + formatter = logging.Formatter(LOG_FORMAT, datefmt=LOG_DATEFMT) + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + def _get_image_infos(self): + response = self.session.get(self.get_dzi_url) + if response.status_code == 200: + dzi = ET.fromstring(response.text) + self.logger.debug(response.text) + return { + 'format': dzi.get('Format'), + 'tile_size': int(dzi.get('TileSize')), + 'width': int(dzi.getchildren()[0].get('Width')), + 'height': int(dzi.getchildren()[0].get('Height')) + } + else: + self.logger.error(response) + + def _get_max_zoom_level(self, img_height, img_width): + return int(math.ceil(math.log(max(img_height, img_width), 2))) + + def _get_scale_factor(self, level, max_level): + return math.pow(0.5, max_level - level) + + def _get_scaled_dimension(self, img_width, img_height, level, max_level): + scale_factor = self._get_scale_factor(level, max_level) + return { + 'width': int(math.ceil(img_width * scale_factor)), + 'height': int(math.ceil(img_height * scale_factor)) + } + + def _save_tile(self, image_data, output_folder, file_name): + with open(os.path.join(output_folder, file_name), 'w') as ofile: + img = Image.open(StringIO(image_data)) + img.save(ofile) + + def _get_tiles(self, level, scaled_width, scaled_height, tile_size, tile_format): + if self.output_folder: + tiles_output_folder = os.path.join(self.output_folder, 'level_%d' % level) + try: + os.makedirs(tiles_output_folder) + except OSError: + pass + for x in xrange(scaled_width / tile_size): + for y in xrange(scaled_height / tile_size): + tr = self.session.get(self.get_tile_pattern % (level, x, y, tile_format)) + if self.output_folder: + self._save_tile(tr.content, tiles_output_folder, '%d_%d.%s' % (x, y, tile_format)) + + def run(self): + image_infos = self._get_image_infos() + max_level = self._get_max_zoom_level(image_infos['height'], image_infos['width']) + self.logger.info('MAX LEVEL: %d' % max_level) + for x in xrange(max_level + 1): + self.logger.info('### Level %d ###' % x) + scaled_dimensions = self._get_scaled_dimension(image_infos['width'], image_infos['height'], + x, max_level) + self.logger.info('Scales dimensions %r' % scaled_dimensions) + self._get_tiles(x, scaled_dimensions['width'], scaled_dimensions['height'], + image_infos['tile_size'], image_infos['format']) + + + +def get_parser(): + parser = ArgumentParser('Download tiles for a given image and optionally save them on file system') + parser.add_argument('--image-id', type=str, required=True, + help='The ID of the image that will be downloaded') + parser.add_argument('--mirax', action='store_true', + help='Add this flag to fetch a MIRAX file') + parser.add_argument('--ome-base-url', type=str, required=True, + help='the base URL of the OMERO.web server') + parser.add_argument('--output-dir', type=str, default=None, + help='output folder where the tiles will be saved') + parser.add_argument('--log-level', type=str, default='INFO', + help='log level (default=INFO)') + parser.add_argument('--log-file', type=str, default=None, + help='log file (default=stderr)') + return parser + + +def main(argv): + parser = get_parser() + args = parser.parse_args(argv) + downloader = ImageTilesDownloader(args.image_id, args.mirax, args.ome_base_url, + args.output_dir, args.log_level, args.log_file) + downloader.run() + +if __name__ == '__main__': + main(sys.argv[1:]) \ No newline at end of file diff --git a/views.py b/views.py index 4aa2228..9908727 100644 --- a/views.py +++ b/views.py @@ -196,8 +196,16 @@ def find_annotations(request, conn=None, **kwargs): @login_required() def get_image_dzi(request, image_id, fetch_original_file=False, file_mimetype=None, conn=None, **kwargs): - rendering_engine = RenderingEngineFactory().get_tiles_rendering_engine(image_id, conn) - dzi_metadata = rendering_engine.get_dzi_description(fetch_original_file, file_mimetype) + rf = RenderingEngineFactory() + rendering_engine = rf.get_primary_tiles_rendering_engine(image_id, conn) + try: + dzi_metadata = rendering_engine.get_dzi_description(fetch_original_file, file_mimetype) + except Exception, e: + rendering_engine = rf.get_secondary_tiles_rendering_engine(image_id, conn) + if rendering_engine: + dzi_metadata = rendering_engine.get_dzi_description(fetch_original_file, file_mimetype) + else: + raise e if dzi_metadata: return HttpResponse(dzi_metadata, content_type='application/xml') else: @@ -207,9 +215,18 @@ def get_image_dzi(request, image_id, fetch_original_file=False, @login_required() def get_image_thumbnail(request, image_id, fetch_original_file=False, file_mimetype=None, conn=None, **kwargs): - rendering_engine = RenderingEngineFactory().get_thumbnails_rendering_engine(image_id, conn) - thumbnail, image_format = rendering_engine.get_thumbnail(int(request.GET.get('size')), - fetch_original_file, file_mimetype) + rf = RenderingEngineFactory() + rendering_engine = rf.get_primary_thumbnails_rendering_engine(image_id, conn) + try: + thumbnail, image_format = rendering_engine.get_thumbnail(int(request.GET.get('size')), + fetch_original_file, file_mimetype) + except Exception, e: + rendering_engine = rf.get_secondary_thumbnail_rendering_engine(image_id, conn) + if rendering_engine: + thumbnail, image_format = rendering_engine.get_thumbnail(int(request.GET.get('size')), + fetch_original_file, file_mimetype) + else: + raise e if thumbnail: response = HttpResponse(content_type="image/%s" % image_format) thumbnail.save(response, image_format) @@ -223,9 +240,18 @@ def get_tile(request, image_id, level, column, row, tile_format, fetch_original_file=False, file_mimetype=None, conn=None, **kwargs): if tile_format != settings.DEEPZOOM_FORMAT: return HttpResponseServerError("Format %s not supported by the server" % tile_format) - rendering_engine = RenderingEngineFactory().get_tiles_rendering_engine(image_id, conn) - tile, image_format = rendering_engine.get_tile(int(level), int(column), int(row), - fetch_original_file, file_mimetype) + rf = RenderingEngineFactory() + rendering_engine = rf.get_primary_tiles_rendering_engine(image_id, conn) + try: + tile, image_format = rendering_engine.get_tile(int(level), int(column), int(row), + fetch_original_file, file_mimetype) + except Exception, e: + rendering_engine = rf.get_secondary_tiles_rendering_engine(image_id, conn) + if rendering_engine: + tile, image_format = rendering_engine.get_tile(int(level), int(column), int(row), + fetch_original_file, file_mimetype) + else: + raise e if tile: response = HttpResponse(content_type='image/%s' % image_format) tile.save(response, image_format) @@ -237,8 +263,16 @@ def get_tile(request, image_id, level, column, row, tile_format, @login_required() def get_image_mpp(request, image_id, fetch_original_file=False, file_mimetype=None, conn=None, **kwargs): - rendering_engine = RenderingEngineFactory().get_tiles_rendering_engine(image_id, conn) - image_mpp = rendering_engine.get_openseadragon_config(fetch_original_file, file_mimetype)['mpp'] + rf = RenderingEngineFactory() + rendering_engine = rf.get_primary_tiles_rendering_engine(image_id, conn) + try: + image_mpp = rendering_engine.get_openseadragon_config(fetch_original_file, file_mimetype)['mpp'] + except Exception, e: + rendering_engine = rf.get_secondary_tiles_rendering_engine(image_id, conn) + if rendering_engine: + image_mpp = rendering_engine.get_openseadragon_config(fetch_original_file, file_mimetype)['mpp'] + else: + raise e return HttpResponse(json.dumps({'image_mpp': image_mpp}), content_type='application/json')