読者です 読者をやめる 読者になる 読者になる

アラタナエンジニアブログ

aratana Engineer's Blog

【Raspberry Pi 2 Model B】増え続ける子どもの写真管理に特化したNASを構築しよう(第2回:実施編-画像処理)

Linux ガジェット Raspberry Pi 画像処理


こんにちは。ガジェット大好き穗滿です。
前回はRaspberry Piに関しての基礎的な知識や作ろうとしているNASの構成、JPEG画像の基礎、JPEG画像をノンレスでさらに容量削減できるネタなどを中心にお送りしました。今回からはいよいよ実践です。まずは画像を中心に扱うNASにしますので、Raspberry Piそのものというよりは、画像処理について研究していきたいと思います。
※「2-9. バックアップ先のストレージについて」までが前回記事の内容となっています。




3.実践編
f:id:issei-homan:20150218100827p:plain

軽く前回のおさらいですが、上図のような構成で写真管理に特化したNASを構築予定です。
今回は写真のExif情報や圧縮の技術について考えてみます(Raspberry Pi の活用は次回になります、すみません!)。



写真からExif情報を取得することで、絞り値や露光時間とかだけでなく、GPS情報が含まれていればどこで撮影した写真かを知ることができ、Google Maps API 上に並べて表示してみるなど遊べます。(※逆を言うと、GPS情報付きの写真を撮影してアップロードするとGPS情報で住所がばれてしまう可能性があるので、ソーシャル利用時などは注意です)。最近のデジカメやスマホのカメラ機能では、なんらかのExif情報を含んだ形で撮影されます。

Exif情報を取得する為に、今回は手っ取り早いPHPを利用したいと思います。
PHPには画像からExif情報を取得する拡張モジュールがあり、exif_read_data()関数を使うと簡単に取得できますが、あらかじめPHPを --enable-exifコンパイルしていなければ利用できません。よくわからんーという場合はGPLライセンスThe PHP JPEG Metadata Toolkit を読み込めばExif以外のJPEGファイル追加ヘッダの操作なども可能になります。ただし、PHP4ベースの古いソースのため、精神衛生上受け付けない場合はご注意です。手元のPHP5.3.26で試しに使ってみましたが、以下のファイルが <? から始まっているところを <?php と書き換える事で利用できました。今回は The PHP JPEG Metadata Toolkit を使った例で進めてみます。
(2015年3月31日時点で、バージョン1.12が最新です)

EXIF.phpを読み込むことで利用ができるようになります。ディレクトリ名がデフォルトだと長すぎるので、適宜修正すると良いと思います。
※ここでは「PHP_JPEG_Metadata_Toolkit_1.12」から「EXIF」というディレクトリ名に変更した例で進めます。

<?php
  include_once 'EXIF/EXIF.php';

The PHP JPEG Metadata Toolkit で取得できる配列をprint_r等で表示するとわかるが、かなり大量の情報が格納されています。各情報は多次元配列として整理されているようですが、このままでは非常にわかりにくいです(EXIF_Tags.phpで定義されている)。
いくつかキーとなる情報があるので、以下にまとめます。$arrExifという配列に情報が入ってる前提とします。

  • 製造会社・・・$arrExif[0][271]['Text Value']
  • 撮影機材・・・$arrExif[0][272]['Text Value']
  • ソフトウェア・ファームウェア・・・$arrExif[0][305]['Text Value']
  • 焦点距離・・・$arrExif[0][34665]['Data'][0][37386]['Text Value']
  • 絞り値(F値)・・・$arrExif[0][34665]['Data'][0][33437]['Text Value']
  • 露光時間・・・$arrExif[0][34665]['Data'][0][33434]['Text Value']
  • ISO感度・・・$arrExif[0][34665]['Data'][0][34855]['Text Value']
  • 横サイズ(px)・・・$arrExif[0][34665]['Data'][0][40962]['Text Value']
  • 縦サイズ(px)・・・$arrExif[0][34665]['Data'][0][40963]['Text Value']
  • 撮影日時・・・$arrExif[0][34665]['Data'][0][36867]['Text Value'] 写真印刷サービスなどでこのExifがあると日付入れができたり便利な可能性があるので、撮影日時のExifはできるだけ残したいと考えてます。
  • GPS情報・・・$arrExif[0][34853]['Data'][0][2]['Data']に北緯(南緯)情報、$arrExif[0][34853]['Data'][0][4]['Data']に東経(西経)情報があります。「度分秒」で取得されるので、10進法に変更すると使いやすくなります。ちなみに$arrExif[0][34853]['Data'][0][1]['Text Value']がNだと北緯でSだと南緯、$arrExif[0][34853]['Data'][0][3]['Text Value']がEだと東経で、Wだと西経ということになりそうです。

