11

I'm using WinForms. Inside my form I have a pictureBox (set to normal mode), next and previous button. I want to resize and load Multipage TIF images quickly. When I go to the next page in the Multipage TIF image I experience a delay every time the image is drawn to the pictureBox. The average speed of the image takes about 800 milliseconds. I want the pages to load within 100 Milliseconds.

I want the performance of processing large TIF images as fast as IrfanView. IrfanView is a small image viewing application. If you Download IrfanView you can see how fast the performance is. Currently I have another solution where I use multi-threading background worker to load the TIF pages into an array then I scale it down. This method requires some time initially, but the goal here is not having to wait.

Is there a way to improve Graphics.DrawImage performance for large images in .NET?

g.DrawImage(img, 0, 0, width, height); //This line causes the delay " 800 milliseconds depending on your computer"

  • The size of TIF images i work with: Width=16800, Height=10800
  • Only Black and White Tif images
  • Bit depth = 1
  • Resolution Unit = 2

enter image description here

 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Data;
 using System.Diagnostics;
 using System.Drawing;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows.Forms;

namespace Tif_Preformance_Question
{
public partial class Form1 : Form
{

    int counter = -1;
    int frameCount = 0;
    Stopwatch s = new Stopwatch();
    Image img;
    Image[] images;

    public Form1()
    {
        InitializeComponent();
    }

    private void btn_Open_Click(object sender, EventArgs e)
    {
        var s = new Stopwatch();
        s.Start();
        s.Stop();
        this.Text = "Elapsed Time Milliseconds" + s.ElapsedMilliseconds;


        img = Image.FromFile(@"C:\image\Large_Tif_Image_15pages.tif");
        frameCount = img.GetFrameCount(System.Drawing.Imaging.FrameDimension.Page);
        images = new Image[frameCount];

        for (int i = 0; i < frameCount; i++)
        {
            img.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, i);
            images[i] = (Image)img.Clone();
        }
        img.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, 0);
        pictureBox1.Image = (Image)img.Clone();

    }

    private void btn_Next_Click(object sender, EventArgs e)
    {
        counter++;
        if (counter >= frameCount)
        {
            counter = frameCount - 1;
            btn_Next.Enabled = false;
        }
        btn_Next.Enabled = false;
        LoadPage();
        btn_Next.Enabled = true;
    }

    private void LoadPage()
    {

        StartWatch();
        img.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, counter);
        pictureBox1.Image = ResizeImage((Image)img.Clone(), pictureBox1.Width, pictureBox1.Height);
        pictureBox1.Refresh();
        Stopwatch();
    }

    public Image ResizeImage(Image img, int width, int height)
    {
        Bitmap b = new Bitmap(width, height);
        using (Graphics g = Graphics.FromImage((Image)b))
        {
            g.DrawImage(img, 0, 0, width, height);
        }
        return (Image)b;
    }

    private void StartWatch()
    {
        s.Start();
    }
    private void Stopwatch()
    {

        s.Stop();
        this.Text = "Elapsed Time Milliseconds: " + s.ElapsedMilliseconds;
        s.Reset();
    }
  }
}

References

IrfanView:

http://www.irfanview.com/

Test: Large TIF image Below

http://www.filedropper.com/largetifimage15pages_2

Visual Studio Solution

http://www.filedropper.com/tifpreformancequestion_1

taji01
  • 2,527
  • 8
  • 36
  • 80
  • 1
    Maybe you can cache the previous, current and next images each time navigation occurs? – Crowcoder Jun 28 '16 at 19:58
  • 1
    Using a backgroundworker to create a list of resized images you can cache is my next guess.. – TaW Jun 28 '16 at 22:07
  • Thanks for the reply, I have a solution similar to what you mentioned. This is the alternative solution that I use: http://stackoverflow.com/questions/35510498/boost-the-performance-when-advancing-to-the-next-page-using-tif-images. This solution uses a background worker which loads the TIF pages and resizes them. This solution is fast only when all the pages are loaded, but that still takes time. I was planning on viewing the pages as soon as the image loads in the picture box. I also don't want to wait a long time for the TIF image to load in the pictureBox. @TaW – taji01 Jun 28 '16 at 22:57
  • 1
    Are there any native (c++ or c) libraries that are faster than the .net code that you could call and have it do the scale down / split on a background thread? If you are not open to a 3rd party solution maybe look at creating a library yourself using c++, having it execute a split scale down might give you the efficiency and corresponding speed boost you are looking for. – Igor Jul 01 '16 at 15:18
  • I'm open on using different library's if i could call it from the app. I was looking at SharpDX from NuGet. This seemed promising, but i figure out how to use it. @Igor – taji01 Jul 01 '16 at 20:07
  • Not a real solution, but caching seems like the best bet (starting with next/previous and then 2 forward, 2 back, 3 forward, 3 back until all pages - up to a limit probably - are cached.) – Gerino Jul 05 '16 at 14:06
  • You also might want to have a look to this answer http://stackoverflow.com/a/3567824/6439999 – Thomas Voß Jul 08 '16 at 08:56

