同一画像検索(2):Finder moduleのための確認
前回はダミーのFinderモジュールで、とりあえず外側を作ったので、 今回からFinderの作成をすすめたい。ただ、その前に幾つか基本的な ところを確認しておきたい。
同一の画像を見つけるやり方は前回も書いた通り解像度4x4に変換して その情報が同じものをグルーピングすることにした。 まずはこの部分を実現するために、次の2つを確認しておく。
- 画像ファイルを解像度4x4に変換して48 bytesのデータを取り出す。
- キーが同じファイルを集めてリストにする。
画像ファイルの変換
まず最初の方について検討する。画像ファイルの変換処理は書いてられない のでImageMagickを使って変換してみよう。最終的に、各画素の色情報 (256段階x3色)が欲しいので、PPM(バイナリ)形式にして後ろから48 bytesを 切り出す。前回も書いたが48 bytesなのは 4x4ドットx色3 bytesだから。 今後、このように画像ファイルを小さく変換して得られるデータを fingerprintと 表記する。今回は48 bytesのfingerprintということだ。なお、ImageMagickの convertコマンドの詳細はここでは割愛する。
$ convert -define jpeg:size=4x4 -filter Cubic -resize 4x4! test.jpg test.ppm
test.ppmの中身は次の通り。
$ tail -c 48 test.ppm > test.out argent-2:work eiji$ od -x test.out 0000000 aecd c17e 7fa9 7e95 8567 6672 ccf2 df9b 0000020 8dba 97b5 a57d 7d8e cfe4 dbba a5bf bed7 0000040 e1a9 c6d3 c0d5 d7b1 a7bc c2db e6ad cdd8 0000060
これを踏まえ、Haskellでconvertコマンドを実行する処理を考える。
外部コマンドを実行するにはSystem.Processモジュール
を使うらしいので、それらしいものを探す。実行したいコマンドは、パイプを使って
必要な部分だけ切り取り、それをプログラムで取り出したいので
runInteractiveCommand
が相当しそうだ。
こことか
ここ
とかを参考に書いてみた。
Main-t0.hs
module Main where import System.IO import System.Process reso = 4 getFingerPrint :: String -> IO String getFingerPrint f = do (sin, sout, serr, ph) <- runInteractiveCommand command waitForProcess ph hGetLine sout where geo = (show reso) ++ "x" ++ (show reso) size = reso * reso * 3 command = "convert -define jpeg:size=" ++ geo ++ " -filter Cubic -resize " ++ geo ++ "! " ++ f ++ " PPM:- | tail -c " ++ (show size) main :: IO () main = do putStr =<< getFingerPrint "~/work/test1.jpg"
コンパイルして実行してみる。
$ ghc -o t0 Main-t1.hs $ ./t0 > /tmp/out.dat rcom: fd:5: hGetLine: invalid argument (invalid byte sequence)
出力結果を取り出すところでしくじっているらしい。ググってもよくわからな かったが、色情報は単なるバイト列なので「文字」にならないバイトも多い。 ということで、ByteStringに変えてみた。
Main-t0.hs (2)
module Main where import Data.ByteString.Char8 as BS import System.IO import System.Process reso = 4 getFingerPrint :: String -> IO ByteString getFingerPrint f = do (sin, sout, serr, ph) <- runInteractiveCommand command waitForProcess ph BS.hGetLine sout where geo = (show reso) ++ "x" ++ (show reso) size = reso * reso * 3 command = "convert -define jpeg:size=" ++ geo ++ " -filter Cubic -resize " ++ geo ++ "! " ++ f ++ " PPM:- | tail -c " ++ (show size) main :: IO () main = do s <- getFingerPrint "~/work/test1.jpg" BS.putStr s
こんどはうまくいった。
$ ./t0 > /tmp/out.dat $ od -x /tmp/out.dat 0000000 aecd c17e 7fa9 7e95 8567 6672 ccf2 df9b 0000020 8dba 97b5 a57d 7d8e cfe4 dbba a5bf bed7 0000040 e1a9 c6d3 c0d5 d7b1 a7bc c2db e6ad cdd8 0000060
最初にハンドでコマンドを流したときと同じ出力が得られている。
同一キーのファイルを集める
画像から同じfingerprintを得られたとして、それらを同一とみなす方法が必要である。 簡単に実現しようとすると、連想配列を用いてキーが同じものをまとめてしまえばよい。 Haskellでは連想配列を扱うにはData.Mapを使うらしい。
ここでは入力はキー(文字列)とファイル名の組のListとし、結果は各キーに対しファイル名の Listが対応するMapとしたい。入力がListなので、変換する関数のシグネチャは
[(String, String)] -> Map String [String]
になるだろう。ただ、再帰で処理させることを考えると、処理済みのMapも引数に与えて
[(String, String)] -> Map String [String] -> Map String [String]
となるだろう。あとはこれに合うように再帰関数を書けば良い。とはいえHaskellの
再帰処理はよくわかっていないので、少々こんがらがったが最終的に下記のような関数
tomap
に落ち着いた。
tomap :: [(String, String)] -> Map String [String] -> Map String [String] tomap (x:xs) m = tomap xs (Map.insert k l m) where k = fst x l = tolist x (Map.lookup k m) tolist :: (String, String) -> Maybe [String] -> [String] tolist x Nothing = [snd x] tolist x (Just l) = (snd x:l)
ちなみに、tolist
はすでに同じキーで登録されているもの(List)があればそれを取り出して
新しい要素をそのListに追加し、なければ新しく要素一つのListを作って返す関数。
なお、tomap
の定義ではMapの初期値が現れてきていない。これについてはtomap
を最初に
呼び出すときに引数としてMap.emptyを与えている。これが綺麗なやり方かどうかは不明。
てきとうに動くプログラムに仕立てて処理した結果は次の通り。
(input) [("a", "apache"), ("e", "emacs"), ("a", "ant"), ("c", "ceph")] (output) [["ant","apache"],["ceph"],["emacs"]]
キーが"a"のものについては結果のListに複数の要素が入っている。
次回は、上記の確認を踏まえて同一画像を判定する簡易な関数を作ってみる。