💨

Common Lispでpng画像をグレースケールしたい

2024/09/29に公開

ライブラリ

slyrus/opticl

このライブラリでてっとり早くpng画像のI/Oが行える。

なお、"opticl"でググったら上位にPythonのライブラリがヒットするので注意。

opticlについて

opticlはCommon Lispの画像処理ライブラリで端的に言えば、画像ファイルを配列で読み込んで、配列で書き込めるライブラリだ。

その他ライブラリとしてはcl-imageがあるそうだが、私は比較など行わずにいきなりこちらを採用したのでよくわからなかった。

一応opticlにさらっと説明が書いてある。

In ch-image, images were represented by a set of CLOS classes which, in turn, either extended or wrapped classes from the CLEM matrix-processing library.

ch-imageはCLOSでガチャガチャやるやつらしい(超意訳)

また、CLEMとかいう行列ライブラリも関聯するようだ。

(slyrus/ch-image)[https://github.com/slyrus/ch-image]

(slyrus/clem)[https://github.com/slyrus/clem]

触っていないのでもしかするとch-imageを利用するほうが楽だったかもしれないが、今回これらについては以後触れないものとする。

そもそも私はCLOSはあまり好きではないのでこれらについては気が向いたら別記事で調査しようと思う。

変換処理

最終的なコード

結果的には以下のコードでグレースケール変換するようになる想定だ。

(opticl:write-png-file "bc_gray.png" (byte-list->array (color-list->gray-scale (png-file->dim-list "bc.png"))))

bc_gray.pngがグレースケール化したファイルで、bc.pngが元のファイルだ。

検証を行う場合は実際のパスに合わせて変更してほしい。

なお、bc.pngは以下を利用した。

bc.png

PNGファイル読み込み(png -> array -> list)

(defun gray-byte->list (two-d-bytes)
  (loop for i below (array-dimension two-d-bytes 0)
	collect (loop for j below (array-dimension two-d-bytes 1)
		      collect (aref two-d-bytes i j))))

(defun color-byte->list (three-d-bytes)
  (loop for i below (array-dimension three-d-bytes 0)
	collect (loop for j below (array-dimension three-d-bytes 1)
		      collect (loop for k below (array-dimension three-d-bytes 2)
				    collect (aref three-d-bytes i j k)))))

(defun png-file-array->dim-list (path)
  (let ((img-bytes (opticl:read-png-file path)))
    (cond ((eq (car (type-of img-bytes)) 'SIMPLE-ARRAY)
	   (let ((dim (length (nth 2 (type-of img-bytes)))))
	     (cond ((= 2 dim) (values (gray-byte->list img-bytes) 'gray))
		   ((= 3 dim) (values (color-byte->list img-bytes) 'color))
		   (t (error (format nil "{~S} bytes not support" path))))))
	  (t (error (format nil "{~S} read data is not n-d array" path))))))

png-file(array)から多次元リストへ変換する関数。

なお、読み込んだ際にN次元判定を行う。2次元ならグレー画像、3次元ならカラー画像という判定を行っている。

これはREADMEにも書いてある処理対応だ。

~ 中略
CL's array type itself enables us to store this metadata directly in a multidimensional array. We define a mapping between various image types and various specialized CL array types, such that, for instance, an 8-bit RGB array is represented by the type (SIMPLE-ARRAY (UNSIGNED-BYTE 8) (* * 3)). Any 3-dimensional simple-array with a third dimension of size 3 and an element-type of (unsigned-byte 8) will satisfy the conditions of being an 8-bit-rgb-image.
https://github.com/slyrus/opticl?tab=readme-ov-file#multi-dimensional-arrays より

8-bit RGB array is represented by the type (SIMPLE-ARRAY (UNSIGNED-BYTE 8) (* * 3)). Any 3-dimensional simple-array with a third dimension of size 3

つまり、8-bitなら3ってこと

replで確認してみた

;; グレースケールした画像
> (type-of (opticl:read-png-file "./bc_gray.png"))
(SIMPLE-ARRAY (UNSIGNED-BYTE 8) (1024 1024))
;; グレースケール前の画像
> (type-of (opticl:read-png-file "../bc.png"))
(SIMPLE-ARRAY (UNSIGNED-BYTE 8) (1024 1024 3))

つまり

色情報が輝度だけのグレー画像 #(1 2 3...)

8BitRGB画像 #((1 3 4) (2 2 2) ...)

と変換されるみたいだ。

そういったわけで、分岐により処理する関数を使い分けるようにしている。

(defun gray-byte->list (two-d-bytes)
  (loop for i below (array-dimension two-d-bytes 0)
	collect (loop for j below (array-dimension two-d-bytes 1)
		      collect (aref two-d-bytes i j))))

(defun color-byte->list (three-d-bytes)
  (loop for i below (array-dimension three-d-bytes 0)
	collect (loop for j below (array-dimension three-d-bytes 1)
		      collect (loop for k below (array-dimension three-d-bytes 2)
				    collect (aref three-d-bytes i j k)))))

グレースケール(list[1024 1024 3] -> list[1024 1024])

(defun color-list->gray-scale (color-list)
  (mapcar #'(lambda (x)
	      (mapcar #'(lambda (y)
			  (ceiling (float (/ (reduce #'+ y :initial-value 0)
					     (length y)))))
		      x))
	  color-list))

list[1024 1024 3]の3次元目の平均を取る。

ここらへんはあまりこだわりないので単純平均で算出する。

グレースケール化した画像の保存(list -> array -> png)

まずは配列化の画像から。

(defun byte-2d-list->array (byte-list byte-size)
  (let ((result (make-array `(,(length byte-list)
			      ,(length (car byte-list)))
			    :element-type `(unsigned-byte ,byte-size)
			    :initial-element 0)))
    (loop for i below (length byte-list)
	  for x in byte-list
	  do (loop for j below (length (car byte-list))
		   for y in x
		   do (setf (aref result i j) y)))
    result))

(defun byte-3d-list->array (byte-list byte-size)
  (let ((result (make-array `(,(length byte-list)
			      ,(length (car byte-list))
			      ,(length (car (car byte-list))))
			    :element-type `(unsigned-byte ,byte-size)
			    :initial-element 0)))
    (loop for i below (length byte-list)
	  for x in byte-list
	  do (loop for j below (length (car byte-list))
		   for y in x
		   do (loop for k below (length (car (car byte-list)))
			    for z in y
			    do (setf (aref result i j k)
				     z))))
    result))

(defun byte-list->array (byte-list &optional (byte-size 8))
  (cond ((and (eq (type-of byte-list) 'CONS)
	      (eq (type-of (car byte-list)) 'CONS)
	      (eq (type-of (car (car byte-list))) 'CONS))
	 (byte-3d-list->array byte-list byte-size))
	((and (eq (type-of byte-list) 'CONS)
	      (eq (type-of (car byte-list)) 'CONS))
	 (byte-2d-list->array byte-list byte-size))
	(t (error "Not 2-3d byte-list"))))

リストを配列化している。例によってbyte-list-arrayから構造を判定して2d,3dと呼びわけている。

そうしたはてに opticl:write-png-file にてファイルへの書き込みを行う。

;; 4. PNGファイルとして書き込む
(opticl:write-png-file
 "bc_gray.png"
 ;; 3. 配列に戻す
 (byte-list->array
		  ;; 2. グレースケールする
		     (color-list->gray-scale
		  ;; 1. リスト化する
		     (png-file->dim-list "../bc.png"))))

反省

list -> array -> png

は感覚的にフローのつもりで書いてみたけど、既視感あって何かと思えば

Haskellか。なおさら分かりづらくなった。

きっかけ

Common Lispでなにか遊ぼうと思ったときに比較的楽そうな課題としてPNGファイルのグレースケール化を思いついたのが動機。

思いついた当初は何をどうすればいいのかわからなかったが、そういえばと過去のコードを漁ってみたらすでに実装していたので、修正と解説を入れて記事にした。

実装したと書いてあるが、数年前のコードなのでもしかするとどこかからコピペしてきたものかもしれないが、ググっても出てこなかったのでもしかすると書籍のコードかもしれないと思っていろいろひっくり返してみたが、特に見つからなかったので大丈夫だろう...多分。

Discussion