SAM3とマトリックス・コードで作る"cat matrix" - エムスリーテックブログ

エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

SAM3とマトリックス・コードで作る"cat matrix"

AI・機械学習チームの鴨田です。この記事はAI・機械学習チームブログリレーの12日目の記事です。11日目は池嶋さんによる「Agentic MLOpsで加速する機械学習開発」でした。

娘の影響でYoutubeで「シナぷしゅ」を見ることが多くなったのですが、毎月更新される月歌(その月のテーマソング)とそれと共に投稿されるパパ・ママ向け解説動画が楽しみになりました。プロデューサーが直接こだわりを解説していて内容が濃くおすすめです。

nanobananaに「マトリックス・コードで猫の黒いシルエットが表現されている画像」で作成した画像

記事要約

  • Facebook/Meta AIの画像セグメンテーションモデル「SAM3」を使って猫の画像からMaskを抽出
  • 抽出したMaskをマトリックス・コードのアニメーションと組み合わせて動く猫のシルエットを表現

きっかけ

YouTube*1を見ていたら、マトリックス・コードで猫のシルエットが表現されているTシャツを着ている人を発見しました。

「これ、手元で実現できないかな?」と考えていたところ、ちょうど以前申請していたFacebook/Meta AIのSAM3(Segment Anything Model 3)のモデル利用許可メールが届きました。

そこで、次のアプローチで実現しました。

  1. SAM3を使って猫の画像から猫のMaskを抽出
  2. マトリックス・コード生成ツールを改修してMask領域を描画しないようにする
  3. 猫のシルエットが浮かび上がるマトリックス・コードアニメーションを完成させる

SAM3とは

SAM3(Segment Anything Model 3)は、Meta AI Researchが2025年にリリースした最新の画像・動画セグメンテーションモデルです。主な特徴は次の通りです。

SAM3の主な特徴

1. Promptable Concept Segmentation

SAM3の最大の特徴は、テキストプロンプトによるセグメンテーションです。従来のSAMでは点やボックスを指定する必要がありましたが、SAM3では「cat」のようなテキストを与えるだけで、画像内の該当するオブジェクトを自動でセグメンテーションします。

「cat」と指示すると、猫の範囲全体をMaskします。「ear」と指示すると生き物の耳をちゃんと認識して、インスタンスごとにMaskが生成されます。

左:プロンプトで「cat」と指示した結果。右:プロンプトで「ear」と指示した結果。

2. 画像と動画の両対応

SAM3は静止画だけでなく、動画のセグメンテーションにも対応しています。動画内のオブジェクトを追跡しながらフレームごとにMaskを生成できるため、今回のような動的なアニメーション作成にも活用できます。

3. ゼロショット学習

事前学習済みモデルを使うだけで、特定のドメインに対するファインチューニングなしで高精度なセグメンテーションができます。

詳細はHugging Faceのモデルページを参照してください。

huggingface.co

実装:SAM3でMaskを抽出

環境セットアップ

まず、必要なライブラリをインストールします。

!pip install -U transformers
!pip install -U av

SAM3は利用申請が必要なモデル(gated model)のため、Hugging Faceでモデルページにアクセスしてライセンスに同意する必要があります。その後、トークンを使ってログインします。

from huggingface_hub import login
login(token="YOUR_HF_TOKEN")

静止画からのMask抽出

次のコードで猫のMaskを抽出します。プロンプトには「cat」を使用します。

from transformers import Sam3Processor, Sam3Model
import torch
from PIL import Image

device = "cuda" if torch.cuda.is_available() else "cpu"

# モデルとプロセッサの読み込み
model = Sam3Model.from_pretrained("facebook/sam3").to(device)
processor = Sam3Processor.from_pretrained("facebook/sam3")

# 画像の読み込み
image = Image.open("cat_image.jpg").convert("RGB")

# テキストプロンプトでセグメンテーション
inputs = processor(images=image, text="cat", return_tensors="pt").to(device)

with torch.no_grad():
    outputs = model(**inputs)

# 後処理
results = processor.post_process_instance_segmentation(
    outputs,
    threshold=0.5,
    mask_threshold=0.5,
    target_sizes=inputs.get("original_sizes").tolist()
)[0]

print(f"Found {len(results['masks'])} objects")

動画からのMask抽出

動画の場合は、SAM3VideoModelを使用します。

from transformers import Sam3VideoModel, Sam3VideoProcessor
from transformers.video_utils import load_video

model = Sam3VideoModel.from_pretrained("facebook/sam3").to(device, dtype=torch.bfloat16)
processor = Sam3VideoProcessor.from_pretrained("facebook/sam3")

# 動画の読み込み
video_frames, _ = load_video("cat_video.mp4")

# セッションの初期化
inference_session = processor.init_video_session(
    video=video_frames,
    inference_device=device,
    processing_device="cpu",
    video_storage_device="cpu",
    dtype=torch.bfloat16,
)

# テキストプロンプトの追加
inference_session = processor.add_text_prompt(
    inference_session=inference_session,
    text="cat",
)

# 全フレームを処理
outputs_per_frame = {}
for model_outputs in model.propagate_in_video_iterator(
    inference_session=inference_session, max_frame_num_to_track=130
):
    processed_outputs = processor.postprocess_outputs(inference_session, model_outputs)
    outputs_per_frame[model_outputs.frame_idx] = processed_outputs

これで、各フレームごとに猫のMaskが取得できます。

MaskをASCII artに変換

抽出したMaskをマトリックス・コード生成ツールで使えるよう、ASCII artに変換します。

import numpy as np
from PIL import Image