以下はExif取得用サンプルとなる画像です。2015年3月31日現在では、はてなにあげた時点でサンプル画像のExif情報が残ってる事を確認してますが、実画像サイズは縮小されているにもかかわらず、横縦のpxのExif情報はもとのままとなってます。

この写真を読み込ませると、以下のような感じで情報が取得できました。

製造会社:
撮影機材:
ソフトウェア・ファームウェアFacebook for iPhone/iPad
焦点距離:83/20 (4.15) mm
絞り値(F値):11/5 (2.2)
露光時間:1/2198 (0.00045495905368517) seconds
ISO感度:32
横サイズ(px):3264 pixels
縦サイズ(px):2448 pixels
撮影日時:2015:03:14 14:09:56
北緯:31度58分42.97秒 東経:130度54分16.07秒


今回の写真は iPhone 6 Plus でfacebookのアプリ内からカメラを起動して撮影したのですが、製造会社や撮影機材の情報が取得できませんでした。ここには詳細は記載しませんが、直接カメラから撮影したものを取り込んでみると、Apple iPhone 6 Plus という文字列が取得できましたが、GPS情報が取得できませんでした。なにか設定はあるかもしれません。


とりあえず表示するだけのPHPソースを貼っておきます。この後の方向性としてはこの情報をなんらかのDB等に保持して、写真からは消してしまう事を考えています。ただし、先述のとおり撮影日時に関しては残すことを考えています。

<?php
  include_once 'EXIF/EXIF.php';
  $img = "パスとJEPG画像名";
  $arrExif = get_EXIF_JPEG($img);
  echo "製造会社:".$arrExif[0][271]['Text Value']."<br />\n";
  echo "撮影機材:".$arrExif[0][272]['Text Value']."<br />\n";
  echo "ソフトウェア・ファームウェア:".$arrExif[0][305]['Text Value']."<br />\n";
  echo "焦点距離:".$arrExif[0][34665]['Data'][0][37386]['Text Value']."<br />\n";
  echo "絞り値(F値):".$arrExif[0][34665]['Data'][0][33437]['Text Value']."<br />\n";
  echo "露光時間:".$arrExif[0][34665]['Data'][0][33434]['Text Value']."<br />\n";
  echo "ISO感度:".$arrExif[0][34665]['Data'][0][34855]['Text Value']."<br />\n";
  echo "横サイズ(px):".$arrExif[0][34665]['Data'][0][40962]['Text Value']."<br />\n";
  echo "縦サイズ(px):".$arrExif[0][34665]['Data'][0][40963]['Text Value']."<br />\n";
  echo "撮影日時:".$arrExif[0][34665]['Data'][0][36867]['Text Value']."<br />\n";
  $arrLat[0] = (INT)$arrExif[0][34853]['Data'][0][2]['Data'][0]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][2]['Data'][0]['Denominator'];
  $arrLat[1] = (INT)$arrExif[0][34853]['Data'][0][2]['Data'][1]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][2]['Data'][1]['Denominator'];
  $arrLat[2] = (INT)$arrExif[0][34853]['Data'][0][2]['Data'][2]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][2]['Data'][2]['Denominator'];
  $arrLon[0] = (INT)$arrExif[0][34853]['Data'][0][4]['Data'][0]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][4]['Data'][0]['Denominator'];
  $arrLon[1] = (INT)$arrExif[0][34853]['Data'][0][4]['Data'][1]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][4]['Data'][1]['Denominator'];
  $arrLon[2] = (INT)$arrExif[0][34853]['Data'][0][4]['Data'][2]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][4]['Data'][2]['Denominator'];
  echo "北緯:".$arrLat[0]."".$arrLat[1]."".$arrLat[2]."秒 ";
  echo "東経:".$arrLon[0]."".$arrLon[1]."".$arrLon[2]."秒<br />\n";


先述のとおり、取得できるGPS情報は度分秒になっているため、以下の要領で10進数へ変換し使いやすくします。

例)北緯:31度58分42.97秒 の場合
度はそのままで、分を60で割る、秒を3600で割ります。
31 + (58/60) + (42.97/3600)
= 31 + 0.96666667 + 0.01193611
= 31.9786028


例)東経:130度54分16.07秒
130 + (54/60) + (16.07/3600)
= 130 + 0.9 + 0.00446389
= 130.904464

Google Map で「31.9786028, 130.904464」を入力して検索すると撮影した場所が特定できます(リンクをはると若干微調整が入ってるみたいですが。。)。
写真は宮崎県小林市にある生駒高原で撮影しましたので、ほぼずれがなくプロットされました!
あとはGoogle Maps API を利用してこの座標にバルーンでプロットしたり、サムネイルを表示できれば良いですね。


先ほどのPHPGPS関連部分だけ以下のように書き換えました。なんだかごちゃっとしてますが、適宜いい感じに加工してみてください。

