画像処理100本ノック完全ガイド 002:大津の二値化・HSV変換

プログラミング

はじめに

画像処理は、デジタル画像を操作してその内容を解析、変換、編集する技術です。この技術は、写真編集、医療画像解析、自動運転車の視覚システムなど、さまざまな分野で利用されています。このシリーズは、実際の画像処理技術をPythonで実装することで、理論と実践を組み合わせた学習を目指しています。画像処理の分野でよく使用される手法に「大津の二値化」と「HSV変換」があります。これらの手法は、画像の特定の特徴を強調したり、色情報を効果的に利用したりするために非常に有用です。本記事では、これらの手法について詳細に解説し、Pythonの標準ライブラリのみを使ってそれらを実装する方法を示します。この記事では、画像処理100本ノックで用意されている「imori.jpg」をダウンロードしてください。ダウンロードのURLはこちらです。

画像処理100本ノック 004. 大津の二値化

大津の二値化を実装せよ。 大津の二値化とは判別分析法と呼ばれ、二値化における分離の閾値を自動決定する手法である。 これはクラス内分散クラス間分散の比から計算される。

画像処理100本ノック

方針

大津の二値化(Otsu’s method)は、1979年に大津健一によって提案された自動二値化手法です。この方法は、画像のヒストグラムを解析し、二つのクラス(背景と前景)の間の分離の最適な閾値を自動的に決定します。ここで重要なのは、画像中のピクセルの分布に基づいて最適な閾値を見つける点です。これにより、手動で閾値を設定する必要がなくなり、異なる画像に対しても適用可能な汎用的な手法となっています。

大津の二値化の基本的なアイデアは、クラス内分散(within-class variance)とクラス間分散(between-class variance)の比を最大化することです。画像の輝度値のヒストグラムを基に、各ピクセルの輝度値を背景と前景の二つのクラスに分け、それぞれのクラス内の分散を計算します。クラス内分散は各クラスのピクセルの輝度値のばらつきを表し、クラス間分散は二つのクラス間の輝度値の差を表します。

まず、画像全体の輝度値のヒストグラムを計算します。次に、各閾値に対して、背景と前景の二つのクラスに分け、それぞれのクラスの平均輝度値を求めます。その後、各クラスの重み(ピクセル数の割合)を計算し、クラス内分散とクラス間分散を求めます。最後に、クラス間分散が最大となる閾値を最適閾値として選定します。

数式で表すと、まず各輝度値 t に対して背景クラス C1​ と前景クラス C2​ に分け、各クラスの平均輝度値 μ1(t) と μ2(t) を求めます。クラスの重み w1(t) と w2(t) は次のように計算されます。

ここで、 p(i) は輝度値 i の出現頻度です。クラスの平均輝度値 μ1(t) と μ2(t) は次のように計算されます。

クラス間分散 σB2(t) は次のように計算されます。

このクラス間分散 σB2(t) が最大となる t を最適閾値とします。

実装手順

  1. ヒストグラムの作成:
    • 画像の各画素の輝度値(0-255)の分布を計算します。
  2. 累積分布関数の計算:
    • 各輝度値の出現頻度を累積的に計算します。
  3. クラス内分散の計算:
    • 各閾値ごとにクラス内分散を計算します。
  4. クラス間分散の計算:
    • 各閾値ごとにクラス間分散を計算します。
  5. 判別分析の最適化:
    • クラス内分散とクラス間分散の比が最大になる閾値を選定します。

解答と実行結果

まず入力画像をグレースケールに変換し、その後、輝度値のヒストグラムを計算します。次に、各閾値に対してクラス内分散とクラス間分散を計算し、最大の判別分析結果を得る閾値を決定します。最後に、その閾値を使用して画像を二値化します。

import cv2
import numpy as np

def otsu_binarization(image):
    # グレースケール化
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # ヒストグラムの計算
    histogram = np.bincount(gray.flatten(), minlength=256)
    
    # 画素数の合計
    total = gray.size
    
    current_max, threshold = 0, 0
    sumB, sum1 = 0, np.dot(np.arange(256), histogram)
    
    wB, wF = 0, 0
    
    for i in range(256):
        wB += histogram[i]
        if wB == 0:
            continue
        wF = total - wB
        if wF == 0:
            break
        sumB += i * histogram[i]
        mB = sumB / wB
        mF = (sum1 - sumB) / wF
        between = wB * wF * (mB - mF) ** 2
        if between > current_max:
            current_max = between
            threshold = i
            
    # 二値化
    binary_image = gray.copy()
    binary_image[gray > threshold] = 255
    binary_image[gray <= threshold] = 0
    
    return binary_image

# 画像の読み込み
image = cv2.imread('imori.jpg')
binary_image = otsu_binarization(image)
cv2.imwrite('binary_image.jpg', binary_image)

このコードを実行すると、次のような結果(右の画像)が得られます。

画像処理100本ノック 005. HSV変換

HSV変換を実装して、色相Hを反転せよ。HSV変換とは、Hue(色相)Saturation(彩度)Value(明度) で色を表現する手法である。

画像処理100本ノック

方針

