RMagickメモ

なんとなく書くことにしました。
画像は8x8 bit の画像ファイル z.png(32倍に拡大してます)↓ を使います。

読み込み

> require 'RMagick'
> img_z = Magick::ImageList.new('z.png')
> # または img_z = Magick::Image.read('z.png')

ImageとImageListの違い

ImageListはImageの集合体である。 複数のファイルを同時に読み込む場合があることや、GIFやTIFFなど1ファイルに複数の画像を含む画像フォーマットがあるため、このような仕組みになっている。

ImageとImageListクラスは密接に関連している。Imageオブジェクトは一枚の画像か、もしくは複数のフレームを持つ画像の一フレームをあらわす。(複数フレームを持つ画像の例としては、アニメーションGIFや、複数レイヤをもつPhotoshopイメージがある。)ImageオブジェクトはGIFやPNGJPEGなどの画像から生成できる。大きさを指定してスクラッチから画像を生成してもいい。画像はディスクに書き込んだり、スクリーンに表示したり、サイズや傾きを変更したり、フォーマットを変更したり、100を超えるメソッドを使ってその他いろいろ修正できる。
ImageListオブジェクトは画像のリストで、ゼロ個以上の画像とシーン番号を持つ。シーン番号は現在のイメージがどれかを示す。ImageListクラスはリストに含まれる全画像を操作するメソッドを持ち、例外もあるがImageクラスで定義される全てのメソッドも実行できる。Imageのメソッドは画像一つだけに有効なので、ImageのメソッドがImagelistに対して呼び出されたときは、シーン番号で示される現在の画像に渡される。
ImageListクラスはArrayクラスのサブクラスなので、ほとんどのArrayメソッドを利用してimagelistに含まれる画像を操作できる。例えば、<<メソッドを使ってリストに画像を追加できる。

画像情報取得


(なお、img = Magick::ImageList.new("img_1.jpg").firstが正しい)

画像を繋げる

とりあえずやりたかった一つとして、2つの画像を横に並べるというのがあります。実際にコードはこれ。

require 'rmagick' # require してライブラリを読み込み

img_append = Magick::ImageList.new("sample01.jpg","sample02.jpg")

img_append = img_append.append(false)
img_append.write("composite.jpg")

ハマった点としてはappendのところでのfalseとtrueの設定。

true 画像を上下に追加
false 画像を左右に追加

ちなみに ImageMagickconvert コマンドだと、上下に積み重ねるのが-append、右に繋げていくのが+append、のように -/+ で挙動を変えてたりします。

-append

Join current images vertically or horizontally.
This option creates a single longer image, by joining all the current images in sequence top-to-bottom. Use +append to stack images left-to-right.

メモリリーク対策(いまは要らないのかなあ…分からん)

RMagickはImageMagickのobj(mallocで確保した)を扱っていて、これはRubyのobjではありません。そのため、GCの対象にならず、メモリリークの危険性をはらむ事になります。

対策
  • RMagick 2.10.0(ImageMagick 6.5.3-10)で変更されたバージョンを使う
  • Magick::Image#destroy!を明示的に呼ぶ。
  • MiniMagick や MagickWand を使う

TL;DR for hurry people:

  • Call Magick::Image#destroy! (ensure block is a damn good idea)(destroy! メソッドを使う)
  • Use methods with exclamation mark as much as possible(出来るだけ破壊的メソッドを使う)

ファイルではなくオンメモリで画像処理

RMagickとImageMagickAPIを眺めていたら、from_blobとto_blobというAPIを使えばオンメモリで処理が可能だとわかった…変換元の画像がリモートに存在する場合や、出力先がファイルではなくデータベースである場合などで、中間ファイルを生成せずにRMagickによる画像処理を行う際に利用できる。

画素値の配列化

軽く検索した範囲だと Magick::Image#pixel_color でいちいち画素毎にデータを取得してループさせるひとが多いのだけど、それはいくら何でも手間だろう…しかも返されるのはMagick::Pixel クラスなので、RGB 値を取得するのにいちいちメソッドを使わないといけない。
調べてみると Magick::Image#export_pixels という複数画素取得&画素値の成分を配列にして出力してくれる、ずっとマシなメソッドがあることに気付きます。
ただし。このメソッドの出力は RGB しかも16 bit なので、そのまま出すとこうなります。

> img_z.export_pixels
=> [0, 65535, 0, 0, 65535, 0, 0, 65535, 0, 0, 65535, 0, 0, 65535, 0, 0, 65535, 0, 0, 65535, 0, 34695, 42662, 
11822, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 
65535, 65535, 51143, 51143, 65535, 5654, 5654, 65535, 46260, 46260, 65535, 65535, 65535, 65535, 65535, 
65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 50372, 50372, 65535, 5911, 5911, 65535, 47031, 
47031, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 50115, 
50115, 65535, 5911, 5911, 65535, 47288, 47288, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 
65535, 65535, 65535, 65535, 65535, 49601, 49601, 65535, 5397, 5397, 65535, 48059, 48059, 65535, 65535, 
65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 48830, 48830, 65535, 5140, 
5140, 65535, 49087, 49087, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 
65535, 65535, 48059, 48059, 65535, 5654, 5654, 65535, 49858, 49858, 65535, 65535, 65535, 65535, 65535, 
65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 23130, 4112, 46517, 0, 0, 65535, 0, 
0, 65535, 0, 0, 65535, 0, 0, 65535, 0, 0, 65535, 0, 0, 65535, 11565, 11565, 65535]

…少なくともRGBの3つを区切って、さらに8 bit にしないと分からないですよね。

> img_z.export_pixels.map { |pix| pix/257 }.each_slice(3).to_a
=> [[0, 255, 0],
 [0, 255, 0],
 [0, 255, 0],
 [0, 255, 0],
 [0, 255, 0],
 [0, 255, 0],
 [0, 255, 0],
 [135, 166, 46],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 199, 199],
 [255, 22, 22],
 [255, 180, 180],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 196, 196],
 [255, 23, 23],
 [255, 183, 183],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 195, 195],
 [255, 23, 23],
 [255, 184, 184],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 193, 193],
 [255, 21, 21],
 [255, 187, 187],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 190, 190],
 [255, 20, 20],
 [255, 191, 191],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 187, 187],
 [255, 22, 22],
 [255, 194, 194],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [255, 255, 255],
 [90, 16, 181],
 [0, 0, 255],
 [0, 0, 255],
 [0, 0, 255],
 [0, 0, 255],
 [0, 0, 255],
 [0, 0, 255],
 [45, 45, 255]]

もう一段階、X軸の繰り返しを区切れば理解しやすいですよね。

> img_z.export_pixels.map { |pix| pix/257 }.each_slice(3).each_slice(img_z.columns).to_a
=> [
  [[0, 255, 0], [0, 255, 0], [0, 255, 0], [0, 255, 0], [0, 255, 0], [0, 255, 0], [0, 255, 0], [135, 166, 46]],
  [[255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 199, 199], [255, 22, 22], [255, 180, 180]],
  [[255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 196, 196], [255, 23, 23], [255, 183, 183], [255, 255, 255]],
  [[255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 195, 195], [255, 23, 23], [255, 184, 184], [255, 255, 255], [255, 255, 255]],
  [[255, 255, 255], [255, 255, 255], [255, 193, 193], [255, 21, 21], [255, 187, 187], [255, 255, 255], [255, 255, 255], [255, 255, 255]],
  [[255, 255, 255], [255, 190, 190], [255, 20, 20], [255, 191, 191], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255]],
  [[255, 187, 187], [255, 22, 22], [255, 194, 194], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255]],
  [[90, 16, 181], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [45, 45, 255]]
]

画素情報を取得するメソッドとして最初に挙げた Magick::Image#pixel_color メソッドは出力が Magick::Pixel クラスなのですが、複数画素を取得出来る Magick::Image#get_pixels メソッドもあります。あとで扱うときに Magick::Pixel クラスが良いのであればこれでも良いでしょう。

Magick::Pixel クラスのメソッド

> ls Magick::Pixel
Magick::Pixel.methods: from_HSL  from_color  from_hsla
Magick::Pixel#methods: 
  <=>    black=  clone  dup   green   intensity  marshal_dump  opacity=  to_HSL    to_s   
  ===    blue    cyan   eql?  green=  magenta    marshal_load  red       to_color  yellow 
  black  blue=   cyan=  fcmp  hash    magenta=   opacity       red=      to_hsla   yellow=

redgreenbluecyanmagentayellowblack はそれぞれ RGB と CMYK の値が返るゲッターメソッドです。セッターメソッドはそれぞれの値をセットします。
intensity メソッドは輝度 Y を返します。YUV 色空間などの Y です。ちなみに RGB からの算出方法は

  • Y = 0.299 × R + 0.587 × G + 0.114 × B

です。
to_color メソッドは色名を返します。色名にならない場合は # を付けて画素値を返します。to_s メソッドと関連してますね。

> img_z.pixel_color(0, 0).to_color
=> "green"
> img_z.pixel_color(0, 0).to_s
=> "red=0, green=65535, blue=0, opacity=0"
> img_z.pixel_color(1, 7).to_color
=> "blue"
> img_z.pixel_color(1, 7).to_s
=> "red=0, green=0, blue=65535, opacity=0"
> img_z.pixel_color(6, 1).to_color
=> "#FFFF16161616"
> img_z.pixel_color(6, 1).to_s
=> "red=65535, green=5654, blue=5654, opacity=0"

グレースケールにする

RMagick 2.12.0: Common Tasks によると Magick::Image#quantize メソッドで2番目の引数(colorspace)に Magick::GRAYColorspace を渡せば良いようです。

> img_z_gray = img_z.quantize(256, Magick::GRAYColorspace)

なお1つ目の引数は量子化数(幾つ区切りにするか)なので、8bit = 256にしました。
出力するとこうなります(32倍に拡大)↓

ただし、 Magick::Image#export_pixels メソッドで画素値を配列化すると、デフォルトの RGB 出力でやはり 16bit のままなので

> img_z_gray.export_pixels
=> [46868,
 46868,
 46868,
 46868,
 46868,
 46868,
 …

となってしまいます。読めません。なのでやはり 8bit にした上で3つずつに区切りましょう。

> img_z_gray.export_pixels.map { |pix| pix/257 }.each_slice(3).each_slice(img_z_gray.columns).to_a
=> [
  [[182, 182, 182], [182, 182, 182], [182, 182, 182], [182, 182, 182], [182, 182, 182], [182, 182, 182], [182, 182, 182], [150, 150, 150]],
  [[255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [210, 210, 210], [71, 71, 71], [195, 195, 195]],
  [[255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [208, 208, 208], [71, 71, 71], [198, 198, 198], [255, 255, 255]],
  [[255, 255, 255], [255, 255, 255], [255, 255, 255], [207, 207, 207], [71, 71, 71], [199, 199, 199], [255, 255, 255], [255, 255, 255]],
  [[255, 255, 255], [255, 255, 255], [206, 206, 206], [70, 70, 70], [201, 201, 201], [255, 255, 255], [255, 255, 255], [255, 255, 255]],
  [[255, 255, 255], [203, 203, 203], [69, 69, 69], [204, 204, 204], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255]],
  [[201, 201, 201], [71, 71, 71], [206, 206, 206], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255]],
  [[43, 43, 43], [18, 18, 18], [18, 18, 18], [18, 18, 18], [18, 18, 18], [18, 18, 18], [18, 18, 18], [60, 60, 60]]
]

でも。RGB がいちいち同じ値で3つ並んでるのはどうかと思いますね。Magick::Image#export_pixels メソッドには map という出力向けパラメータがありますので、出力するのをRGBじゃなくグレースケール成分だけにしてみましょう。

> img_z_gray.export_pixels(0, 0, img_z_gray.columns, img_z_gray.rows, 'i').map { |pix| pix/257 }.each_slice(img_z_gray.columns).to_a
=> [
 [182, 182, 182, 182, 182, 182, 182, 150],
 [255, 255, 255, 255, 255, 210, 71, 195],
 [255, 255, 255, 255, 208, 71, 198, 255],
 [255, 255, 255, 207, 71, 199, 255, 255],
 [255, 255, 206, 70, 201, 255, 255, 255],
 [255, 203, 69, 204, 255, 255, 255, 255],
 [201, 71, 206, 255, 255, 255, 255, 255],
 [43, 18, 18, 18, 18, 18, 18, 60]
]

グレースケールの画像が不要なのであれば、上記の手順で元データのグレースケール成分を直接取得することが出来ます。

> img_z.export_pixels(0, 0, img_z.columns, img_z.rows, 'i').map { |pix| pix/257 }.each_slice(img_z.columns).to_a
=> [
 [182, 182, 182, 182, 182, 182, 182, 150],
 [255, 255, 255, 255, 255, 210, 71, 195],
 [255, 255, 255, 255, 208, 72, 198, 255],
 [255, 255, 255, 207, 72, 199, 255, 255],
 [255, 255, 206, 70, 201, 255, 255, 255],
 [255, 203, 69, 204, 255, 255, 255, 255],
 [201, 71, 206, 255, 255, 255, 255, 255],
 [43, 18, 18, 18, 18, 18, 18, 60]
]

なお map パラメータに指定できるのはこちらに記載がありました。引数には文字列で与えてください。

-map components

pixel map.
Here are the valid components of a map:

r
red pixel component
g
green pixel component
b
blue pixel component
a
alpha pixel component (0 is transparent)
o
opacity pixel component (0 is opaque)
i
grayscale intensity pixel component
c
cyan pixel component
m
magenta pixel component
y
yellow pixel component
k
black pixel component
p
pad component (always 0)

You can specify as many of these components as needed in any order (e.g. bgr). The components can repeat as well (e.g. rgbr).

おまけ:16bit を 8bit にするときになぜ257で割るのか

(分かりやすいように16進数表記にします)
8bit なので 00 から FF まで256段階です。等間隔で 16bit にするには、0000、0101、0202…FEFE、FFFF とすればいいことに気付くかと思います。つまり 16bit の0101=10進数の257の間隔で増加していきます。これが 8bit から 16bit への増やし方です。逆に257で割れば 16bit から 8bit にできます。