unity自带得Texture2d.loadimage可以直接读取,如果你的图片小就直接用,如果图片尺寸过大,那么就可以研究下我的代码

关于通过文件流获取图片宽高参考我之前得文章:获取PNG/JPG/GIF/BMP的宽高

简介

PNG是一种使用无损压缩的图片格式,当原图片数据被编码成PNG格式后,是可以完全还原成原本的图片数据的,PNG保留原始所有的颜色信息,并且支持透明/alpha通道,然后采用无损压缩进行编码。

以下,我们来尝试获取PNG编码的图片数据:

结构

图片是属于二进制文件,因此在拿到PNG图片并想对其进行解析的话,就得以二进制的方式进行读取操作。PNG图片包含两部分:文件头和数据块。

文件头

PNG的文件头就是二进制流的前8个字节,其值为[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]

//设置头
Array.Copy(fileData, 0, png.Herder, 0, png.Herder.Length);

数据块

去掉了PNG图片等前8个字节,剩下的就是存放PNG数据的数据块,我们通常称之为Chunk

数据块就是一段数据,去掉了头的PNG图片数据,不过我们要先知道有哪些类型的数据块才好判断。

数据块格式如下:

描述

长度

数据块内容长度

4字节

数据块类型

4字节

数据块内容

不定字节

crc冗余校验码

4字节

Chunk代码结构如下:

public class Chunk
    {
        public int ChunkLen { get; set; }

        public int ChunkFlag { get; set; }

        public byte[] ChunkData { get; set; }

        public byte[] CRC { get; set; } = new byte[4];

    }

数据块类型

数据块类型有很多种,但是其中大部分我们都不需要用到,因为里面没有存储我们需要用到的数据。我们需要关注的数据块只有以下四种:

  • IHDR:存放图片信息。
  • PLTE:存放索引颜色。
  • IDAT:存放图片数据。
  • IEND:图片数据结束标志。

只要解析这四种数据块就可以获取图片本身的所有数据,因此我们也称这四种数据块为“关键数据块”

IHDR

类型为IHDR的数据块用来存放图片信息,其长度为固定的13个字节:

描述

长度

图片宽度

4字节

图片高度

4字节

图像深度

1字节

颜色类型

1字节

压缩方法

1字节

过滤方式

1字节

扫描方式

1字节

图片深度是指每个像素点中的每个通道占用的位数;

颜色类型用来判断每个像素点中有多少个通道;

压缩方法目前只支持一种(deflate/inflate 压缩算法),其值为0;

过滤方式也只有一种(包含标准的5种过滤类型),其值为0;

扫描方式有两种,一种是逐行扫描,值为0,还有一种是Adam7隔行扫描,其值为1,此次只针对普通的逐行扫描方式进行解析,因此暂时不考虑Adam7隔行扫描。

IHDR代码结构如下:

public class IHDR : Chunk
    {
        //宽
        public int Width { get; set; }
        //高
        public int Height { get; set; }
        //位深 真彩色图像:8或16
        public byte BitDepth { get; set; }
        //颜色类型 6:带α通道数据的真彩色图像,8或16
        public byte ColorType { get; set; }
        //压缩方法(LZ77派生算法) 固定0
        public byte CompressionMethod { get; set; }
        //滤波器方法 固定0
        public byte FilterMethod { get; set; }
        //隔行扫描方法:0:非隔行扫描 1: Adam7(由Adam M.Costello开发的7遍隔行扫描方法)
        public byte InterlaceMethod { get; set; }

        //通道占用的位数
        public int Channel
        {
            get
            {
                switch (ColorType)
                {
                    case 0://灰度图像,只有1个灰色通道
                        return 1;
                    case 2://rgb真彩色图像,有RGB3色通道
                        return 3;
                    case 3://索引颜色图像,只有索引值一个通道
                        return 1;
                    case 4://灰度图像 + alpha通道
                        return 2;
                    default:
                        return 3;
                }
            }
        }
    }

PLTE

