粗大メモ置き場

個人用,たまーに来訪者を意識する雑記メモ

Python Gmail API を用いてメールの締切を自動でリストアップする①

あらまし

最近謎にものづくりのモチベがあります。昨日メール見ていて締切多すぎぃ!ってなったのでメールから自動で締切をリストアップしてくれる手法がなんかないかなと思ってGmail APIのドキュメントとにらめっこしてきました。

目指したい理想像

  • Gmail に届いているメールから「締切」に関係するワードを抽出
  • 同メールから「日付」情報を抽出

Gmail APIを用いたメール抽出

まずはQuickStartのガイドから入っていきます。

developers.google.com

ここではまず,Googleの情報にアクセスする認証キー的なものをダウンロードできます。 キーの仕様についてはこっちを参照すると良いでしょう。

APIの立ち上げ

上の手順でquickstart.pyというサンプルコードを実行すると思います。 実質的なサービスの立ち上げは,以下の箇所にあるようです。

service = build('gmail', 'v1', credentials=creds)

ここでcredentialsというのが初回の登録でDLしたcredential.jsonというAPIの認証キーです。 build関数の仕様に依るとgmailのサービスを'v1'のバージョンで起動するということみたいですね。

Queryとマッチングするメッセージを検索

メールの検索ですがAPIがきちんと準備されています。

developers.google.com

必要なものはだいたい以下の通りです。

  • 先程立ち上げたservice
  • ユーザID :メールアドレス or 'me'(自分自身のこと)
  • 検索クエリ:検索文字列。ここでは「締切」などとする
  • maxResults:検索上限。時間を気にするなら入れておくといい。

messagesに帰ってくるのはメール情報のidの配列です。

response = service.users().messages().list(userId=user_id,
                                           q=query,maxResults=maxlistnum).execute()
messages = []
if 'messages' in response:
  messages.extend(response['messages'])

サンプルコードでは nextPageTokenなるものを使ってますが,これは構造体や配列を辿る時の現在地を表すポインタ的なやつです。

idからメッセージの本文などを抽出

先程の検索ではメッセージのidを入手したのでget関数を用いてメッセージの中身を取得します。

developers.google.com

ここのサンプルコードの困った点はマルチバイト文字が含まれているときにエンコーディングの問題が発生するということです。

APIの記述的にはMIMEという形式の方を推していそうですが,メールに依って文字化けをしたりしなかったりして半日では解決できなかったので今回は採用を見送りました。

それで代わりに書いた関数がこれ。(クラスの形式になってるけど直すの面倒なので各自解釈してください)

    def GetMessageSubjectAndBody(self, msg_id):
        """Get a Message and use it to create a MIME Message.

        Args:

            msg_id: The ID of the Message required.

        Returns:
            A MIME Message, consisting of data from Message.
        """
        try:
            message = self.service.users().messages().get(userId=self.user_id, id=msg_id,
                                                    ).execute()
            body = ""
            subject = ""

            if message['payload']['body']['size']:
                msg_str = base64.urlsafe_b64decode(message['payload']['body']['data'])
                body = msg_str.decode('utf-8')

            if 'headers' in message['payload']:
                for header in message['payload']['headers']:
                    if header['name'] == 'Subject':
                        subject = header['value']

            return subject, body
        except errors.HttpError as error:
            print('An error occurred: %s' % error)

基本的にはmessage['payload']['body']['data']の中に本文がありますが,稀にこのデータが空の場合があります。今の所そういうのは真面目なメールではなかったので例外処理をして回避しています。

また,同様にmessage['payload']['header']の中に件名を保管している箇所があるのでそこも掘り起こしています。気になる方は一度Jupyterなどで中身を見てみると良いでしょう。

提案アルゴリズム

上記の手段でメールの本文を検索して見ることができるようになりました。 これをどのように使っていくかについて書いていきます。

締切に関する語句の検索(メール検索時)

締切と言っても様々な表記があります。その全てを検索クエリに含める必要があるのですが,その時の書き方は以下のようにORでつなぐことで実現します。

"締め切り OR 〆切 OR 締切 OR 締切り OR deadline OR DEADLINE"

もちろん,普段gmailの検索に用いるクエリも使用可能で,ここにあるように, 重要なメールであれば ”AND is:important”とかつけて検索することもできます。

締切の日時の検索(メール内検索)

メール本文を抽出した後,締切に関する日時を検索します。日時というのは意外とメールの中で自然に使われているので"締切"などのワードの近くを検索したいです。

従って,

  • 締切に関連する語句の付近の文章抽出(行単位or文字数で)
  • 抽出した文章から日付の抽出

という手順を取りました。

日付の抽出には非常によく整備されたツールがあるのですが,残念ながら日本語対応しておらず,2バイト文字に対して誤作動的なのを起こすので今回は採用をやめました。 もし使うなら2バイト文字があるかどうか判別してから用いればよいかと思います。海外の方と多く連絡するのであれば導入も考えます。(今のところはそんなにない) github.com

代わりに以下の正規表現を用意しreモジュールを用いて検索をかけています。(正規表現初めて使ったので深く理解していない)

['(\d{1,2})/(\d{1,2})', '(\d{1,2})月(\d{1,2})日']

この表現を用いることで「3/4」や「7月4日」といった表現をキャッチできます。半角全角にかかわらずキャッチできるので偉い。

まとめてデータ化

以上の手順で,締切に関する語句を含んだメールから締切の日時と件名を抽出できました。

これをまとめてデータ化してGoogleカレンダーにぶち込むなどしたいですが,ちょっと息切れしたので辞書型に詰めてひとまず完成とします。

ちょっと見せるとしうかつだらけですね。なおIDは加工しました。検索できないよ。 見学会系はちょっとたくさん締切を発行してきますね。他の用途にも使えそうなので今回遊んでみてまぁまぁよかったんじゃないかなと思ってます。

          {'deadlines': [[3, 18]],
           'message_id': '1697f3bcab',
           'subject': '【3月18日(月)午前9時締切】ソニー・インタラクティブエンタテインメント\u3000'
                      '技術系・事務系エントリーシート受付中'},
          {'deadlines': [[3, 8], [4, 3], [3, 8], [5, 13]],
           'message_id': '1695b47b1',
           'subject': '【日本精工(株)】ログインのお願い:【総合職】応募受付開始しました。'},
          {'deadlines': [[3, 1]],
           'message_id': '16933597b7',
           'subject': '【重要なお知らせ】3月1日0時より『まとめてプレエントリー』機能をご利用いただけます'},
          {'deadlines': [[3, 1]],
           'message_id': '16923774f',
           'subject': '【重要】2/27(水)~2/28(木)プレエントリー受付開始準備による一部機能停止のご案内'},
          {'deadlines': [[1, 31]],
           'message_id': '168944a8b6',
           'subject': ' 【特別号】有効期限が近づいているマイルやe JALポイントはございませんか? / 【先得】 '
                      'JMB会員先行予約サービス 受付間もなく開始!'},
          {'deadlines': [[1, 31]],
           'message_id': '16886a4406',
           'subject': '【UTAS】(周知)【1/31まで deadline Jan '
                      '31】情報セキュリティ教育について/e-learning(Information Security '
                      'Education)'},
          {'deadlines': [[1, 23]],
           'message_id': '1686095a6',
           'subject': '【ご好評につき2期募集開催!】マーケティング、商品企画などコースに特化した短期インターンプログラム"Business '
                      'Master Program"'},
          {'deadlines': [[1, 31]],
           'message_id': '1683af6025',
           'subject': '【2019年1月31日締切】 JAL旅行積立 ボーナスマイルキャンペーン実施中!'},
          {'deadlines': [[1, 8], [1, 8]],
           'message_id': '16827b89',
           'subject': '【締切1/8(火)10:00】(事務系・技術系)ソニー インターンシッププログラム エントリー受付中'},

今後やりたいこと(誰かにやってほしいこと)

今後の課題は,

  • メールのid,件名,抽出した締切 を含んだ適切なデータ構造
  • 件名や締切で検索をかけることができ,APIを叩いて本文全文を参照できるようにする
  • Google Calendarなどに見やすく一覧でマッピングする
  • 重要な締切かどうかまで判別してくれるとめちゃ助かる

といったところでしょうか。 コード公開したら誰か引き継いでくれたりしないだろうか。

