Boost the performance when advancing to the next page using .tif images
Asked Answered
E

2

6

I'm using WinForms. In my forms I have an open and a next button. My application opens .tif images into a picturebox. All the .tif images I work with have multiple pages. The next button is for going to the next page in the tif image. These .tif images I work with are very large.

Example: Dimensions: 2600 x 3300 (.tif images)

Question: How do I optimize the performance of my application? I've read/researched that I might have to load the images directly from the computers memory and some other methods. How would i go about this or is there a better way of coding this?

That is the code I have so far, but my application lags a little when i go to the next page.

Below is a link of a large TIFF image with multiple pages for testing.

Link

http://www.filedropper.com/tiftestingdoc

    FileStream _stream;
    Image _myImg; // setting the selected tiff
    string _fileName;


    private Image _Source = null;
    private int _TotalPages = 0;

    private int intCurrPage = 0;

    private void Clone_File()
    { // Reads file, then copys the file and loads it in the picture box as a temporary image doc. That way files are not locked in users directory when in use by this application.
        try
        {

            if (_myImg == null)
            {

                try
                {
                    _fileName = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
                    File.Copy(@"C:\Picture_Doc\The_Image.tif", _fileName);
                    _stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read);
                    this._Source = Image.FromStream(_stream);
                }
                catch (Exception ex)
                {
                }
            }
            _TotalPages = _Source.GetFrameCount(System.Drawing.Imaging.FrameDimension.Page);

            intCurrPage = 1;

            Display_Page(intCurrPage);

        }catch(Exception ex)
        {
            MessageBox.Show(ex.Message);
        }

    }

    private void Show_Processing_Image_Label()
    {
        Application.DoEvents();
    }

    private void Display_Page(int PageNumber, RotateFlipType Change)
    {
        if (pictureBox1.Image != null && pictureBox1.Image != _Source)
        {
            //Release memory for old rotated image
            pictureBox1.Image.Dispose();
        }

        // set the variable to null for easy Garbage Collection cleanup
        pictureBox1.Image = null;

        _Source.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, PageNumber - 1);

        pictureBox1.Image = new Bitmap(_Source);

        pictureBox1.Image.RotateFlip(Change);

        pictureBox1.Refresh();
        //Refresh() Calls Invalidate and then Update to refresh synchronously.
    }

    private void Display_Page(int PageNumber)
    {
        Show_Processing_Image_Label();

        //You could adjust the PictureBox size here for each frame OR adjust the image to fit the picturebox nicely

        if (pictureBox1.Image != _Source)
        {
            if (pictureBox1.Image != null)
            {
                //Release memory for old copy and set the variable to null for easy GC cleanup
                pictureBox1.Image.Dispose();
                pictureBox1.Image = null;
            }

            pictureBox1.Image = _Source;
        }

        pictureBox1.Image.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Page, PageNumber - 1);

        pictureBox1.Refresh();

    }

    private void Next_btn_Click(object sender, EventArgs e)
    {
        intCurrPage++;
        Display_Page(intCurrPage);
    }

    private void Open_btn_Click(object sender, EventArgs e)
    {

            if (_stream != null)
            {
                _myImg = null; //dispose the copy image
            }

            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                Clone_File();
            }

            pictureBox1.Size = new Size(850, 1100);

     } 

enter image description here

