NumPyでRGB画像を扱う|色チャンネルの分離・単色化・グレースケール・二値化・RGBとBGRの入れ替え

RGB画像(カラー写真)は、NumPyでは「高さ×幅×3」の配列になります。最後の3がR(赤)・G(緑)・B(青)のチャンネルで、a[:, :, 0] のように取り出せば色ごとの分離、0にすれば単色化、重みを掛けて足せば白黒(グレースケール)になります。

この記事では、Pillowで読み込んだ写真をNumPy配列として扱い、チャンネル分離・単色化・グレースケール化・二値化・色の入れ替えまでを、実際に動かした結果つきで説明します。Pillowの変換結果と自分で計算した値が一致するかも確かめました。実行環境はPython 3.12.15、NumPy 2.5.4、Pillow 12.3.0です(2026年10月15日確認)。

RGB画像とは:1画素が3つの数値でできている

RGBは光の三原色である赤(Red)・緑(Green)・青(Blue)の頭文字です。一般的な写真(JPEGやPNG)は、1画素ごとに赤・緑・青の明るさを0〜255の整数で持っています。3色とも0なら黒、3色とも255なら白、赤だけ255なら赤です。1色あたり256段階なので、表せる色は256×256×256=16,777,216通りになります。

色 R G B
黒 0 0 0
白 255 255 255
赤 255 0 0
黄(赤+緑) 255 255 0
オレンジ 255 200 0

画像をNumPy配列として読み込む

手元の写真で試す場合は Image.open("写真.jpg") を使います。ここでは結果を確かめやすいように、左から赤・緑・青、下にオレンジの帯がある小さな画像を作って保存し、それを読み込みます。

import numpy as np
from PIL import Image

img = np.zeros((60, 90, 3), dtype=np.uint8)
img[:, :30] = [255, 0, 0]      # 左: 赤
img[:, 30:60] = [0, 255, 0]    # 中: 緑
img[:, 60:] = [0, 0, 255]      # 右: 青
img[40:, :] = [255, 200, 0]    # 下の帯: オレンジ
Image.fromarray(img).save("sample.png")

a = np.array(Image.open("sample.png"))
print(a.shape, a.dtype)
print(a[0, 0], a[0, 45], a[0, 80], a[50, 10])
(60, 90, 3) uint8
[255   0   0] [  0 255   0] [  0   0 255] [255 200   0]

形は (高さ, 幅, 3) で、横幅が先ではない点に注意してください。a[行, 列] の順なので、a[50, 10] は「上から50、左から10」の画素です。型の uint8 は0〜255の整数を表す型です。

色チャンネルを分離する

r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]
print(r.shape, r[0, 0], g[0, 0], b[0, 0])

r2, g2, b2 = np.moveaxis(a, -1, 0)   # まとめて分ける書き方
print(np.array_equal(r, r2))
(60, 90) 255 0 0
True

取り出した r は2次元の配列で、赤の強さだけが入っています。これをそのまま画像として保存すると「赤が強いところほど白い」白黒画像になります。赤いチャンネルを赤く見せたいときは、次の単色化を使います。

チャンネルごとの平均で色味の傾向を見る

print(a.reshape(-1, 3).mean(axis=0).round(1))
[141.7 123.3  56.7]

全画素のR・G・Bの平均です。この画像はオレンジの帯があるぶん、青の平均が低くなっています。写真の色かぶり(全体が青っぽい、黄色っぽい)を数値で確かめるときに使えます。

単色化:1つのチャンネルだけ残す

red_only = np.zeros_like(a)
red_only[:, :, 0] = a[:, :, 0]
print(red_only[0, 0], red_only[0, 45], red_only[50, 10])
Image.fromarray(red_only).save("red_only.png")
[255   0   0] [0 0 0] [255   0   0]

緑と青を0にしたので、もともと緑だった中央は黒になり、オレンジ(255, 200, 0)は赤(255, 0, 0)になりました。a.copy() してから [:, :, 1] = 0 と [:, :, 2] = 0 を代入しても同じ結果です。

グレースケール(白黒)にする

人の目は緑を明るく、青を暗く感じるため、3色を同じ割合で平均すると見た目の明るさとずれます。よく使われるのは赤0.299・緑0.587・青0.114の重みです。Pillowの convert("L") も、公式ドキュメントで「L = R * 299/1000 + G * 587/1000 + B * 114/1000」という式で変換すると説明しています(Pillow Image.convert)。

gray = 0.299 * a[:, :, 0] + 0.587 * a[:, :, 1] + 0.114 * a[:, :, 2]
gray_u8 = np.round(gray).astype(np.uint8)
print(gray_u8[0, 0], gray_u8[0, 45], gray_u8[0, 80], gray_u8[50, 10])

pil_gray = np.array(Image.open("sample.png").convert("L"))
print(pil_gray[0, 0], pil_gray[0, 45], pil_gray[0, 80], pil_gray[50, 10])

print(a.mean(axis=2).astype(np.uint8)[0, 0],
      a.mean(axis=2).astype(np.uint8)[0, 45],
      a.mean(axis=2).astype(np.uint8)[0, 80])   # 単純平均の場合
76 150 29 194
76 150 29 194
85 85 85

重み付けでは、同じ明るさ255の赤・緑・青がそれぞれ76・150・29になり、緑がいちばん明るく、青がいちばん暗くなります。単純平均では3色とも85で同じ灰色になってしまい、白黒写真にしたときに色の区別がつかなくなります。

ランダムな色の画像(200×200画素)で自分の計算とPillowの結果を比べたところ、差は最大1で、一致率は99.95%でした。小数点以下の丸め方の違いによるもので、実用上は同じと考えて問題ありません。

二値化:白と黒の2色にする

グレースケールの値が、しきい値以上なら白(255)、未満なら黒(0)にします。

binary = np.where(pil_gray >= 128, 255, 0).astype(np.uint8)
print(np.unique(binary, return_counts=True))
(array([  0, 255], dtype=uint8), array([2400, 3000]))

しきい値を自動で決める(大津の二値化)

しきい値を画像ごとに自動で決める代表的な方法が大津の二値化です。明るさのヒストグラムを2つのグループに分けたとき、グループ間のばらつき(クラス間分散)が最大になる値を選びます。画素の平均値をしきい値にするのとは別の方法です。

def otsu_threshold(gray):
    hist = np.bincount(gray.ravel(), minlength=256).astype(float)
    total = gray.size
    levels = np.arange(256)
    best_t, best_var = 0, -1.0
    for t in range(1, 256):
        w0 = hist[:t].sum(); w1 = total - w0
        if w0 == 0 or w1 == 0:
            continue
        m0 = (levels[:t] * hist[:t]).sum() / w0
        m1 = (levels[t:] * hist[t:]).sum() / w1
        var = w0 * w1 * (m0 - m1) * (m0 - m1)
        if var > best_var:
            best_t, best_var = t, var
    return best_t

rng = np.random.default_rng(0)
g = np.concatenate([rng.normal(60, 15, 3000), rng.normal(170, 20, 1000)])
g = np.clip(g, 0, 255).astype(np.uint8).reshape(40, 100)

print("平均", round(g.mean(), 1), "大津", otsu_threshold(g))
print("大津で切った白", (g >= otsu_threshold(g)).sum())
print("平均で切った白", (g >= g.mean()).sum())
平均 86.8 大津 115
大津で切った白 998
平均で切った白 1099

暗い画素3,000個(平均60)と明るい画素1,000個(平均170)を混ぜたデータです。大津の方法では白が998個とほぼ正しく分かれましたが、平均値(86.8)をしきい値にすると暗い側の画素まで白に入ってしまいました。暗い部分が多い画像ほど、平均値では分け方がずれます。OpenCVを使った二値化や適応的二値化は「Pythonで画像を二値化する方法」で詳しく扱っています。

色チャンネルを入れ替える(RGBとBGR)

bgr = a[:, :, ::-1]          # RGB → BGR
print(bgr[0, 0], bgr[0, 80])

gbr = a[:, :, [1, 2, 0]]     # 好きな順番に並べ替え
print(gbr[0, 0])
[  0   0 255] [255   0   0]
[  0   0 255]

[:, :, ::-1] は3番目の軸を逆順にする書き方で、赤と青が入れ替わります。OpenCVは画素をBGRの順で扱うため、PillowやMatplotlib(RGB)と行き来するときに必要になる操作です。ライブラリ間の受け渡しは「Pillow・NumPy・OpenCVの違いと使い分け」にまとめています。

