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
Jimi
  • 29,621
  • 8
  • 43
  • 61
TheRealSuicune
  • 369
  • 3
  • 10
  • 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`](https://learn.microsoft.com/en-us/dotnet/framework/winforms/advanced/how-to-use-interpolation-mode-to-control-image-quality-during-scaling) to `NearestNeighbor` before drawing the scaled bitmap in order to achieve the desired result. – Visual Vincent Feb 16 '19 at 07:55
  • 2
    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. – Jimi Feb 16 '19 at 09:59

2 Answers2

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
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • `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.Click` – TheRealSuicune Feb 17 '19 at 05:58
  • 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_. – TheRealSuicune Feb 17 '19 at 06:05
  • 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'_. – TheRealSuicune Feb 17 '19 at 06:08
  • There's a problem when `Image.Width <> Image.Height`. I think it's because the `Function GetScaledImageRect(...)` doesn't return the right aspect ratio. – TheRealSuicune Feb 17 '19 at 06:56
  • Wait, the image can have a width/height of 0, never knew that! – TheRealSuicune Feb 17 '19 at 06:57
  • 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. – Jimi Feb 17 '19 at 10:43
  • Code updated. The helper method is now handling any image size. – Jimi Feb 17 '19 at 14:49
  • 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](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.pictureboxsizemode?view=netframework-4.7.2). Putting it to `Zoom` should do the trick. – Nyerguds Feb 19 '19 at 12:05
  • @Nyerguds 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 :) – Jimi Feb 19 '19 at 12:12
  • I don't see your point. [It looks perfectly okay with Zoom mode](https://i.stack.imgur.com/33XRq.png). 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. – Nyerguds Feb 19 '19 at 12:23
  • 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. – Jimi Feb 19 '19 at 12:32
  • 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. – Nyerguds Feb 19 '19 at 12:49
  • @Nyerguds 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. – Jimi Feb 19 '19 at 13:58
  • @Nyerguds 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. – Jimi Feb 19 '19 at 14:01
  • 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... – Nyerguds Feb 19 '19 at 14:51
  • Note that Clone [does _not_ reliably free the object from its backing resources](https://stackoverflow.com/a/4804260/395685). 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) – Nyerguds Feb 19 '19 at 20:17
  • @Nyerguds 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 :) – Jimi Feb 19 '19 at 20:33
  • 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](https://stackoverflow.com/a/7972963/395685) he provides. There's a lot of interesting material in there. – Nyerguds Feb 19 '19 at 20:41
  • @Nyerguds 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. – Jimi Feb 19 '19 at 20:49
  • 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](https://stackoverflow.com/a/8701748/395685) ;) – Nyerguds Feb 19 '19 at 20:50
  • @Nyerguds 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. – Jimi Feb 19 '19 at 20:52
  • 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. – Nyerguds Feb 19 '19 at 20:55
  • @Nyerguds Well, you have `File.ReadAllBytes()`. Then, `Bitmap.FromHbitmap()` etc. We can't keep this up forever :) – Jimi Feb 19 '19 at 20:56
  • 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. – Cody Gray - on strike Feb 19 '19 at 22:18
  • @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 :) – Jimi Feb 19 '19 at 22:35
  • *"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. – Cody Gray - on strike Feb 19 '19 at 22:37
  • @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. – Jimi Feb 19 '19 at 22:42
  • @Jimi 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. – TheRealSuicune Feb 21 '19 at 05:39
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.

Nyerguds
  • 5,360
  • 1
  • 31
  • 63
  • 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. – Jimi Feb 19 '19 at 16:07
  • I don't think `Me.` is necessary. – TheRealSuicune Feb 21 '19 at 08:03
  • Actually what about `Drawing2D.Graphics.` – TheRealSuicune Feb 21 '19 at 08:04
  • 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. – Nyerguds Feb 21 '19 at 12:16