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

Haskellでいってみよう

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

同一画像検索(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に複数の要素が入っている。

次回は、上記の確認を踏まえて同一画像を判定する簡易な関数を作ってみる。