1. 画像処理ライブラリの作成開始

Swift の学習のために、自分の研究用プログラムを Swift で書いてみる。ResearchTools というプロジェクト内に ImageLibrary という Framework を別途用意した。記録していないとすぐに忘れてしまうので、自分用の覚書を残しておく。

ImageLibraryTests の準備

ひとまず ImageLibraryTests.swift の冒頭はこうした。MonoMatrix がモノクロ用の画像クラス、ColorMatrix がカラー用の画像クラスである。ColorMatrix については今後さまざまな種類に対応させる予定だが、最初は RGB だけにする。カラー画像の種類は、ColorType という enum で変更できるようにする。

ここでは、今後のテストで使うサンプルデータをそれぞれ、mmat1, cmat1 として用意し、setUp で準備する。これらは setUp で作成し init 時には用意されないことから、Optional 型にしておく(Optional にしない場合、init で無駄な初期化が必要となる)。rnum, cnum はそれぞれ画像の行数・列数を設定するものである(今後 YUV422 や YUV420 が出てきた場合には、Y の大きさとする)。

import Cocoa
import XCTest
import ImageLibrary

class ImageLibraryTests: XCTestCase {
    private var mmat1 : MonoMatrix?
    private var cmat1 : ColorMatrix?

    override func setUp() {
        super.setUp()
        self.mmat1 = MonoMatrix(doubleBuffer: [ 3.0, 1.0, 4.0, 1.0, 5.0, 9.0 ], rnum: 2, cnum: 3)
        self.cmat1 = ColorMatrix(doubleBuffer:
            [
                [ 3.0, 1.0, 4.0, 1.0, 5.0, 9.0 ],
                [ 2.0, 6.0, 5.0, 3.0, 5.0, 8.0 ],
                [ 9.0, 7.0, 9.0, 3.0, 2.0, 3.0 ]
            ], rnum: 2, cnum: 3, type: ColorType.RGB)
    }

    override func tearDown() {
        super.tearDown()
    }
    以下は後述

MonoMatrix.swift の雛形

MonoMatrix.swift を用意する。とりあえず雛形だけ。

import Foundation

public class MonoMatrix {
    public init(doubleBuffer: [Double], rnum: Int, cnum: Int) {
    }
}

ColorMatrix.swift の雛形

ColorMatrix.swift を用意する。こちらもとりあえず雛形だけ。ColorType の enum も RGB だけ準備しておく。

import Foundation

public enum ColorType : Int {
    case RGB
}

public class ColorMatrix {
    public init(doubleBuffer: [[Double]], rnum: Int, cnum: Int, type: ColorType) {
    }
}

testMonoInitialize (ImageLibraryTests)

さっそく MonoMatrix の方の初期化のテストを記載する。mmat1 は Optional であるが、nil であることはあり得ないので、! でUnwrap している。以下のテストでも同様。また、ゼロ配列は結構便利に使うので、doubleBuffer を省略した初期化も用意用意する。この場合、配列はゼロ配列とする。ゼロ配列の作り方は忘れそうなので、テストの側でも書いておく。

ここでのテストのキモは Array は Class でなく Struct であること、また比較は要素の値が等しければ == が満足することにある。この結果、rnum, cnum を配列を使って同時にテストしたり、doubleBuffer の値を直接比較したりできる。

    func testMonoInitialize() {
        XCTAssertEqual([ mmat1!.rnum, mmat1!.cnum ], [ 2, 3 ])
        XCTAssertEqual(mmat1!.doubleBuffer, [ 3.0, 1.0, 4.0, 1.0, 5.0, 9.0 ])

        var zero = MonoMatrix(rnum: 2, cnum: 3)
        XCTAssertEqual([ zero.rnum, zero.cnum ], [ 2, 3 ])
        XCTAssertEqual(zero.doubleBuffer, [Double](count: 6, repeatedValue: 0.0))
    }

MonoMatrix の init (MonoMatrix)

このテストを追加させるために書いた MonoMatrix はこちら。Objective-C の頃の流儀で private な内部変数は _ 付とし、外部から必要な場合には computed property として参照する。_rnum, _cnum は初期化後に変更しないつもりなので、let で定義する。今回の場合は、単に内部変数を返しているだけだが、後々複雑な computed property も出てくる。さらに computed property で getter のみの場合には、get { } は省略でき、単に {} だけでよい。

doubleBuffer がない init は自動的にゼロ配列を準備して上記の init を呼ぶ。このように他の init に依存する init は convenience init と書く(忘れても Xcode が勝手に入れてくれる)。

public class MonoMatrix {
    private let _rnum, _cnum: Int
    private var _doubleBuffer: [Double]
    
    public var rnum: Int { return _rnum }
    public var cnum: Int { return _cnum }
    public var doubleBuffer : [Double] { return _doubleBuffer }

    public init(doubleBuffer: [Double], rnum: Int, cnum: Int) {
            _rnum = rnum
            _cnum = cnum
            _doubleBuffer = doubleBuffer
    }

