メインコンテンツまでスキップ

SAMとは?

概要 (画像は、Geminiで作成されたものです)

SAMの概要

2023年4月にMeta AI Research(FAIR)によって発表されたSAM(Segment Anything Model)は、画像セグメンテーションにおける「ファウンデーションモデル(基盤モデル)」を構築するための画期的なプロジェクトです。

画像セグメンテーション(画像内のどのピクセルがどのオブジェクトに属するかを特定するタスク)は、写真編集から科学画像の解析まで幅広い分野で使われるコンピュータビジョンの中核技術です。しかし従来は、人間が手動で反復的に修正を指示する「インタラクティブ・セグメンテーション」か、事前に定義された特定のカテゴリに対して大量の教師データを用意して学習させる「自動セグメンテーション」のいずれかのアプローチに頼らざるを得ませんでした。用途に合わせた高精度なモデルを作成するためには、高度なモデリングの専門知識や計算リソース、そして独自の手動アノテーションデータが不可欠でした。

SAMは、これら2つのアプローチを一般化して統合し、セグメンテーション技術の民主化を目指しています。自然言語処理(NLP)における大規模言語モデルが「プロンプト」を用いて多様なタスクにゼロショットで適応するように、SAMは「プロンプト可能なセグメンテーションタスク(Promptable Segmentation Task)」を導入しました。これにより、点、ボックス、マスク、あるいは自由形式のテキストといった柔軟なプロンプトを与えるだけで、様々なセグメンテーションタスクを単一のモデルで実行できるようになります。

さらにSAMは、「オブジェクトとは何か」という一般的な概念を学習しており、学習時に見たことのない未知のオブジェクトに対しても対応可能です。細胞の顕微鏡画像や水中写真など全く新しいドメインの画像であっても、追加の再学習を行うことなくそのまま適用し、高精度なセグメンテーションを実行できます(ゼロショット転移)。論文中では、このゼロショットでのセグメンテーション精度を測定・比較する標準的な評価指標として mIoU (mean Intersection over Union) などが用いられており、従来の専用モデルに匹敵、あるいは凌駕する驚異的な汎化性能を示しています。また、現実世界での実践的な利用を想定し、与えられたプロンプトが曖昧な場合(例えば点が「シャツ」を指しているのか、「シャツを着ている人全体」を指しているのか不明確な場合)には、複数の妥当なマスク候補を出力する機能も備えています。

この汎用性と拡張性の高さにより、SAMはより高度なシステムの強力なコンポーネントになることが期待されています。ウェブページの視覚とテキストを統合して理解する大規模マルチモーダルAI、AR/VRグラスでユーザーの視線(Gaze)をもとに対象物を選択する機能、クリエイター向けの高度な動画編集、さらには農業や生物学分野での動植物のトラッキングなど、幅広い領域での活用が見込まれています。

SAMの処理概要

処理概要 (画像は、Geminiで作成されたものです)

SAMが「あらゆるプロンプトに対してリアルタイムにマスクを出力する」という要件を満たすため、そのアーキテクチャは大きく「Image encoder(画像エンコーダ)」「Prompt encoder(プロンプトエンコーダ)」「Mask decoder(マスクデコーダ)」の3つのフェーズに分割されています。

  1. Image encoder(画像エンコーダ): 重厚な特徴抽出
    入力画像は、MAE(Masked Autoencoders)で事前学習された強力なVision Transformer(ViT)を通過し、画像エンベディング(特徴マップ)に変換されます。この処理は重いですが、画像1枚につき「1回だけ」実行すればよく、その後のプロンプト処理にかかる計算コストを償却(Amortized)できる設計になっています。
  2. Prompt encoder(プロンプトエンコーダ): 柔軟なプロンプトの埋め込み
    ユーザーから与えられたプロンプトをエンコードします(※SAMにおける「プロンプト」とは、一般的なテキスト指示だけでなく、画像上の特定の座標を示す「点」や、対象オブジェクトを囲む「ボックス」などの視覚的な指示入力も含まれます)。具体的には、疎なプロンプト(点やボックス)は位置エンコーディングと学習済みの埋め込みで表現され、テキストはCLIPのオフザシェルフのテキストエンコーダで表現されます。密なプロンプト(マスク)は畳み込みによってエンコードされ、画像エンベディングに要素ごとに加算されます。
  3. Mask decoder(マスクデコーダ): 高速かつ軽量なマスク予測
    画像エンベディングとプロンプトエンベディングを組み合わせ、Transformerベースの軽量なデコーダで処理します。このデコーダは非常に軽く、事前に計算された画像エンベディングがあれば、Webブラウザ上のCPU環境であっても約50ミリ秒というリアルタイムな速度でマスクを予測できます。

SAMの構成技術要素(詳細)

SAMの汎用性と速度を両立させているのは、シンプルでありながら極限まで最適化されたアーキテクチャ設計にあります。主要な構成技術要素の詳細を記述します。

1. 償却可能な強力な画像エンコーダ

SAMは、拡張性と強力な事前学習の恩恵を受けるため、画像エンコーダにMAE(Masked Autoencoder)で事前学習されたVision Transformer(ViT)を採用しています。

  • 詳細な仕組み: 高解像度の入力(1024×10241024 \times 1024)を処理できるようにViTを最小限の修正で適応させています(具体的には14×1414 \times 14のウィンドウアテンションと4つの等間隔のグローバルアテンションブロックを使用)。最終的に、入力画像に対して16倍ダウンスケールされた特徴マップ(64×6464 \times 64)を出力します。
  • 技術的な効果: この重いエンコーダはプロンプトを与える前に1度だけ実行されます。これにより、以降のインタラクティブな推論時には計算負荷がかからず、リアルタイムな使い勝手を実現しています。

2. 双方向クロスアテンションを備えた軽量マスクデコーダ

マスクデコーダは、画像の特徴とプロンプトの指示を効率的に融合させ、最終的なマスクを生成する要のモジュールです。

  • 詳細な仕組み: 標準的なTransformerデコーダブロックを改良し、「プロンプトから画像エンベディングへのクロスアテンション」と、その逆方向である「画像エンベディングからプロンプトへのクロスアテンション」の2方向で情報を更新します。2つのブロックを通過した後、画像エンベディングをアップサンプリングし、MLP(多層パーセプトロン)を介して各画像位置のマスク前景確率を計算します。
  • 技術的な効果: プロンプトの幾何学的な位置やタイプと、画像の視覚的な特徴が強力に結びつきます。計算コストは画像エンコーダの1%未満であり、超高速な推論を実現しています。

3. 曖昧さの解決(Ambiguity-aware)

1つの点プロンプトが与えられた場合、それが「シャツ」を指しているのか、「シャツを着ている人全体」を指しているのか、曖昧なケースが頻繁に発生します。

  • 詳細な仕組み: SAMはこの問題に対処するため、1つのプロンプトに対して単一のマスクを出力するのではなく、同時に複数のマスク(デフォルトでは3つ)を予測するように設計されています。3つのマスクはそれぞれ、全体(Whole)、部分(Part)、小部分(Subpart)といった階層構造を表現するのに十分な数です。
  • 技術的な効果: 学習時には、グラウンドトゥルース(正解)と最も損失が小さい(もっともらしい)マスク1つに対してのみバックプロパゲーション(誤差逆伝播)を行う仕組みを採用しています。これにより、モデルは曖昧な指示に対しても平均化された無意味なマスクではなく、妥当なオブジェクト候補を複数提示できるようになり、ゼロショット性能が大幅に向上しました。

SAMの実装(概念的なシンプルな実装)

ここでは、SAMのアーキテクチャの核心である「画像エンコーダ」「プロンプトエンコーダ」「マスクデコーダ」がどのように連動しているのか、PyTorchを用いた擬似的なコードで解説します。プロンプトからどのようにマスクが生成されるか、そのエッセンスを掴んでみましょう。

以下のコードでは、PyTorchを用いてSAMのアーキテクチャ概念を模した、極小規模のモデル MiniSAM を定義します。重い計算を画像エンコーダに寄せ、軽量なデコーダで高速処理を行う設計思想を確認します。

MiniSAMの定義

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import cv2
from skimage import data, color
from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection
from transformers import SamModel, SamProcessor


class MiniSAM(nn.Module):
def __init__(self):
super().__init__()
# 3つの主要なコンポーネント(画像エンコーダ、プロンプトエンコーダ、マスクデコーダ)を定義
self.image_encoder = ImageEncoder()
self.prompt_encoder = PromptEncoder()
self.mask_decoder = MaskDecoder()

def forward(self, image, box_prompt):
# 画像から特徴(エンベディング)を抽出
img_embed = self.image_encoder(image)
# 幾何学的なプロンプト情報を埋め込みベクトルに変換
prompt_embed = self.prompt_encoder(box_prompt)
# 画像特徴とプロンプト特徴をデコーダで融合し、マスクのロジットを算出
mask_logits = self.mask_decoder(img_embed, prompt_embed)
return mask_logits

MiniSAM クラスは、SAMの非対称なアーキテクチャ設計をシンプルに表現したPyTorchモジュールです。 コンストラクタ init では、重厚な特徴抽出を担う self.image_encoder、プロンプト情報の埋め込みを行う self.prompt_encoder、そしてこれらを融合して最終的なマスクを生成する self.mask_decoder の3つの主要コンポーネントを定義しています。

フォワードパス forward では、入力画像 image とバウンディングボックスのプロンプト box_prompt を受け取ります。まず、重厚な画像エンコーダによって入力画像から特徴マップ img_embed を抽出します。次に、プロンプトエンコーダによってバウンディングボックスの情報をエンコーディングした prompt_embed を生成します。最後に、これらのエンベディング(埋め込み表現)をマスクデコーダに流し込み、セグメンテーションのマスク確率(ロジット)である mask_logits を算出します。この一連の流れは、SAMの「重いエンコーダと軽量なデコーダ」を非対称に組み合わせることで、リアルタイム推論を可能にする設計思想を体現しています。

ImageEncoderの定義

以下のコードでは、PyTorchを用いて重厚な特徴抽出を模した ImageEncoder を定義します。畳み込み層(Convolutional Layer)とバッチ正規化(Batch Normalization)、非線形活性化関数である GELU を組み合わせたCNN(畳み込みニューラルネットワーク)により、入力された高解像度の画像を16倍ダウンスケールされた256チャンネルの画像エンベディング(特徴マップ)に変換して出力します。

class ImageEncoder(nn.Module):
def __init__(self):
super().__init__()
# 画像の特徴量を段階的に抽出しつつダウンサンプリングする畳み込みネットワークを定義
self.net = nn.Sequential(
# 入力: (3, 128, 128) -> 出力: (64, 32, 32)
nn.Conv2d(3, 64, kernel_size=4, stride=4),
nn.BatchNorm2d(64),
nn.GELU(),
# 入力: (64, 32, 32) -> 出力: (128, 16, 16)
nn.Conv2d(64, 128, kernel_size=2, stride=2),
nn.BatchNorm2d(128),
nn.GELU(),
# 入力: (128, 16, 16) -> 出力: (256, 8, 8)
nn.Conv2d(128, 256, kernel_size=2, stride=2),
nn.BatchNorm2d(256),
nn.GELU()
)
def forward(self, x):
# 順伝播処理。入力画像から256チャンネルの画像エンベディングを抽出して返す
return self.net(x)

ImageEncoder では、画像の視覚的特徴を効率的に集約するために、3段階の畳み込み処理を定義しています。各畳み込み層(nn.Conv2d)の直後にバッチ正規化層(nn.BatchNorm2d)と非線形活性化関数である nn.GELU を結合することで、ネットワークの学習を安定させ、より表現力の高い非線形な特徴を抽出できるようにしています。

