画像入りzipを人物認識してエクセルに変換する「マイクロサービス」を作った

概要

例の建築家の同期が、動画に映る人の位置を1秒ごとに目視で認識するという虚無作業をしていたので、自動化するWebアプリ的なものを作りました。

github.com

使い方

まず、人物認識したい動画をお好みの間隔(1秒毎とか)で画像に切り出し、適当なフォルダにいれてzip圧縮します。 そして、今回作ったWebアプリを開きます。以下はWebアプリのスクリーンショットです。

f:id:threetea0407:20200524011510p:plain

GCPのアクセストークンを頑張って取得して、「アクセストークン」という入力欄にコピペします。そして、先程のzipファイルを選択します。最後に「変換」ボタンを押すと、以下のようなエクセルファイルがダウンロードされます。

f:id:threetea0407:20200524010137p:plain

左端の列が画像ファイルの名前を表し、その隣の2列は人物を囲う長方形の左上の点の座標を表し、その隣の2列は右下の点の座標を表しています。これによって、画像の中のどの位置に人間が写っているのかを知ることができます。

イメージとしては、以下のような写真を入れると2つの赤丸の座標が取れるという感じです。

f:id:threetea0407:20200524012615p:plain

どうやって実現しているのか

Cloud Vision APIというのが世の中にはあって、物体認識をしてくれます。

cloud.google.com

今回作ったWebアプリがやってるのは、

  • zipを解凍して画像を取り出し
  • 画像をJSONに埋め込んでAPIを叩き
  • 返ってきたデータを若干加工してCSVにする

という処理だけです。つまり、機械学習的なものを自分で実装したわけではありません。

工夫した点

動画から画像への変換をやらない

もともと同期の人がやりたかったことは、「ある街に定点カメラをおいて、その動画の中で人がどう動いているかを調べる」ということでした。

今回は、動画から画像への変換はユーザーにやってもらい、Webサービスは画像入りzipから人物を認識するだけ、というインターフェースを採用しました。

f:id:threetea0407:20200524021034p:plain
ユーザーが動画から画像を切り出す必要がある

しかしこれを例えば、ユーザーに動画をアップロードさせて、それをWebアプリ側で画像に変換し、その画像に対して人物認識をする、というようなインターフェースにすることも可能です。

f:id:threetea0407:20200524021105p:plain
動画を直接変換できる

後者のほうがユーザーの仕事が減り、一見すると便利なように見えます。しかし、このインターフェースには問題があります。

例えば、今までは1秒ごとに画像を切り出していたけど、5秒ごとに切り出すようにしたい、というような要望が出てくることは容易に想像されます。そのたびにソースコードに手を入れてパラメータを修正するのは不毛です。そのようなパラメータをWebアプリ上で設定可能にすることも考えられますが、その場合は設定項目が無数に増えていくことになるでしょう*1

また、一般にこの手の物体検出は、画像の中における物体の相対的な大きさによって、検出精度が大きく変わるようです*2

f:id:threetea0407:20200524114452p:plain
右奥に写った小さな人物は認識されない

f:id:threetea0407:20200524114522p:plain
右奥を拡大すると認識されるようになる

Jörg MöllerによるPixabayからの画像

そのため、「大きな画像に沢山の人が小さく写っている」というような画像を対象にするときは、画像を適当なサイズに分割して検出にかけた上で、その結果を統合する処理が必要になります。その場合、どのようなアルゴリズムで画像を分割するか、またどのようなアルゴリズムで結果を統合するか、ということを考える必要があります。

このように「動画を画像に変換する」という領域は、要求が変化しやすい上に、真面目に考えるとかなり複雑です。

こうしたことを考え、動画から画像への変換方法に自由度を持たせ、かつアプリケーションの複雑度を下げるため、現行のような「画像をzipで固めてアップロードする」というインターフェースを採用しました。これによって、「動画を画像に変換する」という処理はこのソフトウェアの対象領域外となり、実装も使い方も非常に簡単になりました。

また、「動画を1秒ごとに画像に切り出す」というような処理は、それ用のソフトウェアがすでに存在します。そのため、このようなインターフェースを採用しても、ユーザーの手間はそれほど変わりません。

Excel形式で出す

Excelで開けるというのはかなり重要です*3。というのも、同期の人が使っているビジュアルプログラミング環境であるGrasshopperに、「Excelからコピペでデータを貼り付けられるブロック」というのがあるからです*4。これにより、認識した人物の座標をGrasshopper上で処理することができます。

また、Excelで開ける形式であるため、表計算によるデータの加工が可能です。例えば、画像ファイルに含まれる連番の番号を文字列処理で取り出して、「この人物は動画の何秒目に写っていたのか」というデータを取り出すことができます。プログラミングに不慣れな人にとっては、表計算のほうが敷居が低いため、これも便利な点です。

同期の人がGrasshopper上で処理した例が以下です。これは、街並みの3Dモデル上に動画上の人物の位置を投影し、三次元空間上での人間の動きを分析したものです。

GCPのアクセストークンを入力させる

これはCloud Vision APIの利用料金を、利用者である同期に支払わせるための仕組みです。Cloud Vision APIは非常に廉価に利用できますが、それでも60分の動画を1秒ごとに切り出した画像を全部物体検出する、みたいな処理をすると、無視できない料金が発生します。その料金を僕が負担するのは筋違いなので、何らかの手段で同期の人に料金を支払わせる必要があります。定期的に現金ないしはLINE Payとかでお金をやり取りしても良いですが煩雑です。

