【51CTO.com快譯】我喜歡用Photoshop修改各種東西, 再把結果在Slack公司內發佈, 每次都能帶來新的想法我享受在其中。
不過重複打開Photoshop再複製/粘貼面部圖像確實相當乏味。
在最初產生這個想法時, 我就意識到這個項目將主要包含三大組成部分:
1. 簡單圖像修改2. Slack集成3. 面部檢測以往我曾經使用過Go中的image與image/draw套裝軟體, 並閱讀過與之相關的幾篇文章, 因此我對於完成這項任務很有信心。 組成部分1就此搞定。
我唯一不確定的是面部檢測工作到底是否易於實現。 我在穀歌上查找golang面部檢測內容, 並點開第一條結果, 其內容指向StackOverflow上關於go-opencv電腦視覺庫的一條問題。 在查閱了該庫中的面部檢測示例專案後, 我瞭解到了自己需要掌握的一切。 組成部分3也同樣得到了解決。
面部檢測由於熟悉度最低, 所以我決定首先從面部檢測入手。 這是項目中最大的難題, 因此我打算先看看自己能否搞定, 如果不行那其它的工作都將毫無意義。
我決定盡可能對go-opencv庫進行封裝。 可以肯定的是, opencv資料類型與Go標準庫有所區別, 至少在其定義Image與Rectangle兩項介面方面存在差異, 因此必須作出一些調整。
我在其中發現一項對opencv.FromImage方法的引用, 其負責將Go的image.Image轉換為opencv庫的形式。
遺憾的是, 我無法利用同樣的轉換方式載入Haar面部識別XML檔, 不過這樣的結果我還可以接受, 所以暫時先這樣吧。
以此為基礎, 我編寫出了以下facefinder包:
package facefinder import ( "image""github.com/lazywei/go-opencv/opencv" ) var faceCascade *opencv.HaarCascade type Finder struct { cascade *opencv.HaarCascade } func NewFinder(xml string) *Finder { return &Finder{ cascade: opencv.LoadHaarClassifierCascade(xml), } } func (f *Finder) Detect(i image.Image) []image.Rectangle { var output []image.Rectangle faces := f.cascade.DetectObjects(opencv.FromImage(i)) for _, face := range faces { output = append(output, image.Rectangle{ image.Point{face.X(), face.Y()}, image.Point{face.X() + face.Width(), face.Y() + face.Height()}, }) } return output }而後, 我能夠輕鬆找到圖像中的面部區域:
imageReader, _ := os.Open(imageFile) baseImage, _, _ := image.Decode(imageReader) finder := facefinder.NewFinder(haarCascadeFilepath) faces := finder.Detect(baseImage) for _, face := range faces { // [...] }我從穀歌上複製了幾段“繪製矩形”代碼以進行功能檢查, 並確定以上代碼確實能夠正常工作。 有了位置資訊, 我又鼓搗出一條圖像載入轉換函數(其中更關注錯誤內容, 而非急於將一切塞進)。
func loadImage(file string) image.Image { reader, err := os.Open(file) if err != nil { log.Fatalf("error loading %s: %s", file, err) } img, _, err := image.Decode(reader) if err != nil { log.Fatalf("error loading %s: %s", file, err) } return img }圖像修改接下來, 我的新迴圈如下所示:
baseImage := loadImage(imageFile) chrisFace := loadImage(chrisFaceFile) bounds := baseImage.Bounds() finder := facefinder.NewFinder(haarCascadeFilepath) faces := finder.Detect(baseImage) // Convert image.Image to a mutable image.ImageRGBA canvas := image.NewRGBA(bounds) draw.Draw(canvas, bounds, baseImage, bounds.Min, draw.Src) for _, face := range faces { draw.Draw( canvas, face, chrisFace, bounds.Min, draw.Src, ) }令人振奮, 測試結果一切順利。
言歸正傳, 其首次實際效果就遠超我的預期。 矩形繪製演算法真棒!
在圖像修改方面, 我首先得想辦法去掉黑色背景。 我以前曾使用過PNG配合透明背景的方法, 因此確信其一定有效。 在穀歌了幾下後, 我偶然發現了draw.Draw函數中的draw.Over。 我將其塞進正在使用的draw.Src, 確實有效!
雖然也可以用羽毛筆慢慢繪邊, 但腦袋裡的一個聲音告訴我, 差不多就可以了。
好的, 接下來我需要把面部圖像縮小一點。 可以肯定的是, 如果將面部圖像放進尺寸完全相同的矩形, 那麼二者肯定無法匹配。 這只是一款面部檢測工具, 而非頭部檢測工具, 這意味著我獲得的矩形並不適用於替換整個頭部。我編寫了一條快速函數以為image.Rectangle增加特定空白邊緣,最終將具體值設定為30%。
完成後,我開始對圖像進行大小/匹配調整。最終,我選擇了disintegration/imaging,其擁有一條簡單的imaging.Fit函數且提供水準鏡像等其它轉換操作。我的面部源圖像不多,所以我想這種鏡像功能可以提供多一種圖像選擇。
在導入後,我的新迴圈如下所示:
for _, face := range faces { // Pad the rectangle by 30 percent rect := rectMargin(30.0, face) // Grab a random face (also 50/50 chance it's mirrored) newFace := chrisFaces.Random() chrisFace := imaging.Fit(newFace, rect.Dx(), rect.Dy(), imaging.Lanczos) draw.Draw( canvas, rect, chrisFace, bounds.Min, draw.Over, ) }我又進行了一輪新的測試,效果相當不錯!
到這裡,我意識到自己做出了一些真正有價值的東西。
Slack集成我把面部修改代碼轉化為一個可運行的二進位檔案,並打算將其打包成一個Slack機器人。之所以先轉換為二進位形式,是為了方便測試並在確定一切無誤後再行打包。現在時機已經成熟,我將把它變成Slack機器人。
當然,由於個人水準的限制,我又轉向了穀歌。
第一條結果就是我所需要的內容。我花了大量時間閱讀Slack的API說明文檔並加以實踐,最終我得到了以下結果:
不錯
第一套反覆運算使用了Slack上傳,但其作為自由Slack層意味著其不夠理想。我轉而將輸出結果以本地方式存儲在自己的伺服器上,而後再將其鏈至Slack。由於Slack會自動擴展大部分圖像連結,因此這種作法對大多數人來說並不會影響到用戶體驗,也不會引來頂頭上司的注意。
由於訪問過程更為輕鬆,現在我能夠快速獲得大量實驗性面部圖像。我意識到,如果其找不到任何面部圖像,則會全程回復同樣的原有圖像——這就不好玩了。所以我將迴圈調整為:
iflen(faces) == 0 { // Grab a specific face and resize it to 1/3 the width// of the base image face := imaging.Resize( chrisFaces[0], bounds.Dx()/3, 0, imaging.Lanczos, ) face_bounds := face.Bounds() draw.Draw( canvas, bounds, face, // I'll be honest, I was a couple beers in when I came up with this and I// have no idea how it works exactly, but it puts the face at the bottom of// the image, centered horizontally with the lower half of the face cut off bounds.Min.Add(image.Pt( -bounds.Max/2+face_bounds.Max.X/2, -bounds.Max.Y+int(float64(face_bounds.Max.Y)/1.9), )), draw.Over, ) }現在的結果是:
我個人對這套解決方案非常滿意。
到這裡全部工作已經就緒,就等同事們的反應了。我只用了一個晚上就完全了從概念到原型的全部工作,沒人知道我為他們準備了怎樣的驚喜。
截至目前,我的經理是最為積極的Chrisbot手動配置使用者。
抱歉了Mat,看來自動化方案最終一定會取代人類的職位。
但這傢伙自己則非常開心。
不久之後,整個辦公室都在向@Chrisbot發送圖片。
我驚喜地發現,它確實能夠正確地處理面部重疊情況,即首先繪製最遠處的面孔。雖然這純粹屬於go-opencv庫返回矩形時實際順序帶來的副作用,但我對結果非常滿意。
不過雖然自動化面部替換大大增加了Slack當中Chris的亮相次數,但仍有一些人認為,人為操作的結果更有靈性一些。
不得不承認,他們的觀點確實站得住腳——至少在某些情況之下。
這意味著我獲得的矩形並不適用於替換整個頭部。我編寫了一條快速函數以為image.Rectangle增加特定空白邊緣,最終將具體值設定為30%。完成後,我開始對圖像進行大小/匹配調整。最終,我選擇了disintegration/imaging,其擁有一條簡單的imaging.Fit函數且提供水準鏡像等其它轉換操作。我的面部源圖像不多,所以我想這種鏡像功能可以提供多一種圖像選擇。
在導入後,我的新迴圈如下所示:
for _, face := range faces { // Pad the rectangle by 30 percent rect := rectMargin(30.0, face) // Grab a random face (also 50/50 chance it's mirrored) newFace := chrisFaces.Random() chrisFace := imaging.Fit(newFace, rect.Dx(), rect.Dy(), imaging.Lanczos) draw.Draw( canvas, rect, chrisFace, bounds.Min, draw.Over, ) }我又進行了一輪新的測試,效果相當不錯!
到這裡,我意識到自己做出了一些真正有價值的東西。
Slack集成我把面部修改代碼轉化為一個可運行的二進位檔案,並打算將其打包成一個Slack機器人。之所以先轉換為二進位形式,是為了方便測試並在確定一切無誤後再行打包。現在時機已經成熟,我將把它變成Slack機器人。
當然,由於個人水準的限制,我又轉向了穀歌。
第一條結果就是我所需要的內容。我花了大量時間閱讀Slack的API說明文檔並加以實踐,最終我得到了以下結果:
不錯
第一套反覆運算使用了Slack上傳,但其作為自由Slack層意味著其不夠理想。我轉而將輸出結果以本地方式存儲在自己的伺服器上,而後再將其鏈至Slack。由於Slack會自動擴展大部分圖像連結,因此這種作法對大多數人來說並不會影響到用戶體驗,也不會引來頂頭上司的注意。
由於訪問過程更為輕鬆,現在我能夠快速獲得大量實驗性面部圖像。我意識到,如果其找不到任何面部圖像,則會全程回復同樣的原有圖像——這就不好玩了。所以我將迴圈調整為:
iflen(faces) == 0 { // Grab a specific face and resize it to 1/3 the width// of the base image face := imaging.Resize( chrisFaces[0], bounds.Dx()/3, 0, imaging.Lanczos, ) face_bounds := face.Bounds() draw.Draw( canvas, bounds, face, // I'll be honest, I was a couple beers in when I came up with this and I// have no idea how it works exactly, but it puts the face at the bottom of// the image, centered horizontally with the lower half of the face cut off bounds.Min.Add(image.Pt( -bounds.Max/2+face_bounds.Max.X/2, -bounds.Max.Y+int(float64(face_bounds.Max.Y)/1.9), )), draw.Over, ) }現在的結果是:
我個人對這套解決方案非常滿意。
到這裡全部工作已經就緒,就等同事們的反應了。我只用了一個晚上就完全了從概念到原型的全部工作,沒人知道我為他們準備了怎樣的驚喜。
截至目前,我的經理是最為積極的Chrisbot手動配置使用者。
抱歉了Mat,看來自動化方案最終一定會取代人類的職位。
但這傢伙自己則非常開心。
不久之後,整個辦公室都在向@Chrisbot發送圖片。
我驚喜地發現,它確實能夠正確地處理面部重疊情況,即首先繪製最遠處的面孔。雖然這純粹屬於go-opencv庫返回矩形時實際順序帶來的副作用,但我對結果非常滿意。
不過雖然自動化面部替換大大增加了Slack當中Chris的亮相次數,但仍有一些人認為,人為操作的結果更有靈性一些。
不得不承認,他們的觀點確實站得住腳——至少在某些情況之下。