类型为PLTE的数据块用来存放索引颜色,我们又称之为“调色板”。通常使用索引颜色的情况下,图像深度的值即为8,因而调色板里存放的颜色就只有256种颜色,长度为256 * 3个字节。再加上1位布尔值表示透明像素,这就是我们常说的png8图片了。

PLTE代码结构如下:

public class PLTE : Chunk
    {
    }

IDAT

类型为IDAT的数据块用来存放图像数据,跟其他关键数据块不同的是,其数量可以是连续复数个;这里的数据得按顺序把所有连续的IDAT数据块全部解析并将数据联合起来才能进行最终处理。

其他关键数据块在PNG文件里有且只有1个。

IDAT代码结构如下:

public class IDAT : Chunk
    {
    }

IEND

当解析到类型为IEND的数据块时,就表明所有的IDAT数据块已经解析完毕,我们就可以停止解析了。

IEND整个数据块的值时固定的:[0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82],因为IEND数据块没有数据块内容,所以其数据块内容长度字段(数据块前4个字节)的值也是0。

 

解析

获取所有IDAT,如有多个需要按顺序连起来

[-4][-3][-2][-1]IDAT长度     [0][1][2][3]IDAT的标志位  [4~len]实际数据   [len~len+4]CRC数据

if (ChunkFlagBytes[0] == 0x49 && ChunkFlagBytes[1] == 0x44 && ChunkFlagBytes[2] == 0x41 && ChunkFlagBytes[3] == 0x54)
{
    //Log.Info("----------------------IDAT----------------------");
    //chunk length
    Array.Copy(fileData, postion - chunkLengthBytes.Length, chunkLengthBytes, 0, chunkLengthBytes.Length);
    Array.Reverse(chunkLengthBytes, 0, 4);
    length = BitConverter.ToInt32(chunkLengthBytes, 0); ;
    png.IDAT.ChunkLen = length;
    //Log.Info("ChunkLen:{0}", png.IDAT.ChunkLen);
    //chunk flag
    png.IDAT.ChunkFlag = BitConverter.ToInt32(ChunkFlagBytes, 0);
    postion += 4;
    //Log.Info("ChunkFlag:{0}", png.IDAT.ChunkFlag);
    //chunk data
    png.IDAT.ChunkData = new byte[length];
    Array.Copy(fileData, postion, png.IDAT.ChunkData, 0, png.IDAT.ChunkData.Length);
    //添加数据
    png.CombineBytesData.AddRange(png.IDAT.ChunkData);
    postion += length;
    //CRC
    Array.Copy(fileData, postion, png.IDAT.CRC, 0, png.IDAT.CRC.Length);
    postion += 4;
    //Log.Info("CRC:{0}", ByteToHexString(png.IDAT.CRC));
    //找下一个
    postion++;
    Array.Copy(fileData, postion, ChunkFlagBytes, 0, ChunkFlagBytes.Length);
}

解压缩

当我们收集完IDAT的所有数据块内容时,我们要先对其进行解压缩:

这里使用得时C#得zlib库;

private byte[] Buffer = new byte[1024 * 1024];
        private MemoryStream OutStream = new MemoryStream();

        public byte[] Analysis(byte[] ChunkData)
        {
            if (ChunkData == null)
                return null;
            MemoryStream inputStream = new MemoryStream(ChunkData);
            Stream outputStream = deCompressStream(inputStream);
            byte[] outputBytes = new byte[outputStream.Length];
            outputStream.Position = 0;
            outputStream.Read(outputBytes, 0, outputBytes.Length);
            outputStream.Close();
            inputStream.Close();

            return outputBytes;
        }

        /// <summary>
        /// 解压缩流
        /// </summary>
        /// <param name="sourceStream">需要被解压缩的流</param>
        /// <returns>解压后的流</returns>
        private Stream deCompressStream(Stream sourceStream)
        {
            ZOutputStream outZStream = new ZOutputStream(OutStream);
            CopyStream(sourceStream, outZStream);
            outZStream.finish();
            return OutStream;
        }

        public void CopyStream(Stream input, Stream output)
        {
            int len;
            while ((len = input.Read(Buffer, 0, Buffer.Length)) > 0)
            {
                output.Write(Buffer, 0, len);
            }
            output.Flush();
        }

过滤

上文我们说过过滤方法只有1种,其中包含5种过滤类型,图像每一行数据里的第一个字节就表示当前行数什么过滤类型。

大多数情况下,图像的相邻像素点的色值时很相近的,而且很容易呈现线性变化(相邻数据的值是相似或有某种规律变化的),因此借由这个特性对图像的数据进行一定程度的压缩。针对这种情况我们常常使用一种叫差分编码的编码方式,即是记录当前数据和某个标准值的差距来存储当前数据。

比如说有这么一个数组[99, 100, 100, 102, 103],我们可以将其转存为[99, 1, 0, 2, 1]。转存的规则就是以数组第1位为标准值,标准值存储原始数据,后续均存储以前1位数据的差值。

当我们使用了差分编码后,再进行deflate压缩的话,效果会更好(deflate压缩是LZ77延伸出来的一种算法)。

使用过滤代码:

// 每像素字节数
    int bytesPerPixel = Math.Max(1, IHDR.Channel * IHDR.BitDepth / 8);
    // 每行字节数
    int bytesPerRow = bytesPerPixel * Width;
    // 存储过滤后的像素数据
    PixelsBuffer = new byte[bytesPerPixel * Width * Height];
    // 当前行的偏移位置
    int offset = 0;
    // 当前行
    byte[] scanline = new byte[bytesPerRow];
    for (int i = 0; i < ParsedBytesData.Length; i += bytesPerRow + 1)
    {
        Array.Copy(ParsedBytesData, i + 1, scanline, 0, bytesPerRow);
        // 第一个字节代表过滤类型
        switch (ParsedBytesData[i])
        {
            case 0:
                filterNone(scanline, bytesPerPixel, bytesPerRow, offset);
            break;
            case 1:
                filterSub(scanline, bytesPerPixel, bytesPerRow, offset);
            break;
            case 2:
                filterUp(scanline, bytesPerPixel, bytesPerRow, offset);
            break;
            case 3:
                filterAverage(scanline, bytesPerPixel, bytesPerRow, offset);
            break;
            case 4:
                filterPaeth(scanline, bytesPerPixel, bytesPerRow, offset);
            break;
            default:
            break;
        }
        offset += bytesPerRow;
    }

过滤类型0:None

这个没啥好解释的,就是完全不做任何过滤。

private void filterNone(byte[] scanline, int bytesPerPixel, int bytesPerRow, int offset)
        {
            for (int i = 0; i < bytesPerRow; i++)
            {
                PixelsBuffer[offset + i] = scanline[i];
            }
        }

过滤类型1:Sub

当前像素和左边像素的差值。左边起第一个像素是标准值,不做任何过滤。

private void filterSub(byte[] scanline, int bytesPerPixel, int bytesPerRow, int offset)
        {
            for (int i = 0; i < bytesPerRow; i++)
            {
                if (i < bytesPerPixel)
                {
                    // 第一个像素,不作解析
                    PixelsBuffer[offset + i] = scanline[i];
                }
                else
                {
                    // 其他像素
                    byte a = PixelsBuffer[offset + i - bytesPerPixel];
                    // ??????检查
                    int value = scanline[i] + a;
                    PixelsBuffer[offset + i] = Convert.ToByte(value & 0xFF);
                }
            }
        }

过滤类型2:Up

当前像素和上边像素点差值。如果当前行是第1行,则当前行数标准值,不做任何过滤。

