隱形浮水印實驗室

產生一把金鑰,蓋在我們的作品上,肉眼看不出任何差別。然後驗給自己看、試著用別人的金鑰偽造、把圖摧殘一輪看它什麼時候會死。 每一步的中間產物都畫出來,沒有黑箱。所有運算都在瀏覽器裡跑,圖片和金鑰不會離開這台電腦。

這一頁我們會一起走七步

七步分兩種:三步會產出東西:金鑰、蓋好的作品、驗證結果;另外四步是實驗,不產出成品,但那才是我們把演算法拆開的地方。先講清楚哪一步是哪一種,才知道自己現在在幹嘛。

要做出一張有浮水印的作品:走步驟 1 和 3 就好。
步驟 1 產生一把金鑰(順便下載鑰圖,那是我們唯一的憑據,弄丟就證明不了)→ 步驟 3 把作品拖進去、按「蓋上去」、下載蓋好的 PNG。成品到這裡就完成了,中間的步驟 2 不一定要做,它只是把過程畫出來而已。

要驗證一張圖是不是我們的:直接跳到步驟 7。
步驟 7 把那張圖和我們的鑰圖丟進去就好。不需要走前面任何一步,圖可以是別人傳來的,金鑰可以是半年前存的那張鑰圖。

其餘四步(2、4、5、6)不做也能用,但做了我們就知道它為什麼有效、什麼時候會失效。那才是這一頁存在的理由,不然這就只是一個我們不知道能不能信的按鈕。

  1. 實作先有一把金鑰:產生金鑰、下載鑰圖存好
  2. 實驗金鑰畫出來是什麼樣的「場」:看那段字串怎麼變成一張鋪滿整張圖的圖樣
  3. 實作蓋到作品上:蓋完下載,有浮水印的作品就做好了
  4. 實驗驗給自己看,然後試著偽造:同時用我們的金鑰和別人的金鑰各驗一次,看差在哪
  5. 實驗還原:證明它是可逆的:把蓋上去的東西精確減回去
  6. 實驗自己把它弄壞:裁切、縮小、重新壓縮,看它什麼時候會死
  7. 實作拿一張圖來驗真正要驗證的時候,就只用這一步

所有運算都在瀏覽器裡跑,圖片和金鑰不會離開這台電腦。

這頁跟「LINE 對話製造機」的關係

這個實驗室是從LINE 對話製造機拆出來的。那個工具會在匯出的每張圖和每支影片上烙一個隱形標記, 這樣別人拿它做的假截圖去唬人時,任何人都能到驗證頁驗出「這是合成的」。 標記做完之後才發現:會用的人很多,看得懂它為什麼有效的人幾乎沒有。所以把演算法拆開、每一步都畫出來,就是這一頁。

兩邊用的是同一套場紋,但回答的是完全不同的問題。

一樣的地方
同一種圖樣:每 16 個像素一個節點的 +1/−1,中間平滑過渡,只在藍色通道加減 2/255
同一種驗法:把圖切成格子、算出每格的「偏藍程度」、跟圖樣逐格相乘再全部加起來
同樣把圖樣平鋪整張圖,再暴力搜尋位移,所以都抗裁切、抗重新壓縮
都是純瀏覽器、離線、開源,圖片不會上傳
不一樣的地方LINE 對話製造機這一頁
回答的問題這張圖是不是這個工具做的這張圖有沒有蓋過我的金鑰
圖樣哪裡來一個固定的公開函式,8×8 個節點我們的金鑰算出來的,16×16 個節點
誰能驗任何人:這正是目的只有握有金鑰的人
怎麼判定只看一個數字 z
「訊號有多強」
z 和峰值旁瓣比,兩個都要過
再加問「那個最高分有沒有明顯贏過其他位置」,步驟 4 會用數字算一遍
用途防詐騙:昭告「這是合成的」 歸屬線索:說明「這張圖蓋過我的金鑰」
這不等於「這是我做的」:金鑰外流的話別人也蓋得出來,見誠實的界限

「為什麼那邊只看一個數字就夠,這邊卻要兩個?」:這是最值得問的一題,而且我原本的答案是錯的。