1段階目では、3チャンネルのRGB入力画像に対してカーネルサイズ4、ストライド4の畳み込みを適用し、解像度を縦横それぞれ4分の1(128×128128 \times 128 から 32×3232 \times 32)へと圧縮します。2段階目および3段階目では、さらにカーネルサイズ2、ストライド2の畳み込み層を使用し、解像度を段階的に 16×1616 \times 16、そして最終的に 8×88 \times 8 までダウンスケール(合計16倍のダウンスケール)します。これに伴ってチャンネル数は 3641282563 \rightarrow 64 \rightarrow 128 \rightarrow 256 へと増加し、局所的な形状情報からより高次なセマンティック(意味的)な情報を表す、(batch_size,256,8,8)(batch\_size, 256, 8, 8) 次元の画像エンベディングへと変換されます。これが、デコーダ側でプロンプトと照合されるリッチな視覚情報となります。

PromptEncoderの定義

以下のコードでは、入力されたバウンディングボックスの情報を、モデルが画像特徴量と融合しやすい空間に射影する PromptEncoder を定義します。PyTorchの線形全結合層(nn.Linear)と GELU 活性化関数を用いて、バウンディングボックスの4次元の幾何学的座標情報を、画像エンベディングのチャンネル数と一致する256次元の埋め込みベクトルにエンコードします。

class PromptEncoder(nn.Module):
def __init__(self):
super().__init__()
# ボックスプロンプトの4次元入力 (xmin, ymin, xmax, ymax) を受け取り、256次元に変換する全結合層
self.net = nn.Sequential(
nn.Linear(4, 128),
nn.GELU(),
nn.Linear(128, 256)
)
def forward(self, box):
# 順伝播処理。正規化されたボックス座標からプロンプトエンベディングを生成して返す
return self.net(box)

PromptEncoder クラスは、ユーザーから提示されたバウンディングボックス情報を、画像エンベディングと融合可能なセマンティック表現へと低次元から高次元へマッピングします。コンストラクタ内の self.net は、2つの線形全結合層(nn.Linear)と、その間に非線形活性化関数 nn.GELU を挟んだ構成をとっています。

入力されるプロンプトは、バウンディングボックスの左上座標および右下座標を示す4つの実数値 (xmin,ymin,xmax,ymax)(xmin, ymin, xmax, ymax) です。forward メソッドでは、この4次元のテンソルを受け取り、最初の全結合層で128次元へと拡張し、非線形な活性化処理を施したのち、さらに2番目の全結合層によって256次元のプロンプトエンベディングへと変換します。この出力次元(256)は、前述した ImageEncoder が出力する画像エンベディングのチャンネル数(256)と完全に一致しており、デコーダ内での要素ごとの加算やクロスアテンションによるシームレスな特徴量融合を可能にする重要な役割を担っています。

MaskDecoderの定義

以下のコードでは、抽出された画像エンベディングとプロンプトエンベディングを融合して、最終的なピクセルレベルのマスクを復元・予測する MaskDecoder を定義します。PyTorchのマルチヘッド・アテンション(nn.MultiheadAttention)による情報融合と、転置畳み込み層(nn.ConvTranspose2d)を重ねたアップサンプリングネットワークにより、低解像度の特徴マップから高精度な 128×128128 \times 128 の2値マスクを生成します。

class MaskDecoder(nn.Module):
def __init__(self):
super().__init__()
# 画像とプロンプトの間で幾何学的・視覚的特徴を融合するためのマルチヘッドアテンション
self.cross_attn = nn.MultiheadAttention(embed_dim=256, num_heads=8, batch_first=True)
# 融合された特徴マップを段階的に元の画像サイズ(128x128)へ拡大するアップサンプリング層
self.upsample = nn.Sequential(
nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2), # 8x8 -> 16x16
nn.GELU(),
nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2), # 16x16 -> 32x32
nn.GELU(),
nn.ConvTranspose2d(64, 32, kernel_size=4, stride=4), # 32x32 -> 128x128
nn.GELU(),
nn.Conv2d(32, 1, kernel_size=1) # 1チャンネルの確率マップ(ロジット)へ変換
)

def forward(self, image_embeddings, prompt_embeddings):
# image_embeddings: (B, 256, 8, 8)
# prompt_embeddings: (B, 256)
B, C, H, W = image_embeddings.shape
# アテンションの計算のために (B, 256, 64) にフラット化して (B, 64, 256) に軸を入れ替え
img_flat = image_embeddings.view(B, C, -1).permute(0, 2, 1)
# プロンプト埋め込みにシーケンス長の次元を追加して (B, 1, 256) に
prompt_embeddings = prompt_embeddings.unsqueeze(1)

# クロスアテンションを適用:プロンプトをクエリに、画像特徴をキーとバリューに設定
attn_out, _ = self.cross_attn(query=prompt_embeddings, key=img_flat, value=img_flat)

# アテンションの出力を (B, 256, 1, 1) の形状に変形
prompt_expanded = attn_out.transpose(1, 2).unsqueeze(-1)
# 画像エンベディングにプロンプト情報を加算(ブロードキャスト)
fused_features = image_embeddings + prompt_expanded
# アップサンプリング層を通じて解像度を復元し、マスクを出力
masks = self.upsample(fused_features)
return masks

MaskDecoder は、SAMにおけるもっとも重要な「軽量かつ高速なデコーダ」の役割を再現しています。画像から得られた空間的な視覚情報と、プロンプトが示す幾何学的な位置情報を、アテンション機構(nn.MultiheadAttention)とアップサンプリング(nn.ConvTranspose2d)を組み合わせて効率的に融合します。

フォワードパス forward の中では、まず (B,256,8,8)(B, 256, 8, 8) 次元を持つ画像エンベディング image_embeddings を、viewpermute を用いて空間次元をフラット化した (B,64,256)(B, 64, 256) 次元(パッチシーケンス長64、埋め込み次元256)のテンソル img_flat に変形します。これと同様に、プロンプトエンベディング prompt_embeddingsunsqueeze(1) でシーケンス長の次元を追加し (B,1,256)(B, 1, 256) とします。

