/// <summary>
        /// Image pipeline phase 4: Invoke the user-provided delegate (for example, to display the result in a UI)
        /// </summary>
        static void DisplayPipelinedImages(IEnumerable <ImageInfo> filteredImages,
                                           Action <ImageInfo> displayFn,
                                           Action <ImageInfo> updateStatisticsFn,
                                           CancellationTokenSource cts)
        {
            int       count    = 1;
            int       duration = 0;
            var       token    = cts.Token;
            ImageInfo info     = null;

            try
            {
                foreach (ImageInfo infoTmp in filteredImages)
                {
                    info = infoTmp;
                    if (token.IsCancellationRequested)
                    {
                        break;
                    }
                    int displayStart = Environment.TickCount;
                    updateStatisticsFn(info);
                    DisplayImage(info, count, displayFn, duration);
                    duration = Environment.TickCount - displayStart;

                    count = count + 1;
                    info  = null;
                }
            }
            catch (Exception e)
            {
                cts.Cancel();
                if (!(e is OperationCanceledException))
                {
                    throw;
                }
            }
            finally
            {
                if (info != null)
                {
                    info.Dispose();
                }
            }
        }
        /// <summary>
        /// Image pipeline phase 1: Load images from disk and put them a queue.
        /// </summary>
        static void LoadPipelinedImages(IEnumerable <string> fileNames, string sourceDir,
                                        BlockingCollection <ImageInfo> original, CancellationTokenSource cts)
        {
            int       count       = 0;
            int       clockOffset = Environment.TickCount;
            var       token       = cts.Token;
            ImageInfo info        = null;

            try
            {
                foreach (var fileName in fileNames)
                {
                    if (token.IsCancellationRequested)
                    {
                        break;
                    }
                    info = LoadImage(fileName, sourceDir, count, clockOffset);
                    original.Add(info, token);
                    count += 1;
                    info   = null;
                }
            }
            catch (Exception e)
            {
                // in case of exception, signal shutdown to other pipeline tasks
                cts.Cancel();
                if (!(e is OperationCanceledException))
                {
                    throw;
                }
            }
            finally
            {
                original.CompleteAdding();
                if (info != null)
                {
                    info.Dispose();
                }
            }
        }
        /// <summary>
        /// Image pipeline phase 3: Filter images (give them a speckled appearance by adding Gaussian noise)
        /// </summary>
        static void FilterPipelinedImages(
            BlockingCollection <ImageInfo> thumbnailImages,
            BlockingCollection <ImageInfo> filteredImages,
            CancellationTokenSource cts)
        {
            ImageInfo info = null;

            try
            {
                var token = cts.Token;
                foreach (ImageInfo infoTmp in
                         thumbnailImages.GetConsumingEnumerable())
                {
                    info = infoTmp;
                    if (token.IsCancellationRequested)
                    {
                        break;
                    }
                    FilterImage(info);
                    filteredImages.Add(info, token);
                    info = null;
                }
            }
            catch (Exception e)
            {
                cts.Cancel();
                if (!(e is OperationCanceledException))
                {
                    throw;
                }
            }
            finally
            {
                filteredImages.CompleteAdding();
                if (info != null)
                {
                    info.Dispose();
                }
            }
        }
        /// <summary>
        /// Run the image processing pipeline.
        /// </summary>
        /// <param name="fileNames">List of image file names in source directory</param>
        /// <param name="sourceDir">Name of directory of source images</param>
        /// <param name="displayFn">Display action</param>
        /// <param name="cts">Cancellation token</param>
        static void RunSequential(IEnumerable <string> fileNames, string sourceDir,
                                  Action <ImageInfo> displayFn, CancellationTokenSource cts)
        {
            int       count       = 0;
            int       clockOffset = Environment.TickCount;
            int       duration    = 0;
            var       token       = cts.Token;
            ImageInfo info        = null;

            try
            {
                foreach (var fileName in fileNames)
                {
                    if (token.IsCancellationRequested)
                    {
                        break;
                    }

                    info = LoadImage(fileName, sourceDir, count, clockOffset);
                    ScaleImage(info);
                    FilterImage(info);
                    int displayStart = Environment.TickCount;
                    DisplayImage(info, count + 1, displayFn, duration);
                    duration = Environment.TickCount - displayStart;

                    count += 1;
                    info   = null;
                }
            }
            finally
            {
                if (info != null)
                {
                    info.Dispose();
                }
            }
        }