C读取TIFF文件

C读取TIFF文件1 什么是 TIFF TIFF 是 TaggedImageF 的缩写 在现在的标准中 只有 TIFF 存在 其他的提法已经舍弃不用了 做为一种标记语言 TIFF 与其他文件格式最大的不同在于除了图像数据 它还可以记录很多图像的其他信息 它记录图像数据的方式也比较灵活 理论上来说 任何其他的图像格式都能为 TIFF 所用 嵌入到 TIFF 里面 比如 JPEG LosslessJP

0 引言

1 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压缩后得到的是

000010000 00000001 0000

但它是按字节存进去的:

00000100 00 0 00010000

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

00000001 00 00000100 00001010 00001000

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/205465.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 下午5:58
下一篇 2026年3月19日 下午5:58


相关推荐

  • 【2021最新版】Kafka面试题总结(25道题含答案解析)

    【2021最新版】Kafka面试题总结(25道题含答案解析)文章目录 1 Kafka 是什么 2 partition 的数据文件 offffset MessageSize data 3 数据文件分段 segment 顺序读写 分段命令 二分查找 4 负载均衡 partition 会均衡分布到不同 broker 上 5 批量发送 6 压缩 GZIP 或 Snappy 7 消费者设计 8 ConsumerGrou 如何获取 topic 主题的列表 10 生产者和消费者的命令行是什么 11 consumer 是推还是拉 12 讲讲 kafka 维护消费状态跟踪的方法 13 讲一下主从同步 14 为什

    2026年3月18日
    1
  • PreferenceActivity类

    PreferenceActivity类PreferencesA 是 Android 中专门用来实现程序设置界面及参数存储的一个 Activity 我们用一个实例来简介如何使用 PreferencesA 下面是一个设置页面 nbsp 以此为例我们来介绍一下如何实现这个界面 首先建立一个 xml 来描述这个界面 文件为 res xml preferences xmlxmlns android http

    2026年3月26日
    3
  • CentOS7 解决无法使用tab自动补全

    CentOS7 解决无法使用tab自动补全

    2021年6月3日
    125
  • Ngnix 搭建视频直播服务器[通俗易懂]

    Ngnix 搭建视频直播服务器[通俗易懂]受疫情推迟开学影响,这段时间全国如火如荼推广网络教学,前段时间搭建了edx慕课平台,但还缺点什么,就是网络直播教学,花一天时间,搭建成功,记录备用。1.基本技术路线其中,服务器采用nginx+nginx-rtmp-module,推流采用OBS-Studio,拉流采用html5网页播放2.直播服务器安装环境centos7,没有安装桌面图形界面,server版y…

    2022年4月30日
    85
  • 钓鱼网站php,偶遇钓鱼网站的一次代码审计「建议收藏」

    钓鱼网站php,偶遇钓鱼网站的一次代码审计「建议收藏」偶遇一个钓鱼邮件中的钓鱼网站,并与年华大佬做了代码审计。据说近期全国出现多起钓鱼邮件事件,主要以各大高校为主,已有不少人上当,还需多加注意。分析钓鱼网站钓鱼网站采用常用空间钓鱼CMS搭建,可通过百度搜索下载源码。源码观察源码发现,源码中存在360safe防护机制,无法通过正常方式进行攻击。分析猜测钓鱼网站后台管理页面地址,发现地址为无法知道用户名密码,分析源码,查看是否存在绕过。观察index…

    2022年8月24日
    9
  • SCSA两个月

    SCSA两个月在一台HPDL380G3(集成5iRAID卡),上尝试安装solaris10,总是因为iLO的VirtualMedia问题而导致无法读盘,只好放弃。今天在Vmware上面安装好了solaris10,正式开始了solaris学习之旅。SCSA,两个月够么?两个月之后就知道了! 转载于:https://blog.51cto.com/youngshen/8013…

    2022年6月20日
    32

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号