开发者

Determine if Alpha Channel is Used in an Image

开发者 https://www.devze.com 2023-01-03 16:35 出处:网络
As I\'m bringing in images into my program, I want to determine if: they have an alpha-channel if that alpha-channel is used

As I'm bringing in images into my program, I want to determine if:

  1. they have an alpha-channel
  2. if that alpha-channel is used

#1 is simple enough with using Image.IsAlphaPixelFormat. For #2 though, other than looping through every single pixel, is there开发者_运维百科 a simple way I can determine if at least one of the pixels has an alpha channel that is used (i.e. set to some other value than 255)? All I need back is a boolean and then I'll make determination as to whether to save it out to 32-bit or 24-bit.

UPDATE: I have discovered that ImageFlags.HasTranslucent should provide me with what I'm looking for - unfortunately, it doesn't work at all. For example, PNGs with pixel formats that have at least alpha channel of 66 (semi-transparent) continue to report False (Usage: if((img.Flags & ImageFlags.HasTranslucent) == 4) ...;). I've tested on all types of images, including .bmp that have an alpha value >0 and <255 and it still reports False. Anyone ever use this and know if it even works in GDI+?


You don't have to loop through every pixel (well you might, but it depends on the image). Set up to loop over all the pixels, but just break out of the loop when you find an alpha value other than 255 use the following pseudo code:

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

You'll only have to check all the pixels for images that don't have any alpha. For images that do have alpha this will break out quite quickly.


You won't find a solution better than this, it took me hours to optimize:

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;
}


Since posting my first answer here , I found out that the LockBits function can actually convert image data to a desired pixel format. This means that, no matter the input, you can simply check the bytes 'as' 32 bit per pixel ARGB data. Since that format has 4-byte pixels, and the stride in the .Net framework is always a multiple of 4 bytes, the normally very important issue of correctly adjusting data reading to scanline lengths becomes irrelevant. This all vastly simplifies the code.

Of course, the first two checks from my other answer still apply; checking the HasAlpha flag on the bitmap flags and the alpha on the palette entries of indexed formats is a very quick initial way to determine if an image can have transparency, before switching to the full data sweep.

I have also since found out that indexed png with alpha-capable palettes is actually a thing (though poorly supported in .Net), so only checking on a single alpha-capable colour on indexed formats is too naive.

With all that in mind, and a linq operation that turns the palette check into a one-liner, the final adjusted code becomes this:

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;
}

This works for any input pixel format, be it paletted or not.


Combining a bunch of methods for different types of images got me this final method, which seems to do a good job for any image you dump into it, be it a potentially transparent gif or a png containing an alpha channel. Thanks to Elmo's answer for the fast byte reading method.

Side note: do not use Image.IsAlphaPixelFormat(bitmap.PixelFormat)): it sees indexed (paletted) formats as non-alpha-capable, while such images can in fact possess alpha. Just, not per pixel, but per palette entry. Such alpha-enabled 8-bit images do have the HasAlpha flag enabled, though, so that's still a useful check.

[[Note: I have since vastly simplified this logic. See my other answer.]]

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;
}


I get a more advanced solution, based on ChrisF answer:

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);
    }

It has a optional bg color string to non transparent images:

Example of usage:

IsImageTransparent(new Bitmap(myImg),"#FFFFFF");


While I know the OP is not about MagickNet, it might help s/o.

Magick.Net provides a wrapper around Imagick lib and includes a feature to easily access channel statistics.

Example

    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;
    }

We first check if the image supports transparency an return if not. Then we get statistics for alpha channel and can simply check the Min property. There's also a Mean property that allows you to check "how much transparent" your image is.

See also

  • Detect Alpha Channel with ImageMagick
  • https://github.com/dlemstra/Magick.NET


The simplest method using ImageMagick (command line) is to test the alpha channel if the mean is less than 1 (on a scale of 0 to 1). 1 is fully opaque. So

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

If the return value is 1, then at least one pixel is transparent; otherwise (if 0), the image is fully opaque.

0

精彩评论

暂无评论...
验证码 换一张
取 消