【问题标题】:Wrong RGB values in GetPixel() from png image来自 png 图像的 GetPixel() 中的错误 RGB 值
【发布时间】:2016-12-20 11:58:10
【问题描述】:

这就是问题所在:
我在.png 中保存了一个位图,颜色为ARGB(50,210,102,70),尺寸为1 x 1 pixel

我再次检索相同的图像并使用GetPixel(0,0) 方法,我得到的是ARGB(50,209,102,70)

检索到的值略有不同,RGB 值略有不同,但A 值保持不变。

但是,当我将255 用于A 值时,会返回正确的RGB 值。

所以,.. 为A 使用小于255 的值会导致上述问题。

这是保存位图的代码。

Bitmap bmpPut = new Bitmap(1, 1); //Also tried with 'PixelFormat.Format32bppArgb'
bmpPut.SetPixel(0, 0, Color.FromArgb(254, 220, 210, 70)); 
bmpPut.Save("1.png"); //Also tried with using 'ImageFormat.Png'

这是获取像素颜色的代码

Bitmap bit = new Bitmap(Image.FromFile("1.png"));
MessageBox.Show("R:" + bit.GetPixel(0, 0).R.ToString() +
    "| G: " + bit.GetPixel(0, 0).G.ToString() +
    "| B: " + bit.GetPixel(0, 0).B.ToString() +
    "| A: " + bit.GetPixel(0, 0).A.ToString());

我得到的是ARGB(254,219,209,70)

P.S.:我读了几个类似的问题,他们没有解决这个确切的问题,我还没有找到解决方案。

【问题讨论】:

  • 尝试不使用Image.FromFile() 部分,如下所示:Bitmap bit = new Bitmap("1.png");。为我工作。
  • 我会试试然后回来!
  • 哇,这很简单。谢谢@mammago。它现在有效!你可以把它作为答案!这样我就可以将其标记为已回答!

标签: c# .net bitmap


【解决方案1】:

mammago has found a workaround,即使用类构造函数直接从文件构造Bitmap对象,而不是通过Image.FromFile()返回的Image对象间接构造Bitmap对象。

此答案的目的是解释 为什么 有效,特别是,导致不同的每像素颜色值的两种方法之间的实际差异是什么获得。

一个关于差异的建议是色彩管理。然而,这似乎不是一个开始,因为这两个调用都没有要求颜色管理 (ICM) 支持。

但是,您可以通过检查 .NET BCL 的源代码来了解很多信息。在 a comment 中,mammago 发布了指向 ImageBitmap 类实现的代码的链接,但无法辨别相关差异。


让我们从Bitmap class constructor that creates a Bitmap object directly from a file 开始,因为这是最简单的:

public Bitmap(String filename) {
    IntSecurity.DemandReadFileIO(filename);

    // GDI+ will read this file multiple times.  Get the fully qualified path
    // so if our app changes default directory we won't get an error
    filename = Path.GetFullPath(filename);

    IntPtr bitmap = IntPtr.Zero;

    int status = SafeNativeMethods.Gdip.GdipCreateBitmapFromFile(filename, out bitmap);

    if (status != SafeNativeMethods.Gdip.Ok)
        throw SafeNativeMethods.Gdip.StatusException(status);

    status = SafeNativeMethods.Gdip.GdipImageForceValidation(new HandleRef(null, bitmap));

    if (status != SafeNativeMethods.Gdip.Ok) {
        SafeNativeMethods.Gdip.GdipDisposeImage(new HandleRef(null, bitmap));
        throw SafeNativeMethods.Gdip.StatusException(status);
    }

    SetNativeImage(bitmap);

    EnsureSave(this, filename, null);
}

那里发生了很多事情,但大部分都不相关。代码的前几位只是获取并验证路径。之后是重要的一点:调用本机 GDI+ 函数 GdipCreateBitmapFromFilethe many Bitmap-related functions provided by the GDI+ flat API 之一。它完全按照您的想法进行,它从图像文件的路径创建Bitmap 对象,而不使用颜色匹配 (ICM)。这是完成繁重工作的功能。 .NET 包装器然后检查错误并再次验证生成的对象。如果验证失败,它会清理并抛出异常。如果验证成功,它将句柄保存在成员变量中(对SetNativeImage 的调用),然后调用一个函数(EnsureSave),除非图像是 GIF,否则该函数什么都不做。由于这不是,我们将完全忽略它。

好的,所以从概念上讲,这只是 GdipCreateBitmapFromFile 的一个大而昂贵的包装器,它执行一堆冗余验证。


Image.FromFile() 呢?好吧,the overload you're actually calling 只是一个转发到the other overload 的存根,通过false 表示不需要颜色匹配(ICM)。有趣的重载代码如下:

public static Image FromFile(String filename,
                             bool useEmbeddedColorManagement) {
    if (!File.Exists(filename)) {
        IntSecurity.DemandReadFileIO(filename);
        throw new FileNotFoundException(filename);
    }

    // GDI+ will read this file multiple times.  Get the fully qualified path
    // so if our app changes default directory we won't get an error
    filename = Path.GetFullPath(filename);

    IntPtr image = IntPtr.Zero;
    int status;

    if (useEmbeddedColorManagement) {
        status = SafeNativeMethods.Gdip.GdipLoadImageFromFileICM(filename, out image);
    }
    else {
        status = SafeNativeMethods.Gdip.GdipLoadImageFromFile(filename, out image);
    }

    if (status != SafeNativeMethods.Gdip.Ok)
        throw SafeNativeMethods.Gdip.StatusException(status);

    status = SafeNativeMethods.Gdip.GdipImageForceValidation(new HandleRef(null, image));

    if (status != SafeNativeMethods.Gdip.Ok) {
        SafeNativeMethods.Gdip.GdipDisposeImage(new HandleRef(null, image));
        throw SafeNativeMethods.Gdip.StatusException(status);
    }

    Image img = CreateImageObject(image);

    EnsureSave(img, filename, null);

    return img;
}

这看起来非常相似。它以稍微不同的方式验证文件名,但这并没有失败,所以我们可以忽略这些差异。如果未请求嵌入式颜色管理,它会委托给另一个原生 GDI+ 平面 API 函数来完成繁重的工作:GdipLoadImageFromFile


Others have speculated 表示差异可能是这两个不同的原生函数造成的。这是一个很好的理论,但我分解了这些函数,尽管它们有不同的实现,但没有显着差异可以解释这里观察到的行为。 GdipCreateBitmapFromFile 将执行验证,如果可能,尝试加载元文件,然后调用内部GpBitmap 类的构造函数来进行实际加载。 GdipLoadImageFromFile 实现类似,只是它通过内部GpImage::LoadImage 函数间接到达GpBitmap 类构造函数。此外,我无法通过直接在 C++ 中调用这些本机函数来重现您所描述的行为,因此将它们排除为解释的候选对象。

有趣的是,我也无法通过将Image.FromFile 的结果转换为Bitmap 来重现您描述的行为,例如:

Bitmap bit = (Bitmap)(Image.FromFile("1.png")); 

虽然依赖它不是一个好主意,但如果你回到Image.FromFile 的源代码,你会发现这实际上是合法的。它调用the internal CreateImageObject function,它根据正在加载的图像的实际类型将Bitmap.FromGDIplus 委托给Metafile.FromGDIplusThe Bitmap.FromGDIplus function 只是构造了一个Bitmap 对象,调用我们已经看到的SetNativeImage 函数来设置它的底层句柄,然后返回那个Bitmap 对象。因此,当您从文件加载位图图像时,Image.FromFile 实际上会返回一个Bitmap 对象。这个Bitmap 对象的行为与使用Bitmap 类构造函数创建的对象相同。


重现该行为的关键是根据Image.FromFile 的结果创建一个 Bitmap 对象,这正是您的原始代码所做的:

Bitmap bit = new Bitmap(Image.FromFile("1.png"));

这将调用the Bitmap class constructor that takes an Image object,它在内部委托给one that takes explicit dimensions

public Bitmap(Image original, int width, int height) : this(width, height) {
    Graphics g = null;
    try {
        g = Graphics.FromImage(this);
        g.Clear(Color.Transparent);
        g.DrawImage(original, 0, 0, width, height);
    }
    finally {
        if (g != null) {
            g.Dispose();
        }
    }
}

here 是我们最终为您在问题中描述的行为找到解释的地方!可以看到 它从指定的Image 对象创建一个临时的Graphics 对象,用透明颜色填充Graphics 对象,最后将指定Image 的副本绘制到那个Graphics 中语境。此时,它不是您正在使用的同一图像,而是该图像的副本这就是颜色匹配可以发挥作用的地方,以及可能影响图像的各种其他因素。

事实上,除了问题中描述的意外行为之外,您编写的代码还隐藏了一个错误:它无法释放由Image.FromFile 创建的临时Image 对象! p>

谜团解开了。为冗长的间接答案道歉,但希望它教会了你一些关于调试的知识!请继续使用 mammago 推荐的解决方案,因为它既简单又正确。

【讨论】:

    【解决方案2】:

    替换

    Bitmap bit = new Bitmap(Image.FromFile("1.png")); 
    

    Bitmap bit = new Bitmap("1.png");
    

    应该做的伎俩。 Image.FromFile() 似乎不如 Bitmap 构造函数精确。

    【讨论】:

    • 对此有解释吗?
    • 很抱歉,我无法解释这种行为。如果您想研究,您可以查看 System.Drawing 源中两者的实现。我看到这两种方法在底层 GDI+ dll 中使用了不同的函数,但我不能告诉你到底发生了什么,因为我不是 C++ 开发人员。源代码链接到BitmapImage
    • See here 获取更多链接和猜测。最有可能的是使用颜色管理。这两种方法都有一个重载,可以让你控制它,但我发现没有区别。我猜,没有创建 cm 也就不足为奇了。
    • 这个答案和 cmets 引起了我的注意,所以我决定调查一下。这实际上完全很奇怪。我无法使用 GDI+ API 在 C++ 中重现此行为,但我确实使用 .NET BCL 提供的 Graphics 包装类看到它。仍在努力找出 .NET 包装器中的差异。
    猜你喜欢
    • 1970-01-01
    • 2021-08-02
    • 2019-07-26
    • 2012-06-13
    • 1970-01-01
    • 1970-01-01
    • 2019-09-29
    • 2021-01-08
    • 2014-09-19
    相关资源
    最近更新 更多