从PNG图像中使用GetPixel()获取的RGB值错误。

3
这是问题所在:
我使用尺寸为1 x 1像素的颜色为ARGB(50,210,102,70).png格式位图进行保存。

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

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

然而,当我使用255作为A值时,返回正确的RGB值。

因此,使用小于255A值会导致上述问题。

以下是保存位图的代码。

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)

enter image description here

附言:我读了几个类似的问题,但它们没有解决这个确切的问题,我还没有找到解决方案。


1
尝试不使用 Image.FromFile() 部分,像这样:Bitmap bit = new Bitmap("1.png");。对我有效。 - mammago
我会尝试并回复! - Jones Joseph
哇,这非常简单。谢谢@mammago。现在它可以工作了!您可以将其作为答案放置,这样我就可以标记为已回答! - Jones Joseph
2个回答

7

mammago已经找到了一种解决方法, 即使用类构造函数直接从文件构造Bitmap对象,而不是通过Image.FromFile()间接构造Bitmap对象。

这个答案的目的是解释为什么这样做可行,并特别说明两种方法之间实际上的区别是什么,导致获得不同的每像素颜色值。

有人提出可能是颜色管理的问题。然而,这似乎行不通,因为两种调用都没有要求颜色管理(ICM)支持。

您可以通过检查.NET BCL的源代码来了解很多信息。在一条评论中,mammago发布了ImageBitmap类实现的代码链接,但无法确定相关差异。


让我们从直接从文件创建Bitmap对象的Bitmap类构造函数开始,因为这是最简单的:

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+函数GdipCreateBitmapFromFile,其中之一是 GDI+ flat API提供的许多与位图相关的函数。它正是你所想的那样,它从指向图像文件的路径创建一个Bitmap对象,而不使用颜色匹配(ICM)。这个函数承担了大部分工作。然后.NET包装器检查错误并再次验证结果对象。如果验证失败,则清理并抛出异常。如果验证成功,则将句柄保存在成员变量中(调用SetNativeImage),然后调用一个函数(EnsureSave),该函数除非图片是GIF,否则什么都不做。由于这个不是,我们将完全忽略它。
好的,概念上,这只是一个围绕GdipCreateBitmapFromFile的大而昂贵的包装器,执行一堆冗余验证。


关于Image.FromFile()呢?实际上,你正在调用的重载只是一个桩函数,它会转发到另一个重载,传递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其他人推测这种差异可能是这两个不同的本地函数的结果。这是一个好理论,但我反汇编了这些函数,尽管它们具有不同的实现,但没有明显的差异可以解释在此处观察到的行为。GdipCreateBitmapFromFile将执行验证,尝试加载元文件(如果可能),然后调用内部GpBitmap类的构造函数来进行实际加载。GdipLoadImageFromFile的实现类似,只是通过内部GpImage::LoadImage函数间接到达GpBitmap类构造函数。此外,我无法通过在C++中直接调用这些本地函数来复现您所描述的行为,因此将它们排除为解释的候选者。
有趣的是,我也无法通过将Image.FromFile的结果转换为Bitmap来重现您所描述的行为,例如:
Bitmap bit = (Bitmap)(Image.FromFile("1.png")); 

虽然依赖它并不是一个好主意,但如果您回到Image.FromFile的源代码,您会发现这实际上是合法的。它调用内部的CreateImageObject函数,该函数根据正在加载的图像的实际类型委托给Bitmap.FromGDIplusMetafile.FromGDIplusBitmap.FromGDIplus函数只是构造了一个Bitmap对象,并调用我们已经看过的SetNativeImage函数来设置其底层句柄,然后返回该Bitmap对象。因此,当您从文件中加载位图图像时,Image.FromFile实际上返回一个Bitmap对象。而且,这个Bitmap对象的行为与使用Bitmap类构造函数创建的对象完全相同。
关键在于创建一个基于Image.FromFile的结果的Bitmap对象,这正是您原先的代码所做的:
Bitmap bit = new Bitmap(Image.FromFile("1.png"));

这将调用接受Image对象的Bitmap类构造函数, 该构造函数在内部委托给接受显式尺寸的构造函数
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();
        }
    }
}

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

实际上,除了问题中描述的意外行为之外,你编写的代码还隐藏了一个错误:它未能处理由Image.FromFile创建的临时Image对象!

谜团解开了。很抱歉回答过程有些冗长,但希望它能教给你一些关于调试的知识!请继续使用mammago推荐的解决方案,因为它既简单又正确。


1
替换

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

使用

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

应该能解决问题。 看起来Image.FromFile()不如Bitmap构造函数精确。


1
有解释吗? - fubo
1
很抱歉,我无法解释那种行为。如果您想进行研究,可以查看System.Drawing源代码中的实现。我发现这两种方法在底层GDI+ dll中使用了不同的函数,但我无法告诉您具体发生了什么,因为我不是C++开发人员。源代码链接:BitmapImage - mammago
1
请查看此处的更多链接和猜测:https://dev59.com/GFwY5IYBdhLWcg3wyqaH。最有可能的是其中一个使用了颜色管理,但两种方法都有一个重载可以让您控制它,但我没有发现任何区别。这并不令人惊讶,因为我猜没有创建颜色管理。 - TaW
2
这个回答和评论引起了我的注意,所以我决定进行一些调查。这实际上是非常奇怪的。我无法在使用GDI+ API的C++中重现此行为,但是我仍然可以使用.NET BCL提供的Graphics包装器类来看到它。仍在努力尝试确定.NET包装器中的差异。 - Cody Gray

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