Google CalendarとGoogle Keepを連携してPCで賢く予定管理したい

まえがき

古来より人間は道具を発明し,自らの能力の延長として使ってきました。 道具は能力を拡張するだけではなく補完的な意味合いも強くあり,例えば「眼鏡」があることにより近眼という致命的な能力欠如を持った個体でも社会においてそれほど不自由せずに生活を営むことができるようになりました。

眼鏡バンザイ。

f:id:ossyaritoori:20190319050922p:plain
我々の救世主

ということは現代の情報社会の技術を使えば 「私生活がちゃんとしていない系の人」も「健常な人々」と同等な生活を営めるはずなのです。

締切や予定の把握・管理が苦手でもなんかうまくやる方法あるでしょ!助けてGoogle先生!ということです。

f:id:ossyaritoori:20190319051117p:plain
我らが大先生(https://www.google.com/doodles/doraemon-2009?hl=ja より引用)

Google Keep と Google Calendar

今回の秘密道具はこちら。

特に説明することはないですがGoogle Calendarは是非スマホにアプリを入れて使ってください。他の人との予定共有とかめっちゃ捗ります。 というかGoogleホニャララは情報共有において最強と勝手に思っております。Google Photoをみんな使おう。

Google KeepのCalendarリマインダ機能

Google Keep は普通に使えばただのメモ帳です。 しかし,メモをリマインダとして登録すればGoogle Calendarにも表示させることが可能です。 Calendarからは時刻などの情報を編集可能なようです。

詳しくは以下のサイトをば。 loumo.jp

健常者の人々には当たり前かもしれませんが,予定などのちょっとしたメモ書きというのは一般的にすぐにどっかいきがちです。 PCとスマホを持っている場合はその都度適当なファイルに書いて後々ファイル検索をかけて探すなんてこともよくあるわけです。

そういう片付けできない系ダメな奴がダメな理由は行動に一貫性・ルールがないからです。 メモはGoogle Keepだけを使う。少なくとも何か情報を紛失するリスクは減ります。

追記スペース

実はこの記事書いている段階ではあんまり活用法を絞れていません。 思いつきで書いているので。いざ変革。

便利な連携・使い方等見つけたら追記します。

PC版をデスクトップアプリ風に使う

とまぁGoogle系のサービスは便利で重宝するのですが,PC版はいちいちサイトを開く必要があります。

私はタブを片付けられないのでどうしても予定などは別窓で見たいという欲求が有り,デスクトップアプリ作ってほしーなーとは思ってます。

1.ショートカットの作成

以下の記事の手順でデスクトップ上にショートカットを作成することができます。

loumo.jp

また,その後にタスクバー上で該当windowを右クリックし,「タスクバーにピン止め」をすることで速攻で起動できます。いいですね。

2.タスクバーのアイコンがChromeのものになる問題の解決

タスクバーにピンどめする時,タスクバーのアイコンがChromeのものになってしまうというバグ?を経験しました。

f:id:ossyaritoori:20190319053315p:plain
真ん中がGoogle Keep,右側がCalendar。わかんねぇよ

こういうときは一度タスクバーからピンどめを外して,ショートカットキーを右クリックして「タスクバーにピンどめ」を選択しなおします。

f:id:ossyaritoori:20190319054522p:plain
正しい姿。わかりやすい~。

ここまで来ることでカレンダーやメモをアプリ感覚で起動することができて非常に快適になります。

参考になりそうな文献達

拡張機能はなかなか便利そうです。いちいちブックマークするよりも探しやすいと思います。(検索できるので)

blog.thetheorier.com

tono-tax.com

ホモグラフィー行列を用いた鳥瞰変換(Bird's-eye view)

鳥瞰変換とは

一言で言うと,ある視点からとったカメラの画像を別の視点で撮ったように見せかける手法のことです。

他サイト様の説明がわかりやすいのでこちらに載せます。 daily-tech.hatenablog.com

より一般的には「斜めから撮った画像を真上から撮った画像のように写す」という場面で使われます。

http://cdn-ak.f.st-hatena.com/images/fotolife/r/rkoichi2001/20160526/20160526035336.png

引用:Tanaka, S., Yamada, K., Ito, T. and Ohkawa, T.: Vehicle Detection Based on Perspective Transformation Using Rear-View Camera, International Journal of Vehicular Technology, Volume 2011, Article ID 279739 (2011).

鳥瞰変換とホモグラフィー変換

カメラの撮像の式

グローバル座標系にて座標(X,Y,Z)にある点をカメラの位置①から撮像して画像上の点 (x_1,y_1)に写っているとします。 今,仮想的なカメラの回転R_{12}を施した系②でこれが (x_2,y_2) に移るとします。(この系②が上から見下ろす系になっていればいいわけですね。)

それぞれの場合でのカメラの撮像の式は以下のようになります。(Texで打ったので画像で失礼)

f:id:ossyaritoori:20190310153102p:plain
(R,t)は系①でのカメラ姿勢,太字のKはカメラ行列

モグラフィー変換

一方,画像変換で用いられるホモグラフィー変換はホモグラフィー行列Gを用いて次のような形で表せます。

f:id:ossyaritoori:20190310153504p:plain
λはスケーリング定数。

カメラの回転とホモグラフィー変換の関係性

上のニ式を見比べてみると,回転行列R_{12}Gとの間には以下のような直接対応関係があることがわかります。 当然ですが,系①と②の間に純粋な回転以外の並進成分などがあった場合はこんな簡単な関係にはなりません。

f:id:ossyaritoori:20190310153948p:plain
カメラ回転 = ホモグラフィー行列に変換可能!

この関係を用いることでカメラを擬似的に真下向きに回したときの画像を生成することができるというわけです。

おまけ:手ぶれ補正

先程の式は裏を返せば,微小な並進成分を無視すれば画像内のホモグラフィー変換からカメラの回転を補正することができるということを表しています。 この手法を用いて手ぶれ補正をしたのが以下の記事だったりするわけです。

ossyaritoori.hatenablog.com

実践編

例のごとくOpenCVを使います。

下準備

先程の式を用いて鳥瞰変換を行う際には以下のパラメータが必要になります。

  • カメラ行列K
  • 系①におけるカメラ姿勢(R,t)(→ 実はRだけでいい)

これは一般にキャリブレーションと言われる作業です。

Ubuntu-ROS環境が整っている方にとっては造作もないと思いますが,Windowsなどでやる場合は下記のサイト等でやるのが良いかと思います。 ossyaritoori.hatenablog.com

また,カメラ姿勢(R,t)を測るのは結構大変で,ARマーカ等を用いるのが最も楽かなぁという気がします。 プログラムは適当に探してもらうとして,この辺りの記事なんかが説明が簡素でわかりやすいかも?

プログラムの流れ

カメラの姿勢Rの元となるrvecから座標系①における回転{}^gR_1を計算します。 ossyaritoori.hatenablog.com

また,変換したい座標系②での回転{}^gR_2cv2.Rodriguesから生成しておきます。

この時,先程のR_{12}={}^gR_2{}^gR_1^\topと計算できます。 その後,Kをかけて作ったホモグラフィー行列を元にcv2.warpPerspectiveに投げます。以上です。

略記プログラム

コードメモですがこのままでは動きませんよ~

# calculate bird view homography
import math
theta_below = np.array([0, math.pi, 0])
Rc_, _ =  cv2.Rodrigues(theta_below)#変換先の回転
Rc,_ = cv2.Rodrigues(vm.rvecs)#オリジナル関数有


Homo = np.linalg.multi_dot([K,Rc_.T,Rc.T,np.linalg.inv(K)])#ホモグラフィー行列

nHomo = Homo/Homo[2,2]# 正規化


minx = -20000
miny = 0

# 移動量を調整しないと画面外に出ていってしまう…
nHomo[0,2] -= minx 
nHomo[1,2] -= miny 

# 2000pix四方の画面を用意しないとどっか行っちゃうので…
out=cv2.warpPerspective(img,nHomo,(2000,2000))

f:id:ossyaritoori:20190310160628p:plain
元の画像

f:id:ossyaritoori:20190310160653p:plain
変換後の画像(Z軸も回すべきかも)

OpenCVで使われる座標系の作法メモ

OpenCVの座標系

座標系というやつはどう定義されているかが非常に重要です。 従って参照にすべきはQiitaでもなく,このブログでもなく公式APIリファレンスです。

これに書いてあることが全てです。でもなかなか探しづらいのでここにメモを書きます。

前提of前提

超前提知識ですが,OpenCVでは以下の前提があります。

  • 右手系
  • カメラの座標系において,左上が原点,横方向がX(右が正),画像の縦方向がY(下が正),奥行き方向がZ(奥が正)

OpenCVで使われる回転の作法

回転の記述法でメジャーどころと言えば以下の3つが挙げられます。

OpenCVではちょくちょく回転を表す3×1のベクトルを算出しますが,これは全てロドリゲスの回転公式に基づいています。

公式のcv2.Rodrigues()のレファレンスを覗くと

f:id:ossyaritoori:20190302003143p:plain

とあります。

日本語でいうと,「ベクトルのノルムが回転角,正規化されたベクトルが回転軸」を表します。

X軸[1,0,0]まわりに45度回転させたい場合は以下のように書けばいいわけです。

import cv2
import numy as np

R, Jacob = cv2.Rodrigues(np.array([math.pi/4,0,0]))
print(R)
#R = array([[ 1.        ,  0.        ,  0.        ],
#        [ 0.        ,  0.70710678, -0.70710678],
#        [ 0.        ,  0.70710678,  0.70710678]])

なお,cv2.Rodriguesは返り値を2つ返すので注意。

従って以下のようなオイラー角のDCM変換を手で書いている人はOpenCV上では使うのをやめた方がいいです。 ぱっと書きやすいので私は好きなんですが…

# Calculates Rotation Matrix given euler angles.
def eulerAnglesToRotationMatrix(theta) :
     
    R_x = np.array([[1,         0,                  0                   ],
                    [0,         math.cos(theta[0]), -math.sin(theta[0]) ],
                    [0,         math.sin(theta[0]), math.cos(theta[0])  ]
                    ])
         
         
                     
    R_y = np.array([[math.cos(theta[1]),    0,      math.sin(theta[1])  ],
                    [0,                     1,      0                   ],
                    [-math.sin(theta[1]),   0,      math.cos(theta[1])  ]
                    ])
                 
    R_z = np.array([[math.cos(theta[2]),    -math.sin(theta[2]),    0],
                    [math.sin(theta[2]),    math.cos(theta[2]),     0],
                    [0,                     0,                      1]
                    ])
                     
                     
    R = np.dot(R_z, np.dot( R_y, R_x ))
 
    return R

# Checks if a matrix is a valid rotation matrix.
def isRotationMatrix(R) :
    Rt = np.transpose(R)
    shouldBeIdentity = np.dot(Rt, R)
    I = np.identity(3, dtype = R.dtype)
    n = np.linalg.norm(I - shouldBeIdentity)
    return n < 1e-6
 
 
# Calculates rotation matrix to euler angles
# The result is the same as MATLAB except the order
# of the euler angles ( x and z are swapped ).
def rotationMatrixToEulerAngles(R) :
 
    assert(isRotationMatrix(R))
     
    sy = math.sqrt(R[0,0] * R[0,0] +  R[1,0] * R[1,0])
     
    singular = sy < 1e-6
 
    if  not singular :
        x = math.atan2(R[2,1] , R[2,2])
        y = math.atan2(-R[2,0], sy)
        z = math.atan2(R[1,0], R[0,0])
    else :
        x = math.atan2(-R[1,2], R[1,1])
        y = math.atan2(-R[2,0], sy)
        z = 0
 
    return np.array([x, y, z])

OpenCVでの座標変換計算の数式

前節でOpenCVがどのような形で座標を扱うことを想定しているかがわかりました。次はそれがどのように使われることを想定されているかです。

例えばOpenCVではカメラの位置を図るような関数がいくつか存在します。既知の3次元点と投影された画像の点との対応からカメラの姿勢を計算するPNP問題を解く場合が特にそうです。

心当たりがあるのは

  • SolvePNP系統
  • arucoモジュールのEsitmatePose系

でしょうか。

rvecやtvecの扱い

これらでは得られたカメラの位置として返り値にrvecsとtvecsという3×1のベクトルを返してきます。 気になるのはこれらは果たしてどの座標系に属しているのかということです。(都合等をよく考えたらカメラ座標系にきまっているんですが)

以前の記事でもカメラ変位をカメラtoグローバルなのかグローバルtoカメラなのかで扱いが変わることを書いたと思います。 ossyaritoori.hatenablog.com

結論から言うとrvecsやtvecsはカメラのLocal座標系から見た値で表されています。 従って,

OpenCVで測ったカメラ姿勢R_c=Rodrigues(rvec_c)^\topから測った回転物の姿勢R_o=Rodrigues(rvec_o)^\topはグローバル座標系では


^g R_{o} = R_c R_o

と計算できるわけです。

検証用投影プログラム

自分はちょっと疑い深いので実際に計算して確かめました。

三次元の点(グローバル座標)をカメラの視点に投影するcv2.projectPoints()という関数があります。 imgpts, jac = cv2.projectPoints(3Dpoints, rvecs, tvecs, K, dist)のように使えば3次元点を2次元に投影することができます。

f:id:ossyaritoori:20190302005627p:plain
親の顔よりみた式

この中身をDistortionを含めないで書くならこういう風になります。

tc=tvecs
Rc,_=cv2.Rodrigues(rvecs)
def camProjection(points,Rc,tc,K=np.eye(3),campose_is_global=0):
    '''
    Input: CameraPose, Camera Matrix, Object Points in Global Coordinate
    Output: Projected Points
    '''
    h,w = points.shape
    if h != 3:
        print("Input do not match points style!")
    if campose_is_global:#assume Rc,tc is from global frame
        Proj = K.dot(np.concatenate([Rc.T, -np.dot(Rc.T,tc)], axis=1))
    else:#assume Rc,tc is from camera frame( opencv default)
        Proj = K.dot(np.concatenate([Rc, tc], axis=1))        
    pts = np.vstack((points,np.ones((1,w))))
    converted_pts= Proj.dot(pts)
    return Projection(converted_pts)

def Projection(points):
    h,w = points.shape
    if h != 3:
        print("Input do not match points style!")
    Dep = points[2,:]
    Depth = np.tile(Dep,(2,1))
    return points[0:2,:]/Depth

これを使った検証の結果,やはりOpenCVではrvecsやtvecsはカメラのLocal座標系から見たものに適切に変換して使う必要があります。

3D座標変換の勘所メモ② - 異なる座標内での回転の変換 -

メモ第二弾です。

ossyaritoori.hatenablog.com

問題設定

  • ローカル座標系Aとローカル座標系Bがあり,それぞれグローバル座標系に対して R_A, R_Bの回転を持つとする。
  • グローバル座標にある点P をローカル座標系A内で回転 R_{PA}を適用する。
  • この時の点Pの回転をローカル座標系Bから見た回転[tex:R{PB}]とみなした時の[tex: R{PA},R_{PB}]の関係性は?

この問題はつまり,Aの座標系で動いた物体がBの座標系でどのように見えるかを表している。よくある問題です。

導出

まず点Pのグローバル座標ベクトルをp_Gとした時,系AとBから見た点Pのベクトルはそれぞれ

\begin{align} p_A &= R_A^\top p_G\\ p_B &= R_B^\top p_G \end{align}

です。ここで系Aのローカル座標内での回転 R_{PA}を適用した時のベクトルは


R_{PA} p_A

となる。さらにこれを一旦グローバル座標にうつしその後座標Bに移した場合のベクトルは以下のようになる。


R_B R_A^\top R_{PA} p_A

さて,これは座標系Bで見た移動後の点P'であるが最初の式を代入することでp_Gで表せる。


R_B^\top R_A R_{PA} R_A^\top p_G

これが点Pp_Bを座標系BにてR_{PB}回転させた


R_{PB} p_B = R_{PB} R_B^\top p_G

に等しくなることから,答えが導ける。

結論

R_{PA}R_{PB}の関係は,


R_{PB} = R_B^\top R_A R_{PA} R_A^\top R_B

逆の関係も


R_{PA} = R_A^\top R_B R_{PB} R_B^\top R_A

となります。Aが元からGlobal座標系だった場合は,


R_{PB} = R_B^\top R_{G}  R_B

という式になります。

2次元回転との違い

回転まわりは2次元の図を書くとわかりやすいことが多々ありますが,今回のケースは2次元で考えると逆に混乱します。

2次元では回転の計算は可換(順番を変えても結果が同じ)であるため上記の式は2次元の場合


R_{PB} =  R_{PA}

と同義であるからです。3次元以上での回転は非可換であるため上のようなややこしい式が登場するわけです。

参考URL

参考にした質疑

3D座標変換の勘所メモ①

タイトルが非常に付けづらかったので埋もれないようにキーワードだけ並べました。

問題設定

2つの座標系があった時,1つの座標系から見て位置^1t_{1p}にある点Pがもう片方の座標系からはどういう値に見えるかという問題を考えることがままあります。 例えばカメラの撮像系等でこのような話はよくありますね。

図で示すと以下のような感じです。

f:id:ossyaritoori:20190221171254p:plain
手書きですまない

毎回こんな図を書いて確認してしまうので記事として書いて答え合わせにしようと思います。

前提:ベクトルの記法について

どの座標系からみたベクトルかを左に添え字で書いています。そこそこPopularな記法だと思います。

例えば^2t_{1p}はベクトルt_{1p}を座標系2の上で解釈したものということにします。

また,^1_2 R^1_2 tはそれぞれ座標系1から2への回転と並進の変位を表しています。教科書では1,2等の数字ではなく記号で書かれることが多いですね。

導出

まず座標系1からみた「座標系2to点P」のベクトル^1t_{2p}

^1t_{2p}=^1t_{1p}-^1_2 t

のようになります。 このベクトルを座標系2から見た時は,回転量^1_2 Rの分だけ逆に回せばいいので(2次元座標で考えてみればすぐわかる)

\begin{align} ^2 t_{2p} = ^1_2 R^\top {}^1 t_{2p} \end{align}

これを先程の式と合わせると

\begin{align} ^2 t_{2p} = ^1_2 R^\top {}^1 t_{1p} - ^1_2 R^\top {}^1_2 t \end{align}

となります。

答え

結局これを以下のような同次表現に落としたとき,

\begin{align} ^2 t_{2p} = [R | t] \begin{bmatrix} {}^1 t_{1p} \\ 1 \end{bmatrix} \end{align}

R,tと「座標系1と2の関係を表すR,t」との関係は \begin{align} R = ^1_2 R^\top \\ t = - ^1_2 R^\top {}^1_2 t \end{align} という風になるわけです。この関係式はちょうど逆もしかりになります(当たり前)。

画像系でよくこのRとtを求めることがありますがGlobal座標でのカメラの位置に変換するにはこのような手順を踏む必要があるのを忘れないように。

余談:はてなブログTex記法を書く方法

以下の呪文を冒頭で唱えた後に,

<p style="display: none;">[tex: ]</p>

\begin{align}で囲むなどすれば通常とほぼ同様に書くことが出来ます。ただし,^や_などの文字には\でエスケープが必要になるらしく,特にt_{1p}などの複数の文字を配置したい場合にはt\_{1p}のように書いたほうが安定するようです。

[参考サイト]

はてなブログで数式が綺麗に書けるTeXの便利ワザ for 見たままモード - cBlog

追記: 最近同じことを書いているサイトを見つけました。No more 車輪の再発明。金言ですね。私はやってしまったわけですがw mem-archive.com

Travis CIを用いたPythonパッケージのテスト管理手順

本来は公式のチュートリアルに従うのが良いと思いますが,既存のコードにとりあえずTravisのマークを載せたいという方には参考になるかと思います。

Travis CIで何ができるのか

端的にはGithub等でコードを管理する際に自動でテストを走らせる事ができるツールです。

↓よく見るこれです。

Build Status

公式の説明はこちら。

Core Concepts for Beginners - Travis CI

自分の行ったプログラムの変更がシステムに影響を及ぼさないか自動でチェックしてくれます。

今の所,チェックなんてローカルでやるし具体的な共有相手みたいなのも想定していないのでせいぜい環境を移行した時にどう動くかの簡易チェック的なものでしか運用しない予定です。 でもOSSの人たちはみなやっているようなので今はフリだけでもできるようにしておこうというやつです。

本記事では最小限Travis CIでbuildを通すやり方のみ記載します。 賢明な皆さんは是非きちんとしたサイトや資料を元に一から勉強して私に教えてください。

下準備:TravisCIへの登録とリポジトリアクティベーション

Githubのアカウントを持った状態で以下のTravis.comのページに飛びます。

travis-ci.com

ここからGithubアカウントとの連携をぽちぽちと進めて登録は終了です。

その後右上のメニューからSettingを選んで以下の画像のように適用するリポジトリを選びます。

f:id:ossyaritoori:20190208143844p:plain
Travis CIのSetting画面

これで下準備は完了です。

.travis.ymlの作成

.travis.ymlというファイルを作成することで自動テストを実行することが出来ます。

例えばPython3.5のパッケージで何某かを実行させたい場合は以下のように書きます。

language: python
python:
  - "3.5"

install:
  - python setup.py install
  #- pip install yaml
    
script:
  - python test.py
  • 最初の行,language:pythonとあるのはどの言語でテストするかを示します。
  • python: 以下ではテストするバージョンを選択できます。
  • install:以降はどのモジュールが必要になるか記述します。setup.pyがある場合はそれを元にインストールすればいいですが,普通にpipから始まるコマンドも書けます。
  • script:以降では実際実行するコマンド群を表しています。この結果帰ってくる値が0であればテスト成功と判定されます。普通にディレクトリ移動などのシェル操作も出来ます。

結局やってることはリモートで仮想環境を立てて,該当する言語の環境を用意し,テストを実行・判定するという流れのようです。 先程のWebページからどのように処理が進んでいるか確認できます。

f:id:ossyaritoori:20190208190539p:plain

こんな感じ。

ハマったこと

いろいろくだらないミス等をたくさんしましたが,一番時間を取られたのは,

  • GUIを表示するタイプ(cv2.imshowとか)のプログラムはテストを通すのが難しい

ということです。( X server うんたらとかいうErrorが出る)

dvfbというのを使ってGUIの設定などできるようですが,今の理解度ではGUIの有無をSwitchできるように調整する方が早かったです。

テストが通ったら

以下の画面の箇所をクリックするとこの画像のURLを入手できます。

f:id:ossyaritoori:20190208191607p:plain

これをマークダウンでREADMEに貼り付ければ自分のパッケージに反映されます

余談:リポジトリの構造について

継続的インテグレーションとありますが結局ちゃんとしたリポジトリの構造から用意するのが良いです。

リポジトリの構造については様々な意見を聞きますが以下の記事の通りにするのが妥当そうです。

qiita.com

しかし開発とかいろいろやってると変なフォルダたくさん作っちゃったりしてややこしくなるんですよね。 意図的に機能ごとにブランチを切る開発スタイルに自然と移行したいものです。

setup.pyについて

さっきの記事にもありましたが,setup.pyを置いておくといろいろ捗る傾向にあります。

qiita.com

説明は今回は省くとしてこんな感じに書いたりします。(ちょっと元ファイルから変えてる)

from setuptools import setup, find_packages
import sys

sys.path.append('./sample')

setup(
    name = 'HOGE',
    version = '0.1',
    description='This is test codes for travis ci',
    install_requires=['numpy','opencv-contrib-python==3.4.0.12','docopt','matplotlib'],
    packages = find_packages(exclude=('sample', 'markers','trial')),
    license = 'MIT',
    author='Ossyaritoori',
)

あとがき

この手のインテグレーションの話を見るとやはりはじめが肝心です。 きちんとした手法で最初から管理している場合はこんなことには悩みませんが,我流でいろいろコーディングしている所にこういうツールを導入するのは非常にコストが重いように思われます。

エンジニア個人としてはどこかのタイミングで導入に踏み切ってリポジトリ単位で慣れていくというのが良いように思いますが,組織としてこういうのを導入して管理するのは結構難しそうです。 大変なことをする割に単体では実務自体にはつながらないのが辛い所ですよね。 「N日人かけて自動でテストできるようにしました。」『それで次の機能はいつ作るの?』「…」

次のプロジェクトからはきちんと構造考えて作る…!