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

时间:2016-12-20 11:58:10

标签: c# .net bitmap

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

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

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

但是当我使用255获取A值时,会返回正确的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'

以下是获取像素颜色的代码

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

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

2 个答案:

答案 0 :(得分:6)

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

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

差异化的一个提议是色彩管理。但是,这似乎是一个非首发,因为这两个调用都没有要求颜色管理(ICM)支持。

但是,您可以通过检查.NET BCL的源代码来说明问题。在a comment中,mammago发布了ImageBitmap类实施代码的链接,但无法识别相关差异。


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

Image.FromFile()

那里有很多东西,但大部分都不相关。代码的第一部分只是获取并验证路径。之后是重要的一点:调用本地GDI +函​​数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); } the many Bitmap-related functions provided by the GDI+ flat API之一。它完全符合您的想法,它从路径到图像文件创建GdipCreateBitmapFromFile对象,而不使用颜色匹配(ICM)。这是繁重的功能。然后.NET包装器检查错误并再次验证生成的对象。如果验证失败,它会清理并抛出异常。如果验证成功,它会将句柄保存在成员变量(对Bitmap的调用)中,然后调用除了图像(如果是GIF)之外什么都不做的函数(SetNativeImage)。由于这个不是,我们将完全忽略它。

好的,从概念上讲,这只是一个围绕EnsureSave的大而昂贵的包装器,可执行大量冗余验证。


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

false

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


Others have speculated差异可能是这两种不同的原生功能的结果。这是一个很好的理论,但是我对这些函数进行了反汇编,虽然它们有不同的实现,但是没有明显的差异可以解释这里观察到的行为。 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; } 将执行验证,尝试加载元文件(如果可能),然后调用内部GdipCreateBitmapFromFile类的构造函数来执行实际加载。 GpBitmap的实现方式类似,只是它通过内部GdipLoadImageFromFile函数间接到达GpBitmap类构造函数。此外,我无法通过直接在C ++中调用这些本机函数来重现您描述的行为,因此将它们作为解释的候选者。

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

Bitmap

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


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

Image.FromFile

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

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

here 是我们最终找到您在问题中描述的行为的解释!您可以看到它从指定的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(); } } } 对象创建一个临时Graphics对象,用透明颜色填充Image对象,最后绘制指定的{{{ 1}}进入Graphics上下文。此时,与您正在使用的图像相同,但该图像的副本这是色彩匹配可以踢的地方in,以及可能影响图像的各种其他事物。

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

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

答案 1 :(得分:1)

更换

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

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

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