1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

電動歯ブラシのここが汚れるの嫌すぎて、猫の手を借りた話【3Dプリンター × Python】

Posted at

電動歯ブラシを使っている方なら分かると思うのですが、
「ブラシだけ地味に汚れる問題」 ありませんか?

毎回「なんだかイヤだな…」と思いながら洗っていたのですが、
ふと閃いたんです

“これ、かわいく解決できるのでは?”


猫の手を借りられなかったので、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 × 色変換 × グリッド化 は デザイン系の汎用パターン として使える!!!

最後に

日常のモヤモヤを、
技術で便利に・かわいく解決できる
そんな体験をもっと共有できたら嬉しいです💌

全体コード

全体コードはこちら↓
python
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()



1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?