    public convenience init(rnum: Int, cnum: Int) {
        self.init(doubleBuffer: [Double](count: rnum * cnum, repeatedValue: 0.0), rnum: rnum, cnum: cnum)
    }
}

testColorInitialize (ImageLibraryTests)

続いてカラーの方の初期化テスト。cmat1 については、rnum, cnum 以外にプレーン数を保持している pnum もテストする。ColorMatrix は MonoMatrix をプレーン数分確保することとし、mmats という配列を保持するようにする。テストでは、これらのバッファが指定したものになっているかを確認する。

ゼロ行列も便利なので準備しておく。MonoMatrix と異なりプレーン数は可変なので pnum で指示することとした。アルファチャネルがある場合なども考慮し、cmat1 とは異なり 4 枚でテストしている。for は range を使ってこんな感じにかける。最終値を含まない range は当初「..」だったが、「..<」に変更になった(Ruby と逆で紛らわしかったのでよい改変)。念のため、mmats が異なるオブジェクトになっていることも確認しておく。

    func testColorInitialize() {
        XCTAssertEqual([ cmat1!.rnum, cmat1!.cnum, cmat1!.pnum ], [ 2, 3, 3 ])
        var mmats : [MonoMatrix] = cmat1!.mmats
        XCTAssertEqual(mmats[0].doubleBuffer, [ 3.0, 1.0, 4.0, 1.0, 5.0, 9.0 ])
        XCTAssertEqual(mmats[1].doubleBuffer, [ 2.0, 6.0, 5.0, 3.0, 5.0, 8.0 ])
        XCTAssertEqual(mmats[2].doubleBuffer, [ 9.0, 7.0, 9.0, 3.0, 2.0, 3.0 ])
        
        var zero = ColorMatrix(pnum: 4, rnum: 2, cnum: 3, type: ColorType.RGB)
        XCTAssertEqual([ zero.rnum, zero.cnum, zero.pnum ], [ 2, 3, 4 ])
        mmats = zero.mmats
        let zeroBuffer = [Double](count: 6, repeatedValue: 0.0)
        for i in 0 ..< 4 {
            XCTAssertEqual(mmats[i].doubleBuffer, zeroBuffer)
        }
        XCTAssertFalse(mmats[0] === mmats[1])
    }

ColorMatrix の init (ColorMatrix)

修正した ColorMatrix はこちら。private インスタンスとしては、_mmats, _rnum, _cnum, _type を保持し、pnum は computed property で _mmats の count から計算する。これは、_mmats が可変になることも考慮したため。

YUV422 や YUV420 など保持する MonoMatrix のサイズが異なる場合も考慮し、主 init は mmats を入力としたものにした。テストで作成した Double の init はこれを呼び出す convenience init で用意する。Double から [MonoMatrix] への変換は map が使える。Ruby 使いからすると非常にわかりやすい。closure を真面目に書くのは面倒なので、ここでは $0 を使った省略形で書いた。

ゼロ行列を作る pnum を渡す init も convenience init で用意する。二次元のゼロ要素の Double を作ってもよかったが、省略しない closure の記述を忘れないように[Int] から [MonoMatrix] への map を書いてみた。ちなみに、

var mmats = [MonoMatrix](count: pnum, repeatedValue: MonoMatrix(rnum: rnum, cnum: cnum))

としたところ、見事に同じ MonoMatrix を持つ配列ができてテストに失敗したことが確認できた(当たり前)。

public class ColorMatrix {
    private var _mmats : [MonoMatrix]
    private var _rnum, _cnum: Int
    private var _type : ColorType

    public var mmats : [MonoMatrix] { return _mmats; }
    public var rnum : Int { return _rnum }
    public var cnum : Int { return _cnum }
    public var pnum : Int { return _mmats.count }
    
    public init(mmats: [MonoMatrix], rnum: Int, cnum: Int, type: ColorType) {
        _mmats = mmats
        _rnum = rnum
        _cnum = cnum
        _type = type
    }
    
    public convenience init(doubleBuffers: [[Double]], rnum: Int, cnum: Int, type: ColorType) {
        var mmats = doubleBuffers.map { MonoMatrix(doubleBuffer: $0, rnum: rnum, cnum: cnum) }
        self.init(mmats: mmats, rnum: rnum, cnum:cnum, type: type)
    }
    
    public convenience init(pnum: Int, rnum: Int, cnum: Int, type: ColorType) {
        var mmats = [Int](count: pnum, repeatedValue: 0).map { (Int) -> MonoMatrix in MonoMatrix(rnum: rnum, cnum: cnum) }
        self.init(mmats: mmats, rnum: rnum, cnum: cnum, type: type)
    }
}

ImageMatrix プロトコル (ImageMatrix)

この二つのクラスの共通部分を後々楽に書きたいので、ImageMatrix というプロトコルを準備してみる。今のところ共通部分は rnum と cnum プロパティに対応することなので、以下のように書いた。

public protocol ImageMatrix {
    var rnum : Int { get }
    var cnum : Int { get }
}

MonoMatrix を ImageMatrix プロトコルに対応するようにした。

public class MonoMatrix : ImageMatrix {

同様に ColorMatrix も ImageMatrix プロトコルに対応するようにした。

public class ColorMatrix : ImageMatrix {

長くなるのでこの辺で。