How it works(17) Geotrellis是如何读取GeoTiff的(B) 数据类型模型

1. 引入

上一篇我们解析了如何读取元数据,有了元数据,我们就可以创建一个GeoTiffTile对象:

def readSingleband(byteReader: ByteReader, streaming: Boolean, withOverviews: Boolean, byteReaderExternal: Option[ByteReader]): SinglebandGeoTiff = {
    def getSingleband(geoTiffTile: GeoTiffTile, info: GeoTiffInfo): SinglebandGeoTiff =
      SinglebandGeoTiff(
        geoTiffTile,
        info.extent,
        info.crs,
        info.tags,
        info.options,
        info.overviews.map { i => getSingleband(geoTiffSinglebandTile(i), i) }
      )
    // 已经解析了如何获取元数据
    val info = GeoTiffInfo.read(byteReader, streaming, withOverviews, byteReaderExternal)
    // 获取一个GeoTiffTile对象
    val geoTiffTile = geoTiffSinglebandTile(info)
    // 最终获取一个单波段对象
    getSingleband(geoTiffTile, info)
}

def geoTiffSinglebandTile(info: GeoTiffInfo): GeoTiffTile =
  // 省略了多波段的情况,以单波段举例
  GeoTiffTile(
    info.segmentBytes,
    info.decompressor,
    info.segmentLayout,
    info.compression,
    info.cellType,
    Some(info.bandType),
    info.overviews.map(geoTiffSinglebandTile)
  )
}

追寻GeoTiffTile的伴生对象,可知GeotiffTile对象的创建如下:

def apply(
    segmentBytes: SegmentBytes,
    decompressor: Decompressor,
    segmentLayout: GeoTiffSegmentLayout,
    compression: Compression,
    cellType: CellType,
    bandType: Option[BandType] = None,
    overviews: List[GeoTiffTile] = Nil
  ): GeoTiffTile = {
  // 先按波段类型
    bandType match {
      // UInt32类型的波段比较特殊,单独处理
      case Some(UInt32BandType) =>
        cellType match {
          // 再匹配数据类型
          case ct: FloatCells =>
            new UInt32GeoTiffTile(
              segmentBytes,
              decompressor,
              segmentLayout,
              compression,
              ct,
              overviews.map(applyOverview(_, compression, cellType, bandType)).collect { case gt: UInt32GeoTiffTile => gt }
            )
          case _ =>
            throw new IllegalArgumentException("UInt32BandType should always resolve to Float celltype")
        }
      case _ =>
        cellType match {
          // 匹配其他数据类型
          case ct: BitCells =>
            new BitGeoTiffTile(
              segmentBytes,
              decompressor,
              segmentLayout,
              compression,
              ct,
              overviews.map(applyOverview(_, compression, cellType, bandType)).collect { case gt: BitGeoTiffTile => gt }
            )
          // 省略其他类型
        }
      }
    }

可以看出,Geotrellis实际是根据数据类型创造对应的GeotiffTile对象.

以UInt32GeoTiffTile为例,它的继承链路如下:

  • 绿色的为类继承
  • 红色的为特征实现


如图可知,UInt32GeoTiffTile首先分为两支:

  • GeotiffTile一支定义了抽象的数据访问方法.
  • Uint32GeotiffSegmentCollection一支则定义了针对Uint32这类数据的具体访问方法.

就从最底层的类Grid,到最顶层的Uint32GeotiffTile,解析每一层的类定义了哪些功能与关键字段及混入的特质的作用.

2. Grid类

Grid类是最底层的类,定义如下:

abstract class Grid[N: Integral] extends Serializable {
  def cols: N
  def rows: N
  def size: N = cols * rows
  def dimensions: Dimensions[N] = Dimensions(cols, rows)
}

Grid类使用def预定义了cols,rows等字段.为什么要用def定义字段?

  • 如此处所言,在抽象类中使用def定义在继承者中会被覆写的字段是更灵活的选择.(cols和rows是在Geotifftile类中才被实际赋值的).
  • 使用def定义的字段是懒求值的,因此size可以定义为使用预定义的cols和rows的乘积.

我们注意到,Grid类使用了type classes定义泛型操作.Integral类型来自spire.math包.相比原生类型,spire类型具有更方便的操作和更高的效率,表达式也更优雅.

type classes是一种上下文绑定语法糖,class Grid[ N: Integral]意味着class Grid[ N <% Integral[N]],即泛型N必须可转换为Integral类型,且这时编译器将cols/rows当做Integral[N]类型,size的*操作符,其实是Integral[N]类型的操作符.

Grid类具有一个复杂字段:dimensions,类型为Dimensions,其定义如下:

//使用specialized优化泛型装箱性能
case class Dimensions[@specialized(Byte, Short, Int, Long) N: Integral](
    cols: N,
    rows: N
) extends Product2[N, N]
    with Serializable {
  def _1 = cols
  def _2 = rows
  def size: Long =
    Integral[N].toType[Long](cols) * Integral[N].toType[Long](rows)
  override def toString = s"${cols}x${rows}"
}