我本來以為:製造機只有一個公開圖樣,沒有「別人的金鑰」可以拿來碰撞,所以那個破口不存在。去量了才發現不是。

2026-08-14 實測(用製造機的偵測器驗 60 張圖一組,test/cross-lcm.mjs 可以自己重跑):

拿去驗的圖z 中位z 最高判定(門檻 7)
從沒經過任何工具的乾淨圖0.91.50/60 誤判
製造機自己蓋的(對照組)32.332.660/60 正確檢出
這一頁用隨機金鑰蓋的6.3–6.89.4–9.616~25 張被誤判成「製造機做的」

最後一列是區間,因為每跑一輪都用新的隨機金鑰,數字會浮動;跑過的幾輪落在 16/60 到 25/60 之間,也就是四分之一到三分之一。

圖樣完全不同、節點數也不同,為什麼會碰撞?因為兩張隨機的 ±1 圖樣,總會有一部分格子剛好同號。位移搜尋會試遍所有對齊方式,挑出最巧的那一種:巧到某個程度,加起來的分數就夠大了。 而這正是 z 這個數字分辨不出來的地方:蓋得越用力,連碰撞出來的最高分也跟著變大。

碰撞就是「兩個本來不相干的東西,算出來的結果卻剛好一樣」。這裡指的是不同的金鑰在某個對齊位置上剛好有一大半格子同號。

峰值旁瓣比擋得住它:同一批圖,製造機自己蓋的最低是 7.1,這一頁蓋的最高只有 4.4,中間隔得開。差別不在分數多高,在那個最高分有沒有明顯贏過其他位置。

所以正確的說法是:製造機還沒有人去跟它碰撞過,不能叫免疫。而這一頁現在正好會生產這種圖。它的標記本來就只是「提高濫用成本」而不是防偽,所以這個結果不影響它的用途。只是「沒看過誤判」和「不會誤判」是兩回事:我原本差一點就這樣寫下去了。

「為什麼那邊 8×8 個節點就夠,這邊要用 16×16?」

先把單位講清楚:一個節點涵蓋 16×16 個像素,所以 8×8 個節點是一塊 128×128 像素16×16 個節點是一塊 256×256 像素。這一塊會不斷平鋪到整張圖上。

直覺會擔心:一塊變大,是不是就需要更大的殘存區域才驗得出來?量出來不是。test/node-count.mjs,每組 40 張)

圖片尺寸節點數正向 z碰撞 z(中位/最高)裁到 75%
640×4808×832.312.4 / 17.122.5
16×1632.38.1 / 9.922.6
320×2408×815.26.1 / 7.79.7
16×1615.24.1 / 5.29.7

正向分數兩邊完全一樣,抗裁切也一樣,只有碰撞分數降下來了。

為什麼正向沒有變差?因為偵測時是環狀折疊的:圖上每一個 16 像素格子,都會被折進 N×N 格裡的某一格去累加。 所以「一塊 256×256」不等於「要有 256×256 才驗得出來」:決定強度的是整張圖總共有多少格子,跟一塊多大無關。

為什麼碰撞變難?因為別人亂猜的那把金鑰,要在256 格上都碰巧同號才碰撞得到;8×8 只要碰對 64 格。 格子越多,要碰巧的地方越多,運氣就得越好。

所以換成 16×16 是純賺的。抗碰撞明顯變好,而換之前擔心的兩件事:訊號會不會變弱、會不會更不耐裁切:量完發現都沒有發生。

對製造機來說 8×8 就夠了:它的圖樣本來就是公開的,防碰撞從來不在它的目標裡,任何人都能蓋才是它要的。

兩邊都在做的事情是一樣的,只是問題不同,所以在這頁玩懂的每一件事,回頭看那個工具的驗證頁就全部通了。

先搞清楚我們要的是哪一種浮水印

題目需求代表
洩漏溯源圖裡要載得動 ID,才知道是誰外流的invisible-watermark(dwtDct)、RivaGAN
歸屬線索只要回答「這張圖有沒有蓋過我的金鑰」,一個位元就夠本頁

因為只需要一個位元,我們可以把全部的工程餘裕拿去換抗破壞性:平鋪、暴力搜尋、只看整體相關性。溯源型要精準到每個位元的位置,所以對裁切、縮放特別脆弱。看完可能會發現該用別的工具,那也是這頁的目的。