4 Answers4

3

What's very costy is the resizing of the image because it's a big image (you also have an extra clone before resize that seems useless and costs like ~10%).

I'm not sure you can find a faster loader / resizer, maybe irfan view wrote one specifically (TIF like the one in your sample is a simple 1 bpp B&W image. Once the image loaded, you could resize in a multithreaded mode, spawning say 2,4,8 or 16 worker threads, each one on a rectangle portion of the image, and divide overall by the number of threads).

W/o any 3rd party, here is pure .NET a sample that works in your environment, with a specific multi-threaded SizedTifImage utility class that caches all frames already resized in memory. When you run it, you will only see the initial ~1s load time and then browsing through images shouldn't be noticeable:

public partial class Form1 : Form
{
    SizedTifImage _tif;

    private void btn_Open_Click(object sender, EventArgs e)
    {
       ...
        _tif = new SizedTifImage(@"Large_Tif_Image_15pages.tif", pictureBox1.Width, pictureBox1.Height);
        pictureBox1.Image = _tif.GetFrame(0);
        btn_Next_Click(null, null);
    }

    private void btn_Next_Click(object sender, EventArgs e)
    {
        counter++;
        if (counter >= _tif.FrameCount)
        {
            counter = _tif.FrameCount - 1;
            btn_Next.Enabled = false;
        }
        btn_Next.Enabled = false;
        LoadPage();
        btn_Next.Enabled = true;
    }

    private void LoadPage()
    {
        StartWatch();
        pictureBox1.Image = _tif.GetFrame(counter);
        Stopwatch();
    }
}

public class SizedTifImage : IDisposable
{
    private Image _image;
    private ConcurrentDictionary<int, Image> _frames = new ConcurrentDictionary<int, Image>();

    public SizedTifImage(string filename, int width, int height)
    {
        Width = width;
        Height = height;
        _image = Image.FromFile(filename);
        FrameCount = _image.GetFrameCount(FrameDimension.Page);
        ThreadPool.QueueUserWorkItem(ResizeFrame);
    }

    public int FrameCount { get; private set; }
    public int Width { get; private set; }
    public int Height { get; private set; }

    private void ResizeFrame(object state)
    {
        for (int i = 0; i < FrameCount; i++)
        {
            if (_image == null)
                return;

            _image.SelectActiveFrame(FrameDimension.Page, i);
            var bmp = new Bitmap(Width, Height);
            using (var g = Graphics.FromImage(bmp))
            {
                if (_image == null)
                    return;

                g.DrawImage(_image, 0, 0, bmp.Width, bmp.Height);
            }
            _frames.AddOrUpdate(i, bmp, (k, oldValue) => { bmp.Dispose(); return oldValue; });
        }
    }

    public Image GetFrame(int i)
    {
        if (i >= FrameCount)
            throw new IndexOutOfRangeException();

        if (_image == null)
            throw new ObjectDisposedException("Image");

        Image img;
        do
        {
            if (_frames.TryGetValue(i, out img))
                return img;

            Thread.Sleep(10);
        }
        while (true);
    }

