中国领先的IT技术网站
|
|

国外程序员真会玩,他用这个技术整蛊了全公司的人…

我喜欢用Photoshop修改各种东西,再把结果在Slack公司内发布,每次都能带来新的想法我享受在其中。不过重复打开Photoshop再复制/粘贴面部图像确实相当乏味。

作者:核子可乐译来源:51cto|2017-03-13 09:10

【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包:

  1. 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 } 

而后,我能够轻松找到图像中的面部区域:

  1. imageReader, _ :os.Open(imageFile) baseImage, _, _ :image.Decode(imageReader) finder :facefinder.NewFinder(haarCascadeFilepath) faces :finder.Detect(baseImage) for _, face :range faces { // [...] } 

我从谷歌上复制了几段“绘制矩形”代码以进行功能检查,并确定以上代码确实能够正常工作。有了位置信息,我又鼓捣出一条图像加载转换函数(其中更关注错误内容,而非急于将一切塞进)。

  1. 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 } 

图像修改

接下来,我的新循环如下所示:

  1. 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函数且提供水平镜像等其它转换操作。我的面部源图像不多,所以我想这种镜像功能可以提供多一种图像选择。

在导入后,我的新循环如下所示:

  1. 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会自动扩展大部分图像链接,因此这种作法对大多数人来说并不会影响到用户体验,也不会引来顶头上司的注意。

由于访问过程更为轻松,现在我能够快速获得大量实验性面部图像。我意识到,如果其找不到任何面部图像,则会全程回复同样的原有图像——这就不好玩了。所以我将循环调整为:

  1. 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】

【责任编辑:齐琳 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

热门职位+更多

读 书 +更多

SQL实用简明教程(第2版)

SQL(结构化查询语言)是数据库系统的通用语言,利用它可以用几乎同样的语句在不同的数据库系统上执行同样的操作,在数据库系统的开发中有着...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× 51CTO学院,您有一份礼包待领取