ちくわ君と学ぶOpenCVによる画像のアフィン変換とフィルタ処理

梅雨明けの蒸し暑い午後、森の研究所ではちくわ君が複数のモニターに映し出された画像と格闘していた。机の上には赤い木の実が散らばっており、その隣には研究所の設備で撮影したらしい写真の束が積み上げられている。

「ちくわさん、お疲れさまです!」はんぺん君が汗をかきながら研究所に入ってきた。「今日も暑いですね。それにしても、たくさんの写真ですね」

「やあ、はんぺん君」ちくわ君は疲れた表情で振り返った。「実は森の生態調査で撮影した写真の整理をしているんだけれど、角度がバラバラだったり、ピントがぼけていたりで、そのままでは分析に使えないんだ」

「分析に使えない、ですか?」はんぺん君は首をかしげた。

ちくわ君は木の実を一つ口に含んでから説明を始めた。「例えば、この写真を見てごらん」彼は一枚の写真を手に取った。「木の葉っぱの形を測定したいんだけれど、カメラが斜めになっていて正確な計測ができないんだ」

写真を見ると、確かに斜めに撮影されており、さらに少しボケているのがわかった。「確かに、これでは正確な測定は難しそうですね」

「そこで今日は、OpenCVというライブラリを使って画像を補正する方法を学んでみよう」ちくわ君は新しいPythonファイルを開いた。「画像のアフィン変換とフィルタ処理について、一緒に勉強してみないかい?」

OpenCVとMatplotlibのセットアップ

「OpenCVって何ですか?」はんぺん君は興味深そうに尋ねた。

「OpenCVは『Open Source Computer Vision Library』の略で、画像処理やコンピュータービジョンのための強力なライブラリなんだ」ちくわ君は説明しながらコードを書き始めた。「そして、処理結果を見やすく表示するためにMatplotlibも使うよ」

import cv2
import numpy as np
import matplotlib.pyplot as plt

# Matplotlibの日本語対応設定
plt.rcParams['font.family'] = 'DejaVu Sans'

def show_images(images, titles, figsize=(15, 5), cmap='gray'):
    """複数の画像を並べて表示する関数"""
    fig, axes = plt.subplots(1, len(images), figsize=figsize)
    if len(images) == 1:
        axes = [axes]
    
    for i, (img, title) in enumerate(zip(images, titles)):
        if len(img.shape) == 3:
            # カラー画像はBGR→RGBに変換
            img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
            axes[i].imshow(img_rgb)
        else:
            # グレースケール画像
            axes[i].imshow(img, cmap=cmap)
        
        axes[i].set_title(title, fontsize=12)
        axes[i].axis('off')
    
    plt.tight_layout()
    plt.show()

# テスト用のサンプル画像を作成
def create_sample_leaf():
    """葉っぱのような形のサンプル画像を作成"""
    img = np.zeros((400, 600, 3), dtype=np.uint8)
    
    # 背景を薄い茶色に
    img[:, :] = (180, 200, 220)
    
    # 葉っぱの形を描画
    # 楕円形の葉っぱ
    cv2.ellipse(img, (300, 200), (150, 80), 15, 0, 360, (50, 150, 50), -1)
    
    # 葉脈を描画
    cv2.line(img, (180, 170), (420, 230), (30, 100, 30), 3)  # 中央の葉脈
    cv2.line(img, (220, 150), (300, 180), (30, 100, 30), 2)  # 左上の葉脈
    cv2.line(img, (320, 180), (380, 210), (30, 100, 30), 2)  # 右上の葉脈
    cv2.line(img, (240, 220), (300, 200), (30, 100, 30), 2)  # 左下の葉脈
    cv2.line(img, (320, 200), (360, 240), (30, 100, 30), 2)  # 右下の葉脈
    
    return img

# サンプル画像を作成
leaf_image = create_sample_leaf()
print(f"サンプル画像のサイズ: {leaf_image.shape}")

# 画像を表示
show_images([leaf_image], ['Original Leaf Sample'], figsize=(8, 6))

「おお!」はんぺん君は感動した。「プログラムで葉っぱの画像を作ることもできるんですね。そしてMatplotlibできれいに表示されています」

「そう。これで処理前後の比較も見やすくなるよ」ちくわ君は嬉しそうに答えた。「実際の研究では外部から画像ファイルを読み込むことが多いけれど、今日は学習用にプログラムで作成したサンプルを使おう」

画像の傾きを直そう - アフィン変換の基礎

