有意思的數字盲水印的簡單的實現。


  早期大約是10年前從一本數字圖像處理上看到過數字水印的概念,覺得確實一種很有意思的東西,那個時候主要就是基於LSB的圖像信息的隱藏,這種在空域里的方法有較大的缺陷,魯棒性是比較差的。隨便一個后期的都會造成水印的丟失,因此,雖然是一種盲水印,但是不具有很好的推廣性。

  前段時間一個朋友給了我一段使用Opencv的盲水印代碼,是基於FFT變換的, 抽空看了下,對其中部分的實現過程進行了替換和分解,也實現了一個最簡單的基於頻域的盲水印效果。

  我在尋找相關資料的時候在網絡上看到有幾個這方面的文章和工具,現在分享如下:

  https://blog.csdn.net/chenxiao_ji/article/details/52875199https://blog.csdn.net/chenxiao_ji/article/details/52875199

  https://www.sdbeta.com/wg/2018/0903/225358.html

       https://blog.csdn.net/weiyiweiyiweiyiyi/article/details/82847756

       https://blog.csdn.net/linyacool/article/details/71506638

  好像還有一個寫的比較詳細,而且有工具,在github上也有分享代碼。

  但是似乎這些工具大部分只支持文字水印,而不支持圖像水印,文字我不熟悉,因此我還是用圖像做水印模板,核心的代碼如下所示:

int IM_AddBlindWaterMark(unsigned char *Src, unsigned char *WaterMark, unsigned char *Dest, int Width, int Height, int Stride, int WidthW, int HeightW, int StrideW) { int Channel = Stride / Width, ChannelW = StrideW / WidthW; if ((Src == NULL) || (Dest == NULL))                        return IM_STATUS_NULLREFRENCE; if ((Width <= 0) || (Height <= 0))                            return IM_STATUS_INVALIDPARAMETER; if ((Channel != 1) && (Channel != 3) && (Channel != 4))        return IM_STATUS_INVALIDPARAMETER; if ((ChannelW != 1) && (ChannelW != 3) && (ChannelW != 4))    return IM_STATUS_INVALIDPARAMETER; if ((WidthW >= Width / 4) || (HeightW >= Height / 4))        return IM_STATUS_INVALIDPARAMETER;        // 水印圖不能大於原圖尺寸的一半
            
    int Status = IM_STATUS_OK; int OptimalW = IM_GetOptimalDftSize(Width),    OptimalH = IM_GetOptimalDftSize(Height); int OffsetX = (OptimalW - Width) / 2,        OffsetY = (OptimalH - Height) / 2; int HalfW = OptimalW / 2,                    HalfH = OptimalH / 2; if (Channel == 1) { Complex *Data = (Complex *)malloc(OptimalW * OptimalH * sizeof(Complex)); if ((Data == NULL))    return IM_STATUS_OUTOFMEMORY; for (int Y = 0; Y < Height; Y++)                                // 我們把數據居中布置,邊緣用重復像素的方式
 { unsigned char *LinePS = Src + Y * Stride; Complex *LinePD = Data + (Y + OffsetY) * OptimalW; for (int X = 0; X < OffsetX; X++) { LinePD[X].Real = LinePS[0]; LinePD[X].Imag = 0; } for (int X = OffsetX; X < OffsetX + Width; X++) { LinePD[X].Real = LinePS[X - OffsetX]; LinePD[X].Imag = 0; } for (int X = OffsetX + Width; X < OptimalW; X++) { LinePD[X].Real = LinePS[Width - 1]; LinePD[X].Imag = 0; } } for (int Y = 0; Y < OffsetY; Y++) { memcpy(Data + Y * OptimalW, Data + OffsetY * OptimalW, OptimalW * sizeof(Complex)); } for (int Y = OffsetY + Height; Y < OptimalH; Y++) { memcpy(Data + Y * OptimalW, Data + (OffsetY + Height - 1) * OptimalW, OptimalW * sizeof(Complex)); } IM_FFT2D(Data, Data, OptimalW, OptimalH, false, 0, 0); IM_FFTShift(Data, Data, OptimalW, OptimalH); // 數據偏移到中心

        for (int Y = 0; Y < HeightW; Y++) { Complex *LineLT = Data + (Y + OffsetY + Height / 16) * OptimalW + OffsetX + Width / 16;            // 確保在可見的范圍內添加,左上角和右下角都鏡像添加
            Complex *LineRB = Data + (OffsetY + Height - 1 - Height / 16 - Y) * OptimalW + OffsetX + Width - 1 - Width / 16;    // 再稍微往內部移動一點,可以適當增強抵抗變形的能力,但是越往中心其對最終結果的影響越大。
            unsigned char *LinePS = WaterMark + Y * StrideW; if (ChannelW == 1) { for (int X = 0; X < WidthW; X++) { float Cof = ((LinePS[X] * 4) >> 8) + 1; LineLT[X].Real *= Cof;    LineLT[X].Imag *= Cof; LineRB[-X].Real *= Cof;    LineRB[-X].Imag *= Cof; } } else if (ChannelW == 3) { for (int X = 0; X < WidthW; X++) { float Cof = ((((LinePS[0] + LinePS[1] + LinePS[1] + LinePS[2]) >> 2) * 4) >> 8) + 1; LineLT[X].Real *= Cof;    LineLT[X].Imag *= Cof; LineRB[-X].Real *= Cof;    LineRB[-X].Imag *= Cof; LinePS += 3; } } else if (ChannelW == 4) { for (int X = 0; X < WidthW; X++) { float Cof = ((IM_Div255(((LinePS[0] + LinePS[1] + LinePS[1] + LinePS[2]) >> 2) * LinePS[3]) * 4) >> 8) + 1; LineLT[X].Real *= Cof;    LineLT[X].Imag *= Cof; LineRB[-X].Real *= Cof;    LineRB[-X].Imag *= Cof; LinePS += 4; } } } IM_IFFTShift(Data, Data, OptimalW, OptimalH); IM_FFT2D(Data, Data, OptimalW, OptimalH, true, 0, 0); for (int Y = 0; Y < Height; Y++) { Complex *LinePS = Data + (Y + OffsetY) * OptimalW + OffsetX; unsigned char *LinePD = Dest + Y * Stride; for (int X = 0; X < Width; X++) { LinePD[X] = IM_ClampToByte(LinePS[X].Real); } } if (Data != NULL)        free(Data); return IM_STATUS_OK; } else { } }

  首先,把圖像變換到頻域,這里采用了opencv的有關FFT計算的過程,使用IM_GetOptimalDftSize計算最佳的DFT算法的大小,然后將圖像數據居中分布,周邊的空白像素采用鏡像填充方式填充,虛部數據填0。

        FFT變換完成后,對FFT數據進行移位,把高頻數據放置到圖像的中心,低頻的數據放置到圖像的邊緣。為了將水印的圖像嵌入到目標圖像,我們在適當位置根據水印圖像的強度或內容來修改這些頻域值,為了不影響最終的目標圖像的視覺效果,嵌入的數據放置到邊緣的低頻數據中(靠近邊緣的部位),我這里也沒有放置在最邊緣,而是邊緣靠中的部位。

  常用的水印圖像可能是8位灰度、24位彩色或32位透明圖,因此,我在程序里對不同位數的水印圖都做了處理,如果是32位圖,則把Alpha也考慮進去了,使用的嵌入方式就是最簡單的更具水印圖的顏色強度值將目標圖像的頻域系數放大。這里的放大程度我做了固定的設計,測試效果還比較好,如果過度放大,則最后處理的結果將會嚴重的失真,這就失去了算法本身的意義了。當然還有一種方式就是縮小系數,也可以去嘗試下。

  之后,我們需要將平移后的數據再次進行移位,然后就是進行IFFT計算了,並將計算結果返回到圖像域。

  本例只給出了針對灰度目標圖像的代碼,那么彩色圖像其實是一樣的過程,將他們分解成三個通道單獨處理就OK了。 

  同時,為了保證水印對結果圖不會造成太大的影響,我們程序對水印圖大小做了限制,長和寬都不得大於目標圖像的1/4。

  另外,從嵌入的代碼可以看到,我們希望水印圖像盡量是黑色的背景(8位或24位)或純背景部位是透明的(32位),這樣對目標圖像的影響也比較小。

  我們來做一些測試,以下是一張原圖(原圖縮小顯示了)及兩個水印圖進行測試:

   

  分別查看其結果圖和頻譜圖:

 

 

    可見,添加水印后基本未對原始圖像造成視覺上的損失,在處理后的圖像的頻譜上可以明顯看到添加后的水印的樣式。

  如果對添加水印后的圖像進行一些處理,看看水印是否還能有效保存。

  一、亂七八糟的增強

 

2、有局部裁切的旋轉

  

3、含有模糊性質的算法

  

  可見,這個時候水印信息就基本丟失了,這主要是因為我們的水印信息是加在圖像的低頻的,而模糊會對低頻進行處理,所以就看不到水印了,但是如果是銳化算法就不成問題的。

  因此,這個盲水印的功能還是比較初級的,但是如果在自己的比較重要的圖里隱藏個水印有的時候還是值得的,假如某個壞人是直接使用你的圖而沒有做任何更改呢。

  另外,還有一種基於FFT比較常見的水印技術,需要嵌入水印的圖片以及未嵌入水印的原始圖這樣才可以獲得水印,理論上講這種應該不叫做盲水印了,但是他有個好處就是可以對水印進行加密,這樣別人就比較難以知道你對圖像是否嵌入了水印了。需要做的額外工具就是一定需要保留原始的未加水印的圖像了。

  我將這個 小工具也集成到了我的SSE做的DEMO里了,有需要的朋友可以試下:SSE_Optimization_Demo.rar

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM