不过重复打开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的亮相次数,但仍有一些人认为,人为操作的结果更有灵性一些。
不得不承认,他们的不雅观点确实站得住脚——至少在某些情形之下。
原文链接:http://blog.zikes.me/post/how-i-ruined-office-productivity-with-a-slack-bot/
原文标题:How I Ruined Office Productivity With a Face-Replacing Slack Bot
(Without Really Knowing What I Was Doing)
核子可乐译
【51CTO翻译稿件,互助站点转载请注明原文作者和出处为51CTO.com】