5.4. ピクセルの読み書き

画像に対するほとんどの操作は、ピクセルごとの処理を単一のメソッド呼び出しの内部に隠しており、すべてのピクセルに触れるループはネイティブの速度で実行されます。しかし、アプリケーションのコードが特定のピクセルに直接触れたい場合もあります。たとえば、特定の位置にある値を読み取る、新しい値を書き込む、キャリブレーションのために1点をサンプリングする、既知の位置にある値をデバッグする、といった場合です。image モジュールは、こうしたレベルのアクセスを2種類のアドレス指定形式で提供しており、それぞれピクセルがどこにあるかという異なる考え方に対応しています。

5.4.1. 座標によるアドレス指定

最も自然な形式は、座標についての項ですでに語彙が確立されているもの、つまりピクセルをデカルト座標 (x, y) で指定する方法です。get_pixel()(x, y) を受け取ってその位置の値を返し、set_pixel() は同じ (x, y) と値を受け取ってそれを書き込みます。

これらの呼び出しが返す値や受け取る値は、画像のフォーマットによって異なります。グレースケール、バイナリ、Bayer の画像はピクセルごとに単一の値を持ちます。グレースケールでは明るさ、バイナリでは 0 または 1、Bayer では単一のカラーチャンネルのサンプルです。そのため get_pixel() は単一の整数を返します。RGB565 は3つのカラーチャンネルを16ビットにパックして格納しており、get_pixel はデフォルトでそれらを (r, g, b) のタプルに展開し、各チャンネルを 0255 の範囲にマッピングします。

デフォルトの動作はどちらの側でも反転できます。RGB565 画像で get_pixelrgbtuple=False を渡すと、生の16ビットのパックされたワードが返されます。これは線形インデックスが返すのと同じ形式であり、アプリケーションが同じパックされた値をそのまま書き戻す場合に効率的な形式です。単一チャンネルの画像に rgbtuple=True を渡すと逆のことが起こり、格納されている値が返却前に RGB888 のタプルに変換されます。Bayer 画像の場合はその場でデベイヤー処理が行われます。この引数は、呼び出し側のコードが、基となる画像がどのように格納していても、統一されたカラー空間でピクセルを取得できるようにするために存在します。

圧縮画像(JPEG と PNG)は get_pixelset_pixel ではサポートされていません。これらのバイト列は既知の位置のピクセルを表していないため、これらのメソッドは意味をなさない値を返す代わりにエラーを発生させます。

実際のパターンは次のようになります。

v = img.get_pixel(40, 30)            # grayscale: int 0..255
img.set_pixel(40, 30, 255)           # write white

r, g, b = img.get_pixel(40, 30)      # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0))   # write red

要求された (x, y) が画像の外側にある場合、get_pixelNone を返し、set_pixel は何もしません。これは設計上寛容な動作です。多くのアルゴリズムは画像の端の近くを走査し、範囲外の位置を一時的にインデックス参照することがあり、毎回例外を発生させるよりも静かに何もしないほうが処理を妨げないからです。

5.4.2. 線形インデックスによるアドレス指定

もう一つの形式は、基となるバッファ内での位置によってピクセルをアドレス指定する方法です。バッファのレイアウトを思い出してください。ピクセルは行ごとに格納されており、まず一番上の行のすべてのピクセル、次に次の行のすべてのピクセル、というように一番下まで続きます。この配置により、すべてのピクセルは左上の 0 から始まり、各行に沿って順に増加する単一の整数インデックスを持ちます。座標 (x, y) にあるピクセルの線形インデックスは y * width + x です。

4×3のセルのグリッド。各セルには左上の0から右下の11までの大きな線形インデックスが付され、その下に小さな (x, y) のタプルが添えられている。列は上部に x equals 0, 1, 2, 3 とラベル付けされ、行は左端に沿って y equals 0, 1, 2 とラベル付けされている。下のキャプションはその関係を示している。線形インデックスは y 掛ける width 足す x に等しい。

ピクセルはデカルト座標 (x, y) と、バッファを行ごとに左から右へ走査する線形インデックスの両方によってアドレス指定されます。

image モジュールは、このインデックスを通常の Python の添字記法を通じて公開しています。img[i] は線形インデックス i にあるピクセルを読み取り、img[i] = value は値を書き込みます。インデックス形式が返すのは、get_pixel() がデフォルトで返す展開済みのタプルではなく、そのフォーマットの 生の格納値 です。この違いが重要なのは、先に選択したフォーマットによって生の値がどのようなものになるかが決まるためです。

  • グレースケールと Bayer のピクセルは8ビット整数として返されます。

  • RGB565 と YUV422 のピクセルは16ビット整数、つまりパックされたワードとして返されます。

  • バイナリのピクセルは 0 または 1 として返されます。

  • JPEG と PNG のピクセルは8ビット整数として返され、圧縮ストリームのバイトを一度に1バイトずつ取得します。これらの値は不透明です。通常の意味でのピクセルではなく、圧縮されたエンコーディングの断片だからです。

インデックス形式は、すでにバッファのオフセットの観点で考えているコードに適しています。すべてのピクセルを一度走査するループ、一度に1行ずつジャンプする必要のあるアルゴリズム、バッファのレイアウト間を変換するコードなどです。x 座標と y 座標の観点で考えているコードには get_pixelset_pixel のほうが適しています。この2つの形式は、異なるメンタルモデルを通じて同じピクセルをアドレス指定します。

Image は反復可能でもあります。for v in img: は同じ行優先の順序でバッファを走査し、生の値を1ピクセルずつ生成します。len(img) は、非圧縮フォーマットの場合はピクセル数、圧縮ストリームの場合はバイト数です。

5.4.3. ピクセルごとの Python が遅い経路である理由

正直に述べておくべき実践的な注意点があります。Python から画像を1ピクセルずつ走査するのは 遅い です。320 × 240 のグレースケール画像には76,800個のピクセルが含まれます。それぞれに対して for ループ内で get_pixel() を呼び出すと、同等のネイティブメソッドなら数百マイクロ秒で完了できる処理を行うために、数百万個の MicroPython バイトコード命令が実行されます。これは小さな差ではありません。フレームをリアルタイムで処理するスクリプトと、カメラのフレームレートをはるかに下回る速度で這うように動くスクリプトとの違いに相当します。

Image の表面にあるほぼすべてのメソッドが存在するのは、よくあるピクセルごとのパターンに対して、より高速なネイティブ版があるからです。2つの画像を加算するループは単一のネイティブ呼び出しになります。各ピクセルを近傍と平均して平滑化するループは別の呼び出しになります。各ピクセルをしきい値に対して分類するループはさらに別の呼び出しになります。アプリケーションの役割は、ほとんどの場合、そのループが行おうとしていた処理に一致する画像全体のメソッドを見極め、自分でループを書く代わりにそれを使うことです。

ピクセル単位の読み書きは、他に適したものがない場合には依然として正しいツールです。たとえば、特定の測定値をバッファに書き戻す、キャリブレーションのために1つの位置をサンプリングする、既知の位置にある値をデバッグする、といった場合です。重要なのは、これらは遅い経路であり、画像全体のメソッドにアプリケーションが必要とする形式がない場合に使うものであって、ピクセルを操作するデフォルトの方法ではない、ということです。