JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

從框到像素:影像分割究竟在做什麼

認識「為每個像素貼標籤」這項任務、分割的三種型態,以及深度學習之前人們使用的聰明傳統技巧。

為什麼是像素,而不是方框

我們先從你在這個學習階梯前面已經學過的東西出發。影像分類(image classification) 回答關於整張圖片的一個問題:「這是什麼?」——它對整張影像只回傳一個標籤,例如 物件偵測(object detection) 又細了一階:它不只說出物件名稱,還會在每個物件外圍畫一個 邊界框(bounding box),也就是一個剛好框住物件的矩形——「這裡有一隻貓,就在這個矩形裡。」這兩者都很有用,但請注意矩形其實有多粗略:框住一隻貓的方框,也會把牠腿後的草地和耳朵上方的天空一起吞進去。

影像分割(image segmentation) 是精細度上的下一步,而且是很大的一步。它要的不是一張影像一個標籤,也不是一個物件一個方框,而是要為 每一個像素 都貼上一個標籤。想像一本著色本:偵測只是在頁面上畫個矩形說「狗在這個範圍裡的某處」,但分割是真的把狗的形狀塗成一種顏色、地板塗成另一種顏色、牆壁塗成第三種顏色——每一塊區域都整齊地上色,一路填到它真正的邊緣。方框裡不會殘留被困住的背景像素;邊界會緊貼著物件真實的輪廓。

具體來說,分割的 輸出 是一張 地圖,高度與寬度都和輸入影像完全一樣。如果你的照片是 512×512 像素,答案也是一個 512×512 的格子——只不過每一格存的不是紅/綠/藍的數值,而是一個小整數:類別編號(class id)。也許 `0` 代表 背景、`1` 代表 道路、`2` 代表 車輛。把這張格子疊在照片上,每一個像素現在都「知道」自己屬於什麼。因為我們是在 每一個位置 都做出一個輸出,而不是對整張影像只給一個數字,這個任務就叫做 稠密預測(dense prediction)——「稠密」是指充滿了預測,一個像素一個,沒有任何空隙。這正是 影像分割 的核心。

從照片變成逐像素的標籤地圖:每個像素都依其類別上色。

左邊是一張輸入影像,右邊是同樣大小的彩色地圖,其中每個區域(天空、道路、車輛、人)都填上一種不同的純色。

三種型態:語意、實例、全景

「為每個像素貼標籤」聽起來像是單一任務,但它其實分成三個密切相關的子任務。為了體會其中的差別,我們會用同一個貫穿全文的例子,並且反覆回到它:一個 街景,裡面有 兩台並排停放的車、它們下方的 道路,以及上方的 天空。請把這張圖固定住——改變的只是 我們提出的問題

語意分割(semantic segmentation) 只問一件事:每個像素是 哪一種東西?只要屬於任何一台車的像素,都得到 這個標籤;每個道路像素得到 道路;每個天空像素得到 天空。在我們的場景裡,兩台車都被塗上同一種 的顏色——語意分割 不會 把第一台車和第二台車分開。如果兩台車相連,它們的像素就會融成一團分不出彼此的「車性」斑塊。這就是 語意分割:它知道 類別,卻不會數數。

實例分割(instance segmentation) 問的是語意分割回答不了的問題:這個像素屬於 哪一個個別物件?現在第一台車有自己的遮罩,第二台車有另一個分開的遮罩——即使它們同屬 這個類別,也是兩個不同的物件。代價在另一面:實例分割通常只專注於可數的物件,而可能 忽略背景——道路和天空也許就留白,因為「有幾條路?」不是個有意義的問題。這就是 實例分割:它能數、能分開,但常常略過沒有形狀的背景。

全景分割(panoptic segmentation) 是前兩者的統一。(「panoptic」源自希臘文,意為 看見一切。)每個像素都得到一個類別標籤,而且——對於可以數的物件——還會得到一個實例編號。在我們的場景裡:每個車的像素同時說「我是車」與「我屬於第一台車」(或第二台);每個道路像素只說「我是道路」,每個天空像素只說「我是天空」。沒有任何像素被留白,而且可數的物件依然被分辨開來。這就是 全景分割:一張完整的地圖,沒有任何像素無法解釋。

