PGMファイルへの画像書き出し

車輪の再発明を続けています。今日は画像の書き出しです。

スペクトログラムを計算できるようになったものの、それを画面表示するためにはなんらかの画像にしないといけないんですね。画面にウィンドウを作って表示するよりもファイルにするほうが簡単なので、ファイル保存の方法を考えます。

現在ひろく使われている画像のファイルフォーマットにはJPEGPNGがありますが、ほかにも様々なフォーマットがあります。中でも最も単純なのはPNM形式(PNM (画像フォーマット) - Wikipedia)でしょう。白黒・グレースケール・カラーの3種類と、バイナリ版とテキスト版の2種類の保存形式を組み合わせた6種類のフォーマットに分かれますが、いずれも生データを無圧縮で保存するだけの簡単構造です。CSV形式で書き出すのとそれほど変わりません。

今回はスペクトログラムを書き出すので、グレースケールをASCIIで保存します。グレースケールなのでP2形式のPGMファイルということになります(白黒はPBM、グレースケールはPGM、カラーはPPMで、それらをまとめてPNMと呼んでいます)。P2形式では、1行目にP2というマジックナンバー、2行目に横縦のピクセル数、3行目に明度の最大値、4行目以降にスペースで区切ったピクセル値をべた書きするだけです。

(defun write-pgm (filename arry)
  "Write an array as PGM graphics file. Values are normalized to 8-bit grayscale."
  (let* ((nrow (array-dimension arry 0))
         (ncol (array-dimension arry 1))
         (vmin (loop for n from 0 to (1- (array-total-size arry))
                     minimize (row-major-aref arry n)))
         (vmax (loop for n from 0 to (1- (array-total-size arry))
                     maximize (row-major-aref arry n))))
    (with-open-file (fid filename :direction :output :if-exists :supersede)
      (format fid "P2~%")
      (format fid "~A ~A~%" ncol nrow)
      (format fid "255~%")
      (dotimes (r nrow)
        (dotimes (c ncol)
          (format fid "~D " (truncate (* 255 (/ (- (aref arry r c) vmin)
                                                (- vmax vmin))))))
        (format fid "~%")))))

使ってみます。振幅スペクトルはそのままだと振幅の大きいところと小さいところの差が広すぎるので、対数を計算してあげています。

(let* ((snd (wavread "guitar.wav"))
       (x (first snd))
       (fs (second snd))
       (spec (first (spectrogram x fs))))
  (write-pgm "output.pgm" (array-map #'log spec)))

無圧縮のテキストファイルなので、ざっくり1000×500ピクセルくらいで2 MBとかになってしまいますが(PNGにすると300 KBくらい)、スペクトログラムを画像として出力することができるようになりました。無駄に車輪の再発明をしましたが、そういえば画像ライブラリとか使えばよかったですね。