NumPyを画像処理に使う方法

2020年08月12日

本記事では、ブロードキャスト、ベクトル化、高度なインデックス作成などの概念を活用し、NumPy(ナムパイ)を用いて画像処理問題を解く方法を探ります。また、適切なタスクに適切なNumPy関数を使用すれば、非効率な基本的手法よりはるかにパフォーマンスを改善できることも明らかにします。

 

NumPyとは

NumPyとは、大量の配列演算を行うために最適化された機能を提供する優れた機械学習ライブラリです。単純なタスクではかなり簡単にNumPyの機能を最大限に活用できますが、特定の機能をアプリケーションに最適に実装する方法がすぐに見つけられない場合もあります。

こちらでは、上記の概念についてご紹介することは控え、それらがなぜ重要であり、実際どのように利用できるのかについてご説明したいと思います。そのためにいくつかトイプロブレムを設計したので、これから解決方法を見ていきましょう。また、これらのトイプロブレムに利用した最適な手法は、セマンティックセグメンテーションや物体検出など、コンピュータビジョンの実用的アプリケーション用のデータ処理で直面する問題にも容易に拡張できます。

上記の概念について知識があやふやであっても心配は要りません。理解に役立つように、図や文章による説明をたくさん加えておきました。ブラッシュアップが必要な場合は、多くの参考文献も利用できます。

 

では、トイプロブレムを定義することから始めましょう。

画像処理にNumpyを利用する方法を探るため、二つのトイプロブレムを解いていきます。

 

課題① 完全置換

図1の入力画像に対して[255, 0, 0]の色の全てのピクセルを[255, 255, 255]色に変換します。

※ この記事の全ての色は [R, G, B] 値で表されます。

図1

 

課題② 条件付き置換

図2の入力画像に対して、特定のボックス内の[255, 0, 0]色の全てのピクセルを[255, 255, 255]色に変換します。ボックスは図2(中央)で、左上と右下の座標を示す星印を用いて視覚化されています。

図2

図からおわかりのように、条件付き置換は完全置換を拡張したものです。

一見したところ、問題は簡単そうに見えるかもしれません。しかし、中核となる問題はシンプルですが、実装のプロセスを考慮すると、Pythonループを利用してピクセルの反復処理を行うより、NumPyを利用してベクトル化する方が一般的に望ましいことがおわかりいただけるでしょう。

そこで、次に、これらの問題の解決方法について見ていきましょう。ひとつ目のソリューションは、シンプルなPythonループを利用してピクセルの反復処理を行い、問題を解決する方法、もう一つはNumPyの関数を利用する方法です。それぞれのソリューションを実行して、問題解決にかかる時間を比較します。

また、様々なサイズ(または縮尺)の画像を使用して、各手法の所要時間を測定します。こうすると、最適な手法による高速化率が画像の縮尺によってどのように異なるのかが明らかになります。どの問題の入力画像も五種類の縮尺(またはサイズ)から成り、各画像とも高さは幅の半分です。そのため、どの縮尺の画像も幅だけで表示することにします。五種類の縮尺の画像の幅はそれぞれ、2000ピクセル、1000ピクセル、500ピクセル、250ピクセル、100ピクセルです。

※ 画像は元々、幅2000ピクセル、高さ1000ピクセルで作成され、アスペクト比を維持しながら他の縮尺にサイズ変更しました。

こちらに示す全ての所要時間の比較とプロットは、このColabノートブックの時間計測に基づいています。上記のノートブックのリンクまたはこちらのGitHubリポジトリをクローンして指示に従えば、お手持ちのデバイスでこの実験を再現できます。 

※ 測定は2020年8月2日、GitHubリポジトリで言及されているColabノートブックで行われました。正確な所要時間や高速化率はソフトウェアやハードウェアのバージョンなど、いくつかの要素によって異なります。CPU使用率やメモリ使用量などの外的要因に影響を受ける場合もあります。そのため、これらの測定値は、パフォーマンスの違いのおおよその感じを捉えるためだけにご利用ください。使用されたソフトウェアのバージョンに関する詳細はColabノートブックをご覧ください。

問題の設定が完了したので実験を始めましょう。

 

ソリューション① 完全置換
図1の入力画像をより詳しく分析してみましょう。下の図3には問題の理解に役立つよう、アノテーションが含まれています。

入力画像はimgという名前の3DのNumPy配列で表されます。これは[H, W, 3]という形状の配列ですが、まず、最初の二つの次元(高さと幅)だけに注目しましょう。

行インデックスをr、列インデックスをcで示します。図3からrは[0, H-1](両端を含む)の範囲のいずれかの整数値をとることができcは[0, W-1](両端を含む)の範囲のいずれかの整数値をとることがわかります。画像内の各ピクセルは(r, c)という座標で示せます。また<code.img[r, c, :]は(r, c)における画像のRGB値を表します。

