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 ima
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.
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;
}
}
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
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.