禁用PictureBox上的图像混合

9
在我的Windows Forms程序中,我有一个包含小图像的PictureBox,大小为5 x 5像素。当这个Bitmap被分配给PictureBox.Image属性时,它变得非常模糊。
我尝试找到类似混合模式、模糊模式或抗锯齿模式之类的东西,但是没有成功。
请见下方图片1和图片2。
  This is what I want     This is not what I want

2
不幸的是,图片框控件没有这样的选项。 最简单的方法是在将图像添加到图片框之前自己缩放图像(可以通过编程完成)。 在绘制缩放位图之前将Graphics.InterpolationMode设置为NearestNeighbor,以实现所需的结果。 - Visual Vincent
2
补充一下Visual Vincent所说的:设置InterpolationModeNearestNeighbor是必要的,但这还不够。你还需要设置e.Graphics.PixelOffsetMode = PixelOffsetMode.Half。这是设计上的考虑,在NearestNeighbor模式下,绘制的矩形会偏移半个像素:如果你不这样做,顶部/右侧和顶部/底部行中的像素将被裁剪。 - Jimi
2个回答

10
问题:
一个比显示它的容器小得多的位图会变得模糊,颜色区域的清晰边缘也会被不加仪式地混合。
当放大时,这只是对一个非常小的图像(几个像素)应用双线性滤波器的结果。

期望的结果是在放大图像的同时保持单个像素的原始颜色。

为了实现这个结果,只需要将 Graphics 对象的 InterpolationMode 设置为:

e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor

这个滤镜,也被称为点过滤器,简单地选择一个颜色,该颜色是最接近正在评估的像素颜色的颜色。当评估颜色均匀的区域时,所有像素的结果都是相同的像素颜色。
只有一个问题,Graphics对象的PixelOffsetMode的默认值是:

e.Graphics.PixelOffsetMode = PixelOffsetMode.None

在此模式下,与图像顶部和左侧边框相对应的外部像素(在普通图像采样中)绘制在由容器(目标位图或设备上下文)定义的矩形区域的边缘中间。

因此,由于源图像很小且其像素被放大了很多,第一条水平和垂直线的像素明显被切成了两半。
这可以使用其他PixelOffsetMode来解决:

e.Graphics.PixelOffsetMode = PixelOffsetMode.Half

这种模式通过将图像的渲染位置向后移动半个像素来实现。
以下是结果的示例图像,可以更好地解释这一点:

InterpolationMode NearestNeighbor

     Default Filter        InterpolationMode        InterpolationMode
   InterpolationMode        NearestNeighbor          NearestNeighbor
        Bilinear          PixelOffsetMode.None     PixelOffsetMode.Half
                                     

注意:
.Net的MSDN文档没有很好地描述PixelOffsetMode参数。你可以找到6个显然不同的选项。像素偏移模式实际上只有两种:
PixelOffsetMode.None(默认)和PixelOffsetMode.Half

PixelOffsetMode.DefaultPixelOffsetMode.HighSpeedPixelOffsetMode.None相同。
PixelOffsetMode.HighQualityPixelOffsetMode.Half相同。
阅读 .Net 文档,似乎在选择其中一个时有速度影响。实际上差别微不足道。

C++文档(以及GDI+总体),更加明确、精确,应该代替 .Net 文档。

如何进行

我们可以将小源位图绘制到新的、较大的位图上,并将其分配给PictureBox.Image属性。
但是,假设PictureBox大小在某个时刻(因为布局更改和/或DPI感知妥协)发生了变化,我们(几乎)回到了出发点。
一个简单的解决方案是直接在控件表面上绘制新的位图,并在必要时将其保存到磁盘。
这也将允许在需要时缩放位图而不失去质量。

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 是一个辅助方法,用于在容器内缩放图像:

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

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
我喜欢你在缩小图像之前备份完整质量图像的方式,否则当你将其调整为更大尺寸时,它将具有与缩小版本相同的质量。 - TheRealSuicune
我认为没必要将它保存到磁盘上,因为你可以将它存储在一个变量中。然而,我有一个 Button2_Click(...) 函数,它会检查是否有图像,如果有,会将其保存到由 SaveFileDialog1 指定的文件中,当然,前提是点击了“确定”,而不是“取消”。 - TheRealSuicune
Image.Width <> Image.Height 时存在问题。我认为这是因为 Function GetScaledImageRect(...) 没有返回正确的宽高比。 - TheRealSuicune
我不明白你的观点。在缩放模式下看起来完全正常。此外,将位图保存为该大小不是UI控件的工作。另一方面,“让控件后面的代码为您完成工作”正是UI控件的工作。请注意,我只谈到了DrawImage调用。InterpolationModePixelOffsetMode仍然适用;这就是子类化的原因。 - Nyerguds
显示剩余23条评论

3

我曾经看到过一个解决方案,就是创建一个覆盖 PictureBox 的类,该类具有 InterpolationMode 作为类属性。然后,您只需要在UI上使用此类而不是 .Net 自己的 PictureBox,并将该模式设置为 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

正如评论中所指出的,对于最近邻模式,你需要将PixelOffsetMode设置为Half。我真的不明白为什么他们要暴露这个而不是在内部渲染过程中进行自动选择。

大小可以通过设置控件的SizeMode属性来控制。将其设置为Zoom会使其自动居中并扩展而不会在控件的设置大小中裁剪。


在标准的GDI+渲染过程中,卷积核使用外部(透明)像素。其他渲染引擎扩展位图的外部像素超出边界。GDI+改为引入透明像素,而不重新定位/重新计算边缘像素位置。这可能会加速处理速度,但会在边缘创建半透明像素线。 PixelOffsetMode.Half 重新定位像素偏移量,并将其设置为位图边界。但是,当在绘制前设置时,它将成为过程的一部分:没有速度降低。 - Jimi
我认为 Me. 不是必要的。 - TheRealSuicune
实际上,Drawing2D.Graphics. 怎么样? - TheRealSuicune
我认为用 Me. 来引用本地属性总是更加清晰,尤其是当它们与所引用的类型名称相同时。如果您希望,我可以将所需的导入添加到代码中,但并不难找到。 - Nyerguds

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接