HSV(Hue, Saturation, Value)色空間は、RGB色空間とは異なる色の表現方法です。各成分の意味は以下の通りです。

  • Hue(色相): 色の種類を示す値。0-360度の範囲で表され、0度が赤、120度が緑、240度が青を表します。
  • Saturation(彩度): 色の鮮やかさを示す値。0-1の範囲で表され、0は無彩色(グレー)、1は純色を表します。
  • Value(明度): 色の明るさを示す値。0-1の範囲で表され、0は黒、1は最大の明るさを表します。

RGB色空間は、画像の色を赤(Red)、緑(Green)、青(Blue)の三つの成分で表現します。一方、HSV色空間は、色相(Hue)、彩度(Saturation)、明度(Value)の三つの成分で色を表現します。この変換は、色の直感的な操作を可能にします。たとえば、特定の色相を変えたり、彩度や明度を調整することで、画像全体の色調を変更できます。RGBからHSVへの変換は、まずRGB値を正規化して0から1の範囲にします。その後、以下の手順で各成分を計算します。

  • 明度 V は R、G、B の最大値です。
  • 彩度 S は最大値と最小値の差を最大値で割った値です(ただし、最大値が0の場合は0になります)。
  • 色相 H は最大値に応じて計算されます。最大値が R の場合、G と B の差を最大値と最小値の差で割って計算し、360度の範囲に変換します。最大値が G や B の場合も同様に計算されます。

具体的な数式は以下の通りです:

実装手順

HSVからRGBへの変換は、まず色相 H を基にRGB値を計算します。色相は0から360度の範囲で表され、彩度 S と明度 V を使用してRGB値を調整します。具体的な手順は以下の通りです:

  1. RGB画像を読み込む:
    • 画像を読み込み、各ピクセルのRGB値を取得します。
  2. RGBからHSVへの変換:
    • 各ピクセルのRGB値をHSV値に変換します。
  3. 色相の反転:
    • 色相成分を180度反転させます。
  4. HSVからRGBへの再変換:
    • 反転したHSV値をRGB値に再変換します。

解答と実行結果

まず入力画像をHSV色空間に変換し、色相(H)成分を180度反転させます。その後、反転後のHSV値を再びRGB色空間に変換して、色相を反転させた画像を生成します。

import cv2
import numpy as np

def rgb_to_hsv(image):
    image = image.astype(float) / 255.0
    R, G, B = image[..., 0], image[..., 1], image[..., 2]
    M = np.max(image, axis=-1)
    m = np.min(image, axis[-1])
    C = M - m

    H = np.zeros_like(M)
    S = np.zeros_like(M)
    V = M

    mask = C != 0
    S[mask] = C[mask] / M[mask]

    mask_r = (M == R) & mask
    mask_g = (M == G) & mask
    mask_b = (M == B) & mask

    H[mask_r] = (G[mask_r] - B[mask_r]) / C[mask_r] % 6
    H[mask_g] = (B[mask_g] - R[mask_g]) / C[mask_g] + 2
    H[mask_b] = (R[mask_b] - G[mask_b]) / C[mask_b] + 4

    H *= 60
    H[H < 0] += 360

    return np.stack([H, S, V], axis=-1)

def hsv_to_rgb(hsv_image):
    H, S, V = hsv_image[..., 0], hsv_image[..., 1], hsv_image[..., 2]
    C = V * S
    X = C * (1 - np.abs(H / 60 % 2 - 1))
    m = V - C

    rgb_image = np.zeros_like(hsv_image)
    h0, h1, h2 = H < 60, (60 <= H) & (H < 120), (120 <= H) & (H < 180)
    h3, h4, h5 = (180 <= H) & (H < 240), (240 <= H) & (H < 300), H >= 300

    rgb_image[h0] = np.stack([C, X, 0], axis=-1)[h0]
    rgb_image[h1] = np.stack([X, C, 0], axis[-1])[h1]
    rgb_image[h2] = np.stack([0, C, X], axis[-1])[h2]
    rgb_image[h3] = np.stack([0, X, C], axis[-1])[h3]
    rgb_image[h4] = np.stack([X, 0, C], axis[-1])[h4]
    rgb_image[h5] = np.stack([C, 0, X], axis[-1])[h5]

    return (rgb_image + m[..., np.newaxis]) * 255

def hsv_inversion(image):
    hsv_image = rgb_to_hsv(image)
    hsv_image[..., 0] = (hsv_image[..., 0] + 180) % 360
    return hsv_to_rgb(hsv_image).astype(np.uint8)

# 画像の読み込み
image = cv2.imread('imori.jpg')
inverted_image = hsv_inversion(image)
cv2.imwrite('inverted_image.jpg', inverted_image)

このコードを実行すると、次のような結果(右の画像)が得られます。

最後に

大津の二値化とHSV変換は、画像処理において非常に強力な手法です。これらの手法を理解し、実装することで、画像処理の基礎を深く理解することができます。次回も引き続き、画像処理の基本的な手法について学んでいきましょう。

私の経歴などについては以下の記事から確認することができます!

ブログランキングに参加しています。ぜひクリックで応援お願いします

ブログランキング・にほんブログ村へ
「#Python」人気ブログランキング

コメント

タイトルとURLをコピーしました