図3

これによって、この問題を解くために利用できる変数やトリックについてある程度イメージがつかめるのではないでしょうか。では、さっそく、いくつかソリューションを見てみましょう。

 

method 1. 簡単なPythonのfor文によるループ

この問題にはもちろん、シンプルなfor文によるループ処理を適用することができます。画像をNumPy配列imgに読み込めば、以下のコードブロックがソリューションを提供します。

 

output = img.copy()
H, W = img.shape[:2]
for r in range(H):
    for c in range(W):
        if np.all(img[r, c, :] == [255, 0, 0]):
            output[r, c, :] = [255, 255, 255]

 

上記のコードでは、全てのピクセルにループ処理を行い、RGB値が[255, 0, 0]に等しいかどうかをチェックしました。そして[255, 0, 0]であれば、出力のNumPy配列でそのピクセルの位置の値を[255, 255, 255]に上書きします。

簡潔ですが、すぐに、この手法は効率が悪いことに気づくでしょう。

 

method 2. NumPyの関数を利用する

こちらで問題となるのは、これをどのように効率化するかです。NumPyのトリックや手法を利用してベクトル化のメリットを活用するソリューションを構築してみましょう。

前と同じように画像をNumPy配列imgに読み込めば、以下のコードブロックが最適なソリューションを提供します。

output = img.copy()

valid = np.all(img == [255, 0, 0], axis = -1)
rs, cs = valid.nonzero()
output[rs, cs, :] = [255, 255, 255]

コードはまだ簡潔に見えますが、やや複雑でもあります。ブロードキャストや高度なインデックス作成の仕組みに精通していれば、こちらで何が行われているかおわかりになるでしょう。そうでなければ、以下の説明をご覧ください。

  • 全てのピクセルに手動でループ処理をして等価性をチェックする代わりに、NumPyの関数を用いて画像全体に「マスク」を作成します。
  • この「マスク」(validとして示されます)は、元の画像と同じ高さと幅のNumPy配列であり、各座標でブール値(真理値)を持っています。この値がTrueであれば、その座標は[255, 0, 0]の色であり、Falseであれば[255, 0, 0]の色ではありません。
  • 次にvalid.nonzero()を利用してvalidの非ゼロ要素(こちらではFalseでない要素)のインデックスを取得できます。図4は、その仕組みをわかりやすく示したものです。詳細はこちらの記事も参照してください。
図4
  • これでrscsを直接利用し、出力配列で[255, 0, 0]の値を持つピクセルにアクセスして[255, 255, 255]の値を割り当てることができます。

図5は、このワークフロー全体のおおよその仕組みを示したものです。

図5

高度なインデックス作成とブロードキャストを活用することによって、シンプルなPythonのループ処理を適用する必要がなくなり、ベクトル化のメリットを享受できます。

とはいえ、この追加作業には価値があるのでしょうか。結果を見てみましょう。

 

パフォーマンス比較

method1と2を幅2000ピクセル(高さ1000ピクセル)の画像に適用して完全置換を解いてみましょう。Pythonのtimeitモジュールを利用して、問題の解決にかかった時間を測定します。 

図6

上の図から、method 2がmethod 1と比べてはるかに効率的であることがわかります。

下の棒グラフは、五種類の縮尺の画像全てに両方の手法を用いた場合の所要時間を示しています。右側のグラフは、パフォーマンスの差をより明らかにするために、左側のグラフを拡大しています。

拡大バージョンには、method 2を使った場合の高速化率(method 1でかかった時間をmethod 2の時間で割ったもの)も示されています。

図7

 

ソリューション② 条件付き置換

図2の入力画像をより詳しく分析してみましょう。下の図8は、問題の理解に役立つよう、入力画像にいくつかアノテーションが加えられています。 

画像の軸、インデックス、高さおよび幅のパラメータは、完全置換と同じです。唯一の違いは、点線のボックス内のみ置換を行う必要があることです。点線のボックスは、左上と右下の座標(下の図の星印)で示されています。ボックスや星印は架空のものであり(実際の入力画像には存在しません)、視覚化するためだけに表示されています。 

図8

boxesと呼ばれるNumPy配列で全てのボックスの左上と右下の座標が与えられています。問題を簡単にするため、座標は整数になるようにしています。コードブロックでwは画像の幅の1/10、hは画像の高さの1/10を指します。

