Adding Tiling to Your Application
Large image tiling results from the interaction between an IDLgrImage object, an IDLgrView object, and a destination object (IDLgrWindow, IDLgrClipboard, IDLgrBuffer, or IDLgrPrinter). The destination and view objects are key in determining what data the image object should contain. Each destination object has a QueryRequiredTiles method that determines the visible data based on the view and zoom level, and returns information about the visible image tiles. This information and image data are then passed to the image object SetTileData method. Initially, however, an image that supports tiling does not contain data.
To create an image that supports tiling, you must minimally set two IDLgrImage object properties:
You can also define how tiles from image levels in an image pyramid are accessed using the TILE_LEVEL_MODE mode property. Set it to 1 (automatic mode) to have IDL automatically request the proper tile level based on the zoom level. This is useful when you have an image pyramid and want to use lower resolution images when zooming out.
Note
You should not set TILE_LEVEL_MODE to automatic unless you have an image pyramid. Otherwise IDL will request non-existent lower-resolution data.
The default TILE_LEVEL_MODE value is zero (manual mode), meaning your application must specify which level should be used (where TILE_CURRENT_LEVEL defines that tile level). QueryRequiredTiles will always request tiles at this level and the image will always render using this level. This is useful if you will be panning the image without zooming. If your application does allow zooming, it is best to create an image pyramid so that you can take advantage of the memory savings afforded by displaying lower resolution images when the view is zoomed out.
Even after you have set the necessary image properties that enable tiling, the image still does not contain data. If you attempt to draw the image at this point, it will be the color of the TILE_COLOR property. You must call the QueryRequiredTiles method on the destination object (a window, printer, buffer, or clipboard object) to determine what portion of the image needs to be drawn.
Note
The following sections provide general information and code examples using tiling elements in IDL. For a complete, working example, see Example: JPEG2000 Files for Tiling.
Querying Required Tiles
The QueryRequiredTiles method requires references to a view object and an image object. It returns an array of structures (one for each required tile) that contains information about the tile data needed to fill the view. Once this information has been passed to the IDLgrImage object SetTileData method, call the destination object's Draw method to display the tiled image data.
For example, suppose your application displays a region of a large image (20,000 by 20,000 pixels at full resolution, where one image pixel maps to one screen pixel). Your application window is 800 by 800 pixels, which means that only this much of the image is visible at any one time. To enable tiling in this instance, create the IDLgrWindow object and then create the IDLgrImage object that supports tiling as follows:
oImage = OBJ_NEW('IDLgrImage', TILING=1, $
TILED_IMAGE_DIMENSIONS=[20000,20000], $
TILE_LEVEL_MODE=0)
Setting TILING=1 denotes this image will contain tile data, and TILED_IMAGE_DIMENSIONS defines the size of the full resolution image. The TILE_LEVEL_MODE=0 indicates manual level control (by default, the full resolution, level 0 image is always displayed). Not setting the TILE_DIMENSIONS accepts the default tile size, 1024 by 1024 pixels.
Initialize the IDLgrView object so the lower-left corner of the image is displayed. Where windowDims = [800,800], configure the viewplane rectangle as follows:
Create a IDLgrModel object and add the image. After you add this model to the view, you can call QueryRequiredTiles to determine which tiles are visible in the view and need data as follows:
ReqTiles is an nTiles element array of named structures describing the tiles required. See "IDLgrWindow::QueryRequiredTiles" (IDL Reference Guide) for information on the fields in this structure. The destination objects that support tiling share this method and named structure. Your application will need to iterate through this array, extracting the tile data from the image data and passing it to IDLgrImage::SetTileData.
For a TIFF image (largeimage.tif), you can use the READ_TIFF routine's SUB_RECT keyword to extract the tile data from the image as follows:
FOR i = 0, nTiles - 1 DO BEGIN SubRect = [ReqTiles[i].X, ReqTiles[i].Y, $ ReqTiles[i].Width, ReqTiles[i].Height] TileData = READ_TIFF('largeimage.tif', SUB_RECT=SubRect) TileData = BYTSCL(TileData, MIN=0, MAX=1024) oImage->SetTileData, ReqTiles[i], TileData ENDFOR
For a JPEG2000 image (oJP2File) with the same SubRect variable as that defined in the previous example, you can use the IDLffJPEG2000::GetData method's REGION keyword to extract the data as follows:
; Load the data. TileData = oJP2File->GetData(REGION=SubRect) oImage->SetTileData, ReqTiles[i], TileData
When the destination object's Draw method is called, the display will contain the correct portion of the image since the data associated with the visible tiles has been loaded.
Note
You do not need to pass only a single tile to SetTileData. You can pass a row of tiles or load tiles without a prior call to QueryRequiredTiles (tile caching). See Preloading Tiles for details.
Panning Tiled Images
To pan an image, you can query and assign the tile data without regard for image level as shown in the previous section, Querying Required Tiles. Panning is accomplished by changing the x and y elements of the view object's VIEWPLANE_RECT property, where [x, y, width, height] describe the visible view area. After changing the VIEWPLANE_RECT, call the window object's QueryRequiredTiles method to determine if new data is required. If so, load the data as before. The following code shows an example of modifying the viewplane for panning:
oView->GetProperty, VIEWPLANE_RECT=vp ; Panning. This is done by changing the position of the ; VIEWPLANE_RECT (vp) which is described by [x,y,width,height] ; where x and y are the lower-left corner. How far to move it ; is computed from the distance of the mouse from the center of ; the window (xDelta,yDelta) and the 'zoom factor', ; (vp[2] / windowDims[0]), which is the viewplane ; width divided by the window x dimension. The farther ; the cursor is from the center of the window, the faster the ; view pans. factor = (vp[2] / windowDims[0]) vp[0] += xDelta * factor vp[1] += yDelta * factor (*pState).oView->SetProperty, VIEWPLANE_RECT=vp
See Example: JPEG2000 Files for Tiling for information on where to locate the full tiling example.
Zooming Tiled Images
As with panning, you can use the view object's VIEWPLANE_RECT to zoom (by changing the width and height elements). However, care must be taken when zooming out as many tiles of high-resolution data may need to be loaded, which could exhaust the tile cache. It is best to enable zooming for very large images only when you have an image pyramid of lower resolution images.
When you zoom out to view more of an image, multiple image pixels are mapped to a single pixel on the screen. When dealing with large tiled images, you can take advantage of this situation by displaying a series of lower resolution images (an image pyramid), which uses memory more efficiently. There is no need to use the full resolution image. For example, say you have an image that is 20,000 by 20,000 pixels and over 300 MB. Without an image pyramid, if you zoom out so that the entire image is visible in an 800 by 800 pixel view, the entire image (381 MB) will be loaded into memory. While this might be possible, it isn't efficient. With an image pyramid, you could easily display a lower resolution image that fit the window size. This image would likely be less than one MB in size, would easily fit into memory, and would still be of a sufficient resolution for identifying general areas of interest.
Note
Unless the image file format automatically includes an image pyramid (such as JPEG2000 files), you will need to either create a JPEG2000 file that contains your image data or create an image pyramid manually. See Image Pyramids for details.
The following code shows how to modify the viewplane rectangle associated with the view object to support zooming:
; Zooming. This is done by changing the position and dimensions ; of the VIEWPLANE_RECT (vp), is described by [x,y,width,height] ; where x and y are the lower-left corner. When zooming in, a ; smaller portion of the total image is displayed in the viewplane ; rectangle, which is reflected in smaller vp width and height ; values. The rectangle size is computed from: ; factor - the vp width divided by the window x dimension. ; delta - yDelta (the absolute vertical change from the ; center of the image times the factor. The ; further the mouse cursor is from the center, ; the faster the zoom. ; aspect - the window y dimension divided by the x dimension. factor = (vp[2] / windowDims[0]) delta = yDelta * factor aspect = float(windowDims[1]) / windowDims[0] vp[0] += delta/2 vp[1] += delta * aspect /2 vp[2] -= delta vp[3] -= delta * aspect oView->SetProperty, VIEWPLANE_RECT=vp zoom = windowDims[0] / vp[2]
See Example: JPEG2000 Files for Tiling for information on where to locate the full tiling example. See Using Image LEVEL When Zooming for information on how to request tile data based on zoom level.
Calculating the Number of Image Pyramid Levels
When you have an image pyramid, you will want to set the IDLgrImage TILE_LEVEL_MODE property to 1 (automatic). Doing so causes TILE_NUM_LEVELS to automatically calculate the number of levels needed unless you set a different value. IDL requests levels up to TILE_NUM_LEVELS - 1. This property is based on the original (level 0) image size and the tile size such that the lowest resolution image is just slightly smaller than the tile size. See "TILE_NUM_LEVELS" (IDL Reference Guide) for an example.
To figure out how many levels are needed, create an image object with dimensions equal to the dimensions of your image, the tile mode to automatic, and tiling equal to 1. For example, for the 20,000 by 20,000 image, with the default tile size (1024 by 1024 pixels), create an image object as follows:
oImage = OBJ_NEW('IDLgrImage', $
TILED_IMAGE_DIMENSIONS=[20000,20000], $
TILING=1, TILE_LEVEL_MODE=1)
Here TILE_LEVEL_MODE is set to 1 (automatic) so the level requested by the destination object's QueryRequiredTiles method is calculated automatically from the view information. Return the number of levels that are needed in an image pyramid as follows:
The nLevels variable contains the number of levels IDL will request. You will need nLevels - 1 levels in your pyramid since level 0 is the full resolution image.
Using Image LEVEL When Zooming
In an application that has an image pyramid and supports zooming, you will use information returned by QueryRequiredTiles to load different resolution image tiles. As in the basic query example (Querying Required Tiles), create and initialize the view so the lower-left corner of the image is initially displayed:
Again, create an IDLgrModel, and add the image. Once the model has been added to the view (not shown), call QueryRequiredTiles to determine which tiles are visible and need data.
Rather than reading from the original, full-resolution image, determine which image to use based on the LEVEL field of the returned structure contained in ReqTiles. If you have created an image pyramid for TIFF images, consider using the following naming scheme to return the correct resolution image based on the LEVEL field:
filenames = strarr(6) filenames[0] = 'largeimage.tif' ; Full-resolution image filenames[1] = 'largeimage1.tif' ; Half-resolution image filenames[2] = 'largeimage2.tif' ; Quarter-resolution image filenames[3] = 'largeimage3.tif' ; Eighth-resolution image filenames[4] = 'largeimage4.tif' ; 1/16-resolution image filenames[5] = 'largeimage5.tif' ; 1/32-resolution image
You can then request the correct image level (level) and set tile data as follows:
FOR i = 0, nTiles - 1 DO BEGIN SubRect = [ReqTiles[i].X, ReqTiles[i].Y, $ ReqTiles[i].Width, ReqTiles[i].Height] level = ReqTiles[i].Level TileData = READ_TIFF(filenames[Level], SUB_RECT=SubRect) TileData = BYTSCL(TileData, MIN=0, MAX=1024) oImage->SetTileData, ReqTiles[i], TileData ENDFOR
For a JPEG2000 image (oJP2File), you can use the IDLffJPEG2000::GetData method's DISCARD_LEVELS keyword to return the correct image level as follows:
FOR i = 0, nTiles - 1 DO BEGIN SubRect = [ReqTiles[i].x, ReqTiles[i].y, $ ReqTiles[i].width, ReqTiles[i].height] ; Convert to JPEG2000 canvas coords. level = ReqTiles[i].level Scale = ISHFT(1, level) SubRect = SubRect * Scale ; Load the data. TileData = oJP2File->GetData(REGION=SubRect, $ DISCARD_LEVELS=level, ORDER=1) oImage->SetTileData, ReqTiles[i], TileData ENDFOR
An image that supports TILE_LEVEL_MODE=1 (automatic) can be panned and zoomed using VIEWPLANE_RECT and the above QueryRequiredTiles and SetTileData combination. This determines tile visibility and loads the appropriate data. As the image is zoomed out, lower resolution data will be automatically requested to ensure physical memory does not run out.
Note
See Example: JPEG2000 Files for Tiling for the complete JPEG2000 tiling example.
Copying and Printing a Tiled Image
The IDLgrClipboard and IDLgrPrinter objects have a QueryRequiredTiles method just like IDLgrWindow. Return the visible tiles using QueryRequiredTiles, set the data on the image object, and use the Draw method of the printer or clipboard object to output the portion of the tiles that are visible in the view. This is all that is required for a clipboard object. For a printer object, you need to take the view dimensions into account when printing. The following code excerpt shows this for the object, oPrinter:
; Set the dimensions of the view so the aspect ratio is ; correct when printed. windowAspect = FLOAT(windowDims[0]) / windowDims[1]) oPrinter->GetProperty, DIMENSIONS = pageSize pageSize[1] = pageSize[0] / windowAspect oView->SetProperty, DIMENSIONS=pageSize
Call QueryRequiredTiles on the printer object and set the tile data using SetTileData on the image object (as described in Querying Required Tiles). It is then simple to print the output:
;...PRINT!... oPrinter->Draw, oView, VECTOR=0 oPrinter->NewDocumentNote
Clipboard and printer vector output (VECTOR=1) is not supported for tiled images.
An example in the IDL distribution provides working examples of copying and printing a tiled image. See Example: JPEG2000 Files for Tiling for information on where to locate the full tiling example.
Preloading Tiles
You can load more tiles of data than what are currently visible in a view in a couple of ways:
- Pass a row of tile data to SetTileData based on an initial query (see Loading a Row of Tiles below)
- Pass data to SetTileData without a query (see Caching Non-Visible Tiles)
Loading a Row of Tiles
SetTileData can accept more than a single tile's worth of data in one call. In some cases, it can be more efficient to read an entire row of tiles rather than extract single tiles from that row. In general, raw binary image formats (such as TIFF) that are not stored on disk in a blocked manner can be tiled more efficiently by passing rows of data. The following code shows how to load entire scanlines at once when using these image formats.
As in the example shown in Querying Required Tiles, which creates view and image objects, call the destination object's QueryRequiredTiles method to determine what tile data is initially visible in the viewport as follows:
; Return structure information for visible tiles. ReqTiles = oWindow->QueryRequiredTiles(oView, oImage, $ COUNT=nTiles) ; While there are tiles, determine the width of the information to ; be requested by dividing the width of the original image by the ; current level. WHILE (nTiles gt 0) DO BEGIN TileInfo = ReqTiles[0] level = TileInfo.Level width = imageDims[0] / (2 ^ level) ; Set the area to be read (equal to image width) to the SubRect ; variable. SubRect = [0, TileInfo.Y, width, TileInfo.Height] ; Insert code here to read the tile data, passing SubRect to the ; correct data access procedure for your file type. ; ... ; Update the tile structure. TileInfo.X = 0L TileInfo.Width = width ; Set the row of tile data to the image. oImage->SetTileData, TileInfo, TileData ReqTiles = oWindow->QueryRequiredTiles(oView, oImage, $ COUNT=nTiles) ENDWHILE
SubRect is set such that the entire width of the image is read at the requested level for the given vertical position and height. The tile structure is updated to reflect the fact that the information being passed to SetTileData starts at X=0 and is the entire width of the image. Notice that rather than iterate through the entire ReqTiles array the code calls QueryRequiredTiles again after calling SetTileData since the remaining tiles in the array can now be loaded.
Caching Non-Visible Tiles
You do not need to call QueryRequiredTiles before passing data to the SetTileData method. The QueryRequiredTiles call just limits the requested data to those tiles that are visible in the view. Setting tile data without first requesting it has a couple of important uses: loading an entire level and predictive tile caching.
When an application starts, it can automatically load an entire level of low-resolution tile data. If higher resolution data is requested but not currently available, the lower resolution tiles are used. For example, if level 3 tile data has been loaded, but you attempt to zoom in so that the level 0 data is needed, level 3 data will continue to be displayed until the higher resolution data can be loaded. This results in a blurred or blocky version of the image, which can still be used until the required level has been loaded.
To load an entire level (assumed to be level 3, 2500 by 2500 pixels in this example), you first need to request that level of data from your image pyramid. How you access this data depends on the file type. For example, if you have created a series of TIFF files, access the image data using the file name:
If you have created a JPEG2000 image, access the image data using the GetData method where level should be set to the level of data you want to return (e.g., 3):
Create a tile structure that encompasses the entire level and pass the data to SetTileData:
tile = { IDLIMAGETILE, X:0, Y:0, Width:2500, Height:2500, $
Level:3, Dest:oWindow }
oImage->SetTileData, tile, TileData
The second use for preloading tiles is predictive tile loading. For example, if the user is panning right, but tiles to the right of the view that are not yet visible, these tiles can be preloaded if there is any idle time. Then when the view reaches those tiles, there will be no interruption as the tiles have already been loaded.