導(dǎo)讀Adobe Photoshop,簡(jiǎn)稱(chēng)“PS”,是由Adobe Systems開(kāi)發(fā)和發(fā)行的圖像處理軟件。Photoshop主要處理以像素所構(gòu)成的數(shù)字圖像。使用其眾多的編修與繪圖工具,可以有效地進(jìn)行圖片... Adobe Photoshop,簡(jiǎn)稱(chēng)“PS”,是由Adobe Systems開(kāi)發(fā)和發(fā)行的圖像處理軟件。Photoshop主要處理以像素所構(gòu)成的數(shù)字圖像。使用其眾多的編修與繪圖工具,可以有效地進(jìn)行圖片編輯工作。ps有很多功能,在圖像、圖形、文字、視頻、出版等各方面都有涉及。 之所以不寫(xiě)系列文章一、系列文章二這樣的標(biāo)題,是因?yàn)槲也恢牢夷軋?jiān)持多久。我知道我對(duì)事情的表達(dá)能力和語(yǔ)言的豐富性方面的天賦不高。而一段代碼需要我去用心的把他從基本原理--》初步實(shí)現(xiàn)--》優(yōu)化速度 等過(guò)程用文字的方式表述清楚,恐怕不是一件很容易的事情。 我所掌握的一些Photoshop中的算法,不能說(shuō)百分之一百就是正確的,但是從執(zhí)行的效果中,大的方向肯定是沒(méi)有問(wèn)題的。 目前,從別人的文章、開(kāi)源的代碼以及自己的思考中我掌握的PS的算法可能有近100個(gè)吧。如果時(shí)間容許、自身的耐心容許,我會(huì)將這些東西慢慢的整理開(kāi)來(lái),雖然在很多人看來(lái),這些算法并不具有什么研究的價(jià)值了,畢竟人家都已經(jīng)商業(yè)化了。說(shuō)的也有道理,我姑且把他作為自我欣賞和自我滿(mǎn)足的一種方式吧。 今天,我們講講查找邊緣算法。可能我說(shuō)了原理,很多人就不會(huì)看下去了,可有幾人層仔細(xì)的研究過(guò)呢。 先貼個(gè)效果圖吧: 原理:常見(jiàn)的Sobel邊緣算子的結(jié)果進(jìn)行反色即可。 為了能吸引你繼續(xù)看下去,我先給出我的代碼的執(zhí)行速度: 針對(duì)3000*4000*3的數(shù)碼圖片,處理時(shí)間300ms。 何為Sobel,從百度抄幾張圖過(guò)來(lái)了并修改地址后: 對(duì)上面兩個(gè)式子不做過(guò)多解釋?zhuān)阒恍枰榔渲蠥為輸入圖像,把G作為A的輸出圖像就可以了,最后還要做一步: G=255-G,就是查找邊緣算法。 查找邊緣類(lèi)算法都有個(gè)問(wèn)題,對(duì)圖像物理邊緣處的像素如何處理,在平日的處理代碼中,很多人就是忽略四個(gè)邊緣的像素,作為專(zhuān)業(yè)的圖像處理軟件,這可是違反最基本的原則的。對(duì)邊緣進(jìn)行的單獨(dú)的代碼處理,又會(huì)給編碼帶來(lái)冗余和繁瑣的問(wèn)題。解決問(wèn)題的最簡(jiǎn)單又高效的方式就是采用哨兵邊界。 寫(xiě)多了特效類(lèi)算法的都應(yīng)該知道,除了那種對(duì)單個(gè)像素進(jìn)行處理的算法不需要對(duì)原始圖像做個(gè)備份(不一定去全局備份),那些需要領(lǐng)域信息的算法由于算法的前一步修改了一個(gè)像素,而算法的當(dāng)前步需要未修改的像素值,因此,一般這種算法都會(huì)在開(kāi)始前對(duì)原始圖像做個(gè)克隆,在計(jì)算時(shí),需要的領(lǐng)域信息從克隆的數(shù)據(jù)中讀取。如果這個(gè)克隆的過(guò)程不是完完全全的克隆,而是擴(kuò)展適當(dāng)邊界后再克隆,就有可能解決上述的邊界處理問(wèn)題。 比如對(duì)下面的一個(gè)圖,19×14像素大小,我們的備份圖為上下左右各擴(kuò)展一個(gè)像素的大小,并用邊緣的值填充,變?yōu)?1*16大小: 這樣,在計(jì)算原圖的3*3領(lǐng)域像素時(shí),從擴(kuò)展后的克隆圖對(duì)應(yīng)點(diǎn)取樣,就不會(huì)出現(xiàn)不在圖像范圍內(nèi)的問(wèn)題了,編碼中即可以少很多判斷,可讀性也加強(qiáng)了。 在計(jì)算速度方面,注意到上面的計(jì)算式G中有個(gè)開(kāi)方運(yùn)算,這是個(gè)耗時(shí)的過(guò)程,由于圖像數(shù)據(jù)的特殊性,都必須是整數(shù),可以采用查找表的方式優(yōu)化速度,這就需要考慮表的建立。 針對(duì)本文的具體問(wèn)題,我們分兩步討論,第一:針對(duì)根號(hào)下的所有可能情況建立查找表。看看GX和GY的計(jì)算公式,考慮下兩者的平方和的最大值是多少,可能要考慮一會(huì)吧。第二:就是只建立0^2到255^2范圍內(nèi)的查找表,然后確保根號(hào)下的數(shù)字不大于255^2。為什么可以這樣做,就是因?yàn)閳D像數(shù)據(jù)的最大值就是255,如果根號(hào)下的數(shù)字大于255^2,在求出開(kāi)方值后,還是需要規(guī)整為255的。因此,本算法中應(yīng)該取后者。 private void CmdFindEdgesArray_Click(object sender, EventArgs e) { int X, Y; int Width, Height, Stride, StrideC, HeightC; int Speed, SpeedOne, SpeedTwo, SpeedThree; int BlueOne, BlueTwo, GreenOne, GreenTwo, RedOne, RedTwo; int PowerRed, PowerGreen, PowerBlue; Bitmap Bmp = (Bitmap)Pic.Image; if (Bmp.PixelFormat != PixelFormat.Format24bppRgb) throw new Exception("不支持的圖像格式."); byte[] SqrValue = new byte[65026]; for (Y = 0; Y < 65026; Y++) SqrValue[Y] = (byte)(255 - (int)Math.Sqrt(Y)); // 計(jì)算查找表,注意已經(jīng)砸查找表里進(jìn)行了反色 Width = Bmp.Width; Height = Bmp.Height; Stride = (int)((Bmp.Width * 3 + 3) & 0XFFFFFFFC); StrideC = (Width + 2) * 3; HeightC = Height + 2; // 寬度和高度都擴(kuò)展2個(gè)像素 byte[] ImageData = new byte[Stride * Height]; // 用于保存圖像數(shù)據(jù),(處理前后的都為他) byte[] ImageDataC = new byte[StrideC * HeightC]; // 用于保存擴(kuò)展后的圖像數(shù)據(jù) fixed (byte* Scan0 = &ImageData[0]) { BitmapData BmpData = new BitmapData(); BmpData.Scan0 = (IntPtr)Scan0; // 設(shè)置為字節(jié)數(shù)組的的第一個(gè)元素在內(nèi)存中的地址 BmpData.Stride = Stride; Bmp.LockBits(new Rectangle(0, 0, Bmp.Width, Bmp.Height), ImageLockMode.ReadWrite | ImageLockMode.UserInputBuffer, PixelFormat.Format24bppRgb, BmpData); Stopwatch Sw = new Stopwatch(); // 只獲取計(jì)算用時(shí) Sw.Start(); for (Y = 0; Y < Height; Y++) { System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1), 3); // 填充擴(kuò)展圖的左側(cè)第一列像素(不包括第一個(gè)和最后一個(gè)點(diǎn)) System.Buffer.BlockCopy(ImageData, Stride * Y + (Width - 1) * 3, ImageDataC, StrideC * (Y + 1) + (Width + 1) * 3, 3); // 填充最右側(cè)那一列的數(shù)據(jù) System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1) + 3, Width * 3); } System.Buffer.BlockCopy(ImageDataC, StrideC, ImageDataC, 0, StrideC); // 第一行 System.Buffer.BlockCopy(ImageDataC, (HeightC - 2) * StrideC, ImageDataC, (HeightC - 1) * StrideC, StrideC); // 最后一行 for (Y = 0; Y < Height; Y++) { Speed = Y * Stride; SpeedOne = StrideC * Y; for (X = 0; X < Width; X++) { SpeedTwo = SpeedOne + StrideC; // 盡量減少計(jì)算 SpeedThree = SpeedTwo + StrideC; // 下面的就是嚴(yán)格的按照Sobel算字進(jìn)行計(jì)算,代碼中的*2一般會(huì)優(yōu)化為移位或者兩個(gè)Add指令的,如果你不放心,當(dāng)然可以直接改成移位 BlueOne = ImageDataC[SpeedOne] + 2 * ImageDataC[SpeedTwo] + ImageDataC[SpeedThree] - ImageDataC[SpeedOne + 6] - 2 * ImageDataC[SpeedTwo + 6] - ImageDataC[SpeedThree + 6]; GreenOne = ImageDataC[SpeedOne + 1] + 2 * ImageDataC[SpeedTwo + 1] + ImageDataC[SpeedThree + 1] - ImageDataC[SpeedOne + 7] - 2 * ImageDataC[SpeedTwo + 7] - ImageDataC[SpeedThree + 7]; RedOne = ImageDataC[SpeedOne + 2] + 2 * ImageDataC[SpeedTwo + 2] + ImageDataC[SpeedThree + 2] - ImageDataC[SpeedOne + 8] - 2 * ImageDataC[SpeedTwo + 8] - ImageDataC[SpeedThree + 8]; BlueTwo = ImageDataC[SpeedOne] + 2 * ImageDataC[SpeedOne + 3] + ImageDataC[SpeedOne + 6] - ImageDataC[SpeedThree] - 2 * ImageDataC[SpeedThree + 3] - ImageDataC[SpeedThree + 6]; GreenTwo = ImageDataC[SpeedOne + 1] + 2 * ImageDataC[SpeedOne + 4] + ImageDataC[SpeedOne + 7] - ImageDataC[SpeedThree + 1] - 2 * ImageDataC[SpeedThree + 4] - ImageDataC[SpeedThree + 7]; RedTwo = ImageDataC[SpeedOne + 2] + 2 * ImageDataC[SpeedOne + 5] + ImageDataC[SpeedOne + 8] - ImageDataC[SpeedThree + 2] - 2 * ImageDataC[SpeedThree + 5] - ImageDataC[SpeedThree + 8]; PowerBlue = BlueOne * BlueOne + BlueTwo * BlueTwo; PowerGreen = GreenOne * GreenOne + GreenTwo * GreenTwo; PowerRed = RedOne * RedOne + RedTwo * RedTwo; if (PowerBlue > 65025) PowerBlue = 65025; // 處理掉溢出值 if (PowerGreen > 65025) PowerGreen = 65025; if (PowerRed > 65025) PowerRed = 65025; ImageData[Speed] = SqrValue[PowerBlue]; // 查表 ImageData[Speed + 1] = SqrValue[PowerGreen]; ImageData[Speed + 2] = SqrValue[PowerRed]; Speed += 3; // 跳往下一個(gè)像素 SpeedOne += 3; } } Sw.Stop(); this.Text = "計(jì)算用時(shí): " + Sw.ElapsedMilliseconds.ToString() + " ms"; Bmp.UnlockBits(BmpData); // 必須先解鎖,否則Invalidate失敗 } Pic.Invalidate(); } 為簡(jiǎn)單的起見(jiàn),這里先是用的C#的一維數(shù)組實(shí)現(xiàn)的,并且計(jì)時(shí)部分未考慮圖像數(shù)據(jù)的獲取和更新, 因?yàn)檎嬲膱D像處理過(guò)程中圖像數(shù)據(jù)肯定是已經(jīng)獲得的了。 針對(duì)上述代碼,編譯為Release模式后,執(zhí)行編譯后的EXE,對(duì)于3000*4000*3的彩色圖像,耗時(shí)約480ms,如果你是在IDE的模式先運(yùn)行,記得一定要在選項(xiàng)--》調(diào)試--》常規(guī)里不勾選 在模塊加載時(shí)取消JIT優(yōu)化(僅限托管)一欄。 上述代碼中的填充克隆圖數(shù)據(jù)時(shí)并沒(méi)有新建一副圖,然后再填充其中的圖像數(shù)據(jù),而是直接填充一個(gè)數(shù)組,圖像其實(shí)不就是一片連續(xù)內(nèi)存加一點(diǎn)頭信息嗎,頭信息已經(jīng)有了,所以只要一片內(nèi)存就夠了。 克隆數(shù)據(jù)的填充采用了系統(tǒng)Buffer.BlockCopy函數(shù),該函數(shù)類(lèi)似于我們以前常用CopyMemory,速度非常快。 為進(jìn)一步調(diào)高執(zhí)行速度,我們首先來(lái)看看算法的關(guān)鍵耗時(shí)部位的代碼,即for (X = 0; X < Width; X++)內(nèi)部的代碼,我們?nèi)∫恍写a的反編譯碼來(lái)看看: BlueOne = ImageDataC[SpeedOne] + 2 * ImageDataC[SpeedTwo] + ImageDataC[SpeedThree] - ImageDataC[SpeedOne + 6] - 2 * ImageDataC[SpeedTwo + 6] - ImageDataC[SpeedThree + 6]; 上述匯編碼我只注釋一點(diǎn)點(diǎn),其中最0000073c 標(biāo)號(hào),我們跟蹤后返現(xiàn)是調(diào)用了另外一個(gè)函數(shù): 0000073c call 685172A4 我們看到在獲取每一個(gè)數(shù)組元素前,都必須執(zhí)行一個(gè)cmp 和 jae指令,從分析我認(rèn)為這里是做類(lèi)似于判斷數(shù)組的下標(biāo)是否越界之類(lèi)的工作的。如果我們能確保我們的算法那不會(huì)產(chǎn)生越界,這部分代碼有很用呢,不是耽誤我做正事嗎。 為此,我認(rèn)為需要在C#中直接利用指針來(lái)實(shí)現(xiàn)算法,C#中有unsafe模式,也有指針,所以很方便,而且指針的表達(dá)即可以用*,也可以用[],比如*(P+4) 和P[4]是一個(gè)意思。那么只要做很少的修改就可以將上述代碼修改為指針版。
private void CmdFindEdgesPointer_Click(object sender, EventArgs e) { int X, Y; int Width, Height, Stride, StrideC, HeightC; int Speed, SpeedOne, SpeedTwo, SpeedThree; int BlueOne, BlueTwo, GreenOne, GreenTwo, RedOne, RedTwo; int PowerRed, PowerGreen, PowerBlue; Bitmap Bmp = (Bitmap)Pic.Image; if (Bmp.PixelFormat != PixelFormat.Format24bppRgb) throw new Exception("不支持的圖像格式."); byte[] SqrValue = new byte[65026]; for (Y = 0; Y < 65026; Y++) SqrValue[Y] = (byte)(255 - (int)Math.Sqrt(Y)); // 計(jì)算查找表,注意已經(jīng)砸查找表里進(jìn)行了反色 Width = Bmp.Width; Height = Bmp.Height; Stride = (int)((Bmp.Width * 3 + 3) & 0XFFFFFFFC); StrideC = (Width + 2) * 3; HeightC = Height + 2; // 寬度和高度都擴(kuò)展2個(gè)像素 byte[] ImageData = new byte[Stride * Height]; // 用于保存圖像數(shù)據(jù),(處理前后的都為他) byte[] ImageDataC = new byte[StrideC * HeightC]; // 用于保存擴(kuò)展后的圖像數(shù)據(jù) fixed (byte* P = &ImageData[0], CP = &ImageDataC[0], LP = &SqrValue[0]) { byte* DataP = P, DataCP = CP, LutP = LP; BitmapData BmpData = new BitmapData(); BmpData.Scan0 = (IntPtr)DataP; // 設(shè)置為字節(jié)數(shù)組的的第一個(gè)元素在內(nèi)存中的地址 BmpData.Stride = Stride; Bmp.LockBits(new Rectangle(0, 0, Bmp.Width, Bmp.Height), ImageLockMode.ReadWrite | ImageLockMode.UserInputBuffer, PixelFormat.Format24bppRgb, BmpData); Stopwatch Sw = new Stopwatch(); // 只獲取計(jì)算用時(shí) Sw.Start(); for (Y = 0; Y < Height; Y++) { System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1), 3); // 填充擴(kuò)展圖的左側(cè)第一列像素(不包括第一個(gè)和最后一個(gè)點(diǎn)) System.Buffer.BlockCopy(ImageData, Stride * Y + (Width - 1) * 3, ImageDataC, StrideC * (Y + 1) + (Width + 1) * 3, 3); // 填充最右側(cè)那一列的數(shù)據(jù) System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1) + 3, Width * 3); } System.Buffer.BlockCopy(ImageDataC, StrideC, ImageDataC, 0, StrideC); // 第一行 System.Buffer.BlockCopy(ImageDataC, (HeightC - 2) * StrideC, ImageDataC, (HeightC - 1) * StrideC, StrideC); // 最后一行 for (Y = 0; Y < Height; Y++) { Speed = Y * Stride; SpeedOne = StrideC * Y; for (X = 0; X < Width; X++) { SpeedTwo = SpeedOne + StrideC; // 盡量減少計(jì)算 SpeedThree = SpeedTwo + StrideC; // 下面的就是嚴(yán)格的按照Sobel算字進(jìn)行計(jì)算,代碼中的*2一般會(huì)優(yōu)化為移位或者兩個(gè)Add指令的,如果你不放心,當(dāng)然可以直接改成移位 BlueOne = DataCP[SpeedOne] + 2 * DataCP[SpeedTwo] + DataCP[SpeedThree] - DataCP[SpeedOne + 6] - 2 * DataCP[SpeedTwo + 6] - DataCP[SpeedThree + 6]; GreenOne = DataCP[SpeedOne + 1] + 2 * DataCP[SpeedTwo + 1] + DataCP[SpeedThree + 1] - DataCP[SpeedOne + 7] - 2 * DataCP[SpeedTwo + 7] - DataCP[SpeedThree + 7]; RedOne = DataCP[SpeedOne + 2] + 2 * DataCP[SpeedTwo + 2] + DataCP[SpeedThree + 2] - DataCP[SpeedOne + 8] - 2 * DataCP[SpeedTwo + 8] - DataCP[SpeedThree + 8]; BlueTwo = DataCP[SpeedOne] + 2 * DataCP[SpeedOne + 3] + DataCP[SpeedOne + 6] - DataCP[SpeedThree] - 2 * DataCP[SpeedThree + 3] - DataCP[SpeedThree + 6]; GreenTwo = DataCP[SpeedOne + 1] + 2 * DataCP[SpeedOne + 4] + DataCP[SpeedOne + 7] - DataCP[SpeedThree + 1] - 2 * DataCP[SpeedThree + 4] - DataCP[SpeedThree + 7]; RedTwo = DataCP[SpeedOne + 2] + 2 * DataCP[SpeedOne + 5] + DataCP[SpeedOne + 8] - DataCP[SpeedThree + 2] - 2 * DataCP[SpeedThree + 5] - DataCP[SpeedThree + 8]; PowerBlue = BlueOne * BlueOne + BlueTwo * BlueTwo; PowerGreen = GreenOne * GreenOne + GreenTwo * GreenTwo; PowerRed = RedOne * RedOne + RedTwo * RedTwo; if (PowerBlue > 65025) PowerBlue = 65025; // 處理掉溢出值 if (PowerGreen > 65025) PowerGreen = 65025; if (PowerRed > 65025) PowerRed = 65025; DataP[Speed] = LutP[PowerBlue]; // 查表 DataP[Speed + 1] = LutP[PowerGreen]; DataP[Speed + 2] = LutP[PowerRed]; Speed += 3; // 跳往下一個(gè)像素 SpeedOne += 3; } } Sw.Stop(); this.Text = "計(jì)算用時(shí): " + Sw.ElapsedMilliseconds.ToString() + " ms"; Bmp.UnlockBits(BmpData); // 必須先解鎖,否則Invalidate失敗 } Pic.Invalidate(); } 同樣的效果,同樣的圖像,計(jì)算用時(shí)330ms。 我們?cè)趤?lái)看看相同代碼的匯編碼: BlueOne = DataCP[SpeedOne] + 2 * DataCP[SpeedTwo] + DataCP[SpeedThree] - DataCP[SpeedOne + 6] - 2 * DataCP[SpeedTwo + 6] - DataCP[SpeedThree + 6]; 生產(chǎn)的匯編碼簡(jiǎn)潔,意義明確,對(duì)比下少了很多指令。當(dāng)然速度會(huì)快很多。 注意這一段代碼: fixed (byte* P = &ImageData[0], CP = &ImageDataC[0], LP = &SqrValue[0]) { byte* DataP = P, DataCP = CP, LutP = LP; 如果你把更換為: fixed (byte* DataP = &ImageData[0], DataCP = &ImageDataC[0], LutP = &SqrValue[0]) { 代碼的速度反而比純數(shù)組版的還慢,至于為什么,實(shí)踐為王吧,我也沒(méi)有去分析,反正我知道有這個(gè)結(jié)果。你可以參考鐵哥的一篇文章: 閑談.Net類(lèi)型之public的不public,fixed的不能fixed 當(dāng)然這個(gè)還可以進(jìn)一步做小動(dòng)作的的優(yōu)化,比如movzx eax,byte ptr [esi+edi] 這句中,esi其實(shí)就是數(shù)組的基地址,向這樣寫(xiě)DataCP[SpeedOne] ,每次都會(huì)有這個(gè)基址+偏移的計(jì)算的,如果能實(shí)時(shí)直接動(dòng)態(tài)控制一個(gè)指針變量,使他直接指向索要的位置,則少了一次加法,雖然優(yōu)化不是很明顯,基本可以達(dá)到問(wèn)中之前所提到的300ms的時(shí)間了。具體的代碼可見(jiàn)附件。 很多人可能對(duì)我這些東西不感冒,說(shuō)這些東西丟給GPU比你現(xiàn)在的.......希望這些朋友也不要過(guò)分的打擊吧,每個(gè)人都有自己的愛(ài)好,我只愛(ài)好CPU。 更多PhotoShop算法原理解析系列 - 風(fēng)格化-查找邊緣。相關(guān)文章請(qǐng)關(guān)注PHP中文網(wǎng)!
Photoshop默認(rèn)保存的文件格式,可以保留所有有圖層、色版、通道、蒙版、路徑、未柵格化文字以及圖層樣式等。 |
溫馨提示:喜歡本站的話(huà),請(qǐng)收藏一下本站!