0 引言

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

1 TIFF图像格式详解

要解码TIFF,首先要了解TIFF文件格式。当然是去看官方提供的说明文档最为直接。

https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFF6.pdf

除此之外,一些中文博客也提供了很好的讲解。这里也简单介绍一下。

TIFF文件中的三个关键词是:

IFH(Image File Header) 图像文件头

IFD (Image File Directory)图像文件目录

DE (Directory Entry) 目录项

每一幅图像是以8字节的IFH开始的, 这个IFH指向了第一个IFD。IFD又包含了多个DE,每个DE都是一种图像信息,如高度,宽度,压缩方式,位深度,图像数据的位置等。

要解码TIFF,就是要读IFH,然后读每一个IFD的每一个DE。扩展的TIFF图像经常包含多个IFD,每一个IFD都都是一个子文件,包含了不同的信息。不过我的TIFF图像貌似只有一个IFD。

在TIFF6.0中,定义了12种数据类型,如下表所示

表1 图像信息的12种数据类型

字节数
1 byte 1 无符号整型
2 ASCII 1 字符(字符串)
3 short 2 无符号整型
4 long 4 无符号整型
5 rational 8 两个long,第一个是分子,第二个是分母
6 sbyte 1 有符号整型
7 Undefined 1 无限可能
8 sshort 2 有符号整型
9 slong 4 有符号整型
10 srational 8 两个slong,第一个是分子,第二个是分母
11 float 4 单精度浮点
12 double 8 双精度浮点

这些数据类型是图像信息的数据类型,我们读取图像信息的时候会用到。

表2 IFH的构成

字节 数据类型 内容
0 1 short 字节顺序标志位,值为II或者MM。II表示小字节在前,又称为little-endian。MM表示大字节在前,又成为big-endian。这个值影响之后如何读取byte数据。
2 3 short TIFF的标志位,都是42
4 5 6 7 long 第一个IFD的位置(byte数组的下标)。

表2 IFD的构成

字节 数据类型 内容
short 表示此IFD包含了多少个DE,假设数目为n
0*12+2 .... 1*12+1 short + short + long DE 1,每个DE是12个Byte
1*12+2 .... 2*12+1 short + short + long DE 2
short + short + long
short + short + long DE n
n*12 + 1 ... n*12 + 4 long 下一个IFD的位置,如果没有则为0

表3 DE的结构

字节 数据类型 内容
0 1 short 标签Tag(见表4)
2 3 short 数据类型(见表1)
4 5 6 7 long 数量。通过类型和数量可以确定存储此Tag的数据需要占据的字节数
8 9 10 11 如果占用的字节数少于4, 则数据直接存于此。 如果超过4个,则这里存的数据的位置(Byte数组的下标)

表4 DE的类型

Tag 名称 数据类型
256 ImageWidth short/long 图像宽度
257 ImageLength short/long 图像高度
258 BitsPerSample short 每个采样点的位数
259 Compression short 压缩方式
262 PhotometricInterpretation short 对于黑白图像,是0代表白色还是1代表白色
273 StripOffsets short/long 扫描线位置
274 Orientation 方向
277 SamplesPerPixel 每个像素的采样点
278 RowsPerStrip short/long 每条扫描线存了多少行图像数据,一般是1
279 StripByteCounts short/long 扫面线字节数(如果压缩的话,每一条扫描线都可能不一样)
282 XResolution rational 水平方向分辨率
283 YResolution rational 竖直方向分辨率
284 PlanarConfig
296 ResolutionUnit
305 Software ASCII 创建该TIF的软件
306 DateTime ASCII 创建时间
315 Artist ASCII 作者
317 Differencing Predictor short 若使用LZW方式压缩,则可以把每个像素储存为与前一个像素的差值
320 ColorMap short
338 ExtraSamples
339 SampleFormat