Dimensions几乎与Grid对象内容一模一样.这在需要同时传递cols和rows值的时候非常方便.在代码中dimensions字段常常与模式匹配一起使用,直接同时提取出cols和rows变量.

3. CellGrid类

顺着继承链路向上,进入CellGrid类.CellGrid是对Grid的扩展,定义十分简单:

abstract class CellGrid[N: Integral] extends Grid[N] {
  def cellType: CellType
}

cellType是唯一字段,值得注意的是,celltype也在最开始的创建不同类型的GeoTiffTile对象时出现了.我们先不管Celltype具体具有什么样的内容,来看看那时的Celltype是如何被创建的.

4. CellType类

上一篇解析了GeotiffInfo的生成,cellType字段也包含在其中:

{
  // 省略GeotiffInfo类的其他属性与方法...
  
  def cellType: CellType = (bandType, noDataValue) match {
    // Bit类型的数据不存在Nodata的概念
    case (BitBandType, _) =>
      BitCellType
    // Byte
    // 非默认Nodata值的情况
    case (ByteBandType, Some(nd)) if (nd.toInt > Byte.MinValue.toInt && nd <= Byte.MaxValue.toInt) =>
      ByteUserDefinedNoDataCellType(nd.toByte)
    // 默认Nodata值的情况
    case (ByteBandType, Some(nd)) if (nd.toInt == Byte.MinValue.toInt) =>
      ByteConstantNoDataCellType
    // 不存在Nodata值的情况
    case (ByteBandType, _) =>
      ByteCellType
    // ...省略其他类型
  }
}

可以看出,CellType是根据BandType和noDataValue的值生成的.

其中noDataValue直接来自于gdal_nodata标签,而BandType的定义来自两个标签:BitsPerSample和SampleFormat:

object SampleFormat {
  val UnsignedInt = 1
  val SignedInt = 2
  val FloatingPoint = 3
  val Undefined = 4
}

object BandType {
  def apply(bitsPerSample: Int, sampleFormat: Int): BandType =
    (bitsPerSample, sampleFormat) match {
      case (1, _) => BitBandType
      case (8, UnsignedInt) => UByteBandType
      case (8, SignedInt) => ByteBandType
      case (16, UnsignedInt) => UInt16BandType
      case (16, SignedInt) => Int16BandType
      case (32, UnsignedInt) => UInt32BandType
      case (32, SignedInt) => Int32BandType
      case (32, FloatingPoint) => Float32BandType
      case (64, FloatingPoint) => Float64BandType
      case _ =>
        throw new UnsupportedOperationException(s"Unsupported band type ($bitsPerSample, $sampleFormat)")
    }
}

从代码中我们就能看出,Celltype有两个职能:

  • 定义具体的数据类型
  • 定义Nodata值类型

我们再回到CellType的定义,CellType是一个声明类型:

type CellType = DataType with NoDataHandling

其定义确实包含前面预想的职能:

  • DataType:代表数值类型
  • Nodatahandling:代表Nodata值的处理策略

DataType与NoDataHandling定义于Celltype.scala中.我们逐一解析.

4.1 NodataHandling与Nodata处理策略

首先来看NodataHanling及其分支的定义:

// 基本的特质,没有具体细节
sealed trait NoDataHandling { cellType: CellType => }

// 无Nodata值类型,与NoDataHandling实际一致,也没有任何具体操作
sealed trait NoNoData extends NoDataHandling { cellType: CellType => }

// 有Nodata值类型
sealed trait HasNoData[@specialized(Byte, Short, Int, Float, Double) T] extends NoDataHandling { cellType: CellType =>
  val noDataValue: T
    
  // 该函数只有隐式参数列表,输入的类型T被隐式转换为Numeric[T]
  // WidenedNoData以int形式存储除float/double外类型的Nodata值,以double形式存储float和double类型的Nodata值
  def widenedNoData(implicit ev: Numeric[T]): WidenedNoData =
    if (cellType.isFloatingPoint) WideDoubleNoData(ev.toDouble(noDataValue))
    else WideIntNoData(ev.toInt(noDataValue))
}

// 固定Nodata值类型
sealed trait ConstantNoData[@specialized(Byte, Short, Int, Float, Double) T] 
  extends HasNoData[T] { cellType: CellType => }

// 用户自定义Nodata值类型
sealed trait UserDefinedNoData[@specialized(Byte, Short, Int, Float, Double) T]
  extends HasNoData[T] { cellType: CellType => }

在Geotrellis中,Nodata处理策略有3种:

  • NoNodata:没有Nodata的情况
  • hasNodata:
    • ConstantNoData:恒定的Nodata值的情况.该值由Geotrrellis定义
    • UserDefinedNoData:用户自定义的Nodata值的情况

可以在代码中发现形如trait NoDataHandling { cellType: CellType => }的表达式,这里是使用了self -arrow表达式,这意味着NodataHandling只能混入DataType中,以构成一个CellType类型的this,否则无法编译通过.