    public void Dispose()
    {
        var images = _frames.Values.ToArray();
        _frames.Clear();
        foreach (var img in images)
        {
            img.Dispose();
        }

        if (_image != null)
        {
            _image.Dispose();
            _image = null;
        }
    }
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
3

I suspect that there are several issues here. First I suspect that IrfanView isn't written in C#. C# is a wonderful language but some of it's strengths do not always promote maximum performance. For example C# has more overhead when dealing with memory (it clears it on allocation, it tracks usage and garbage collects, etc).

The areas I would look at are I/O and threading. On my machine it takes ~30 ms to read the file (that is almost 1/3 of your 100 ms budget. I suspect the problem with DrawImage is that it isn't threaded (my guess). To do the resize it has to run thru 22 MB of data on non-byte boundary's; 10x10 1 bit pixels in the old image have to be processed to produce 1 pixel in the new (scaled) image. You could confirm this by watching the task manager CPU graph (logical processors view) during your execution and then during an execution of IrfanView.

Fixing either problem may be non-trivial. You can speed up your I/O using Memory Mapped I/O. I suspect the real win would be threading the resize; 800 ms / 8 cores ~= 100 ms (since resizing is very parallelizable). You could write your own threaded resizer or there may be a 3rd party library available to do what you need. Or you may be able to modify an open source library to be threaded/faster.

You can also look at MS's source for the DrawImage call here It appears to be wrapping a gidplus.dll GdipDrawImageRectI call.

You might also take a look at Parallelizing GDI+ Image Resizing .net for ideas

Community
  • 1
  • 1
Dweeberly
  • 4,668
  • 2
  • 22
  • 41
3

You might have an advantage in creating your own PictureBox that inherits from the original. You can override the OnPaint and tweak the following parameters of the passed Graphics object:

private override OnPaint(object sender, PaintEventArgs e)
{
    e.Graphics.CompositingQuality = CompositingQuality.HighSpeed;
    e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
    e.Graphics.SmoothingMode = SmoothingMode.None;
    e.Graphics.PixelOffsetMode = PixelOffsetMode.Half;
    e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
    e.Graphics.CompositingMode = CompositingMode.SourceCopy;
    base OnPaint(sender, e)
}

Some of these parameters have a huge impact on the rendering speed (and to the quality of the result).

The parameters used in the code example are already quite fast but maybe you find better combinations for your requirements.

Thomas Voß
  • 1,145
  • 8
  • 20
3

Your image is too big. Resizing which normally includes smoothing calculation can be extremely slow.

Therefore, the only way is using pointer to access image bits and displaying pixels selectively.

public unsafe Image ResizeImage(Bitmap img, int width, int height)
{
    var stopwatch = Stopwatch.StartNew();

    var imgBits = img.LockBits(new Rectangle(Point.Empty, img.Size), ImageLockMode.ReadOnly, img.PixelFormat);

    Bitmap b = new Bitmap(width, height);
    var bBits = b.LockBits(new Rectangle(Point.Empty, b.Size), ImageLockMode.WriteOnly, b.PixelFormat);

    for (int j = 0; j < height; j++)
    {
        var imgJ = j * img.Height / height;

        for (int i = 0; i < width; i++)
        {
            var imgI = i * img.Width / width;

            var imgPointer = (byte*)imgBits.Scan0 + imgJ * imgBits.Stride + (imgI >> 3);
            var mask = (byte)(0x80 >> (imgI & 0x7));
            var imgPixel = (uint)(*imgPointer & mask);

            var bPointer = (uint*)bBits.Scan0 + j * bBits.Width + i;
            *bPointer = imgPixel > 0 ? 0x00FFFFFF : 0xFF000000;
        }
    }

    img.UnlockBits(imgBits);
    b.UnlockBits(bBits);

    stopwatch.Stop();
    Console.WriteLine("Resize to " + width + " x " + height + " within " + stopwatch.ElapsedMilliseconds + "ms");

    return b;
}

public void Test()
{
    var rawImage = new Bitmap(@"Large_Tif_Image_15pages.tif");
    rawImage.SelectActiveFrame(FrameDimension.Page, 3);

    pictureBox1.Image = ResizeImage(rawImage, pictureBox1.Width, pictureBox1.Height);
}

Resize to 525 x 345 within 31ms

The result is significant. However, the quality is of course not as good as 1 full second calculation.

var outputFactor = 1.5;
var outputWidth = (int)(pictureBox1.Width * outputFactor);
var outputHeight = (int)(pictureBox1.Height * outputFactor);
var outputImage = ResizeImage(rawImage, outputWidth, outputHeight);

To regain quality, resize with a factor, like 1.5, giving more details.

Balance between speed and quality.

Tommy
  • 3,044
  • 1
  • 14
  • 13