记录一次手动读取BigTiff文件(Python)

BigTiff规范
http://bigtiff.org/#INTERNAL_OBJECT_CHANGES
https://www.awaresystems.be/imaging/tiff/bigtiff.html

错误信息

由于使用ENVIimageiogdal读取都会出错,因此尝试手动读取文件解析原因,尝试修复。
报错信息:

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文件(Python)_第1张图片
根据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文件(Python)_第2张图片
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压缩)得到该行数据。

你可能感兴趣的:(遥感,工具,python,TIFF,BigTiff)