その後、self.cross_attn においてマルチヘッド・アテンションを適用します。ここではプロンプト埋め込みを query に、フラット化した画像特徴を key および value に指定することで、「プロンプトに示された位置や意図が、画像のどのパッチと強く関連しているか(幾何学的な融合)」を反映したアテンション出力 attn_out(B,1,256)(B, 1, 256) 次元)を得ます。この出力を (B,256,1,1)(B, 256, 1, 1) 次元に拡張して prompt_expanded とし、ブロードキャスト機能によって元の画像エンベディング image_embeddings と要素ごとに加算(特徴の融合)し、fused_features を得ます。

最後に、融合された (B,256,8,8)(B, 256, 8, 8) 特徴マップを self.upsample のネットワークへと通します。転置畳み込み(nn.ConvTranspose2d)により、解像度を 8×816×1632×32128×1288 \times 8 \rightarrow 16 \times 16 \rightarrow 32 \times 32 \rightarrow 128 \times 128 へと段階的にアップサンプリングします。最終層の nn.Conv2d(32, 1, kernel_size=1) によって、チャンネル数を1へと圧縮し、元の画像サイズに対応する 128×128128 \times 128 ピクセルのセグメンテーションロジットを出力します。

サンプル画像の取得とGrounding DINOとHugging Face SAMモデルによる教師データの作成

以下のコードでは、モデル学習のための高品質な教師データ(グラウンドトゥルース)を自動生成するパイプラインを実装します。scikit-image からサンプルのカメラマン画像をロードし、ゼロショット物体検出モデルである Grounding DINO を用いてテキスト「man(男性)」のバウンディングボックスを検出し、そのボックスをもとに Hugging Face にホストされている公式の SAM(facebook/sam-vit-base)に入力して高精度の教師用マスクを生成、学習用のテンソルとして整形・保存します。

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# skimageから引きの画像(カメラマン)を取得
image = data.camera()

# グレースケール画像をRGBに変換(モデル入力用)
if image.ndim == 2:
image = color.gray2rgb(image)

IMG_SIZE = 128

# 人物全体を検出するために "man" を指定("camera" に変更するとカメラ部分を検出します)
text_prompt = "man."

print(f"Grounding DINOを用いて '{text_prompt}' のバウンディングボックスを検出しています...")
# Grounding DINOのプロセッサとモデルをHugging Faceから取得
dino_processor = AutoProcessor.from_pretrained("IDEA-Research/grounding-dino-base")
dino_model = AutoModelForZeroShotObjectDetection.from_pretrained("IDEA-Research/grounding-dino-base").to(device)

# 入力画像を前処理してモデルへ送る
dino_inputs = dino_processor(images=image, text=text_prompt, return_tensors="pt").to(device)
with torch.no_grad():
dino_outputs = dino_model(**dino_inputs)

# ボックスの座標を元の画像サイズに逆変換して取得
target_sizes = torch.tensor([image.shape[:2]]).to(device)
dino_results = dino_processor.image_processor.post_process_object_detection(
dino_outputs, threshold=0.3, target_sizes=target_sizes
)[0]

# 最も検出スコア(信頼度)の高いボックスを採用
best_box_idx = dino_results["scores"].argmax()
bbox = dino_results["boxes"][best_box_idx].tolist() # [xmin, ymin, xmax, ymax]
print(f"検出されたボックス: {bbox}")

print("SAMを用いて、検出したボックスから高精度な教師マスクを生成しています...")
# 公式のSAMモデルとプロセッサをロード
sam_processor = SamProcessor.from_pretrained("facebook/sam-vit-base")
sam_model = SamModel.from_pretrained("facebook/sam-vit-base").to(device)

# Boxプロンプトを入力として渡す ([[[xmin, ymin, xmax, ymax]]])
sam_inputs = sam_processor(image, input_boxes=[[bbox]], return_tensors="pt").to(device)
with torch.no_grad():
sam_outputs = sam_model(**sam_inputs)

# 3つの出力マスクの中からIoUスコアが最も高いものを選択
scores = sam_outputs.iou_scores[0, 0].cpu().numpy()
best_idx = np.argmax(scores)
best_mask_logits = sam_outputs.pred_masks[0, 0, best_idx].cpu().numpy()
# ロジット閾値(>0)で2値化してマスクを生成
target_mask_highres = (best_mask_logits > 0).astype(np.uint8)

# 自作モデルMiniSAMの入出力仕様に合わせて画像をリサイズ
image_resized = cv2.resize(image, (IMG_SIZE, IMG_SIZE))
target_mask_resized = cv2.resize(target_mask_highres, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_NEAREST)

# PyTorchテンソルへの変換および [0, 1] への正規化
image_tensor = torch.tensor(image_resized, dtype=torch.float32).permute(2, 0, 1).unsqueeze(0).to(device) / 255.0
target_mask_tensor = torch.tensor(target_mask_resized, dtype=torch.float32).unsqueeze(0).unsqueeze(0).to(device)

# ボックス座標を画像サイズに対する比率 [0.0, 1.0] に正規化してテンソル化
h, w, _ = image.shape
bbox_normalized = [bbox[0]/w, bbox[1]/h, bbox[2]/w, bbox[3]/h]
box_tensor = torch.tensor([bbox_normalized], dtype=torch.float32).to(device)

このステップでは、本物の事前学習済みVision-Language基盤モデルを活用して、MiniSAM モデルの学習と検証に用いるための高品質なグラウンドトゥルース(教師用ペアデータ)を全自動で組み立てるデータエンジニアリングの手順を示しています。

  1. ゼロショット物体検出によるボックスプロンプトの自動作成: IDEA-Research/grounding-dino-base(Grounding DINO)モデルを使用しています。プロセッサ dino_processor で画像をトークン化し、テキストプロンプト「man.」を渡すことで、画像中の「男性(カメラマン)」の位置を特定するバウンディングボックス bbox を座標 (xmin,ymin,xmax,ymax)(xmin, ymin, xmax, ymax) として自動的に検出します。
  2. 基盤モデルSAMによる超高精度アノテーション生成: 検出されたバウンディングボックスをプロンプトとして、今度は本物の facebook/sam-vit-base に入力します。Hugging Faceの SamProcessor は画像とバウンディングボックスを同時に受け取ることができ、適切な入力形式に内部的に変換します。モデルが出力する複数のマスク候補のうち、IoUスコアが最大である最適なマスク(best_mask_logits)を取り出し、0を超える部分を論理演算で抽出して高品質なアノテーションマスク target_mask_highres(2値マスク)を作成します。
  3. 極小モデルMiniSAM用の入力データ整形: 生成された画像およびマスクを 128×128128 \times 128 ピクセルにリサイズ(マスクは cv2.INTER_NEAREST で最近傍補間を行い、輪郭をぼかさないように縮小)します。さらに、チャンネル順の入れ替え((H,W,C)(C,H,W)(H, W, C) \rightarrow (C, H, W))、値の [0,1][0, 1] へのスケーリング、バッチ次元の追加を経て、テンソル image_tensor および target_mask_tensor を完成させます。ボックスプロンプトについても、画像の解像度に依存しないよう幅・高さで割ることで、[0.0,1.0][0.0, 1.0] の範囲に正規化されたテンソル box_tensor を作成します。このような外部の基盤モデルを利用したアノテーション手法は、医療や産業用など独自の新規ドメインでセグメンテーションモデルを高速に構築したい際にも、極めて強力なアプローチとなります。

モデルの学習

以下のコードでは、構築した極小規模モデル MiniSAM を用いて、自動生成した教師データ(画像、バウンディングボックス、教師マスク)に対するサニティチェック(過学習による動作検証)を実行します。損失関数に BCEWithLogitsLoss、最適化アルゴリズムに Adam を用い、1つのデータに対して150エポックの訓練を行い、損失(Loss)がゼロに収束し、モデルが提示されたプロンプトと画像情報を正しく結びつけて学習できているかを確認します。

# MiniSAMモデルをインスタンス化し、計算デバイスへ転送
model = MiniSAM().to(device)
# ロジット入力をサポートするバイナリクロスエントロピー損失関数を設定(BCEWithLogitsLossは数値的に安定)
criterion = nn.BCEWithLogitsLoss()
# Adamオプティマイザを学習率1e-3で設定
optimizer = optim.Adam(model.parameters(), lr=1e-3)

epochs = 150
print("\nMiniSAMのサニティチェック(過学習)を開始します...")
model.train() # モデルを訓練モードに設定
for epoch in range(epochs):
optimizer.zero_grad() # 勾配の初期化

# ボックスプロンプトを入力してMiniSAMを実行し、予測マスク(ロジット)を取得
pred_logits = model(image_tensor, box_tensor)

# 予測ロジットと教師用ターゲットマスクの間でバイナリクロスエントロピー損失を計算
loss = criterion(pred_logits, target_mask_tensor)
loss.backward() # 勾配の逆伝播
optimizer.step() # パラメータの更新

# 30エポックごとに現在のエポック数と損失を表示
if (epoch + 1) % 30 == 0:
print(f"Epoch [{epoch+1:3d}/{epochs}], Loss: {loss.item():.4f}")

モデル設計が妥当であるかを検証するための「過学習サニティチェック」プロセスを実装しています。これは、限られた極小のデータ(ここでは1枚の画像とそのアノテーションマスクのペア)に対して、モデルの表現力が十分であるか、勾配が爆発または消失することなく正常に伝播しているかを検証するための、深層学習における極めて実用的なアプローチです。

損失関数には nn.BCEWithLogitsLoss を使用しています。この関数は、デコーダが出力する生のロジット(閾値処理やシグモイド関数を適用する前の値)を直接受け取り、内部で数学的に安定したシグモイド演算を行ってからバイナリクロスエントロピーを算出するため、勾配の計算エラーやアンダーフローを防ぐことができます。最適化には広く一般的に利用される optim.Adam を学習率 0.0010.001 で選択しています。

ループ内の model(image_tensor, box_tensor) で、入力画像特徴量とプロンプト情報を融合しながらフォワードパスを実行します。得られた loss を基準に loss.backward() で各ネットワーク層への逆伝播を行い、optimizer.step() で画像エンコーダ、プロンプトエンコーダ、マスクデコーダの全てのウェイトを更新します。 エポックを経るごとに表示される Loss の値が 0.13230.05630.00650.1323 \rightarrow 0.0563 \rightarrow 0.0065 と極めて滑らかに減少し、ゼロに限りなく近付いていることから、自作した MiniSAM のアーキテクチャが画像特徴、幾何プロンプト、および対象のマスク形状を完全に結びつける表現力と、正常な勾配伝播能力を持っていることが証明されます。

モデルの推論

以下のコードでは、学習済みの MiniSAM モデルを使用して、実際に評価(推論)を行い、その結果を可視化します。モデルを評価モードに切り替え、勾配計算を無効化した上で予測を実行し、得られたマスクの予測確率を閾値判定(0.5)によって2値化し、元画像、Grounding DINOで得られたバウンディングボックス、教師用マスク、そしてモデルの出力予測マスクを matplotlib を使って並べて描画し、推論の妥当性を評価します。

model.eval() # モデルを評価(推論)モードに設定
with torch.no_grad(): # 推論時は不要な勾配の追跡をオフにしてメモリを節約
# 画像テンソルと正規化したプロンプトボックスを入力して推論
pred_logits = model(image_tensor, box_tensor)
# シグモイドを適用し、0.5の閾値でTrue/Falseの2値マスクに変換
pred_mask = torch.sigmoid(pred_logits) > 0.5

