#daiizメモ

Scrapboxに夢中

【GAE/Python 解決済】GAEでPILをimportするのに躓いて一日悩んだ話

昨日からずっと悩んでいたこれ

が解決した.結局のところプログラムの書き方が間違っていたということではなくて,どこのPIL(Python Image Library; Pillow)を呼び出すかという一点で詰まっていたらしい.まずは結論を書く.

  • Google App Engine でも普通にPILは使える.
  • Product環境(GAEサーバ上)では,GAE側で用意されているPILをロードする必要がある.
  • ローカル開発環境で使うためのPILをGAEプロジェクト内に含めてはいけない.

これだけ守り,app.yamlappengine_config.py の書き方を間違えなければ上手くいく.一つずつ振り返りメモを書いておく.

PIL のインストール

Product環境には PIL は既に用意されているので,GAEプロジェクト内にPILをインストールする必要はない.とは言っても,ローカルサーバを起動して開発するときは各自で端末にインストールする必要がある.このときは,以下のように-t lib指定した pip install をしてはいけない

# これは gcloud などを入れるときのやり方
$ pip install -t lib Pillow

これだと,プロジェクトルートのライブラリフォルダlibにインストールされてしまう.libフォルダは諸々のソースコードとともにGAEにアップロードされるのでここにPILを入れてしまうと,サーバ上で実行されるアプリケーションはこちらのPILを参照してしまう.正しくは,こうやる.

$ pip install Pillow

なぜ自前で用意したこちらのPIL*1を参照させてはいけないのか?
これはおそらく PIL/Image.py 内でGAEが禁止している関数が呼び出されているからだと思われる.実際のところ,GAEプラグインが入ったPyCharmなどのIDEでこのファイルを開くと,幾つかの行において「System calls are not allowed by Google App Engine sandbox」のような警告が出る. GAE側で提供されているPILはこれらのシステムコールの部分を上手いこと解決してくれているのだろう.

PILに限らず,このページに挙げられているような,既にGAEに組み込まれている Third-party Libraries を手元の環境にインストールするときは気を付けたい.

import文

正解な書き方はこちら(もちろん lib/ には PIL を入れない!)

from PIL import Image

これを書いておくだけで,

  • Product環境では GAEが用意した PIL/Image.pyc
  • ローカル環境では .pyenv/versions/2.7.10/lib/python2.7/site-packages/PIL/Image.pyc

が読み込まれる.

以下は,少々厄介な話になるが,一応書いておく.ローカルでは動くけどデプロイしたら動かないという現象の表. ここまでで書いたようにしていれば何の問題もないので,全部僕の勘違いが悪いのけれど,折角なのでまとめてみた.

「O」は正常に動く,「X」はエラーで失敗することを表す.マス内の上段はProduct環境での結果,下段はローカル開発環境での結果を記している.

lib/PIL あり lib/PIL なし(正解)
from PIL import Image(正解) X
O
O
O
from lib.PIL import Image X
X
-
-

ちなみにサーバでのエラーはすべて ImportError: dynamic module does not define init function (init_imaging) となる.

作業中のメモ

こういうふうに詰まると辛いけれど,ある瞬間を越えて,徐々に全貌が垣間見えてくると楽しくなってくる. f:id:daiiz:20160918231756j:plain

参考ドキュメント

*1:/base/data/home/s~[project-id]/[version].[数字列]/lib/PIL/Image.py

【GAE/Python 解決済】IOError: cannot identify image file ...

Google App Engine(Python)のローカル開発環境で,Base64エンコードされたJPEGファイルをPIL(Python Image Library)にロードしようとしている. IOError: cannot identify image file というエラーが出て進めない.ぐぐったらcStringIOあるいはBytesIOを使う方法が出てきたので両方試してみたけどダメだった.

一昨日の作業終了時は普通に動いていたのが一番の謎.

とりあえず,GAEのローカル開発環境問題なのかPythonのコードが間違ってるのかを突き止めるために,GAEでない環境で動かしてみる.

追記

# coding: utf-8
from PIL import Image
import StringIO
import cStringIO
from io import BytesIO

if __name__ == '__main__':
    img_str = "data:image/jpeg;base64,/9j/..."
    img0 = StringIO.StringIO(img_str)
    img1 = cStringIO.StringIO(img_str)
    img2 = BytesIO(img_str)
    im = Image.open(img0)

最後の行の引数をimg0, img1, img2 として試したところ全部似たようなIOエラー