ちくわ君は先ほど作成した葉っぱの画像を15度回転させて、斜めになった状態を作り出した。「実際の撮影でよくあるように、カメラが傾いて撮影された状況を再現してみよう」

# 画像を傾けて撮影ミスを再現
def create_tilted_image(image, angle):
    """画像を指定角度だけ傾ける"""
    height, width = image.shape[:2]
    center = (width // 2, height // 2)
    
    # 回転行列を作成
    rotation_matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
    
    # 回転を適用
    tilted = cv2.warpAffine(image, rotation_matrix, (width, height))
    return tilted

# 葉っぱの画像を15度傾ける
tilted_leaf = create_tilted_image(leaf_image, 15)

# 傾いた画像を表示
show_images([leaf_image, tilted_leaf], 
           ['Original', 'Tilted (15°)'], 
           figsize=(12, 6))

「なるほど、確かに傾いていますね」はんぺん君は画面を見ながら言った。「これを元に戻すにはどうするんですか?」

「アフィン変換を使って、逆向きに回転させれば良いんだ」ちくわ君は説明した。「傾きの角度がわかっている場合は簡単だよ」

# 画像の傾きを補正
def correct_rotation(image, angle):
    """画像の回転を補正する"""
    height, width = image.shape[:2]
    center = (width // 2, height // 2)
    
    # 逆方向の回転行列を作成
    rotation_matrix = cv2.getRotationMatrix2D(center, -angle, 1.0)
    
    # 補正を適用
    corrected = cv2.warpAffine(image, rotation_matrix, (width, height))
    return corrected

# 傾きを補正
corrected_leaf = correct_rotation(tilted_leaf, 15)

# 比較表示
show_images([tilted_leaf, corrected_leaf], 
           ['Tilted', 'Corrected'], 
           figsize=(12, 6))

print("画像の傾きを補正しました")

「すごい!」はんぺん君は目を輝かせた。「傾いていた葉っぱが真っ直ぐになりました!これで正確な測定ができそうですね」

3点を使った精密な補正

「でも実際の研究では、傾きの角度がわからないことも多いんだ」ちくわ君は続けた。「そういう時は、画像の中の基準となる3つの点を使って補正するんだよ」

ちくわ君は新しい例を作成した。「例えば、標本を方眼紙の上に置いて撮影したとしよう。でもカメラの角度が悪くて、方眼が歪んで見えているんだ」

# 方眼紙の上の標本画像を作成
def create_grid_sample():
    """方眼紙上の標本のサンプル画像"""
    img = np.ones((400, 600, 3), dtype=np.uint8) * 240  # 薄いグレー背景
    
    # 方眼を描画
    for i in range(0, 600, 20):
        cv2.line(img, (i, 0), (i, 400), (200, 200, 200), 1)
    for i in range(0, 400, 20):
        cv2.line(img, (0, i), (600, i), (200, 200, 200), 1)
    
    # 主要な格子線を太く
    for i in range(0, 600, 100):
        cv2.line(img, (i, 0), (i, 400), (150, 150, 150), 2)
    for i in range(0, 400, 100):
        cv2.line(img, (0, i), (600, i), (150, 150, 150), 2)
    
    # 標本(小さな葉っぱ)を配置
    cv2.ellipse(img, (300, 200), (60, 30), 10, 0, 360, (100, 180, 100), -1)
    cv2.line(img, (250, 185), (350, 215), (50, 120, 50), 2)
    
    return img

# 方眼紙画像を作成し、歪ませる
grid_image = create_grid_sample()

# 射影変換で歪みを作成(透視効果をシミュレート)
def create_perspective_distortion(image):
    """透視効果による歪みを作成"""
    height, width = image.shape[:2]
    
    # 元の4つの角
    src_points = np.float32([
        [0, 0],
        [width-1, 0],
        [width-1, height-1],
        [0, height-1]
    ])
    
    # 歪んだ4つの角(台形のような形)
    dst_points = np.float32([
        [50, 30],
        [width-50, 10],
        [width-30, height-20],
        [30, height-40]
    ])
    
    # 射影変換行列を計算
    perspective_matrix = cv2.getPerspectiveTransform(src_points, dst_points)
    
    # 変換を適用
    distorted = cv2.warpPerspective(image, perspective_matrix, (width, height))
    return distorted

distorted_grid = create_perspective_distortion(grid_image)

show_images([grid_image, distorted_grid], 
           ['Perfect Grid', 'Distorted Grid'], 
           figsize=(12, 6))

「わあ、方眼紙が台形のように歪んでしまいましたね」はんぺん君は驚いた。「これでは正確な測定は不可能です」

「そうなんだ。でも3点を指定したアフィン変換で、この歪みを補正できるよ」ちくわ君は解決策を示した。

# 3点を指定したアフィン変換による補正
def three_point_correction(image):
    """3点を指定して歪みを補正"""
    # 歪んだ画像での3つの基準点(方眼の交点)
    src_points = np.float32([
        [80, 50],    # 左上の交点
        [520, 30],   # 右上の交点  
        [60, 360]    # 左下の交点
    ])
    
    # 正しい位置の3点
    dst_points = np.float32([
        [100, 100],  # 左上
        [500, 100],  # 右上
        [100, 300]   # 左下
    ])
    
    # アフィン変換行列を計算
    affine_matrix = cv2.getAffineTransform(src_points, dst_points)
    
    # 変換を適用
    height, width = image.shape[:2]
    corrected = cv2.warpAffine(image, affine_matrix, (width, height))
    
    return corrected, src_points, dst_points

# 歪み補正を実行
corrected_grid, src_pts, dst_pts = three_point_correction(distorted_grid)

# 基準点をマーク
marked_distorted = distorted_grid.copy()
for i, point in enumerate(src_pts):
    cv2.circle(marked_distorted, tuple(point.astype(int)), 8, (0, 0, 255), -1)
    cv2.putText(marked_distorted, f'P{i+1}', 
               tuple((point + [10, -10]).astype(int)), 
               cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)

show_images([marked_distorted, corrected_grid], 
           ['Distorted (with reference points)', 'Corrected'], 
           figsize=(12, 6))

print("3点指定により方眼紙の歪みを補正しました")

「すごいです!」はんぺん君は興奮した。「歪んでいた方眼紙がきれいな格子状に戻りました。赤い点で示された3つの基準点を使って、全体の歪みを計算したんですね」

「その通り!」ちくわ君は嬉しそうに頷いた。「アフィン変換は3つの点があれば一意に決まるんだ。だから実際の研究でも、既知の寸法の物体を写真に含めておくことで、後から正確な補正ができるんだよ」

平滑化フィルタでノイズを除去しよう

「今度は画像の質を改善してみよう」ちくわ君は新しいトピックに移った。「実際の撮影では、ノイズやぼけが避けられないことが多いんだ」

まず、ちくわ君はノイズの多い画像を作成した。

# ノイズを含む画像を作成
def add_noise_to_image(image, noise_type='gaussian'):
    """画像にノイズを追加"""
    noisy = image.copy()
    
    if noise_type == 'gaussian':
        # ガウシアンノイズ
        noise = np.random.normal(0, 25, image.shape).astype(np.int16)
        noisy = np.clip(image.astype(np.int16) + noise, 0, 255).astype(np.uint8)
    
    elif noise_type == 'salt_pepper':
        # 塩胡椒ノイズ
        noise = np.random.random(image.shape[:2])
        noisy[noise < 0.01] = 0      # 胡椒(黒い点)
        noisy[noise > 0.99] = 255    # 塩(白い点)
    
    return noisy

# 葉っぱ画像にノイズを追加
noisy_leaf_gaussian = add_noise_to_image(leaf_image, 'gaussian')
noisy_leaf_salt_pepper = add_noise_to_image(leaf_image, 'salt_pepper')

show_images([leaf_image, noisy_leaf_gaussian, noisy_leaf_salt_pepper], 
           ['Original', 'Gaussian Noise', 'Salt & Pepper Noise'], 
           figsize=(15, 5))

print("ノイズを含む画像を作成しました")

「うわあ、ノイズがあると画像が台無しですね」はんぺん君は困った顔をした。「特に右の塩胡椒ノイズは、葉脈が見えなくなってしまいます」

「そうなんだ。でも平滑化フィルタを使えば、これらのノイズを効果的に除去できるよ」ちくわ君は説明した。「まずは最も基本的なガウシアンフィルタから試してみよう」

ガウシアンフィルタ

# ガウシアンフィルタによる平滑化
def apply_gaussian_filter(image, kernel_sizes=[5, 15, 31]):
    """異なるカーネルサイズでガウシアンフィルタを適用"""
    results = []
    
    for ksize in kernel_sizes:
        # カーネルサイズは奇数である必要がある
        filtered = cv2.GaussianBlur(image, (ksize, ksize), 0)
        results.append(filtered)
    
    return results

# ガウシアンノイズの画像に対してフィルタを適用
gaussian_filtered = apply_gaussian_filter(noisy_leaf_gaussian)

# 結果を表示
titles = ['Noisy Image'] + [f'Gaussian σ={k}' for k in [5, 15, 31]]
images = [noisy_leaf_gaussian] + gaussian_filtered

show_images(images, titles, figsize=(20, 5))

print("ガウシアンフィルタでノイズ除去を行いました")

「おお!」はんぺん君は感動した。「ノイズが段々と減っていくのがよくわかります。でも、大きなフィルタサイズだと画像がぼやけてしまいますね」

「いい観察だね」ちくわ君は感心した。「ノイズ除去と画像の鮮明さは、しばしばトレードオフの関係にあるんだ。用途に応じて適切なバランスを見つけることが大切なんだよ」

メディアンフィルタ

「塩胡椒ノイズには、メディアンフィルタの方が効果的なんだ」ちくわ君は続けた。「これは周囲のピクセルの中央値を取るフィルタで、極端な値を効果的に除去できるよ」

# メディアンフィルタによる塩胡椒ノイズ除去
def apply_median_filter(image, kernel_sizes=[3, 5, 9]):
    """異なるカーネルサイズでメディアンフィルタを適用"""
    results = []
    
    for ksize in kernel_sizes:
        filtered = cv2.medianBlur(image, ksize)
        results.append(filtered)
    
    return results

# 塩胡椒ノイズの画像に対してフィルタを適用
median_filtered = apply_median_filter(noisy_leaf_salt_pepper)

# ガウシアンフィルタとの比較も行う
gaussian_on_salt_pepper = apply_gaussian_filter(noisy_leaf_salt_pepper, [9])

# 結果を表示
comparison_images = [noisy_leaf_salt_pepper] + median_filtered + gaussian_on_salt_pepper
comparison_titles = ['Salt & Pepper Noise', 'Median 3x3', 'Median 5x5', 'Median 9x9', 'Gaussian 9x9']

show_images(comparison_images, comparison_titles, figsize=(20, 5))

print("メディアンフィルタで塩胡椒ノイズを除去しました")

「すごい!」はんぺん君は驚いた。「メディアンフィルタの方が塩胡椒ノイズをきれいに除去できていますね。ガウシアンフィルタだと白と黒の点がぼやけて灰色になってしまうのに、メディアンフィルタだときれいに消えています」

「まさにその通りだ!」ちくわ君は嬉しそうに答えた。「メディアンフィルタは、周囲の値の中央値を取るから、極端に大きかったり小さかったりする値(つまりノイズ)を効果的に除去できるんだ」

バイラテラルフィルタ

「でも、理想的なのはノイズを除去しながらも、重要な輪郭(エッジ)は保持することなんだ」ちくわ君は発展的な技術を紹介した。「そのためのバイラテラルフィルタというものがあるよ」

# バイラテラルフィルタ(エッジ保持平滑化)
def apply_bilateral_filter(image):
    """バイラテラルフィルタを適用"""
    # パラメータの説明:
    # d: 近傍の直径
    # sigmaColor: 色空間での標準偏差
    # sigmaSpace: 座標空間での標準偏差
    
    bilateral_weak = cv2.bilateralFilter(image, 9, 75, 75)
    bilateral_strong = cv2.bilateralFilter(image, 15, 150, 150)
    
    return bilateral_weak, bilateral_strong

# ノイズ画像に各種フィルタを適用して比較
gaussian_result = cv2.GaussianBlur(noisy_leaf_gaussian, (9, 9), 0)
bilateral_weak, bilateral_strong = apply_bilateral_filter(noisy_leaf_gaussian)

# 比較表示
filter_comparison = [noisy_leaf_gaussian, gaussian_result, bilateral_weak, bilateral_strong]
filter_titles = ['Noisy Original', 'Gaussian 9x9', 'Bilateral (weak)', 'Bilateral (strong)']

show_images(filter_comparison, filter_titles, figsize=(16, 4))

print("バイラテラルフィルタでエッジを保持しながらノイズ除去を行いました")

「これは驚きです!」はんぺん君は目を見開いた。「バイラテラルフィルタは、ノイズを除去しながらも葉っぱの輪郭や葉脈をはっきりと保持していますね。ガウシアンフィルタと比べると、その違いは明らかです」

「そうなんだ」ちくわ君は説明を続けた。「バイラテラルフィルタは、空間的な距離だけでなく、色の類似性も考慮してフィルタリングするんだ。だから、色が大きく変わる境界(エッジ)では平滑化を弱くし、色が似ている領域では強く平滑化する」

画像をくっきりと - シャープ化フィルタ

「今度は逆に、ぼやけた画像をくっきりさせてみよう」ちくわ君は新しいトピックに移った。「手ぶれやピントのずれで、ぼやけてしまった画像を改善するんだ」

# ぼやけた画像を作成
blurred_leaf = cv2.GaussianBlur(leaf_image, (15, 15), 0)

# シャープ化フィルタ
def apply_sharpening_filters(image):
    """各種シャープ化フィルタを適用"""
    
    # 1. 基本的なシャープ化カーネル
    sharpen_kernel1 = np.array([
        [0, -1, 0],
        [-1, 5, -1],
        [0, -1, 0]
    ], dtype=np.float32)
    
    # 2. より強いシャープ化カーネル
    sharpen_kernel2 = np.array([
        [-1, -1, -1],
        [-1, 9, -1],
        [-1, -1, -1]
    ], dtype=np.float32)
    
    # 3. アンシャープマスク
    gaussian = cv2.GaussianBlur(image, (9, 9), 10.0)
    unsharp_mask = cv2.addWeighted(image, 1.5, gaussian, -0.5, 0)
    
    # フィルタを適用
    sharpened1 = cv2.filter2D(image, -1, sharpen_kernel1)
    sharpened2 = cv2.filter2D(image, -1, sharpen_kernel2)
    
    # 値の範囲を0-255に制限
    sharpened1 = np.clip(sharpened1, 0, 255).astype(np.uint8)
    sharpened2 = np.clip(sharpened2, 0, 255).astype(np.uint8)
    unsharp_mask = np.clip(unsharp_mask, 0, 255).astype(np.uint8)
    
    return sharpened1, sharpened2, unsharp_mask

# シャープ化を実行
sharp1, sharp2, unsharp = apply_sharpening_filters(blurred_leaf)

# 結果を表示
sharp_images = [leaf_image, blurred_leaf, sharp1, sharp2, unsharp]
sharp_titles = ['Original', 'Blurred', 'Basic Sharpen', 'Strong Sharpen', 'Unsharp Mask']

show_images(sharp_images, sharp_titles, figsize=(20, 4))

print("各種シャープ化フィルタを適用しました")

「すごいですね!」はんぺん君は感動した。「ぼやけていた葉脈が、シャープ化によってくっきりと見えるようになりました。特にアンシャープマスクは自然な仕上がりですね」

「アンシャープマスクは、実はとても興味深い手法なんだ」ちくわ君は説明した。「元の画像からぼかした画像を引くことで、エッジを強調するんだよ。写真の現像でも古くから使われている技術なんだ」

輪郭を見つけよう - エッジ検出

「今度は少し違ったアプローチをしてみよう」ちくわ君は新しいトピックに移った。「画像から物体の輪郭だけを抽出してみるんだ」

「輪郭だけですか?」はんぺん君は興味深そうに尋ねた。

「そう。例えば、葉っぱの形を正確に測定したい時は、葉っぱの輪郭がわかれば十分なことが多いんだ」ちくわ君は実例を示した。「いくつかの異なるエッジ検出手法を試してみよう」

# エッジ検出フィルタの比較
def apply_edge_detection(image):
    """各種エッジ検出フィルタを適用"""
    
    # グレースケールに変換
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # 1. Sobelフィルタ(x方向とy方向)
    sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
    sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
    sobel_combined = np.sqrt(sobel_x**2 + sobel_y**2)
    sobel_combined = np.uint8(np.clip(sobel_combined, 0, 255))
    
    # 2. Laplacianフィルタ
    laplacian = cv2.Laplacian(gray, cv2.CV_64F)
    laplacian = np.uint8(np.absolute(laplacian))
    
    # 3. Cannyエッジ検出
    canny = cv2.Canny(gray, 50, 150)
    
    # 4. Scharrフィルタ(Sobelの改良版)
    scharr_x = cv2.Scharr(gray, cv2.CV_64F, 1, 0)
    scharr_y = cv2.Scharr(gray, cv2.CV_64F, 0, 1)
    scharr_combined = np.sqrt(scharr_x**2 + scharr_y**2)
    scharr_combined = np.uint8(np.clip(scharr_combined, 0, 255))
    
    return gray, sobel_combined, laplacian, canny, scharr_combined

# エッジ検出を実行
gray, sobel_edge, laplacian_edge, canny_edge, scharr_edge = apply_edge_detection(leaf_image)

# 結果を表示
edge_images = [gray, sobel_edge, laplacian_edge, canny_edge, scharr_edge]
edge_titles = ['Grayscale', 'Sobel', 'Laplacian', 'Canny', 'Scharr']

show_images(edge_images, edge_titles, figsize=(20, 4), cmap='gray')

print("各種エッジ検出フィルタを適用しました")

「わあ!」はんぺん君は驚いた。「それぞれのフィルタで、輪郭の見え方が違いますね。Cannyエッジ検出は特にきれいな線で輪郭を表現しています」

「Cannyエッジ検出は最も高性能なエッジ検出手法の一つなんだ」ちくわ君は説明した。「ノイズの影響を受けにくく、細い線で正確な輪郭を検出できるよ。研究や産業用途でよく使われているんだ」

実践:研究室の問題を解決しよう

午後の日差しが和らいできた頃、ちくわ君は机の上の写真の束を指差した。「さあ、今まで学んだことを使って、実際の研究室の問題を解決してみよう」

「どんな問題ですか?」はんぺん君は身を乗り出した。

「森で撮影した動物の足跡写真があるんだけれど、雨上がりで泥が汚れていたり、影が濃すぎたりして、足跡の詳細がよく見えないんだ」ちくわ君は困った顔をした。「これを何とか分析可能な状態にしたいんだよ」

まず、ちくわ君は実際の問題をシミュレートする画像を作成した。

# 汚れた足跡のサンプル画像を作成
def create_muddy_footprint():
    """泥だらけの足跡画像をシミュレート"""
    img = np.random.randint(80, 120, (300, 400, 3), dtype=np.uint8)  # 泥の背景
    
    # 動物の足跡を描画(くぼみとして暗く)
    # 大きな肉球
    cv2.ellipse(img, (200, 180), (40, 35), 0, 0, 360, (40, 35, 30), -1)
    
    # 4つの指
    cv2.ellipse(img, (160, 130), (15, 20), 15, 0, 360, (35, 30, 25), -1)
    cv2.ellipse(img, (190, 120), (15, 20), 0, 0, 360, (35, 30, 25), -1)
    cv2.ellipse(img, (220, 125), (15, 20), -15, 0, 360, (35, 30, 25), -1)
    cv2.ellipse(img, (245, 140), (12, 18), -30, 0, 360, (35, 30, 25), -1)
    
    # ノイズと汚れを追加
    noise = np.random.normal(0, 20, img.shape).astype(np.int16)
    muddy = np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8)
    
    # 不均一な照明効果をシミュレート
    y, x = np.ogrid[:300, :400]
    mask = ((x - 100)**2 + (y - 150)**2) / 10000
    shadow = np.exp(-mask) * 50
    for i in range(3):
        muddy[:, :, i] = np.clip(muddy[:, :, i] - shadow, 0, 255)
    
    return muddy

muddy_footprint = create_muddy_footprint()

show_images([muddy_footprint], ['Muddy Footprint'], figsize=(8, 6))

写真を見ると、確かに泥だらけで、動物の足跡らしきものは見えるものの、詳細な形はよくわからなかった。

「よし、総合的なアプローチで処理してみよう」ちくわ君は意気込んだ。「今まで学んだ技術を組み合わせて、段階的に画像を改善していくんだ」

# 総合的な画像処理パイプライン
def analyze_footprint_complete(image):
    """足跡画像の包括的な分析処理"""
    
    print("画像処理パイプラインを開始...")
    
    # Step 1: ノイズ除去(バイラテラルフィルタ)
    print("Step 1: ノイズ除去")
    denoised = cv2.bilateralFilter(image, 15, 100, 100)
    
    # Step 2: コントラスト強調(ヒストグラム平坦化)
    print("Step 2: コントラスト強調")
    gray = cv2.cvtColor(denoised, cv2.COLOR_BGR2GRAY)
    enhanced = cv2.equalizeHist(gray)
    
    # Step 3: さらなるノイズ除去(ガウシアンフィルタ)
    print("Step 3: 平滑化")
    smoothed = cv2.GaussianBlur(enhanced, (5, 5), 0)
    
    # Step 4: シャープ化で詳細を強調
    print("Step 4: シャープ化")
    sharpen_kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], dtype=np.float32)
    sharpened = cv2.filter2D(smoothed, -1, sharpen_kernel)
    sharpened = np.clip(sharpened, 0, 255).astype(np.uint8)
    
    # Step 5: エッジ検出で輪郭を抽出
    print("Step 5: エッジ検出")
    edges = cv2.Canny(sharpened, 30, 100)
    
    # Step 6: モルフォロジー処理で輪郭を連結
    print("Step 6: 輪郭の連結")
    kernel = np.ones((3, 3), np.uint8)
    morphed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
    
    return denoised, enhanced, smoothed, sharpened, edges, morphed

