0

In my program, I'm coding a basic image editor. Part of this allows the user to draw a rectangular region and I pop up a display that shows that region zoomed by 3x or so (which they can adjust further with the mouse wheel). If they right click and drag this image, it will move the zoom region around on the original image, basically acting as a magnifying glass.

The problem is, I'm seeing some serious performance issues even on relatively small bitmaps. If the bitmap showing the zoomed region is around 400x400 it's still updating as fast as mouse can move and is perfectly smooth, but if I mouse wheel the zoom up to around 450x450, it immediately starts chunking, only down to around 2 updates per second, if that. I don't understand why such a small increase incurs such an enormous performance problem... it's like I've hit some internal memory limit or something. It doesn't seem to matter the size of the source bitmap that is being zoomed, just the size of the zoomed bitmap.

The problem is that I'm using Graphics.DrawImage and a PictureBox. Reading around this site, I see that the performance for both of these is typically not very good, but I don't know enough about the inner workings of GDI to improve my speed. I was hoping some of you might know where my bottlenecks are, as I'm likely just using these tools in poor ways or don't know of a better tool to use in its place.

Here are some snippets of my mouse events and related functions.

private void pictureBox_MouseDown(object sender, MouseEventArgs e)
    {

        else if (e.Button == System.Windows.Forms.MouseButtons.Right)
        {
            // slide the zoomed part to look at a different area of the original image
            if (zoomFactor > 1)
            {
                isMovingZoom = true;
                // try saving the graphics object?? are these settings helping at all??
                zoomingGraphics = Graphics.FromImage(displayImage);
                zoomingGraphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
                zoomingGraphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Low;
                zoomingGraphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighSpeed;
                zoomingGraphics.PixelOffsetMode = PixelOffsetMode.HighSpeed;
            }
        }
    }


private void pictureBox_MouseMove(object sender, MouseEventArgs e)
    {
        if (isMovingZoom)
        {
            // some computation on where they moved mouse ommitted here

            zoomRegion.X = originalZoomRegion.X + delta.X;
            zoomRegion.Y = originalZoomRegion.Y + delta.Y;
            zoomRegionEnlarged = scaleToOriginal(zoomRegion);

            // overwrite the existing displayImage to prevent more Bitmaps being allocated
            createZoomedImage(image.Bitmap, zoomRegionEnlarged, zoomFactor, displayImage, zoomingGraphics);
        }
    }

private void createZoomedImage(Bitmap source, Rectangle srcRegion, float zoom, Bitmap output, Graphics outputGraphics)
    {
        Rectangle destRect = new Rectangle(0, 0, (int)(srcRegion.Width * zoom), (int)(srcRegion.Height * zoom));

            outputGraphics.DrawImage(source, destRect, srcRegion, GraphicsUnit.Pixel);

        if (displayImage != originalDisplayImage && displayImage != output)
            displayImage.Dispose();
        setImageInBox(output);
    }

// sets the picture box image, as well as resizes the window to fit
    void setImageInBox(Bitmap bmp)
    {
        pictureBox.Image = bmp;
        displayImage = bmp;
        this.Width = pictureBox.Width + okButton.Width + SystemInformation.FrameBorderSize.Width * 2 + 25;
        this.Height = Math.Max(450, pictureBox.Height) + SystemInformation.CaptionHeight + SystemInformation.FrameBorderSize.Height * 2 + 20;
    }

private void pictureBox_MouseUp(object sender, MouseEventArgs e)
    {
        else if (e.Button == System.Windows.Forms.MouseButtons.Right)
        {
            if (isMovingZoom)
            {
                isMovingZoom = false;
                zoomingGraphics.Dispose();
            }
        }
    }

As you can see, I'm not declaring a new Bitmap every time I want to draw something, I'm reusing an old Bitmap (and the Bitmap's graphics object, though I don't know if there is much cost with calling Graphics.FromImage repeatedly). I tried adding Stopwatches around to benchmark my code, but I think DrawImage passes functionality to another thread so the function claims to be done relatively quickly. I'm trying to Dispose all my Bitmap and Graphics objects when I'm not using them, and avoid repeated calls to allocate/deallocate resources during the MouseMove event. I'm using a PictureBox but I don't think that's the problem here.

Any help to speed up this code or teach me what's happening in DrawImage is appreciated! I've trimmed some excess code to make it more presentable, but if I've accidentally trimmed something important, or don't show how I'm using something which may be causing problems, please let me know and I'll revise the post.

XenoScholar
  • 187
  • 3
  • 10
  • You'll likely not be able to squeeze much more performance out of this method. It's definitely worth investing some time to learn about either `Marshal.Copy` or pointers (`LockBits`, `BitmapData`, etc). – Simon Whitehead Jan 09 '13 at 23:16
  • You are tinkering with "zoomingGraphics" setting but the one that actually matters is "outputGraphics". Because that's the one that actually draws the image. It fell from the sky in the code snippet, hard to give advice. – Hans Passant Jan 09 '13 at 23:18

