【51CTO.com快译】我喜欢用Photoshop修改各种东西,再把结果在Slack公司内发布,每次都能带来新的想法我享受在其中。
不过重复打开Photoshop再复制/粘贴面部图像确实相当乏味。
在最初产生这个想法时,我就意识到这个项目将主要包含三大组成部分:
1. 简单图像修改
2. Slack集成
3. 面部检测
以往我曾经使用过Go中的image与image/draw软件包,并阅读过与之相关的几篇文章,因此我对于完成这项任务很有信心。组成部分1就此搞定。
我还曾经在Go中构建过一款玩具性质的Slack机器人,其中用到了查找自谷歌的几条指令。虽然缺少Go Slack官方整体客户端会让问题变得更为复杂,但出于最基本的需求,我相信自己能够完成通过Slack下载及上传图像这样一项工作。组成部分2也就不是问题了。
我唯一不确定的是面部检测工作到底是否易于实现。我在谷歌上查找golang面部检测内容,并点开***条结果,其内容指向StackOverflow上关于go-opencv计算机视觉库的一条问题。在查阅了该库中的面部检测示例项目后,我了解到了自己需要掌握的一切。组成部分3也同样得到了解决。
面部检测
由于熟悉度***,所以我决定首先从面部检测入手。这是项目中***的难题,因此我打算先看看自己能否搞定,如果不行那其它的工作都将毫无意义。
我决定尽可能对go-opencv库进行封装。可以肯定的是,opencv数据类型与Go标准库有所区别,至少在其定义Image与Rectangle两项接口方面存在差异,因此必须作出一些调整。
我在其中发现一项对opencv.FromImage方法的引用,其负责将Go的image.Image转换为opencv库的形式。这意味着我不再需要将文件路径传递至opencv.LoadImage方法以进行转换,而可以直接处理存储在内存中的镜像。这能够节约从Slack接收图像后将其保存在文件系统中的步骤。
遗憾的是,我无法利用同样的转换方式加载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的亮相次数,但仍有一些人认为,人为操作的结果更有灵性一些。
不得不承认,他们的观点确实站得住脚——至少在某些情况之下。
【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】