把能在瀏覽器標籤頁裡跑的分割做出來
點選即分割與開放詞表文本提示都在客戶端執行,無需 GPU。為此付出了什麼:一份經過驗證的編碼器契約、一個由實測選出的量化方案,以及一個手寫的分詞器。
pip install potato-annotation 之後就應當得到可用的分割能力:不需要 GPU,不需要新的 Python 依賴,標註時也不產生對外網路請求。
最後這條約束不是錦上添花。有若干研究組把 Potato 部署在氣隙環境中,在那裡,一個要在點選時去拉取的模型不是"慢",而是"缺失"。因此 Potato 的兩個影像輔助模型都通過 ONNX Runtime Web 在標註者的瀏覽器中執行:MobileSAM 負責點選即分割,Grounding DINO 負責文本提示。
其中有三件事值得記下來,因為它們的適用範圍超出了這個項目本身。
編碼器契約允許多種看似合理的解讀
SAM 的影像編碼器接收一個經過預處理的張量,而規格說明留下了解釋空間:歸一化方式、縮放約定、填充、通道順序。
我們實現了其中一種解讀,得到了掩碼。它們看起來是對的。相對基準的質心誤差在 70 到 148 畫素之間——這種誤差讀起來像是標註者有點馬虎,而不像是流水線壞了。三種各自看似合理的解讀,都產出了自信、看似合理卻錯誤的掩碼。
而正確的解讀落在 0.1 畫素。
這件事的教訓不是"要更仔細地讀規格"。而是:此處解讀錯誤在沒有基準的情況下是不可見的——掩碼形狀正確、位置也大致正確,下游的每一項檢查都能通過。只有與已知真值做數值比對,才能把這四個候選區分開,而其中只有一個是對的。
因此這份契約是由一項針對真實權重的測試固定下來的,而不是寫在註釋裡。註釋同樣是"真的",卻抓不住迴歸。
量化是一個真實的選擇
把一個 686 MB 的模型量化到能在瀏覽器中執行,這不是可選項。但採用哪種量化,通常是由匯出工具的預設值決定的。
對照全精度的 Grounding DINO 匯出實測:
| 匯出方式 | 相對全精度的框 IoU | 體積 |
|---|---|---|
q4f16 | 0.972 | 151 MB |
int8 | 0.874 | 201 MB |
q4f16 保留了明顯更多的原始幾何資訊,並且還小 50 MB。int8 是慣常選擇,採用它就會用更大的檔案裝上一個更差的模型。
這項測量花了一個下午,否則它會成為永久性的決定,因為一旦框已經出現在螢幕上,沒人會再回頭審視量化方案。
之後又在 COCO 的雙貓照片上做了實測:兩隻貓均被檢出,置信度分別為 0.724 與 0.688,邊界框與 Python 參考實現吻合到小數點後四位。
手寫分詞器
Grounding DINO 需要其文字描述按 BERT 的方式分詞。最直接的做法是引入 transformers.js。
那是為了一個函式而引入約 2 MB 的 JavaScript,而這個頁面已經要載入一個 151 MB 的模型,所以我們直接實現了 WordPiece——大約 200 行——並與 HuggingFace 的 tokenizers 做了逐 token 比對。
省下的位元組其實不是重點。重點是:一個與模型訓練時所用分詞器不一致的分詞器,會產出細微錯誤的檢測結果,看上去像是閾值沒調好。你會先花一整天去調 box_threshold,才可能懷疑到分詞器頭上。有了 token 級別的等價性測試,這種失效模式就成了不可能,而不只是不太可能。
瀏覽器不再是正確答案的地方
影片掩碼傳播執行在服務端,這是一個有意為之的例外,而不是不一致。
它的成本結構不同。點選即分割每次點選只需一次廉價的解碼器前向,而編碼是每張圖片算一次並快取的。傳播則是每一幀都要跑一次完整的模型前向,模型有 181 MB、分佈在五張計算圖中,而影片本來就在服務端。在瀏覽器標籤頁中跑上一百幀,意味著頁面要凍結好幾分鐘。
我們保留了一條更輕量的瀏覽器內延續路徑,它逐幀重新提示,並且我們很小心地不把兩者說成同一種能力。"SAM 2 傳播在你的瀏覽器中執行"是一句更好聽的話,也是一句假話。
如果有人要做同樣的事,我們會說什麼
- 先拿到基準,再看輸出。 看似合理卻錯誤的輸出才是真正的失效模式,而且它能躲過評審。
- 實測量化方案。 預設值是別人在另一套約束下替你做的選擇。
- 用數值方式對照參考實現測試預處理。 肉眼一掃是分不出 0.1 畫素與 148 畫素的誤差的。
- 把編碼器與解碼器分開,並快取開銷大的那一半。這一分離正是點選之所以感覺像互動的全部原因。