Haskellでいってみよう

日曜プログラマにも満たないレベルでもHaskellで何かソフトウェアを作りたい!

同一画像検索(4): 簡易版を作ってみた

これまでの確認などを踏まえ、今回は簡易版を作ろう。仕様では4x4解像度の画像比較をして 一致したら次に16x16解像度でもう少し詳細に比較する、としているところ、まずは4x4解像度での 比較だけで作ってみようと思う。

最初に、「外枠」で示したfindSame関数の型シグネチャに問題があったので訂正する。 これまで示したのは下記の形だった。

findSame :: [String] -> [[String]]

画像比較のためImageMagickを呼び出す仕様なので、どうしても副作用が発生する。 haskellではそれが含まれる場合はどうやっても純粋な(副作用のない)型シグネチャには できなさそうだ。(言われてみれば、その関数の処理のどこか一部に副作用があるなら 全体として関数の処理結果が引数だけで一意に決まらないのは当然か。) 下記が訂正版とその呼び出し元mainの変更部分。

findSame :: [String] -> IO [[String]]

  :
  
main = do
  ds <- getArgs
  fs <- mapM getFileLists ds
  ss <- findSame $ concat fs
  putGroups ss

さて、肝心のfindSameをどうするかだが、処理の流れは次の通りかと。

  • 各画像のfingerprint(前々回説明)を取得(ここでImageMagickを使う)
  • 同じfingerprintを持つ画像群をListにまとめる
  • 同じものが見つかった(=Listの長さが2以上)ら、そういった画像群のListを返す

これを関数にしてみる。プログラムが上の"処理の流れ"そのまんまなのはさすが haskellといったところか。なお、以後のコード片ではファイル名をStringではなく 別名のFilePath(Preludeで定義済み)、fingerprintもByteStringではなく FingerPrint型とした。

findSame :: [FilePath] -> IO [[FilePath]]                                       
findSame fs = do                                                                
  fps <- mapM getFingerPrint4 fs                                                
  let es = Map.elems $ foldl insertItem Map.empty (zip fps fs)                  
  return $ filter (\x -> length x > 1) es                                       

getFingerPrint4は4x4解像度のfingerprintを取得する関数だが、実際は 解像度を引数で与えるgetFingerPrintに第一引数を4としたもの。この辺も 関数型言語らしい。

getFingerPrint :: Int -> FilePath -> IO FingerPrint                             
getFingerPrint r f = do                                                         
  (sin, sout, serr, ph) <- runInteractiveCommand command                        
  waitForProcess ph                                                             
  BS.hGetLine sout                                                              
  where                                                                         
    geo = (show r) ++ "x" ++ (show r)                                           
    size = r * r * 3                                                            
    command = "convert -define jpeg:size=" ++ geo                               
           ++ " -filter Cubic -resize " ++ geo ++ "! "                          
           ++ f ++ " PPM:- | tail -c " ++ (show size)                           
                                                                                
getFingerPrint4 = getFingerPrint 4                                              

insertItemは前回書いたままである。万が一、全ソースに興味がある方は GitHubをどうぞ。 早速実行して試してみよう。以下がサンプルで使った画像(の25%縮小版)。 オリジナル(IMG_0309.jpg)、その単純なコピー(IMG_0309-2.jpg)、 50%縮小版(IMG_0309-3.jpg)、25%縮小版(IMG_0309-4.jpg)を用意して試した。

f:id:eijian:20150307020747j:plain

結果は惨敗。

$ ghc -o picf Main.hs
[1 of 2] Compiling Finder           ( Finder.hs, Finder.o )
[2 of 2] Compiling Main             ( Main.hs, Main.o )
Linking picf ...
$ ./picf ~/work
probably same: work/IMG_0309.jpg, work/IMG_0309-2.jpg

同じサイズの画像しか同一とみなしていない。オリジナルと50%版のfingerprintを 比べてみる。

$ convert -define jpeg:size=4x4 -filter Cubic -resize 4x4! ~/work/IMG_0309.jpg PPM:- | tail -c 48 | od -x
0000000      4f47    5d52    6561    9996    bd9b    c6c1    3d3e    4d36
0000020      3b43    6171    7e59    7476    555d    5a40    3b4c    596f
0000040      7f47    5970    707a    7b4f    4f6e    7485    8c54    5a7c
0000060
$ convert -define jpeg:size=4x4 -filter Cubic -resize 4x4! ~/work/IMG_0309-3.jpg PPM:- | tail -c 48 | od -x
0000000      4f47    5d52    6561    9996    bd9b    c5c1    3d3e    4d37
0000020      3b43    6171    7e59    7476    555d    5a40    3b4c    596f
0000040      7f47    5970    707a    7b4e    4f6e    7485    8c54    5a7c
0000060

違う箇所が3つあるので3つの画素でRGBのうち一色だけ1/256の差があると。。。 本当に微妙な差だが、fingerprintが違うのだから「別の画像」ですね・・・。

さてどうしたものか。ちょっと考えてみて、結果は次回で報告。