从Geotrellis对于Nodata值存储的规则可以窥见,对于8种数据类型,Geotrellis实际上只需要分成按int处理和按double处理即可.

再来看关于DataType的定义.

4.2 DataType类

sealed abstract class DataType extends Serializable { self: CellType =>
  val bits: Int
  val isFloatingPoint: Boolean
  def name: String = CellType.toName(self)

  // 判断两个类型是否相等的预定义方法
  def equalDataType(other: DataType): Boolean

  // 创建基于本类型的DataType with UserDefinedNoData类型的celltype
  def withNoData(noDataValue: Option[Double]): CellType

  // 创建基于本类型的DataType with ConstantNoData类型的celltype
  def withDefaultNoData(): CellType

  def bytes: Int = bits / 8

  // 融合两种数据类型,保证不出现溢出或丢失信息
  def union(other: CellType): CellType =
    // 位数不同保留位数多的
    if (bits < other.bits)
      other
    else if (bits > other.bits)
      self
    // 位数相同保留浮点类型
    else if (isFloatingPoint && !other.isFloatingPoint)
      self
    else
      other

  // 返回指定数量的该类型数据的字节大小
  def numBytes(size: Int): Int = bytes * size

  override def toString: String = name
}

4.3 生成实际的Celltype

以Byte类型为例

sealed trait ByteCells extends DataType { self: CellType =>
  val bits: Int = 8
  val isFloatingPoint: Boolean = false
  // 实现对比方法
  def equalDataType(other: DataType): Boolean = other.isInstanceOf[ByteCells]
  //返回带有用户自定义Nodata值的Byte类型的CellType
  def withNoData(noDataValue: Option[Double]): ByteCells with NoDataHandling =
    ByteCells.withNoData(noDataValue.map(_.toByte))
  // 返回使用默认Nodata值的Byte类型的Celltype
  def withDefaultNoData(): ByteCells with NoDataHandling = ByteConstantNoDataCellType
}

// 伴生对象,方便生成Celltype
object ByteCells {
  def withNoData(noDataValue: Option[Byte]): ByteCells with NoDataHandling =
    noDataValue match {
      case Some(nd) if nd == Byte.MinValue =>
        ByteConstantNoDataCellType
      case Some(nd) =>
        ByteUserDefinedNoDataCellType(nd)
      case None =>
        ByteCellType
    }
}

// 对应Byte类型的实际3种Celltype
case object ByteCellType
    extends ByteCells with NoNoData

// Byte类型对象的默认Nodata值为byteNODATA(Byte.MinValue)
case object ByteConstantNoDataCellType
    extends ByteCells with ConstantNoData[Byte] { val noDataValue = byteNODATA }
    
case class ByteUserDefinedNoDataCellType(noDataValue: Byte)
    extends ByteCells with UserDefinedNoData[Byte]

对比一下UByte:

case object UByteCellType
    extends UByteCells with NoNoData

// 因为Scala实际不存在UByte类型,所以也使用的是Byte类型
case object UByteConstantNoDataCellType
    extends UByteCells with ConstantNoData[Byte] { val noDataValue = ubyteNODATA }
    
/* 
* 与上面的ByteUserDefinedNoDataCellType相比,UByteUserDefinedNoDataCellType
* 需要覆盖原有的widenedNoData方法,因为Byte的范围是[-128,128),而UByte是[0,256),
* 默认的Numeric[byte].toInt(noDataValue)有可能因溢出而丢失信息.
*/
case class UByteUserDefinedNoDataCellType(noDataValue: Byte)
    extends UByteCells with UserDefinedNoData[Byte] {
  override def widenedNoData(implicit ev: Numeric[Byte]) = WideIntNoData(noDataValue)

4.4 Geotrellis支持的全部数据类型

GDAL支持读写的Geotiff数值类型与Geotrellis对比:

GDAL Geotrellis
-- bit
byte byte
-- ubyte
int16 short
uint16 ushort
int32 int32
uint32 --
float32 float32
float64 double
Cint16,Cint32,Cfloat32,Cfloat64 --

可见,Geotrellis的数值模型与GDAL的还是有些许不同:

  • Geotrellis直接支持Bit(Boolean)格式,GDAL并不直接支持.
    • GDAL其实可以通过其他方式读取这种类型的数据.不过对于那些不基于GDAL的Tiff文件读取器,可能不支持.
  • Geotrellis支持Ubyte类型的数据,其实可以用uint16模拟ubyte.
  • Geotrellis不支持uint32类型,在Geotrellis中,uint32被模拟做float32处理.
  • 所有复数类型都不支持.某些卫星影像产品的数据是复数类型的,这些数据可能不被支持,使用geotrellis内部的GDAL模块也不支持.

5. 总结

这次的入手点是Celltype类.Geotrellis面对不同的数据类型和不同的Nodata值处理策略,实现7*3(3种Nodata处理策略)+1(bit没有Nodata的概念)=22种CellType,基本满足了实际需求.

你可能感兴趣的:(How it works(17) Geotrellis是如何读取GeoTiff的(B) 数据类型模型)