boxes = np.array([
    [[h, w], [9*h, 2*w]],
    [[6.5*h, 2.5*w], [9*h, 8*w]],
    [[h, 2.5*w], [5.5*h, 6*w]],
    [[h, 6.5*w], [5.5*h, 9*w]],
    [[6.5*h, 8.5*w], [9*h, 9*w]]
]).astype(np.int32)

 

whはどちらも画像のサイズに依存するため、ボックスのサイズは画像のサイズに比例します。これによって、異なる縮尺でのパフォーマンスを分析するのがより簡単になります。では、いくつかソリューションを見てみましょう。

 

method 1. 簡単なPythonのfor文によるループ

Pythonのループ処理は非常にシンプルです。基本的に、完全置換のmethod 1と同じ構造ですが、架空のボックス内のピクセルだけを処理するために少々変更を行います。
以下のコードブロックがソリューションを示しています。完全置換のmethod 1と同様なので、これ以上分析は行いません。

 

output = img.copy()
H, W = img.shape[:2]

## Looping over all boxes
for box in boxes:

    ## Getting the box coordinates
    top_h, top_w = box[0]
    bot_h, bot_w = box[1]

    ## Similar to Method I of Problem #1
    for r in range(top_h, bot_h + 1):
        for c in range(top_w, bot_w + 1):
            if np.all(img[r, c, :] == [255, 0, 0]):
                output[r, c, :] = [255, 255, 255]

 

method 2. NumPyの関数(タイプ1)

NumPyベースのソリューションは様々な方法でアプローチできます。簡単なソリューションの一つは、架空のボックス内の画像部分をそれぞれサブ画像に抽出し、完全置換のmethod 2と同様のソリューションを繰り返すことです。これを示したのが、次のコードブロックです。

 

output = img.copy()

## Looping over all boxes
for box in boxes:

    ## Getting the box coordinates
    top_h, top_w = box[0]
    bot_h, bot_w = box[1]

    ## Extracting sub-images
    sub_img_inp = img[top_h: bot_h + 1, top_w: bot_w + 1]
    sub_img_out = output[top_h: bot_h + 1, top_w: bot_w + 1]

    ## Similar to Method II of Problem #1
    valid = np.all(sub_img_inp == [255, 0, 0], axis = -1)
    rs, cs = valid.nonzero()
    sub_img_out[rs, cs, :] = [255, 255, 255]

 

上記のコードブロックでは、boxes配列にループ処理を行っています。変数boxは一つのボックスを示します。ボックス内の利用可能な座標情報を直接利用して、NumPy配列imgoutputをスライスし、sub_img_inpsub_img_outをそれぞれ取得します。

新しく作成されたNumPy配列(sub_img_inpおよびsub_img_out)は、imgoutputそれぞれの元のNumPy配列のビュー(view)です。つまり、サブ画像の配列の値を変更すると、それぞれの元の配列に変更が反映されます。

こちらのトリックによって、問題はより簡単になります。次は、完全置換のmethod 2を各ボックスのsub_img_inpとsub_img_outに使用すればよいだけです。以下の図はおおよそのワークフローを表しています。

図9

一点ご注意していただきたいのは、ボックスの反復処理にPythonのループを利用していることです。とはいえ、ボックスが五つしかなく、各反復処理で実行されるコードはかなり高速なので、大きな影響はありません。

完全置換のmethod 2と同じように、こちらの使い方は、全てのピクセルにPythonループ処理を実行するよりはるかに高速であることが期待できます。

 

method 3. NumPyの関数(タイプ2)

この問題を解決するために利用できるもう一つのアプローチを見てみましょう。前のアプローチより少々複雑で、効率も若干劣ります。しかし、こちらでご紹介するトリックは他の問題の解決に役立つ可能性があるため、この手法にも触れたいと思います。どのような場合に役立つかについては、後で例を挙げてご説明いたします。

以下のコードブロックがこの手法を示しています。後で詳しく説明します。

output = img.copy()

## Getting valid coordinates of color [255,0,0]
valid = np.all(img == [255, 0, 0], axis = -1)
rs, cs = valid.nonzero()

## Initializing rcs mask.
all_valid_rcs = np.full(rs.shape, False)

## Looping over all boxes
for box in boxes:

    ## Getting the box coordinates
    top_h, top_w = box[0]
    bot_h, bot_w = box[1]

    ## Finding valid coordinates that are also
    ## inside the current box.
    cur_valid_rs = ((rs >= top_h) & (rs <= bot_h))
    cur_valid_cs = ((cs >= top_w) & (cs <= bot_w))
    cur_valid_rcs = cur_valid_rs & cur_valid_cs

    ## Updating the rcs mask with valid coordinates
    ## that are inside the current box.
    all_valid_rcs |= cur_valid_rcs

## Selecting indices that belong to any box
rs = rs[all_valid_rcs]
cs = cs[all_valid_rcs]

output[rs, cs, :] = [255, 255, 255]

こちらでのNumPyの関数の仕組みは以下となります。

  • 完全置換のmethod 2と同じように、画像全体のマスク配列validとインデックスrsおよびcsを作成します。
  • 次に、五つのボックスいずれかの内側にあるrscsのインデックスを選択したいと思います。
  • これを行うため、全てFalse値で埋められた、rsと同じ形状を持つ(※ rscsの形状は同じです)all_valid_rcsという名前の配列を作成します。
  • そして、座標[r, c]が一つのボックス内にあればall_valid_rcsの対応するインデックスがTrueに設定されます。
  • 例えば、rs[i]とcs[i]が両方とも一つのボックス内にあればall_valid_rcs[i]Trueに設定されます。上記のコードブロックのfor文によるループ内のコードが、このロジックのベクトル化バージョンを実行します。
  • 全てのボックスについてrscsの要素にこのプロセスを完了すれば、all_valid_rcsを利用していずれかのボックスの内側にあるrsやcsのインデックスを選択できます。下の図は、こちらの使い方のおおよそのプロセスを示しています。
図10
  • いずれかのボックスの内側にあるrs値とcs値の選択が済んだら、それを利用して色を[255, 255, 255]に変更します。下の図がこの手法全体のおおよそのプロセスを示しています。
図11

この手法を利用すれば、method 2のように各ボックスを別々に置き換えることなく、少しトリックを使って正しいインデックスを選択し、一回だけ置換を実行すればよいことになります。

今回の問題では、このワークフローはメリットがないかもしれませんが、他の実践的な問題で役立つ可能性があります。例えば、「置換」ではなく、実行に費用と時間のかかる操作を行っている場合などは、この使い方が役立つかもしれません。

また、ボックスが重複している場合、この使い方を利用して、複数のボックスに属する座標が複数回操作されないようにすることもできます。これは、全ての問題で懸念材料になるわけではありませんが、そのような問題が発生した場合に対処できることを知っておくとよいでしょう。

 

パフォーマンス比較

図12

前と同じようにmethod 1、method 2、そしてmethod 3を幅2000ピクセル(高さ1000ピクセル)の画像に適用して条件付き置換を解き、かかった時間を測定しましょう。

上の図から、method 1はmethod 2やmethod 3ほど効率的でないことがわかります。また、method 3は、method 2より少し遅いことも明らかです。

下の棒グラフは、五種類の縮尺の画像全てに三つの手法を用いた場合の所要時間を示しています。こちらでも、右側のグラフは、パフォーマンスの差をより明らかにするために、左側のグラフを拡大しています。

拡大バージョンには、method 2とmethod 3を使った場合の高速化率も示されています。高速化率とは、method 2の場合、method 1でかかった時間をmethod 2の時間で割ったもの、method 3の場合は、method 1でかかった時間をmethod 3の時間で割ったものです。

図13

 

さいごに

ここまで、いくつかのトイプロブレムの助けを借りて、画像処理にNumPyのトリックを利用し、コードの効率を大幅に向上させる方法を探りました。また、それぞれのソリューションの所要時間を比較して、効率化を証明することもできました。

以前にも述べたように、こちらではトイプロブレムだけに着目しましたが、ご紹介したトリックは実用的なアプリケーションでも簡単に利用できます。例えば、バウンディングボックスや特定の色のピクセルの取り扱いは、物体検出やセマンティックセグメンテーションでよく直面するシナリオでしょう。ご説明したソリューションのコードは、比較的容易に読んで実行でき、しかも可能な限り最適なものにしてあります。これらの問題に独自のソリューションを試してみたい方は、こちらのGitHubリポジトリColabノートブックをご覧ください。技術的な観点から考察した記事に興味があれば、以下の関連記事も参照してください。また、取材記事や最新ニュースを直接受信できるよう、ぜひLionbridge.aiのメールマガジンにご登録ください。

 

Lionbridge AIについて

当社は教師データの作成やアノテーションサービスを提供し、機械学習の研究開発を支援しております。100万人の認定コントリビューターが登録されており、20年に渡るAIプロジェクトの実績がございます。無料トライアルやお見積は、こちらからお問い合わせ下さい。

※ 本記事は2020年8月6日、弊社英語ブログに掲載された寄稿記事に基づいたものです

AI向け教師データの作成やアノテーションサービスを提供し、研究開発を支援いたします。

メディア掲載結果

    AI・機械学習の最新情報をお届けします!

    Lionbridge AIのブログで紹介している事例記事やトレンドニュースといったビジネスに役立つ情報はもちろん、オープンデータセット集なども合わせてメール配信しております。