Pixel iterator(vector 版) - Rubyist の Python 学習記録(9)

PythonLibrary Python NumPy 2019年 1月 9日

ピクセルごとにアクセスをするイテレータのテストを行いたい. 例として,整数ベースのヒストグラムを計算する generate_with_int_hist のテストを行う.

ピクセルベースのイテレータのテスト(vector 版)

  • tests/test_mono_image.py に generate_with_int_hist のテストを記述する. まずは MonoImage がベクトルの場合のテストである. obj.generate_with_int_hist は 2^obj.bits の長さのヒストグラムを作成する. 今回のテストでは,1が2個,3が1個,4が1個,5が1個,9が1個あるかどうか確認している.
  • with such.A('MonoImage class') as it:
        (省略)
        @it.should('generate integer histogram')
        def test():
            correct = MonoImage.create_with_array([0, 2, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], np.uint32, 32);
    
            ### vector test
            vec = MonoImage.create_with_array([3, 1, 4, 1, 5, 9], np.uint8, 4);
            it.assertEqual(vec.generate_with_int_hist(), correct)

self.generate_with_int_hist() の実装

  • self.generate_with_int_hist は 2^obj.bits の長さの零ベクトルを作成し,画素ごとに画素値の値を一つ足していく.
  • def generate_with_int_hist(self):
        ans = MonoImage.create_with_zeros(self.max_value+1, np.uint32, 32)
        for pi in self.pixel_iterator():
            ans.inc_value_at(pi.value)
        return ans
  • 記述は簡単だが何をやっているのかわかりにくいので,細かくメモを残しておく.
    • 最初に結果を入れるベクトルを用意する.長さは self.max_value+1(=2^self.bits),要素はピクセル数を保持するために np.uint32,bits は 32とする.
    • ans = MonoImage.create_with_zeros(self.max_value+1, np.uint32, self.bits)
    • 次がイテレータである.今回は,MonoImage は pixel iterator と block iterator の複数種類のイテレータを持つため,MonoImage 自体がイテレータにはならない. そこで,MonoImage に関連するイテレータオブジェクトを新規に作成することにした. self.pixel_iterator() はイテレータオブジェクトを作成するメソッドである. このオブジェクトに対して,イテレータが発動する(for 文内などで使われる)と,self.__iter__ が発火する. 今回は,self.__iter__ に対して generator function を返すこととする. この generator function はアクセスごとに,イテレータ自体を返す. 最初は値とイテレータのタプルを返していたのだが,イテレータの setter と getter を使った方がループ内のプログラムが書きやすいことに気づいたのでイテレータのみを返すことにした.
    • for pi in self.pixel_iterator():
    • イテレータが回ってしまえば,ヒストグラムはその画素値の数値を +1 するだけである. そこで,self.inc_value_at(v) というメソッドに任せることにした. 画素値はイテレータの value という getter で取得する.
    • ans.inc_value_at(pi.value)
    • 最後に作成したベクトルを返せば終わりである.
    • return ans

self.pixel_iterator() の実装

  • self.pixel_iterator() は self を渡して MonoImagePixelIterator クラスのオブジェクトを一つ作成するだけである.
  • def pixel_iterator(self):
        return MonoImagePixelIterator(self)

MonoImagePixelIterator のコンストラクタ

  • MonoImagePixelIterator は MonoImage に関連するので,mono_image.pl の中に記述することにした. Python は import が面倒になるためか,一つのファイル(モジュール)に複数のクラスなどを記述することが多いようだ. 今回はそれに従った.最後の _f, _getter, _setter の部分はキモなので,詳細は後述する(また後で書き換える). 後で,matrix ベースの pixel_iterator に対応するため,_width と _height の両方を保持しておく.
  • class MonoImage:
        (中略: ここに MonoImage のクラスの内容が記述されている)
    
    class MonoImagePixelIterator:
        def __init__(self, mono_image):
            self._mono_image = mono_image
            self._width = mono_image.width
            self._height = mono_image.height
            self._f = self.vector_iterator_func
            self._getter = self.vector_getter_func
            self._setter = self.vector_setter_func

MonoImagePixelIterator の __iter__ メソッド

  • 単純にイテレータを generator で作成してしまうと, イテレータが発火すると,__iter__ メソッドが呼ばれる. ここでネックなのが,単なる generator function では一つのオブジェクトに対して,一回しかループが発動しないことである(参考: Python のジェネレータを何回もイテレートしたい ). ここで紹介されたように,イテレータが発火するときに呼ばる __iter__ で generator function を新規に返すようにすることで,複数回のイテレータ呼び出しに対応できる. このため,__iter__ ではコンストラクタで登録した _f を呼び出すようにした.
    class MonoImagePixelIterator:
        (中略)
        def __iter__(self):
            return self._f()

MonoImagePixelIterator の vector_iterator_func メソッド

  • ここまでの仕組みができてしまえば,vector_iterator_func という generator function を作成すればよい. このメソッドでは self._nowi という現在位置を保持するインスタンス変数を用意し,その位置の値を返す. Python の generator function の面白いところは,yield で処理を切り上げ元の関数に戻るところである. 次回 generator function が呼ばれた場合,その続きから処理が再開される. self._nowi を更新し,最後かどうか確認する処理は yield の先に書かれている. 今回はベクトルなので self._width と等しいかどうか判断し,等しければ self._nowi を -1 とする. while 文で self._nowi が -1 かどうかを判断しているので,ループが終了すれば yield は呼ばれず,呼び出し元にループ終了が伝えられる.
  • class MonoImagePixelIterator:
        (中略)
        def vector_iterator_func(self):
            self._nowi = 0
            while (self._nowi != -1):
                yield self
                self._nowi += 1
                if self._nowi == self._width:
                    self._nowi = -1

MonoImagePixelIterator のプロパティ

  • MonoImagePixelIterator は value という getter と setter を持つ. すでに getter については, computed property に記載している.setter は @value.setter というデコレータを付与した value メソッドを作ればよい. getter と setter は,それぞれ __init__ で準備した vector_getter_func と vector_setter_func を間接的に呼び出すことにする.
  • class MonoImagePixelIterator:
        (中略)
        @property
        def value(self):
            return self._getter()
    
        @value.setter
        def value(self, v):
            self._setter(v)
  • vector_getter_func は _nowi の位置の値を単に返す.
  • def vector_getter_func(self):
        return self._mono_image._array[self._nowi]
  • vector_setter_func は _nowi の位置のスライスに指定された値を書き込む. NumPy のスライスはビューであると同時に,値を書き込むと元のベクトルの要素に書き込んでくれる.
  • def vector_setter_func(self, v):
        self._mono_image._array[self._nowi] = v

MonoImage への inc_value_at の追加実装

ここまでの実装で MonoImage クラスに inc_value_at というインスタンスメソッドが必要となった. 通常はテストを書いてから実装をするのだが,今回の一連のテストが通過すれば十分このメソッドのテストをしていることになるため,直接のテストは省略する.

  • inc_value_at は pos の場所の値を +1 する.vector_setter_func の時と同様,スライスの値を更新することで,元の ndarray のベクトル値が更新されていることになる.
  • class MonoImage:
        (中略)
        def inc_value_at(self, pos):
            self._array[pos] += 1

複数回呼び出しの確認

  • 先に説明した複数回のジェネレータ呼び出しが実現可能か確認するために,一番最初に書いたテストで同じメソッドをもう一度呼んだときに同じ値を返せるかを確認した. このテストが通過することで,複数回のループ呼び出しが実現できていることを確認した.
  • it.assertEqual(vec.generate_with_int_hist(), correct)
    # Check if the method is called twice
    it.assertEqual(vec.generate_with_int_hist(), correct)

行列版も描こうとしたが長くなったので今日はここまで.次回はこれを行列に対応させる.