豪鬼メモ

一瞬千撃

写真カタログとWebサービス

人間の記憶というのは脆弱なもので、どんなに輝かしい思い出も時が経つと薄れていってしまう。一方でどうでもいいことを意外に覚えていたりするものだ。いつまでも覚えている記憶とそうでない記憶の違いは、定期的に思い出してその記憶を司る構造を上書きしていけるかどうかで決まるらしい。では、楽しかった思い出を定期的に思い出すにはどうすればいいか。写真アルバムを見返すのが一番だ。自分だけでなく、パートナーや子供達にも手軽に見てほしいのであれば、PCやスマートフォンタブレット端末上からWebブラウザでアクセスできるようにしたいところだ。

そこで、多数の画像ファイルを保存用に加工するためのスクリプトcram_iamge.pyを使って、自前で写真カタログを作成しよう。さらに、カタログ閲覧用プログラムlist_image.pyを使って、Webブラウザで閲覧できるようにしよう。
f:id:fridaynight:20180928211104p:plain

デモ

こちらのデモサイトにアクセスされたい。パスワード入力画面になるので、「abc」と入れて送信する。ログインに成功すると、写真のサムネイルが撮影日の逆順に並べられて表示される。Google Photosと似た表示だが、ここには私のコダワリがある。撮影日が新しいもの順に表示されるのだが、同じ撮影日の中では古い順に並べられているのだ。過去の写真を閲覧する際には、まずどんな日だったのか思い出して、その日の朝から晩までの写真を巡っていくものだ。探しやすさやアクセス頻度の観点から日付は新しい順に並べる方が使いやすいが、追体験を重視するなら同日の写真は古い順の方がいい。
f:id:fridaynight:20180928211414p:plain

各写真のサムネイルをクリックすれば、その写真の表示用画像が見られる。これは元画像ではなくディスプレイでの表示用に最大25万画素のJPEGに縮小されたものである。動画も表示できる。写真や動画はディスプレイの幅や高さに合わせた大きさで表示されるが、それに合わせてさらに縮小表示されている場合、画像をクリックすると拡大率を切り替えることができる。画面下方にはEXIFメタデータが表示されたり、元画像にアクセスするための「original」というリンクがあったりする。画面右肩にあるページめくりボタンを押すと、その日の次の写真や前の写真に遷移することができる。キーボードの左右の矢印キーでも前後に移動できる。「a」を押すとフルスクリーン表示になり、「esc」または「q」を押すとトップページに戻る。「z」を押すと、元データがブラウザで表示可能な形式の場合は元データを表示し、動画の場合は動画の再生を行う。「x」を押すと画像の拡大表示切り替えを行う。「m」を押すとメタデータの表示切り替えを行う。
f:id:fridaynight:20180928211544p:plain

デモのトップページに戻って、今度は日付の部分をクリックされたい。その日付の写真のみを閲覧することができる。最大100枚の写真のサムネイルが一画面に表示されるのだが、それ以上の枚数がある場合には画面右肩にページめくりボタンが現れる。ここでも私のコダワリがある。同じ日付内に100枚以上の写真があって、その101枚目以降の「子の兄弟」を見たい場合には「▻」を押せばよい。次の日付の「自分の兄弟」に飛びたい場合には、「▻▻」を押せば良い。戻る場合も同じ。キーボードの矢印キーを押した場合、子の兄弟がいればそれを表示し、いなければ自分の兄弟に飛ぶようになっている。つまり矢印キーだけで全ての写真を見ていける。なお、上を押すと「親」に行き、下を押すと「長子」に行くようになっている。
f:id:fridaynight:20180928211719p:plain