# 3つの領域に分けて結果を可視化するためのグリッドを生成
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# リサイズ後のボックス座標を計算 (128x128サイズでの描画用)
xmin_r, ymin_r = bbox_normalized[0] * IMG_SIZE, bbox_normalized[1] * IMG_SIZE
width_r = (bbox_normalized[2] - bbox_normalized[0]) * IMG_SIZE
height_r = (bbox_normalized[3] - bbox_normalized[1]) * IMG_SIZE

# バウンディングボックス(赤い四角)を描画するための補助関数
def draw_box(ax):
rect = patches.Rectangle((xmin_r, ymin_r), width_r, height_r, linewidth=2, edgecolor='red', facecolor='none')
ax.add_patch(rect)

# 1枚目: 入力画像と、DINOによって検出された赤いボックスを表示
axes[0].imshow(image_resized)
draw_box(axes[0])
axes[0].set_title(f"Input Image & DINO Box ('{text_prompt}')")
axes[0].axis('off')

# 2枚目: 入力画像の上に、教師データとして作成したSAMのマスクをオーバーレイ表示
axes[1].imshow(image_resized)
axes[1].imshow(target_mask_resized, cmap='jet', alpha=0.5)
draw_box(axes[1])
axes[1].set_title("Target Mask (SAM with Box)")
axes[1].axis('off')

# 3枚目: 入力画像の上に、自作モデルMiniSAMが予測したマスクをオーバーレイ表示
axes[2].imshow(image_resized)
axes[2].imshow(pred_mask[0, 0].cpu().numpy(), cmap='jet', alpha=0.5)
draw_box(axes[2])
axes[2].set_title("Predicted Mask (MiniSAM)")
axes[2].axis('off')

plt.tight_layout()
plt.show()

学習を終えた MiniSAM のセグメンテーション推論を実行し、その結果を視覚的に評価するための可視化プロセスを構成しています。

  1. 評価モードへの切り替えと効率的な推論: model.eval() を呼び出すことで、ドロップアウトやバッチ正規化といった訓練時と評価時で挙動が異なるレイヤーを推論用のモードに統一します。さらに、with torch.no_grad(): ブロックを使用することで、逆伝播に必要な計算グラフの追跡や履歴の保持を行わないようにし、推論処理にかかるメモリ使用量を劇的に低減、処理スピードを向上させます。
  2. モデル出力の確率化と2値化: デコーダから出力される生のロジット pred_logitstorch.sigmoid 関数を通すことで、各ピクセルが「対象(ここでは男性)」に属する確率([0,1][0, 1])にマッピングします。さらに、> 0.5 という条件式を用いて閾値判定を行うことで、確率が50%以上のピクセルを True(1)、それ以外を False(0) とする2値のセグメンテーション予測マスク pred_mask を作成します。
  3. matplotlibによる対比可視化: plt.subplots により、左から「入力画像と検出ボックス」「教師用マスク(本家SAMの出力)」「予測マスク(MiniSAMの出力)」の3つを並べて表示するグリッドを描画します。バウンディングボックスの描画には、正規化座標を解像度(128×128128 \times 128)に戻した上で、patches.Rectangle を作成して各プロットに重ねています。教師用マスクと予測したマスクは、alpha=0.5 を用いて画像の上に半透明(オーバーレイ)で重ねて表示し、MiniSAMが教師データの形状をどこまで完璧に模倣できているかをビジュアルで評価できる環境を整えています。

実行結果

上記のコードを実行すると以下の結果が得られます。

Grounding DINOを用いて 'man.' のバウンディングボックスを検出しています...
検出されたボックス: [0.547149658203125, 62.034332275390625, 331.4342956542969, 508.1872863769531]
SAMを用いて、検出したボックスから高精度な教師マスクを生成しています...
MiniSAMのサニティチェック(過学習)を開始します...
Epoch [ 30/150], Loss: 0.1323
Epoch [ 60/150], Loss: 0.0563
Epoch [ 90/150], Loss: 0.0328
Epoch [120/150], Loss: 0.0160
Epoch [150/150], Loss: 0.0065

MiniSAM実行結果

表示された実行結果および出力画像(Predicted Mask (MiniSAM))は、自作した MiniSAM モデルがたった150エポックの学習で、提示された画像特徴とプロンプト(バウンディングボックス)から完璧なセグメンテーションマスクを予測できていることを示しています。

  • ログの推移: Grounding DINOによって画像中の男性が正確に囲い込まれ(検出ボックス: [0.547..., 62.034..., 331.434..., 508.187...])、それをもとに本家SAMから非常に精緻なアノテーションマスクが抽出されています。これを用いて構築したMiniSAMの訓練では、30エポック時点で Loss が 0.1323 まで低下し、150エポック終了時には 0.0065 という極限状態にまで収束しています。

  • 可視化の対比: 1番左の「Input Image & DINO Box」では、テキスト指示「man」によって男性の全身が正確に赤いボックスで捉えられていることがわかります。そして、2番目の「Target Mask (SAM with Box)」と、3番目の「Predicted Mask (MiniSAM)」を詳細に比較すると、自作した MiniSAM の予測マスク(青色の半透明領域)が、教師マスクが持つカメラマンの頭部、服、ズボン、および背景との境界線を極めて美しく、かつ正確に再現していることがひと目で確認できます。

    これにより、巨大な画像エンコード部から得られた特徴空間と、極小モデルのプロンプト情報をクロスアテンションで効率的に接続し、転置畳み込みで解像度を復元するという「SAMの非対称アーキテクチャ設計」が、画像セグメンテーションタスクにおいて驚異的に機能することをコードレベルで証明できました。

実際のHugging Faceライブラリを用いた推論テスト

以下のコードでは、Hugging Faceの transformers パッケージを使用して、実用的なセグメンテーション推論を実行します。公式のリポジトリを直接扱う場合、重みファイルを手動で管理する手間がありますが、Hugging Face版を利用すれば、モデルのロードから画像の前処理までを極めてシンプルかつ安全に実装できます。

