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,基本满足了实际需求.