BigTiff规范
http://bigtiff.org/#INTERNAL_OBJECT_CHANGES
https://www.awaresystems.be/imaging/tiff/bigtiff.html
由于使用ENVI
,imageio
和gdal
读取都会出错,因此尝试手动读取文件解析原因,尝试修复。
报错信息:
from osgeo import gdal
path = r'C:\img.tif'
dataset = gdal.Open(path)
img = dataset.ReadAsArray()
ERROR 1: LZWDecode:Corrupted LZW table at scanline 26848
ERROR 1: TIFFReadEncodedStrip() failed.
ERROR 1: C:\img.tif, band 1: IReadBlock failed at X offset 0, Y offset 26848: TIFFReadEncodedStrip() failed.
以二进制读取文件,由于一次读取全部数据(2.8G)会导致后续步骤严重卡顿,故这里只读取这一步所需的数据
# 读取tif文件
file_offset = 0 # 读取起始位置(Bytes)
file_len = 1000000 # 读取长度(Bytes)
with open(path, 'rb') as f:
f.seek(file_offset, 0)
b = f.read(file_len)
根据BigTiff标准,文件头的16位(bit),即2字节(byte),为0x4D4D
,转为ASCII码即为II
In: b[:2]
Out: b'II'
我本来以为是GeoTIFF,结果发现不对劲,绕回来看才发现版本号是43而不是42
In: b[2:4]
Out: b'+\x00'
由于版本号是整数格式,因此需要解码才能看到
import sys
import struct
# 解码二进制数据
def dec(x, type='i'):
if type == 'i': # int
return int.from_bytes(x, byteorder=sys.byteorder)
elif type == 'f': # float
if len(x) == 4:
return struct.unpack('f', x)[0]
elif len(x) == 8:
return struct.unpack('d', x)[0]
elif type == 's': # string
return struct.unpack('s', x)[0].decode()
else:
return struct.unpack(type, x)[0]
In: dec(b[2:4])
Out: 43
头文件中除了确定版本号外,最重要的就是获取第一个目录位置(offset):
offset_dir1 = dec(b[8:8 + 8]) # 16
BigTiff中每个目录头8个字节表示目录条目数量,一个条目占用20字节,每个条目中的信息及其分布如上图所示。
# 读取目录
len_num = 8
len_entry = 20
num_entries = dec(b[offset_dir1:offset_dir1 + len_num])
tags = []
for offset_entry in range(offset_dir1 + len_num, offset_dir1 + len_num + num_entries * len_entry, len_entry):
entries = b[offset_entry:offset_entry + len_entry]
tag = dec(entries[0:2])
type = dec(entries[2:4])
count = dec(entries[4:12])
value = entries[12:20]
tags.append((tag, type, count, value))
# 下一个目录的位置(0即无下个目录)
offset_dir = dec(b[offset_dir1 + len_num + num_entries * len_entry:
offset_dir1 + len_num + num_entries * len_entry + 8])
得到的目录中的条目如下所示
# Tag编号,数据类型,数据量,值
(256, 3, 1, b'd\xc9\x00\x00\x00\x00\x00\x00')
(257, 3, 1, b'[\xca\x00\x00\x00\x00\x00\x00')
(258, 3, 4, b'\x08\x00\x08\x00\x08\x00\x08\x00')
(259, 3, 1, b'\x05\x00\x00\x00\x00\x00\x00\x00')
(262, 3, 1, b'\x02\x00\x00\x00\x00\x00\x00\x00')
(273, 16, 51803, b'tT\x06\x00\x00\x00\x00\x00')
(277, 3, 1, b'\x04\x00\x00\x00\x00\x00\x00\x00')
(278, 3, 1, b'\x01\x00\x00\x00\x00\x00\x00\x00')
(279, 16, 51803, b'\x9c\x01\x00\x00\x00\x00\x00\x00')
(284, 3, 1, b'\x01\x00\x00\x00\x00\x00\x00\x00')
(305, 2, 12, b'L\xa7\x0c\x00\x00\x00\x00\x00')
(317, 3, 1, b'\x02\x00\x00\x00\x00\x00\x00\x00')
(338, 3, 1, b'\x02\x00\x00\x00\x00\x00\x00\x00')
(339, 3, 4, b'\x01\x00\x01\x00\x01\x00\x01\x00')
(33550, 12, 3, b'X\xa7\x0c\x00\x00\x00\x00\x00')
(33922, 12, 6, b'p\xa7\x0c\x00\x00\x00\x00\x00')
(34735, 3, 32, b'\xa0\xa7\x0c\x00\x00\x00\x00\x00')
(34737, 2, 80, b'\xe0\xa7\x0c\x00\x00\x00\x00\x00')
(42113, 2, 7, b'-10000\x00\x00')
在这里查找条目对应的含义并读取:
# 读取目录条目
def get_TLO_by_tag_num(tag_num):
for tag, type, count, value in tags:
if tag == tag_num:
if type == 2: # ASCII
if count * 1 > 8: # value 作为偏置定位数据
offset = dec(value)
value = b[offset:offset + 1 * count]
s = [dec(value[i:i+1], 's') for i in range(count-1)]
string = ''
for i in s:
string += i
return string
elif type == 3: # SHORT # 16-bit unsigned integer
if count * 2 > 8:
offset = dec(value)
value = b[offset:offset + 2 * count]
return [dec(value[i*2:i*2+2]) for i in range(count)]
elif type == 4: # LONG # 32-bit unsigned integer
if count * 4 > 8:
offset = dec(value)
value = b[offset:offset + 4 * count]
return [dec(value[i*4:i*4+4]) for i in range(count)]
elif type == 11: # FLOAT # 32-bit IEEE floating point
if count * 4 > 8:
offset = dec(value)
value = b[offset:offset + 4 * count]
return [dec(value[i*4:i*4+4], 'f') for i in range(count)]
elif type == 12: # DOUBLE # 64-bit IEEE floating point
if count * 8 > 8:
offset = dec(value)
value = b[offset:offset + 8 * count]
return [dec(value[i*8:i*8+8], 'd') for i in range(count)]
elif type == 16: # RATIONAL # 64-bit unsigned fraction
if count * 8 > 8:
offset = dec(value)
value = b[offset:offset + 8 * count]
return [dec(value[i*8:i*8+8], 'i') for i in range(count)]
else:
raise NotImplementedError
raise Exception('tag not found')
# 解析目录条目
width = get_TLO_by_tag_num(256)[0]
height = get_TLO_by_tag_num(257)[0]
bits_per_sample = get_TLO_by_tag_num(258)
compression = get_TLO_by_tag_num(259)[0] # 图像压缩方案
PhotometricInterpretation = get_TLO_by_tag_num(262)[0] # 图像色彩模型
StripOffsets = get_TLO_by_tag_num(273) # 每个条带的字节偏移量(数据存储位置)
SamplesPerPixel = get_TLO_by_tag_num(277)[0] # 每个像素的通道数
RowsPerStrip = get_TLO_by_tag_num(278)[0] # 每个条带的行数
StripByteCounts = get_TLO_by_tag_num(279) # 每个条带的字节数(压缩后)
PlanarConfiguration = get_TLO_by_tag_num(284)[0] # 图像数据的存储方式
Software = get_TLO_by_tag_num(305) # 软件信息
Predictor = get_TLO_by_tag_num(317)[0] # 差分预测模式(用于LZW编码)
ExtraSamples = get_TLO_by_tag_num(338)[0] # 在每个像素的颜色通道中,额外的通道数
SampleFormat = get_TLO_by_tag_num(339) # 指定如何解释像素中的每个数据样本
ModelPixelScaleTag = get_TLO_by_tag_num(33550) # 像素大小 用于可互换的 GeoTIFF 文件
ModelTiepointTag = get_TLO_by_tag_num(33922) # 像素总体偏移 用于可互换的 GeoTIFF 文件
GeoKeyDirectoryTag = get_TLO_by_tag_num(34735) # 地理信息关键字目录 用于可互换的 GeoTIFF 文件
GeoAsciiParamsTag = get_TLO_by_tag_num(34737) # 地理信息 ASCII 参数 用于可互换的 GeoTIFF 文件
GDAL_NODATA = get_TLO_by_tag_num(42113) # 由 GDAL 库使用,包含 ASCII 编码的 nodata 或背景像素值。
结果如下:
width: 51556
height: 51803
bits_per_sample: [8, 8, 8, 8]
compression: 5
PhotometricInterpretation: 2
StripOffsets: [829488, 830262, 831057, 832155, 833266, ...]
SamplesPerPixel: 4
RowsPerStrip: 1
StripByteCounts: [774, 795, 1098, 1111, 1122, ...]
PlanarConfiguration: 1
Software: pix4dmapper
Predictor: 2
ExtraSamples: 2
SampleFormat: [1, 1, 1, 1]
ModelPixelScaleTag: [0.030000000000000000, 0.030000000000000000, 0.0]
ModelTiepointTag: [0.0, 0.0, 0.0, 123456.78901, 234567.89012, 0.0]
GeoKeyDirectoryTag: [1, 1, 0, 7, 1024, 0, 1, 1, 1025, 0, 1, 1, 1026, 34737, 41, 0, 2049, 34737, 38, 41, 2054, 0, 1, 9102, 3072, 0, 1, 4546, 3076, 0, 1, 9001]
GeoAsciiParamsTag: CGCS2000 / 3-degree Gauss-Kruger CM 111E|China Geodetic Coordinate System 2000|
GDAL_NODATA: -10000
目录中有GeoKeyDirectoryTag
,这代表该文件还有地理信息拓展,这里不展开。
使用gdal读取出现错误的位置是26848行,因此读取该行二进制数据
# 读取tif文件
file_offset = 1600000000 # 读取起始位置(Bytes)
file_len = 100000000 # 读取长度(Bytes)
with open(path, 'rb') as f:
f.seek(file_offset, 0)
b = f.read(file_len)
# 错误位置(行)
errp = 26848
# 读取错误行
err_line = b[StripOffsets[l]-file_offset:StripOffsets[l]+StripByteCounts[l]-file_offset]
通过解码(compression: 5,为LZW压缩)得到该行数据。