确定图像中是否使用了Alpha通道

15

当我将图片导入我的程序时,我想确定以下几点:

  1. 它们是否有alpha通道
  2. 如果使用了该alpha通道

#1 使用Image.IsAlphaPixelFormat就足够简单了。 对于#2,除了循环遍历每个像素之外,是否有一种简单的方法可以确定至少有一个像素使用了alpha通道(即设置为与255不同的某些其他值)? 我只需要返回一个布尔值,然后根据情况决定是否将其保存为32位或24位。

更新: 我发现ImageFlags.HasTranslucent应该提供我所需的功能,但不幸的是,它根本不起作用。例如,具有至少66(半透明)alpha通道的像素格式的PNG文件仍然报告False(用法:if((img.Flags & ImageFlags.HasTranslucent) == 4) ...;)。 我已经测试了所有类型的图像,包括具有大于0且小于255的alpha值的.bmp图像,但仍然报告False。 有没有人使用过它并知道它在GDI +中是否起作用?


你链接的是本地帮助文档而不是在线版本。 - ChrisF
帮助文档中指出:“指定像素数据具有除0(透明)和255(不透明)以外的alpha值。”这并不完全符合您的要求,因为您还想说0也是true。 - ChrisF
奇怪,我没有本地的帮助副本。不管怎样,是的,HasTranslucent会让我走得很远,但不是100%,因为我仍然需要0值。 - Todd Main
7个回答

10

自从我发布了我的第一个答案以来,我发现LockBits函数实际上可以将图像数据转换为所需的像素格式。这意味着,无论输入是什么,您都可以简单地将字节'as' 32位每像素ARGB数据进行检查。由于该格式具有4个字节的像素,并且在.Net框架中,跨度始终是4个字节的倍数,因此正确调整数据读取以适应扫描线长度通常非常重要成为无关紧要。所有这一切都大大简化了代码。

当然,我其他答案中的前两个检查仍然适用;在切换到完整数据扫描之前,检查位图标志上的HasAlpha标志和索引格式的调色板条目上的alpha是确定图像是否可以具有透明度的非常快速的初始方法。

我还发现具有透明度能力的调色板的索引PNG实际上是存在的(尽管在.Net中支持不佳),因此仅在索引格式上检查单个具有透明度能力的颜色是太幼稚了。

考虑到所有这些,以及将调色板检查转换为一行代码的linq操作,最终调整后的代码变成:

public static Boolean HasTransparency(Bitmap bitmap)
{
    // Not an alpha-capable color format. Note that GDI+ indexed images are alpha-capable on the palette.
    if (((ImageFlags)bitmap.Flags & ImageFlags.HasAlpha) == 0)
        return false;
    // Indexed format, and no alpha colours in the image's palette: immediate pass.
    if ((bitmap.PixelFormat & PixelFormat.Indexed) != 0 && bitmap.Palette.Entries.All(c => c.A == 255))
        return false;
    // Get the byte data 'as 32-bit ARGB'. This offers a converted version of the image data without modifying the original image.
    BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
    Int32 len = bitmap.Height * data.Stride;
    Byte[] bytes = new Byte[len];
    Marshal.Copy(data.Scan0, bytes, 0, len);
    bitmap.UnlockBits(data);
    // Check the alpha bytes in the data. Since the data is little-endian, the actual byte order is [BB GG RR AA]
    for (Int32 i = 3; i < len; i += 4)
        if (bytes[i] != 255)
            return true;
    return false;
}

这适用于任何输入像素格式,无论是调色板格式还是其他格式。

1
非常好的答案!谢谢! - A X
调色板检查是个好主意... - Sreenikethan I

8

您不必遍历每个像素(嗯,可能需要,但这取决于图像)。设置循环所有像素,但仅在找到不等于255的Alpha值时中断循环,使用以下伪代码:

bool hasAlpha = false;
foreach (var pixel in image)
{
    hasAlpha = pixel.Alpha != 255;
    if (hasAlpha)
    {
        break;
    }
}

对于没有任何透明度的图像,您只需要检查所有像素。对于具有透明度的图像,这将很快破解。


谢谢ChrisF。这似乎是唯一的解决方法。我会让问题继续存在一段时间,看看是否有其他解决方案出现。 - Todd Main
@Otaku - 我也很想看看是否有其他的方法。 - ChrisF
哦,伙计,这看起来像是唯一的方法。我本来希望有更简单的方式,但是... - Todd Main
一个小备注:我之前有过这个任务(检查 alpha 通道是否实际使用),所以我想说你应该考虑在循环中使用不安全代码。 - Sergey.quixoticaxis.Ivanov
一旦您确定图像是否不透明,就可以将结果保存到标志中。只要图像保持不变,您就可以检查该标志,而无需重新检查图像。 - Dwedit

