Disable Image blending on a PictureBox

前端 未结 2 1754
无人及你
无人及你 2020-12-06 14:39

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

相关标签:
2条回答
  • 2020-12-06 14:51

    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.

    0 讨论(0)
  • 2020-12-06 15:01

    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
    
    0 讨论(0)
提交回复
热议问题