同一個街景,三種答案:語意把兩台車合併;實例把它們分開但丟掉背景;全景兩者兼具。

三張並排的街道地圖,街上有兩台車。左(語意):兩台車同一種顏色。中(實例):兩台車用兩種不同顏色,道路與天空留白。右(全景):兩台車各自分明,並且道路與天空也都有標籤。

傳統工具箱:閾值法與分水嶺

早在神經網路出現之前,人們就已經需要分割影像,並發明了一些確實聰明的技巧。最簡單的是 閾值法(thresholding)。想像一份掃描的黑白文件:墨水是暗的,紙張是亮的。要把字母抽出來,只要挑一個亮度切點——一個 閾值——把所有比它暗的像素宣告為 前景(墨水),比它亮的像素宣告為 背景(紙張)。一個數字就把整張影像分成兩群。就這樣。只要前景與背景的亮度明顯不同,它就運作得漂亮極了。

# Thresholding: the simplest segmenter of all.
# 'gray' is a grayscale image, 0 (black) .. 255 (white).

threshold = 128            # the cutoff brightness we chose

for each pixel (x, y) in gray:
    if gray[x, y] < threshold:
        label[x, y] = FOREGROUND   # darker than cutoff -> ink
    else:
        label[x, y] = BACKGROUND   # brighter than cutoff -> paper

# 'label' is now a map the same size as the image:
# a 1-bit answer at every pixel. That is already dense prediction.
閾值法把「拿每個像素和一個數字比較」變成一張完整的分割地圖。

只要一個區域和其他亮度相近的區域相連,閾值法就會瓦解——它對 連通的形狀 毫無概念。解法是一個非常視覺化的演算法:分水嶺分割(watershed segmentation)。把影像想成一片 地形:把每個像素的強度(或更常用的 梯度——亮度變化的快慢,會在邊緣處飆高)當作地勢的 高度。暗而平坦的區域成為谷地;明亮的邊緣成為山脊。接著想像 落在每個地方。水會聚集在低處的 盆地 裡,水位慢慢上升。每個盆地各自積成一座小湖。每當來自 兩個不同盆地 的水即將匯合,我們就築一道 水壩 把湖隔開。雨停之後,這些水壩就描出了區域之間的邊界——一個盆地對應一個區段。

把像素格當成地形來讀:低數值是會被水填滿的盆地,高的山脊是築壩(邊界)的地方。

一個帶數字的小像素格,被重畫成由谷地與山脊組成的立體地形,水填滿低處盆地直到水壩線為止。

超像素:把屬於一起的像素聚成一群

這裡有個令人不安的數字:一張不算大的 1000×1000 影像就已經有 一百萬 個像素。把每一個都當成獨立的決定極度浪費,因為相鄰的像素幾乎總是一致——一片藍天是好幾千個幾乎相同的像素,全都注定屬於同一個標籤。超像素(superpixel) 正是利用這一點:它把一小群 相鄰、感知上相似 的像素——顏色與紋理相近——聚成一個小區域,此後我們就以區域、而非原始像素為單位來推理。想像一幅馬賽克:往後退,退到你不再看見個別像素、而開始看見一塊塊磁磚,每塊磁磚大致是一種顏色。那些磁磚就是超像素。這就是 超像素 的概念。

最受歡迎的做法是 SLIC(Simple Linear Iterative Clustering,簡單線性迭代分群)。它的核心就只是 k-means 分群——你在前面軌道學過的「把每個點指派到最近的中心,再移動中心」那個迴圈——只不過是在一個 結合了顏色與位置的空間 裡執行。每個像素不只用它的顏色來描述,還用它的 (x, y) 位置來描述,所以兩個像素只有在 外觀相似彼此靠近 時才算「接近」。位置這一項讓群集 緊湊且大小大致一致(不會有群集橫跨整張影像),顏色這一項則讓邊界能 緊貼真實的物件邊緣。結果是一床整齊的拼布,由一塊塊緊湊、尊重畫面輪廓的色塊組成。