そこで考え出したのが「アクセストークンを入力させる」というやり方で、これによってAPIの利用料金は同期の人のクレジットカードに直接請求されるため、私とのお金のやり取りが発生せずに済みます。なお、アクセストークンはサーバー上で保存しておらず、変換処理を行うたびに入力する必要があります*5

技術的に特筆すべきこと

Steepを使ってみた

Rubyに型をつける技術というのが最近できつつあるのですが、それを使ってみました。

github.com

上のレポジトリから例をコピーしてきますが、以下のような型定義ファイルをRubyスクリプトとは別に書いて、テストみたいな感じで型チェックを回すという感じになります。

class Person
  @name: String
  @contacts: Array[Email | Phone]

  def initialize: (name: String) -> untyped
  def name: -> String
  def contacts: -> Array[Email | Phone]
  def guess_country: -> (String | nil)
end

今回書いた型定義ファイルは以下のような感じです(抜粋)。

interface _BoxLocalizer
  def localize: (images: ::Array[Boxboxbox::BinaryImage]) -> ::Array[Boxboxbox::Box]
end

class Boxboxbox::BoxLocalizer::GoogleVisionApiOnline
  @access_token: String
  @max_results: Integer
  @min_percentage: Float
  @max_retry: Integer
  @logger: Logger

  def initialize: (access_token: String, max_results: Integer, min_percentage: Float, max_retry: Integer, ?logger: Logger) -> untyped

  def localize: (images: ::Array[Boxboxbox::BinaryImage]) -> ::Array[Boxboxbox::Box]

  (略)
end

_BoxLocalizer というインターフェースがあって、その実装として Boxboxbox::BoxLocalizer::GoogleVisionApiOnline というのがある、という気持ちです*6。ここでI/Fを切ったのは、Cloud Vision APIを叩くときに叩き方がいくつかあるし、他の物体検出系のサービスもあるので、そこを差し替えられるようにしたかったからです。

github.com

Steepを使ってみた感想は以下です

  • クラスの階層を全て明示した形で書かなきゃいけないので若干見づらい(型定義のネストができない)
  • private methodも型定義書かなきゃいけないのでつらい
  • 標準ライブラリも型定義が存在しないものがあり、自分で書く必要があったりした
  • 型を書いてるとき、Rubyスクリプトファイルと独立した全然関係ないファイルを書くことになるので、地味に虚無な気持ちになる(慣れの問題かも)
  • Rubyのダックタイピングをかなりいい感じに型に落とし込めてるという印象
  • 型がないライブラリを使うときに、自分で適当にシュッと型をつけてしまえるのは結構体験が良かった
  • 型がないライブラリの実装から型定義ファイルを自動生成するやつというのがあって、大体のケースはそれを使うとなんとなく型がついてくれるんだけど、たまになんか無理なやつがあってつらい (concurrent-rubyというgemは無理だった)

Steepを使う際にはruby-jpの#typeチャンネルの皆さんに大変お世話になりました。ありがとうございました。

今後の課題的な

同期の人の要件を満たすソフトウェアは作れたので一旦は満足なんですが、一般に公開したら誰かしら使いたがる人はいる気がしていて、公開したいという気持ちがあります。ただ、いろいろ気になるところがあって公開を見送っています。

例えば現状の実装だと、アップロードされてきたzipファイルはそのまま/tmpディレクトリ以下に書き込まれるようになっており、大容量のファイルが送られてきたり、利用者が増えてきたりすると問題になりそうです。/tmpディレクトリに置くのをやめてS3に上げるのも考えられるんですが、そうなるとファイルサイズに対して従量で料金がかかってくるわけで、テロされてAWS破産みたいな話もあり得るわけです。

あとは、GCPのアクセストークンをサーバーに送信させてるわけですが、これって大丈夫なんだっけみたいな話はあって、僕個人との信頼関係がない人が、ここにアクセストークンを書き込みたいとは思わないでしょう。そもそも、Webでこれをやる必要は一切なくて、Windowsのデスクトップアプリとして公開すれば良いはずです。今回サーバーサイドアプリケーションとして実装したのは、単純に僕にデスクトップアプリを作る技術がないからです。

ただ、デスクトップアプリとして公開する場合も、GCPのアクセストークンの取得手続きは利用者に踏んでもらう必要があり、ユーザーに一定程度難易度の高いタスクを要求することになります。そう考えると、サーバーでその処理をまるっと隠蔽してあげるというのはありうる選択肢で、ユーザーに対して適切に料金を課す手段が見つかってないことが問題なのだと考えることもできます。

というわけで、今後あり得る展開としては

  1. フリーのデスクトップアプリにして一般公開する(アクセストークンは自分で取得させる)
  2. ユーザーに料金を課すスキームを確立し、一般公開する(アクセストークンの取得をさせない)
  3. 需要がないためこれ以上何もしない。現実は非情である。

という感じです。雑にユーザーに課金させるシステムをどなたかご存知でしたら教えてください。よろしくおねがいします。

*1:しかもそれらの設定項目のうち実際に使われるのは極少数であると予想します

*2:この場合、画質の粗さは問題とならず、純粋に画像全体のなかで物体が相対的にどのぐらいの大きさで写っているのかというのが重要っぽい

*3:Excelではなく正確にはCSVファイルを出力している

*4:一般的に、Excelからコピペでデータを貼り付けられるソフトウェアは結構多い

*5:実際にはブラウザの機能で自動入力されるようです

*6:duck typingなので、明示的にinterfaceを実装しなくても、interfaceを満たしていれば実装したことになる