diff options
author | Franck Cuny <franck.cuny@gmail.com> | 2013-09-28 15:12:03 -0700 |
---|---|---|
committer | Franck Cuny <franck.cuny@gmail.com> | 2013-09-28 15:12:03 -0700 |
commit | 3b95d4aec6334b0e18eed433120fd549eaa5fa36 (patch) | |
tree | dc849b8a5e5414cda9bd7836a7b356f4948015c1 /seadragon.py | |
download | cpan-explorer-3b95d4aec6334b0e18eed433120fd549eaa5fa36.tar.gz |
import cpan-explorer master
Diffstat (limited to '')
-rw-r--r-- | seadragon.py | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/seadragon.py b/seadragon.py new file mode 100644 index 0000000..4d4fa48 --- /dev/null +++ b/seadragon.py @@ -0,0 +1,235 @@ +#!/usr/bin/python +## Copyright (c) 2008, Kapil Thangavelu <kapil.foss@gmail.com> +## All rights reserved. + +## Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: + +## Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in the +## documentation and/or other materials provided with the +## distribution. The names of its authors/contributors may be used to +## endorse or promote products derived from this software without +## specific prior written permission. + +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +## FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +## COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +## SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +## HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +## STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +## OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Implements a Deep Zoom / Seadragon Composer in Python + +For use with the seajax viewer + +reversed from the excellent blog description at + + http://gashi.ch/blog/inside-deep-zoom-2 + from Daniel Gasienica + +incidentally he's got an updated version of this script that supports collections +thats included in the openzoom project + +http://code.google.com/p/open-zoom/source/browse/trunk/src/main/python/deepzoom/deepzoom.py + +Author: Kapil Thangavelu +Date: 11/29/2008 +License: BSD +""" + +import math, os, optparse, sys +from PIL import Image + +xml_template = '''\ +<?xml version="1.0" encoding="UTF-8"?> +<Image TileSize="%(tile_size)s" Overlap="%(overlap)s" Format="%(format)s" + xmlns="http://schemas.microsoft.com/deepzoom/2008"> + <Size Width="%(width)s" Height="%(height)s"/> +</Image> +''' + + +filter_map = { + 'cubic' : Image.CUBIC, + 'bilinear' : Image.BILINEAR, + 'bicubic' : Image.BICUBIC, + 'nearest' : Image.NEAREST, + 'antialias' : Image.ANTIALIAS, + } + + +class PyramidComposer( object ): + + def __init__( self, image, tile_size=256.0, overlap=1, format="png", filter=None): + + self.image = image + self.tile_size = tile_size + self.overlap = overlap + self.format = format + self.width, self.height = self.image.size + self._levels = None + self.filter = filter + + @property + def levels( self ): + """ number of levels in an image pyramid """ + if self._levels is not None: + return self._levels + self._levels = int( math.ceil( math.log( max( (self.width, self.height) ), 2) ) ) + return self._levels + + def getLevelDimensions( self, level ): + assert level <= self.levels and level >= 0, "Invalid Pyramid Level" + scale = self.getLevelScale( level ) + return math.ceil( self.width * scale) , math.ceil( self.height * scale ) + + def getLevelScale( self, level ): + #print math.pow( 0.5, self.levels - level ) + return 1.0 / (1 << ( self.levels - level ) ) + + def getLevelRowCol( self, level ): + w, h = self.getLevelDimensions( level ) + return ( math.ceil( w / self.tile_size ), math.ceil( h / self.tile_size ) ) + + def getTileBox( self, level, column, row ): + """ return a bounding box (x1,y1,x2,y2)""" + # find start position for current tile + + # python's ternary operator doesn't like zero as true condition result + # ie. True and 0 or 1 -> returns 1 + if not column: + px = 0 + else: + px = self.tile_size * column - self.overlap + if not row: + py = 0 + else: + py = self.tile_size * row - self.overlap + + # scaled dimensions for this level + dsw, dsh = self.getLevelDimensions( level ) + + # find the dimension of the tile, adjust for no overlap data on top and left edges + sx = self.tile_size + ( column == 0 and 1 or 2 ) * self.overlap + sy = self.tile_size + ( row == 0 and 1 or 2 ) * self.overlap + + # adjust size for single-tile levels where the image size is smaller + # than the regular tile size, and for tiles on the bottom and right + # edges that would exceed the image bounds + sx = min( sx, dsw-px ) + sy = min( sy, dsh-py ) + + return px, py, px+sx, py+sy + + def getLevelImage( self, level ): + + w, h = self.getLevelDimensions( level ) + w, h = int(w), int(h) + + # don't transform to what we already have + if self.width == w and self.height == h: + return self.image + + if not self.filter: + return self.image.resize( (w,h) ) + return self.image.resize( (w,h), self.filter) + + + def iterTiles( self, level ): + col, row = self.getLevelRowCol( level ) + for w in range( 0, int( col ) ): + for h in range( 0, int( row ) ): + yield (w,h), ( self.getTileBox( level, w, h ) ) + + def __len__( self ): + return self.levels + + def save( self, parent_directory, name ): + dir_path = ensure( os.path.join( ensure( expand( parent_directory ) ), "%s_files"%name ) ) + + # store images + for n in range( self.levels + 1 ): + level_dir = ensure( os.path.join( dir_path, str( n ) ) ) + level_image = self.getLevelImage( n ) + for ( col, row), box in self.iterTiles( n ): + tile = level_image.crop( map(int, box) ) + tile_path = os.path.join( level_dir, "%s_%s.%s"%( col, row, self.format ) ) + tile_file = open( tile_path, 'wb+') + tile.save( tile_file ) + + # store dzi file + fh = open( os.path.join( parent_directory, "%s.dzi"%(name)), 'w+' ) + fh.write( xml_template%( self.__dict__ ) ) + fh.close() + + def info( self ): + for n in range( self.levels +1 ): + print "Level", n, self.getLevelDimensions( n ), self.getLevelScale( n ), self.getLevelRowCol( n ) + for (col, row ), box in self.iterTiles( n ): + if n > self.levels*.75 and n < self.levels*.95: + print " ", "%s/%s_%s"%(n, col, row ), box + +def expand( d): + return os.path.abspath( os.path.expanduser( os.path.expandvars( d ) ) ) + +def ensure( d ): + if not os.path.exists( d ): + os.mkdir( d ) + return d + +def main( ): + parser = optparse.OptionParser(usage = "usage: %prog [options] filename") + parser.add_option('-s', '--tile-size', dest = "size", type="int", + default=256, help = 'The tile height/width') + parser.add_option('-q', '--quality', dest="quality", type="int", + help = 'Set the quality level of the image') + parser.add_option('-f', '--format', dest="format", + default="jpg", help = 'Set the Image Format (jpg or png)') + parser.add_option('-n', '--name', dest="name", help = 'Set the name of the output directory/dzi') + parser.add_option('-p', '--path', dest="path", help = 'Set the path of the output directory/dzi') + parser.add_option('-t', '--transform', dest="transform", default="antialias", + help = 'Type of Transform (bicubic, nearest, antialias, bilinear') + parser.add_option('-d', '--debug', dest="debug", action="store_true", default=False, + help = 'Output debug information relating to box makeup') + + (options, args ) = parser.parse_args() + + if not args: + parser.print_help() + sys.exit(1) + image_path = expand( args[0] ) + + if not os.path.exists( image_path ): + print "Invalid File", image_path + sys.exit(1) + + if not options.name: + options.name = os.path.splitext( os.path.basename( image_path ) )[0] + if not options.path: + options.path = os.path.dirname( image_path ) + if options.transform and options.transform in filter_map: + options.transform = filter_map[ options.transform ] + + img = Image.open( image_path ) + composer = PyramidComposer( img, tile_size=options.size, format=options.format, filter=options.transform ) + + if options.debug: + composer.info() + sys.exit() + + composer.save( options.path, options.name ) + +if __name__ == '__main__': + main() + |