Processing Large Images Using Pipelines

Graphics Mill incorporates an image processing approach named "pipeline", originally introduced in version 6. Pipelines allow reading and writing image files and processing them without the necessity to load a whole bitmap into memory. This essentially means that you can process large images without getting out-of-memory errors, even on an x86 platform.

This topic discusses the benefits pipelines offer when measured against a conventional image processing approach - keeping a full sized bitmap in RAM. It also demonstrates how pipelines can be used in a real life.

Pipeline Benefits

Memory-friendly

  • Within a pipeline, an image is processed stripe by stripe - a portion of image data having the same width as the original and height selected by Graphics Mill for optimal processing.
  • Temporary bitmap data may be dumped to the hard drive if it is necessary.

Easy to program

  • Descriptive image processing sequence, meaning that image data flows through the pipeline from one transformation to another, so each next element receives the result of the previous processing.
  • Ability to build branched pipelines. This is an approach whereby the transformation result can be passed to an arbitrary number of receivers and each of them can process the received data in its own way. For example, you can open an image and save it to JPEG and PNG files, or produce multiple thumbnails of different size at one pass. See the example below for details.

Examples

Common Usage

In a common case, to process an image using pipeline you should perform the following steps:

  1. Create the Pipeline.
  2. Construct the pipeline. Here you can use a variety of building blocks (descendants of the PipelineElement class) which can be divided into the following groups:

  3. Run the pipeline by calling the Pipeline.Run() method.

This code sample reads an image from JPEG file, crops a square from its center, adjusts brightness, and writes the result to another file.

C#
using (var reader = ImageReader.Create(@"Images\in.jpg"))
{
    var rect = new System.Drawing.Rectangle()
    {
        X = Math.Max((reader.Width - reader.Height) / 2, 0),
        Y = Math.Max((reader.Height - reader.Width) / 2, 0),
        Width = Math.Min(reader.Width, reader.Height),
        Height = Math.Min(reader.Width, reader.Height)
    };

    using (var crop = new Crop(rect))
    using (var brightness = new Brightness())
    using (var writer = ImageWriter.Create(@"Images\Output\out.jpg"))
    {
        var pipeline = new Pipeline();
        pipeline.Add(reader);
        pipeline.Add(crop);
        pipeline.Add(brightness);
        pipeline.Add(writer);

        pipeline.Run();
    }
}

Simplified Syntax

To utilize pipelines in a more convenient way you may use the addition (+) operator and a static Pipeline.Run(Pipeline) method which allows producing and running an arbitrary length pipeline in a single line of code. The following code demonstrates how to use this simplified syntax to implement the same functionality as the snippet above.

C#
using (var reader = ImageReader.Create(@"Images\in.jpg"))
{
    var rect = new System.Drawing.Rectangle()
    {
        X = Math.Max((reader.Width - reader.Height) / 2, 0),
        Y = Math.Max((reader.Height - reader.Width) / 2, 0),
        Width = Math.Min(reader.Width, reader.Height),
        Height = Math.Min(reader.Width, reader.Height)
    };

    using (var crop = new Crop(rect))
    using (var brightness = new Brightness())
    using (var writer = ImageWriter.Create(@"Images\Output\out.jpg"))
    {
        Pipeline.Run(reader + crop + brightness + writer);
    }
}

Branched Pipelines

As mentioned above, pipelines support branched image processing. To branch a pipeline you need to use the PipelineElement.Receivers property which represents a collection of pipeline elements that will receive the result of the current. In a common case, this collection consists of a single element - the next one in the pipeline. So to pass the result of some element to several pipelines, you need to add these pipelines to the element's Receivers collection. In the code, each pipeline branch should be constructed and built separately and then added to the collections of receivers. After you are done with a pipeline don't forget to dispose of its elements and make sure that each branch is disposed as well. See the Disposing Pipeline Elements article for additional details.

The following code shows how to create two 128x128 and 2048x2048 thumbnails for a single image. Here we branch the first pipeline element - ImageReader, and pass its result to two pipelines. Each of them consists of its own Resize transform and ImageWriter.

C#
using (var reader = ImageReader.Create(@"Images\in.jpg"))
{
    var resizeBig = new Pipeline()
    { 
        new Resize(2048, 0), 
        ImageWriter.Create(@"Images\Output\out_2048.jpg")
    };
    var resizeSmall = new Pipeline()
    { 
        new Resize(128, 0),
        ImageWriter.Create(@"Images\Output\out_128.jpg")
    };

    reader.Receivers.Add(resizeBig.Build());
    reader.Receivers.Add(resizeSmall.Build());

    Pipeline.Run(reader);

    resizeBig.DisposeAllElements();
    resizeSmall.DisposeAllElements();
}

See Also

Reference

Manual