1 Answers1

2

The way I handle issues like that is when receiving the Paint event, I draw the whole image to a memory bitmap, and then BLT it to the window. That way, all visual flash is eliminated, and it looks fast, even if it actually is not.

To be more clear, I don't do any painting from within the mouse event handlers. I just set up what's needed for the main Paint handler, and then do Invalidate. So the painting happens after the mouse event completes.


ADDED: To answer Tom's question in a comment, here's how I do it. Remember, I don't claim it's fast, only that it looks fast, because the _e.Graphics.DrawImage(bmToDrawOn, new Point(0,0)); appears instantaneous. It just bips from one image to the next. The user doesn't see the window being cleared and then repainted, thing by thing. It gives the same effect as double-buffering.

    Graphics grToDrawOn = null;
    Bitmap bmToDrawOn = null;

    private void DgmWin_Paint(object sender, PaintEventArgs _e){
        int w = ClientRectangle.Width;
        int h = ClientRectangle.Height;
        Graphics gr = _e.Graphics;

        // if the bitmap needs to be made, do so
        if (bmToDrawOn == null) bmToDrawOn = new Bitmap(w, h, gr);
        // if the bitmap needs to be changed in size, do so
        if (bmToDrawOn.Width != w || bmToDrawOn.Height != h){
            bmToDrawOn = new Bitmap(w, h, gr);
        }
        // hook the bitmap into the graphics object
        grToDrawOn = Graphics.FromImage(bmToDrawOn);
        // clear the graphics object before drawing
        grToDrawOn.Clear(Color.White);
        // paint everything
        DoPainting();
        // copy the bitmap onto the real screen
        _e.Graphics.DrawImage(bmToDrawOn, new Point(0,0));
    }

    private void DoPainting(){
        grToDrawOn.blahblah....
    }
Mike Dunlavey
  • 40,059
  • 14
  • 91
  • 135
  • Do you mean double-buffering? I think this article might be of use - http://www.codeproject.com/Articles/4646/Flicker-free-drawing-using-GDI-and-C – SpruceMoose Jan 09 '13 at 23:32
  • It does the same job as double-buffering, but rather than switching video buffers, its quite fast enough to just do a block-transfer from a memory bitmap to the window. I do all my graphics that way, and it always looks snappy and dynamic, even if the actual drawing code is not that fast, because you're not having to watch it write each line, text, and whatnot. Besides, it doesn't have to do window-clipping as it paints. It does it all at once during the BLT. – Mike Dunlavey Jan 10 '13 at 01:04
  • Would you mind stating how you do that? I have similar problems. I'm already drawing to a memory bitmap, but I am still drawing this bitmap with Graphics.DrawImage onto my (double buffered) panel graphics. I'd be very curious to know how to transfer the memory bitmap faster. – Tom Aug 07 '14 at 10:04
  • Awesome, thanks a lot! This is roughly what I am doing, I thought you might use some PInvoked magic like the oldschool WinAPI BitBlt. For some odd reason, my drawing is not smooth, and I am yet to identify the bottleneck. I'm drawing a few thousand lines on DoPainting() and whenever onPaint gets fired, I do things like you are. When I add a rubberband selection implementation to my onpaint, its super choppy and lags behind the mouse cursor a lot :( – Tom Aug 07 '14 at 12:00
  • @Tom: If I've got that many lines to draw, I start filtering, because chances are a lot of them are too small to see. There's a certain overhead per call, no matter how small the line is. Also, I hope you're not doing much `new` inside the painting routine. That's a horrible time-taker. [*Here's how I find out for sure.*](http://stackoverflow.com/a/378024/23771) If I find myself doing that, I try to re-use previously allocated objects. Good luck! – Mike Dunlavey Aug 07 '14 at 12:58
  • Yep, after I noticed how often OnPaint is called I removed all instantiations that you usually see in most examples like pens and so forth. I'm converting my code to using DrawLines() instead of looping and calling DrawLine over and over. Still, Ive added some debug output that counts calls to onPaint() where I really only am blitting the bitmap to my panel. When moving the mouse over the panel, I get way more than 25 calls per second. I guess even drawing the bitmap is slow at this rate :( – Tom Aug 07 '14 at 17:49
  • @Tom: I believe in stack sampling for seeing what takes time, but another way is simply comment out your main line-drawing loop, and then seeing what kind of frame rate you can get. Also, if mouse events are coming closer together than 50ms, you might just ignore some. ALSO, I just thought, if you're only moving a few lines in response to the mouse, you might be able to paint only those lines in XOR mode, rather than continually invalidating the whole window. That should be plenty fast enough. – Mike Dunlavey Aug 07 '14 at 19:27