Disable Image blending on a PictureBox

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 = DirectCast(New Bitmap("File Path").Clone(), Bitmap)
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

Leave a Comment