C#读取TIFF文件

0 引言

最近想在Unity中加载一张TIFF图片,因为该图片存储的是海洋流场数据,所以每个像素存的是四通道的32位float,并且还采用了LZW压缩。在网上找了很多读取TIFF文件的代码,也试了下载FreeImage.Net包,但都无法读取该格式的TIFF。与其继续在网上找下去,还不如自己写一个。
代码在这:OSC_TIFF

1 TIFF图像格式详解

要解码TIFF,首先要了解TIFF文件格式。当然是去看官方提供的说明文档最为直接。
TIFF 6.0
除此之外,一些中文博客也提供了很好的讲解。可以直接参考这篇文章:
解码TIFF文件

2 C#解码TIFF图像

解码TIFF实际上是个很简单的工作,只要有耐心读官方的说明文档,人人都可以自己写代码解码TIFF,只不过TIFF格式的图像种类太多,要想适用于所有的TIFF文件,对于个人来说是件非常耗时的事情。

下面我就来针对我自己想要解码的图像(32位Float * 四通道),来做一个解码小程序,希望也能对其他人有一点点帮助。

我这里适用C#来解码图像,但其实用什么语言并不影响,逻辑对的就行。

2.1

我们先写个TIFF类,里面主要放TIFF图像的各种属性和解码用到的函数。

