Because this article focuses only on one step of a larger
process, yet uses code created for the larger process, it is challenging to
give code examples that do not intimate elements of the larger process.
Therefore, before diving in, perhaps a high level overview will help with any
abstractions.
1.
Output. Yes, output first. Because we are
dealing with high-res, memory consuming images, we must put these images to
rest as soon as they are processed. Output, therefore occurs throughout the
process.
2.
Input. Iterate through collection of
images, and process individually.
·
Retrieve Images. Obvious, yes, but none-the-less complicated.
This code was created for a real-world implementation, so the implementation
has to be somewhat more professional then the generic 'hello world' example
that seems to prevail these types of articles. The code does not assume the
source of the image; it allows for a custom provider that must only implement a
basic reader interface.
·
[Abstract] Compose the images for
display. Not so obvious! Remember, the point is display multiple high res
images on a viewer; at some point, we need to determine the position and
scaling of these images in the viewer. Because we are doing this 'on the fly',
our compositions have to be pre-calculated early in the process, and often
generic. The code in the download, for example, creates 100 pixel width
thumbnails, positioned horizontally with a 10 pixel gap.
3. Process
·
For each image, create the image pyramid and output to the file system.
For simplicity, it is fortunate that DeepZoom can only access these artifacts
if they are local; therefore we can assume that the output will ultimately be
the local file system.
·
[Abstract] At the completion of each
image, the process will register its output with a DZ file manager that will
ultimately write the necessary DZ file manifests at the end of the process,
thus creating the collection.
·
[Abstract] After all images are
processed into pyramids, the process will create a composition pyramid, and
write the final DZ file manifest that is ultimately fed to the Viewer.
Output
DeepZoom can only access source files from a local file
system, and those files, of course, must adhere to a defined structure.
Abstracting the implementation, this process uses a class that implements the
following contract. This class can be extended to customize the root folder
for the output, without allowing for corruption of the underlying folder
structure as needed by DeepZoom.
public interface IDZFileWriter
{
void WritePyramidImage(IDZImage image, int layer, int column, int row);
void WriteImageDZFile(IDZImage image);
//remaining methods left out intentionally
}
The WritePyramidImage method will write the scaled image to
the appropriate folder, using the supplied column and row for the file naming
convention.
The WriteImageDZfile will create the DZ file that instructs
DeepZoom where, and at what scale to render the associated image in the viewer.
Input
Abstracting the source of the images, this example uses and
interface for which the implementation can be customized to draw its images
from a local file system, a web service, or any other imaginable source.
public interface IDZImageReader
{
System.Collections.Generic.IEnumerable<IDZImage> Images(IDZComposer composer);
int ImageCount { get; }
}
The implementation is responsible for the underlying
mechanics of reading the image from its defined source, processing each image
through a custom composer, then returning them iteratively to the caller as a
custom object which contains both the image as well as elements that indicate
where the image will reside in the composition (as calculated by the
composer). For this article, it is necessary only to know that the image is
returned in a System.Drawing.Image class. Coincidentally, DeepZoom will only
support image formats that the Image class can support; another fortunate
simplification. At this point, the process has no knowledge of the source of
the image.
Process
For each image in the collection, create a pyramid. It is
very useful to know that this process will yield files that amount to in size
approximately 1.33 percent of the original file. The main method takes in an
implementation of IDZImage, and of IDZFileWriter which is responsible for
managing the output.
public void ProcessImagePyramid(IDZImage image, IDZFileWriter writer)
{
On a personal note, I believe that all variables used in an
example should be visible, so that the reader does not have to assume.
//number of pyramid layers, using 256x256 tiles
int numberOfLevels;
int currentLevel;
int columns, rows, column, row;
int tileWidth, tileHeight;
int imageWidth, imageHeight;
int x, y;
int padLeft = 0, padRight = 0, padTop = 0, padBottom = 0;
Rectangle coords;
Bitmap tile;
Bitmap bm256 = null;
1.
Determine the number of levels in the pyramid.
This can be determined using the Log base 2 of the larger of the image's width
and height. Therefore, a factor between 257 and 512 would yield 9 levels; (129
and 256) à 8 levels; (65 and 128) à 7 levels… and so on. The 0'th level is
always the image scaled to 1 pixel. Obviously, we would not do this for an
image that is only 512 pixels.
numberOfLevels = (int)Math.Ceiling(Math.Log(Math.Max
(image.Image.Width, image.Image.Height), 2));
2.
Write the DZ file that instructs DeepZoom
how and where to render the image on the viewer. Remember, although
abstracted, the IDZImage that is passed includes the coordinates and size of
the image as it should be rendered.
writer.WriteImageDZFile(image);
/* process each level of pyramid
*/
for (currentLevel = numberOfLevels; currentLevel >= 0; currentLevel--)
{
imageWidth = image.Image.Width;
imageHeight = image.Image.Height;
3.
Tile each layer. If the image at the
current level is larger than 256 pixels, we need to chop it up into max 256x256
tiles, with a 1 pixel overlap. Interestingly, this algorithm is slightly
different from the one used in the current version of DeepZoom composer.
Including the overlap, this algorithm will not yield any tile larger than 256
width or height, whereas Microsoft's algorithm will yield tiles that are 258
pixels in width or height. This modification was made on the advice of Ben
Vanik (www.noxa.org), who asserts that this
is more efficient. Both will run, and I am inclined to take his advice.
Furthermore, I understand that Microsoft is aware of the inefficiency and has
addressed it in the next version. The TILESIZE constant is defined elsewhere
to equal 256, OVERLAP = 1.
if ((image.Image.Width <= (TILESIZE + 2)) &&
(image.Image.Height <= (TILESIZE + 2))
)
{
if (bm256 == null) bm256 = image.Image;
writer.WritePyramidImage(image, currentLevel, 0, 0);
}
else
{
/* Chop it up into max(256x256) tiles, overlapping by the value
* of OVERLAP constant.
*/
decimal tileSize = (decimal)(TILESIZE);
4.
Process each tile, column then row, using a 1 pixel overlap.
columns = (int)Math.Ceiling(image.Image.Width / tileSize);
rows = (int)Math.Ceiling(image.Image.Height / tileSize);
for (column = 0; column < columns; column++)
{
padLeft = (column == 0) ? 0 : 1;
padRight = (column == (columns - 1)) ? 0 : 1;
x = (column * TILESIZE) - column;
for (row = 0; row < rows; row++)
{
padTop = (row == 0) ? 0 : 1;
padBottom = (row == (rows - 1)) ? 0 : 1;
y = (row * TILESIZE) - row;
//if the remaining column is less than 256,
//return the difference
tileWidth = ((imageWidth - x) < TILESIZE)
? (imageWidth - x)
: TILESIZE;
tileHeight = ((imageHeight - y) < TILESIZE)
? (imageHeight - y)
: TILESIZE;
coords = new Rectangle(x, y,
tileWidth + padLeft + padRight,
tileHeight + padTop + padBottom);
tile = new Bitmap(tileWidth + padLeft + padRight,
tileHeight + padTop + padBottom);
Graphics.FromImage(tile).DrawImage(
image.Image,
new Rectangle(0, 0, tileWidth + padLeft + padRight,
tileHeight + padTop + padBottom),
coords, GraphicsUnit.Pixel);
DZImage tileImage = new DZImage(image.Name, tile);
5.
Write each file to disk as soon as it is created, therefore not holding
it in memory.
writer.WritePyramidImage(tileImage, currentLevel,
column, row);
}
}
}
if (currentLevel >= 0)
{
//again, thanx to Ben Vanik....
imageHeight = (int)Math.Ceiling((double)imageHeight / 2);
imageWidth = (int)Math.Ceiling((double)imageWidth / 2);
tile = new Bitmap(imageWidth, imageHeight);
Graphics.FromImage(tile).DrawImage(
image.Image,
new Rectangle(0, 0, imageWidth, imageHeight),
new Rectangle(0, 0, image.Image.Width, image.Image.Height),
GraphicsUnit.Pixel);
image.Image = tile;
}
}
image.Image = bm256;
}