ここでは点プロンプトによる曖昧さを排除するため、「Grounding DINO」という別の強力なゼロショット物体検出モデルとSAMを組み合わせます。テキスト(例:"man.")から自動で対象のバウンディングボックスを検出し、それをSAMにプロンプトとして渡す「テキストから高精度マスクを生成するパイプライン(Grounded-SAM)」のプロセスを示します。

import torch
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
from skimage import data, color
from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection
from transformers import SamModel, SamProcessor

print("Hugging Faceからモデルをロードして推論を実行します...")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 1. 画像の準備(scikit-imageの内蔵データからカメラマンの画像を読み込み)
image = data.camera()
# グレースケール画像をモデル入力用のRGB(3チャンネル)に変換
if image.ndim == 2:
image = color.gray2rgb(image)

text_prompt = "man."
print(f"Grounding DINOで '{text_prompt}' の領域を検出中...")

dino_processor = AutoProcessor.from_pretrained("IDEA-Research/grounding-dino-base")
dino_model = AutoModelForZeroShotObjectDetection.from_pretrained("IDEA-Research/grounding-dino-base").to(device)

dino_inputs = dino_processor(images=image, text=text_prompt, return_tensors="pt").to(device)
with torch.no_grad():
dino_outputs = dino_model(**dino_inputs)

# ボックスの座標を取得
target_sizes = torch.tensor([image.shape[:2]]).to(device)
dino_results = dino_processor.image_processor.post_process_object_detection(
dino_outputs, threshold=0.3, target_sizes=target_sizes
)[0]

# 最もスコアの高いボックスを採用
best_box_idx = dino_results["scores"].argmax()
bbox = dino_results["boxes"][best_box_idx].tolist() # [xmin, ymin, xmax, ymax]

print("SAMでボックス領域のセグメンテーションを実行中...")
sam_name = "facebook/sam-vit-base"
sam_processor = SamProcessor.from_pretrained(sam_name)
sam_model = SamModel.from_pretrained(sam_name).to(device)

# Boxプロンプトを入力として渡す (Hugging Faceの仕様でリストをネスト: [[[xmin, ymin, xmax, ymax]]])
sam_inputs = sam_processor(image, input_boxes=[[[bbox]]], return_tensors="pt").to(device)

with torch.no_grad():
sam_outputs = sam_model(**sam_inputs)

# SAMの出力(256x256)を元の画像サイズに正確に復元する
# post_process_masksを使用することで、アスペクト比を維持したパディングなども適切に逆変換されます
masks = sam_processor.image_processor.post_process_masks(
sam_outputs.pred_masks.cpu(),
sam_inputs["original_sizes"].cpu(),
sam_inputs["reshaped_input_sizes"].cpu()
)

# 曖昧さに対処するため3つのマスクが出力されるので、最も確信度(IoUスコア)が高いものを選択
scores = sam_outputs.iou_scores[0, 0].cpu().numpy()
best_mask_idx = np.argmax(scores)

# masks[0]の形状は [num_prompts, num_masks, H, W] -> [1, 3, 元の高さ, 元の幅]
# 既に元の画像サイズに復元されているため、インデックスを指定して取得
best_mask = masks[0][0, best_mask_idx].numpy()

fig, ax = plt.subplots(1, 1, figsize=(8, 8))
ax.imshow(image)

# マスクを青色の半透明でオーバーレイ表示
mask_image = np.zeros((*best_mask.shape, 4))
mask_image[best_mask] = np.array([0.1, 0.2, 0.9, 0.6]) # RGBA形式
ax.imshow(mask_image)

# Grounding DINOが検出したバウンディングボックスを赤枠で描画
xmin, ymin, xmax, ymax = bbox
width, height = xmax - xmin, ymax - ymin
rect = patches.Rectangle((xmin, ymin), width, height, linewidth=2, edgecolor='red', facecolor='none')
ax.add_patch(rect)

plt.title(f"Grounded-SAM Inference (Text: '{text_prompt}')")
plt.axis("off")
plt.show()

ここでは、Hugging Faceの transformers パッケージを用いた実務的な推論コードを解説します。コードの簡潔さと、2つの異なるAIモデルをシームレスに結合するパイプライン設計に注目してください。

  • テキストからマスクを生成する高度な連携: 点をクリックする代わりに、自然言語で「man.」と指定するだけで、まずGrounding DINOが画像のどこに人がいるかの矩形(バウンディングボックス)を特定します。その正確な座標情報をSAMのプロンプトとして引き渡すことで、完全に自動化された高精度なセグメンテーションが実現します。
  • スマートなデータ処理の抽象化: SamProcessor は、入力されたバウンディングボックスのリスト input_boxes=[[[bbox]]] を受け取ると、SAM特有の複雑な画像リサイズや座標の正規化をすべて裏側で自動処理し、モデルが直接処理できるテンソル形式に変換してくれます。
  • 曖昧さへの対応と確信度の活用: SAMの推論結果の outputs.pred_masks には、デフォルトで3つの階層的なマスク(全体・部分など)が含まれています。ここでは outputs.iou_scores を用いて、モデルが「最も対象物にフィットしている」と自信を持っているマスク(IoUスコアが最大のもの)をプログラム的に選択しています。
  • post_process_masks の活用: 単純な cv2.resize では、SAMが行う「アスペクト比を保持したパディング(余白の追加)」が考慮されないため、縦横比の違う画像ではズレが発生してしまいます。sam_inputs に含まれる original_sizes(元の画像サイズ)と reshaped_input_sizes(パディング前のリサイズ状態)を post_process_masks メソッドに渡すことで、内部で逆算処理が行われ、ピクセルレベルで完璧に一致するマスクが抽出されます。

実行結果

上記のコードを実行すると以下の結果が得られます。

Hugging Faceからモデルをロードして推論を実行します...
Grounding DINOで 'man.' の領域を検出中...
SAMでボックス領域のセグメンテーションを実行中...

Hugging Face SAM実行結果

