2009年10月26日月曜日

Androidアプリケーションを開発するための準備

このブログ記事をはてなブックマークに追加

来月、海外に出張することもあってdocomoのHT-03Aを購入してしまった。本当は海外で使うというのは口実でAndroidアプリケーションを開発したいだけだったりする。ここでは個人的なメモも兼ねて、Windows上でAndroidアプリケーションを開発するための準備について記しておく。

まず、Eclipse Classicをダウンロード(現時点の最新版は3.5.1)して、それを適当なディレクトリに展開する。C:\に展開した場合、C:\eclipse\というディレクトリができる。因みに日本語版でも問題ないと思うが、自分は英語版のEclipseを使っている。

次にAndroid SDKをダウンロード(現時点の最新プラットフォームは2.0 "eclair")して、やはり適当な場所に展開する。C:\android\に展開した場合、C:\android\android-sdk-windows\というディレクトリが作成される。C:\android\android-sdk-windows\toolsを環境変数PATHに追加しておく。

展開したディレクトリにあるSDK Setup.exeを実行すると、Android SDK and AVD Managerが起動するので、そのままプラットフォームをアップデートする。もし、SSLでエラーが出る場合は、Settingsの"Force https://... sources to be fetched using http://..."のチェックボックスをオンにすること。Installed Packagesでインストールされたプラットフォームを確認できる。必要なバージョンのプラットフォームがインストールされていないときは、Available Packagesで必要なバージョンをインストールできる。

Androidのアップデートが終わったら、Eclipseを起動させる。最初に作業ディレクトリ(workspace)を訊かれるが適当なディレクトリを指定する。起動したら[Help]-[Install New Software...]でAddボタンを押し、NameにAndroid Plugin、Locationにhttps://dl-ssl.google.com/android/eclipse/を指定する。Developer Toolsという項目が表示されるので、チェックボックスをチェックしてNextボタンを押し、最後にFinishボタンを押して、Eclipseを再起動する。

次いで、[Window]-[Preferences]でダイアログを開き、ツリーメニューのAndroidを選択する。SDK LocationにC:\android\android-sdk-windowsを指定して、Applyボタンを押す。

Eclipse上からAndroidのエミュレータを利用するために、Android仮想デバイス(Android Virtual Device, AVD)を作成する。[Windows]-[Android SDK and AVD manager]で作成するか、コマンドラインで以下のコマンドを実行する(ここではデバイス名をmy_avdとしているが、どのような名前でも良い)。自分はコマンドラインで作成した。因みにtargetオプションで使用するプラットフォームのバージョンを設定できる。

android create avd --target 3 --name my_avd

あとは、コードを書くだけだ。動作の確認の意味でも、最初はHello, Worldアプリケーションあたりを作成するのが良いと思う。

Androidの実機でデバッグするには、まず、開発しているプロジェクトのManifestファイルのApplicationタグにandroid:debuggable="true"を記述する(または、ダイアログのApplicationタグの項目でDebuggableをtrueにする)。

さらに、実機(自分の場合はHT-03A)のメニューで、[設定]-[アプリケーション]-[開発]-[USBデバッグ]をチェックする。開発中にスリープモードにしたくない場合は、[スリープモードにしない]をチェックしておく。そして、実機と開発するPCをUSBで接続する。そうするとWindowsが新しいハードウェアを見つけるのでWindows Updateはせずに特定の場所からインストールする。場所はC:\android\android-sdk-windows\usb_driverを指定する。最後にWindowsを再起動する。

実機をUSB接続してEclipseを起動し、先ほどandroid:debuggable="true"にしたプロジェクトを開き、[Run]-[Run Configurations]を選択して、ダイアログを開く。ツリーメニューのAndroid Applicationから現在開いているプロジェクトを選択して、右のペインのTargetタグをクリックし、Deployment Target Selection ModeをManualにする。これでアプリケーション実行時にAndroid Device Chooserダイアログが開くので、実機を選択して実行する。実機上でアプリケーションが起動したら成功だ。

