In my Windows Forms program, I have a PictureBox
. The bitmap image inside it is small, 5 x 5
pixels.
When assigned to a PictureBox
A solution I've seen around a couple of times is to make an overriding class of PictureBox
which has the InterpolationMode
as class property. Then all you need to do is use this class on the UI instead of .Net's own PictureBox
, and set that mode to NearestNeighbor
.
Public Class PixelBox
Inherits PictureBox
<Category("Behavior")>
<DefaultValue(InterpolationMode.NearestNeighbor)>
Public Property InterpolationMode As InterpolationMode = InterpolationMode.NearestNeighbor
Protected Overrides Sub OnPaint(pe As PaintEventArgs)
Dim g As Graphics = pe.Graphics
g.InterpolationMode = Me.InterpolationMode
' Fix half-pixel shift on NearestNeighbor
If Me.InterpolationMode = InterpolationMode.NearestNeighbor Then _
g.PixelOffsetMode = PixelOffsetMode.Half
MyBase.OnPaint(pe)
End Sub
End Class
As was remarked in the comments, for Nearest Neighbor mode, you need to set
the PixelOffsetMode
to Half
. I honestly don't understand why they bothered exposing that rather than making it an automatic choice inside the internal rendering process.
The size can be controlled by setting the control's SizeMode
property. Putting it to Zoom
will make it automatically center and expand without clipping in the control's set size.
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:
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:
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