private void filterUp(byte[] scanline, int bytesPerPixel, int bytesPerRow, int offset)
        {
            if (offset < bytesPerRow)
            {
                // 第一行,不作解析
                for (int i = 0; i < bytesPerRow; i++)
                {
                    PixelsBuffer[offset + i] = scanline[i];
                }
            }
            else
            {
                for (int i = 0; i < bytesPerRow; i++)
                {
                    byte b = PixelsBuffer[offset + i - bytesPerRow];

                    int value = scanline[i] + b;
                    PixelsBuffer[offset + i] = Convert.ToByte(value & 0xFF);
                }
            }
        }

过滤类型3:Average

当前像素与左边像素和上边像素的平均值的差值。

  • 如果当前行数第一行:做特殊的Sub过滤,左边起第一个像素是标准值,不做任何过滤。其他像素记录该像素与左边像素的二分之一的值的差值。

如果当前行数不是第一行:左边起第一个像素记录该像素与上边像素的二分之一的值的差值,其他像素做正常的Average过滤。

private void filterAverage(byte[] scanline, int bytesPerPixel, int bytesPerRow, int offset)
        {
            if (offset < bytesPerRow)
            {
                // 第一行,只做Sub
                for (int i = 0; i < bytesPerRow; i++)
                {
                    if (i < bytesPerPixel)
                    {
                        // 第一个像素,不作解析
                        PixelsBuffer[offset + i] = scanline[i];
                    }
                    else
                    {
                        // 其他像素
                        byte a = PixelsBuffer[offset + i - bytesPerPixel];

                        int value = scanline[i] + (a >> 1); // 需要除以2
                        PixelsBuffer[offset + i] = Convert.ToByte(value & 0xFF);
                    }
                }
            }
            else
            {
                for (int i = 0; i < bytesPerRow; i++)
                {
                    if (i < bytesPerPixel)
                    {
                        // 第一个像素,只做Up
                        byte b = PixelsBuffer[offset + i - bytesPerRow];

                        int value = scanline[i] + (b >> 1); // 需要除以2
                        PixelsBuffer[offset + i] = Convert.ToByte(value & 0xFF);
                    }
                    else
                    {
                        // 其他像素
                        byte a = PixelsBuffer[offset + i - bytesPerPixel];
                        byte b = PixelsBuffer[offset + i - bytesPerRow];

                        int value = scanline[i] + ((a + b) >> 1);
                        PixelsBuffer[offset + i] = Convert.ToByte(value & 0xFF);
                    }
                }
            }
        }

过滤类型4:Paeth

这种过滤方式比较复杂,Pr的计算方式(伪代码)如下:

p = a + b - c
pa = abs(p - a)
pb = abs(p - b)
pc = abs(p - c)
if pa <= pb and pa <= pc then Pr = a
else if pb <= pc then Pr = b
else Pr = c
return Pr
  • 如果当前行数第一行:做Sub过滤。
  • 如果当前行数不是第一行:左边起第一个像素记录该像素与上边像素的差值,其他像素做正常的Peath过滤。
private void filterPaeth(byte[] scanline, int bytesPerPixel, int bytesPerRow, int offset)
        {
            if (offset < bytesPerRow)
            {
                // 第一行,只做Sub
                for (int i = 0; i < bytesPerRow; i++)
                {
                    if (i < bytesPerPixel)
                    {
                        // 第一个像素,不作解析
                        PixelsBuffer[offset + i] = scanline[i];
                    }
                    else
                    {
                        // 其他像素
                        byte a = PixelsBuffer[offset + i - bytesPerPixel];

                        int value = scanline[i] + a;
                        PixelsBuffer[offset + i] = Convert.ToByte(value & 0xFF);
                    }
                }
            }
            else
            {
                for (int i = 0; i < bytesPerRow; i++)
                {
                    if (i < bytesPerPixel)
                    {
                        // 第一个像素,只做Up
                        byte b = PixelsBuffer[offset + i - bytesPerRow];

                        int value = scanline[i] + b;
                        PixelsBuffer[offset + i] = Convert.ToByte(value & 0xFF);
                    }
                    else
                    {
                        // 其他像素
                        byte a = PixelsBuffer[offset + i - bytesPerPixel];
                        byte b = PixelsBuffer[offset + i - bytesPerRow];
                        byte c = PixelsBuffer[offset + i - bytesPerRow - bytesPerPixel];

                        int p = a + b - c;
                        int pa = Math.Abs(p - a);
                        int pb = Math.Abs(p - b);
                        int pc = Math.Abs(p - c);
                        byte pr;

                        if (pa <= pb && pa <= pc) pr = a;
                        else if (pb <= pc) pr = b;
                        else pr = c;

                        int value = scanline[i] + pr;
                        PixelsBuffer[offset + i] = Convert.ToByte(value & 0xFF);
                    }
                }
            }
        }