# 段階的処理を実行
step1, step2, step3, step4, step5, step6 = analyze_footprint_complete(muddy_footprint)

# 処理過程を表示
process_images = [muddy_footprint, step1, step2, step3, step4, step5]
process_titles = ['Original', 'Denoised', 'Enhanced', 'Smoothed', 'Sharpened', 'Edges']

show_images(process_images, process_titles, figsize=(24, 4))

# 最終結果を大きく表示
show_images([muddy_footprint, step6], 
           ['Original Muddy Footprint', 'Final Processed Result'], 
           figsize=(12, 6))

print("\n足跡画像の分析処理が完了しました!")

処理が完了すると、最初はよく見えなかった足跡の形が、段階を追うごとにはっきりと見えるようになった。最終的には、動物の肉球と4本の指の形が明確に識別できるようになった。

「これはすごいですね!」はんぺん君は興奮した。「最初の写真では足跡かどうかもわからなかったのに、処理後は指の形まではっきり見えます。これなら動物の種類も特定できそうです」

「そうなんだ」ちくわ君は満足そうに微笑んだ。「一つ一つの処理は単純だけれど、適切に組み合わせることで、こんなに劇的な改善ができるんだよ。これが画像処理パイプラインの威力なんだ」

画像処理の原理を理解しよう

