Disable Image blending on a PictureBox
Asked Answered
S

2

9

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
Shuma answered 16/2, 2019 at 7:35 Comment(2)
Unfortunately the picture box control has no such option. The easiest way would be to scale the image yourself (can be done programmatically) before you add it to the picture box. Set Graphics.InterpolationMode to NearestNeighbor before drawing the scaled bitmap in order to achieve the desired result.Penick
Adding to what Visual Vincent already said: setting InterpolationMode NearestNeighbor is necessary, but is not enough. You also need e.Graphics.PixelOffsetMode = PixelOffsetMode.Half. This is by design, the drawing Rectangle is offset by half a pixel in NearestNeighborg mode: the pixels, in the Top/Right and Top/Bottom lines, will be clipped if you don't.Contortionist
C
10

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
Contortionist answered 16/2, 2019 at 19:12 Comment(28)
PictureBox1.Image can change in my program, by clicking Button1, should I change Private Sub Form1_Load(...) Handles MyBase.Load to Private Sub Button1_Click(...) Handles Button1.ClickShuma
I like how you have to backup the full quality image, before down-scaling, otherwise when you resize it bigger, it will have the same quality as the down-scaled version.Shuma
I don't think there's any need to save it to the disk, because you just store it in a variable, well I do have Button2_Click(...), that checks if there's an image, then if so, saves it to a file specified by SaveFileDialog1, of course if 'ok' is clicked, not 'cancel'.Shuma
There's a problem when Image.Width <> Image.Height. I think it's because the Function GetScaledImageRect(...) doesn't return the right aspect ratio.Shuma
Wait, the image can have a width/height of 0, never knew that!Shuma
HI! 1) The Image source is assigned in the Form.Load event just for testing. You, of course, can assign a new source (a File or a project resource) to the pixelBitmap Bitmap whenever you want (clicking a Button or using any other means) Don't assign it to the PictureBox, though: assign it to the pixelBitmap Bitmap instead (always cloning it as shown in code). 2) The method that scales the Bitmap is actually optiized for squared Bitmaps. Don't worry about it, I'll post an update as soon as I can.Contortionist
Code updated. The helper method is now handling any image size.Contortionist
If you make a subclass of PictureBox and override its OnPaint method instead of using the event, the e.Graphics.DrawImage stuff shouldn't be necessary at all; you can just call base.OnPaint after setting the interpolation stuff. The PictureBox can do that stuff automatically, depending on the SizeMode property. Putting it to Zoom should do the trick.Mersey
@Mersey So, which SizeMode do you propose to show a 5x5 Bitmap in a 200x200 PictureBox, preserving the pixels colors as described here? Test it with SizeMode.Zoom and see what happens. Then, you'll also want to save the Bitmap using the currently selected size... Anyway, I don't see why I should let the code behind a Control to do my stuff for me :)Contortionist
I don't see your point. It looks perfectly okay with Zoom mode. Also, saving the bitmap at that size is not the job of a UI control. On the other hand, "letting the code behind the control do your stuff for you" is exactly the job of a UI control. Note that I was only talking about the DrawImage call. The InterpolationMode and PixelOffsetMode stuff still applies; that's what the subclass is for.Mersey
I think we're talking about two different things, here. 1) The control is already not doing what's required. You say yourself, you'ld need to derive a control from an existing one to override it's standard behaviour. You just chose a different tool. 2) The Bitmap needs to be treated independently from what the PictureBox does or does not 3) Using custom code to perform custom painting is the norm. Same as using Owner Drawing to paint controls that don't provide specific functionalities by themselves. Here, I chose a method that will be useful both for painting a Control and a Bitmap.Contortionist
1) I'm just pointing out that you're adding a whole lot of code (the entire GetScaledImageRect function) for something that is already out-of-the-box behaviour if you work with a tiny subclass. 2) The only place that mentions the concept of resizing the source bitmap is your own answer. The default operating mode of PictureBox does no such thing, and thus neither does my subclass suggestion. 3) Using custom code for things already in the framework isn't "the norm", it's a waste of time and an unnecessary increase in code complexity.Mersey
@Mersey Not a whole lot of code, just 11 lines of code that can be reused for other purposes. At point (2) the OP: well I do have Button2_Click(...), that checks if there's an image, then if so, saves it to a file specified by SaveFileDialog1.... I'm not asking the PictureBox to do anything. I paint its surface: this is a very common task. Plus, the code also scales a Bitmap using a Container rectangle as reference; you now have a resusable tool. Having a specialize overriden Control that performs a specific task may be useful. I think, in this case, this is more useful. Opinions.Contortionist
@Mersey But, you may decide that you don't want to use a PictureBox, maybe it's a Panel, a Label; the Form itself. Well, you don't need to change a single line of code. You don't even need to override a Control to paint a Bitmap the way you want it to be.Contortionist
That comment never mentioned an intent to save the resized image, though. As for the ability to use that function on different controls, that is kind of my point: it's reinventing the wheel ; PictureBox already contains code that can do that, in a variety of pre-set ways that work fine. My only point in all this was that the same thing could be accomplished without needing any helper methods...Mersey
Note that Clone does not reliably free the object from its backing resources. The simplest way to do that is to put a Using directive around the bitmap loading the file, and then make a New Bitmap(bitmapFromFile) out of that and keep that one. The disadvantage of that is that it becomes 32bppARGB, but for UI-only stuff that's not an issue. (and I posted an answer there that does a deep data clone that retains the colour depth)Mersey
@Mersey Clone() doesn't carry over the stream. The errors you could see there are about the PictureBox.Image handling. I'm not going to discuss this here :)Contortionist
Oh, it allows you to close the stream. But it does not prevent errors from occurring because of the closed stream. Check the accepted answer there, and the links to other answers he provides. There's a lot of interesting material in there.Mersey
@Mersey Plus (and I'm going to discuss it here :) who declares a new Bitmap (var bmp) inside a using block and then assign it? That's really wrong. It will never work. Thanks for the link, but I've already seen that (it's very old material). I've tested this stuff. I know what's implied when you assign an Image to a PictureBox. Old thing this one too.Contortionist
You didn't read the answers I linked... you declare your final bitmap var before the Using, and assign the new one to that, like this ;)Mersey
@Mersey Yes, because I have all the reasons to do that. If you want to avoid GDI+ locking a file and have a Bitmap separated from the stream, that's one of the best methods to do it. The other is loading the byte array. I never assign the PicureBox.Image using a Bitmap that comes directly from a stream.Contortionist
The only way to load a Bitmap from bytes is using a stream, though. There's no constructor of Bitmap that accepts a byte array.Mersey
@Mersey Well, you have File.ReadAllBytes(). Then, Bitmap.FromHbitmap() etc. We can't keep this up forever :)Contortionist
Looks like some relevant discussion in the comments here. Unfortunately, comments are not the place for that. Can I bother one of you to add it as an answer? Either as an edit to this answer (which is already quite outstanding), or as a separate answer? Thanks.Palmapalmaceous
@Cody Gray Sure. Are you referring to the Bitmap.Clone thing or the difference between overriding the PictureBox default behaviour and implementing a standalone/indepenent method or saving a scaled Btimap in both ways or all of the above? This discussion has taken quite different paths :)Contortionist
"This discussion has taken quite different paths" Yes, that's the problem. I didn't have anything specific in mind. Just got an auto-flag about the comments here, so came to investigate. Normally, moderators will purge the whole lot, but these didn't look like pointless bickering or back-and-forth debugging, so I chose to keep most of them. We do, however, like to keep all of the information in posts, so future viewers don't have to read all of the comments to get all of the information.Palmapalmaceous
@Cody Gray Yes, I knew a moderator would come by because of the length of the comments section. Maybe you could keep some of the comments that Nyerguds wrote and I can post an About the comments update, with informations of the matters discussed here.Contortionist
@Contortionist I knew the load function didn't matter, but I thought another function will call the load function, and I would have to find that to replace as well.Shuma
M
3

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.

Mersey answered 19/2, 2019 at 15:24 Comment(4)
In the standard GDI+ rendering process, the external (transparent) pixels are used for the convolution kernels. Other rendering engines extend the outer pixels of a Bitmap outside the bounds. GDI+ brings in the transparent pixels instead. without repositioning/recalculating the edge pixels position. This could speed-up the process, with the draw back of creating semi-transparent lines of pixels in the edges. PixelOffsetMode.Half repositions the pixels offset, setting it to the Bitmap bounds. But, when set before the painting, it becomes part of the process: no speed reduction.Contortionist
I don't think Me. is necessary.Shuma
Actually what about Drawing2D.Graphics.Shuma
I think it's always cleaner to refer to local properties with Me., especially if (like here) they have the same name as a referenced type name. I could add the required imports to the code if you want, but they're not exactly hard to figure out.Mersey

© 2022 - 2024 — McMap. All rights reserved.