Hayami Kaito Web
~/tech

セグメンテーション AI のしくみ:FCN・U-Net から Mask R-CNN・SAM まで

published

セグメンテーション AI は、写真を受け取って「この画素は猫、この画素は道路、この画素は空」のように、画素 1 つ 1 つに種類のラベルを付けて返すプログラムです。物体検出が四角い枠で「だいたいこのあたり」と答えるのに対して、セグメンテーションは物体の輪郭まで正確に塗り分けます。自動運転の道路と歩道の区別、医療画像の臓器や腫瘍の切り出し、写真アプリの背景ぼかしや切り抜きなどで使われています。

この記事は 画像分類 AI のしくみ、物体検出 AI のしくみ の続きです。畳み込み・ソフトマックス・学習のしくみは画像分類の記事で、IoU・アンカー・Faster R-CNN・DETR は物体検出の記事で説明したので、この記事では「画素ごとに答えるには何が必要か」に絞って説明します。

結論を先に書くと、セグメンテーションのモデルは次の 3 つの工夫でできています。

  • 画素ごとに分類する: 画像分類のソフトマックスを、画像全体に 1 回ではなく、画素ごとに行う
  • 小さくした特徴マップを元の大きさに戻す: CNN は画像を縮めながら特徴をつかむので、縮めた結果をアップサンプリングで元の解像度に戻し、浅い層の細かい情報と組み合わせる
  • 物体を 1 つずつ見分けるなら検出と組み合わせる: 「猫の画素」だけでなく「1 匹目の猫」「2 匹目の猫」を分けるには、物体検出の枠ごとにマスク(塗り分け)を出す

説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。

環境

計算例を動かした環境です。

項目バージョン
Python3.13
NumPy2.5

セグメンテーションとは:画素ごとに「何か」を答える

物体検出の記事で、画像を扱うタスクを答えの細かさで分けました。セグメンテーションは、その中でいちばん細かい答えを出すタスクです。

タスク答え例
画像分類画像全体に 1 つのラベル「猫の写真」
物体検出物体ごとに枠と種類「(50, 50)〜(150, 150) に猫」
セグメンテーション画素ごとに種類「この画素は猫、この画素は背景」

3 種類のセグメンテーション

セグメンテーションは、「同じ種類の物体を 1 つずつ区別するかどうか」で、さらに 3 つに分かれます。猫が 2 匹並んだ写真で考えます。

種類答え2 匹の猫は
セマンティックセグメンテーション(意味的領域分割)すべての画素に種類のラベルどちらも「猫」で、区別しない
インスタンスセグメンテーション物体ごとにマスクと種類「猫 1」「猫 2」と区別する。背景の画素にはラベルを付けない
パノプティックセグメンテーションすべての画素に種類と物体の番号「猫 1」「猫 2」と区別し、空や道路などの画素にも種類を付ける

ここで、猫・人・車のように 1 つ、2 つと数えられるものを thing(物体)、空・道路・草地のように数えられず広がっているものを stuff(領域)と呼びます。セマンティックセグメンテーションは stuff を得意とし、インスタンスセグメンテーションは thing だけを扱います。パノプティックセグメンテーションは両方をまとめて扱います。

答えは画像と同じ大きさのラベルの配列

セマンティックセグメンテーションの答えは、画像と同じ高さ・幅を持つ、整数のラベルの配列です。たとえば 0 を背景、1 を猫、2 を犬とすると、次のような配列になります。

0 0 0 0 0 0
0 1 1 0 2 0
0 1 1 0 2 2
0 0 0 0 0 0

この配列はマスクとも呼ばれます。インスタンスセグメンテーションでは、物体ごとに「この物体に含まれる画素は 1、それ以外は 0」という 0 と 1 のマスクを 1 枚ずつ出します。

深層学習以前の方法

深層学習が広まる前は、明るさや色の似た画素をまとめる方法で領域を分けていました。

  • しきい値処理: 明るさがある値より大きい画素と小さい画素に分ける。白い紙の上の文字のように、背景と物体の明るさがはっきり違うときに使える
  • クラスタリング: 画素の色を 3 次元の点とみなし、k-means 法などで似た色どうしをまとめる