因みに作成したアプリケーションを配布するには、Java SEのkeytoolでキーストアと鍵を作成して、EclipseのAndroid Toolsからアプリケーションに署名をする必要がある。Android Marketを利用したい場合は、デベロッパープロフィールを作成して、クレジットカードで$25を支払えば配布できるようになる。

追記(2009/10/28): Android 2.0 (eclair)が正式リリースされたので、それに合わせて内容を修正した。

続きを読む...

2009年10月21日水曜日

無線LANで現在位置を取得してGoogleマップとストリートビューで表示する

このブログ記事をはてなブックマークに追加

昨年の8月に書いた記事、「Google App Engine: Gearsで位置情報を取得してストリートビューで表示させる」で、無線LANから取得した現在位置をGoogleマップストリートビューで表示するウェブアプリを作ったが、今回、それをブログなどにもそのまま貼り付けられるようにしてみた。もともと、JavaScriptを使ったアプリなのでそれほど変更するところもなかったのだが、無用な部分を削除し、きちんと画面にエラーメッセージを表示するようにした。動作させるためにはGearsをインストールしている必要がある。

無線LANから現在位置情報を取り出すためにGearsのGeolocation APIを使った。位置情報を取り出したらあとはGoogle Maps APIでそれをGoogleマップとストリートビューで表示させるだけだ。因みに、今年の7月からGoogleマップの画面で左上にある四角いボタンを押すと現在位置を表示できるようになっている。

今回のアプリでは、現在位置を示すマーカーに自作の扇形アイコンを使った。ストリートビューの向きと連動している。ペグマン(人型アイコン)でもよかったのだろうけど、個人的には向いている方向が分かりづらいと思う。



以下にソースファイルを示す。ページを読み込んだ際にGearsにより位置情報を取得している。