8

您不会找到比这更好的解决方案,我花了几个小时来进行优化:

public bool IsAlphaBitmap(ref System.Drawing.Imaging.BitmapData BmpData)
{
    byte[] Bytes = new byte[BmpData.Height * BmpData.Stride];
    Marshal.Copy(BmpData.Scan0, Bytes, 0, Bytes.Length);
    for (p = 3; p < Bytes.Length; p += 4) {
        if (Bytes[p] != 255) return true;
    }
    return false;
}

小心哦,你必须检查每像素的位数才能正确执行此操作。这仅适用于32 bpp图像。 - Nyerguds
1
唯一可能比这更快的是通过“unsafe”和指针直接遍历内存而不进行复制。 - IS4
@IllidanS4 当然,在实践中这是正确的方法。 - Sergey.quixoticaxis.Ivanov

7

将不同类型的图像组合起来使用一些方法,最终得到了这个方法。似乎对于任何你放进去的图像都做得很好,无论是可能透明的gif还是包含alpha通道的png。感谢Elmo的快速字节读取方法。

附注:不要使用Image.IsAlphaPixelFormat(bitmap.PixelFormat)):它认为索引(调色板)格式不支持alpha,而这种图像实际上可以拥有alpha。只是它不是按像素,而是按调色板项来计算。这样启用alpha的8位图像确实启用了HasAlpha标志,所以这仍然是一个有用的检查。

[[注意:我已经大大简化了这个逻辑。请看我的其他回答。]]

public static Boolean HasTransparency(Bitmap bitmap)
{
    // not an alpha-capable color format.
    if ((bitmap.Flags & (Int32)ImageFlags.HasAlpha) == 0)
        return false;
    // Indexed formats. Special case because one index on their palette is configured as THE transparent color.
    if (bitmap.PixelFormat == PixelFormat.Format8bppIndexed || bitmap.PixelFormat == PixelFormat.Format4bppIndexed)
    {
        ColorPalette pal = bitmap.Palette;
        // Find the transparent index on the palette.
        Int32 transCol = -1;
        for (int i = 0; i < pal.Entries.Length; i++)
        {
            Color col = pal.Entries[i];
            if (col.A != 255)
            {
                // Color palettes should only have one index acting as transparency. Not sure if there's a better way of getting it...
                transCol = i;
                break;
            }
        }
        // none of the entries in the palette have transparency information.
        if (transCol == -1)
            return false;
        // Check pixels for existence of the transparent index.
        Int32 colDepth = Image.GetPixelFormatSize(bitmap.PixelFormat);
        BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, bitmap.PixelFormat);
        Int32 stride = data.Stride;
        Byte[] bytes = new Byte[bitmap.Height * stride];
        Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
        bitmap.UnlockBits(data);
        if (colDepth == 8)
        {
            // Last line index.
            Int32 lineMax = bitmap.Width - 1;
            for (Int32 i = 0; i < bytes.Length; i++)
            {
                // Last position to process.
                Int32 linepos = i % stride;
                // Passed last image byte of the line. Abort and go on with loop.
                if (linepos > lineMax)
                    continue;
                Byte b = bytes[i];
                if (b == transCol)
                    return true;
            }
        }
        else if (colDepth == 4)
        {
            // line size in bytes. 1-indexed for the moment.
            Int32 lineMax = bitmap.Width / 2;
            // Check if end of line ends on half a byte.
            Boolean halfByte = bitmap.Width % 2 != 0;
            // If it ends on half a byte, one more needs to be processed.
            // We subtract in the other case instead, to make it 0-indexed right away.
            if (!halfByte)
                lineMax--;
            for (Int32 i = 0; i < bytes.Length; i++)
            {
                // Last position to process.
                Int32 linepos = i % stride;
                // Passed last image byte of the line. Abort and go on with loop.
                if (linepos > lineMax)
                    continue;
                Byte b = bytes[i];
                if ((b & 0x0F) == transCol)
                    return true;
                if (halfByte && linepos == lineMax) // reached last byte of the line. If only half a byte to check on that, abort and go on with loop.
                    continue;
                if (((b & 0xF0) >> 4) == transCol)
                    return true;
            }
        }
        return false;
    }
    if (bitmap.PixelFormat == PixelFormat.Format32bppArgb || bitmap.PixelFormat == PixelFormat.Format32bppPArgb)
    {
        BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, bitmap.PixelFormat);
        Byte[] bytes = new Byte[bitmap.Height * data.Stride];
        Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
        bitmap.UnlockBits(data);
        for (Int32 p = 3; p < bytes.Length; p += 4)
        {
            if (bytes[p] != 255)
                return true;
        }
        return false;
    }
    // Final "screw it all" method. This is pretty slow, but it won't ever be used, unless you
    // encounter some really esoteric types not handled above, like 16bppArgb1555 and 64bppArgb.
    for (Int32 i = 0; i < bitmap.Width; i++)
    {
        for (Int32 j = 0; j < bitmap.Height; j++)
        {
            if (bitmap.GetPixel(i, j).A != 255)
                return true;
        }
    }
    return false;
}

