来自png图像的GetPixel()中的RGB值错误

Jon*_*eph 3 .net c# bitmap

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

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

检索值略有变化,RGB值略有不同但A值保持不变.

然而,当我使用255A价值,正确的RGB返回值.

因此,使用..不到值255用于A在上述问题的结果.

这是保存位图的代码.

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'
Run Code Online (Sandbox Code Playgroud)

这是获取像素颜色的代码

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());
Run Code Online (Sandbox Code Playgroud)

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

在此输入图像描述

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

Cod*_*ray 6

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

这个答案的目的是解释为什么会起作用,特别是两种方法之间的实际差异是什么导致获得不同的每像素颜色值.

差异的一个提议是色彩管理.但是,这似乎是一个非首发,因为两种调用都不需要颜色管理(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);
}
Run Code Online (Sandbox Code Playgroud)

那里有很多东西,但大部分内容都不相关.代码的第一部分只是获取并验证路径.之后是重要的一点:调用本机GDI +函​​数GdipCreateBitmapFromFile,这是GDI + flat API提供的许多与Bitmap相关的函数之一.它完全符合您的想法,它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;
}
Run Code Online (Sandbox Code Playgroud)

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


其他人推测这种差异可能是这两种不同的原生功能的结果.这是一个很好的理论,但我反汇编了这些函数,虽然它们有不同的实现,但没有明显的差异可以解释这里观察到的行为.GdipCreateBitmapFromFile将执行验证,尝试加载元文件(如果可能),然后调用内部GpBitmap类的构造函数来执行实际加载.GdipLoadImageFromFile类似地实现,除了它GpBitmap通过内部GpImage::LoadImage函数间接到达类构造函数.此外,我无法通过直接在C++中调用这些本机函数来重现您描述的行为,因此将它们作为解释的候选者.

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

Bitmap bit = (Bitmap)(Image.FromFile("1.png")); 
Run Code Online (Sandbox Code Playgroud)

虽然依靠它不是一个好主意,但你可以看到,如果你回到源代码,这实际上是合法的Image.FromFile.它调用内部CreateImageObject功能,其代表要么Bitmap.FromGDIplusMetafile.FromGDIplus被加载根据实际类型的图像.Bitmap.FromGDIplus函数只构造一个Bitmap对象,调用SetNativeImage我们已经看到的函数来设置它的底层句柄,并返回该Bitmap对象.因此,从文件加载位图图像时,Image.FromFile实际上会返回一个Bitmap对象.此Bitmap对象的行为与使用Bitmap类构造函数创建的对象相同.


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

Bitmap bit = new Bitmap(Image.FromFile("1.png"));
Run Code Online (Sandbox Code Playgroud)

这将调用接受一个对象Bitmap类构造函数,该Image对象在内部委托给一个采用显式维度的对象:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这里就是我们终于找到你在问题中描述的行为的解释!您可以看到Graphics从指定Image对象创建临时对象,Graphics用透明颜色填充对象,最后将指定的副本绘制Image到该Graphics上下文中.在这一点上,它是不是你正在使用的相同的图像,但复制该图像.这是色彩匹配可能引发的地方,以及可能影响图像的各种其他事物.

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

谜团已揭开.为长期,间接的答案道歉,但希望它已经教会了一些关于调试的东西!继续使用mammago推荐的解决方案,因为它既简单又正确.