核密度计算是一个我看到很多次但是记不住的“老熟人”。
文档里说
Kernel density is one way to convert a set of points (an instance of vector data) into a raster. In this process, at every point in the point set, the contents of what is effectively a small Tile (called a Kernel) containing a predefined pattern are added to the grid cells surrounding the point in question (i.e., the kernel is centered on the tile cell containing the point and then added to the Tile).
核密度估计是一种将一系列点转换成一个栅格的方式。在现实中的统计量大多是离散的。以一维角度来看,通常可以用直方图来近似表示某一区间的概率密度,核密度估计的作用就是通过平滑密度函数将柱状图变成连续的曲线。
从二维角度来看,感觉有点像用一系列点去插值出一个面。
产生权值在0-32范围内的随机点
val extent = Extent(-109, 37, -102, 41);
//这是科罗拉多州的范围
def randomPointFeature(extent: Extent): PointFeature[Double] = {
//根据给定范围产生随机值
def randInRange(low: Double, high: Double): Double = {
val x = Random.nextDouble()
low * (1 - x) + high * x
}
//产生权重随机在[0.32)的随机点
Feature(Point(randInRange(extent.xmin, extent.xmax), randInRange(extent.ymin, extent.ymax))
, Random.nextInt() % 16 + 16)
}
val pts = (for (i <- 1 to 1000) yield randomPointFeature(extent)).toList
Feature 一般是将几何和属性结合起来的一种结构
scala:for循环中的 yield 会把当前的元素记下来,保存在集合中,循环结束后将返回该集合。Scala中for循环是有返回值的。如果被循环的是Map,返回的就是Map,被循环的是List,返回的就是List。所以这里返回的是PointFeature[Double]的集合。
创建核密度函数,根据点集、和密码函数和空间范围,以及像素分辨率生成瓦片
import geotrellis.raster._
import geotrellis.raster.mapalgebra.focal.Kernel
val kernelWidth: Int = 9
/* Gaussian kernel with std. deviation 1.5, amplitude 25 */
val kern: Kernel = Kernel.gaussian(kernelWidth, 1.5, 25)
val kde: Tile = pts.kernelDensity(kern, RasterExtent(extent, 700, 400))
结果。700表示列数,400表示行数
每个点变成了对应的9*9【kernelWidth】的方格大小,值都是从中心依次下降
分割瓦片
对大图进行分割,分成74的网格,每个网格为100100的单元
val tl = TileLayout(7, 4, 100, 100)
val ld = LayoutDefinition(extent, tl)
计算以每个点为中心的核范围大小
def pointFeatureToExtent[D](kwidth: Double, ld: LayoutDefinition, ptf: PointFeature[D]): Extent = {
val p = ptf.geom
Extent(p.x - kwidth * ld.cellwidth / 2,
p.y - kwidth * ld.cellheight / 2,
p.x + kwidth * ld.cellwidth / 2,
p.y + kwidth * ld.cellheight / 2)
}
def ptfToExtent[D](p: PointFeature[D]) = pointFeatureToExtent(9, ld, p)
计算每个网格索引对应的点序列
import geotrellis.spark._
def ptfToSpatialKey[D](ptf: PointFeature[D]): Seq[(SpatialKey,PointFeature[D])] = {
val ptextent = ptfToExtent(ptf)
val gridBounds = ld.mapTransform(ptextent)
for {
(c, r) <- gridBounds.coords
if r < ld.tileLayout.layoutRows && r >= 0//这个改了官网的例子
if c < ld.tileLayout.layoutCols && c >= 0
} yield (SpatialKey(c,r), ptf)
}
val keyfeatures: Map[SpatialKey, List[PointFeature[Double]]] =
pts
.flatMap(ptfToSpatialKey)
.groupBy(_._1)
.map { case (sk, v) => (sk, v.unzip._2) }
注意,if r < ld.tileLayout.layoutRows && r >= 0 if c < ld.tileLayout.layoutCols && c >= 0
改了官网的例子,行列索引号应该小于7和4才对。
结果如下
结果是长度28的列表,每一个元素是一个哈希映射,键为空间索引,值为核落在索引对应的点的列表
对每一个子瓦片进行核密度估计
val keytiles = keyfeatures.map { case (sk, pfs) =>
(sk, pfs.kernelDensity(
kern,
RasterExtent(ld.mapTransform(sk), tl.tileDimensions._1, tl.tileDimensions._2)
))
}
结果如下,结果是长度28的列表,每一个元素是一个哈希映射,键为空间索引,值包含一个100*100的数组,即subtitle的核密度估计结果。
将结果合并
import geotrellis.spark.stitch.TileLayoutStitcher
val tileList =
for {
r <- 0 until ld.layoutRows
c <- 0 until ld.layoutCols
} yield {
val k = SpatialKey(c,r)
(k, keytiles.getOrElse(k, IntArrayTile.empty(tl.tileCols, tl.tileRows)))
}
val stitched = TileLayoutStitcher.stitch(tileList)._1
最后得到的结果和直接进行核密度估计的结果是一样的
通过spark进行分布式计算
为什么我们要进行切片然后进行运算呢?
通过将整个区域分解成更小的部分,这样就可以利用spark提供的分布式框架将任务分散到许多机器上,从而加速了整个过程。这个教程关注的是如何利用spark提供的接口-RDD。一个RDD是一个分布式的集合,具有集合的所有常用功能——map、reduce以及一些序列操作的分布式版本。等等。
执行步骤
- 利用点列表创建rdd。将点分为10个分区。
val conf = new SparkConf().setMaster("local").setAppName("Kernel Density")
val sc = new SparkContext(conf)
val pointRdd = sc.parallelize(pts, 10)
这里的10代表的就是分区数量。
让我们来了解一下spark里的分区
RDD的数据集在逻辑上被划分为多个分片,每一个分片称为一个分区。分区是RDD内部并行计算的一个单元。分区的规格决定了并行计算的粒度,每一个分区的数值计算都是在一个任务中进行的。也就是任务的个数,是由RDD的分区数决定的。
RDD的分区的原则是尽量使得分区个数等于集群核心数目
所以,这里将1000个点分成了10部分。
- 接下
val tileRdd: RDD[(SpatialKey, Tile)] =
pointRdd.flatMap(ptfToSpatialKey).mapPartitions({ partition =>
partition.map { case (spatialKey, pointFeature) =>
(spatialKey, (spatialKey, pointFeature))
}
}, preservesPartitioning = true)
.aggregateByKey(ArrayTile.empty(DoubleCellType,ld.tileCols, ld.tileRows))(stampPointFeature, sumTiles)
.mapValues { tile: MutableArrayTile => tile.asInstanceOf[Tile] }
pointRdd .flatMap(ptfToSpatialKey)
是每个点和空间索引构成的map((2,0)->(PointFeature))
组成一个数组
mapPartitions()
是进行分区。
aggregateByKey(参数1)(参数2,参数3)
需要三个参数,第一个是初始值和key的每一个value进行的操作,第二个参数是两个现有值的聚合操作
- 参数1 :
ArrayTile.empty(DoubleCellType, ld.layoutCols, ld.layoutRows)
一个指定了行列数的空数组,注意这里改了官网的代码,应该是创建100*100的分辨率的tile - 参数2:
stampPointFeature
根据瓦片和(空间索引,点)进行KDN - 参数3:
sumTiles
进行求和操作
最后将MutableArrayTile 类型转换成Tile类型
stampPointFeature和sumTiles代码如下
import geotrellis.raster.density.KernelStamper
def stampPointFeature(
tile: MutableArrayTile,
tup: (SpatialKey, PointFeature[Double])
): MutableArrayTile = {
val (spatialKey, pointFeature) = tup
val tileExtent = ld.mapTransform(spatialKey)
val re = RasterExtent(tileExtent, tile)
val result = tile.copy.asInstanceOf[MutableArrayTile]
KernelStamper(result, kern)
.stampKernelDouble(re.mapToGrid(pointFeature.geom), pointFeature.data)
result
}
import geotrellis.raster.mapalgebra.local.LocalTileBinaryOp
object Adder extends LocalTileBinaryOp {
def combine(z1: Int, z2: Int) = {
if (isNoData(z1)) {
z2
} else if (isNoData(z2)) {
z1
} else {
z1 + z2
}
}
def combine(r1: Double, r2:Double) = {
if (isNoData(r1)) {
r2
} else if (isNoData(r2)) {
r1
} else {
r1 + r2
}
}
}
def sumTiles(t1: MutableArrayTile, t2: MutableArrayTile): MutableArrayTile = {
Adder(t1, t2).asInstanceOf[MutableArrayTile]
}
- 组织元数据和结果rdd,并保存到本地
val metadata = TileLayerMetadata(DoubleCellType,
ld,
ld.extent,
LatLng,
KeyBounds(SpatialKey(0,0),
SpatialKey(ld.layoutCols-1,
ld.layoutRows-1)))
val resultRdd = ContextRDD(tileRdd, metadata)
resultRdd.foreach(result => {
val col = result._1._1
val row = result._1._2
if(col >=0 && col < 7 && row >= 0 && row < 4) {
GeoTiff(result._2, ld.mapTransform(result._1), LatLng)
.write("C:\\Users\\79128\\IdeaProjects\\Geotrellis\\study\\output\\spark\\" + row + "," + col + ".tif")
}
})
总结
个人对scala的语法和作用还不是很理解。需要多进行实操,遇到不懂得,多去查询官网文档