gdalでfocal statistics

生き物に対する環境の評価をGISで行う際、行動圏の範囲を集計する近傍計算が効果的です(と私は思っています)。
そのためのツールはArcGISなら"Focal Statistics"、GRASSなら"r.neighbors"を利用します。だけども、ArcGISでは高価なSpatial Analystが必要だし、GRASSは準備が面倒だし、QGISではなぜかプロセッシングツールでエラーがでます(私だけ?)。なのでGDAL Pythonを使って、近傍計算が簡単にできるgdalfocal.pyを実装したので紹介します。

使い方

OSGeo4Wのコンソールで以下のように打ちます。

python gdalfocal.py input.tif output.tif -r 500 --nodata -9999.0 -s mean

半径500m圏の平均値をoutput.tifに出力します。ラスタの外に集計円がはみ出るエッジ部分はnodataとして-9999.0に設定しています。

説明・制限
  • -r オプションは集計円の半径を指定します。単位は地図の単位です。セル数ではありません。
  • -s オプションはsum(合計)かmean(平均)を指定します。
  • いまのところFloat32のラスタデータにしか対応していません。
  • 集計範囲内にnodataがあるラスタには対応していません。
  • 結果に不具合があるかもしれませんので at your own risk ということで。
コード

ここから
https://github.com/tmizu23/gdalfocal

もしくは、これをコピペしてgdalfocal.pyで保存

# -*- coding: utf-8 -*-
import os, sys, gdal, ogr, numpy,argparse
from gdalconst import *
from osgeo import osr

#x,y座標を中心とした円からx方向に1or-1移動するときの差分
def xdiff(x,y,dx):
    sum=0
    if dx == 1 or dx == -1:
        for (j,i) in zip(xlist,ylist):
            sum = sum + data[y+i][x+j*dx+dx]- data[y+i][x-j*dx]
    return sum
#x,y座標を中心とした円からy方向に1or-1移動するときの差分
def ydiff(x,y,dy):
    sum=0
    if dy == 1 or dy == -1:
        for (i,j) in zip(xlist,ylist): #i,j入れ替え
            sum = sum + data[y+i*dy+dy][x+j]- data[y-i*dy][x+j]
    return sum

#x,y座標を中心としてdist距離以下の値の合計
def focalsum(x,y,dist):
    sum=0
    row_num = int(round(-dist/y_size))
    col_num = int(round(dist/x_size))

    if (y-row_num < 0 or y+row_num >= rows or x-col_num < 0 or x+col_num >= cols):
        return ndv
    for i in range(-row_num,row_num+1):
        for j in range(-col_num,col_num+1):
            if (i*y_size)*(i*y_size)+(j*x_size)*(j*x_size) <= dist*dist:
                sum = sum + data[y+i][x+j]
    return sum

if __name__ == '__main__':
    ##
    # inputのデータ形式はFloat32だけ対応
    # フォーカル範囲内にNODATがあるものはダメ

    parser = argparse.ArgumentParser(description='This is focal statistics program.') # parserを作る
    parser.add_argument('input')
    parser.add_argument('output')
    parser.add_argument('-nodata', type=float,default=-9999.0,help="Area's edge is nodata of this value.") # オプションを追加します
    parser.add_argument('-r', type=int,required=True,help='statistical radius by map unit') # このオプションは必須です
    parser.add_argument('-s', default='sum',type=str, choices=['sum','mean'],help='statistical type, sum or mean') # このオプションは必須です
    parser.add_argument('--version', action='version', version='%(prog)s 0.1') # version
    args = parser.parse_args()


    argvs = sys.argv
    input=args.input
    output=args.output
    dist=args.r
    statics = args.s
    ndv=args.nodata


    gdal.AllRegister()
    ds = gdal.Open(input)
    data = ds.GetRasterBand(1).ReadAsArray()
    cols = ds.RasterXSize
    rows = ds.RasterYSize
    (upper_left_x, x_size, x_rotation, upper_left_y, y_rotation, y_size) = ds.GetGeoTransform()
    Projection = osr.SpatialReference()
    Projection.ImportFromWkt(ds.GetProjectionRef())

    outdata = numpy.empty((rows,cols))
    outdata.fill(ndv)


    row_num = int(round(-dist/y_size))
    col_num = int(round(dist/x_size))
    xlist=[]
    ylist=[]

    circlenumber = 0

    ##半円の円周部分のX,Y座標取得。円内のセル数カウント
    for i in range(-row_num,row_num+1):
        j=0
        circlenumber = circlenumber + 1
        while (i*y_size)*(i*y_size)+(j*x_size)*(j*x_size) <= dist*dist:
            j = j + 1

        circlenumber = circlenumber + (j-1)*2
        xlist.append(j-1)
        ylist.append(i)


    print "### INFO"
    print "cols_cell_count:%d" % cols
    print "rows_cell_count:%d" % rows
    print "focal_cell_count:%d" % circlenumber
    print ""

    #row_num行目をまず計算
    outdata[row_num][col_num] = focalsum(col_num,row_num,dist)

    for j in range(col_num+1, cols-col_num):
        outdata[row_num][j] = outdata[row_num][j-1] + xdiff(j-1,row_num,1)

    print "0...",
    k=0
    maxnum = (rows-row_num-row_num-1)*(cols-col_num-col_num)
    #row_num+1行目以降を計算
    for i in range(row_num+1,rows-row_num):
        for j in range(col_num, cols-col_num):
            outdata[i][j] = outdata[i-1][j]+ydiff(j,i-1,1)
            #プログレス表示
            if (100*k/maxnum) % 100 > (100*(k-1)/maxnum) % 100 and ((100*k/maxnum) % 10 == 0):
                sys.stdout.write("%d..."% ((100*k/maxnum) % 100))
            k=k+1


    #平均値を計算
    if statics == 'mean':
        for i in range(rows):
            for j in range(cols):
                if outdata[i][j] != ndv:
                    outdata[i][j]=outdata[i][j]/circlenumber


    driver = gdal.GetDriverByName('GTiff')
    dst_ds = driver.Create(output, cols, rows, 1,gdal.GDT_Float32)

    dst_ds.SetGeoTransform(ds.GetGeoTransform())
    dst_ds.SetProjection( Projection.ExportToWkt())
    dst_band = dst_ds.GetRasterBand(1)
    dst_band.WriteArray(outdata)
    dst_band.SetNoDataValue(ndv)

    dst_ds = None
    ds = None

感想

  • gdal python 便利だなー
  • 環境の評価をメッシュに切ったベクトルデータでやってるのを見ると、ラスタ解析ではだめなの?と思うんだけど、単にSpatialAnalystが高いからとか、GRASS GISの使い方が難しいからというのが、案外その原因だったりして。