トップページに行くと、右肩に薄い色で変な記号のアイコンがあることに気づくかもしれない。メニューアイコンだ。それを押すと、特殊な表示方法を選ぶことができる。これも私のコダワリの機能だ。「Flatten Tree」は、コレクションの階層構造を無視して、あたかも全てのレコードが同じコレクションに入っているかのように表示する。「Collection Snippets」は、日付ごとに最大3枚の写真しか表示されないが、その分たくさんの日付が表示されるので、日付を特定してから古い写真を閲覧する際に便利だ。たった3枚でその日を表す必要があるので、3枚はその日の朝から3枚ではなく、順番で三等分したグループの先頭が採用されるようになっている。「Collection Names」は単に日付の文字列だけのリストを出すので、すでに日付がわかっている場合にはこれを使うと良い。「Newest First Order Collections」をクリックすると、デフォルトである、日付間だけ新しい順かつ日付内は古い順で表示される。「Newest First Order Through」は日付間も日付内も新しい順になる。「Oldest First Order Through」は全てが古い順になる。「More Records」を押すと一画面の最大表示数が10倍になり、「Less Records」は10分の1になる。
f:id:fridaynight:20180928211902p:plain

スマートフォンの配慮もしてあり、画面幅が1000ピクセルを下回ると、それに最適化した画面配置になる。画面のスワイプ操作でコンテンツの前後移動もできる。また、前後のページの表示画像を先読みしておくことで表示遅延の低減を図ってもいる。
f:id:fridaynight:20180928213033p:plain

重要なポイントとして、Google Photosやその他いくつかのサービスと違って、サムネイルの生成時にICCプロファイルを剥奪するだけじゃなくてデータもsRGB色空間にちゃんと変換しているというのがある。なのでAdobe RGB等で現像した写真も問題なく表示できる。

インストール

前提として、あなたは大量の多数の画像ファイルを自分のストレージ上に保管しているものとする。動画ファイルがあってもよい。それらのカタログを作って、Webブラウザや自作のアプリケーションソフトウェアで手軽に閲覧できるようにしたい。画像ファイルの形式はJPEGJPEG2000TIFFPNGかGIFとする。また、MOVやMP4の動画ファイルがあってもよい。

あなたはUNIX系(MacOSLinuxを含む)かWindowsのコンピュータを持っていて、そこから画像ファイルのストレージが読み込めるものとする。また、そのコンピュータ上でWebサーバを動作させられるものとする。家で閲覧するだけれあればそのWebサーバは外部に公開されている必要はなく、LAN内からアクセスできればよい。もちろんインターネットに繋がっていれば世界中どこからでも閲覧できる環境が構築できる。

あなたのコンピュータには、Python(2系でも3系でもOK)、ImageMagick(6系でも7系でもOK)、ExifToolがインストールされているものとする。動画ファイルのサムネイルを作りたい場合にはavconvも入っていると良いが、なくても構わない。

あとは、cram_image.pylist_image.pyをダウンロードして、実行パーミッションをつけて、コマンド検索パスの通った適当なディレクトリに置けば良い。

$ chmod 755 cram_image.py list_image.py
$ sudo cp cram_image.py list_image.py /usr/local/bin

写真カタログ

保存する画像データはできるだけ高画質で高解像度のものが望ましいが、それをそのまま閲覧環境に転送するのは現実的ではない。100MBとか50MBとかのデータを毎回転送するのは時間も金もかかりすぎる。それに1600万画素とかの解像度は普通のPC用ディスプレイで見るにしても解像度が高すぎるし、スマートフォンで見るならなおさらだ。したがって、保存用画像から閲覧用画像を生成しておいて、普段はそれを閲覧するのが望ましい。拡大してもっと良く見たいと思った場合にだけ保存用画像にアクセスすればいい。また、たくさんの写真を表示した一覧画面から個別の写真を選びたいので、一覧画面に表示されるサムネイル画像も必要だ。さらに、撮影日時や撮影設定などのメタデータも一緒に表示したり、日付に基づいてフォルダ分けして管理したりしたいので、予めEXIF等のメタデータを抽出したファイルも作っておきたい。