夕方になって、はんぺん君はようやく画像処理の面白さを理解し始めていた。「でも、どうしてこんなことができるんですか?コンピューターは写真をどう理解しているんでしょう?」

「いい質問だね」ちくわ君は木の実を噛みながら答えた。「実は、コンピューターにとって画像は数字の集まりなんだ。簡単な例を見てみよう」

# 画像の数値的な表現を理解する
def understand_image_structure():
    """画像の数値構造を理解するためのデモ"""
    
    # 小さなサンプル画像を作成(8x8ピクセル)
    small_img = np.array([
        [100, 100, 100, 200, 200, 100, 100, 100],
        [100, 100, 150, 250, 250, 150, 100, 100],
        [100, 150, 200, 255, 255, 200, 150, 100],
        [150, 200, 250, 255, 255, 250, 200, 150],
        [150, 200, 250, 255, 255, 250, 200, 150],
        [100, 150, 200, 255, 255, 200, 150, 100],
        [100, 100, 150, 250, 250, 150, 100, 100],
        [100, 100, 100, 200, 200, 100, 100, 100]
    ], dtype=np.uint8)
    
    # ガウシアンフィルタを適用
    filtered_img = cv2.GaussianBlur(small_img, (3, 3), 0)
    
    print("元画像の数値:")
    print(small_img)
    print("\nフィルタ後の数値:")
    print(filtered_img)
    
    # 視覚的に表示
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
    
    im1 = ax1.imshow(small_img, cmap='gray', interpolation='nearest')
    ax1.set_title('Original (8x8 pixels)')
    ax1.set_xticks(range(8))
    ax1.set_yticks(range(8))
    plt.colorbar(im1, ax=ax1)
    
    im2 = ax2.imshow(filtered_img, cmap='gray', interpolation='nearest')
    ax2.set_title('After Gaussian Filter')
    ax2.set_xticks(range(8))
    ax2.set_yticks(range(8))
    plt.colorbar(im2, ax=ax2)
    
    plt.tight_layout()
    plt.show()
    
    return small_img, filtered_img

