Boost the Performance when Advancing to the Next Page Using .tif Images

倾然丶 夕夏残阳落幕 提交于 2019-12-05 00:11:32

问题


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. Can you please help me out?

Below is a link of a large tif 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);

     } 


回答1:


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.




回答2:


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
}


来源:https://stackoverflow.com/questions/35510498/boost-the-performance-when-advancing-to-the-next-page-using-tif-images

工具导航Map

JSON相关