以上の要求から、各入力画像ファイルに対して、「保存用画像ファイル」「表示用画像ファイル」「サムネイル画像ファイル」「メタデータファイル」の4つを生成して適当なディレクトリに配置したものを写真カタログと呼ぶことにする。フォルダの中にどのようにサブディレクトリを作って整理するかはユーザの自由だ。ただし、4種類のファイルは同じディレクトリの中に置くこととする。ファイル名の先頭が同じ文字列で始まり、接尾辞がそれぞれ「-data.*」「-view.jpg」「-thumb.jpg」「-meta.tsv」で終わるファイルを一つのレコードとして認識する。保存用画像ファイルは元の入力ファイルそのものであり、拡張子はデータ形式に応じて .jpg、.jp2、.tifなどになる。

実用的には、日付でサブディレクトリを作るのがおすすめである。年や月でサブディレクトリを作る流儀もあるが、日付で十分だ。単一ディレクトリ内のエントリ数は、100年間撮り続けたとしても35000個にしかならないので、今日のOSであれば全く問題ない規模だ。

さっそくカタログを作ってみよう。 例として、撮りためた写真の画像データが /home/john/photo_archive というディレクトリに保存してあるとしよう。その中にはJPEGTIFFJPEG 2000とMOVとMP4の画像および動画のファイルが大量に入っているものとする。そのカタログ構造を /home/john/photo_album の下に作って、さらに撮影した日付のサブディレクトリに自動的に分類して保存する。

$ mkdir /home/john/photo_album
$ find /home/john/photo_archive -type f  | egrep -i '\.(jpg|tif|jp2|mov|mp4)' | sort |
  cram_image.py --read_stdin --catalog --destination /home/john/photo_album \
    --date_subdir --timestamp_name --unique_name --intact

findコマンドの行は、-type fでファイルのみを出力させ、その出力を正規表現にかけて拡張子が .jpg と .tif と .mov と .mp4 のものだけに限定して、辞書順で並び替えている。cram_image.pyのフラグも説明する。--read_stdinで標準入力から入力ファイルのパスのリストを受け取る。--catalogで、上述の4種類のカタログファイルを作成させる。--destinationで出力先のディレクトリを指定する。--date_subdir で撮影日のサブディレクトリを作る。--timestamp_name で保存ファイルの名前を撮影日時の名前にする。名前が日時だけだと同秒に撮った写真が上書きされる恐れがあるので、--uique_name でユニークな接尾辞をつけて重複を回避している。--intactをつけると、入力ファイルの縮小や加工や再圧縮を抑制して、そのまま保存する。cram_image.py の詳しい使い方については以前の記事を参照のこと。

結果として、こんなようなファイルが作られるだろう。それぞれはただのファイルなので、普通にファイルマネージャでアクセスして開いたり移動したりができる。

$ find /home/john/photo_album -type f
/home/john/photo_album/2018-01-01/20180101092345-af0e2ire-data.mov
/home/john/photo_album/2018-01-01/20180101092345-af0e2ire-meta.tsv
/home/john/photo_album/2018-01-01/20180101092345-af0e2ire-thumb.jpg
/home/john/photo_album/2018-01-01/20180101092345-af0e2ire-view.jpg
/home/john/photo_album/2018-01-01/20180101131533-8whi4rex-data.jp2
/home/john/photo_album/2018-01-01/20180101131533-8whi4rex-meta.tsv
/home/john/photo_album/2018-01-01/20180101131533-8whi4rex-thumb.jpg
/home/john/photo_album/2018-01-01/20180101131533-8whi4rex-view.jpg
...

閲覧用サービスの設置

list_image.pyは上述の写真カタログを閲覧するためのコマンドラインツールであり、かつCGIスクリプトでもある。まずはコマンドとして使ってみよう。

$ list_image.py --album_dir /home/john/photo_album /

上記では、--album_dir でカタログ構造のあるデータディレクトリを指定して、その中の「/」というリソースを検索している。以後、カタログ構造のあるディレクトリとそのデータを総称して「アルバム」と呼ぶことにする。アルバムの中には撮影日に分けたサブディレクトリが存在しているわけだが、「/」を検索するとそれら日付のサブディレクトリを一覧することができる。アルバム内のリソース管理の文脈では、ディレクトリやサブディレクトリのことを「コレクション」と呼び、コレクション内の実データを示すエントリのことを「レコード」と呼ぶ。 コレクションとレコードの総称を「リソース」と呼ぶ。全てのリソースにはIDがついていて、それはアルバム内の相対パスと一致するのだが、それを使ってリソースの内容を問い合わせることができる。

