summaryrefslogtreecommitdiff
path: root/seadragon.py
diff options
context:
space:
mode:
Diffstat (limited to 'seadragon.py')
-rw-r--r--seadragon.py235
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()
+