Erythroblastosis answered 19/2, 2016 at 16:49 Comment(13)
why don't you put breakpoints in the code especially in the code where you are navigating to the next page.. and see if you can pinpoint the bottle neck.. I hate using Application.DoEvents() however after you call the .Refresh method try putting Application.DoEvents() call after thatAndorra
I've been using breakpoints. Just curious why do you hate using Application.DoEvents() ? @AndorraErythroblastosis
Read up on the Pro's and Cons of Application.DoEvents() there are tons of post on SO about this as well as others that will tell you to steer clear of it but I was just making a personal opinion / statement about why I hate using it.. it's not worth going into in that regard...Andorra
Not a solution to your current approach but I if you are not doing anything in particular with the image it will be much better if you load a lower resolution version of the image and the do a .next .previous on it.Rugging
I am actually doing other things with these images, for example printing these .tif docs, and all the images i work with is high quality and large in size. Usually the size of these images are 1.70MB. So i simply cant just load a lower resolution of these images. @RuggingErythroblastosis
I understand, but even in those cases, unless you are actually modifying the image and then sending it to the printer or anywhere else, you should be able to DISPLAY a lower resolution image and when the printing needs arise just send the actual image behind the scenes, a user can cope with the wait time for printing (you would be including a few extra second or so to load the real image and then send it to the printer) just my two cents.Rugging
As @Rugging said the best you are going to be able to do is display a lower resolution image but when a user needs to do anything, is use the higher res one that is behind the scenes.Ima
Is the entire 2600 x 3300 pixels visible on the screen, e.g. do you use a screen with that large resolution? Or is the image scaled down when viewed?Scorpaenid
My pictureBox size is set to zoom and scales the picture down to (850 x 1100) if that is what you meant. Other than that, don't think i'm scaling down the resolution to view these image document because my code above is basically what I have now. I don't know if scaling down will effect the tif viewer app that I'm trying to make. Others have said like @Rugging and @Ima that it will not effect me unless I'm modifying the image. I don't know if they meant modifying the image as in cropping image, highlighting words, or inverting the colors which i currently have. @vidstige.Erythroblastosis
@Erythroblastosis You can always do all your actions on a scaled image and in the background also apply them to the normal oneIma
The thing is I'm still new to programming and i wouldn't know how to even start coding that. It took me a extremely long time to figure this out and now I've been stuck in this performance issue for more than a month. @ImaErythroblastosis
@Erythroblastosis Look into creating a thumbnail image and display that, If your user crops say half the image save the direction they cropped it and the amount then do that on the normal image in background thread, essentially do the same thing twiceIma
For specific code there are tons of examples already posted here and all over the web. One of them, to get you started is this one: #17441598 For keeping track of the actions done to the image, you need to search a little bit more or open another question hereRugging
A
4

It turns out that the slow part is the Image.SelectActiveFrame call.

As usual, the solution is caching. However, in order to not increase the initial load time, it should be performed lazily on background.

The idea is simple. Start a worker thread and load all the image frames as separate Bitmaps in an array. Then use the cached image from the array instead of the SelectActiveFrame.

Since all that require some thread synchronization, I've encapsulated it in a helper class:

class PageBuffer : IDisposable
{
    public static PageBuffer Open(string path)
    {
        return new PageBuffer(File.OpenRead(path));
    }

    private PageBuffer(Stream stream)
    {
        this.stream = stream;
        Source = Image.FromStream(stream);
        PageCount = Source.GetFrameCount(FrameDimension.Page);
        if (PageCount < 2) return;
        pages = new Image[PageCount];
        var worker = new Thread(LoadPages) { IsBackground = true };
        worker.Start();
    }

    private void LoadPages()
    {
        for (int index = 0; ; index++)
        {
            lock (syncLock)
            {
                if (disposed) return;
                if (index >= pages.Length)
                {
                    // If you don't need the source image, 
                    // uncomment the following line to free some resources
                    //DisposeSource();
                    return;
                }
                if (pages[index] == null)
                    pages[index] = LoadPage(index);
            }
        }
    }

    private Image LoadPage(int index)
    {
        Source.SelectActiveFrame(FrameDimension.Page, index);
        return new Bitmap(Source);
    }

    private Stream stream;
    private Image[] pages;
    private object syncLock = new object();
    private bool disposed;

    public Image Source { get; private set; }
    public int PageCount { get; private set; }
    public Image GetPage(int index)
    {
        if (disposed) throw new ObjectDisposedException(GetType().Name);
        if (PageCount < 2) return Source;
        var image = pages[index];
        if (image == null)
        {
            lock (syncLock)
            {
                image = pages[index];
                if (image == null)
                    image = pages[index] = LoadPage(index);
            }
        }
        return image;
    }

    public void Dispose()
    {
        if (disposed) return;
        lock (syncLock)
        {
            disposed = true;
            if (pages != null)
            {
                foreach (var item in pages)
                    if (item != null) item.Dispose();
                pages = null;
            }
            DisposeSource();
        }
    }

    private void DisposeSource()
    {
        if (Source != null)
        {
            Source.Dispose();
            Source = null;
        }
        if (stream != null)
        {
            stream.Dispose();
            stream = null;
        }
    }
}

A full working demo:

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Windows.Forms;

namespace Demo
{
    class TestForm : Form
    {
        public TestForm()
        {
            var panel = new Panel { Dock = DockStyle.Top, BorderStyle = BorderStyle.FixedSingle };
            openButton = new Button { Text = "Open", Top = 8, Left = 16 };
            prevButton = new Button { Text = "Prev", Top = 8, Left = 16 + openButton.Right };
            nextButton = new Button { Text = "Next", Top = 8, Left = 16 + prevButton.Right };
            panel.Height = 16 + openButton.Height;
            panel.Controls.AddRange(new Control[] { openButton, prevButton, nextButton });
            pageViewer = new PictureBox { Dock = DockStyle.Fill, SizeMode = PictureBoxSizeMode.Zoom };
            ClientSize = new Size(850, 1100 + panel.Height);
            Controls.AddRange(new Control[] { panel, pageViewer });
            openButton.Click += OnOpenButtonClick;
            prevButton.Click += OnPrevButtonClick;
            nextButton.Click += OnNextButtonClick;
            Disposed += OnFormDisposed;
            UpdatePageInfo();
        }

        private Button openButton;
        private Button prevButton;
        private Button nextButton;
        private PictureBox pageViewer;
        private PageBuffer pageData;
        private int currentPage;

        private void OnOpenButtonClick(object sender, EventArgs e)
        {
            using (var dialog = new OpenFileDialog())
            {
                if (dialog.ShowDialog(this) == DialogResult.OK)
                    Open(dialog.FileName);
            }
        }

        private void OnPrevButtonClick(object sender, EventArgs e)
        {
            SelectPage(currentPage - 1);
        }

        private void OnNextButtonClick(object sender, EventArgs e)
        {
            SelectPage(currentPage + 1);
        }

        private void OnFormDisposed(object sender, EventArgs e)
        {
            if (pageData != null)
                pageData.Dispose();
        }

        private void Open(string path)
        {
            var data = PageBuffer.Open(path);
            pageViewer.Image = null;
            if (pageData != null)
                pageData.Dispose();
            pageData = data;
            SelectPage(0);
        }

        private void SelectPage(int index)
        {
            pageViewer.Image = pageData.GetPage(index);
            currentPage = index;
            UpdatePageInfo();
        }

        private void UpdatePageInfo()
        {
            prevButton.Enabled = pageData != null && currentPage > 0;
            nextButton.Enabled = pageData != null && currentPage < pageData.PageCount - 1;
        }
    }

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new TestForm());
        }
    }

    class PageBuffer : IDisposable
    {
        public static PageBuffer Open(string path)
        {
            return new PageBuffer(File.OpenRead(path));
        }

        private PageBuffer(Stream stream)
        {
            this.stream = stream;
            Source = Image.FromStream(stream);
            PageCount = Source.GetFrameCount(FrameDimension.Page);
            if (PageCount < 2) return;
            pages = new Image[PageCount];
            var worker = new Thread(LoadPages) { IsBackground = true };
            worker.Start();
        }

        private void LoadPages()
        {
            for (int index = 0; ; index++)
            {
                lock (syncLock)
                {
                    if (disposed) return;
                    if (index >= pages.Length)
                    {
                        // If you don't need the source image, 
                        // uncomment the following line to free some resources
                        //DisposeSource();
                        return;
                    }
                    if (pages[index] == null)
                        pages[index] = LoadPage(index);
                }
            }
        }

        private Image LoadPage(int index)
        {
            Source.SelectActiveFrame(FrameDimension.Page, index);
            return new Bitmap(Source);
        }

        private Stream stream;
        private Image[] pages;
        private object syncLock = new object();
        private bool disposed;

        public Image Source { get; private set; }
        public int PageCount { get; private set; }
        public Image GetPage(int index)
        {
            if (disposed) throw new ObjectDisposedException(GetType().Name);
            if (PageCount < 2) return Source;
            var image = pages[index];
            if (image == null)
            {
                lock (syncLock)
                {
                    image = pages[index];
                    if (image == null)
                        image = pages[index] = LoadPage(index);
                }
            }
            return image;
        }

        public void Dispose()
        {
            if (disposed) return;
            lock (syncLock)
            {
                disposed = true;
                if (pages != null)
                {
                    foreach (var item in pages)
                        if (item != null) item.Dispose();
                    pages = null;
                }
                DisposeSource();
            }
        }

        private void DisposeSource()
        {
            if (Source != null)
            {
                Source.Dispose();
                Source = null;
            }
            if (stream != null)
            {
                stream.Dispose();
                stream = null;
            }
        }
    }
}