{
  "result": {
    "id": "/", 
    "type": "collection", 
    "collection_serial": 0, 
    "num_child_records": 0, 
    "num_child_collections": 16, 
    "num_orphans": 0, 
    "num_reachable_records": 0, 
    "items": [
      {
        "id": "/2017-08-01"
      }, 
      {
        "id": "/2017-08-02"
      }, 
      {
        "id": "/2017-08-04"
      }, 
      {
        "id": "/2017-08-05"
      }, 
      ...
     ],
  }, 
  ...
}

引数として「/」ではなく「/2017-08-01」などと指定すると、そのコレクションの中のレコードを探してくれる。そこで提示される「/2017-08-01/20170801123456」などのレコードIDを指定すると、レコードを構成するファイル群を知ることができる。

$ list_image.py --album_dir /home/john/photo_album /2017-08-01/20170801120000
{
  "result": {
    "id": "/2017-08-01/20170801120000", 
    "type": "record", 
    "record_serial": 0, 
    "data_url": "file:///home/john/photo_album/2017-08-01/20170801120000-data.jpg", 
    "metadata_url": "file:///home/john/photo_album/2017-08-01/20170801120000-meta.tsv", 
    "viewimage_url": "file:///home/john/photo_album/2017-08-01/20170801120000-view.jpg", 
    "thumbnail_url": "file:///home/john/photo_album/2017-08-01/20170801120000-thumb.jpg"
  }, 
  ...
}

お気づきのように、出力はJSON形式である。JSONデータを眺めて嬉しい人はそんなにいないだろうが、ここで機械可読性を確保しておくことはインターオペラビリティすなわち相互運用性の観点で非常に重要だ。--output_format htmlとかつけるとXHTML5準拠のデータを吐くのだが、それは冒頭に述べたデモ画面を描画しているデータそのものである。

オプションの --depth で深度を指定すると、指定したリソースIDから再帰的に深さ優先探索を行った結果を表示してくれる。--depth=0 とかやると全ての子孫が表示される。そのほかにも細かい機能がたくさんあるが、それは --help で確認されたい。

list_image.pyをCGIスクリプトとして実行すると、写真閲覧が可能になる。Webサーバの設定はここでは割愛するが、CGIFastCGIなどの設定を適宜行って、list_image.pyを呼び出すようにすればよい。典型的には、list_image.py を適当な場所にコピーして、list_image.cgiとかいう名前にする。さらに、それをエディタで開いて、冒頭付近にある以下の設定を書き換える。

ALBUM_DIR = "/home/john/photo_album"               # アルバムディレクトリの絶対パス
URL_PREFIX = "https://example.com/photo_album"     # アルバムディレクトリの公開URL

アルバムディレクトリの中のデータは、Webサーバ上のリソースとして直接公開される。そのために、アルバムディレクトリ自体の公開URLを指定する必要がある。それぞれのレコードに属するファイルは、アルバムディレクトリの公開URLを基準とした相対パスとして解釈される。インターネット上で運用する場合には、リソースのセキュリティはWebサーバの機能で確保することになる。

CGIスクリプトを /list_image としてアクセスできるようにしたならば、/list_image/hoge というURLは「/hoge」というリソースにアクセスする。CGIスクリプトを「/foobar/list_image.cgi」として設置しなら、「/foobar/list_image.cgi/hoge」で「/hoge」にアクセスできる。

$ curl https://example.com/list_cgi/2017-08-01

CGIスクリプトにアクセスした場合、デフォルトではJSONが返される。しかし、Acceptヘッダで application/xhtml+xml とか application/xml とか text/html とかを指定すると、XHTML5(XML-serialized HTML 5)のデータを返してくるようになる。ブラウザで閲覧できるのはこのためである。