<?php
  include_once 'EXIF/EXIF.php';
  $img = "パスとJEPG画像名";
  echo "製造会社:".$arrExif[0][271]['Text Value']."<br />\n";
  echo "撮影機材:".$arrExif[0][272]['Text Value']."<br />\n";
  echo "ソフトウェア・ファームウェア:".$arrExif[0][305]['Text Value']."<br />\n";
  echo "焦点距離:".$arrExif[0][34665]['Data'][0][37386]['Text Value']."<br />\n";
  echo "絞り値(F値):".$arrExif[0][34665]['Data'][0][33437]['Text Value']."<br />\n";
  echo "露光時間:".$arrExif[0][34665]['Data'][0][33434]['Text Value']."<br />\n";
  echo "ISO感度:".$arrExif[0][34665]['Data'][0][34855]['Text Value']."<br />\n";
  echo "横サイズ(px):".$arrExif[0][34665]['Data'][0][40962]['Text Value']."<br />\n";
  echo "縦サイズ(px):".$arrExif[0][34665]['Data'][0][40963]['Text Value']."<br />\n";
  echo "撮影日時:".$arrExif[0][34665]['Data'][0][36867]['Text Value']."<br />\n";
  $arrLat[0] = (INT)$arrExif[0][34853]['Data'][0][2]['Data'][0]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][2]['Data'][0]['Denominator'];
  $arrLat[1] = (INT)$arrExif[0][34853]['Data'][0][2]['Data'][1]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][2]['Data'][1]['Denominator'];
  $arrLat[2] = (INT)$arrExif[0][34853]['Data'][0][2]['Data'][2]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][2]['Data'][2]['Denominator'];
  $arrLon[0] = (INT)$arrExif[0][34853]['Data'][0][4]['Data'][0]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][4]['Data'][0]['Denominator'];
  $arrLon[1] = (INT)$arrExif[0][34853]['Data'][0][4]['Data'][1]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][4]['Data'][1]['Denominator'];
  $arrLon[2] = (INT)$arrExif[0][34853]['Data'][0][4]['Data'][2]['Numerator'] / (INT)$arrExif[0][34853]['Data'][0][4]['Data'][2]['Denominator'];
  $arrLat['dec'] = $arrLat[0] + ($arrLat[1]/60) + ($arrLat[2]/3600);
  $arrLon['dec'] = $arrLon[0] + ($arrLon[1]/60) + ($arrLon[2]/3600);
  echo "北緯:".$arrLat[0]."".$arrLat[1]."".$arrLat[2]."秒(".$arrLat['dec'].") ";
  echo "東経:".$arrLon[0]."".$arrLon[1]."".$arrLon[2]."秒(".$arrLon['dec'].")<br />\n";


出力結果は以下のような感じです。

製造会社:
撮影機材:
ソフトウェア・ファームウェアFacebook for iPhone/iPad
焦点距離:83/20 (4.15) mm
絞り値(F値):11/5 (2.2)
露光時間:1/2198 (0.00045495905368517) seconds
ISO感度:32
横サイズ(px):3264 pixels
縦サイズ(px):2448 pixels
撮影日時:2015:03:14 14:09:56
北緯:31度58分42.97秒(31.978602777778) 東経:130度54分16.07秒(130.90446388889)

Google でも画像の圧縮について推奨しているわけですが、触れられている jpegtran でどの程度圧縮されるのか確認してみましょう。圧縮について手元にあるMac OSで考察してから、Raspberry Piで実装する方法を決定したいと思います。Mac OSの場合は以下のような感じで簡単に導入してテストができます。

$ brew install jpeg

そして、早速画像を変換してみます。とりあえずExif情報は全部削除し、エントロピー符号(Huffman 符号)を最適化するオプションをつけてやってみました。

$ jpegtran -copy none -optimize test.jpg > output.jpg

すると、 1,760,133バイトの写真が1,742,302バイトと17キロバイトほどの削減になりました。予測はしてましたがExifを削った程度ではほとんど変わらない事がわかりました。jpegtranは基本的にロスレスで画像を回転したりするツールですので、これ以上の容量圧縮は期待できそうにありません。
そのほかにjpegoptimというツールがあり、ロスレスはもちろんのこと、圧縮率も指定できますので、こちらもインストールしてみます。

$ brew install jpegoptim

インストールが終わったら、とりあえず--strip-allオプションを指定してExifなどのメタ情報を削除してみました。jpegoptimは基本上書き保存ぽいので、--destオプションで別ディレクトリを指定すると別のフォルダに処理後の画像が出力されます(下記の例ではtestフォルダ)。

$ jpegoptim test.jpg --dest=./test --strip-all
test.jpg 3264x2448 24bit N Exif [OK] 1760133 --> 1742302 bytes (1.01%), optimized.