# SLIC superpixels = k-means in (color, position) space.
# Goal: ~K compact patches covering the image.

initialize K cluster centers on a regular grid over the image
for a few iterations:
    for each pixel p:
        # distance mixes COLOR difference and SPATIAL difference
        # 'm' weights how much we care about compactness vs color
        D = color_dist(p, center) + (m / grid_step) * spatial_dist(p, center)
        assign p to the nearest center (only search a local window)
    move each center to the mean (color, x, y) of its pixels

# Output: a label per pixel saying which superpixel it joined.
# Thousands of patches instead of a million pixels.
SLIC:在顏色與位置上做 k-means,且只在局部範圍搜尋以加速。

為什麼要費這個工夫?有兩大回報。第一是 規模:用幾千個超像素取代一百萬個像素,把問題縮小了好幾個數量級,於是較笨重的演算法突然變得負擔得起。第二,超像素是極佳的 建構積木:後續的方法——尤其是像下一節 GrabCut 那樣以圖為基礎的方法——可以把每個超像素當成單一節點,而非每個像素,這正是讓那些圖小到能解的關鍵。不過有個必須尊重的取捨:超像素 太少,單一色塊就會橫跨兩個物件,在你最想保留的邊緣處 滲漏太多,則幾乎沒減少像素數量,把加速的好處丟掉了。選擇數量,是在尊重邊緣與節省運算之間取得平衡。

退得夠遠,個別像素就會融合成沿著物件邊緣排列的超像素磁磚。

左邊是一個像素格,右邊是同一塊區域被切成一塊塊緊湊的超像素,其邊界描著物件的輪廓。

GrabCut:用一點點提示把前景從背景中拉出來

有時候,一點點人類的引導就能解鎖極佳的成果。GrabCut 是經典範例,也是你初嚐 互動式分割(interactive segmentation) 的滋味:使用者只要在主體周圍拖出一個粗略的矩形——「前景就在這個框裡的某處」——或塗上幾筆,演算法就把其餘的事做完,琢磨出一個緊貼主體真實輪廓的乾淨剪影。它是你今天在修圖軟體裡用的「點一下你想要的東西」這類工具的祖先。這就是 GrabCut

底下的引擎是 在圖上做能量最小化(energy minimization on a graph)。把每個像素想成一個節點。在旁邊再加兩個特別的節點:一個 前景端點(terminal) 和一個 背景端點。每個像素都同時連到這兩個端點,也連到它的鄰近像素。每一條連線都有一個 成本。現在,分割影像就等於做一次 切割(cut)——把這張圖切成兩塊,一塊連著前景端點,另一塊連著背景端點——而我們要的是 最便宜的切法,也就是切斷的總成本最小的那一刀。你找到的那一刀 就是 分割:每個像素最後被綁到哪個端點,就成為它的標籤。

在任何公式之前,先把這兩種成本內化成直覺。第一種是 資料成本(data cost):對每個像素問「這個像素的顏色比較像前景,還是比較像背景?」——如果我們指派的標籤和像素本身的顏色不合,就要付出代價。第二種是 平滑成本(smoothness cost):問「相鄰的像素彼此一致嗎?」——每當兩個並排的像素被給了 不同 的標籤,我們就付一筆罰金,這會阻止參差、斑駁的邊界。關鍵在於,當 這兩個鄰居本身顏色就差很多時,這筆罰金會被打折,因為真實的物件邊緣,正是 應該 允許標籤免費改變的地方。把這兩種成本的總和最小化,就會得到最乾淨、最平滑、又仍尊重顏色的分割。

E(L) = \sum_{p} U(L_p) \; + \; \lambda \sum_{(p,q) \in N} V(L_p, L_q)

