SAGA-GIS API を Python から操作する

SAGA-GISによる解析の自動化。今まではR上でrsagaパッケージを使って行っていたが、この方法では解析の中間生成物をメモリ上で読み書きすることができず、ファイルに出力するしかなかった。そのため中間生成物のデータ量が多くなるといろいろと大変。Python からSAGAのAPIを使うとメモリ上でデータの読み書きができるという記事を見つけたので、やってみた。環境構築で何か所もコケて大変だったので作業記録を残しておく。

Python のインストー

PythonでSAGAを操作するためのPythonパッケージは、最新版のPythonには対応していないので、古いPython 2.7.13 をインストール。インストーラをダウンロード・起動し実行。インストール先をC:\Program Files (x86)\Python27に指定し、環境変数PathにPythonのパスを追加するオプションをオンにして、インストール。

saga_api のダウンロード

使用しているSAGAはSAGA2.2.2(64 bit windows)だったので、Source Forge から、同バージョンのAPI (saga_2.2.2_win32_python27.zip) をダウンロード。zipにはいくつかファイルが入っているが、この中の

  1. _saga_api.pyd
  2. saga_api.py

をC:\Program Files (x86)\Python27\lib\site-packages\Python27にコピー。他のファイルは不要。

エラー

サンプルコードを走らせてみる。SAGA GIS のWikiのあるページに載っていた、print_modules_in_modulelib.py を利用。

import saga_api, sys, os

#*******************************************************************************
def printLibraries(modlib):
    mlb     = saga_api.CSG_Module_Library()

    if mlb.Create(saga_api.CSG_String(modlib)) == 0:
        print 'ERROR loading ' + modlib
        return 0

    for i in range(0, mlb.Get_Count()):
        m = mlb.Get_Module(i)

        if m.is_Interactive():        
            print i, ' ' + m.Get_Name() + ' (interactive)'
        else:
            print i, ' ' + m.Get_Name()

    return 1

#*******************************************************************************
if __name__ == '__main__':

    if len( sys.argv ) != 2:
        print 'Usage: get_info_modulelib.py '
    else:
        modlib    = os.environ['SAGA_MLB'] + '/' + sys.argv[1]

        printLibraries(modlib)

コマンドプロンプト上で

Python "(パス)print_modules_in_modulelib.py"

を実行。次のようなエラーが返ってくる。

Traceback (most recent call last):
  File "<pyshell#0>", line 1, in 
    import saga_api
  File "C:\Python27\lib\site-packages\saga_api.py", line 28, in 
    _saga_api = swig_import_helper()
  File "C:\Python27\lib\site-packages\saga_api.py", line 24, in swig_import_helper
    _mod = imp.load_module('_saga_api', fp, pathname, description)
ImportError: DLL load failed: 指定されたモジュールが見つかりません。

このエラーメッセージから、saga_api.py 自体の読み込みは成功し、saga_api.py が依存する _saga_api.pyd の読み込みが失敗したと推定。はじめ、_saga_api.pydが見つけられないのかと思ったが、このタイプのエラーは、pydが依存している他のDLLが読み込めないときにも起こるらしい(なお、pydもDLLの一種)。そこでDLLの依存関係を調べる Dependency Walker というフリーのツールを入手し、読み込めないDLLを特定を試みる。

Dependency Walker

www.dependencywalker.com/からDLし、GUIを起動。_saga_api.pydをドラッグドロップし、読み込ませる。

f:id:ykiu:20170524162741g:plain

(問題の解決後に撮ったスクリーンショットなので表示がすこし違う。詳しくは後述)左上のペイン(と中段のペイン)で、?アイコンがついているのが、読み込めなかったDLL。かなりたくさんのDLLに?アイコンが付いているが、現行のDependency Walker は、Win7以降に導入されたAPI-MS-WIN-*.DLLとEXT-MS-WIN-*.DLLのDLL群に正しく対応できないので、これらのDLLに?アイコンが付いていても、実際には問題ない。そこで、API-MS-WIN-*.DLLとEXT-MS-WIN-*.DLL以外で?アイコンが付いているものを探すと、以下の2つが浮上。

SAGA_API.DLLに関しては、saga_gui.exeなどと同じフォルダに同名のファイルがある。そこで、環境変数Pathにこのファイルへのパスを追加(SAGA_GISインストーラが追加してくれていてもよさそうなものだが、そんなことはしてくれないらしい)。

PYTHON27.DLLに関しては、C:\Windows\SysWOW64で発見。Win32のDLLをWin64上で動かすためのWOW64というエミュレーションシステムがあり、32bitのWindowsでSystem32にいるDLLは、エミュレーションの恩恵を受けるためにここに置かれるらしい。この仕組みにより、32ビットプログラムによるSystem32へのアクセスはSysWOW64に自動的にリダイレクトされるDependency Walker にはWin7以降に導入されたDLL群がmissingと誤表示される問題もあったので、PYTHON27.DLLの?マークも、Depndency Walker の WOW64非対応による誤表示と推定。 ということで、この段階で再びテストスクリプトを実行。なお、環境変数の更新を反映するためにはコマンドプロンプトを再び立ち上げる必要があるようなので、コマンドプロンプトの再起動後にテストスクリプトを実行。またもやエラー。

ImportError: DLL load failed: %1 は有効なWin32 アプリケーションではありません

32bitのPythonで64bitのDLLを読み込もうとするとこのエラーが出るらしい。インストールしているsagaが64bit版だったので、これが原因と推定。32bit版のSAGAをC:\Program Files (x86)\SAGA-GISにインストールし、環境変数Pathもこれに合わせる。ここで再度テストスクリプトを実行。やっと動いてくれた。

Load table: Proj.4-WKT Dictionary...
failed
Load table: Proj.4-WKT Dictionary...
failed
Usage: get_info_modulelib.py 

さきほどのサンプルスクリプトには

modlib    = os.environ['SAGA_MLB'] + '/' + sys.argv[1]

という行があるが、これはSAGA_MLBという環境変数を参照している。この環境変数インストーラは設定してくれないので、手動で登録する必要がある。

環境構築はこれでほぼ終了。このあと自動化のためのスクリプトを書く途中でも大いにハマったのだが、それに関してはいずれ時間があるときに追記したい。