どちらも「似た見た目の画素をまとめる」だけなので、「茶色い猫と茶色いソファ」のように見た目が似ているのに意味の違うものは分けられません。意味まで考えて塗り分けるには、画像分類で使われてきた CNN の力が必要でした。

このサイトの k-means 法の可視化 では、平面上の点が割り当てと中心の更新をくり返してまとまっていく様子を見られます。画素の色を点とみなせば、同じしくみで色の似た画素をまとめられます。

画素ごとの分類:ソフトマックスを画素の数だけ行う

出力は「クラスの数 × 高さ × 幅」の点数

画像分類のモデルは、最後に「クラスの数」個の点数を出し、ソフトマックスで確率に変えていました。セグメンテーションのモデルは、これを画素ごとに行います。出力は「クラスの数 × 高さ × 幅」の点数の配列で、画素ごとにクラスの方向でソフトマックスを取り、いちばん確率の高いクラスをその画素のラベルにします。

損失は画素ごとの交差エントロピー誤差の平均

学習の損失も、画像分類と同じ交差エントロピー誤差を画素ごとに計算し、全画素で平均したものです。

高さ 2・幅 3 の小さな画像で、3 クラス(0: 背景、1: 猫、2: 犬)の点数から、画素ごとのラベルと損失を計算してみます。

import numpy as np

# 3 クラス(0: 背景, 1: 猫, 2: 犬)の点数。形は (クラス, 高さ, 幅) = (3, 2, 3)
logits = np.array([
    [[2.0, 1.0, 0.1], [2.5, 0.2, 0.3]],  # 背景
    [[0.5, 2.0, 0.2], [0.1, 1.5, 0.4]],  # 猫
    [[0.1, 0.3, 1.8], [0.2, 0.1, 2.2]],  # 犬
])
probs = np.exp(logits) / np.exp(logits).sum(axis=0, keepdims=True)  # 画素ごとのソフトマックス
pred = probs.argmax(axis=0)
print(pred)
target = np.array([[0, 1, 2], [0, 1, 1]])  # 正解のラベル
p_true = np.take_along_axis(probs, target[None], axis=0)[0]
print(np.round(p_true, 3))
print(round(float(-np.log(p_true).mean()), 3))

実行結果:

[[0 1 2]
 [0 1 2]]
[[0.728 0.645 0.722]
 [0.84  0.658 0.126]]
0.625

1 行目が予測したラベルの配列、2 行目が各画素で「正解のクラス」に付いた確率、3 行目がその損失の平均です。右下の画素は正解が猫(1)なのに犬(2)と予測していて、正解のクラスの確率が 0.126 と低くなっています。この画素の損失 −log(0.126) ≒ 2.07 が、平均の損失を押し上げています。学習では、この平均の損失が下がるようにフィルターの値を調整します。

FCN:全結合層をなくして画素ごとに答える

全結合層のかわりに 1 × 1 の畳み込みを使う

画像分類の CNN は、最後の全結合層で特徴マップを 1 列に並べてクラスの点数を出していました。全結合層は入力の大きさが決まっていて、出力から「どこにあったか」の情報が失われます。

2015 年の FCN(Fully Convolutional Network、全畳み込みネットワーク)は、全結合層を 1 × 1 の畳み込みに置き換えました。1 × 1 の畳み込みは、特徴マップの各位置で、チャンネル方向の値からクラスの点数を計算します。これで、どの大きさの画像を入れても、位置の情報を保ったまま「位置ごとのクラスの点数」が出るようになりました。FCN は、AlexNet・VGG・GoogLeNet などの画像分類で学習済みのモデルをこの形に作り替え、セグメンテーション用に追加で学習させています。

FCN は PASCAL VOC 2012 のセマンティックセグメンテーションで、それまでの最高の結果を相対値で 20% 上回る 62.2% の mIoU(後述)を出しました。

アップサンプリング:縮んだ特徴マップを元の大きさに戻す

ただし、CNN はプーリングやストライドで特徴マップをくり返し縮めるので、最後の層の特徴マップは元の画像の 1/32 などの大きさしかありません。画素ごとの答えを出すには、これを元の大きさに戻すアップサンプリングが必要です。

アップサンプリングには、いくつかの方法があります。

方法しくみ
最近傍補間各値をそのまま縦横に複製する
双線形補間周りの 4 つの値から、距離に応じた重み付きの平均で間の値を作る
転置畳み込み入力の各値でフィルターを拡大・縮小して並べ、重なったところを足す。フィルターの値は学習で決める

転置畳み込み(transposed convolution)は、畳み込みの逆向きの操作です。畳み込みが「周りの値を集めて 1 つの値にする」のに対して、転置畳み込みは「1 つの値を周りにばらまく」操作です。2 × 2 の入力を、ストライド 2 で 4 × 4 に広げてみます。

def conv_transpose2d(x, k, stride):
    h, w = x.shape
    kh, kw = k.shape
    out = np.zeros(((h - 1) * stride + kh, (w - 1) * stride + kw))
    for i in range(h):
        for j in range(w):
            out[i*stride:i*stride+kh, j*stride:j*stride+kw] += x[i, j] * k
    return out

x = np.array([[1.0, 2.0], [3.0, 4.0]])
print(conv_transpose2d(x, np.ones((2, 2)), 2))
print(conv_transpose2d(x, np.array([[1.0, 0.5], [0.5, 0.25]]), 2))

実行結果:

[[1. 1. 2. 2.]
 [1. 1. 2. 2.]
 [3. 3. 4. 4.]
 [3. 3. 4. 4.]]
[[1.   0.5  2.   1.  ]
 [0.5  0.25 1.   0.5 ]
 [3.   1.5  4.   2.  ]
 [1.5  0.75 2.   1.  ]]

フィルターがすべて 1 なら最近傍補間と同じ結果になり、フィルターの値を変えると広げ方が変わります。転置畳み込みでは、このフィルターの値を学習で決めるので、「どう広げると正解のマスクに近づくか」をモデル自身が覚えます。

スキップ接続で細かい位置の情報を補う

1/32 に縮んだ特徴マップを一気に 32 倍に広げると、輪郭がぼやけたマスクになります。深い層の特徴マップは「何があるか」はよく分かっていても、「正確にどこか」の情報が縮める途中で失われているためです。

FCN は、深い層の出力を広げたものに、浅い層の特徴マップから計算した点数を足し合わせることでこれを補いました。浅い層は解像度が高く、細かい位置や輪郭の情報を持っています。

  • FCN-32s: 最後の層の出力を一気に 32 倍に広げる
  • FCN-16s: 2 倍に広げて 1/16 の層の点数と足し、それを 16 倍に広げる
  • FCN-8s: さらに 1/8 の層の点数とも足し、8 倍に広げる

浅い層の情報を使うほど、輪郭が細かくなります。このように、離れた層の情報を途中を飛ばしてつなぐしくみをスキップ接続と呼びます。

U-Net:縮める道と戻す道を左右対称にする

2015 年の U-Net は、もともと顕微鏡の細胞画像のセグメンテーションのために提案されたモデルです。FCN のスキップ接続の考え方を、より徹底させました。

  • 縮める道(エンコーダー): 畳み込みとプーリングをくり返して特徴マップを小さくし、「何があるか」の情報を集める
  • 戻す道(デコーダー): 転置畳み込みで少しずつ大きくしながら、位置を細かく合わせていく
  • スキップ接続: 縮める道の各段の特徴マップを、同じ大きさの戻す道の段にそのまま連結して渡す

構造を図にすると、縮める道と戻す道がアルファベットの U の字に並ぶので、U-Net と呼ばれます。

入力 ─▶ [段1] ──────────────────────────▶ [段1'] ─▶ 出力
            ↓ 縮める                      ↑ 戻す
          [段2] ─────────────────────▶ [段2']
              ↓                         ↑
            [段3] ────────────────▶ [段3']
                ↓                    ↑
                  ───▶ [いちばん下] ──▶
(横向きの矢印がスキップ接続)

FCN が浅い層の情報を「点数として足す」だったのに対して、U-Net は特徴マップを「チャンネルの方向に連結する」ので、戻す道の畳み込みが浅い層の情報を自由に組み合わせて使えます。

U-Net は少ない枚数の学習画像でも精度が出るように、画像を回転・変形させて水増しするデータ拡張を多く使っています。論文では、電子顕微鏡の画像で神経の構造を切り出すコンテスト(ISBI)でそれまでの最高の手法を上回り、512 × 512 の画像のセグメンテーションを GPU で 1 秒未満で処理できたと報告されています。医療画像の分野では、今でも U-Net とその改良版が広く使われています。

膨張畳み込み:解像度を保ったまま広い範囲を見る

縮めずに見る範囲を広げたい

画像を縮めると位置の情報が失われ、縮めないと 1 つの値が見ている画像の範囲(受容野)が広がりません。画像分類の記事で説明したとおり、3 × 3 の畳み込みを重ねると受容野は 3 → 5 → 7 と 2 ずつしか広がらないからです。

この取り合いを解くのが、膨張畳み込み(dilated convolution、atrous convolution とも)です。フィルターの値と値の間に隙間を空けて、離れた画素を読むようにした畳み込みです。

普通の 3 × 3(膨張率 1)   膨張率 2 の 3 × 3
                           ■ □ ■ □ ■
■ ■ ■                      □ □ □ □ □
■ ■ ■                      ■ □ ■ □ ■
■ ■ ■                      □ □ □ □ □
                           ■ □ ■ □ ■
(■ がフィルターで読む画素)

膨張率 2 の 3 × 3 のフィルターは、重みの数は 9 個のままで、5 × 5 の範囲を見ます。

受容野の広がり方を比べる

3 × 3 の畳み込みを 3 層重ねたときの受容野を、普通の畳み込みと、膨張率を 1・2・4 と倍にしていく膨張畳み込みで比べます。1 層ごとに受容野は「(フィルターの大きさ − 1) × 膨張率」だけ広がります。

def receptive_field(dilations, k=3):
    rf = 1
    for d in dilations:
        rf += (k - 1) * d
    return rf

for name, ds in [("普通", [1, 1, 1]), ("膨張", [1, 2, 4])]:
    print(name, [receptive_field(ds[:n]) for n in range(1, 4)])

実行結果:

普通 [3, 5, 7]
膨張 [3, 7, 15]

普通の畳み込みでは 3 層で 7 × 7 の範囲しか見えないのに対して、膨張率を倍にしていくと 15 × 15 の範囲まで見えます。層を重ねるほど受容野は指数関数的に広がり、しかも特徴マップを縮めていないので、位置の情報は失われません。

DeepLab と ASPP:いくつもの範囲を同時に見る

DeepLab は、膨張畳み込みを中心に据えたセマンティックセグメンテーションのモデルで、2014 年から改良が続けられました。2016 年の版(DeepLab v2)では、ASPP(Atrous Spatial Pyramid Pooling)を導入しています。同じ特徴マップに、膨張率の違う膨張畳み込みを並列にかけて結果をまとめるしくみです。

膨張率の小さい畳み込みは近くの細かい模様を、大きい畳み込みは広い範囲の文脈(「周りに道路があるから、これは車だろう」)を捉えます。これを同時に使うことで、大きさの違う物体をまとめて扱えます。DeepLab v2 は PASCAL VOC 2012 で 79.7% の mIoU を出しました。2018 年の DeepLab v3+ は、ASPP に U-Net のような戻す道(デコーダー)を組み合わせて、輪郭をさらに細かくしています。

評価のしかた:画素の正解率と mIoU

画素の正解率は背景に引きずられる

いちばん素直な評価は、正しくラベルを付けられた画素の割合(画素の正解率)です。しかし、セグメンテーションの画像の多くは背景が大半を占めるので、この指標は当てになりません。

10 × 10 = 100 画素の画像のうち、物体が 10 画素だけの例で確かめます。「すべての画素を背景と答える」モデルと、「物体をだいたい当てる」モデルを比べます。

gt = np.zeros((10, 10), dtype=int); gt[3:5, 3:8] = 1   # 正解: 物体は 10 画素
pred_all_bg = np.zeros_like(gt)                        # すべて背景と答える
pred_ok = np.zeros_like(gt); pred_ok[3:5, 4:8] = 1; pred_ok[5, 4:6] = 1  # だいたい当てる

def scores(pred, gt, n_classes=2):
    acc = (pred == gt).mean()
    ious = []
    for c in range(n_classes):
        inter = np.sum((pred == c) & (gt == c))
        union = np.sum((pred == c) | (gt == c))
        ious.append(inter / union)
    p, g = pred == 1, gt == 1
    dice = 2 * np.sum(p & g) / (p.sum() + g.sum())
    return round(float(acc), 3), [round(float(v), 3) for v in ious], round(float(np.mean(ious)), 3), round(float(dice), 3)

print("すべて背景", scores(pred_all_bg, gt))
print("だいたい当たり", scores(pred_ok, gt))

実行結果:

すべて背景 (0.9, [0.9, 0.0], 0.45, 0.0)
だいたい当たり (0.96, [0.957, 0.667], 0.812, 0.8)

表にすると次のとおりです。

モデル画素の正解率背景の IoU物体の IoUmIoU物体の Dice
すべて背景0.9000.9000.0000.4500.000
だいたい当たり0.9600.9570.6670.8120.800

物体を 1 画素も見つけていない「すべて背景」のモデルでも、画素の正解率は 0.9 です。画素の正解率だけを見ると、2 つのモデルの差は 0.06 しかありません。

mIoU:クラスごとの IoU を平均する

そこでよく使われるのが mIoU(mean IoU、平均 IoU)です。物体検出の記事で枠どうしの重なりを測った IoU を、画素の集合に当てはめます。

クラス c の IoU = (予測も正解も c の画素数) ÷ (予測か正解の少なくとも一方が c の画素数)
mIoU = クラスごとの IoU の平均

クラスごとに IoU を出してから平均するので、画素の少ないクラスも、画素の多い背景と同じ重みで評価されます。上の例では、「すべて背景」のモデルは物体の IoU が 0 なので、mIoU が 0.45 まで下がり、2 つのモデルの差がはっきり出ます。

Dice 係数と Dice 損失

医療画像などでは、IoU とよく似た Dice 係数もよく使われます。

Dice = 2 × (予測と正解の重なった画素数) ÷ (予測の画素数 + 正解の画素数)

上の「だいたい当たり」のモデルでは、重なりが 8 画素、予測と正解がどちらも 10 画素なので、Dice = 2 × 8 ÷ 20 = 0.8 です。Dice と IoU は Dice = 2 × IoU ÷ (1 + IoU) の関係にあり、どちらも「重なりの割合」を測っています(IoU 0.667 なら Dice 0.8)。

学習では、「1 − Dice 係数」を損失として使うことがあります(Dice 損失)。画素ごとの交差エントロピー誤差は全画素の平均なので、背景が 9 割を占めると背景の損失に引きずられます。Dice 損失は物体の画素の重なりだけで決まるので、腫瘍のように小さな領域を切り出すときに、背景に埋もれずに学習を進められます。

インスタンスセグメンテーション:Mask R-CNN

ここまでのセマンティックセグメンテーションでは、並んだ 2 匹の猫は「猫の画素のかたまり」になり、1 匹ずつには分かれません。物体を 1 つずつ区別するには、物体検出と組み合わせる方法が代表的です。

Faster R-CNN にマスクを出す枝を足す

2017 年の Mask R-CNN は、物体検出の記事で紹介した Faster R-CNN に、候補の領域ごとにマスクを出す枝を足したモデルです。

  1. Faster R-CNN と同じく、RPN で物体の候補の領域を出す
  2. 候補ごとに、種類の確率と枠の補正を出す(Faster R-CNN と同じ)
  3. 候補ごとに、小さな FCN で 28 × 28 のマスクを出す(ここが新しい)
  4. マスクを枠の大きさに引き伸ばして、画像の上に置く

検出の枠ごとにマスクを出すので、2 匹の猫には別々の枠があり、別々のマスクが付きます。マスクの枝は、クラスごとに「この画素が物体かどうか」を 0〜1 で出し、検出の枝が判定した種類のマスクだけを使います。種類の判定とマスクの形を別々に学習させることで、精度が上がったと報告されています。

マスクの枝は計算量が小さく、Mask R-CNN は毎秒 5 枚で動きます。また、人の関節の位置を当てる姿勢推定など、ほかのタスクにも同じ枠組みで広げられることが示されました。

RoIAlign:候補の位置を丸めない

Mask R-CNN にはもう 1 つ大事な工夫があります。Fast R-CNN の RoI プーリングは、候補の領域を特徴マップから切り出すときに、座標を整数に丸めていました。枠を出すだけならこのずれは小さな問題でしたが、画素単位のマスクでは、丸めによるずれがそのまま輪郭のずれになります。

特徴マップの 1 マスが画像の 16 画素に当たるとき、画像の (y = 20, x = 44) の位置は、特徴マップでは (1.25, 2.75) です。この位置の値を、丸めて読む場合と、小数の位置のまま周りの 4 マスから双線形補間で読む場合を比べます。

def bilinear(f, y, x):
    y0, x0 = int(np.floor(y)), int(np.floor(x))
    dy, dx = y - y0, x - x0
    return (f[y0, x0] * (1 - dy) * (1 - dx) + f[y0, x0 + 1] * (1 - dy) * dx
            + f[y0 + 1, x0] * dy * (1 - dx) + f[y0 + 1, x0 + 1] * dy * dx)

fmap = np.arange(25, dtype=float).reshape(5, 5) * 10  # 5 × 5 の特徴マップ(値は 0, 10, 20, …)
stride = 16                      # 特徴マップの 1 マス = 画像の 16 画素
y, x = 20 / stride, 44 / stride  # 画像の (y=20, x=44) は特徴マップでは
print(y, x)
print(fmap[round(y), round(x)])               # RoI プーリング: 整数に丸めてから読む
print(round(float(bilinear(fmap, y, x)), 3))  # RoIAlign: 小数の位置のまま補間して読む

実行結果:

1.25 2.75
80.0
90.0

丸めると (1, 3) のマスを読むことになり、本来の位置から縦横に 0.25 マス、つまり画像の上で 4 画素ずれた場所の値(80)を使ってしまいます。双線形補間なら、小数の位置に合った値(90)を読めます。Mask R-CNN では、この丸めをなくした切り出し方を RoIAlign と呼び、マスクの精度を大きく改善しました。

パノプティックセグメンテーションと PQ

stuff と thing をまとめて塗り分ける

2019 年に提案されたパノプティックセグメンテーションは、セマンティックセグメンテーションとインスタンスセグメンテーションを 1 つのタスクにまとめたものです。すべての画素に「種類」と「物体の番号」を付けます。空や道路(stuff)は種類だけ、猫や車(thing)は種類と「何匹目・何台目か」を持ちます。1 つの画素は、必ずどれか 1 つの領域に属します(マスクどうしが重ならない)。

PQ:認識の質と塗り分けの質を掛け合わせる

パノプティックセグメンテーションの評価には、同じ論文で提案された PQ(Panoptic Quality)を使います。

  1. 予測の領域と正解の領域を、同じ種類どうしで IoU を計算して突き合わせ、**IoU が 0.5 を超えた組を当たり(TP)**とする。マスクどうしは重ならないので、IoU が 0.5 を超える組は 1 つの正解に対して高々 1 つしかなく、組み合わせ方に迷わない
  2. 組にならなかった予測を外れ(FP)、組にならなかった正解を見落とし(FN)とする
  3. 次の式で計算する
PQ = (当たりの組の IoU の合計) ÷ (当たりの数 + 0.5 × 外れの数 + 0.5 × 見落としの数)
   = SQ × RQ

SQ(塗り分けの質)= 当たりの組の IoU の平均
RQ(認識の質)    = 当たりの数 ÷ (当たりの数 + 0.5 × 外れの数 + 0.5 × 見落としの数)

正解が 3 個、予測が 3 個あり、そのうち 2 組が IoU 0.8 と 0.6 で当たり、残りの予測 1 個が外れ、正解 1 個が見落としだった例で計算します。

ious_tp = [0.8, 0.6]  # IoU が 0.5 を超えて組になった予測と正解
n_fp, n_fn = 1, 1
sq = np.mean(ious_tp)
rq = len(ious_tp) / (len(ious_tp) + 0.5 * n_fp + 0.5 * n_fn)
print(round(float(sq), 3), round(rq, 3), round(float(sq * rq), 3))

実行結果:

0.7 0.667 0.467

塗り分けの質 SQ は 0.7、認識の質 RQ は 2 ÷ (2 + 0.5 + 0.5) ≒ 0.667 で、PQ は 0.467 です。PQ を SQ と RQ に分けて見ると、「物体を見つけられていない」のか「見つけた物体の輪郭が甘い」のかを切り分けられます。

Transformer の時代:マスク分類と SAM

MaskFormer・Mask2Former:画素ではなくマスクを分類する

ここまでのセマンティックセグメンテーションは「画素ごとの分類」、インスタンスセグメンテーションは「検出してからマスク」と、タスクごとに別のしくみを使っていました。

2021 年の MaskFormer は、物体検出の記事で紹介した DETR の考え方をセグメンテーションに持ち込み、「マスクの集合を出し、マスクごとに種類を付ける」という 1 つの形にまとめました。

  1. 決まった数のクエリ(問い合わせ)を Transformer のデコーダーに入れる
  2. 各クエリが、0 と 1 のマスク 1 枚と、そのマスクの種類を出す
  3. 学習では、DETR と同じく、予測と正解を 1 対 1 で組み合わせて損失を計算する

セマンティックセグメンテーションなら「猫のマスク」を 1 枚、インスタンスセグメンテーションなら「猫 1 のマスク」「猫 2 のマスク」を別々に出せばよいので、同じモデル・同じ損失で両方のタスクを扱えます。

2022 年の Mask2Former は、デコーダーの注意(アテンション)を、前の段で予測したマスクの内側だけに絞る masked attention を導入しました。これにより、パノプティック(COCO で 57.8 PQ)・インスタンス(COCO で 50.1 AP)・セマンティック(ADE20K で 57.7 mIoU)の 3 つのタスクすべてで、それぞれの専用モデルを上回る結果を出しました。

SAM:指示されたものを何でも切り出す

2023 年の SAM(Segment Anything Model)は、「決まった種類を塗り分ける」のではなく、「指示された場所にある物体を、何であっても切り出す」モデルです。

  • プロンプト: 画像の上の点(「ここ」)や枠を指示として受け取り、そこにある物体のマスクを返す
  • あいまいさへの対応: 人のシャツの上の点は「シャツ」とも「人全体」とも取れるので、候補のマスクを複数出す
  • 種類は答えない: 出すのはマスクだけで、「猫」のような種類のラベルは付けない

SAM の学習には、1,100 万枚の画像に 10 億個以上のマスクを付けたデータセット SA-1B が使われました。モデルにマスクの案を出させ、人が直し、直したデータでモデルを学習し直すことをくり返して、マスクの大半を自動で作っています。これだけのデータで学習したことで、学習に使っていない種類の物体や、見たことのない種類の画像でも、追加の学習なしで切り出せるようになりました。

2024 年の SAM 2 は、これを動画に広げました。前のフレームの指示と予測を覚えておくメモリーを持ち、1 つのフレームで指示した物体を、ほかのフレームでも追いかけて切り出せます。

セグメンテーションの手法の流れ

この記事で紹介した手法を年代順に並べます。

年手法種類ポイント
2015FCNセマンティック全結合層をなくし、アップサンプリングとスキップ接続で画素ごとに答える
2015U-Netセマンティック縮める道と戻す道を左右対称にし、各段をスキップ接続でつなぐ
2016DeepLab v2セマンティック膨張畳み込みと ASPP で、解像度を保ったまま広い範囲を見る
2017Mask R-CNNインスタンスFaster R-CNN に候補ごとのマスクの枝を足し、RoIAlign で位置のずれをなくす
2018DeepLab v3+セマンティックASPP にデコーダーを組み合わせて輪郭を細かくする
2019パノプティックセグメンテーションパノプティックstuff と thing をまとめるタスクと PQ を提案
2021MaskFormer3 種類ともマスクの集合を出して分類する形で、タスクをまとめる
2022Mask2Former3 種類ともmasked attention で、3 つのタスクすべてで専用モデルを上回る
2023SAM指示による切り出し点や枠の指示で何でも切り出す。10 億個以上のマスクで学習
2024SAM 2指示による切り出しメモリーを持ち、動画の中で物体を追いかけて切り出す

まとめ

  • セグメンテーションは画素ごとに種類を答えるタスクで、物体を区別しないセマンティック、物体ごとにマスクを出すインスタンス、両方をまとめたパノプティックの 3 種類がある
  • 基本は画素ごとの分類で、画像分類のソフトマックスと交差エントロピー誤差を画素の数だけ計算する
  • CNN で縮んだ特徴マップは、転置畳み込みなどのアップサンプリングで元の大きさに戻し、スキップ接続で浅い層の細かい位置の情報を補う(FCN・U-Net)
  • 膨張畳み込みは、特徴マップを縮めずに受容野を広げられる(DeepLab)
  • 背景が大半を占めるので、評価は画素の正解率ではなく、クラスごとの IoU を平均した mIoU や Dice 係数を使う
  • インスタンスセグメンテーションは、物体検出の候補ごとにマスクを出す Mask R-CNN が代表的で、RoIAlign で位置の丸めによるずれをなくした
  • パノプティックセグメンテーションは、塗り分けの質と認識の質を掛け合わせた PQ で評価する
  • 最近は、DETR と同じくマスクの集合を直接出す Mask2Former や、指示されたものを何でも切り出す SAM のように、Transformer を使ったモデルが主流になっている

参考

  • J. Long et al., "Fully Convolutional Networks for Semantic Segmentation", CVPR 2015(arXiv:1411.4038)
  • O. Ronneberger et al., "U-Net: Convolutional Networks for Biomedical Image Segmentation", MICCAI 2015(arXiv:1505.04597)
  • L.-C. Chen et al., "DeepLab: Semantic Image Segmentation with Deep Convolutional Nets, Atrous Convolution, and Fully Connected CRFs", TPAMI 2017(arXiv:1606.00915)
  • L.-C. Chen et al., "Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation", ECCV 2018(arXiv:1802.02611)
  • K. He et al., "Mask R-CNN", ICCV 2017(arXiv:1703.06870)
  • A. Kirillov et al., "Panoptic Segmentation", CVPR 2019(arXiv:1801.00868)
  • B. Cheng et al., "Per-Pixel Classification is Not All You Need for Semantic Segmentation", NeurIPS 2021(arXiv:2107.06278)
  • B. Cheng et al., "Masked-attention Mask Transformer for Universal Image Segmentation", CVPR 2022(arXiv:2112.01527)
  • A. Kirillov et al., "Segment Anything", ICCV 2023(arXiv:2304.02643)
  • N. Ravi et al., "SAM 2: Segment Anything in Images and Videos", ICLR 2025(arXiv:2408.00714)