圖切割的能量:資料項加上加權的平滑項,對所有像素與鄰居對求和。

我們把每個符號拆開。L標籤分配——也就是完整的答案,為每個像素給一個標籤(前景或背景);L_p 是目前指派給像素 p 的標籤。E(L) 是整套標籤分配的總 能量(成本)——越低越好,GrabCut 就是在尋找讓 E 最小的那個 L。第一個總和 ∑_p U(L_p)資料項/一元項(unary term):對每個像素 p 加上 U,也就是依一個學到的 顏色模型 來評斷「給 p 這個標籤 L_p」的成本(GrabCut 對前景顏色擬合一個 高斯混合模型(Gaussian Mixture Model),對背景顏色再擬合另一個——大致是「這是前景的調色盤」)。如果像素 p 的顏色和我們給它的標籤很不搭,U 就很大。第二個總和跑過 (p, q) ∈ N,也就是每一對 相鄰 的像素 p 與 q(N 是相鄰對的集合)。V(L_p, L_q)成對項/平滑項(pairwise term):當兩個鄰居共用同一個標籤時它大致為零,不同時則為正——但正如前述,當 p 與 q 本身顏色就差很多(一條邊緣)時,這個正的罰金會被 打折。最後 λ(lambda)是一個旋鈕,用來 平衡 這兩股力量:λ 大就信任平滑,給出團塊狀、非常乾淨的邊界;λ 小就信任逐像素的顏色,會跟隨細節但可能看起來雜亂。舉個小例子:一個綠色像素深處在框內、四周都是綠色像素,它得到 前景 的 U 很小(顏色和前景調色盤相符),和綠色鄰居之間的 V 也很小(彼此一致)——於是它安穩地加入前景。一個位於框緣的藍色像素,符合背景調色盤、又與框外的藍色像素相鄰,標成背景最便宜。把 E 最小化,就讓每個像素安頓到總帳單最小的配置——也就是主體與背景之間最乾淨的分離。

為什麼傳統方法會撞牆

閾值法、分水嶺、超像素、GrabCut——這些都很聰明、很快,而且至今仍真的有用。但退一步看,會發現它們有個共同點:每一個都是用 低階線索(low-level cues) 來把像素分群——原始顏色、強度、邊緣、空間上的鄰近。它們沒有任何一個 知道車或狗究竟是什麼。GrabCut 並不認得「狗」;它只是注意到某些顏色看起來像你框內的東西、某些看起來像框外。傳統工具箱裡 完全沒有語意(semantics)——沒有對物件的理解,只有像素的統計。這就是 傳統分割 的天花板。

這份缺失的語意,會以脆弱的形式顯現出來。一道橫過馬路的 陰影 會把一個區域硬切成兩半,因為強度變了,即使物件並沒有變。一個有 紋理 的表面——碎石、樹葉、條紋襯衫——會爆出一堆假邊緣,而分水嶺會乖乖地把它們變成數十個碎片。雜亂的背景(clutter) 會搞糊顏色模型。而且幾乎每個傳統方法都需要 逐張影像手動調參:挑這個閾值、放那些標記、畫那個框、微調 λ。只要光線或場景一變,你又得回去擺弄旋鈕。這些方法無法泛化,因為它們從未學過什麼才重要——它們只遵循固定、手寫的規則。

而這,正是本軌道其餘部分的動力引擎。要產生 真正 的標籤——「這些像素是 道路,那些是某一台特定的 」——而且對陰影、紋理與雜亂都穩健,我們就不能再繼續手寫關於顏色的規則。我們需要一個 從資料中學習特徵 的系統:它看過成千上萬的車與道路,並自己發現了標誌每個類別的視覺樣式。這個轉變——從手調線索到學到的 語意 特徵——正是解鎖現代分割的關鍵。在 下一篇指南 裡,我們就要跨出這一步:用 全卷積網路(fully convolutional networks),以及由它衍生的編碼器–解碼器與 U-Net 設計,把一個分類網路改造成逐像素的網路。