Geotrellis系列文章链接地址http://www.cnblogs.com/shoufengwei/p/5619419.html
目录
一、前言
最近做一项实验,简单的说就是读取已经存入Accumulo中的瓦片,然后对瓦片进行简单的Map操作然后RenderPng生成瓦片,前台显示。看上去是个很简单的操作,但是中间一直存在一个问题,就是明明数据值范围在[0-10] (除了某些地方无值),但是处理完后某些地方会出现数值严重偏差的情况,在100以上(处理逻辑也不应该出现这么大的值),具体效果就是瓦片中某些地方是空白的(因为用了ColorMap,超过10的没有定义,所以是空白的),百思不得其解,辗转反侧,最后终于顿悟,遂记录之。
二、BUG还原
首先准备一个8位有符号类型的tiff,然后使用ingest导入Accumulo,然后读取tile并进行简单的逻辑处理,然后渲染发送到前台显示,这时候你就可以看到很多诡异的事情。
三、查找BUG
此BUG查找过程颇为闹心,前前后后折腾了好几天,猜测并实验了各种原因,最后才发现真正的问题。
3.1 怀疑处理逻辑
因为我的处理为11-value
,因为原始范围是[0, 10],所以此处相当于将数值反了个个,这个地方会有什么问题呢,怎么结果会大于100多呢,通过各种调试生成tiff等,发现原始数据有-100多的,所以此处就变成了正的100多,那么就先判断value是否小于0,大于0的才处理,成功解决问题,显示OK。但是真的解决问题了吗?(当然没解决,解决了就不会有这篇文章了,哈哈)为什么会出现值为负的情况呢,我原始数据范围可是[0, 10]啊?
3.2 怀疑Byte类型
然后以为是Byte类型造成的,在Scala中,Byte的范围是[-128, 127],而在C#等有些语言中,Byte的范围是[0, 255]。在Geotrellis中ByteArrayTile对应基础数据类型为Byte,UByteArrayTile对应基础数据类型为UByte,二者同时对应tiff中的Byte类型,ByteArrayTile类型生成的tiff多了一项PIXELTYPE=SIGNEDBYTE
。所以刚开始一直以为是数据类型的问题,想当然的认为tiff文件所支持的Byte类型的范围也是[0, 255],其实这时候根本没有发现问题的本质,并且也没有对tiff进行认真研究,认为使用UByteArrayTile就能解决问题,但是考虑到万一将来数据有负值的情况怎么办呢?所以又苦思冥想半天没有结果,折腾了半天BUG也没有解决。
3.3 真相浮出水面
将从Accumulo读出来的数据直接生成tiff,会发现一个很诡异的问题,NODATA这一项居然没有了,我原来可是正儿八经写的-128,在又咨询了圈内人士之后大概明白了为什么会出BUG。
因为在瓦片切割的过程中会进行重采样,这样肯定是读的数据不包含NODATA值,所以在进行重采样的时候有些点自然就变成了负值,因为0到10之间的数与-128作用自然就是负的(比如内插法的线性)。
但是问题又来了,为什么切瓦片之前读TIFF的时候没有读入TIFF的NODATA呢,之前为了解决切瓦片采样方式的问题,重写了ETL类,但是大部分地方都一样,只有在投影和建立金字塔的时候添加了其他采样方法,所以刚好可以在这里进行打印调试,一试果然与猜测一致,输出cellType,全是int8raw,表示读入的是没有NODATA值的Byte类型,怎么会这样,明明原始数据是有NODATA值的,这时候看到输入参数,可以指定cellType,于是数据导入的时候加了一项参数--cellType int8
,测试,发现问题解决,导入的时候打印信息全部正常。
幸福来的太突然,让人不知所措,难道问题就这么解决了(了解国产电视剧的观众都知道:没有,哈哈)。这时候再看从Accumulo中读出来的Tile,发现数据类型居然变成了int8ud0,这是什么鬼,查了一下源码发现是byte类型的用户自定义NODATA,并且NODATA值为0的这么一种类型。
为什么会出现这么一种类型,只能再看读取瓦片的源代码,主要代码如下:
def read(key: K): V = {
val scanner = instance.connector.createScanner(header.tileTable, new Authorizations())
scanner.setRange(new ARange(rowId(keyIndex.toIndex(key))))
scanner.fetchColumnFamily(columnFamily(layerId))
val tiles = scanner.iterator
.map { entry =>
AvroEncoder.fromBinary(writerSchema, entry.getValue.get)(codec)
}
.flatMap { pairs: Vector[(K, V)] =>
pairs.filter(pair => pair._1 == key)
}
.toVector
if (tiles.isEmpty) {
throw new TileNotFoundError(key, layerId)
} else if (tiles.size > 1) {
throw new LayerIOError(s"Multiple tiles found for $key for layer $layerId")
} else {
tiles.head._2
}
}
其实上述代码最关键的就是AvroEncoder.fromBinary(writerSchema, entry.getValue.get)(codec)
,意思就是将二进制数据读成Tile,没看出有什么问题,好吧,请教原作者,只告诉我采用新版本可以,于是我更新新版本Geotrellis,发现这块读取确实好了,但是悲剧的是前面的采样造成的负值的问题又出来了。
又折腾了数次,问题还是没有解决,想到刚开始在数据导入的时候为了实现Tiff边界拼接的问题,路径输入的是文件夹,这样相当于同时导入一个文件夹下的所有Tiff,现在是不是这个地方变了呢。一试果然如此,导入单个Tiff,采样没有问题,同时导入一个文件夹则会出问题。那么这显然又是Geotrellis的一个BUG。
四、解决方案
解决方案就三点:
- 导入数据的时候添加
--cellType int8
即添加指定的类型,可以解决导入的时候无数据值的问题,并能够解决瓦片切割重采样时候造成的无效值。至于为何需要添加此配置项,为什么Geotrellis不能自动读出Tiff的NODATA值还需下一步进一步研究。 - 针对不能再导入文件夹下所有Tiff的问题,有又三种解决方案。第一,如果不需要考虑重采样负值带来的影响可以继续使用文件夹作为输入;第二,可以事先将Tiff拼接起来,当然Tiff不能太大;第三,不考虑Tiff边界处缝隙带来的影响。貌似三种都不是最好的解决方案,下一步要继续研究数据导入这块的源代码,看看有没有办法从根本上解决。
- 从Accumulo读取瓦片cellType的问题在升级到0.10.1后自动解决。
五、总结
本次BUG调试,历经数天,折腾无数次,总结出以下几点:
- 对采样、切瓦片等基础地理信息知识掌握的不够全面。
- 研读源码不够细致。
- 不要完全相信Geotrellis,其也是有不完善以及BUG的————尽信书则不如无书。
- 发现问题远比解决问题重要。
- 做事情要执着。
六、后记
恰逢今日高中群里原先语文老师孩子参加高考,咨询志愿情况,有人建议先选城市、有人建议先选学校、有人建议学计算机、有人建议学会计。。。众说纷纭,各种出世入世。
我说我建议学哲学,其实我觉得其他任何专业都只是工具,只有思想上去了,你干任何事情都能做好。当然有人要说,很多人没学哲学思想也很有深度,做事也很成功。其实还是那句话,一个人一辈子做好一件事情足以,将一件事情能做到至善至美,那么你的思想自然而然的也就上去了,这时候你再去做任何事情,岂有不成的道理。但是一般人深受花花世界的吸引,不能耐得住这份寂寞去做一件事,唯有哲学,能教你从方法论等角度去思考世界、探索世界,自然你的思想也就慢慢得到升华。
对待写程序同样如此,只有拥有一颗执着的心,遇到问题能够刨根问底,你的思想自然也就上去了,对待任何问题你都将如履平地。如果只是一味的为写程序而写程序,那么你终究是一个代码的搬运工。
思想高于一切!