電動歯ブラシを使っている方なら分かると思うのですが、
「ブラシだけ地味に汚れる問題」 ありませんか?
毎回「なんだかイヤだな…」と思いながら洗っていたのですが、
ふと閃いたんです
“これ、かわいく解決できるのでは?”
猫の手を借りられなかったので、3Dプリンターを借りた🐈
最初は「猫の手を借りよう!」と思いました。
- 近所の猫 → 借りられない
- 友達の猫 → 断られる
- 猫カフェの猫 → ビジネス猫(ビジネスでしか動かない)
という悲しい事実を受け入れ、
Blender で“従順な猫”を自作 → 3Dプリンターで造形
思ったよりメンチ切ってる顔になりましたが、それも愛嬌
しかし…3Dプリンター最大の悩みに直面する
かわいいのはいい。
でも フィラメント(色)が決まらない
- 最近フィラメント研究で多色買いしすぎた結果、逆に決まらない問題
- 「白?黒?ピンク?ゴールド?」
- フィラメント棚を前に30分悩む
3Dプリンター使っている人なら、
「造形より色選びで時間溶ける問題」
共感してもらえるはず
🔍 そもそもフィラメントって?
3Dプリンターで使う糸状の熱可塑性樹脂
種類によって扱いが全然違う
| 種類 | 特徴 | オススメ度 |
|---|---|---|
| PLA | 扱いやすい / 色が多い | ⭐⭐⭐⭐⭐ |
| PETG | 水・熱に強い / 若干扱いにクセ | ⭐⭐⭐⭐ |
| ABS | 強いが反りやすい | ⭐⭐ |
色も質感も多すぎる
→ 決まらん
そこでエンジニア的思考が発動
「悩むならツール作ればよくない?」
対象物(猫の手)だけ色を変えて、
9色を 3×3 で一発比較するツールを Python で作ることにしました。
使った技術は以下の2つ:
- OpenCV(GrabCut) → 対象物だけくり抜く
- tkinter → デスクトップアプリ化
完成イメージはこちら👇
#️⃣ ① GrabCut で対象物だけくり抜く(mask 作成)
写真から“猫だけ”抽出するために GrabCut を使用
import cv2
import numpy as np
def cutout_object(image):
h, w = image.shape[:2]
# ① 初期マスク(全画素=背景仮定)
mask = np.zeros((h, w), np.uint8)
# ② GrabCut のバックグラウンド/前景モデル
bgdModel = np.zeros((1, 65), np.float64)
fgdModel = np.zeros((1, 65), np.float64)
# ③ 画像全体を囲む矩形
rect = (10, 10, w-20, h-20)
# ④ GrabCut 実行
cv2.grabCut(image, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
# ⑤ 0/1 の二値マスクへ変換
mask_bin = np.where((mask == 2) | (mask == 0), 0, 1).astype("uint8")
return mask_bin
◎ GrabCut を使う理由
- 矩形を与えるだけで背景を自動判定
- 輪郭抜きの手間がない
- 3Dプリント用の“単色オブジェクト”と相性が良い
② 指定した色だけピンポイントで塗り替える
作ったマスクを使い、対象物だけ任意の色に塗り替えます
色はこんな感じで指定
def apply_background(original_img, mask, color):
result = original_img.copy()
# 色レイヤーを作成
color_layer = np.zeros_like(original_img, dtype=np.uint8)
color_layer[:] = color
# mask を 3ch へ
mask3 = mask[:, :, np.newaxis]
# 背景は original、対象物だけ color を適用
result = original_img * (1 - mask3) + color_layer * mask3
return result.astype(np.uint8)
◎ 合成式の解説
-
original_img * (1 - mask3)→ 背景を残す -
color_layer * mask3→ 対象物だけ塗る - 足すだけで透明合成と同じ効果が得られる
③ 9色を 3×3 のグリッドでまとめる
def make_grid(images, size=(300, 300)):
grid = np.zeros((size[1] * 3, size[0] * 3, 3), dtype=np.uint8)
for idx, img in enumerate(images):
r = idx // 3
c = idx % 3
img_resized = cv2.resize(img, size)
grid[r*size[1]:(r+1)*size[1], c*size[0]:(c+1)*size[0]] = img_resized
return grid
◎ この形式のメリット
- 「色比較投稿」みたいで見やすい
- 3×3 以外にも拡張が容易
完成:Pythonで色比較ツールを作ってフィラメント迷宮から脱出した
結果、
フィラメントの色選びが一瞬で終わるようになりました
同じモデルを別色で印刷するときにも便利
✅ このアプリの応用例
- 3Dプリンターのフィラメント色比較
- プロダクトデザインの色検討
- ロゴ・アイコンのカラーテーマ生成
- UI デザインのカラーパレット確認
- ゲーム素材の色差分自動生成
- ハンドメイド作品のカラー提案
GrabCut × 色変換 × グリッド化 は デザイン系の汎用パターン として使える!!!
最後に
日常のモヤモヤを、
技術で便利に・かわいく解決できる
そんな体験をもっと共有できたら嬉しいです💌
全体コード
全体コードはこちら↓
import cv2
import numpy as np
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk
import os
# ------------------------
# 対象物をくり抜く関数(GrabCut)
# ------------------------
def cutout_object(image):
h, w = image.shape[:2]
mask = np.zeros((h, w), np.uint8)
rect = (10, 10, w-20, h-20)
bgdModel = np.zeros((1, 65), np.float64)
fgdModel = np.zeros((1, 65), np.float64)
cv2.grabCut(image, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
# mask2 = 1(前景) / 0(背景)
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype("uint8")
return mask2
# ------------------------
# 対象物部分に色を入れる
# ------------------------
def apply_background(original_img, mask, color):
result = original_img.copy()
# 色レイヤー
color_layer = np.zeros_like(original_img, dtype=np.uint8)
color_layer[:] = color
mask3 = mask[:, :, np.newaxis] # 3ch化
# 対象物(mask=1)だけ色で塗る
result = original_img * (1 - mask3) + color_layer * mask3
return result.astype(np.uint8)
# ------------------------
# 3×3 に並べて1枚の画像へ
# ------------------------
def make_grid(images, size=(300, 300)):
grid = np.zeros((size[1] * 3, size[0] * 3, 3), dtype=np.uint8)
for idx, img in enumerate(images):
r = idx // 3
c = idx % 3
img_resized = cv2.resize(img, size)
grid[r * size[1]:(r + 1) * size[1], c * size[0]:(c + 1) * size[0]] = img_resized
return grid
# ------------------------
# GUIの動作
# ------------------------
def select_file():
global original_img, selected_path
path = filedialog.askopenfilename(
filetypes=[
("PNG files", "*.png"),
("JPG files", "*.jpg"),
("JPEG files", "*.jpeg"),
("All Image Files", "*.png;*.jpg;*.jpeg"),
("All Files", "*.*")
]
)
if not path:
return
selected_path = path
original_img = cv2.imread(path)
label_file.config(text=f"選択中: {path}")
def run_process():
if original_img is None:
label_file.config(text="画像がありません")
return
label_result.config(text="進行中…(1/4)くり抜き処理中..")
root.update()
# 1. 対象物のマスク
mask = cutout_object(original_img)
label_result.config(text="進行中…(2/4)色の読み込み..")
root.update()
# 2. カラーの読み込み
colors = []
for entry in color_entries:
txt = entry.get().strip()
if txt:
try:
rgb = tuple(map(int, txt.split(",")))
if len(rgb) == 3:
colors.append(rgb[::-1]) # BGRへ変換
except:
pass
if not colors:
colors = [(255, 255, 255)] # デフォルト白
# 3. 色塗り画像を生成
label_result.config(text="進行中…(3/4)画像生成中..")
root.update()
created_images = []
for col in colors[:9]:
out = apply_background(original_img, mask, col)
created_images.append(out)
# 足りないところは白で埋める
while len(created_images) < 9:
created_images.append(np.ones_like(original_img) * 255)
# 4. グリッド化
label_result.config(text="進行中…(4/4)グリッド生成中..")
root.update()
grid_img = make_grid(created_images)
# 出力パス(元画像と同じ場所)
out_path = os.path.join(os.path.dirname(selected_path), "result_grid.png")
cv2.imwrite(out_path, grid_img)
label_result.config(text=f"完了!結果を保存しました → {out_path}")
# ------------------------
# Tkinter GUI
# ------------------------
root = tk.Tk()
root.title("Color choice🐈")
original_img = None
selected_path = ""
frame = tk.Frame(root)
frame.pack(padx=10, pady=10)
btn = tk.Button(frame, text="画像を選択", command=select_file)
btn.grid(row=0, column=0, sticky="w")
label_file = tk.Label(frame, text="ファイル未選択")
label_file.grid(row=1, column=0, sticky="w")
# 色入力欄(最大9個)
tk.Label(frame, text="対象物に塗る色 (R,G,B) を最大9色入力").grid(row=2, column=0, pady=5)
color_entries = []
for i in range(9):
e = tk.Entry(frame, width=20)
e.grid(row=3 + i, column=0)
color_entries.append(e)
btn_run = tk.Button(frame, text="実行", command=run_process)
btn_run.grid(row=12, column=0, pady=10)
label_result = tk.Label(frame, text="")
label_result.grid(row=13, column=0)
root.mainloop()