CGIスクリプトが受け付けるパラメータはコマンドラインで指定できるパラメータと全く同じなので、--help で仕様を確認されたい。ただし、CGIモードだけの特別な設定として、いくつかのパラメータのデフォルト値が上書きされている。list_image.py の冒頭の定数定義を書き換えればその辺りの挙動を変えることができる。

設計思想

一寸のシステムにも、五分の設計思想があるのだ。今回は、とかくレガシーに徹した。写真やカタログデータを格納するのはただのディレクトリであり、単なる命名規則でファイル間の紐付けをしている。DBMSも使っていないし、設定ファイルすらない。しかし、仕様を単純化することでインターオペラビリティは最大化する。list_image.pyはPythonで書かれているが、C++でもJavaでもRubyでも同じ機能を簡単に実現できるだろう。なんならシェルスクリプトでだって書ける。メタデータが単なるTSVなのも、findとgrepだけで検索が完結できるようにするためだ。メンテナビリティの点でも有利だ。もし元データをデータベース内のBLOBとして保存していて、仕様書がちゃんと引き継げていなかったなら、ちょっとした地獄を見るだろう。今回のように単純なら、管理者が突然飽きて失踪したり死んだりしてもデータの引き継ぎに苦労することはなかろう。

list_image.pyやそのCGIスクリプトの出力がJSONなのもインターオペラビリティのためだ。デモ用にHTMLを出力してブラウザで操作できるようにもしたが、それはあくまでデモとフィージビリティテストのためである。デモのHTMLモードでも、JSON出力の元になる同じ連想配列のみを使ってレンダリングを行っている。つまり、デモでできることはJSONデータを受け取ったいかなるクライアントでも実現できることが保証されている。すなわち、AJAXJavaScriptコードをHTML文書から呼び出すだけで、任意の機能を持った写真閲覧サイトを作ることができる。もちろんAndroidiPhoneのアプリを書いたっていい。デモではエージェントからの各リクエストに対してカタログに対するクエリを1回しか発行しないという縛りで開発したが、実際には複数回のリクエストを発行したっていいし、それを非同期的に行ってもいい。スライドショーとかは非同期データ取得で実装した方が格好いい。

MVC的な見方をすれば、cram_image.pyやlist_image.pyはMVCのModel部分のみを担当していて、ViewとかControllerとかは勝手にやってくれという方針だ。そしてlist_image.pyがリソースIDをREQUEST_PATHとして受け取ったりJSONを返したりするのはちょっとRESTfulアーキテクチャを意識しているからだ。今回は写真の追加や削除の実装をしていないが、それらもそのうち作る予定だ。でも更新系はちょっと設計が悩ましいので、閲覧系だけで区切って公開してみた。

まとめ

クラウド系の写真サイトに写真を預けるのは便利だし、それはデータ保護の観点からも重要なことだ。でも、DIY派の人は、自分で管理したいって思うだろう。大手のサービスは概して良いが、ちょっとかゆいところに手が届かないとか思っちゃうこともあるだろう。そんなあなたにはぜひcram_image.pyとlist_image.pyを使ってみてほしい。「俺の考えた最強の写真閲覧サイト」はあなたにしか作れないのだから。

写真は思い出の付箋。後で見返すために撮っているわけだが、写真愛好家・カメラ愛好家の諸兄姉で閲覧環境の改善に熱意を注いでいる人は少ないように思える。素敵な写真を撮るのも大事だが、一度しか見ないのではもったいない。私は、子達がスマートフォンを持つようになった時にも使ってもらえるシステムを作りたい。ちと大げさだが、自己肯定感の向上にそれが役立つこともあるだろう。

Just Look 'n Learn English Picture Dictionary (Just Look ©n Learn Picture Dictionary Series)

Just Look 'n Learn English Picture Dictionary (Just Look ©n Learn Picture Dictionary Series)

SONY デジタルカメラ DSC-RX100 1.0型センサー F1.8レンズ搭載 ブラック Cyber-shot DSC-RX100

SONY デジタルカメラ DSC-RX100 1.0型センサー F1.8レンズ搭載 ブラック Cyber-shot DSC-RX100