実行結果の画像を見ると、Grounding DINOによってカメラマンの周囲に赤いバウンディングボックス(矩形)が正確に引かれ、その枠情報を手掛かりにSAMがカメラマンの身体の輪郭をピクセルレベルで完璧に切り出していることが分かります。

点プロンプト単体では「カメラだけ」や「顔だけ」に誤検知してしまうような曖昧なケースでも、このようにテキストによる「対象物の意味」とバウンディングボックスによる「領域の制限」を組み合わせることで、実環境での推論精度と安定性が飛躍的に向上します。これが、現在様々なビジョンAIシステムでSAMが強力なコンポーネントとして活用されている最大の理由です。

まとめ

本記事では、コンピュータビジョンの分野において「ファウンデーションモデル(基盤モデル)」という新たなパラダイムを切り拓いた Segment Anything Model (SAM) の画期的なアーキテクチャとその実力について解説しました。

記事を通じて、以下の内容を学び、実践しました。

  • アーキテクチャの非対称性の理解: 巨大なビジョン処理を「画像エンコーダ」に1度だけ担わせ、リアルタイムに動作する軽量な「プロンプトエンコーダ」と「マスクデコーダ」を組み合わせることで、インタラクティブで汎用的な推論を実現していることを学びました。
  • 曖昧さへの対応 (Ambiguity-aware): 点やボックスのような曖昧なプロンプトに対しても、複数のマスク候補(全体・部分・小部分)を同時に予測することで、ゼロショット性能を飛躍的に向上させるモデル設計の工夫を確認しました。
  • MiniSAMのスクラッチ実装: PyTorchを用いて、画像エンコーダ、プロンプトエンコーダ、マスクデコーダがどのように連動してアテンション情報を処理するかをコードレベルで設計・検証しました。
  • 公式パッケージを用いたテキスト連携・高精度推論: transformers ライブラリを活用し、ゼロショット物体検出モデル(Grounding DINO)とSAMを組み合わせた「テキストから直接マスクを生成する(Grounded-SAM)」パイプラインを検証しました。また、post_process_masks を活用してアスペクト比を維持したパディングなどのリサイズ逆算処理を適用し、任意の画像サイズに対してズレのない高精度なマスクを自動復元・描画できる高度な実装手法を学びました。

SAMは、モデル自体の革新性に加え、モデルを組み込んだインタラクティブなアノテーションループ(データエンジン)により構築された11億のマスクデータセット(SA-1B)によって学習されています。この学習済みの強力なモデルは、医療画像やドローン空撮画像、自動運転データなど、学習時に一度も見たことのない全く新しい領域に対しても、ゼロショットで高い汎化性能を発揮します。今後、SAMは様々な画像認識システムに組み込まれる共通の「部品(コンポーネント)」として、ビジョンAIの可能性をさらに押し広げていくでしょう。

ライセンスに関する重要な注意点

SAMのソースコードおよび学習済みモデルの重みは、オープンソースとして広く普及させることを目的として Apache 2.0 ライセンスの下で公開されています。

Apache 2.0は非常に寛容(パーミッシブ)なライセンスであり、商用利用、修正、配布、特許の利用が許可されています。YOLO11が採用している強いコピーレフトのAGPL-3.0とは異なり、SAMを組み込んで開発した新しいソフトウェアやクラウドサービスに対して、ソースコードの公開義務は発生しません。 ただし、免責事項の記載や著作権表示、元のライセンスへのリンクを含める必要がある点には留意してください。このライセンスの柔軟さも、SAMが世界中の研究者や企業によって爆発的に活用されている理由の一つです。

また、副次的な注意点として、SAMの画期的な汎化性能の源泉となった11億のセグメンテーションデータセット SA-1B は、研究用途に限定されたライセンスで公開されています。そのため、モデルそのものやコードを商用プロジェクトに組み込んでサービス展開することは完全に許可されていますが、SA-1Bデータセットそのものをダウンロードしてアノテーション用途以外で商用システムに二次利用・再配布する場合には制限を受ける可能性がある点に留意してください。

※法的な助言ではありませんので、実際の製品への組み込み時はご自身でライセンス条文を必ずご確認ください。

注釈・用語解説

  • ファウンデーションモデル(Foundation Model): 大規模なデータセットで事前学習され、プロンプトエンジニアリングなどを用いて、学習時に想定されていなかった多様なタスクや新しいデータ分布適応(ゼロショット学習)できる強力な基盤モデルのこと。
  • ゼロショット転移(Zero-shot transfer): 新しいデータセットや未知のタスクに対して、追加の学習(ファインチューニング)を一切行うことなく、プロンプトなどを工夫するだけで直接モデルを適用して結果を得る能力。
  • SA-1B: SAMのデータエンジンによって収集された、1,100万枚のライセンス済み・プライバシー保護処理済みの画像と、11億以上の高品質な自動生成セグメンテーションマスクからなる史上最大のデータセット。
  • mIoU (mean Intersection over Union): 予測されたマスクと正解(グラウンドトゥルース)マスクの重なり度合いを示す指標。IoUの全対象物における平均値。セグメンテーションの精度評価における標準的な指標。
  • MAE (Masked Autoencoders): 画像の一部を隠し(マスクし)、残りの部分から隠された部分を再構成するようにモデルを学習させる自己教師あり学習の強力な手法。SAMの画像エンコーダの事前学習に使用されている。

引用・参考文献

学術論文やプロジェクトにおいてSAMを引用する際は、以下のBibTeXを使用してください。

@article{kirillov2023segany,
title={Segment Anything},
author={Kirillov, Alexander and Mintun, Eric and Ravi, Nikhila and Mao, Hanzi and Rolland, Chloe and Gustafson, Laura and Xiao, Tete and Whitehead, Spencer and Berg, Alexander C. and Lo, Wan-Yen and Doll{\'a}r, Piotr and Girshick, Ross},
journal={arXiv:2304.02643},
year={2023}
}

本記事の文章・構成の一部に生成AIを使用しています。