def generate_mask_ascii_art(masks, scale_factor=0.5, fill_char="#", bg_char=" "):
    """
    Mask領域をASCII artとして出力
    """
    if hasattr(masks, 'cpu'):
        masks = masks.cpu().numpy()
    
    # 複数Maskを結合
    if masks.ndim == 3:
        combined_mask = np.any(masks > 0, axis=0).astype(np.uint8) * 255
    else:
        combined_mask = (masks > 0).astype(np.uint8) * 255
    
    # リサイズ
    mask_img = Image.fromarray(combined_mask)
    orig_w, orig_h = mask_img.size
    
    aspect_correction = 0.5  # コンソール文字の縦横比補正
    new_w = int(orig_w * scale_factor)
    new_h = int(orig_h * scale_factor * aspect_correction)
    
    resized_img = mask_img.resize((new_w, new_h), resample=Image.NEAREST)
    resized_mask = np.array(resized_img)
    
    # ASCII artに変換
    aa_lines = []
    for row in resized_mask:
        line = "".join([fill_char if val > 0 else bg_char for val in row])
        aa_lines.append(line)
    
    return "\n".join(aa_lines)

# ASCII artを生成してファイルに保存
ascii_art = generate_mask_ascii_art(results["masks"], scale_factor=1/7, bg_char=" ")
with open("ascii_art.txt", "w") as f:
    f.write(ascii_art)

ネコチャンの動画からMaskを作成した様子

夕方に撮影者の影が重なる影響で、尻尾が欠ける程度の欠損はあるものの、シルエット全体としては猫と認識できる十分な精度でした。*2

茶トラの尻尾と背景が似ているため誤検出(False Positive)が多くなると思いましたが、むしろ逆で尻尾が消失していたのが面白い発見でした。

実装:マトリックス・コードと組み合わせ

OSSの選定と改修

マトリックス・コードのアニメーションには、unimatrixというOSSを使用しました。これはターミナル上で映画「マトリックス」のような緑色の文字が流れるアニメーションを表示するツールです。

このOSSを改修して、次の機能を追加しました。

1. Mask機能の追加

ASCII artファイルから#がある座標を読み取り、その座標では文字を描画しない。

def load_mask(filename):
    """
    ASCII artファイルを読み込み、#がある座標のセットを返す
    """
    mask_positions = set()
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            for y, line in enumerate(f):
                for x, char in enumerate(line.rstrip('\n')):
                    if char == '#':
                        mask_positions.add((y, x))
    except Exception as e:
        print(f"Warning: Could not load mask file '{filename}': {e}")
    
    return mask_positions

2. アニメーション機能の追加

ディレクトリ内の複数のASCII artファイルを順番に読み込んで、フレームアニメーションを実現します。

def load_mask_files(directory):
    """
    ディレクトリ内のascii_art_*.txtファイルを番号順に読み込む
    """
    pattern = str(Path(directory) / "ascii_art_*.txt")
    files = glob.glob(pattern)
    
    def extract_number(filepath):
        match = re.search(r'ascii_art_(\d+)\.txt$', filepath)
        return int(match.group(1)) if match else 0
    
    files.sort(key=extract_number)
    return files

3. 描画処理の修正

Writerクラスに、Mask領域をスキップする処理を追加しました。

def is_masked(self, y, x):
    """
    指定座標がMask領域かチェック
    """
    return (y, x) in self.mask_positions

def draw(self, node):
    """
    Mask領域では描画をスキップ
    """
    y = node.y_coord
    x = node.x_coord
    
    if self.is_masked(y, x):
        return  # Mask領域では何も描画しない
    
    # 通常の描画処理...

実施結果

猫のシルエットがマトリックス・コードの中に浮かび上がりました。

cat matrixを動かしている様子

コード

今回作成したコードはMask含めてGitHubで公開しています。

github.com

余談:Google Colabのメモリ不足

動画のセグメンテーションを実行する際、Google Colabの無料枠ではメモリが不足してしまいました。SAM3VideoModelは動画の全フレームをメモリ上に展開するため、数分の動画でも数GBのメモリを消費します。

結局、Google Colab Proに課金してハイメモリランタイムを使用することで解決しました。もしメモリに制限がある場合は、次のような対策が有効です:

  • 動画の解像度を下げる
  • フレームレートを下げる(間引く)
  • 処理するフレーム数を制限する(max_frame_num_to_trackパラメータ)
# フレーム数を制限する例
for model_outputs in model.propagate_in_video_iterator(
    inference_session=inference_session, 
    max_frame_num_to_track=50  # 最初の50フレームのみ処理
):
    ...

まとめ

SAM3のPromptable Concept Segmentationを使うことで、「cat」という一言のプロンプトから猫のMaskを簡単に抽出できました。抽出したMaskをマトリックス・コードと組み合わせることで、猫のシルエットが浮かび上がる「cat matrix」を作成できました。

SAM3は画像処理の民主化を大きく前進させるツールだと感じました。従来はセグメンテーションに専門知識が必要でしたが、SAM3を使えば誰でも簡単に高精度なセグメンテーションが可能です。

皆さんもSAM3を使って、面白いビジュアル表現に挑戦してみてはいかがでしょうか。

We are hiring!

AI チームでは、最新の機械学習技術を用いて、実社会の複雑な課題解決に取り組むエンジニアを募集しています。 ご気軽にカジュアル面談からでも応募をお待ちしております。

エンジニア採用ページはこちら

jobs.m3.com

カジュアル面談もお気軽にどうぞ

jobs.m3.com

インターンも常時募集しています

open.talentio.com