UPDATE: As mentioned in the comments, the above implementation is using quite simple greedy caching strategy, which uses a lot of memory and does not work for big files.

The good thing though is that once the logic is encapsulated inside the class, we can change the strategy without touching our app code. For instance, we can remove the caching at all (return to the initial state), or optimize for "prev/next" navigation by maintaining a small set of cached image "window" like this

class PageBuffer : IDisposable
{
    public const int DefaultCacheSize = 5;

    public static PageBuffer Open(string path, int cacheSize = DefaultCacheSize)
    {
        return new PageBuffer(File.OpenRead(path), cacheSize);
    }

    private PageBuffer(Stream stream, int cacheSize)
    {
        this.stream = stream;
        source = Image.FromStream(stream);
        pageCount = source.GetFrameCount(FrameDimension.Page);
        if (pageCount < 2) return;
        pageCache = new Image[Math.Min(pageCount, Math.Max(cacheSize, 3))];
        var worker = new Thread(LoadPages) { IsBackground = true };
        worker.Start();
    }

    private void LoadPages()
    {
        while (true)
        {
            lock (syncLock)
            {
                if (disposed) return;
                int index = Array.FindIndex(pageCache, 0, pageCacheSize, p => p == null);
                if (index < 0)
                    Monitor.Wait(syncLock);
                else
                    pageCache[index] = LoadPage(pageCacheStart + index);
            }
        }
    }

    private Image LoadPage(int index)
    {
        source.SelectActiveFrame(FrameDimension.Page, index);
        return new Bitmap(source);
    }

    private Stream stream;
    private Image source;
    private int pageCount;
    private Image[] pageCache;
    private int pageCacheStart, pageCacheSize;
    private object syncLock = new object();
    private bool disposed;

    public Image Source { get { return source; } }
    public int PageCount { get { return pageCount; } }
    public Image GetPage(int index)
    {
        if (disposed) throw new ObjectDisposedException(GetType().Name);
        if (PageCount < 2) return Source;
        lock (syncLock)
        {
            AdjustPageCache(index);
            int cacheIndex = index - pageCacheStart;
            var image = pageCache[cacheIndex];
            if (image == null)
                image = pageCache[cacheIndex] = LoadPage(index);
            return image;
        }
    }

    private void AdjustPageCache(int pageIndex)
    {
        int start, end;
        if ((start = pageIndex - pageCache.Length / 2) <= 0)
            end = (start = 0) + pageCache.Length;
        else if ((end = start + pageCache.Length) >= PageCount)
            start = (end = PageCount) - pageCache.Length;
        if (start < pageCacheStart)
        {
            int shift = pageCacheStart - start;
            if (shift >= pageCacheSize)
                ClearPageCache(0, pageCacheSize);
            else
            {
                ClearPageCache(pageCacheSize - shift, pageCacheSize);
                for (int j = pageCacheSize - 1, i = j - shift; i >= 0; j--, i--)
                    Exchange(ref pageCache[i], ref pageCache[j]);
            }
        }
        else if (start > pageCacheStart)
        {
            int shift = start - pageCacheStart;
            if (shift >= pageCacheSize)
                ClearPageCache(0, pageCacheSize);
            else
            {
                ClearPageCache(0, shift);
                for (int j = 0, i = shift; i < pageCacheSize; j++, i++)
                    Exchange(ref pageCache[i], ref pageCache[j]);
            }
        }
        if (pageCacheStart != start || pageCacheStart + pageCacheSize != end)
        {
            pageCacheStart = start;
            pageCacheSize = end - start;
            Monitor.Pulse(syncLock);
        }
    }

