10
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?

猫用ベッドを定点観測して最高の瞬間を通知させよう with ClaudeCode

Last updated at Posted at 2025-12-20

本記事は、リンクアンドモチベーション Advent Calendar 2025 の21日目になります。

はじめに

リンクアンドモチベーションでバックエンド中心にプロダクト開発をしているymk25です。

ちょうど昨年の今頃は、入手難易度Sアイテムの確保に奮闘しているサンタクロース達に想いを馳せながら駆け込み開発をしていましたね。

さて、今年も駆け込みましょう。

何か作ろう

何を作ろう。

そういえば最近、我が家の3匹の猫たち全員が気に入って利用頻度の高いベッドがあり、さらに冬の寒さが手伝ったことで、猫たちが一箇所に集まり寄り添って寝ている絶景を見ることができるようになりました。

この絶景を見つけ次第、猫たちの睡眠の妨げにならぬよう最小の絶叫をあげ、写真アプリの一面が埋め尽くされるまでシャッターを押すのが生きがいなのですが、如何せん、業務中にはこの絶景を何度か見逃しているような気がする…。

これは我が人生において甚大な損失、早急に対策を立てなければならない。

猫用ベッドを定点観測し、2匹以上集まった瞬間を自動検知して通知させよう

今回の相棒には ClaudeCode を召喚し、年の瀬のワガママを聞いてもらいましょう。

要件

  • PCに接続したWebカメラで猫ベッドを定点観測
  • 「猫」だけをリアルタイム検出
  • 猫ベッド内に 2匹以上の猫 が一定時間存在したら検知
  • 検知時に、
    • PC(macOS)へ通知
    • プレビュー画面にもド派手にメッセージを表示して気づかせて欲しい

技術スタック

  • Python 3.10+
  • OpenCV
    • Webカメラからのフレーム取得
    • プレビュー表示( ≒ 猫用ベッドのライブ配信)
  • Ultralytics YOLOv8(yolov8m.pt)
    • 物体検知、COCO学習済みモデルを使用
  • Pillow
    • 日本語テキスト描画
  • colorsys
    • 虹色グラデーション生成

全体構成

Webカメラ
   ↓
OpenCVでフレーム取得
   ↓
CLAHE画像前処理(コントラスト強調)
   ↓
YOLOv8で猫検出
   ↓
猫ベッドROI内の猫をカウント
   ↓
過去10秒間で7回以上2匹検出
   ├ macOS通知(60秒ごとに再通知)
   └ プレビュー画面にメッセージ表示

主要な実装

最終的なディレクトリ構成と主要なファイルの中身はこちら

lovely-cats-watch/
├── main.py              # メインプログラム
├── requirements.txt     # 依存パッケージ
├── roi_config.json      # ROI設定(自動生成)
└── yolov8m.pt          # YOLOv8モデル(自動ダウンロード)

コード全体の共有は長くなるため、主要な実装を紹介します。

1. ROI(Region of Interest, 関心領域)の選択

初回起動時、または設定ファイルがない場合、OpenCVのselectROIで猫ベッドのエリアをマウスで選択。
設定はroi_config.jsonに保存され、次回以降は自動的に読み込まれます。

def select_roi(self, frame):
    """猫ベッドのエリアをマウスで選択"""
    roi = cv2.selectROI("ROI選択", frame, fromCenter=False, showCrosshair=True)
    cv2.destroyWindow("ROI選択")

    if roi[2] > 0 and roi[3] > 0:
        self.roi = roi
        self.save_roi()
        return True
    return False

2. CLAHE画像前処理

YOLOの検出精度を向上させるため、CLAHE(Contrast Limited Adaptive Histogram Equalization)でコントラストを強調。
2匹の猫の区別がなかなかシビアだったため、入れてみました(効果のほどは…未検証)

def enhance_image(self, frame):
    """コントラスト強調でYOLOの検出精度を向上"""
    # LAB色空間に変換
    lab = cv2.cvtColor(frame, cv2.COLOR_BGR2LAB)
    l, a, b = cv2.split(lab)

    # CLAHEでコントラスト強調
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    l = clahe.apply(l)

    # BGR色空間に戻す
    enhanced_lab = cv2.merge([l, a, b])
    enhanced = cv2.cvtColor(enhanced_lab, cv2.COLOR_LAB2BGR)
    return enhanced

3. YOLOv8による猫検出

COCO学習済みモデル(yolov8m.pt)で猫(クラスID: 15)のみを検出。
前処理した画像を使用し、信頼度閾値を下げることで検出感度を向上させています。

# YOLOv8 medium モデル(精度とスピードのバランス)
self.model = YOLO('yolov8m.pt')

# 前処理した画像で検出
enhanced_frame = self.enhance_image(frame)
results = self.model(
    enhanced_frame,
    verbose=False,
    iou=0.3,        # 標準的なIoU閾値
    conf=0.05,      # 信頼度5%以上の候補を取得
    agnostic_nms=False,
    max_det=10
)

# 猫のみをフィルタリング
for result in results:
    for box in result.boxes:
        if int(box.cls[0]) == self.CAT_CLASS_ID:
            if float(box.conf[0]) >= 0.15:  # 信頼度15%以上
                # 検出結果を保存

4. カウントベースの検出ロジック

一時的な検出失敗があっても安定して判定できるよう、連続時間ではなく過去10秒間の検出回数で判定。

# 検出履歴に追加
self.detection_history.append((current_time, cat_count_in_roi))

# 検出期間外の古い履歴を削除
cutoff_time = current_time - self.detection_window
self.detection_history = [
    (t, count) for t, count in self.detection_history
    if t >= cutoff_time
]

# 検出期間内で2匹以上が検出された回数をカウント
detection_count = sum(1 for _, count in self.detection_history if count >= 2)

# 閾値を超えたら通知(30秒ごとに再通知)
if detection_count >= self.detection_threshold:
    if current_time - self.last_notification_time >= self.notification_cooldown:
        self.send_notification(...)

5. macOS通知

AppleScriptで通知を送信。60秒のクールダウン後、猫がまだいれば再通知(見逃し防止!)

def send_notification(self, message, title="猫ベッド監視"):
    """macOS通知を送信"""
    script = f'''
    display notification "{message}" with title "{title}" sound name "Glass"
    '''
    subprocess.run(['osascript', '-e', script], check=True)

6. メッセージ表示

とても尊く、ありがたい事象が起きたことを派手に表現。

def draw_center_rainbow_message(self, img_bgr, lines, now):
    # 虹色に表示(ゆっくり回す)
    base_hue = (now * 0.12) % 1.0

    for i, ch in enumerate(line):
        hue = (base_hue + (i * 0.05)) % 1.0
        r, g, b = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
        fill = (int(r * 255), int(g * 255), int(b * 255), 255)

        # 黒縁取り + 虹色テキスト
        for dx, dy in [(-3, 0), (3, 0), (0, -3), (0, 3), ...]:
            draw.text((x + dx, y + dy), ch, font=font, fill=(0, 0, 0, 255))
        draw.text((x, y), ch, font=font, fill=fill)

動かしてみよう

…おや?
スクリーンショット 0007-12-20 0.18.25.png

おやおや??
スクリーンショット 0007-12-20 0.42.51.png

おやおやおや?!?!
IMG_7820_jpg.jpg

無事、最高の瞬間に立ち会えました。
この絶景を5分眺めると、眼精疲労回復/集中力向上/血行促進/エトセトラな効能により大幅なパフォーマンス向上が見込めるため、来年のプロジェクトも引き続き大成功を収めることでしょう。

ちなみに、1匹の時や猫以外の場合は判定除外されておりました。やったね。

スクリーンショット 0007-12-20 1.58.13.png
スクリーンショット 0007-12-20 2.00.33.png

おわり

…しかし、精度にはまだまだ課題があり、一部の成功例をご紹介したまでになります。

  • カメラの角度で検知率が変わる
    • 猫たちを俯瞰して全体を収める形でないと、複数匹の判定が厳しい
  • 猫同士が密着したり重なると、1匹として判定されがち
    • ベッド占領割合やアスペクト比による判定を試みたが、猫たちの大きさの違いや、猫が液体であったり伸縮自在であるが故に精度が安定しなかった

今度はもう少し時間をかけて猫×IoTを発展させたいな、という野望もじわじわ出てきたため、猫検知スキルを引き続き上げていきたいと思います。

駆け込み的に始めた年末即興開発 with AIですが、普段使わない技術に触れ、アウトプットを通して知見を深められる良い機会だなと思いました。

専門分野ではないため実装に関して至らぬところがあるかもしれませんが、こんなもの欲しいなを実現してみるワクワクを伝えることができれば幸いです。

ちなみに前回同様、この開発で一番大変だったことは猫をカメラに映すことでした。

10
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
10
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?