DE的内容有很多,没一项有值得展开来解释。不过很多都是针对特定格式图像的。比如317 Differencing Predictor,一般只有经过LZW压缩黑白图像才会用到,它表示是否将图像数据的每一项保存为它和前一项的差值。在使用LZW压缩前先这样处理可以提高压缩效率,但针对RGB的彩色图像,这样做压缩效率反而降低。又比如320 ColorMap,只有调色板彩色图像才会使用。

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;elsethrow 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]};elsebyteTemp =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 integernew 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) integernew DType("undefined",1),//An 8-bit byte that may contain anything, depending on the definition of the fieldnew 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 formatnew 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;//NewSubfileTypecase 255: break;//SubfileTypecase 256://ImageWidthImageWidth = GetInt(pdata,typesize);break;case 257://ImageLengthif (TypeIndex == 3)//shortImageLength = GetInt(pdata,typesize);break;case 258://BitsPerSamplefor (int i = 0; i < Count; i++){int v = GetInt(pdata+i*typesize,typesize);BitsPerSample.Add(v);PixelBytes += v/8;}break;case 259: //CompressionCompression = GetInt(pdata,typesize);break;case 262: //PhotometricInterpretationPhotometricInterpretation = GetInt(pdata,typesize);break;case 273://StripOffsetsfor (int i = 0; i < Count; i++){int v = GetInt(pdata+i*typesize,typesize);StripOffsets.Add(v);}break;case 274: break;//Orientationcase 277: break;//SamplesPerPixelcase 278://RowsPerStripRowsPerStrip = GetInt(pdata,typesize);break;case 279://StripByteCountsfor (int i = 0; i < Count; i++){int v = GetInt(pdata+i*typesize,typesize);StripByteCounts.Add(v);}break;case 282: //XResolutionXResolution = GetRational(pdata); break;case 283://YResolutionYResolution = GetRational(pdata); break;case 284: break;//PlanarConfigcase 296://ResolutionUnitResolutionUnit = GetInt(pdata,typesize);break;case 305://SoftwareSoftware = GetString(pdata,typesize); break;case 306://DateTimeDateTime = GetString(pdata,typesize); break;case 315: break;//Artistcase 317: //Differencing PredictorPredictor = GetInt(pdata,typesize);break;case 320: break;//ColorDistributionTablecase 338: break;//ExtraSamplescase 339: //SampleFormatfor (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就先不管了,先把图像读出来再说,无非是显示出来的图像是正的还是倒的或是镜像对称的。

2.2

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

关于LZW压缩的原理,在https://blog.csdn.net/lzljy/article/details/103575209这篇文章中已经介绍的很详细了。我在这里就主要给出代码实现,以及在解压图片时遇到的一些问题。

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的时候,下一个开始读11bitreturn 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时候不要读错。

今天先更新到这里吧~