img0 => IOError: cannot identify image file <StringIO.StringIO instance at 0x...>
img1 => IOError: cannot identify image file <cStringIO.StringI object at 0x...>
img2 => IOError: cannot identify image file <_io.BytesIO object at 0x...>

img_strの接頭辞data:image/jpeg;base64,を外しても結果は同じ

追記

a2b_base64という関数を使ってimg_strをバイナリにする方法でPythonスクリプトを書いたところ動いた.

from PIL import Image
import StringIO
import cStringIO
from io import BytesIO
from binascii import a2b_base64

if __name__ == '__main__':
    img_str = "data:image/jpeg;base64,/9j/...".split("data:image/jpeg;base64,")[1]
    img3 = a2b_base64(img_str)
    a = StringIO.StringIO(img3)
    im = Image.open(a)

しかし,これをGAE環境に移植すると相変わらずエラーが出る.aに代入しているところまでは動いていて,imの行で落ちていることは確認できている.

cannot identify image file <StringIO.StringIO instance at 0x...>

一体どうすればいいんだ

解決報

【GAE/Python 解決済】ImportError: No module named PIL._imaging

Pythonで画像処理するライブラリ PIL (Pillow) を入れて,ローカルで動かそうとしているところ.Macでローカルサーバを立ち上げたところ表題のエラーがでた.

from PIL import Image

の箇所でこけてる.

Pillow は以下のようにインストール済み

$ pip install -t lib pillow

ここ のドキュメントを読んで app.yaml とかは記述した.

現在進行形で詰まってる.進展があったら追記する.

追記1

app.yaml の記述:

- name: Pillow
  version: "3.1.1"

を,以下のように変更.

- name: PIL
  version: "3.1.1"

追記2

バージョンについて注意を受けたので,さらに以下のように変更

- name: PIL
  version: "latest"

これで解決.

lua触ってる その2

5月頃に早稲田大学から発表された,ディープラーニングを用いて白黒画像をカラー画像に着色するプログラムを実行してみた.Luaで書かれたソースコードと学習済みモデルがGitHubで公開されているので自由に試すことができる.ありがたい.

白黒画像とカラー画像の組を大量に使って,着色に必要となる特徴をディープラーニングで学習しているらしい.学習後は,成果物として得られた特徴を使うことで,与えられた未知の白黒画像に対して,"これを自然な感じで色付けするとこんなふうになるだろう" というカラー画像を生成できる.以下は『ディープネットワークを用いた大域特徴と局所特徴の学習による白黒写真の自動色付け』より引用した,ネットワークのモデル構造のスクリーンショットです.

併せて公開されている論文も読んでいるところだが,難しい.


ここからは,手元のMacで画像変換を実行するまでの手順といくつかの実行例を書いていく.

Torch7

先に挙げたリポジトリのREADMEを読むと分かるが,実行するにはtorchが必要.torchA SCIENTIFIC COMPUTING FRAMEWORK FOR LUAJIT ということで,機械学習アルゴリズムを幅広くサポートしているライブラリである(初めて知った).image, nn, nngraph のようにニューラルネットワークを構成する上で便利なパッケージが用意されている.
公式の説明に従って,まずはTorchをインストールする.cmakeが必要なので入っていない場合は

$ sudo brew install cmake

などを実行してインストールしておく.torch本体は下記を順に実行すればインストールできる.

$ git clone https://github.com/torch/distro.git ~/torch --recursive
$ cd ~/torch
$ ./install.sh

最後に to PATH and LD_LIBRARY_PATH in your /Users/daiki/.zshrc? (yes/no) のように聞かれるので,yesと答えてインストール完了.仕上げに source ~/.zshrc するのを忘れない.

$ th

と打って,

f:id:daiiz:20160911012334p:plain

のように表示されればインストール成功.

siggraph2016_colorization

最初に挙げたリポジトリをcloneする.続けて,READMEにある通り,colorization model をダウンロードする.

$ ./download_model.sh

これを実行すると colornet.t7 というファイルがダウンロードされる.約690MBあるので少々時間がかかる.

着色実験

  • input_image: 白黒画像ファイルパス
  • output_image: 生成されたカラー画像ファイルの保存パス

を指定して実行する.

$ th colorize.lua <input_image> [<output_image>]

サンプル画像

リポジトリ内に用意されていた白黒画像 ansel_colorado_1941.png で試してみる.左の白黒画像から,右の画像が生成された.

f:id:daiiz:20160911013259p:plain:w340 f:id:daiiz:20160911013313p:plain:w340

那須で撮った入道雲の写真を白黒に変換したもので試すとこうなった.

f:id:daiiz:20160911014835j:plain:w340 f:id:daiiz:20160911014846j:plain:w340

山の斜面はこうなる.

f:id:daiiz:20160911015047j:plain:w340 f:id:daiiz:20160911015102j:plain:w340

風景以外ではどうか.ケンタッキーのチキンとポテトで試す.

f:id:daiiz:20160911015332j:plain:w340 f:id:daiiz:20160911015342j:plain:w340

正解カラー画像(オリジナル画像)はGoogle フォトのアルバムに置いておいた.プログラムによって生成された画像と比べてみると,良い感じに再現されていることが分かる.すごい技術だ.

lua触ってる

去年くらいから身のまわりでスクリプト言語”Lua”の紹介文などをよく見かけて気になっていた.最近再び見かけるようになってきて,気になり度MAXになったので実行環境を整えた.Macではbrewで入れられるのでラク.

$ brew install lua

まずはHello World.簡単に記述できる.

print('Hello, world!')

続いてFizzBuzz.1から順に整数を表示していくが,その数が3の倍数であれば「Fizz」を,5の倍数であれば「Buzz」を,3の倍数かつ5の倍数であるときは「FizzBuzz」を数の代わりに出力する.繰り返しと条件分岐の書き方を学ぶ.

-- 1から16までのFizzBuzz
for i = 1, 16 do
    if i % 15 == 0 then
        print('FizzBuzz')
    elseif i % 5 == 0 then
        print('Buzz')
    elseif i % 3 == 0 then
        print('Fizz')
    else
        print(i)
    end
end

書きやすい.

Shokujin Camera

食神*1の590円定食の写真を撮ると定食番号が判定されるAndroidアプリを作っている.開発のきっかけとしては『TensorFlowで食神の定食画像を分類する実験 - ShikousakuGo』での成果を何か具体的なアプリに仕上げたかったという思いが大きい.このアプリを作り始める前に『写真を与えると食神定食番号が判定されるウェブアプリをつくりました - ShikousakuGo』という似たようなものをウェブアプリ形式で作ったけれど,アプリというより実験サイトっぽくてあまり納得いかなかった.

アプリを起動するとカメラ画面になる.ここで定食写真を撮りしばらく待つと,写真に予測した番号を入れてくれる*2.判定はサーバ側でやっていて,その結果を用いて端末側で写真加工して保存している.以下は判定に成功したときのツイート.

ストアではまだ公開していないけれど,GitHubからは一応入手できる.Android Studioで開発中のものをそのままアップしているので,cloneしたものを読み込むだけで動くと期待している.Android 5.0 Lollipop 以降のバージョンで動く.

GitHub - daiz713/ShokujinCamera: 食神590円定食専用カメラ - 定食番号を判定します

*1:調布にある中華料理店

*2:定食1, 3, 4, 5番に対応

【GAE/Python】URLセーフキー/id を用いてCloud Datastore のアイテムを取得する

前回の記事の続きの勢いでメモ.

f:id:daiiz:20160827035845p:plain:h60

URL セーフキーを用いる場合

Screenshotというデータモデルが定義されているとする.関数getを使う.

e_key = 'ahFkZXZ-c3Znc2NyZWVuc2hvdHIXCxIKU2NyZWVuc2hvdBiAgICAgMCvCQw'
entities = Screenshot.get(keys=[e_key])
# => [<models.Screenshot.Screenshot object at 0x...>] 

エンティティidを用いる場合

Screenshotというデータモデルが定義されているとする.関数get_by_idを使う.

e_id = 5275456790069248  # long型 or int型で指定
entities = Screenshot.get_by_id(ids=[e_id])
# => [<models.Screenshot.Screenshot object at 0x...>] 

【GAE/Python】Google App EngineのデータストアでのURLセーフキー, id, エンティティの種類

Google App Engine のデータストア(Cloud Datastore)に保存されるアイテム(エンティティ)には,「エンティティの種類」と「id」が与えられている.

例えば,ユーザー情報を管理するデータモデルを

from google.appengine.ext import db

class User (db.Model): 
    first_name = db.StringProperty()
    last_name  = db.StringProperty()
    user_name  = db.StringProperty()

のように定義して,これのインスタンスをデータストアに保存した場合には「エンティティの種類」はUserとなる.idは保存時に自動で採番される.

エンティティの種類, idを取得する方法

プログラム中でエンティティeの情報を取得したい場合は,関数keyを使えばよい.先のものとは別の例での実行結果は以下のとおり.

print(e.key())
# -> datastore_types.Key.from_path(u'Screenshot', 5348024557502464L, _app=u'dev~svgscreenshot')

print("%s" % str(s.key().id()))  # エンティティのid
# -> 5348024557502464

print("%s" % s.key().kind())  # エンティティの種類
# -> Screenshot

エンティティの種類やidを取得できると,エンティティ固有内容をシェアするウェブページのURLを構成する場合に役立つ.

f:id:daiiz:20160827035845p:plain:h70

URLセーフキー

上記例での e.key() で得られた結果を文字列化すると,以下のように,

print("%s" % str(e.key()))
# -> ahFkZXZ-c3Znc2NyZWVuc2hvdHIXCxIKU2NyZWVuc2hvdBiAgICAgIDACQw

少々長めのキーが得られる.これは何か?
Google Cloud Consoleの「データストア」の項目での説明によると,上記で得られた文字列は「URL セーフキー」と呼ばれるものらしく,下記引用のような説明が書かれていた.

これはエンティティの種類と ID を base64 でエンコードした、シリアライズ バージョンです。 Cloud Console、App Engine クライアント ライブラリ、Cloud Datastore NDB クライアント ライブラリの URL セーフキーを使用できます。 警告: URL セーフキーは暗号化されていません。種類や ID に機密データが含まれている場合、URL セーフキーから簡単に解読できるので注意してください。

これを読む限りだと,URLセーフキーを解読(base64デコード)すれば「エンティティの種類」と「id」を取得することができるようなので,挑戦してみる.

url_safe_key = 'ahFkZXZ-c3Znc2NyZWVuc2hvdHIXCxIKU2NyZWVuc2hvdBiAgICAgIDACQw'
decoded_key  = base64.urlsafe_b64decode(url_safe_key + ('=' * (4 - len(url_safe_key) % 4)))
# -> 'j\x11dev~svgscreenshotr\x17\x0b\x12\nScreenshot\x18\x80\x80\x80\x80\x80\x80\xc0\t\x0c'

16進表記と制御文字の変換は,http://www.oocities.org/dtmcbride/tech/charsets/ascii.html を眺めるとわかる.

  • \x11: ^Q (Default UNIX START char)
  • dev~svgscreenshot: アプリ名(svgscreenshotのローカル開発環境)
  • \x17: ^W (End of transmission block)
  • \x0b\x12 ^K (Vertical tab) ^R
  • \n: 改行
  • Screenshot: エンティティの種類
  • \x18: ^X (Cancel)
  • \x80\x80\x80\x80\x80\x80\xc0: おそらくidを表している?(正解id = 5348024557502464 = 128 ^ 6 + 1216.128は \x80 の10進表記で,6は\x80の出現回数?.1216の出処が不明.\xc0未使用)
  • \t: タブ
  • \x0c: ^L (Form feed)

エンティティidがわからない...
このあたりの解読方法について,上手くいきそうな手法をご存知の方がいれば教えてください!!

Chrome拡張機能でカッコいいポップアップウィンドウを開く

ブラウザでポップアップ画面を開くには

window.open('https://www.google.co.jp', '', 'location=no, width=400, height=300');

と書く.引数は左から順に,開くページのURL, ウィンドウ名, オプション文字列となっている.第3引数を見ると分かるように,アドレスバーlocationは非表示にするように要求している.しかし,これをChromeで実行すると,

f:id:daiiz:20160825003113p:plain

のように必ずアドレスバー付きで表示される.何のサイトにアクセスしているかを分かりやすくする配慮だと思うが,ウェブアプリの一機能として開くサブウィンドウだった場合,格好悪い.

f:id:daiiz:20160309005235p:plain:h56

Chrome拡張機能*1のAPIには,chrome.windows.createというものがある.これはwindow.openと用途が似ていて拡張機能でサブウィンドウを開くために使ったりする.上のやつと同じことをするためには,

chrome.windows.create({
    url: 'https://www.google.co.jp', 
    type: 'popup',
    width: 400, 
    height: 300
});

こちらのAPIのオプションはJSONで,また,ウィンドウタイプtypeに "popup" を与える. これを実行すると,

f:id:daiiz:20160825004905p:plain

というふうになり,アドレスバーは表示されなかった.格好良い.

まとめ

  • Chromeでサブウィンドウ画面を開く場合は,嫌でもアドレスバーは付いてきて消すことはできない.ウェブサイトでサブウィンドウを開くときは,格好悪いけれど諦めるしか無い.
  • Chrome拡張機能でサブウィンドウを開くときは,window.openを使うよりもchrome.windows.createを使うと格好良くできる

できればウェブサイトでも格好良く開きたい.

