iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0
生成式 AI

AI 情感偵測:從聲音到表情的多模態智能應用系列 第 5

【貓也要打卡 | AI 的貓臉辨識】

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250924/20178322W0YVRi9KbT.jpg

貓咪圖片來源:皇阿瑪的後宮生活

在人臉辨識的應用場景中,我們已經非常熟悉它出現在門禁系統、考勤打卡、以及手機解鎖等功能應用。
但如果將這項技術應用到「貓咪」身上,會是什麼樣的體驗?

__本 DEMO 嘗試設計一個「貓臉辨識系統」:
透過 OpenCV 偵測貓咪的臉部 --> 從資料庫比對是哪一隻貓 --> 最後在介面上顯示辨識結果

想像一下,一個家庭有多隻貓咪,只要攝影機拍攝,系統就能直接顯示:「這是 Jojo」或「這是 Momo」。這樣的技術甚至能擴展應用,例如:
自動紀錄哪隻貓先回家、追蹤每日進食次數、智慧寵物門禁

1) 先裝環境

pip install opencv-python scikit-learn numpy

你需要 OpenCV 的 貓臉 Haar 模型:haarcascade_frontalcatface.xml
多數 OpenCV 會自帶在 cv2.data.haarcascades 路徑;若沒有,請自行下載並放在腳本同資料夾。

2) 準備資料

把你的資料整理成這樣:

cats/
  Mimi/
    001.jpg
    002.jpg
    ...
  Kuro/
    a.jpg
    b.jpg
    ...

每隻貓建議至少 15–30 張、不同角度/光線(正面為主)。
https://ithelp.ithome.com.tw/upload/images/20250924/20178322dJStfB47wD.jpg
圖片來源:我家貓貓JoJo

3) 單檔腳本(存成 catfaces_demo.py)

import os
import cv2
import sys
import glob
import json
import joblib
import numpy as np
from sklearn.neighbors import KNeighborsClassifier

# -----------------------
# 可調參數(簡化版本)
# -----------------------
DATA_DIR = "cats"                 # 資料集根目錄:cats/<cat_name>/*.jpg
MODEL_PATH = "cat_knn.pkl"        # 模型檔
LABELS_PATH = "labels.json"       # 貓名與數字 id 對照
FACE_SIZE = (128, 128)            # 取樣尺寸
K = 3                              # KNN 的 K 值
CASCADE_NAME = "haarcascade_frontalcatface.xml"

def get_cascade_path():
    """盡量自動找到 cat face cascade,找不到就用當前資料夾"""
    default_path = os.path.join(cv2.data.haarcascades, CASCADE_NAME)
    if os.path.exists(default_path):
        return default_path
    local = os.path.join(os.path.dirname(__file__), CASCADE_NAME)
    if os.path.exists(local):
        return local
    raise FileNotFoundError(
        f"找不到 {CASCADE_NAME}。\n"
        f"請將檔案放在:{cv2.data.haarcascades} 或腳本同目錄。"
    )

CAT_CASCADE = cv2.CascadeClassifier(get_cascade_path())

# -----------------------
# 工具函數
# -----------------------
def detect_cat_faces(img_bgr):
    """回傳偵測到的 cat face bounding boxes"""
    gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
    faces = CAT_CASCADE.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=3, minSize=(60, 60))
    return faces

def face_to_feature(img_bgr, box):
    """裁切貓臉 -> 灰階 -> resize -> 向量 (L2 normalize)"""
    x, y, w, h = box
    face = img_bgr[y:y+h, x:x+w]
    gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)
    gray = cv2.resize(gray, FACE_SIZE, interpolation=cv2.INTER_AREA)
    vec = gray.flatten().astype(np.float32)
    # 簡單 L2 normalize
    norm = np.linalg.norm(vec) + 1e-8
    return (vec / norm)

def scan_dataset(data_dir=DATA_DIR):
    """掃描 cats/<name>/*.jpg → (X, y, label_map)"""
    X, y = [], []
    name2id, id2name = {}, {}
    next_id = 0

    if not os.path.isdir(data_dir):
        raise RuntimeError(f"找不到資料夾:{data_dir}")

    for name in sorted(os.listdir(data_dir)):
        folder = os.path.join(data_dir, name)
        if not os.path.isdir(folder):
            continue
        if name not in name2id:
            name2id[name] = next_id
            id2name[next_id] = name
            next_id += 1
        cid = name2id[name]

        imgs = []
        for ext in ("*.jpg", "*.jpeg", "*.png", "*.JPG", "*.JPEG", "*.PNG"):
            imgs.extend(glob.glob(os.path.join(folder, ext)))
        if not imgs:
            print(f"[警告] {name} 沒有圖片,略過。")
            continue

        kept = 0
        for p in imgs:
            img = cv2.imread(p)
            if img is None:
                continue
            faces = detect_cat_faces(img)
            if len(faces) == 0:
                continue
            # 取第一張臉
            feat = face_to_feature(img, faces[0])
            X.append(feat)
            y.append(cid)
            kept += 1

        print(f"[資料] {name} 取樣 {kept} 張可用臉部")

    if len(X) < 2 or len(set(y)) < 2:
        raise RuntimeError("樣本不足(至少需要 >=2 隻貓且每隻有偵測到臉的圖片)。")

    X = np.vstack(X)
    y = np.array(y, dtype=np.int64)
    return X, y, {"name2id": name2id, "id2name": {int(k): v for k, v in id2name.items()}}