保存多序列tiff文件_解码TIFF文件相关推荐

  1. python 循环写文件_循环-读写文件-字符编码

    目录: 1.1 while与for循环 1.赋值魔法 #1. 序列解包: 将多个值的序列解开,然后放到序列的变量中. x,y,z = 1,2,3 print(x,y,z) #the result : ...

  2. navicat运行db文件_使用 YAML 文件配置 Jenkins 流水线

    本文转载自:Jenkins 中文社区 这也是一种自定义流水线 DSL 的方法 几年前,我们的 CTO 写了一篇关于 使用 Jenkins 和 Docker 为 Ruby On Rails 应用提供持续 ...

  3. ftp可以传输什么类型文件_使用FTP文件传输典型案例配置

    亲爱的小伙伴们 咱们8月整月开课计划已出 座位有限 感兴趣的小伙伴赶紧预约啦 建策科技8月开班计划 如下图所示:两台路由器连接,我们将FTP服务器和FTP客户端中的文件(例如:配置文件.系统升级文件等 ...

  4. java bat 运行 jar文件_运行bat文件启动java的jar且不弹出DOS窗口,后台运行java的jar包...

    本文主要是将java的jar包启动的cmd命令添加到bat文件来执行,且不弹出DOS窗口,也就是后台运行java的jar包. 这里以win10为例 1.新建 一个txt文件,在文件添加以下内容.其中E ...

  5. pydicom读取头文件_.dcm格式文件软件读取及python处理详解

    要处理一些.dcm格式的焊接缺陷图像,需要读取和显示.dcm格式的图像.通过搜集资料收集到一些医学影像,并通过pydicom模块查看.dcm格式文件. 若要查看dcm格式文件,可下echo viewe ...

  6. python 追加写文件_如何往文件中追加文本

    在用python从网站中爬取内容并保存到本地的txt文件中时,发现每次写入都是把txt文件中原来存在的内容覆盖掉了,那么如何才能在原来的基础上继续往里面添加内容呢? 1.原来的打开文件的方式是:fil ...

  7. python怎么运行ipynb文件_运行.ipynb文件遇到的问题

    错误如下图: 通过将access_literature_data和access_science_shared都添加到sys.path中 方法:(有的人说.pth建在dist-package目录下,也有 ...

  8. java怎么导入文件_怎么将文件导入java

    这篇文章主要介绍了Java的写入文件的几种方法,需要的朋友可以参考下: 一.FileWritter写入文件 FileWritter,字符流写入字符到文件.默认情况下,它会使用新的内容取代所有现有的内容 ...

  9. fileinputstream resources 读取文件_压缩20M文件从30秒到1秒,包教包会

    作者:不学无数的程序员链接:https://www.jianshu.com/p/25b328753017 压缩20M文件从30秒到1秒的优化过程 有一个需求需要将前端传过来的10张照片,然后后端进行处 ...

最新文章

  1. SAP Spartacus UI Duplicated keys has been found in the config of i18n chunks
  2. 如何监测服务器网络稳定性centos,centos下网络监测工具nethogs
  3. C++对类(或者结构体)中字符数组赋值时,出现表达式必须是可修改的左值的问题
  4. poj 3420 Quad Tiling 【矩阵乘法】
  5. 页面打开自动触发onlick事件
  6. Leetcode. 14. Longest Common Prefix
  7. spring-第十篇之XML Schema的简化配置,p、c、util命名空间
  8. android 免 root修改位置打卡
  9. Linux环境中的网络分段卸载技术 GSO/TSO/UFO/LRO/GRO
  10. 李力刚《谈判博弈》读书笔记
  11. linux安装frps服务,Debian手动搭建frps服务端
  12. java imageio 内存问题_java imageio内存泄漏
  13. java按键发出声音代码_用Java写的一个根据按键发声的程序,为什么只有前16次按键响...
  14. 贾俊平《统计学》第七章知识点总结及课后习题答案
  15. 苏州博物馆计算机系统操作工,行车及铁钢包调度系统在炼钢厂应用.doc
  16. 史上最完整的5G NR介绍
  17. Python数据结构02-顺序表、链表
  18. 上传图片错误提示:error=3,原因及解决方法
  19. 海洋cms泛目录系统
  20. 从“七国八制”到“中华”脊梁

热门文章

  1. 一文速览 | 对话生成预训练模型
  2. 建议收藏!早期人类驯服『图神经网络』的珍贵资料
  3. 翻译pdf中的英文 python_浅谈python实现Google翻译PDF,解决换行的问题
  4. 机载激光雷达测量技术及工程应用实践_倾斜摄影与激光雷达技术在实景三维测量应用中的比较...
  5. ubuntu使用python_Ubuntu+Python环境配置(III)—用Python
  6. Tiktok下载量激增至20亿次,视频带货或将席卷海外,跨境电商的风口
  7. 蓝桥杯 入门训练 Fibonacci数列
  8. python字符串join和+_Python字符串通过'+'和join函数拼接新字符串的性能测试比较
  9. php对接银行接口,php 银行接口开发写法
  10. java statement 动态参数_java_web学习(九) PreparedStatement动态参数的引入