    void ClearPageCache(int start, int end)
    {
        for (int i = start; i < end; i++)
            Dispose(ref pageCache[i]);
    }

    static void Dispose<T>(ref T target) where T : class, IDisposable
    {
        var value = target;
        if (value != null) value.Dispose();
        target = null;
    }

    static void Exchange<T>(ref T a, ref T b) { var c = a; a = b; b = c; }

    public void Dispose()
    {
        if (disposed) return;
        lock (syncLock)
        {
            disposed = true;
            if (pageCache != null)
            {
                ClearPageCache(0, pageCacheSize);
                pageCache = null;
            }
            Dispose(ref source);
            Dispose(ref stream);
            if (pageCount > 2)
                Monitor.Pulse(syncLock);
        }
    }
}

or implement other "smart" caching strategy. We can even make the strategy selectable by implementing the Strategy pattern.

Bu that will be another story. The second PageBuffer implementation should be sufficient for the OP use case.

Ambidexter answered 22/2, 2016 at 21:1 Comment(8)
This is an excellent solution If the .tif images have a low number of pages, for example 20 pages, but if the .tif document has more than 100 pages it becomes an issue. The program keeps loading (for (int index = 0; ; index++)), and the RAM usages keeps getting higher. I tested this on 2 computers on first computer it worked fine because I had a lot of ram in it. The second computer however could not handle it, because this method required to much RAM when the document has many pages. @Ivan StoevErythroblastosis
@Erythroblastosis Yeah, that's the obvious drawback. It's hard to find the trade-off between the memory and the speed. Of course the above is quite naïve brute force caching strategy, but it could be adjusted for instance to keep a sliding window of a few cached images etc.Ambidexter
If i re-size the image smaller proportionally before it loads up and this would be faster right?Erythroblastosis
I was looking under return new Bitmap(source); in the private Image LoadPage(int index) method if i did something like (source, 800, 1100) it re sizes the images smaller and loads it faster. The problem is its not scaled properly.Erythroblastosis
I'm not sure it would be faster (resizing takes time), but for sure it will consume less memory. I saw you new question, still having troubles even with 5 pages cache?Ambidexter
yeah :( i'm still having issues. I tried to do it myself without asking for help again but I just can't seem to get my head around the problem. If you download the tif image from the link you'll have a good tif image to test on. its a big file that will break this program.Erythroblastosis
The only way to not break it is if i re-size it. Well that's the solution i came up with like, (source, 800, 1100) when i do this it works fine. If i don't do this the application stops and gives me an error OutOfMemoryException was unhandled my process memory was 864.Erythroblastosis
Strange. I used the tiff image from the link to test all my posted suggestions, and had no problems with any of them. But my machines have a lot of memory though. Anyway, I've posted an answer in your newer post, hope that helps.Ambidexter
S
-1

You could try double-buffering your form; I know this helped me with a similar issue where my form was painting the controls very slowly when they involved either a high resolution image, or a lot of image-based controls.

public partial class form1 : Form
{
    public form1()
    {
    InitializeComponent();
    }

    //Paste this in your form:
    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ExStyle |= 0x02000000;
            return cp;
        }
    }

    //... your code here
}
Sheepshank answered 19/2, 2016 at 21:3 Comment(2)
Thanks for replying maybe this will help someone else, but I'm still having the issue.Erythroblastosis
This won't increase the performance only minimize any tearing in the application as it rendersIma

© 2022 - 2024 — McMap. All rights reserved.