明るさを変えるときの落とし穴:uint8のあふれ

uint8は0〜255しか入らないため、そのまま足し算すると255を超えた分が0から数え直しになります。

p = np.array([200, 100, 50], dtype=np.uint8)
print(p + 100)
print(np.clip(p.astype(np.int16) + 100, 0, 255).astype(np.uint8))
print(p * 1.5)
[ 44 200 150]
[255 200 150]
[300. 150.  75.]

200+100が300ではなく44になっています(300−256=44)。明るくしたはずの部分が暗くなるのはこれが原因です。大きい型(int16やfloat)に変えてから計算し、np.clip で0〜255に収めてからuint8に戻します。小数を掛けた場合は型がfloatに変わり、255を超えた値もそのまま残るので、画像として保存する前にやはりclipが必要です。

透明度付きの画像(RGBA)

PNGには透明度(アルファ)を持つRGBA形式があり、その場合は最後の軸が4になります。

rgba = np.array(Image.fromarray(a).convert("RGBA"))
print(rgba.shape, rgba[0, 0])
rgb = rgba[..., :3]
print(rgb.shape)
(60, 90, 4) [255   0   0 255]
(60, 90, 3)

4番目の値がアルファ(255で不透明)です。ここまでの処理をそのまま当てはめると、a[:, :, 2] の後ろにアルファが残っていて結果がおかしくなることがあります。shape を確かめ、必要なら [..., :3] でRGBだけにしてから処理してください。

処理の早見表

やりたいこと NumPyでの書き方
赤だけ取り出す a[:, :, 0]
赤だけ残した画像 b = np.zeros_like(a); b[:, :, 0] = a[:, :, 0]
グレースケール 0.299*R + 0.587*G + 0.114*B を丸めてuint8に
二値化 np.where(gray >= しきい値, 255, 0)
RGB→BGR a[:, :, ::-1]
明るさを上げる int16かfloatで計算→np.clip(…, 0, 255)→uint8
RGBAからRGBへ a[..., :3]

画素の配列を自分で操作できるようになると、画像を機械学習に入力するときの前処理(正規化や色の扱い)も理解しやすくなります。テックジムのAIエンジニア養成コース(16章で機械学習とディープラーニングを学ぶコース)では、第8章で画像を扱うCNN、第13章でOpenCVを使った物体検出を学びます。

よくある質問

Q. RGB画像をNumPy配列にするとshapeはどうなりますか?

(高さ, 幅, 3) になります。最後の3がR・G・Bのチャンネルで、型は通常uint8(0〜255の整数)です。透明度を持つRGBAのPNGでは最後が4になります。

Q. グレースケールは3色の平均ではだめですか?

計算はできますが、見た目の明るさとずれます。人の目は緑を明るく青を暗く感じるため、0.299・0.587・0.114の重みを掛けて足すのが一般的です。Pillowのconvert(“L”)もこの重みで変換します。

Q. 明るくしようと値を足したら一部が暗くなりました。

uint8は255を超えると0から数え直すためです。int16やfloatに変換してから計算し、np.clipで0〜255に収めてからuint8に戻してください。

Q. OpenCVで読み込んだ画像の色がおかしいのはなぜですか?

OpenCVは画素をBGRの順で扱うためです。PillowやMatplotlibで表示する前に、a[:, :, ::-1]で赤と青を入れ替えてRGBに戻してください。

まとめ

  • RGB画像はNumPyでは (高さ, 幅, 3) のuint8配列。a[行, 列] の順で画素を指定する
  • チャンネル分離は a[:, :, 0]、単色化は他のチャンネルを0にする
  • グレースケールは0.299・0.587・0.114の重み付け。Pillowのconvert(“L”)と同じ式
  • 二値化のしきい値を自動で決めるなら大津の方法。平均値で切るのとは別物
  • uint8のあふれに注意し、計算は大きい型で行ってclipしてから戻す

NumPy|画像処理の関連記事

NumPyを画像データの処理に使う方法の記事です。

NumPy全体の使い方は「NumPyの使い方」、「NumPy関数一覧」でまとめています。

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら