Swift で画像のピクセルを操作しようとすると面倒ですよね?大抵 CoreGraphics を使うことになるわけですが、 API は C で書かれていて古いし、フォーマットは色々あるし、メモリマネジメントは苦痛だし、画像のピクセルにちょっとアクセスしたいだけなのにうんざりしてしまいます。
そこで、 Swift で画像を簡単にあつかえるようにするライブラリ EasyImagy を作りました。
var image = Image(named: "ImageName")!
// ピクセルのアクセス
println(image[y][x]) // `image[x, y]` も可
image[y][x] = Pixel(red: 255, green: 0, blue: 0, alpha: 255)
// 全ピクセルをイテレート
for pixel in image {
...
}
// 画像の変換(下記は二値化の例)
let binarized = image.map { $0.gray < 128 ? Pixel.black : Pixel.white }
// `UIImage` との相互変換
image = Image(UIImage: imageView.image!)
imageView.image = image.UIImage
EasyImagy でできること
以下、変換後のサンプル画像が掲載されている項がありますが、オリジナルは↑の画像です。
ファイルからの読み込み
UIImage
のようにファイル名を指定するだけで main bundle からインスタンスを生成することができます。
let image = Image(named: "ImageName")
もちろん、パスを指定して読み込むこともできます。
let image = Image(contentsOfFile: "path/to/file")
EasyImage で読み込んだ画像はすべて RGBA 形式に展開されます。各ピクセルは次のようなシンプルな構造体 Pixel
で表されます。
struct Pixel {
var red: UInt8
var green: UInt8
var blue: UInt8
var alpha: UInt8
}
UIImage との相互変換
いくら簡単に画像をあつかうことができても、 UIImage
からインスタンスを生成したり、最終的に UIImage
に変換できないと意味がないことも多いと思います。
次のように、 UIImage
との相互変換も簡単に実現できます。
let image = Image(UIImage: imageView.image!)
imageView.image = image.UIImage
ピクセルへのアクセス
Subscript ( [ ]
)を使ってピクセルにアクセスすることができます。
指定した座標が画像からはみでてしまった場合は nil
が返されます。クラッシュしたり、変な結果が返ってきたり( pixels[y * width + x]
で計算するとありがち)することはないので安全です。
if let pixel = image[y][x] { // `image[x, y]` も可
println(pixel.red)
println(pixel.green)
println(pixel.blue)
println(pixel.alpha)
println(pixel.gray) // (red + green + blue) / 3
println(pixel) // "#FF0000FF" のようにフォーマットされる
} else {
// はみ出した場合
println("Out of bounds")
}
上記のように、 Pixel
には gray
のような便利プロパティも用意されています( Computed Property なので、あくまで Pixel
のサイズは 4 バイトの構造体です)。
Image
は構造体なので、 var
で宣言されていれば Subscript を使ってピクセルを更新することができます。
image[y][x] = Pixel(red: 255, green: 0, blue: 0, alpha: 255)
image[y][x]?.alpha = 127
イテレーション
Image
は Pixel
の SequenceType
なので、次のようにして全ピクセルをいてレートして取り出すことができます。
for pixel in image {
...
}
また、次のようにして、座標と一緒に取り出すことも可能です。
for (x, y, pixel) in image.enumerate() {
...
}
回転
rotate
メソッドを引数なしで呼ぶと、時計回りに 90 度回転した画像が得られます。
let result = image.rotate() // 時計回り
rotate
メソッドに引数を渡すことで、逆回転したり 180 度、 270 度回転したりすることができます。
let result = image.rotate(-1) // 反時計回り
let result = image.rotate(2) // 180 度回転
反転
let result = image.flipX() // 水平方向に反転
let result = image.flipY() // 垂直方向に反転
リサイズ
let result = image.resize(width: 100, height: 100)
let result = image.resize(width: 100, height: 100,
interpolationQuality: kCGInterpolationNone) // Nearest neighbor
切り出し
切り出しには、ピクセルのコピーコストは発生しません。
let resultOrNil = image[0..<100][0..<100] // はみ出したら `nil`
変換
Array
と同じように、 map
を使って画像を変換することができます。
グレースケール化
let result = image.map { (pixel: Pixel) -> Pixel in
Pixel(gray: pixel.gray)
}
// Shortened form
let result = image.map { Pixel(gray: $0.gray) }
二値化
let result = image.map { (pixel: Pixel) -> Pixel in
pixel.gray < 128 ? Pixel.black : Pixel.white
}
// Shortened form
let result = image.map { $0.gray < 128 ? Pixel.black : Pixel.white }
二値化(自動閾値)
let threshold = UInt8(image.reduce(0) { $0 + $1.grayInt } / image.count)
let result = image.map { $0.gray < threshold ? Pixel.black : Pixel.white }
平滑化フィルタの適用
map
と切り出しを併用してより複雑なフィルタをかけることもできます。切り出しにピクセルのコピーコストが発生しないため、このような処理もシンプルに書くことができます。
let result = image.map { x, y, pixel in
image[(y - 1)...(y + 1)][(x - 1)...(x + 1)].map {
Pixel.mean($0)
} ?? pixel
}
ガウシアンフィルタの適用
let weights = [
1, 4, 6, 4, 1,
4, 16, 24, 16, 4,
6, 24, 36, 24, 6,
4, 16, 24, 16, 4,
1, 4, 6, 4, 1,
]
let result = image.map { x, y, pixel in
image[(y - 2)...(y + 2)][(x - 2)...(x + 2)].map {
Pixel.weightedMean(zip(weights, $0))
} ?? pixel
}
EasyImagy の Image がクラスではなく構造体な理由
EasyImagy の Image
はクラスではなく構造体です。
構造体(値型)ということは、代入の度に画像がまるごとコピーされるのかと思われるかもしれませんが、そんなことはありません。 Image
は Array
同様に Copy-on-write が効いているため、本当に必要になるまでコピーが実行されることはありません。逆に、 Image
がクラスだと次のようなケースで無駄なコピーが必要になります。
例えば、イミュータブルなクラスがイニシャライザで Image
を受け取る場合を考えます。もし Image
がクラスであれば、イミュータブル性を保証するためにイニシャライザの内部で受け取った Image
オブジェクトをコピーして保持する必要があります(その Image
オブジェクトが外部で変更されてしまうとイミュータブルでなくなってしまうため)。しかし、実際には渡されたオブジェクトは変更されないこともあり、コピーが無駄になってしまうケースが多いはずです。構造体 & Copy-on-write であればコピーは Lazy に実行されるので、本当にコピーが必要になったとき(渡された Image
が外部でその後変更されたとき)までコピーが実行されることはありません。
画像の場合、データサイズが大きいことも多く、このコピーのコストはバカになりません。一方で、無駄な状態を避けメンテナンス性を上げるためには、イミュータブルにできるものはできる限りイミュータブルにしたい気持ちになります。そんなときに Image
がクラスだと辛いことになります。僕が "SwiftのArrayが実はすばらしかった" を書いたときに思い浮かべていたのはそのようなケースで、 EasyImagy はそのすばらしさを体現したライブラリです。
ジェネリクスを採用しなかった理由
Image
を Image<T>
というジェネリックな構造体にして、 RGBA の画像は Image<Pixel>
、グレースケール画像は Image<UInt8>
で表すという案もありました。
しかし、実際に実装してみたところ最適化がうまくいかないらしく著しくパフォーマンスが悪かったので、リリースバージョンとしては採用を見送りました。
あとは、今の Swift (1.2) では extension Image<Pixel>
のようなことができないので、黒魔術を使いまくって実装したという微妙さもあります(例えば、 UIImage
プロパティは任意の T
に対して実装することはできないので、 extension Image<Pixel|UInt8>
のようなものがほしくなります)。
ジェネリックな Image
ができれば Image<Int>
で Integral Image にするとか、 flatMap
でリサイズとか夢が広がるのですが・・・。
インストール
Carthage
Carthage に対応しています。 Carthage をご利用の場合は Cartfile に次の 1 行を足して下さい。
github "koher/EasyImagy" >= 0.1.0
手作業
EasyImagy.xcodeproj
をプロジェクトまたはワークスペースに追加し、 EasyImagy.framework
を Embedded Binaries に追加して下さい。
まとめ
EasyImagy を使えば Swift から簡単に画像のピクセルを操作することができるようになります。画像をあつかうのが面倒だと思ったときには是非試してみて下さい。