使用 .tif 图像前进到下一页时提高性能

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

我正在使用 WinForms。在我的表单中,我有一个打开按钮和一个下一步按钮。我的应用程序将 .tif 图像打开到图片框中。我使用的所有 .tif 图像都有多个页面。下一个按钮用于转到 tif 图像中的下一页。这些 .tif 我使用的图像非常大。

示例: 尺寸:2600 x 3300(.tif 张图片)

问题:如何优化应用程序的性能?我 read/researched 我可能必须直接从计算机内存和其他一些方法加载图像。我将如何解决这个问题或者是否有更好的编码方法?

这是我目前的代码,但是当我转到下一页时我的应用程序有点滞后。

下面是一张 link 包含多个页面的大型 TIFF 图像用于测试。

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);

     } 

您可以尝试对表单进行双缓冲;我知道这帮助我解决了一个类似的问题,即当涉及高分辨率图像或大量基于图像的控件时,我的表单绘制控件的速度非常慢。

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
}

原来慢的部分是Image.SelectActiveFrame调用。

像往常一样,解决方案是缓存。但是,为了不增加初始加载时间,应该在后台延迟执行。

这个想法很简单。启动工作线程并将所有图像帧作为单独的 Bitmap 加载到数组中。然后使用数组中的缓存图像而不是 SelectActiveFrame

由于所有这些都需要一些线程同步,所以我将其封装在一个帮助程序中 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;
        }
    }
}

一个完整的工作演示:

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

更新: 正如评论中提到的,上面的实现使用了非常简单的贪婪缓存策略,它使用大量内存并且不适用于大文件。

但好处是,一旦逻辑封装在 class 中,我们就可以更改策略,而无需触及我们的应用程序代码。例如,我们可以完全删除缓存(return 到初始状态),或者通过维护一小组缓存图像 "window" 来优化 "prev/next" 导航,就像这样

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);
        }
    }
}

或实施其他 "smart" 缓存策略。我们甚至可以通过实施 Strategy pattern 使策略可选择。

但那将是另一个故事。第二个 PageBuffer 实现对于 OP 用例应该足够了。