获取像素

到这里,解析的工作就做完了,上面代码里的PixelsBuffer数组里存的就是像素的数据了,不过我们要如何获取具体某个像素的数据呢?方式可参考下面代码:

private Color PixelColor(int x, int y)
        {
            if (x < 0 || x >= Width || y < 0 || y >= Height)
            {
                Debug.Log("x或y的值超出了图像边界!");
                return Color.white;
            }
            // 每像素字节数
            int bytesPerPixel = Math.Max(1, IHDR.Channel * IHDR.BitDepth / 8);
            int index = bytesPerPixel * (y * Width + x);

            switch (IHDR.ColorType)
            {
                case 0:
                    // 灰度图像
                    return new Color(PixelsBuffer[index] / 255f, PixelsBuffer[index] / 255f, PixelsBuffer[index] / 255f, 1);
                case 2:
                    // rgb真彩色图像
                    return new Color(PixelsBuffer[index] / 255f, PixelsBuffer[index + 1] / 255f, PixelsBuffer[index + 2] / 255f, 1);
                case 3:
                    // 索引颜色图像
                    int paletteIndex = PixelsBuffer[index];
                    return new Color(PLTE.ChunkData[paletteIndex * 3 + 0] / 255f, PLTE.ChunkData[paletteIndex * 3 + 1] / 255f, PLTE.ChunkData[paletteIndex * 3 + 2] / 255f, 1);
                case 4:
                    // 灰度图像 + alpha通道
                    return new Color(PixelsBuffer[index] / 255f, PixelsBuffer[index] / 255f, PixelsBuffer[index] / 255f, PixelsBuffer[index + 1] / 255f);
                case 6:
                    // rgb真彩色图像 + alpha通道
                    return new Color(PixelsBuffer[index] / 255f, PixelsBuffer[index + 1] / 255f, PixelsBuffer[index + 2] / 255f, PixelsBuffer[index + 3] / 255f);
            }
            return Color.white;
        }

小结

如果对完整代码有兴趣的同学可以私信我,也可以下载

在Unity中配合Loom使用:

Loom.RunAsync(() =>
        {
                m_LoadImageThread = new Thread(new ThreadStart(LoadAssert));
                m_LoadImageThread.IsBackground = true;
                m_LoadImageThread.Start();
        });


        private void LoadAssert()
        {
            FileInfo info = (FileInfo)item;
            if (File.Exists(info.FullName) && !ContainsTexture2d(info.FullName))
            {
                byte[] fileData = null;
                Vector2 Size = Vector2.zero;
                FileUtility.ImageType type = FileUtility.FileInfo(info.FullName, out fileData, out Size);
                PNG png = PNG.CreatePNG(fileData);
                Color[] colors = png.GetColor();
                Loom.QueueOnMainThread(() =>
                {
                    string fullPath = info.FullName;
                    int x = (int)Size.x;
                    int y = (int)Size.y;
                    Color[] autoColors = (Color[])colors.Clone();
                    Texture2D tex = new Texture2D(x, y, TextureFormat.RGBA32, false);
                    tex.SetPixels(autoColors);
                    tex.Apply();
                });
             }
        }

也可以把color[]保存下来用协程按行apply显示

注意: Texture2D 读取像素点是从左下角开始一行一行往上读,而获取的像素从左上角一行一行往下读的,所以在对Texture2d赋值颜色的时候要自己做个转化