public class TIFF
{
byte[] data;//把TIFF文件读到byte数组中

//接下来是TIFF文件的各种属性
bool ByteOrder;//true:II  false:MM
public int ImageWidth = 0;
public int ImageLength = 0;
public List<int> BitsPerSample = new List<int>();
public int PixelBytes = 0;
public int Compression = 0;
public int PhotometricInterpretation = 0;
public List<int> StripOffsets = new List<int>();
public int RowsPerStrip = 0;
public List<int> StripByteCounts = new List<int>();
public float XResolution = 0f;
public float YResolution = 0f;
public int ResolutionUnit = 0;
public int Predictor = 0;
public List<int> SampleFormat = new List<int>();
public string DateTime = "";
public string Software = "";

public void Decode(string path){
//...
}
private int DecodeIFH(){
//...
}
public int DecodeIFD(int Pos){
//...
}
private void DecodeDE(int Pos){
//...
}
private void GetDEValue(int TagIndex, int TypeIndex, int Count, byte[] val){
//...
}
private void DecodeStrips(){
//...
}
static private DType[] TypeArray = {
//...
};
struct DType
{
    public DType(string n, int s)
    {   //...
     }
    public string name;
    public int size;
}

我们从Init函数开始。

public void Decode(string path)
        {
            data = File.ReadAllBytes(path);

            //首先解码文件头,获得编码方式是大端还是小端,以及第一个IFD的位置
            int pIFD = DecodeIFH();

            //然后解码第一个IFD,返回值是下一个IFD的地址
            while (pIFD != 0)
            {
                pIFD = DecodeIFD(pIFD);
            }
        }

Decode函数的参数是TIFF文件的位置,我们把文件数据读进来,放在byte数组中。接下来,我们需要解码TIFF文件中的各种信息。首先解码的是IFH,它可以告诉我们文件的编码方式,这直接影响了我们如何将byte数组转换成Int、Float等类型。

private int DecodeIFH()
        {
            string byteOrder = GetString(0,2);
            if (byteOrder == "II")
                ByteOrder = true;
            else if (byteOrder == "MM")
                ByteOrder = false;
            else
                throw new UnityException("The order value is not II or MM.");

            int Version = GetInt(2, 2);

            if (Version != 42)
                throw new UnityException("Not TIFF.");

            return GetInt(4, 4);
        }

来看看II和MM的区别,它将影响后面GetInt和GetFloat函数

private int GetInt(int startPos, int Length)
        {
            int value = 0;
            if (ByteOrder)// "II")
                for (int i = 0; i < Length; i++) value |= data[startPos + i] << i * 8;
            else // "MM")
                for (int i = 0; i < Length; i++) value |= data[startPos + Length - 1 - i] << i * 8;
            return value;
        }
        private float GetRational(int startPos)
        {
            int A = GetInt(startPos,4);
            int B = GetInt(startPos+4,4);
            return A / B;
        }
        private float GetFloat(byte[] b, int startPos)
        {
            byte[] byteTemp;
            if (ByteOrder)// "II")
                byteTemp =new byte[]{b[startPos],b[startPos+1],b[startPos+2],b[startPos+3]};
            else
                byteTemp =new byte[]{b[startPos+3],b[startPos+2],b[startPos+1],b[startPos]};
            float fTemp = BitConverter.ToSingle(byteTemp,0);
            return fTemp;
        }
        private string GetString(int startPos, int Length)//II和MM对String没有影响
        {
            string tmp = "";
            for (int i = 0; i < Length; i++)
                tmp += (char)data[startPos];
            return tmp;
        }

读出的第二个数据是值为42的标志位,它是TIFF文件的标志。因为我是用在Unity中的,所以使用的是Unity中的抛出异常。可以删掉或替换程其他形式,这个无关紧要。

Decode函数的最后一部分是一个while循环,不停的解码IFD,直到读完所有的IFD文件。DecodeIFD这个函数返回的是下一个IFD的位置,如果返回的是0的话,就说明读完了,也就是说整个文件读完了。不过一般的TIFF,比如我的这个,只有一个IFD文件。(可能多页TIFF会有多个IFD文件吧,但这个我还没有验证过)

public int DecodeIFD(int Pos)
        {
            int n = Pos;
            int DECount = GetInt(n, 2);
            n += 2;
            for (int i = 0; i < DECount; i++)
            {
                DecodeDE(n);
                n += 12;
            }
            //已获得每条扫描线位置,大小,压缩方式和数据类型,接下来进行解码
            DecodeStrips();
            int pNext = GetInt(n, 4);
            return pNext;
        }

每个IFD文件里存的第一个信息是该IFD中DE的个数。DE里存的就是我们要读取的TIFF文件信息。每个DE占12字节,因此我们先用个循环,解码所有的DE,在这个过程中,我们将会获得TIFF图像的高度、宽度、压缩方式、图像数据的开始位置等信息。在这之后,就到了解码扫描线数据的环节。

我们先来看看DE的解码

public void DecodeDE(int Pos)
        {
            int TagIndex = GetInt(Pos, 2);
            int TypeIndex = GetInt(Pos + 2, 2);
            int Count = GetInt(Pos + 4, 4);
            //Debug.Log("Tag: " + Tag(TagIndex) + " DataType: " + TypeArray[TypeIndex].name + " Count: " + Count);

            //先把找到数据的位置
            int pData = Pos + 8;
            int totalSize = TypeArray[TypeIndex].size * Count;
            if (totalSize > 4)
                pData = GetInt(pData, 4);

            //再根据Tag把值读出并存起来
            GetDEValue(TagIndex, TypeIndex, Count, pData);
        }

对于每一个DE,首先解码前两个字符,它存的是改DE的标签,根据标签我们就可以找到该DE存的是什么值(见表4)。然后再解码两个字符,它存的是该DE存放的数据的类型号,根据类型号可以找到数据类型(见表1)。在代码中,我写了个结构体DType存数据类型的名称和长度,有创建了一个DType的数组存放12种数据类型,数组的下标正好队形类型号。

struct DType
{
    public DType(string n, int s)
    {
        name = n;
        size = s;
    }
    public string name;
    public int size;
}
static private DType[] TypeArray = {
                new DType("???",0),
                new DType("byte",1), //8-bit unsigned integer
                new DType("ascii",1),//8-bit byte that contains a 7-bit ASCII code; the last byte must be NUL (binary zero)
                new DType("short",2),//16-bit (2-byte) unsigned integer.
                new DType("long",4),//32-bit (4-byte) unsigned integer.
                new DType("rational",8),//Two LONGs: the first represents the numerator of a fraction; the second, the denominator.
                new DType("sbyte",1),//An 8-bit signed (twos-complement) integer
                new DType("undefined",1),//An 8-bit byte that may contain anything, depending on the definition of the field
                new DType("sshort",1),//A 16-bit (2-byte) signed (twos-complement) integer.
                new DType("slong",1),// A 32-bit (4-byte) signed (twos-complement) integer.
                new DType("srational",1),//Two SLONG’s: the first represents the numerator of a fraction, the second the denominator.
                new DType("float",4),//Single precision (4-byte) IEEE format
                new DType("double",8)//Double precision (8-byte) IEEE format
                };
        

接着解码四个字节,这四个字节存的是数据的个数,因为有的数据是数组,比如每个通道的bit数,RGBA图像有4个。我的TIFF文件是128位的RGBA,所以我的BitsPerSample这一项是32,32,32,32四个数。

一般DE中数据的存放位置是该DE的第8到第12个字节。而像存放数组的,或者存的数据比较大的DE,这4个字节只存数据的位置,数据放在其他地方。因此,我们先要根据数据所占字节数,判断数据的其实位置。

//先把找到数据的位置
int pData = Pos + 8;
int totalSize = TypeArray[TypeIndex].size * Count;
if (totalSize > 4)
    pData = GetInt(pData, 4);

找到数据位置之后,再把数据读出来。根据标签,把TIFF类里对应的属性值填上(见表4)

private void GetDEValue(int TagIndex, int TypeIndex, int Count, int pdata)
        {
            int typesize = TypeArray[TypeIndex].size;
            switch (TagIndex)
            {
                case 254: break;//NewSubfileType
                case 255: break;//SubfileType
                case 256://ImageWidth
                        ImageWidth = GetInt(pdata,typesize);break;
                case 257://ImageLength
                    if (TypeIndex == 3)//short
                        ImageLength = GetInt(pdata,typesize);break;
                case 258://BitsPerSample
                    for (int i = 0; i < Count; i++)
                    {
                        int v = GetInt(pdata+i*typesize,typesize);
                        BitsPerSample.Add(v);
                        PixelBytes += v/8;
                    }break;
                case 259: //Compression
                    Compression = GetInt(pdata,typesize);break;
                case 262: //PhotometricInterpretation
                    PhotometricInterpretation = GetInt(pdata,typesize);break;
                case 273://StripOffsets
                    for (int i = 0; i < Count; i++)
                    {
                        int v = GetInt(pdata+i*typesize,typesize);
                        StripOffsets.Add(v);
                    }break;
                case 274: break;//Orientation
                case 277: break;//SamplesPerPixel
                case 278://RowsPerStrip
                        RowsPerStrip = GetInt(pdata,typesize);break;
                case 279://StripByteCounts
                    for (int i = 0; i < Count; i++)
                    {
                        int v = GetInt(pdata+i*typesize,typesize);
                        StripByteCounts.Add(v);
                    }break;
                case 282: //XResolution
                    XResolution = GetRational(pdata); break;
                case 283://YResolution
                    YResolution = GetRational(pdata); break;
                case 284: break;//PlanarConfig
                case 296://ResolutionUnit
                    ResolutionUnit = GetInt(pdata,typesize);break;
                case 305://Software
                    Software = GetString(pdata,typesize); break;
                case 306://DateTime
                    DateTime = GetString(pdata,typesize); break;
                case 315: break;//Artist
                case 317: //Differencing Predictor
                    Predictor = GetInt(pdata,typesize);break;
                case 320: break;//ColorDistributionTable
                case 338: break;//ExtraSamples
                case 339: //SampleFormat
                    for (int i = 0; i < Count; i++)
                    {
                        int v = GetInt(pdata+i*typesize,typesize);
                        SampleFormat.Add(v);
                    } break;
                    
                default: break;
            }
        }

当所有的DE都被解码后,我们就可以来解码图像数据了。因为图像数据是一条一条的存放在TIFF文件中,DE 273 StripOffsets记录了每条扫描线的位置。DE 278 RowsPerStrip 记录了一条扫描线存了多少行图形数据。DE 279 StripByteCounts是一个数组,记录了每条扫描线数据的长度。如果不经过压缩的话,每条扫描线长度一般是相同的。

应为我的TIFF文件是采用了LZW压缩,DE 259 Compression =5,下面我就针对这种数据来解码一波。

private void DecodeStrips()
        {
            int pStrip = 0;
            int size = 0;
            tex = new Texture2D(ImageWidth,ImageLength,TextureFormat.RGBA32,false);
            Color[] colors = new Color[ImageWidth*ImageLength];

            if (Compression == 5)
            {
                
                int stripLength = ImageWidth * RowsPerStrip * BitsPerSample.Count * BitsPerSample[1] / 8;
                CompressionLZW.CreateBuffer(stripLength);
                
                if(Predictor==1)
                {
                    int index = 0;
                
                    for (int y = 0; y < StripOffsets.Count; y++)
                    {
                        pStrip = StripOffsets[y];//起始位置
                        size = StripByteCounts[y];//读取长度
                        byte[] Dval = CompressionLZW.Decode(data, pStrip, size);
                        for(int x = 0;x<ImageWidth;x++)
                        {
                             float R = GetFloat(Dval, x * PixelBytes   );
                             float G = GetFloat(Dval, x * PixelBytes+4 );
                             float B = GetFloat(Dval, x * PixelBytes+8 );
                             float A = GetFloat(Dval, x * PixelBytes+12);
                             colors[index++] = new Color(R,G,B,A);
                        }
                    }
                } 
                else
                {
                }
            }
            tex.SetPixels(colors);
            tex.Apply();
        }

因为是在Unity中开发的脚本,所以使用的是Unity的Texture,这个可以换成其他的,无关紧要。这里面我专门写了个类来解码LZW压缩的文件。解码后的数据直接转成Float存在Colors[]数组中,最后赋值给Texture。DE 274 Orientation就先不管了,先把图像读出来再说,无非是显示出来的图像是正的还是倒的或是镜像对称的。

下面来着重介绍一下LZW的解压方式。

while ((Code = GetNextCode()) != EoiCode) {
    if (Code == ClearCode) {
        InitializeTable();
        Code = GetNextCode();
        if (Code == EoiCode)
            break;
        WriteString(StringFromCode(Code));
        OldCode = Code;
    } /* end of ClearCode case */
    else {
        if (IsInTable(Code)) {
            WriteString(StringFromCode(Code));
            AddStringToTable(StringFromCode(OldCode)+FirstChar(StringFromCode(Code)));
            OldCode = Code;
        } else {
            OutString = StringFromCode(OldCode) +
            FirstChar(StringFromCode(OldCode));
            WriteString(OutString);
            AddStringToTable(OutString);
            OldCode = Code;
        }
    } /* end of not-ClearCode case */
} /* end of while loop */

其实也是比较简单的,上面写的是TIFF官方说明文档中解压TIFF的伪代码,我直接把它copy下来,粘贴在我的程序中,然后逐个实现里面的函数就好了。剩下的就是不断的调试了,总会遇到各式各样的bug。下面是我写的CompressionLZW类的大体框架。

public class CompressionLZW
{
    static private int Code = 0;
    static private int EoiCode = 257;
    static private int ClearCode = 256;
    static private int OldCode = 256;
    static private string[] Dic= new string[4096];
    static private int DicIndex;
    static private byte[] Input;
    static private int startPos;
    static private byte[] Output;
    static private int resIndex;
    static private int current=0;
    static private int bitsCount = 0;
    static string combine ="{0}{1}";
    static private void ResetPara()
    {
        OldCode = 256;
        DicIndex = 0;
        current = 0;
        resIndex = 0;
    }
    static public void CreateBuffer(int size){
    //...
    }
    static public byte[] Decode(byte[] input,int _startPos,int _readLength){
    //...
    }
    static private int GetNextCode(){
    //...
    }
    static private int GetBit(int x){
    //...
    }
    static private int GetStep(){
    //...
    }
    static private void InitializeTable(){
    //...
    }
    static private void WriteResult(string code){
    //...
    }
}

先来看看核心函数Decode

static public byte[] Decode(byte[] input,int _startPos,int _readLength)
    {
        Input = input;
        startPos = _startPos;
        bitsCount = _readLength*8;
        ResetPara();
        while ((Code = GetNextCode()) != EoiCode) 
        {
            if (Code == ClearCode) 
            {
                InitializeTable();
                Code = GetNextCode();
                if (Code == EoiCode)
                    break;
                WriteResult(Dic[Code]);
                OldCode = Code;
            }
            else 
            {
                if (Dic[Code]!=null) 
                {
                    WriteResult(Dic[Code]);
                    Dic[DicIndex++] =string.Format(combine, Dic[OldCode],Dic[Code][0]);
                    OldCode = Code;
                } 
                else 
                {   
                    string outs = string.Format(combine, Dic[OldCode], Dic[OldCode][0]);
                    WriteResult(outs);
                    Dic[DicIndex++] =outs;
                    OldCode = Code;
                }
            }
        }
        return Output;
    }

按照TIFF官方说明文档中的伪代码写完后,我遇到的第一个bug就是没有重置一些变量。当然,这是非常低级的错误了。因为我用的是静态函数,所以,每次调用Decode函数时,都要注意将一些变量重置一些。

这串代码里最重要的应该就是GetNextCode()了。

static private int GetNextCode()
{
    int tmp = 0;
    int step = GetStep();
    if (current + step > bitsCount)
        return EoiCode;
    for (int i = 0; i<step; i++)
    {
        int x = current + i;
        int bit = GetBit(x)<<(step-1-i);
	tmp+=bit;
    }
    current += step;
			//一开始读9个bit
			//读到510的时候,下一个开始读10bit
			//读到1022的时候,下一个开始读11bit
			//读到2046的时候,下一个开始读11bit
    return tmp;
}
static private int GetStep()
{
    int res = 12;
    int tmp = DicIndex-2047;//如果大于2046.则为正或零
    res+=(tmp>>31);
    tmp = DicIndex-1023;
    res+=(tmp>>31);
    tmp = DicIndex-511;
    res+=(tmp>>31);
    return res;
}
static private int GetBit(int x)		
{
    int byteIndex = x/8; //该bit在第几个byte里
    int bitIndex =7-x+byteIndex*8;//该bit是这个byte的第几位
    byte b = Input[startPos + byteIndex];
    return (b>>bitIndex)&1;
}

因为这几个函数可能会被上百万次的调用,我这里尽量使用位操作替代了if/else语句,所以看起来不是很直观。GetNextCode()函数的任务就是获取下一个字符,但是下一个字符占几位需要判断一下,这是有GetStep()函数来完成的。

因为tmp是有符号整型,当tmp<0时,tmp的最高位为1,代表负数,右移31位后,代表负数的1移动到了最低位,但由于移位也不改变符号,所以tmp变成了-1;当tmp>=0时,tmp的最高位为0,代表正数,右移31位后,代表正数的0移动到了最低位,所以tmp变成了0。这样便避免了使用多个if/else语句。

GetBit函数直接根据下标读原始的TIFF数据数组,我没有用BitArray去操作,这里用它效率不高。要特别注意这里

int bitIndex =7-x+byteIndex*8;//该bit是这个byte的第几位

将被LZW压缩过的数据进TIFF文件的时候,是按字节写进去的。

假设我们的TIFF图像是一个只有一个像素的图像,该像素的RGB值为(16,16,16) ,将它进行LZW压缩后得到的是

100000000 000010000 1000000101 00000001 0000

但它是按字节存进去的:

10000000 00000100 00100000 01010000 00010000

如果我们直接从0开始读的话,得到的结果是这样的。

00000001 00100000 00000100 00001010 00001000

这是因为被LZW压缩后的数据是按高位存在低位的方式写入字节数据的。所以一定要注意将bit数组转换成int时候不要读错。
今天先更新到这里吧~

FreeImage是一个开源的图像处理库,它主要用于读取、写入和操作各种图像文件格式,包括TIFF(Tagged Image File Format)。FreeImage并没有直接提供一个特定的"tif文件合并接口",它主要是作为一个底层工具,供开发者用于打开、操作单个TIFF图像文件。 如果你想要合并多个TIFF文件,通常的做法是先使用FreeImage加载每个单独的文件,然后将它们的数据合并到一个新的内存缓冲区,最后再通过FreeImage或其他图像处理库的功能(如`FreeImage_Save`)保存成新的TIFF文件。这是一个通用的流程: ```cpp // 假设你已经包含了FreeImage.h头文件并初始化了FreeImage FIBITMAP* LoadTiff(const std::string& filePath) { return FreeImage_LoadTIFF(FreeImage_GetFileType(filePath.c_str())); } void MergeTiffs(const std::vector<std::string>& fileNames, const std::string& outputFile) { FIBITMAP* combinedBitmap = nullptr; for (const auto& fileName : fileNames) { FIBITMAP* singleBitmap = LoadTiff(fileName); if (!combinedBitmap) { combinedBitmap = singleBitmap; // 第一个文件作为基础 } else { // 使用FreeImage_MergeIntoAlpha或FreeImage_MergeLayers将单个图片合并到已有组合中 FreeImage_MergeIntoAlpha(combinedBitmap, singleBitmap); } // 清理单个文件 FreeImage_Unload(singleBitmap); } // 将合并后的图像保存 bool success = FreeImage_SaveTIFF(outputFile.c_str(), combinedBitmap, 0); // 第二个参数表示是否压缩 FreeImage_Unload(combinedBitmap); if (!success) { // 处理保存失败的情况 } } ```
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值