1先有一把金鑰

金鑰是一段隨機字串,不是圖。它決定浮水印長什麼樣子,而且只有握有它的人能蓋、能驗

這一步一定要做完的事:按「產生一把新金鑰」,然後把鑰圖下載存好。那張鑰圖是我們唯一的憑據:我們沒有伺服器、沒有備份、沒有「忘記密碼」。

或貼上已經有的:

2金鑰畫出來是什麼樣的「場」

把金鑰餵給一個亂數產生器,得到 16×16 個 +1−1。這就是我們的指紋。

這裡的亂數產生器是確定性的:看起來很亂,但同樣的金鑰永遠得到同樣的結果。不然今天蓋的圖,明天就驗不出來了。 代價是換一個字元就完全不同,所以金鑰要一個字不差。

第五次做壞的地方,是使用者問出來的。問題是:「金鑰只有 20 個字元,指紋有 16×16 格,會不會很容易重複?」

光看大小不會:20 個字元的 Crockford base32 是 100 bit(2¹⁰⁰ 種),指紋是 256 格 ±1(2²⁵⁶ 種),指紋空間大得多。 但中間那個亂數產生器把它掐掉了。舊寫法是先算出一個 32 bit 的雜湊,再從它連續吐四個數字去啟動亂數產生器: 看起來有 128 bit,其實全部由那 32 bit 決定。所以指紋最多只有 2³² 種。

實測 30 萬把不同的金鑰,撞出 9~12 組完全一樣的指紋(2³² 的理論期望值是 10.5 組)。 修法是改成跑四次獨立的雜湊、每次都吃過完整的金鑰;同樣 30 萬把,碰撞 0 組
新產生的金鑰開頭是 IWL2-舊的 IWL1- 走原本的路徑,以前蓋過的圖照樣驗得出來: 修一個安全問題卻讓別人的舊作品驗不出來,那是另一種做壞。出處:test/keyspace.mjs

為什麼是隨機的 ±1,而不是我們自己的 logo?這頁的第一版真的是讓人上傳 logo 當金鑰,後來量出兩個補不掉的問題:

隨機 ±1 沒有這兩個問題:相鄰兩格互不相干,而且每一格都用滿振幅。這也是 1997 年那篇奠基論文用偽隨機圖樣的原因。

不過第二點沒有我原本寫的那麼嚴重。後來在同一張圖上實測,把 logo 的指紋「拉開」(見附錄)之後表現追得上來,所以真正否決 logo 的是第一點。附錄可以自己拖一張 logo 進去跑

3蓋到作品上

這一步做完,有浮水印的作品就完成了。把作品拖進來、按「蓋上去」、按「下載蓋好的 PNG」:那張下載下來的圖就是我們要拿去用的成品。 後面的步驟 4、5、6 都是拿這張圖做實驗,不影響成品。

點這裡或拖一張作品進來
沒有現成的?用內建示範圖

只動藍色通道,每個像素最多加減 2。螢幕上每個像素的紅綠藍各是 0 到 255 的整數,所以「加減 2」是全部範圍的 0.8%。 而且人眼分辨藍色細節的能力最差:同樣的改動量藏在藍色裡最不容易被看到。

接近全白、接近全黑的地方只加減 1。那些地方最平、最容易露餡。

取整數的時候要「抖」一下。算出來的值有小數,例如 1.4 和 1.6,直接四捨五入會變成 1 和 2: 於是一整片平坦的底色上,會出現一圈一圈像等高線的痕跡(進位的地方連成一條線)。 做法是給每個像素一個固定的小偏移量,4×4 一組循環,讓進位的位置散開成細碎的點,人眼看到的是均勻的一片,不是線。 這叫有序抖動(ordered dithering),是報紙用網點印照片、用了一百年的老技術。這是我們做壞的第三版才修好的。

4驗給自己看,然後試著偽造

驗證會給我們兩個數字。這一整段用國中數學就能算完,先把它算一遍,等一下看到那兩個數字才有意義。

一、先把整張圖算成一堆數字

把圖切成 16×16 像素的小格子,每一格算出一個數字,代表這一格「偏藍多少」。算法是:

偏藍程度 = 藍 - (紅 + 綠) ÷ 2

為什麼要減紅綠?因為一張圖有亮有暗,亮的地方紅綠藍三個值都大。減掉紅綠的平均,等於把「這裡整體有多亮」扣掉,只留下「這裡比周圍偏藍多少」:那才是我們偷偷加上去的東西。

二、把「畫面內容」扣掉

問題來了:藍天本來就偏藍,這樣算出來的數字裡,絕大部分是畫面自己的顏色,不是我們的浮水印。怎麼分開?靠變化的快慢

所以把每一格減掉上下左右四個鄰居的平均。慢慢變化的畫面內容被扣得差不多;一格一格跳的圖樣不但留下來,還變得更強,因為鄰居常常是反號的,減掉一個負數等於加上一個正數

這一步的專業名稱叫高通濾波(high-pass filter),意思就是「只放行變化快的部分」。名字聽起來很硬,做的事就是上面那個減法。

三、對答案:相乘再相加

假設整張圖只有四格(真的圖有好幾千格,道理一樣)。扣掉畫面之後量到的數字,跟我們的圖樣擺在一起:

圖上量到的+3−2+4−1
我們的圖樣+1−1+1−1
逐格相乘3241加起來 = 10

全部變成正的。因為圖樣說「這格該是正的」,圖上量到的就真的是正的;圖樣說負的,量到的也是負的:負負得正。

換一把不對的金鑰,圖樣就變成別的樣子:

圖上量到的+3−2+4−1
別人的圖樣+1+1−1−1
逐格相乘3−2−41加起來 = −2

有正有負,互相抵消。對得上就一路累積,對不上就抵消掉:這就是整套演算法的全部核心。格子越多,抵消得越乾淨。

這裡刻意不用相減去比對。相減要成立,得先假設圖上只有我們的圖樣、沒有別的東西;真實的圖有內容也有壓縮雜訊,相減永遠不會是零。乘法決定「這一格算不算數」,加法讓雜訊自己抵消。

四、10 到底算大還是小?:這就是 z

光看 10 沒有意義,要有比較的基準。做法是:把圖樣滑到每一個可能的位置各算一次(16×16 個節點,共 256 種對齊方式)。這 256 個分數裡,絕大多數都是沒對齊的,它們就是「隨便猜會得到多少」的樣本

假設這些分數大多落在 −3 到 +3 之間,那「一般會晃多大」差不多是 1.5。這個「一般會晃多大」,數學上叫標準差

z = 我們這次的分數 ÷ 一般會晃多大 = 10 ÷ 1.5 ≒ 6.7

z 的全名是 z 分數(z-score),中文叫標準分數,意思就是「比一般的晃動大幾倍」。門檻設 ,是在說:要比一般的晃動大 6 倍以上,我才相信這不是碰巧。

五、為什麼還要第二個數字:峰值旁瓣比

z 有一個它自己分辨不出來的狀況:蓋得越用力,連碰撞出來的最高分也會跟著變大。別人拿一把完全不對的金鑰,在 256 個位置裡總會挑到最巧的那一個,剛好有一大半格子同號:分數就上去了。

所以要再問一句:那個最高分,有沒有明顯贏過其他 255 個位置?

峰值旁瓣比 = (最高分 - 其他位置的平均) ÷ 其他位置晃動的幅度

全名是峰值旁瓣比(peak-to-sidelobe ratio)。「峰值」是最高的那一根,「旁瓣」是旁邊那些沒對齊的。這是雷達判斷「螢幕上那個亮點到底是飛機還是雜訊」用的老方法,不是這頁發明的。

兩個數字問的事情不一樣:z 問「這個分數大不大」,峰值旁瓣比問「它有沒有明顯贏過別人」。蓋得用力會讓所有分數一起變大,但不會讓「贏多少」變大,所以第二個數字騙不了。

下面驗完會用兩張熱圖把這件事畫出來:那是這一節最重要的一張圖。

一次跑兩把金鑰:我們的,和一把隨機產生的「別人的」。

兩種「誤判」的破口不一樣,而且剛好相反:這就是為什麼上面那兩個數字缺一不可:

只看其中一個,都會被另一種騙過去。兩個都要求,就都擋掉了。這是實測逼出來的:一開始只用 z、門檻照舊版設 7,第一次跑負控制就抓到誤判。 上面那張跟 LINE 對話製造機的對照表就是這件事的實例:那邊只看 z,結果被這頁蓋出來的圖碰撞成功了三分之一。

附錄:第一版是拿 logo 當金鑰的。為什麼放棄:可以自己動手驗

這頁的第一版沒有金鑰字串,是上傳自己的 logo,把它壓成 16×16 的指紋。聽起來很美:我們的浮水印就是我們的標誌。當時列了兩個放棄的理由,後來重量一遍,發現其中一個是我講錯的。兩個都放在這裡。

理由一,定義上的,無解。logo 是我們到處在用的東西:網站上、名片上、每張作品的角落。公開的東西不能當金鑰。任何人下載我們的 logo,就能在別人的作品上蓋出我們的浮水印,宣稱那是我們做的。這一條改演算法救不回來。

理由二,我原本以為也是致命的,量完發現不是。偵測端第一件事就是高通(減掉四個鄰居的平均),因為畫面內容是低頻的、指紋是格子間交替的高頻。而 logo 也是低頻的:大色塊、平滑漸層,相鄰的格子長得差不多,高通一過能量掉一大半。但這件事有解:白化(把指紋每格振幅重新正規化到偵測端看得見的頻段)。下面這張表就是白化前後的差距。

或拖自己的 logo 進來,看它掉多少:
點這裡或拖一張 logo 進來

所以第二個理由被推翻了,logo 當金鑰在效能上其實堪用。它還是被放棄,因為第一個理由本身就足夠:一個公開的東西不管訊號多強,都不是金鑰。

把這段留在頁面上,是因為「為什麼不行」比「怎麼做」難學,而且連「為什麼不行」都會講錯。這個 repo 一共做壞四次:8×8 太好偽造、直接四捨五入在平坦底色留下等高線、還原用「蓋相反的場」只回得去 64.7%、以及這一次把可解的問題當成致命的。每一次都是量出來才知道的,包括這一次的更正。

5還原:證明它是可逆的

有金鑰的話,可以把場減回去。這是理解整件事最快的一條路:浮水印就是一個加上去的訊號,不是什麼玄學。

用的是跟步驟 3 同一種「差異放大 20 倍」的看法。蓋好的那張看得到指紋鋪滿;還原後那張應該什麼都沒有

為什麼一個「移除浮水印」的功能會放在這裡?因為它證明了兩件我們必須誠實說的事:這個標記是可逆的加法訊號;而且沒有金鑰就精確減不掉:別人只能用模糊、旋轉那種粗暴手段,那本來就擋不住。

這頁從頭到尾的立場是:開源、可拔除、防的是順手盜用而不是防偽。與其假裝拔不掉,不如把怎麼拔、以及拔了會怎樣,一次講完。

6自己把它弄壞

用的就是我們上面剛蓋好的那張圖,不是另外準備的範例。把它摧殘一輪再各驗一次。這張表同時證明「它真的活得下來」和「它有明確的極限」,比任何文案都可信。

表裡有兩把梯子,對照著看。純裁切 90→75→60→50→30,裁完再縮一半 90→75→60→50。 純裁切的斷點在 75% 與 60% 之間;裁完再縮一半的斷點在 60% 與 50% 之間: 也就是說多做一道縮小,反而更耐用。縮小再放大等於過了一次低通濾波,把畫面的高頻雜訊壓掉、留下平滑的場紋。 「有效格數」那一欄是還原之後的格數,縮放那幾列會補回來,裁切那幾列補不回來。

「預期」是 2026-08-13 用 test/survive-matrix.mjs 在四種代表性圖片上量出來的(平滑插畫、實拍照片、高細節紋理、大面積淺色),三張全活才寫「應該活」。 那四張都是 900×620。裁切那幾列只對這個尺寸成立:同一種內容放大到 3600×2480,裁到剩 10% 還活著,見誠實的界限不是保證:存活很吃圖片本身。跟預期不符的那幾行會標成黃底,那才是新資訊。

這一項會跑一段時間:電腦上大約 20 秒,手機上大約一到兩分鐘(21 種摧殘方式,每一種都要重新搜尋位移和尺度)。跑的時候旁邊會顯示進度,可以先往下看。

7拿一張圖來驗(實際會用到的就是這一步)

這是實際要驗證時唯一會用到的一步,前面那些都不必走。情境是這樣:作品流出去了,我們拿回一張圖、配上當初存的鑰圖,問它「這是不是我的」。 兩邊都不需要是這個分頁產生的,圖可以是別人傳來的,金鑰可以是半年前下載的那張鑰圖。

要驗的圖
點這裡或拖進來
我們的鑰圖
拖鑰圖進來,會自動讀出金鑰

還原只在「原尺寸、沒被轉檔」的圖上有效。被縮過、裁過、存成 JPEG 的圖,格線已經對不回去,減下去只會多加一層雜訊。要拿回乾淨的原圖,用當初自己下載的那張 PNG。

「未檢出」不等於「不是我們的」。可能是被裁得太兇、被旋轉過、或那張圖本身雜訊太高蓋不上去。這個工具只能指出一個方向,不能當證據。

反過來「檢出」也只證明「這張圖蓋過這把金鑰」,不證明是誰蓋的:金鑰外流的話,別人也能蓋。

誠實的界限

開源,所以拔得掉。防的是順手盜用,不是防偽。原始碼就在下面的連結裡。

金鑰外流就全毀。拿到我們金鑰的人可以在任何圖上蓋出我們的浮水印,宣稱那是我們做的。這是這套機制唯一的信任根。

金鑰弄丟就證明不了。我們沒有伺服器、沒有備份、沒有「忘記密碼」。所以鑰圖裡存了三份。

拿到一張你蓋過的圖,就能把場紋估出來:不需要金鑰,也不需要很多張。 這一條原本寫的是「蒐集夠多張取平均才可能反推」,2026-08-14 量完發現講輕了:一張就夠。 因為場紋是平鋪的,攻擊者知道演算法(開源),就能做跟偵測器一樣的事:高通、把每個格子按 16 為週期折疊累加、取正負號。 畫面內容被高通殺掉又被折疊平均掉,場紋每一塊都同號,累加起來就浮出來了。

他估出來之後能做什麼實測
估中多少70% 的節點(瞎猜是 50%)
拿去驗另一張同金鑰的圖z 30–58、峰值旁瓣比 約 9,檢出:等於他手上有一把能用的金鑰
拿去把浮水印減掉z 從 44 掉到 25,原主人仍然驗得出來:抹不乾淨

這是平鋪式浮水印的結構性弱點,不是這份實作的疏忽:文獻上早就記載自同步(平鋪)浮水印容易被估計攻擊, 而平鋪正是它能抗裁切的原因。兩者是同一件事的兩面。出處與可重跑的腳本:test/estimate-attack.mjs。 「一件作品一把金鑰」擋不住這一條(單張就夠),它只擋得住跨作品的關聯。

高細節、高雜訊的圖蓋不上去。2026-08-13 量到的:一張密集紋理的測試圖連原圖都驗不出來(z=6.1)。±2 的訊號從一開始就蓋不過那張圖本身的雜訊底,跟有沒有被摧殘無關。平滑的插畫、實拍照片、大面積淺色的商品圖都沒問題。

裁切比想像中脆弱。同一批測試裡,裁掉一半只有大面積淺色那張活得下來,平滑漸層與實拍照片都死了。留得住的關鍵是剩餘格數加上雜訊底夠低。

旋轉會殺死它。我們沒有搜尋角度,這是明確的已知限制。

「能裁掉多少」沒辦法用一個百分比講。下面存活測試那張表寫的斷點(留下 75% 到 60% 之間)是用 900×620 量出來的: 那是那張圖的斷點,不是這套演算法的。同一種內容放大之後,能裁掉的比例差非常多:

同一種內容(平滑漸層)總格數裁到剩多少還驗得出來剩下的格子
3600×248034875剩 10% 還活著330
1800×12408624剩 22%408
900×6202128剩 75%1218
600×414925幾乎裁不得925

但它也不是單純的「剩下幾格」:斷點的格數從 176 格到 8624 格都有。真正決定的是 剩下的格子數 × 每一格的訊噪比:裁切減少前者,圖片本身的細節決定後者。 大圖佔兩個便宜:格子多,而且同樣的內容放大之後,每一格裡面更平滑。

高細節那張是極端例子,也剛好解釋了上面那條「蓋不上去」:900×620 的高細節圖連原圖都驗不出來(z=4.7), 但同一份內容放大到 3600×2480 就驗得出來,還能裁到只剩一半。所以這種圖不是不能用,要夠大才行。 出處:test/crop-vs-size.mjs

縮放幾乎不影響(縮到 25% 照樣驗得出來),而且反直覺的是,裁完再縮一半反而比只裁還耐用: 裁到 60% 單獨會死,裁到 60% 再縮一半卻活。因為縮小再放大等於過了一次低通濾波:場紋是 16 像素節點的平滑訊號, 重新取樣殺不掉;畫面內容的高頻雜訊卻被平均掉了,每一格的訊噪比反而變好。跟上面那條是同一個道理的兩種走法。

未檢出不等於沒被盜。只代表這張圖不是、或已經無法辨識為用這把金鑰蓋的。

站在誰的肩膀上

這裡沒有任何一塊是新的。把出處列出來,是為了讓你可以直接去讀原文:這頁只是把這些東西接起來、做成看得見、可以動手跑的樣子。

這頁用的出自哪裡
把振幅很低的偽隨機圖樣鋪滿整張圖,偵測時用相關性比對 Cox、Kilian、Leighton、Shamoon(1997),Secure Spread Spectrum Watermarking for Multimedia, IEEE Transactions on Image Processing 6(12), 1673–1687。
差別:他們埋在 DCT 係數,而且論文裡抗裁切、縮放、旋轉那部分需要拿原圖來對位;這頁埋在空間域的藍色通道,驗證時不需要原圖。
只改藍色通道,振幅跟著亮度調整 Kutter、Jordan、Bossen(1997),Digital signature of color images using amplitude modulation,SPIE 3022。
這是跟這頁最接近的前人工作:一樣是空間域、一樣只動藍色通道、一樣讓振幅跟著亮度走、一樣用祕密金鑰決定圖樣。
「逐格相乘再全部加起來」的偵測方式 訊號偵測理論裡的匹配濾波(matched filter),教科書內容
用峰值旁瓣比當第二道判準 雷達與相關濾波器的標準做法。我們用的公式(峰值 − 旁瓣平均)÷ 旁瓣標準差, 跟 MOSSE 追蹤器(Bolme 等人,2010)判斷「追丟了沒」用的是同一條
平鋪 + 暴力搜尋位移來對齊 對抗裁切的標準做法。代價也是文獻上早就知道的:平鋪的週期結構會被自相關看出來,所以可以被估計攻擊,就是誠實的界限裡量到的那一條
取整數之前先抖動 有序抖動(ordered dithering),印刷業用網點印照片用了幾十年

那這一頁有什麼是自己的?

演算法上沒有。如果問題是「有沒有人這樣做過」:有,而且是 1997 年。上面前兩篇加起來,幾乎就是這頁在做的事。 說這是獨創、或說「沒有人這樣做」,都不成立。

自己的部分是工程取捨和量測:±2 的振幅、16 像素一個節點、近白近黑減半、抖動、雙門檻判定、 把位移搜尋折疊成環狀互相關(數值一位不差、快 40 倍),以及那些做壞的版本被淘汰的過程。 這頁上每一個數字都附了可以自己重跑的腳本,沒有一個是從論文抄來的,包括那些讓這套做法看起來很弱的(裁切比想像中脆弱、單張圖就能估出場紋、舊版的指紋空間只有 2³²)。

專案做法跟這裡的差別
invisible-watermarkPython,dwtDct/dwtDctSvd/RivaGAN載得動 ID,可溯源;抗裁切與縮放較弱
hiddenmark瀏覽器端,傅立葉頻域環狀調變方向最接近,頻域天生較抗縮放旋轉,實作複雜
watermark-js-plus瀏覽器端,可見+盲浮水印功能最全,目標是保護網頁與文件