original, filtered = understand_image_structure()

「おお!」はんぺん君は感動した。「画像が本当に数字の表なんですね。0が黒、255が白で、その間がグレーの濃淡を表しているんですか」

「その通り!」ちくわ君は嬉しそうに頷いた。「だから画像処理というのは、実は数学的な計算なんだよ。例えば、ぼかし処理は周囲のピクセルの平均を取る計算だし、シャープ化は隣り合うピクセルの差を強調する計算なんだ」

「カラー画像の場合は、赤、緑、青の3つのチャンネルがあって、それぞれが0-255の値を持っているんだ」

応用分野と将来への展望

日が傾き始めた頃、二人は今日の成果を振り返っていた。机の上には、処理前と処理後の写真が並べて置かれており、その違いは一目瞭然だった。

「今日学んだのは基礎の部分だけれど、応用は無限にあるんだ」ちくわ君は将来の可能性について語った。「例えば、医療分野では病気の早期発見に、農業では作物の生育状況の監視に、自動車産業では自動運転の技術に使われているよ」

「僕たちの森の研究にも、もっと活用できそうですね」はんぺん君は希望に満ちた声で言った。「動物の個体識別とか、植物の成長記録とか...」

「まさにその通りだ!」ちくわ君は嬉しそうに頷いた。「最近では人工知能と組み合わせて、さらに高度な画像認識も可能になってきている。例えば、写真を見ただけで動物の種類を自動判別したり、病気の兆候を見つけたりできるんだよ」

