For Windows machines
follow the installation on openslide-python
Steps breakdown
- Download Windows Binaries and unzip
- Add the bin folder to the very beginning of PATH
pip install --no-deps openslide-python
For Windows machines
follow the installation on openslide-python
Steps breakdown
pip install --no-deps openslide-python
# | |
# deepzoom_tile - Convert whole-slide images to Deep Zoom format | |
# | |
# Copyright (c) 2010-2015 Carnegie Mellon University | |
# | |
# This library is free software; you can redistribute it and/or modify it | |
# under the terms of version 2.1 of the GNU Lesser General Public License | |
# as published by the Free Software Foundation. | |
# | |
# This library 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 Lesser General Public | |
# License for more details. | |
# | |
# You should have received a copy of the GNU Lesser General Public License | |
# along with this library; if not, write to the Free Software Foundation, | |
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
# | |
"""An example program to generate a Deep Zoom directory tree from a slide.""" | |
from __future__ import print_function | |
import json | |
from multiprocessing import Process, JoinableQueue | |
import openslide | |
from openslide import open_slide, ImageSlide | |
from openslide.deepzoom import DeepZoomGenerator | |
from optparse import OptionParser | |
import os | |
import re | |
import shutil | |
import sys | |
from unicodedata import normalize | |
VIEWER_SLIDE_NAME = 'slide' | |
class TileWorker(Process): | |
"""A child process that generates and writes tiles.""" | |
def __init__(self, queue, slidepath, tile_size, overlap, limit_bounds, | |
quality): | |
Process.__init__(self, name='TileWorker') | |
self.daemon = True | |
self._queue = queue | |
self._slidepath = slidepath | |
self._tile_size = tile_size | |
self._overlap = overlap | |
self._limit_bounds = limit_bounds | |
self._quality = quality | |
self._slide = None | |
def run(self): | |
self._slide = open_slide(self._slidepath) | |
last_associated = None | |
dz = self._get_dz() | |
while True: | |
data = self._queue.get() | |
if data is None: | |
self._queue.task_done() | |
break | |
associated, level, address, outfile = data | |
if last_associated != associated: | |
dz = self._get_dz(associated) | |
last_associated = associated | |
tile = dz.get_tile(level, address) | |
tile.save(outfile, quality=self._quality) | |
self._queue.task_done() | |
def _get_dz(self, associated=None): | |
if associated is not None: | |
image = ImageSlide(self._slide.associated_images[associated]) | |
else: | |
image = self._slide | |
return DeepZoomGenerator(image, self._tile_size, self._overlap, | |
limit_bounds=self._limit_bounds) | |
class DeepZoomImageTiler(object): | |
"""Handles generation of tiles and metadata for a single image.""" | |
def __init__(self, dz, basename, tile_size, format, associated, queue): | |
self._dz = dz | |
self._basename = basename | |
self._tile_size = tile_size | |
self._format = format | |
self._associated = associated | |
self._queue = queue | |
self._processed = 0 | |
def run(self): | |
self._write_tiles() | |
self._write_dzi() | |
def _write_tiles(self): | |
for level in range(self._dz.level_count): | |
if max(self._dz.level_dimensions[level]) * 2 < self._tile_size: | |
continue | |
tiledir = os.path.join("%s" % self._basename) | |
if not os.path.exists(tiledir): | |
os.makedirs(tiledir) | |
cols, rows = self._dz.level_tiles[level] | |
for row in range(rows): | |
for col in range(cols): | |
tilename = os.path.join(tiledir, '%d_%d_%d.%s' % ( | |
self._dz.level_count - level - 1, | |
col, row, self._format)) | |
if not os.path.exists(tilename): | |
self._queue.put((self._associated, level, (col, row), | |
tilename)) | |
self._tile_done() | |
def _tile_done(self): | |
self._processed += 1 | |
count, total = self._processed, self._dz.tile_count | |
if count % 100 == 0 or count == total: | |
print("Tiling %s: wrote %d/%d tiles" % ( | |
self._associated or 'slide', count, total), | |
end='\r', file=sys.stderr) | |
if count == total: | |
print(file=sys.stderr) | |
def _write_dzi(self): | |
with open('%s.dzi' % self._basename, 'w') as fh: | |
fh.write(self.get_dzi()) | |
def get_dzi(self): | |
return self._dz.get_dzi(self._format) | |
class DeepZoomStaticTiler(object): | |
"""Handles generation of tiles and metadata for all images in a slide.""" | |
def __init__(self, slidepath, basename, format, tile_size, overlap, | |
limit_bounds, quality, workers, with_viewer): | |
if with_viewer: | |
# Check extra dependency before doing a bunch of work | |
import jinja2 | |
self._slide = open_slide(slidepath) | |
self._basename = basename | |
self._format = format | |
self._tile_size = tile_size | |
self._overlap = overlap | |
self._limit_bounds = limit_bounds | |
self._queue = JoinableQueue(2 * workers) | |
self._workers = workers | |
self._with_viewer = with_viewer | |
self._dzi_data = {} | |
for _i in range(workers): | |
TileWorker(self._queue, slidepath, tile_size, overlap, | |
limit_bounds, quality).start() | |
def run(self): | |
self._run_image() | |
if self._with_viewer: | |
for name in self._slide.associated_images: | |
self._run_image(name) | |
self._write_html() | |
self._write_static() | |
self._shutdown() | |
def _run_image(self, associated=None): | |
"""Run a single image from self._slide.""" | |
if associated is None: | |
image = self._slide | |
if self._with_viewer: | |
basename = os.path.join(self._basename, VIEWER_SLIDE_NAME) | |
else: | |
basename = self._basename | |
else: | |
image = ImageSlide(self._slide.associated_images[associated]) | |
basename = os.path.join(self._basename, self._slugify(associated)) | |
dz = DeepZoomGenerator(image, self._tile_size, self._overlap, | |
limit_bounds=self._limit_bounds) | |
tiler = DeepZoomImageTiler(dz, basename, self._tile_size, self._format, | |
associated, self._queue) | |
tiler.run() | |
self._dzi_data[self._url_for(associated)] = tiler.get_dzi() | |
def _url_for(self, associated): | |
if associated is None: | |
base = VIEWER_SLIDE_NAME | |
else: | |
base = self._slugify(associated) | |
return '%s.dzi' % base | |
def _write_html(self): | |
import jinja2 | |
env = jinja2.Environment(loader=jinja2.PackageLoader(__name__), | |
autoescape=True) | |
template = env.get_template('slide-multipane.html') | |
associated_urls = dict((n, self._url_for(n)) | |
for n in self._slide.associated_images) | |
try: | |
mpp_x = self._slide.properties[openslide.PROPERTY_NAME_MPP_X] | |
mpp_y = self._slide.properties[openslide.PROPERTY_NAME_MPP_Y] | |
mpp = (float(mpp_x) + float(mpp_y)) / 2 | |
except (KeyError, ValueError): | |
mpp = 0 | |
# Embed the dzi metadata in the HTML to work around Chrome's | |
# refusal to allow XmlHttpRequest from file:///, even when | |
# the originating page is also a file:/// | |
data = template.render(slide_url=self._url_for(None), | |
slide_mpp=mpp, | |
associated=associated_urls, | |
properties=self._slide.properties, | |
dzi_data=json.dumps(self._dzi_data)) | |
with open(os.path.join(self._basename, 'index.html'), 'w') as fh: | |
fh.write(data) | |
def _write_static(self): | |
basesrc = os.path.join(os.path.dirname(os.path.abspath(__file__)), | |
'static') | |
basedst = os.path.join(self._basename, 'static') | |
self._copydir(basesrc, basedst) | |
self._copydir(os.path.join(basesrc, 'images'), | |
os.path.join(basedst, 'images')) | |
def _copydir(self, src, dest): | |
if not os.path.exists(dest): | |
os.makedirs(dest) | |
for name in os.listdir(src): | |
srcpath = os.path.join(src, name) | |
if os.path.isfile(srcpath): | |
shutil.copy(srcpath, os.path.join(dest, name)) | |
@classmethod | |
def _slugify(cls, text): | |
text = normalize('NFKD', text.lower()).encode('ascii', 'ignore').decode() | |
return re.sub('[^a-z0-9]+', '_', text) | |
def _shutdown(self): | |
for _i in range(self._workers): | |
self._queue.put(None) | |
self._queue.join() | |
if __name__ == '__main__': | |
parser = OptionParser(usage='Usage: %prog [options] <slide>') | |
parser.add_option('-B', '--ignore-bounds', dest='limit_bounds', | |
default=True, action='store_false', | |
help='display entire scan area') | |
parser.add_option('-e', '--overlap', metavar='PIXELS', dest='overlap', | |
type='int', default=1, | |
help='overlap of adjacent tiles [1]') | |
parser.add_option('-f', '--format', metavar='{jpg|png}', dest='format', | |
default='jpg', | |
help='image format for tiles [jpg]') | |
parser.add_option('-j', '--jobs', metavar='COUNT', dest='workers', | |
type='int', default=4, | |
help='number of worker processes to start [4]') | |
parser.add_option('-o', '--output', metavar='NAME', dest='basename', | |
help='base name of output file') | |
parser.add_option('-Q', '--quality', metavar='QUALITY', dest='quality', | |
type='int', default=85, | |
help='JPG compression quality [85]') | |
parser.add_option('-r', '--viewer', dest='with_viewer', | |
action='store_true', | |
help='generate directory tree with HTML viewer') | |
parser.add_option('-s', '--size', metavar='PIXELS', dest='tile_size', | |
type='int', default=254, | |
help='tile size [254]') | |
(opts, args) = parser.parse_args() | |
try: | |
slidepath = args[0] | |
except IndexError: | |
parser.error('Missing slide argument') | |
if opts.basename is None: | |
opts.basename = os.path.splitext(os.path.basename(slidepath))[0] | |
DeepZoomStaticTiler(slidepath, opts.basename, opts.format, | |
opts.tile_size, opts.overlap, opts.limit_bounds, opts.quality, | |
opts.workers, opts.with_viewer).run() |