Search code examples
vb.netwinformsbitmapgdi+picturebox

Disable Image blending on a PictureBox


In my Windows Forms program, I have a PictureBox that contains a small image, 5 x 5 pixels.
When this Bitmap is assigned to the PictureBox.Image property, it becomes very blurry.

I tried to find something like blending mode, blurring mode, or anti-aliasing mode, but I had no luck.

Image1 Image2

  This is what I want     This is not what I want

Solution

  • The problem:
    A Bitmap, with a size that is much smaller than the container used to show it, is blurred and the otherwise sharp edges of the well-defined areas of color are unceremoniously blended.
    This is just the result of a Bilinear filter applied to a really small image (a few pixels) when zoomed in.

    The desired result is to instead maintain the original color of the single pixels while the Image is enlarged.

    To achieve this result, it's enough to set the Graphics object's InterpolationMode to:

    e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor
    

    This filter, also known as Point Filter, simply selects a color which is the nearest to the pixel color that is being evaluated. When evaluating homogeneous areas of color, the result is the same pixel color for all the pixels.
    There's just one problem, the default value of the Graphics object's PixelOffsetMode, which is:

    e.Graphics.PixelOffsetMode = PixelOffsetMode.None
    

    With this mode active, the outer pixels, corresponding to the top and left borders of an Image (in the normal image sampling) are drawn in the middle of the edges of the rectangular area defined by the container (the destination Bitmap or device context).

    Because of this, since the source Image is small and its pixels are enlarged quite a lot, the pixels of the first horizontal and vertical lines are visibly cut in half.
    This can be resolved using the other PixelOffsetMode:

    e.Graphics.PixelOffsetMode = PixelOffsetMode.Half
    

    This mode moves back the image's rendering position by half a pixel.
    A sample image of the results can explain this better:

    InterpolationMode NearestNeighbor

         Default Filter        InterpolationMode        InterpolationMode
       InterpolationMode        NearestNeighbor          NearestNeighbor
            Bilinear          PixelOffsetMode.None     PixelOffsetMode.Half
                                         
    

    Note:
    The .Net's MSDN Docs do not describe the PixelOffsetMode parameter very well. You can find 6, apparently different, choices. The Pixel Offset modes are actually only two:
    PixelOffsetMode.None (the default) and PixelOffsetMode.Half.

    PixelOffsetMode.Default and PixelOffsetMode.HighSpeed are the same as PixelOffsetMode.None.
    PixelOffsetMode.HighQuality is the same as PixelOffsetMode.Half.
    Reading the .Net Docs, there seems to be speed implications when choosing one over the other. The difference is actually negligible.

    The C++ documentation about this matter (and GDI+ in general), is much more explicit and precise, it should be used instead of the .Net one.

    How to proceed:

    We could draw the small source Bitmap to a new, larger Bitmap and assign it to a PictureBox.Image property.

    But, assume that the PictureBox size changes at some point (because the layout changes and/or because of DPI Awareness compromises), we're (almost) back at square one.

    A simple solution is to draw the new Bitmap directly on the surface of a control and save it to disc when/if necessary.

    This will also allow to scale the Bitmap when needed without losing quality:

    PixelOffsetMode Scale Bitmap

    Imports System.Drawing
    Imports System.Drawing.Drawing2D
    
    Private pixelBitmap As Bitmap = Nothing
    
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        pixelBitmap = Image.FromStream(New MemoryStream(File.ReadAllBytes("[File Path]")), True, False)
    End Sub
    
    Private Sub PictureBox1_Paint(sender As Object, e As PaintEventArgs) Handles PictureBox1.Paint
        e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor
        e.Graphics.PixelOffsetMode = PixelOffsetMode.Half
        e.Graphics.DrawImage(pixelBitmap, GetScaledImageRect(pixelBitmap, DirectCast(sender, Control)))
    End Sub
    
    Private Sub PictureBox1_Resize(sender As Object, e As EventArgs) Handles PictureBox1.Resize
        PictureBox1.Invalidate()
    End Sub
    

    GetScaledImageRect is a helper method used to scale an Image inside a container:

    Public Function GetScaledImageRect(image As Image, canvas As Control) As RectangleF
        Return GetScaledImageRect(image, canvas.ClientSize)
    End Function
    
    Public Function GetScaledImageRect(image As Image, containerSize As SizeF) As RectangleF
        Dim imgRect As RectangleF = RectangleF.Empty
    
        Dim scaleFactor As Single = CSng(image.Width / image.Height)
        Dim containerRatio As Single = containerSize.Width / containerSize.Height
    
        If containerRatio >= scaleFactor Then
            imgRect.Size = New SizeF(containerSize.Height * scaleFactor, containerSize.Height)
            imgRect.Location = New PointF((containerSize.Width - imgRect.Width) / 2, 0)
        Else
            imgRect.Size = New SizeF(containerSize.Width, containerSize.Width / scaleFactor)
            imgRect.Location = New PointF(0, (containerSize.Height - imgRect.Height) / 2)
        End If
        Return imgRect
    End Function