Exifなどのメタ情報を削っただけでは、jpegtranもjpegoptimも全く同じ結果になりました(当たり前っちゃ当たり前ですが)。
そこで、90%クオリティ指定(-m90)を行って実行したところ、30.44%ほど圧縮されました。
※すでにtestフォルダに同じ名前の画像があると処理されないので、実行前に削除しておきます。

$ jpegoptim test.jpg -m90 --dest=./test --strip-all
test.jpg 3264x2448 24bit N Exif [OK] 1760133 --> 1224290 bytes (30.44%), optimized.

スマホで撮影したような風景写真であれば、割と高圧縮しても平気かもしれません(見た目的には気にならない)。
ただ、グラデーション掛かった空の写真とか人物の拡大写真とか、細かい模様の柄だと圧縮しすぎるとモアレが発生する可能性があるので、注意です。


jpegoptimをつかって、以下のような写真をいくつかの圧縮レベルで出力してみたいと思います。

この画像は 横 1180px、縦 610px で容量は約931KB(931,770 bytes)あります。Exif情報は今回の画像では削っています。24ビットなので、1pxあたり3バイトだとすると2.16MBほどの容量になる計算ですが、第1回の記事の通りJPEGの処理によって最終的に40%くらいの容量になっているようです。ただ、それにしてもWEBで表示する場合、このピクセル数で931KBは重ための画像です。どの程度の圧縮までが耐えられそうか比較していきましょう。


一気に画質を90から10まで出力してみます(出力画像名は出力後に都度リネームしています)。
90が高画質で、数値が下がるほど低画質になっていきます。

$ jpegoptim sample.jpg -m90 --dest=./test
sample.jpg 1180x610 24bit N JFIF [OK] 931770 --> 206945 bytes (77.79%), optimized.

$ jpegoptim sample.jpg -m80 --dest=./test
sample.jpg 1180x610 24bit N JFIF [OK] 931770 --> 142214 bytes (84.74%), optimized.

$ jpegoptim sample.jpg -m70 --dest=./test
sample.jpg 1180x610 24bit N JFIF [OK] 931770 --> 114611 bytes (87.70%), optimized.

$ jpegoptim sample.jpg -m75 --dest=./test
sample.jpg 1180x610 24bit N JFIF [OK] 931770 --> 124837 bytes (86.60%), optimized.

$ jpegoptim sample.jpg -m60 --dest=./test
sample.jpg 1180x610 24bit N JFIF [OK] 931770 --> 96198 bytes (89.68%), optimized.

$ jpegoptim sample.jpg -m50 --dest=./test
sample.jpg 1180x610 24bit N JFIF [OK] 931770 --> 83600 bytes (91.03%), optimized.

$ jpegoptim sample.jpg -m40 --dest=./test
sample.jpg 1180x610 24bit N JFIF [OK] 931770 --> 71987 bytes (92.27%), optimized.

$ jpegoptim sample.jpg -m30 --dest=./test
sample.jpg 1180x610 24bit N JFIF [OK] 931770 --> 58812 bytes (93.69%), optimized.

$ jpegoptim sample.jpg -m20 --dest=./test
sample.jpg 1180x610 24bit N JFIF [OK] 931770 --> 42430 bytes (95.45%), optimized.

$ jpegoptim sample.jpg -m10 --dest=./test
sample.jpg 1180x610 24bit N JFIF [OK] 931770 --> 23095 bytes (97.52%), optimized.

出力画像は以下です。


画質90の時点でいきなり77.79%圧縮されています。これは全体的に青っぽい色が多いために大胆に削って圧縮がかかってるのだと思います。個人的な主観になりますが、この画像の場合、画質70くらいまでは許容範囲になりそうだなと感じています。60以下になってくると少しずつですが細かい布地の質感が徐々に失われていく様が見て取れます。


さて、ここで試しに JPEGmini で圧縮したものを比較してみましょう。

約 125KB(125,127 bytes)となり、これは画質75とかなり近似値となりました。別の色数の多そうな風景写真で確認したところ、こちらの場合は、画質85の画質と容量が近似値となりました。もっとたくさんの写真で確認しないとなんともいえませんが、JPEGminiも内部的に jpegoptim を使っているか似たようなアルゴリズムで色数が偏ったものは画質75 前後、色数が多いものは画質85前後で出力していると仮定して、これをなんとか今回のシステムで自動的に行えるようにするとバランスがよいかなと考えています。


すごくアバウトですが、画質75で圧縮した場合に80%を超える圧縮率の場合は基本的に同色が多い画像と認識し、それ以下であれば色数の多い画像と判断して画質85で再生成するような感じだと、(正直遠回しですが)自動変換でもそこそこいい感じにまとまってくれそうですね。



これで一通り画像処理についての基礎知識が身についたと思います。
次回以降は Raspberry Pi上でWEBサーバーを構築し、ナウでヤングな表示を行ってみることにします!
お楽しみに!