猫画像を三角形で分割して、塗ってみた話(Python+OpenCVで遊んでみた)

ローポリ猫

たまには、猫画像でちょっと遊んでみようかと 仕事でもプログラムは書いていますが、たまに「何の役にも立たないけど楽しいこと」をやってみたくなり、、、 今日はそんな気分で、猫の画像を使ってちょっとした画像アートのようなことをしてみたので、記録として残してみようと思います。

技術的にはPythonとOpenCVを使っていますが、文系出身の私でも感覚的に楽しめる内容なので、ぜひお気軽に読んでいただけたらと思います。。🐾

(この記事は広告を含みます)


1. 目指す完成形

最終的にはこんな仕上がりにしたい!というイメージ👇

  • 元画像を残しつつ、アートっぽく三角形で分割
  • お目目キラキラ、鼻ピンクふんわり
  • 隙間やカクカク感がなく、全体がなめらか

2. 猫画像をPythonで読み込む(癒しから始めましょう)

まずは猫画像を用意して、それをPythonで読み込んでみます。
癒される画像じゃないと、そもそもやる気が出ませんからね。。

OpenCVはコチラの記事を参考に

kunio-ud-all.com

3. 使用するライブラリ

pip install opencv-python numpy matplotlib

4. やったことのざっくり手順

1. 猫画像を読み込む
2. 特徴点を抽出(目・鼻を重視)
3. ドロネー三角形分割を行う
4. 各三角形ごとに平均色で塗る
5. 明るさや目の中をちょこっと補正
6. 最後にふんわりブラーをかけて完成!

5. 試行錯誤のポイント

特徴点を大量に拾う「ハイポリ化」
  • goodFeaturesToTrack() で点を2000点以上取得
  • qualityLevel をかなり下げて細かい部分まで抽出
お目目を守る「フォーカス点」
  • 目や鼻など、表情に関わるパーツは手動で補完
  • 絶妙な位置に点を追加することで、自然な印象に
塗り色をちょこっと明るく補正
  • 三角形ごとの平均色が暗くなりすぎないよう、明度を少しだけ上げる
キャンバス初期化は「白」
  • OpenCVの fillConvexPoly() は境界に隙間ができやすい
  • 初期状態を「白」で塗っておくことで黒い隙間問題を回避
最後のひと手間「ブラー処理」
  • 三角形の境界をなめらかに見せるため、軽めのガウシアンブラーでふんわり演出

6. 最終コード(コピペOK)

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

# === 画像読み込み ===
img = cv2.imread("IMG_5835.JPG")
if img is None:
    raise FileNotFoundError("画像ファイルが見つかりませんでした。ファイル名やパスを確認してください。")

h, w = img.shape[:2]

# === 特徴点の検出(ハイポリ)===
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
points = cv2.goodFeaturesToTrack(
    gray,
    maxCorners=2000,
    qualityLevel=0.0001,
    minDistance=1
)
points = points.reshape(-1, 2)

# === 境界補完点 ===
border_points = np.array([
    [0, 0], [w // 2, 0], [w - 1, 0],
    [0, h // 2], [w - 1, h // 2],
    [0, h - 1], [w // 2, h - 1], [w - 1, h - 1]
], dtype=np.float32)

# === 顔・目の注目ポイント ===
face_focus = np.array([
    [w // 2 - 50, h // 2 + 20],   # 鼻
], dtype=np.float32)

eye_focus = np.array([
    # 左目まわり
    [w // 2 - 60, h // 2 - 45],
    [w // 2 - 50, h // 2 - 40],
    [w // 2 - 55, h // 2 - 35],
    # 右目まわり
    [w // 2 + 45, h // 2 - 45],
    [w // 2 + 55, h // 2 - 40],
    [w // 2 + 50, h // 2 - 35],
], dtype=np.float32)

# === 全ての点を統合 ===
all_points = np.concatenate([points, border_points, face_focus, eye_focus])

# === ドロネー三角形分割 ===
subdiv = cv2.Subdiv2D((0, 0, w, h))
for p in all_points:
    subdiv.insert((p[0], p[1]))
triangleList = subdiv.getTriangleList()

# === キャンバス作成(白背景で初期化)===
canvas = np.full_like(img, fill_value=255)

# === 各三角形を平均色で塗る(補正・ノイズ除去あり)===
for t in triangleList:
    pts = t.reshape(3, 2).astype(np.int32)

    if np.any(pts < 0) or np.any(pts[:, 0] > w) or np.any(pts[:, 1] > h):
        continue

    area = cv2.contourArea(pts)
    if area < 5:
        continue

    mask = np.zeros(img.shape[:2], dtype=np.uint8)
    cv2.fillConvexPoly(mask, pts, 255)
    mean_color = cv2.mean(img, mask=mask)[:3]
    brightness = np.mean(mean_color)

    # 明るさ補正(暗ければ強めに補正)
    if brightness < 60:
        bright_color = tuple(min(255, int(c * 1.15)) for c in mean_color)
    else:
        bright_color = tuple(min(255, int(c * 1.05)) for c in mean_color)

    cv2.fillConvexPoly(canvas, pts, bright_color)

# === 最終仕上げ:スムース処理でふんわり演出 ===
canvas_rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
smoothed = cv2.GaussianBlur(canvas_rgb, (3, 3), sigmaX=0.5)

# === 表示 ===
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.imshow(img_rgb)
plt.axis("off")
plt.title("original")

plt.subplot(1, 2, 2)
plt.imshow(smoothed)
plt.axis("off")
plt.title("triangulated (final ver.)")

plt.tight_layout()
plt.show()

7. 最後に:アートは試行錯誤が楽しい

最初は「猫画像をちょっと遊んでみよう」くらいの気持ちでしたが、 やってみると、

  • 三角形の数や大きさで雰囲気が全然違う
  • 目の周りは繊細に作らないと可愛さが出ない
  • ブラーや補色のちょっとした工夫がすごく大事

などなど、ちょっとした数値の違いで印象が大きく変わる世界でした。 自動化は無理だなぁと思い、こういうのこそ、AIさんに任せるのがいいのかな?

でも、画像処理の勉強になりました。