# 今後の学習への道筋
def future_learning_path():
    """画像処理の発展的な学習内容"""
    
    advanced_topics = [
        "機械学習との組み合わせ",
        "深層学習を用いた画像認識",
        "動画処理とリアルタイム解析", 
        "3次元画像処理",
        "医療画像解析",
        "リモートセンシング画像解析",
        "工業用画像検査システム"
    ]
    
    print("=== 画像処理の発展的な学習分野 ===")
    for i, topic in enumerate(advanced_topics, 1):
        print(f"{i}. {topic}")
    
    print("\n=== 推奨する次のステップ ===")
    print("1. より多くの実画像での練習")
    print("2. 異なる種類のノイズや歪みへの対処")
    print("3. 処理速度の最適化")
    print("4. 機械学習ライブラリ(scikit-learn、TensorFlow)の学習")
    print("5. 専門分野への応用(医療、産業、科学研究など)")

future_learning_path()

「すごい時代になったんですね」はんぺん君は感慨深げにつぶやいた。「でも、基本をしっかり理解することが大切だということもよくわかりました」

今日の学びを振り返って

夕日が研究所の窓を照らし始めた頃、はんぺん君は満足そうに立ち上がった。「今日は本当にたくさんのことを学べました。最初は難しそうだと思った画像処理も、実際にやってみると理解できることがわかりました」