*1:先日廃止が発表された「Chromeアプリ」とは別物!

Windows、Mac、LinuxでChromeアプリが廃止される! 開発者はどうすればよいか?

驚いた

どういうことか

Chrome OS以外のOSでは,Chromeアプリ(Chrome Packaged Apps と Chrome Hosted Apps)が2018年までに廃止されるそうです.

jp.techcrunch.com

Chromeアプリとは,Chromeのブックマークバーの左隅にある f:id:daiiz:20160821021247p:plain をクリックして開いた先のページ*1で一覧されているアプリたちのこと.Chromium Blog のポストによれば,これらのアプリは

  • 2016 末〜: Chrome OSユーザー以外の,Chromeアプリの新規出品は不可になる.すでに公開済みのものについては,どのOSからでも更新可能.
  • 2017 後半: Windows, Mac, Linux ユーザーに対してはChromeウェブストアでChromeアプリが表示されなくなる.拡張機能とテーマは引き続き提供される.
  • 2018 初め: Windows, Mac, Linux ユーザーはChromeアプリを使用できなくなる.

のようなスケジュールで段階的に廃止される.


f:id:daiiz:20160304231004p:plain:h34 f:id:daiiz:20160304231004p:plain:h34 f:id:daiiz:20160304231004p:plain:h34
ここからは開発者向け情報

どうするか

このニュースが発表されたChromium Blog の記事のタイトルが『From Chrome Apps to the Web』であった通り,ウェブアプリ化することが推奨されているように思える. 昔に比べて充実してきたブラウザAPI や Service worker,web push を使って Progressive Web Apps を作ろうよという感じで書かれていた.

Transitioning from Chrome apps on Windows, Mac, and Linux - Google Chrome

というページも用意されていて,Chromeアプリの移行について丁寧に書かれている.このページでは以下のような選択肢が挙げられている.

  • Build a web app

    • Progressive Web Apps を構築する方法.手元にあるChromeアプリをそのままウェブアプリに変換してくれるツール「Caterpillar」も提供されている.
    • 自分のChromeアプリをウェブアプリに置き換えるにあたってブラウザAPIでは不十分だと思う部分があれば,それを送信できるフォームも設けられている.
  • Build an extension-enhanced web page

    • Chrome拡張機能とウェブアプリの合わせ技で提供する方法.ブラウザAPIでは不十分な点を拡張機能APIで補う作戦.
    • これまで通り,拡張機能とウェブサイトを連携させるMessage Passingの技術が使える.
  • Build an extension

    • ChromeアプリをChrome拡張機能に変更する方法
    • Browser actionを使ってブラウザ上に小窓を表示させることができる.
  • Build a native app

    • PCにインストールするタイプのネイティブアプリを構築する方法.
    • Electoron」や「NW.js 」などのフレームワークを使えば,Chromeアプリと同じHTML+JavaScriptでデスクトップアプリを作れる.

ひとまずこんな感じ.

追記1

僕もいくつかChrome アプリを開発してウェブストアに公開しているので,これから徐々に移行作業をしていきます.以降の方針や,作業の様子も記事にする予定です.(2016/08/22 15:00)

追記2

Chrome ウェブストアのデベロッパーダッシュボード(自分が出品しているアプリを管理するための画面)に以下のようなメッセージが出ていることを確認しました.(2016/09/08 01:00) f:id:daiiz:20160908015342p:plain

2016 年 11 月以降、新しく公開するパッケージ化アプリやホスト型アプリの利用は Chrome OS のみに制限され、Windows、Mac、Linux のユーザーは利用できなくなります。既存のアプリは引き続きすべての主要なプラットフォームで利用できます。- 詳細

*1:chrome://apps/

GAEウェブアプリでのユーザーログイン維持期間を伸ばす

Google App Engine(GAE)で動かしているウェブアプリは,ユーザーさんにGoogleアカウントでログインしてもらうことができる.

こうすることで,新たに情報を要求してユーザー登録してもらう必要が無く,ユーザー側もGoogle DriveやGmailを使うためにログインしているような感覚でログインできるので安心感がある.

ログインすると,各種セッション情報を保つためにブラウザにCookieが保存される.GAEではデフォルトの有効維持期間が1日になっているらしく,頻繁にログインし直す必要があって不便だった.

実は,GAEのデベロッパーコンソールで設定できた.
App Engine > 設定 > アプリケーション設定 の「GoogleログインCookieの有効期限」を編集すれば良い.

f:id:daiiz:20160817003125p:plain:w500
  • 1日(デフォルト)
  • 1週間
  • 2週間

のなかから選べる.