次ログ

次ログ

ゆるりと働いているSREの技術ブログのような何か。趣味の話も書く

NimでPNM画像を扱うライブラリを書いた

NimでPNM画像を扱うライブラリを書きました。

リポジトリは下記の通りです。

github.com

Qiita - Nimのパッケージリポジトリに自前のライブラリを追加する流れの手順を実施して すでにnimble install可能な状態です。

Qiitaにも書いたのですが 自作のライブラリなのでブログに載っけといたほうがいいようなのでブログに転記しました。

そもそもNimって?

Qiita - 至高の言語、Nimを始めるエンジニアへにNimとは何かが書かれています。 僕の感触としてはPythonの皮をかぶったRubyっぽいC言語です。

開発環境

  • Ubuntu18.10
  • Nim 0.19.4

PNMとは

以下のWikiに書かれている内容がわかりやすいです。

PNM (画像フォーマット) - Wikipedia

一応説明すると、2次元の数値の並びがそのまま画像として表示される画像フォーマットになります。

たとえば以下のテキストはPNMの1つです。

P1
5 5
0 0 1 0 0
0 1 1 0 0
0 0 1 0 0
0 0 1 0 0
0 1 1 1 0

これを画像ビューワで開くと、以下のようにレンダリングされます。

t.png

前述のテキストファイルは以下のような書式になっています。

  • 1行目に画像フォーマット名
  • 2行目に列数、行数
  • 3行目以降に画像のデータ

データ部分については、0が白色、1が黒色としてレンダリングされます。 PNMはテキストファイルだけ渡されても自力で脳内レンダリングしやすいフォーマットです。

前述の P1 というディスクリプタの画像は PBM という画像フォーマットになります。 PNM(Portable Anymap)は1つの画像フォーマットではなく、PBM、PGM, PPMの総称です。

PNMの画像フォーマットはWikiにも記載あるとおり6種類あります。

ディスクリプタ データフォーマット
P1 PBM (テキスト)
P2 PGM (テキスト)
P3 PPM (テキスト)
P4 PBM (バイナリ)
P5 PGM (バイナリ)
P6 PPM (バイナリ)

今回作成したpnmというライブラリでは、この6種類すべて扱えるように実装しました。

PBMの実装

PBM、PGM、PPMと書式があって、どれも簡単なフォーマットだったので実装には苦労しませんでした。 そのうち、一番めんどくさかったのは、PBM(P4)です。

バイナリ形式なのですが、バイナリデータのビットがそれぞれ画像のドットに対応するので 0, 1の数値データを8個ずつ切り出して1byteのデータに変換する必要がありました。

せっかくなので、この記事では一番面倒だったPBMの書き込みについてを取り扱います。

ライブラリの使い方

pnmライブラリを使ってPBM P4として画像出力するコード例をいかに示します。

import pnm

let col = 5
let row = 5
let data = @[
  0'u8, 0, 1, 0, 0,
  0,    1, 1, 0, 0,
  0,    0, 1, 0, 0,
  0,    0, 1, 0, 0,
  0,    1, 1, 1, 0,
]
let pbm = newPBM(pbmFileDiscriptorP4, col, row, data.toBin(5))
writePBMFile("1.pbm", pbm)

このコードを実行すると、前述のPNM画像ファイルが生成されます。

2進数のデータをbyteデータに変換する

dataは5行5列のデータです。 25個のデータですが、これをbyteデータに変換します。 期待値としては、以下のようなbyteデータにします。

0b0010_0000
0b0110_0000
0b0010_0000
0b0010_0000
0b0111_0000

newPBM(pbmFileDiscriptorP4, col, row, data.toBin(5)) でのtoBinはbyteデータへの変換をやっています。

5データずつ切り出して5bitのデータにする必要がありますが byteデータは8bitです。 byte型にするには3bit分たりないのですが、足りない分は左シフトします。

toBinは今回作成したライブラリのサブモジュール内のプロシージャの1つです。 toBinプロシージャの実装をいかに示します。