「君の熱心さと良い質問のおかげで、僕も教え甲斐があったよ」ちくわ君は微笑んだ。「画像処理は見た目の変化がわかりやすいから、プログラミングの入門としてもとても良い分野なんだ」

「特に今日学んだMatplotlibでの表示方法は、研究発表でも役立ちそうです」はんぺん君は目を輝かせた。「処理前後の比較を見やすく表示できるのは、とても重要ですよね」

「その通りだ!」ちくわ君は頷いた。「研究では、結果を他の人にわかりやすく伝えることがとても大切なんだ。Matplotlibは、そのための強力なツールなんだよ」

「明日からの研究が楽しみです」はんぺん君は希望に満ちた声で言った。「今度は動画の処理にも挑戦してみたいです。リアルタイムで動物の行動を追跡できたら面白そうですね」

「それは素晴らしいアイデアだね」ちくわ君は励ました。「動画は連続した画像の集まりだから、今日学んだ技術の延長で処理できるよ。動体追跡や行動分析など、また新しい世界が広がるはずだ」

はんぺん君が帰った後、ちくわ君は今日処理した画像を眺めながら思った。技術の進歩によって、かつては不可能だと思われていた画像の復元や分析が、今では比較的簡単にできるようになった。しかし、その技術を有効活用するためには、基礎をしっかりと理解することが何より大切なのだ。

そして、適切な可視化ツールを使って結果を効果的に表示することも、研究においては欠かせない技術である。今日学んだ平滑化フィルタから始まり、シャープ化、エッジ検出まで、それぞれの手法には適切な用途があり、組み合わせることでより強力な解析が可能になる。

森の研究所には、今夜も新しい発見への期待と、技術の可能性に対する希望が満ちていた。明日もまた、OpenCVとMatplotlibを使って、自然界の謎に迫る研究が続くことだろう。そして、より多くの研究者がこれらの技術を使って、世界の理解を深めていくのである。

教育 OpenCV 画像処理 Python Matplotlib