糟糕,修复了调色板图像行位置计算中的一个错误。 - Nyerguds
我有很多带有透明位设置为254的照片。因此,我将该行代码从“if(col.A!= 255)”更改为“if(col.A <254)”。 - mike nelson
哇,那太糟糕了。为什么人们要使用几乎不透明? - Nyerguds
@mikenelson 注意,我刚刚添加了另一个答案,大大简化了这个逻辑;发布后一段时间后,我发现LockBits实际上可以将图像数据转换为其他像素格式,这简化了很多东西。 - Nyerguds

6
我提供了一个更高级的解决方案,基于ChrisF的答案:

public bool IsImageTransparent(Bitmap image,string optionalBgColorGhost)
    {
        for (int i = 0; i < image.Width; i++)
        {
            for (int j = 0; j < image.Height; j++)
            {
                var pixel = image.GetPixel(i, j);
                if (pixel.A != 255)
                    return true;
            }
        }

        //Check 4 corners to check if all of them are with the same color!
        if (!string.IsNullOrEmpty(optionalBgColorGhost))
        {
            if (image.GetPixel(0, 0).ToArgb() == GetColorFromString(optionalBgColorGhost).ToArgb())
            {
                if (image.GetPixel(image.Width - 1, 0).ToArgb() == GetColorFromString(optionalBgColorGhost).ToArgb())
                {
                    if (image.GetPixel(0, image.Height - 1).ToArgb() ==
                        GetColorFromString(optionalBgColorGhost).ToArgb())
                    {
                        if (image.GetPixel(image.Width - 1, image.Height - 1).ToArgb() ==
                            GetColorFromString(optionalBgColorGhost).ToArgb())
                        {
                            return true;
                        }
                    }
                }
            }
        }

        return false;
    }

    public static Color GetColorFromString(string colorHex)
    {
        return ColorTranslator.FromHtml(colorHex);
    }

它具有可选的背景颜色字符串,适用于非透明图像:
使用示例:
IsImageTransparent(new Bitmap(myImg),"#FFFFFF");

不确定额外的代码有什么用,但是...为什么不直接将最后一个参数作为颜色对象而不是字符串呢?更不用说你在代码中解析了5次该字符串,而不是只解析一次,然后将其保存在“Color”类型变量中。 - Nyerguds
1
循环遍历所有像素时,不应使用GetPixel(和SetPixel)。 - Sreenikethan I

1

虽然我知道这个帖子不是关于MagickNet的,但它可能有助于某些人。

Magick.Net提供了Imagick库的包装器,并包括一个轻松访问通道统计信息的功能。

示例

    public bool HasTransparentBackground(MagickImage image)
    {
        if (!image.HasAlpha) return false;

        var statistics = image.Statistics();
        var alphaStats = statistics.GetChannel(PixelChannel.Alpha);

        var alphaMax = Math.Pow(2, alphaStats.Depth);
        return alphaStats.Minimum < alphaMax * .2;
    }

我们首先检查图像是否支持透明度,如果不支持则返回。然后我们获取alpha通道的统计信息,并可简单地检查Min属性。还有一个Mean属性,可以检查图像的“透明度”。
参见: - 使用ImageMagick检测Alpha通道 - https://github.com/dlemstra/Magick.NET

0

使用ImageMagick(命令行)的最简单方法是测试alpha通道,如果平均值小于1(在0到1的范围内),则为不透明。 1表示完全不透明。因此

convert image -channel a -separate -format "%[fx:(mean<1)?1:0]" info:

如果返回值为1,则至少有一个像素是透明的;否则(如果为0),图像完全不透明。


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