How to implement pan/zoom on gigapixel bitmaps?

前端 未结 3 1575
庸人自扰
庸人自扰 2021-02-06 08:23

In my project, I\'m using (uncompressed 16-bit grayscale) gigapixel images which come from a high resolution scanner for measurement purposes. Since these bitmaps can not be loa

3条回答
  •  北荒
    北荒 (楼主)
    2021-02-06 08:36

    I decided to try something myself. I came up with a straightforward GDI+ code, which uses the tiles I've already got. I just filter out the parts which are relevant for current clipping region. It works like magic! Please find my code below. (Form settings double buffering for the best results)

     protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            Graphics dc = e.Graphics;
            dc.ScaleTransform(1.0F, 1.0F);
            Size scrollOffset = new Size(AutoScrollPosition);
    
            int start_x = Math.Min(matrix_x_size, 
                                 (e.ClipRectangle.Left - scrollOffset.Width) / 256);
            int start_y = Math.Min(matrix_y_size, 
                                 (e.ClipRectangle.Top - scrollOffset.Height) / 256);
            int end_x = Math.Min(matrix_x_size, 
                            (e.ClipRectangle.Right - scrollOffset.Width + 255) / 256);
            int end_y = Math.Min(matrix_y_size, 
                          (e.ClipRectangle.Bottom - scrollOffset.Height + 255) / 256);
    
            // start * contain the first and last tile x/y which are on screen 
            // and which need to be redrawn.
            // now iterate trough all tiles which need an update 
            for (int y = start_y; y < end_y; y++)
                for (int x = start_x; x < end_x; x++)
                {  // draw bitmap with gdi+ at calculated position.
                    dc.DrawImage(BmpMatrix[y, x], 
                               new Point(x * 256 + scrollOffset.Width, 
                                         y * 256 + scrollOffset.Height));
                }
        }
    

    To test it, I've created a matrix of 80x80 of 256 tiles (420 MPixel). Of course I'll have to add some deferred loading in real life. I can leave tiles out (empty) if they are not yet loaded. In fact, I've asked my client to stick 8 GByte in his machine so I don't have to bother about performance too much. Once loaded tiles can stay in memory.

    public partial class Form1 : Form
    {
        bool dragging = false;
        float Zoom = 1.0F;
        Point lastMouse;
        PointF viewPortCenter;
    
        private readonly Brush solidYellowBrush = new SolidBrush(Color.Yellow);
        private readonly Brush solidBlueBrush = new SolidBrush(Color.LightBlue);
        const int matrix_x_size = 80;
        const int matrix_y_size = 80;
        private Bitmap[,] BmpMatrix = new Bitmap[matrix_x_size, matrix_y_size];
        public Form1()
        {
            InitializeComponent();
    
            Font font = new Font("Times New Roman", 10, FontStyle.Regular);
            StringFormat strFormat = new StringFormat();
            strFormat.Alignment = StringAlignment.Center;
            strFormat.LineAlignment = StringAlignment.Center;
            for (int y = 0; y < matrix_y_size; y++)
                for (int x = 0; x < matrix_x_size; x++)
                {
                    BmpMatrix[y, x] = new Bitmap(256, 256, PixelFormat.Format24bppRgb);
                    //                    BmpMatrix[y, x].Palette.Entries[0] = (x+y)%1==0?Color.Blue:Color.White;
    
                    using (Graphics g = Graphics.FromImage(BmpMatrix[y, x]))
                    {
                        g.FillRectangle(((x + y) % 2 == 0) ? solidBlueBrush : solidYellowBrush, new Rectangle(new Point(0, 0), new Size(256, 256)));
                        g.DrawString("hello world\n[" + x.ToString() + "," + y.ToString() + "]", new Font("Tahoma", 8), Brushes.Black,
                            new RectangleF(0, 0, 256, 256), strFormat);
                        g.DrawImage(BmpMatrix[y, x], Point.Empty);
                    }
                }
    
            BackColor = Color.White;
    
            Size = new Size(300, 300);
            Text = "Scroll Shapes Correct";
    
            AutoScrollMinSize = new Size(256 * matrix_x_size, 256 * matrix_y_size);
        }   
    

    Turned out this was the easy part. Getting async multithreaded i/o done in the background was a lot harder to acchieve. Still, I've got it working in the way described here. The issues to resolve were more .NET/Form multithreading related than to this topic.

    In pseudo code it works like this:

    after onPaint (and on Tick)
       check if tiles on display need to be retrieved from disc
           if so: post them to an async io queue
           if not: check if tiles close to display area are already loaded
               if not: post them to an async io/queue
       check if bitmaps have arrived from io thread
          if so: updat them on screen, and force repaint if visible
    

    Result: I now have my own Custom control which uses roughly 50 MByte for very fast access to arbitrary size (tiled) TIFF files.

提交回复
热议问题