保存多序列tiff文件_解码TIFF文件
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文件相关推荐
- python 循环写文件_循环-读写文件-字符编码
目录: 1.1 while与for循环 1.赋值魔法 #1. 序列解包: 将多个值的序列解开,然后放到序列的变量中. x,y,z = 1,2,3 print(x,y,z) #the result : ...
- navicat运行db文件_使用 YAML 文件配置 Jenkins 流水线
本文转载自:Jenkins 中文社区 这也是一种自定义流水线 DSL 的方法 几年前,我们的 CTO 写了一篇关于 使用 Jenkins 和 Docker 为 Ruby On Rails 应用提供持续 ...
- ftp可以传输什么类型文件_使用FTP文件传输典型案例配置
亲爱的小伙伴们 咱们8月整月开课计划已出 座位有限 感兴趣的小伙伴赶紧预约啦 建策科技8月开班计划 如下图所示:两台路由器连接,我们将FTP服务器和FTP客户端中的文件(例如:配置文件.系统升级文件等 ...
- java bat 运行 jar文件_运行bat文件启动java的jar且不弹出DOS窗口,后台运行java的jar包...
本文主要是将java的jar包启动的cmd命令添加到bat文件来执行,且不弹出DOS窗口,也就是后台运行java的jar包. 这里以win10为例 1.新建 一个txt文件,在文件添加以下内容.其中E ...
- pydicom读取头文件_.dcm格式文件软件读取及python处理详解
要处理一些.dcm格式的焊接缺陷图像,需要读取和显示.dcm格式的图像.通过搜集资料收集到一些医学影像,并通过pydicom模块查看.dcm格式文件. 若要查看dcm格式文件,可下echo viewe ...
- python 追加写文件_如何往文件中追加文本
在用python从网站中爬取内容并保存到本地的txt文件中时,发现每次写入都是把txt文件中原来存在的内容覆盖掉了,那么如何才能在原来的基础上继续往里面添加内容呢? 1.原来的打开文件的方式是:fil ...
- python怎么运行ipynb文件_运行.ipynb文件遇到的问题
错误如下图: 通过将access_literature_data和access_science_shared都添加到sys.path中 方法:(有的人说.pth建在dist-package目录下,也有 ...
- java怎么导入文件_怎么将文件导入java
这篇文章主要介绍了Java的写入文件的几种方法,需要的朋友可以参考下: 一.FileWritter写入文件 FileWritter,字符流写入字符到文件.默认情况下,它会使用新的内容取代所有现有的内容 ...
- fileinputstream resources 读取文件_压缩20M文件从30秒到1秒,包教包会
作者:不学无数的程序员链接:https://www.jianshu.com/p/25b328753017 压缩20M文件从30秒到1秒的优化过程 有一个需求需要将前端传过来的10张照片,然后后端进行处 ...
最新文章
- SAP Spartacus UI Duplicated keys has been found in the config of i18n chunks
- 如何监测服务器网络稳定性centos,centos下网络监测工具nethogs
- C++对类(或者结构体)中字符数组赋值时,出现表达式必须是可修改的左值的问题
- poj 3420 Quad Tiling 【矩阵乘法】
- 页面打开自动触发onlick事件
- Leetcode. 14. Longest Common Prefix
- spring-第十篇之XML Schema的简化配置,p、c、util命名空间
- android 免 root修改位置打卡
- Linux环境中的网络分段卸载技术 GSO/TSO/UFO/LRO/GRO
- 李力刚《谈判博弈》读书笔记
- linux安装frps服务,Debian手动搭建frps服务端
- java imageio 内存问题_java imageio内存泄漏
- java按键发出声音代码_用Java写的一个根据按键发声的程序,为什么只有前16次按键响...
- 贾俊平《统计学》第七章知识点总结及课后习题答案
- 苏州博物馆计算机系统操作工,行车及铁钢包调度系统在炼钢厂应用.doc
- 史上最完整的5G NR介绍
- Python数据结构02-顺序表、链表
- 上传图片错误提示:error=3,原因及解决方法
- 海洋cms泛目录系统
- 从“七国八制”到“中华”脊梁
热门文章
- 一文速览 | 对话生成预训练模型
- 建议收藏!早期人类驯服『图神经网络』的珍贵资料
- 翻译pdf中的英文 python_浅谈python实现Google翻译PDF,解决换行的问题
- 机载激光雷达测量技术及工程应用实践_倾斜摄影与激光雷达技术在实景三维测量应用中的比较...
- ubuntu使用python_Ubuntu+Python环境配置(III)—用Python
- Tiktok下载量激增至20亿次,视频带货或将席卷海外,跨境电商的风口
- 蓝桥杯 入门训练 Fibonacci数列
- python字符串join和+_Python字符串通过'+'和join函数拼接新字符串的性能测试比较
- php对接银行接口,php 银行接口开发写法
- java statement 动态参数_java_web学习(九) PreparedStatement动态参数的引入