🔳用途
・画像の、エッジ部分を強調して出す。
・線形のフィルタリングなので、Sobelフィルタ単体よりも、よりエッジの値が上がる。
🔳結果
🔳ソースコード
# ================================
# 1セル完結テンプレ(Colab)
# Driveマウント -> 画像選択 -> 表示 -> Sobel -> Unsharp -> Sobel(先鋭後)
# OpenCVなし / NumPy + PIL + matplotlib + ipywidgets
# ================================
#--- install
!pip -q install ipywidgets
import os, glob
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display, clear_output
from google.colab import drive
drive.mount('/content/drive')
# ========= ここを必要なら変更 =========
# Drive内のどこから探すか(重いならフォルダを絞る)
# ROOT = "/content/drive/MyDrive"
ROOT = "/content/drive/MyDrive/画像フィルタ_25-26_001"
# =====================================
# ---------- 画像処理 基本関数 ----------
def to_gray_np(img_np):
"""img_np: (H,W,3) or (H,W) 0..255 -> float32 gray"""
if img_np.ndim == 2:
return img_np.astype(np.float32)
r, g, b = img_np[..., 0], img_np[..., 1], img_np[..., 2]
return (0.299*r + 0.587*g + 0.114*b).astype(np.float32)
def conv2d_gray(gray, kernel):
gray = gray.astype(np.float32)
k = np.array(kernel, dtype=np.float32)
kh, kw = k.shape
pad_h, pad_w = kh//2, kw//2
padded = np.pad(gray, ((pad_h, pad_h), (pad_w, pad_w)), mode="edge")
out = np.zeros_like(gray, dtype=np.float32)
for y in range(out.shape[0]):
for x in range(out.shape[1]):
region = padded[y:y+kh, x:x+kw]
out[y, x] = np.sum(region * k)
return out
def sobel(gray):
"""gray: (H,W) float32"""
Kx = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]], dtype=np.float32)
Ky = np.array([[1, 2, 1],
[0, 0, 0],
[-1, -2, -1]], dtype=np.float32)
gx = conv2d_gray(gray, Kx)
gy = conv2d_gray(gray, Ky)
mag = np.sqrt(gx*gx + gy*gy)
mag = mag / (mag.max() + 1e-8) * 255.0
return gx, gy,mag.astype(np.uint8)
def gaussian_kernel_1d(radius, sigma):
x = np.arange(-radius, radius+1, dtype=np.float32)
k = np.exp(-(x*x)/(2*sigma*sigma))
k /= (k.sum() + 1e-8)
return k
def conv1d_h(gray, k):
r = len(k) // 2
padded = np.pad(gray, ((0,0),(r,r)), mode="edge")
out = np.zeros_like(gray, dtype=np.float32)
for y in range(gray.shape[0]):
for x in range(gray.shape[1]):
out[y,x] = np.sum(padded[y, x:x+2*r+1] * k)
return out
def conv1d_v(gray, k):
r = len(k)//2
padded = np.pad(gray, ((r,r),(0,0)), mode="edge")
out = np.zeros_like(gray, dtype=np.float32)
for y in range(gray.shape[0]):
for x in range(gray.shape[1]):
out[y,x] = np.sum(padded[y:y+2*r+1, x] * k)
return out
def gaussian_blur(gray, radius=2, sigma=1.2):
k = gaussian_kernel_1d(radius, sigma)
tmp = conv1d_h(gray.astype(np.float32), k)
out = conv1d_v(tmp, k)
return out
def unsharp_mask(gray, radius=2, sigma=1.2, amount=1.2, threshold=2.0):
g = gray.astype(np.float32)
blur = gaussian_blur(g, radius=radius, sigma=sigma)
detail = g - blur
if threshold > 0:
mask = np.abs(detail) >= threshold
detail = detail * mask
sharp = g + amount * detail
sharp = np.clip(sharp, 0, 255)
return sharp.astype(np.uint8), blur.astype(np.uint8)
def show_results(img_rgb, gray, edge, blur, sharp, edge_sharp, title_path=""):
h, w = img_rgb.shape[:2]
disp_w = min(900, w)
disp_h = int(disp_w * h / w)
img_disp = Image.fromarray(img_rgb).resize((disp_w, disp_h))
plt.figure(figsize=(18,5))
plt.subplot(1,5,1); plt.title("Original"); plt.imshow(img_disp); plt.axis("off")
plt.subplot(1,5,2); plt.title("Gray"); plt.imshow(gray, cmap="gray"); plt.axis("off")
plt.subplot(1,5,3); plt.title("Sobel"); plt.imshow(edge, cmap="gray"); plt.axis("off")
plt.subplot(1,5,4); plt.title("Unsharp"); plt.imshow(sharp, cmap="gray"); plt.axis("off")
plt.subplot(1,5,5); plt.title("Sobel on Unsharp"); plt.imshow(edge_sharp, cmap="gray"); plt.axis("off")
plt.suptitle(title_path, y=1.02, fontsize=11)
plt.show()
# ---------- Drive内の画像を収集 ----------
exts = ("*.png","*.jpg","*.jpeg","*.bmp","*.webp")
paths = []
for e in exts:
paths += glob.glob(os.path.join(ROOT, "**", e), recursive=True)
paths = sorted(paths)
if len(paths) == 0:
raise FileNotFoundError(f"画像が見つからないよ。ROOT={ROOT} を画像フォルダに変更してね。")
MAX_LIST = 2000
paths_view = paths[:MAX_LIST]
# ---------- UI ----------
dd = widgets.Dropdown(
options=paths_view,
description="Image:",
layout=widgets.Layout(width="95%")
)
radius = widgets.IntSlider(value=2, min=1, max=8, step=1, description="radius")
sigma = widgets.FloatSlider(value=1.2, min=0.5, max=5.0, step=0.1, description="sigma")
amount = widgets.FloatSlider(value=1.2, min=0.0, max=3.0, step=0.1, description="amount")
thresh = widgets.FloatSlider(value=2.0, min=0.0, max=20.0, step=0.5, description="threshold")
btn_run = widgets.Button(description="Run", button_style="success")
out = widgets.Output()
def run_process(_):
with out:
clear_output(wait=True)
path = dd.value
img = np.array(Image.open(path).convert("RGB"))
gray = to_gray_np(img)
_, _, edge = sobel(gray)
sharp, blur = unsharp_mask(gray, radius=radius.value, sigma=sigma.value, amount=amount.value, threshold=thresh.value)
_, _, edge_sharp = sobel(sharp.astype(np.float32))
show_results(img, gray, edge, blur, sharp, edge_sharp, title_path=path)
print(f"Loaded: {path}")
print(f"Params: radius={radius.value}, sigma={sigma.value}, amount={amount.value}, threshold={thresh.value}")
if len(paths) > MAX_LIST:
print(f"※画像が多いので表示候補は先頭 {MAX_LIST} 件だけ。ROOTを絞ると探しやすいよ。")
btn_run.on_click(run_process)
display(dd, widgets.HBox([btn_run]), widgets.VBox([radius, sigma, amount, thresh]), out)
# 初回自動実行
run_process(None)

