(X) Hide this
    • Login
    • Join
      • Say No Bots Generate New Image
        By clicking 'Register' you accept the terms of use .

Deep zooming on the fly

(5 votes)
Alexey Zakharov
>
Alexey Zakharov
Joined Sep 25, 2008
Articles:   8
Comments:   5
More Articles
16 comments   /   posted on Jun 15, 2009
Categories:   General , Media

This article is compatible with the latest version of Silverlight.

1. Introduction

The use of Deep Zoom Composer for creating deep zoom source files is often inconvenient.

For example, one guy has his online shop with thousands of high quality product images and after the release of Silverlight Deep Zoom technology he decided to use it. Surely he can’t decompose his images using Deep Zoom Composer, because due to the amount of existing site images it would take too much time.

And this is not the only case when you want to dynamically generate multi scale image tile source for high quality images without any other preparations.

In this article I’m going to show you one approach, which will let you solve this problem using ASP.NET http handler and a custom deep zoom tile source.

Download source code

2. Content

2.1 Solution overview

If you are familiar with ASP.NET you may have already written  http handler for dynamic image resizing. It helps to reduce site traffic, caused by images with high quality. A common solution of that problem is to add a http handler on URIs which refers to files with jpg, gif or png extension. The http handler gets image stream,  resizes it using .NET drawing api and puts the resized image to the output stream. The required size of the image is supplied via query string parameter.

Example: http://mysite.com/images/foo.jpg?width=200&height=300

The same approach I am going to use in my solution. But instead of supplying required image size, I will supply tile level, tile horizontal position, tile vertical position, tile width and tile height parameters which are needed to generate deep zoom tile. So my handler will process URIs like this:

http://mysite.com/images/foo.jpg?tileLevel=0&tilePositionX=0&tilePositionY=0&tileWidth=127&tileHeight=127

Also I will save generated tiles on the disc, which exclude any charges caused by the image decomposition process for further requests.

So we need to create two main classes:

  1. ASP.NET Http handler, which generates deep zoom tiles for specified URI.
  2. Custom deep zoom tile source, which will perform parameterized request to our http handler according to specified image URI.

Let’s start with the http handler.

2.2 Http handler

To create a http handler in asp.net you should implement IHttpHandler interface:

  1. ProcessRequest method -  receives http context object.
  2. IsReusable – indicates whether another request can use the http handler. We will set this property to true, so this class would be used as singleton which is our reason to lock critical sections of our code to avoid threading issues.

First of all we should receive the requested image Uri and tile parameters:

 NameValueCollection queryString = context.Request.QueryString;
 int tileLevel = int.Parse(queryString["tileLevel"]);
 int tilePositionX = int.Parse(queryString["tilePositionX"]);
 int tilePositionY = int.Parse(queryString["tilePositionY"]);
 int tileHeight = int.Parse(queryString["tileHeight"]);
 int tileWidth = int.Parse(queryString["tileWidth"]);
   
 Uri requestedFileUrl = context.Request.Url;

To cache generated deep zoom tiles on the disc I will use the following directory structure:

{ Temp folder }/{ Image name }/{ Tile level }/{ Tile horizontal position }_{ Tile vertical position }.jpg.

You may also store generated tiles in the database. It would be very useful if you are storing the original images in the database too. In this case you can create relation between the table of tiles and the table of original images, which will allow you to create cascade deletion rule to clean up tiles after the deleting of the original image. But for simplicity reasons I won’t use this approach in this article.

After receiving the request, we should first of all check if an image has been already cached by checking existence of the folder with specified image name:

 private bool IsTilesExists(HttpContext context, string fileName)
 {
     return Directory.Exists(context.Server.MapPath(string.Format("/Temp/{0}", fileName)));
 }

If tiles are already cached on the disc we will put them directly to the output stream.

 string tilePath = context.Server.MapPath(string.Format("/Temp/{0}/{1}/{2}_{3}.jpg", fileName, tileLevel,
                                                         tilePositionX, tilePositionY));
 using (var tileFileStream = new FileStream(tilePath, FileMode.Open))
 {
     using (var memoryStream = new MemoryStream())
     {
         for (int i = 0; i < tileFileStream.Length; ++i)
         {
             int readedByte = tileFileStream.ReadByte();
             memoryStream.WriteByte(Convert.ToByte(readedByte));
         }
  
         memoryStream.WriteTo(context.Response.OutputStream);
     }
 }

If tiles are still not cached we should create them. Creating and scaling a bitmap object have rather big costs that is why I will create all tiles on the first request to the selected image.

Because of the many requests thet will be performed on the same URI at once, we should create a lock object for each image name. All image locks will be stored in a dictionary, which key is an image name. Access to this dictionary must be also thread safe.

 private static readonly Dictionary<string, object> imageLocks = new Dictionary<string, object>();
   
 private static readonly object sync = new object();
  
 private static void RemoveImageLock(string fileName)
 {
     lock (sync)
     {
         imageLocks.Remove(fileName);
     }
 }
   
 private static object GetImageLock(string fileName)
 {
     lock (sync)
     {
         object imageLock;
         if (imageLocks.ContainsKey(fileName))
         {
             imageLock = imageLocks[fileName];
         }
         else
         {
             imageLock = new object();
             imageLocks.Add(fileName, imageLock);
         }
   
         return imageLock;
     }
 }

So we are going to create a lock object related to the image file name before the generation of tiles and will release it after the generation process is completed. We should check the existence of cached tile directory twice, because all concurrent threads think that tiles are not cached and will try to cache it once again after acquiring the lock.

 if (!IsTilesExists(context, fileName))
 {
     lock (GetImageLock(fileName))
     {
         if (!IsTilesExists(context, fileName))
         {
             CreateTiles(context.Request.PhysicalPath,
                        context.Server.MapPath(string.Format("/Temp/{0}", fileName)), tileWidth, tileHeight);
             RemoveImageLock(fileName);
         }
     }
 }

Now it is time to generate image tiles. To do it we should:

1. Loop through all possible tile levels. Max tile level will be reached when the ratio of the original image size to 2^(TileLevel) will be less than 1.

2. For each tile level we should scale the original image and split it into tiles of specified size.

Here is the source code of the methods, which perform these tasks.

 private void CreateTiles(string imagePath, string tileDirectoryPath, int tileWidth, int tileHeight)
 {
     if (!Directory.Exists(tileDirectoryPath))
     {
         Directory.CreateDirectory(tileDirectoryPath);
     }
  
     using (var imageFileStream = new FileStream(imagePath, FileMode.Open))
     {
         using (Image originalImage = Image.FromStream(imageFileStream))
         {
             double tileLevel = 0;
             double scaleRate = originalImage.Width/1;
   
             while (scaleRate > 1)
             {
                 double originalWidth = originalImage.Width;
                 double size = Math.Pow(2, tileLevel);
                 scaleRate = (originalWidth/size) > 1 ? originalWidth/size : 1;
   
                 string tileLevelDirectoryPath = string.Format("{0}/{1}", tileDirectoryPath, tileLevel);
                 if (!Directory.Exists(tileLevelDirectoryPath))
                 {
                     Directory.CreateDirectory(tileLevelDirectoryPath);
                 }
   
                 using (Image scaledImage = ScaleImage(scaleRate, originalImage))
                 {
                     int tilePositionX = 0;
                     for (int i = 0; i < scaledImage.Width; i += tileWidth)
                     {
                         int tilePositionY = 0;
                         for (int j = 0; j < scaledImage.Height; j += tileHeight)
                         {
                             using (Bitmap tileImage = GetTileImage(i, j, tileWidth, tileHeight,
                                                                     scaledImage))
                             {
                                 ImageCodecInfo[] info = ImageCodecInfo.GetImageEncoders();
                                 var encoderParameters = new EncoderParameters(1);
                                 encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, 100L);
                                 string tilePath = string.Format("{0}/{1}_{2}.jpg", tileLevelDirectoryPath,
                                                                 tilePositionX, tilePositionY);
  
                                 using (var tileFileStream = new FileStream(tilePath, FileMode.OpenOrCreate))
                                 {
                                     tileImage.Save(tileFileStream, info[1], encoderParameters);
                                 }
                             }
  
                             tilePositionY++;
                         }
  
                         tilePositionX++;
                     }
  
                     tileLevel++;
                 }
             }
         }
     }
 }
   
 private static Bitmap GetTileImage(
     int tileLeft, int tileTop, int tileWidth, int tileHeight, Image scaledImage)
 {
     var srcRectnagle = new Rectangle(
         tileLeft,
         tileTop,
         tileLeft + tileWidth < scaledImage.Width
             ? tileWidth
             : scaledImage.Width - tileLeft,
         tileTop + tileHeight < scaledImage.Height
             ? tileHeight
             : scaledImage.Height - tileTop);
   
     var destRectangle = new Rectangle(0, 0, srcRectnagle.Width, srcRectnagle.Height);
  
     var tileImage = new Bitmap(destRectangle.Right, destRectangle.Bottom);
     using (Graphics graphic = Graphics.FromImage(tileImage))
     {
         graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
         graphic.SmoothingMode = SmoothingMode.HighQuality;
         graphic.PixelOffsetMode = PixelOffsetMode.HighQuality;
         graphic.CompositingQuality = CompositingQuality.HighQuality;
   
         graphic.DrawImage(scaledImage, destRectangle, srcRectnagle, GraphicsUnit.Pixel);
     }
   
     return tileImage;
 }
   
 private static Image ScaleImage(double scaleRate, Image originalImage)
 {
     var scaleRect = new Rectangle(
         0,
         0,
         Convert.ToInt32(originalImage.Width/scaleRate),
         Convert.ToInt32(originalImage.Height/scaleRate));
   
     Image scaledImage = new Bitmap(scaleRect.Right, scaleRect.Bottom);
     using (Graphics graphic = Graphics.FromImage(scaledImage))
     {
         graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
         graphic.SmoothingMode = SmoothingMode.HighQuality;
         graphic.PixelOffsetMode = PixelOffsetMode.HighQuality;
         graphic.CompositingQuality = CompositingQuality.HighQuality;
   
         graphic.DrawImage(
             originalImage,
             scaleRect,
             new Rectangle(0, 0, originalImage.Width, originalImage.Height),
             GraphicsUnit.Pixel);
     }
   
     return scaledImage;
 }

Finally we should register our http handler in the web config.

If you are using IIS 6.0 or lower you should add the following line to the httphanlders section:

<add verb="GET,HEAD" path="*.jpg" type="Handlers.DynamicDeepZoomHttpHandler"/> 

For IIS 7.0 - the following line to the handlers section:

<add name="DynamicDeepZoomHttpHandler" verb="GET,HEAD" path="*.jpg" type="Handlers.DynamicDeepZoomHttpHandler"/>

2.3 Custom multi scale tile source

To implement your custom multi scale tile source you should inherit from MultiScaleTileSource, override GetTileLayers method and use base constructor.

To the base constructor we will simply add image URI parameter:

 public DynamicDeepZoomSource(int imageWidth, int imageHeight, int tileWidth, int tileHeight, Uri imageUri)
     : base(imageWidth, imageHeight, tileWidth, tileHeight, 0)
 {
     this.tileWidth = tileWidth;
     this.tileHeight = tileHeight;
     this.imageUri = imageUri;
 }

In the GetTileLayers method we'll generate image tile URIs according to the format which we have specified in the http handler:

 protected override void GetTileLayers(
     int tileLevel, int tilePositionX, int tilePositionY, IList<object> tileImageLayerSources)
 {
     string orignalString = imageUri.OriginalString;
     string source =
         string.Format(
            "{0}?tileLevel={1}&tilePositionX={2}&tilePositionY={3}&tileWidth={4}&tileHeight={5}",
            orignalString,
            tileLevel,
            tilePositionX,
            tilePositionY,
            tileWidth,
            tileHeight);
   
     var uri = new Uri(source, UriKind.Absolute);
   
     tileImageLayerSources.Add(uri);
 }

Now you can open the default deep zoom project generated by the Deep Zoom Composer and replace MultiScaleImage Source property with the created DynamicDeepZoomSource.

 msi.Source = new DynamicDeepZoomSource(
                1024, 768, 127, 127, new Uri(Application.Current.Host.Source, @"../Images/marilyn_monroe.jpg"))

3. Summary

I hope this article will help you to integrate deep zoom to your existing web projects. Now you can make all of the existing images on your website zoomable. It will also reduce your overall traffic, because all high quality images are scaled to smaller size according to the default zoom level.

4. Links

Here are some useful links, which will be a good start to learn something more about creating custom multi scale image source and http handlers:

  1. Virtutal earth deep zooming – you can learn more about custom multi scale image source in my article about using deep zoom for browsing virtual earth maps.
  2. 20 Image Resizing Pitfalls – Nathanael Jones describes some issues connected to image resizing in .NET.
  3. Image resizing hanlder – Here Jigar Desai describes how you can resize images using ASP.NET http handler.


Subscribe

Comments

  • lexer

    RE: Deep zooming on the fly


    posted by lexer on Jun 15, 2009 13:31
    I haven't used any Silverlight 3 specific feature in this article. So it would also work with Silverlight 2.
  • -_-

    RE: Deep zooming on the fly


    posted by John on Jun 17, 2009 10:00

    Next step is to do some caching, imagine you had a 2MB image and had 100 people asking for 100x100 tiles, you would load the 2MB image into memory 10,000 times.

    That said this is an awesome way to get a bunch of small images into the MSI control, which is exactly what you said in the intro. Good work!

  • -_-

    RE: Deep zooming on the fly


    posted by krudo meir on Jun 17, 2009 10:23

    Hi,

    links for examples are borken :(
    Great job here!

     

  • -_-

    RE: Deep zooming on the fly


    posted by Alexey Zakharov on Jun 17, 2009 10:57
    >> Next step is to do some caching, imagine you had a 2MB image and had 100 people asking for 100x100 tiles, you would load the 2MB image into memory 10,000 times.

    It already solved. I'm locking image while decomposing, that is why for 100 people it would be done only once.

  • -_-

    RE: Deep zooming on the fly


    posted by Alexey Zakharov on Jun 17, 2009 11:00
    >> links for examples are borken :(

    It is fake links. It is not an example.

  • -_-

    RE: Deep zooming on the fly


    posted by John on Jun 24, 2009 08:59

    How did I miss that! very good.

    Having done something similar in Azure I just wonder how well this will work in the real world. I had serious problems with images that took up more then the 512MB memory allocated - large satellite images from NASA. The benifit of the Azure solution was it was one image per machine. How do you handle running out of memory if you're dealing with large images and many new requests?

    Most deep zoom dynamic tile solutions I've seen proposed are window services listening for new files to be dropped into a folder and then tile them up in advance of the web application. This kind of strategy can also detect when an image is modified and update the tiles accordingly. That said I can see the benifits in having it inside your asp.net application.

  • -_-

    RE: Deep zooming on the fly


    posted by David on Aug 07, 2009 11:08

    Hi John,

    Where have you seen the other dynamic tile solutions?

    Thanks

  • -_-

    RE: Deep zooming on the fly


    posted by Marthinus on Aug 20, 2009 18:38

    Hi there,

    Im working on a Proof of COncept regarding silverlight. I am especially wanting to proof the use of DeepZoom. I want to annotate for instance a rectangle on a particular image within the multi-image group. The problem I currently have is that the animation of the deepzoom, and my resizing and moving of the rectangle gets out of sync and then it just doesnt look that good.

    Please see a primitive example here:
    http://martycode.blogspot.com/2009/06/testing-my-deepzoom-page.html

    Do you have any ideas as to how to fix this. I need to work on it ASAP :) Thanks a lot.

    PS: I was thinking of maybe adding an INK-type overlay on each sub-image - how can I overwrite the sub-images and keep annimation in sync...

    Go well,
    Marthinus

  • -_-

    RE: Deep zooming on the fly


    posted by AE on Aug 24, 2009 18:21
    Is it possible to use this example to deep zoom images not stored locally, but speciyfing their url ? (for example: http://www.ole.clarin.com/diario/2009/08/23/futbollocal/thumb/mdayan_rc1.jpg)

     Thanks and great job!

  • -_-

    RE: Deep zooming on the fly


    posted by AE on Aug 24, 2009 18:27
    Is it possible to use this example to deep zoom images not stored locally, but speciyfing their url ? (for example: http://www.ole.clarin.com/diario/2009/08/23/futbollocal/thumb/mdayan_rc1.jpg)

     Thanks and great job!

  • -_-

    RE: Deep zooming on the fly


    posted by lexer on Aug 24, 2009 21:06
    I think yes.. you should rewrite some code using WebClient which will download external image.
  • -_-

    RE: Deep zooming on the fly


    posted by Adrian Eidelman on Aug 31, 2009 01:09
    Hi Alexey, I've implemented the image url deepzoom taking your suggestion, thanks. One more question: should the tile image size be determined from the image actual size? Or is this independent? Thanks again.
  • -_-

    RE: Deep zooming on the fly


    posted by Juan on Sep 03, 2009 15:40
    Works fine for certain image sizes, but not all are displayed correctly. Why's that?

     Thanks


  • -_-

    RE: Deep zooming on the fly


    posted by Al on Sep 25, 2009 17:15
    Really nice example and exact what I was looking for, great job - thanks!
  • jstaffer

    Re: Deep zooming on the fly


    posted by jstaffer on Nov 15, 2011 10:17

    Great article! One question, how did you determine that the tile size should be 127 by 127 for your sample image? Should the tile size be different for differently sized images?

  • -_-

    Re: Deep zooming on the fly


    posted by on Nov 24, 2011 12:10
    I like http://www.imagesurf.net/ for online deep zoom of photos!

Add Comment

Login to comment:
  *      *