proc toBin*(arr: openArray[uint8], col: int =  8): seq[uint8] =
  ## Returns sequences that binary sequence is converted to uint8 every 8 bits.
  runnableExamples:
    doAssert @[1'u8, 1, 1, 1, 0, 0, 0, 0].toBin == @[0b1111_0000'u8]
    doAssert @[1'u8, 1, 1, 1, 1, 1].toBin == @[0b1111_1100'u8]
    var s: seq[uint8]
    doAssert s.toBin == s
  var data: uint8
  var i = 0
  for u in arr:
    data = data shl 1
    data += u
    i.inc
    if i mod 8 == 0:
      result.add data
      data = 0'u8
      continue
    if i mod col == 0:
      data = data shl (8 - (i mod 8))
      result.add data
      data = 0'u8
      i = 0
  if data != 0:
    result.add data shl (8 - (i mod 8))

arrから1つずつデータを取り出して加算して左シフトを繰り返し、 1byte分データが加算されたらresultに追加を繰り返すような実装です。

Nimでは##をプロシージャ内に書くとドキュメンテーションコメントとして扱われます。 ライブラリとして公開するためにドキュメントも必要と思ったので書いています。

runnableExamplesドキュメンテーションコメントの1つです。 nim docでドキュメントを生成する際に、runnableExamplesのブロックのコードを実際にコンパイルして実行して コードが実行可能であることを検証してくれます。また、このブロックにかかれているコードもドキュメントに含まれます。

このソースコードから以下のドキュメントが生成されます。 https://jiro4989.github.io/pnm/util.html#toBin%2CopenArray%5Buint8%5D%2Cint

ファイル出力

writePBMFileではnewPBM()で生成した構造体をbyteデータに変換してファイル出力します。 byteデータへの変換は以下のような実装になっています。 データ構造は単純で、それぞれのデータをuint8型に変換しているだけです。

proc formatP4*(self: PBM): seq[uint8] =
  ## Return formatted byte data for PBM P4.
  runnableExamples:
    let p4 = newPBM(pbmFileDiscriptorP4, 1, 1, @[0b1000_0000'u8])
    doAssert p4.formatP4 == @[
      'P'.uint8, '4'.uint8, '\n'.uint8,
      '1'.uint8, ' '.uint8, '1'.uint8, '\n'.uint8,
      0b10000000'u8,
    ]
  # header part
  # -----------
  # file discriptor
  result.add self.fileDiscriptor.mapIt(it.uint8)
  result.add '\n'.uint8
  # col and row
  result.add self.col.`$`.mapIt(it.uint8)
  result.add ' '.uint8
  result.add self.row.`$`.mapIt(it.uint8)
  result.add '\n'.uint8
  # data part
  # ---------
  result.add self.data

書き込みをしている箇所はこれだけ。特に凝ったことはしていません。

let bin = data.formatP4
discard f.writeBytes(bin, 0, bin.len)

まとめ

PNMを扱うためのライブラリの使い方と、その実装の一部について説明しました。

PNMはPNGなどの一般的な画像フォーマットと比べると、非常に簡単な書式なので、実装の練習としては有用です。 僕の場合は、ビット演算を今までほとんどやったことなかったのですが、PBM P4の実装を通してビット演算の理解が深まりました。

あと実装についてですが、メソッドチェーンで処理をどんどんつないで コードをかけるのが面白いな、と感じました。

特にsequtilsstrutilsのモジュールには強力なものがたくさんあるので 少ないコード量でスイスイ実装を進められたと感じています。 また、スライスの値比較とかも普通に==でできますし、構造体のポインタ型のデータの値比較も 変数名の後に[]と書くだけで値型として扱えるのでテストコードも書きやすいです。

コード量はテストコード込で以下のようになりました。

-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Nim                              4            105            402           1075
-------------------------------------------------------------------------------
TOTAL                            4            105            402           1075
-------------------------------------------------------------------------------

しかしながら、後からコードを見直したときに、1行の情報量が増えやすく 気を抜くとすぐに可読性が低下しそうな危うさも感じました。 普段良く使ってるGoと比べると可読性は低くなりやすいと感じます。

以上です。