def train():
    X, y, labels = scan_dataset(DATA_DIR)
    print(f"[訓練] 總樣本:{len(X)},類別數:{len(set(y))}")

    knn = KNeighborsClassifier(n_neighbors=K, metric="cosine")
    knn.fit(X, y)

    joblib.dump(knn, MODEL_PATH)
    with open(LABELS_PATH, "w", encoding="utf-8") as f:
        json.dump(labels, f, ensure_ascii=False, indent=2)
    print(f"[完成] 模型已存:{MODEL_PATH},標籤:{LABELS_PATH}")

def load_model():
    if not os.path.exists(MODEL_PATH) or not os.path.exists(LABELS_PATH):
        raise RuntimeError("找不到模型或標籤,請先執行:python catfaces_demo.py train")
    knn = joblib.load(MODEL_PATH)
    with open(LABELS_PATH, "r", encoding="utf-8") as f:
        labels = json.load(f)
    id2name = {int(k): v for k, v in labels["id2name"].items()}
    return knn, id2name

def predict_image(img_path, show=True):
    knn, id2name = load_model()
    img = cv2.imread(img_path)
    if img is None:
        raise RuntimeError(f"讀不到圖片:{img_path}")
    faces = detect_cat_faces(img)
    if len(faces) == 0:
        print("沒有偵測到貓臉。")
        if show:
            cv2.imshow("cat", img); cv2.waitKey(0)
        return

    for (x, y, w, h) in faces:
        feat = face_to_feature(img, (x, y, w, h)).reshape(1, -1)
        pred = knn.predict(feat)[0]
        proba = 1 - knn.kneighbors(feat, n_neighbors=K, return_distance=True)[0].mean()  # 粗略分數
        name = id2name.get(int(pred), "Unknown")
        cv2.rectangle(img, (x, y), (x+w, y+h), (0, 200, 0), 2)
        cv2.putText(img, f"{name} ({proba:.2f})", (x, y-8),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 200, 0), 2, cv2.LINE_AA)
    if show:
        cv2.imshow("predict", img); cv2.waitKey(0)
    print("預測完成。")

def webcam():
    knn, id2name = load_model()
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    if not cap.isOpened():
        cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        raise RuntimeError("開啟攝影機失敗。")

    print("[提示] 按 Q 離開")
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        faces = detect_cat_faces(frame)
        for (x, y, w, h) in faces:
            feat = face_to_feature(frame, (x, y, w, h)).reshape(1, -1)
            pred = knn.predict(feat)[0]
            proba = 1 - knn.kneighbors(feat, n_neighbors=K, return_distance=True)[0].mean()
            name = id2name.get(int(pred), "Unknown")
            cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 200, 255), 2)
            cv2.putText(frame, f"{name} ({proba:.2f})", (x, y-8),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 200, 255), 2, cv2.LINE_AA)

        cv2.imshow("Cat Face ID (Q to quit)", frame)
        if (cv2.waitKey(1) & 0xFF) in (ord('q'), ord('Q')):
            break

    cap.release()
    cv2.destroyAllWindows()

# -----------------------
# CLI
# -----------------------
def main():
    if len(sys.argv) < 2:
        print("用法:")
        print("  訓練: python catfaces_demo.py train")
        print("  單張: python catfaces_demo.py predict <image_path>")
        print("  攝影機:python catfaces_demo.py webcam")
        sys.exit(0)

    cmd = sys.argv[1]
    if cmd == "train":
        train()
    elif cmd == "predict":
        if len(sys.argv) < 3:
            print("請提供圖片路徑,例如:python catfaces_demo.py predict test.jpg")
            sys.exit(1)
        predict_image(sys.argv[2], show=True)
    elif cmd == "webcam":
        webcam()
    else:
        print("未知指令。可用:train / predict / webcam")

if __name__ == "__main__":
    # 需要 joblib(sklearn 會裝),若沒有可改用 pickle
    try:
        import joblib  # noqa
    except Exception:
        os.system("pip install joblib")
        import joblib  # noqa
    main()

4) 怎麼跑

# 1) 先訓練
python catfaces_demo.py train

# 2) 單張圖片測試
python catfaces_demo.py predict path/to/your/cat.jpg

# 3) 攝影機即時辨識
python catfaces_demo.py webcam

經過一步步的資料準備、模型訓練與測試,我們已經完成了這個「貓臉辨識系統」的小專案 DEMO。
這個過程不只是讓電腦學會分辨不同的貓咪,更重要的是,你已經親手走過了 從影像處理、特徵擷取到模型應用 的完整流程。

現在,就換你動手試試看吧!
打開電腦,準備好資料夾,把屬於你家貓咪的照片放進去,執行程式,看看螢幕會不會告訴你:「這是你家 Jojo」或「這是 Momo」。

當 AI 開始認得你的貓咪時,你就成功地把抽象的技術變成一個 有趣又實用的生活小應用。這就是學習電腦視覺最迷人的地方──從零到一,親手做出屬於自己的 AI 小工具。


上一篇
【🎙️ 情緒測謊機 | 讓 AI 聽懂你語音的真心話】
下一篇
【從夜市烤肉到 AI 多模態融合 | Early Fusion v.s. Late Fusion 的故事】
系列文
AI 情感偵測:從聲音到表情的多模態智能應用6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言