LoginSignup
147

More than 5 years have passed since last update.

Swiftで画像をめちゃくちゃ簡単にあつかえるライブラリ

Last updated at Posted at 2015-05-18

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 でできること

Original.jpg

以下、変換後のサンプル画像が掲載されている項がありますが、オリジナルは↑の画像です。

ファイルからの読み込み

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

イテレーション

ImagePixelSequenceType なので、次のようにして全ピクセルをいてレートして取り出すことができます。

for pixel in image {
    ...
}

また、次のようにして、座標と一緒に取り出すことも可能です。

for (x, y, pixel) in image.enumerate() {
    ...
}

回転

RotateClockwise.jpg

rotate メソッドを引数なしで呼ぶと、時計回りに 90 度回転した画像が得られます。

let result = image.rotate() // 時計回り

RotateCounterClockwise.jpg

rotate メソッドに引数を渡すことで、逆回転したり 180 度、 270 度回転したりすることができます。

let result = image.rotate(-1) // 反時計回り

Rotate180.jpg

let result = image.rotate(2) // 180 度回転

反転

FlipHorizontal.jpg

let result = image.flipX() // 水平方向に反転

FlipVertical.jpg

let result = image.flipY() // 垂直方向に反転

リサイズ

Resize.jpg

let result = image.resize(width: 100, height: 100)
let result = image.resize(width: 100, height: 100,
    interpolationQuality: kCGInterpolationNone) // Nearest neighbor

切り出し

Crop.jpg

切り出しには、ピクセルのコピーコストは発生しません。

let resultOrNil = image[0..<100][0..<100] // はみ出したら `nil`

変換

Array と同じように、 map を使って画像を変換することができます。

グレースケール化

Grayscale.jpg

let result = image.map { (pixel: Pixel) -> Pixel in
    Pixel(gray: pixel.gray)
}
// Shortened form
let result = image.map { Pixel(gray: $0.gray) }

二値化

Binarize.jpg

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 }

二値化(自動閾値)

BinarizeAutoThreshold.jpg

let threshold = UInt8(image.reduce(0) { $0 + $1.grayInt } / image.count)
let result = image.map { $0.gray < threshold ? Pixel.black : Pixel.white }

平滑化フィルタの適用

MeanFilter.jpg

map と切り出しを併用してより複雑なフィルタをかけることもできます。切り出しにピクセルのコピーコストが発生しないため、このような処理もシンプルに書くことができます。

let result = image.map { x, y, pixel in
    image[(y - 1)...(y + 1)][(x - 1)...(x + 1)].map {
        Pixel.mean($0)
    } ?? pixel
}

ガウシアンフィルタの適用

GaussianFilter.jpg

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 がクラスではなく構造体な理由

EasyImagyImage はクラスではなく構造体です。

構造体(値型)ということは、代入の度に画像がまるごとコピーされるのかと思われるかもしれませんが、そんなことはありません。 ImageArray 同様に Copy-on-write が効いているため、本当に必要になるまでコピーが実行されることはありません。逆に、 Image がクラスだと次のようなケースで無駄なコピーが必要になります。

例えば、イミュータブルなクラスがイニシャライザで Image を受け取る場合を考えます。もし Image がクラスであれば、イミュータブル性を保証するためにイニシャライザの内部で受け取った Image オブジェクトをコピーして保持する必要があります(その Image オブジェクトが外部で変更されてしまうとイミュータブルでなくなってしまうため)。しかし、実際には渡されたオブジェクトは変更されないこともあり、コピーが無駄になってしまうケースが多いはずです。構造体 & Copy-on-write であればコピーは Lazy に実行されるので、本当にコピーが必要になったとき(渡された Image が外部でその後変更されたとき)までコピーが実行されることはありません。

画像の場合、データサイズが大きいことも多く、このコピーのコストはバカになりません。一方で、無駄な状態を避けメンテナンス性を上げるためには、イミュータブルにできるものはできる限りイミュータブルにしたい気持ちになります。そんなときに Image がクラスだと辛いことになります。僕が "SwiftのArrayが実はすばらしかった" を書いたときに思い浮かべていたのはそのようなケースで、 EasyImagy はそのすばらしさを体現したライブラリです。

ジェネリクスを採用しなかった理由

ImageImage<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.frameworkEmbedded Binaries に追加して下さい。

まとめ

EasyImagy を使えば Swift から簡単に画像のピクセルを操作することができるようになります。画像をあつかうのが面倒だと思ったときには是非試してみて下さい。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
147