<script src="http://maps.google.com/maps?file=api&v=2.x&key=Google Maps API Key" type="text/javascript"></script> <script type="text/javascript" src="http://code.google.com/apis/gears/gears_init.js"></script> <script type="text/javascript"> var map; var pano; var overlayInstance = null; var marker; var info_window = false; var lat, lng; var error_msg = ""; function updatePosition(position) { lat = position.latitude; lng = position.longitude; initialize(); } function initialize_geo() { if (window.google && google.gears) { var geo = google.gears.factory.create("beta.geolocation"); geo.getCurrentPosition(updatePosition, function(positionError) { document.getElementById("error").innerHTML = positionError.message; }); } else { error_msg = "位置情報の取得に<a href=\"http://gears.google.com/\">Gears</a>が必要です。"; document.getElementById("error").innerHTML = error_msg; // 秋葉原. lat = 35.698584; lng = 139.774216; initialize(); } } function initialize() { if (GBrowserIsCompatible()) { var latlng = new GLatLng(lat, lng); var zoom = 15; marker = new GMarker(latlng, {icon: getArrowIcon(0.0), clickable: false}); pano = new GStreetviewPanorama(document.getElementById("pano")); GEvent.addListener(pano, "error", handleNoFlash); GEvent.addListener(pano, "initialized", moveStreet); GEvent.addListener(pano, "yawchanged", yawStreet); map = new GMap2(document.getElementById("map_canvas")); GEvent.addListener(map, "click", moveWalker); map.addControl(new GSmallMapControl()); map.addControl(new GMapTypeControl()); map.setCenter(latlng, zoom); map.addOverlay(marker); map.enableScrollWheelZoom(); pano.setLocationAndPOV(latlng); toggleOverlay(); GEvent.addListener(map, "click", function(overlay, latlng) { pano.setLocationAndPOV(latlng); }); } var client = new GStreetviewClient(); client.getNearestPanorama(latlng, setNewLatLng) } function setNewLatLng(data){ if (data.code != 200) { return; } var new_latlng = data.location.latlng; pano.setLocationAndPOV(new_latlng); map.panTo(new_latlng); marker.setLatLng(new_latlng); } function getArrowIcon(bearing) { var icon = new GIcon(); icon.image = getArrowUrl(bearing); icon.iconSize = new GSize(92, 92); icon.iconAnchor = new GPoint(46, 46); return icon; } function getArrowUrl(bearing) { var id = 3 * Math.round(bearing / 3); return "view_marker_" + id + ".png"; } function moveStreet(location_) { map.setCenter(location_.latlng); marker.setLatLng(location_.latlng); document.getElementById("error").innerHTML = error_msg; } function yawStreet(yaw_) { marker.setImage(getArrowUrl(yaw_)); } function moveWalker(overlay_, latlng_) { var client = new GStreetviewClient(); client.getNearestPanorama(latlng_, setNewLatLng) } function toggleOverlay() { if (!overlayInstance) { overlayInstance = new GStreetviewOverlay(); map.addOverlay(overlayInstance); } else { map.removeOverlay(overlayInstance); overlayInstance = null; } } function toggleMarkerLocation() { if (!info_window) { pov = pano.getPOV(); var displayString = [ marker.getLatLng(), "POV yaw: " + pov.yaw, "POV pitch: " + pov.pitch, "POV zoom: " + pov.zoom ].join("<br />"); map.openInfoWindowHtml(marker.getLatLng(), displayString); info_window = true; } else { map.closeInfoWindow(); info_window = false; } } function handleNoFlash(errorCode) { if (errorCode == GStreetviewPanorama.ErrorValues.FLASH_UNAVAILABLE) { document.getElementById("error").innerHTML = "Flashがサポートされていません。"; } else if (errorCode == GStreetviewPanorama.ErrorValues.NO_NEARBY_PANO) { document.getElementById("error").innerHTML = "ストリートビューの範囲外です。"; } } if (window.attachEvent){ window.attachEvent("onload", initialize_geo); window.attachEvent("onunload", GUnload); } else { window.addEventListener("load", initialize_geo, false); window.addEventListener("unload", GUnload, false); } </script> <table><tr> <td><div id="pano" style="float:left; background-color: black; width: 250px; height: 250px"></div></td> <td><div id="map_canvas" style="width: 250px; height: 250px"></div></td> </tr></table> <div><input type="button" onclick="toggleOverlay()" value="ストリートビューレイヤー" /> <input type="button" onclick="toggleMarkerLocation()" value="現在位置" /> <span id="error"></span></div>

続きを読む...

2009年10月5日月曜日

Python: 画像で与えられた迷路に対し2点間の最短経路を求める

このブログ記事をはてなブックマークに追加

迷路の描かれた画像に対して、ピクセルの座標で指定したスタート地点とゴール地点の最短経路を求めるプログラムをPython+PILで書いてみた。使用する画像は、デジカメで撮ったものでも、ウェブから拾ってきたものでも、ペイントソフトで自作したものでも構わない。

まずは使用例を見て欲しい。この画像は携帯カメラで撮った自作の簡単な迷路だ(画像上)。それに対して指定した2点間の最短経路を赤線で示してみた(画像下)。ピクセル単位で計測しているので赤線が若干ガタガタしていて完全な最短経路ではないがほぼ最短と考えていいだろう。迷路画像(画像上)をmaze01.jpgとし、スタート地点の座標が(240, 160)、ゴール地点の座標が(210, 400)の場合、コマンドラインで以下のように実行する。

maze_solver.py maze01.jpg -s 240 160 -g 210 400

これで最短経路を求めることができ、画像ビューアが立ち上がって経路の描かれた画像が表示される(画像下)。画像ビューアではなく画像ファイル(ここではmaze01out.jpgとする)に出力したい場合は、以下のようにする。

maze_solver.py maze01.jpg -s 240 160 -g 210 400 -o maze01out.jpg

このプログラムは2つのパートで成り立っていて、一つは画像認識を使った画像データの領域分けであり、もう一つはその領域内の2点間の経路探索である。それぞれ、連結成分ラベル付け(connected component labeling, CCL)アルゴリズムA*探索アルゴリズムを利用して処理している。詳細についてはそれぞれのリンク先と最後に示したソースコードを参照して欲しい。

次の例は、Wikipediaの迷路の項目で例示されている迷路画像(画像上)を解いてみたものだ(画像中、下)。いずれも迷路の真ん中の小部屋(270, 130)をスタート地点とし、画像中段ではゴールを左上(0, 0)、下段では右下(490, 268)とした。スタートとゴールまでの経路は複数あるが、いずれも最短経路を示している。

画像上段をmaze02.pngとした場合、画像中段、下段は、それぞれ以下のように実行して作成した。

maze_solver.py maze02.png -s 270 130 -g 0 0

maze_solver.py maze02.png -s 270 130 -g 490 268

使い方の詳細は、-hオプションを付けて実行すれば良い。

-aオプションで、探索する画像の大きさを指定している。あまり大きな画像だと時間が掛かるのでデフォルトで60,000ピクセルにしている。ただし、複雑な迷路や写りの悪い写真だと画像がつぶれてしまい、正しい結果にならないかもしれないので、そのようなときは指定するピクセルを大きくする必要がある。元の画像ファイルが指定サイズ以下の場合は実サイズのままで処理される。

-tオプションでは、CCLアルゴリズムで使用する色の閾値を指定している。デジカメなどで撮影した画像は、例えば白い部分でも黄色っぽい白や赤っぽい白など完全に同じ色情報でない場合が多く、大体白に近ければ同じ色と見なすわけだが、その許容範囲がこの閾値となる。因みに閾値を0にすると完全に同じ色情報を持っていないと違う色だと見なされる。

Usage: maze_solver.py [options] imagefile Options: -h, --help show this help message and exit -o OUT_IMAGE, --output-image=OUT_IMAGE output-imagefile -s START, --start=START start position [default: (0, 0)] -g GOAL, --goal=GOAL goal position [default: (0, 0)] -a AREA, --area-size=AREA area size [default: 60000] -t THRESHOLD, --threshold=THRESHOLD threshold of the CCL [default: 30]

以下、ソースコード。

maze_solver.py

#!/usr/bin/env python # -*- coding: utf-8 -*- import sys, os, math, heapq, Image, ImageDraw # Connected Component Labeling - Label Equivalence method class CCL: def __init__(self, imagefile, threshold, area): im = Image.open(imagefile) im = im.convert("RGB") self.orig_size = im.size orig_area = self.orig_size[0] * self.orig_size[1] if orig_area <= area: self.size_rate = 1.0 else: self.size_rate = math.sqrt(float(area) / orig_area) im = im.resize((int(self.orig_size[0] * self.size_rate), int(self.orig_size[1] * self.size_rate))) self.size = im.size self.th = threshold self.W = im.size[0] self.D = im.getdata() self.N = len(self.D) self.L = [] self.R = [] for i in range(len(self.D)): self.L.append(i) self.R.append(i) def diff(self, d1, d2): return abs(d1[0] - d2[0]) + abs(d1[1] - d2[1]) + abs(d1[2] - d2[2]) def scanning(self): has_label = False for i in range(self.N): label = self.N if i - self.W >= 0 and self.diff(self.D[i], self.D[i-self.W]) <= self.th: label = min(label, self.L[i-self.W]) if i + self.W < self.N and self.diff(self.D[i], self.D[i+self.W]) <= self.th: label = min(label, self.L[i+self.W]) if i % self.W != 0 and self.diff(self.D[i], self.D[i-1]) <= self.th: label = min(label, self.L[i-1]) if (i + 1) % self.W != 0 and self.diff(self.D[i], self.D[i+1]) <= self.th: label = min(label, self.L[i+1]) if label < self.L[i]: self.R[self.L[i]] = label has_label = True return has_label def analysis(self): for i in range(self.N): label = self.L[i] if label == i: ref = label label = self.R[ref] while True: ref = label label = self.R[ref] if ref == label: break self.R[i] = label def labeling(self): for i in range(self.N): self.L[i] = self.R[self.R[self.L[i]]] def calculate(self): while True: if self.scanning(): self.analysis() self.labeling() else: return self.L, self.W, self.size_rate, self.orig_size, self.size # A* Search Algorithm class AStar: def __init__(self, data, start, goal): self.data = data self.nrow, self.ncol = len(data), len(data[0]) self.start = start self.goal = goal if data[self.start[0]][self.start[1]] != data[self.goal[0]][self.goal[1]]: print "No route!" sys.exit(0) self.passage = data[self.start[0]][self.start[1]] def heuristic(self, pos): return math.sqrt((pos[0] - self.goal[0])**2 + (pos[1] - self.goal[1])**2) def distance(self, path): if len(path) == 0: return 0 sum = 0 p1 = self.start for p2 in path: sum += math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2) p1 = p2 return int(sum) def neighborhood(self, pos): row, col = pos neighbors = [] if row > 0 and self.data[row-1][col] == self.passage: neighbors.append((row - 1, col)) if row < self.nrow - 1 and self.data[row+1][col] == self.passage: neighbors.append((row + 1, col)) if col > 0 and self.data[row][col-1] == self.passage: neighbors.append((row, col - 1)) if col < self.ncol - 1 and self.data[row][col+1] == self.passage: neighbors.append((row, col + 1)) if row > 0 and col > 0 and self.data[row-1][col-1] == self.passage: neighbors.append((row - 1, col - 1)) if row < self.nrow - 1 and col > 0 and self.data[row+1][col-1] == self.passage: neighbors.append((row + 1, col - 1)) if row > 0 and col < self.ncol - 1 and self.data[row-1][col+1] == self.passage: neighbors.append((row - 1, col + 1)) if row < self.nrow - 1 and col < self.ncol - 1 and self.data[row+1][col+1] == self.passage: neighbors.append((row + 1, col + 1)) return neighbors def search(self): path = [] queue = [] checked = [self.start] heapq.heappush(queue, (self.distance(checked) + self.heuristic(self.start), checked)) while len(queue) > 0: score, path = heapq.heappop(queue) last = path[-1] if last == self.goal: return path neighbors = self.neighborhood(last) for nb in neighbors: if nb in checked: continue checked.append(nb) newpath = path + [nb] heapq.heappush(queue, (self.distance(newpath) + self.heuristic(nb), newpath)) return [] def draw_path(in_image, out_image, path, SR): im = Image.open(in_image) im = im.convert("RGB") draw = ImageDraw.Draw(im) draw.line([(int(p[1] / SR), int(p[0] / SR)) for p in path], fill=255) if out_image: im.save(out_image) else: im.show() def main(args): from optparse import OptionParser usage = "usage: %prog [options] imagefile" parser = OptionParser(usage=usage) parser.add_option("-o", "--output-image", type="string", help="output-imagefile", dest="out_image", default="") parser.add_option("-s", "--start", type="int", nargs=2, help="start position [default: %default]", dest="start", default=(0, 0)) parser.add_option("-g", "--goal", type="int", nargs=2, help="goal position [default: %default]", dest="goal", default=(0, 0)) parser.add_option("-a", "--area-size", type="int", help="area size [default: %default]", dest="area", default=60000) parser.add_option("-t", "--threshold", type="int", help="threshold of the CCL [default: %default]", dest="threshold", default=30) options, args = parser.parse_args() if len(args) == 0: parser.print_help() parser.exit() in_image = args[0] print "Connected component labeling phase..." ccl = CCL(in_image, options.threshold, options.area) D, W, SR, ORIG_SZ, SZ = ccl.calculate() print " Width: %d (%d), Height: %d (%d)" % (ORIG_SZ[0], SZ[0], ORIG_SZ[1], SZ[1]) data = [D[i:i+W] for i in range(0, len(D), W)] print "A* searching phase..." print " Start: (%d, %d), Goal: (%d, %d)" % (options.start + options.goal) start = (int(options.start[1] * SR), int(options.start[0] * SR)) goal = (int(options.goal[1] * SR), int(options.goal[0] * SR)) astar = AStar(data, start, goal) path = astar.search() print "Drawing path on the imagefile..." draw_path(in_image, options.out_image, path, SR) if __name__ == "__main__": main(sys.argv)

続きを読む...