赞
踩
根据公司年会的要求,需要征集员工的照片制作笑脸照片墙,并且要用照片墙拼出一些图案。
在收照片之前,我给大家作出了标准示范,比如不能人太大,不能人太小,不能是背影,图片需要清晰,等等。
但是收集照片这种事情嘛,照片能收集齐了就谢天谢地了(最终收齐率95%),全部照片符合要求是不太可能的。之后还要做后期的处理,比如将“人脸”的部分识别出来,只保留“笑脸”的部分。
我使用微信的小程序“统计助手”收集照片,最后可以汇总导出Excel。照片不能直接导出,但是在Excel表格存储了超链接可以下载。
通过链接只能下载640像素宽度的缩略图,不过根据链接的格式很容易猜出原图的链接。写了一段程序就可以批量下载图片,并完成自动命名和分文件夹归类。
但是这篇文章的重点不是分析Excel的内容抓取和图片链接下载,所以怎么找照片就不赘述了。并且收集照片嘛,你手动收集也是一样的。
总而言之,制作照片墙的条件是你先整来一大堆照片。
首先我尝试了Python的图像识别OpenCV库,使用自动识别的方法将人脸识别出来。
只是误识别率和漏识别率感人。
实现代码参考:
import os import cv2 import numpy as np def imread(file): # 读取中文路径下的图片 return cv2.imdecode(np.fromfile(file, np.uint8), -1) def imwrite(file, im): # 写入中文路径下的图片 cv2.imencode('.jpg', im)[1].tofile(file) def MyWalk(path, exts=[]): # 遍历文件夹内符合格式的文件 result = [] for root, folders, files in os.walk(path): result += [os.path.join(root, file) for file in files if os.path.splitext(file)[1] in exts] return result def SaveFaces(folder): os.makedirs(folder+'_face', exist_ok=1) for file in MyWalk(folder, ['.jpg', '.png']): fileroot = os.path.join(folder+'_face', os.path.splitext(os.path.basename(file))[0]) img = imread(file) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.5, 1, minSize=(50, 50)) for j, (x, y, w, h) in enumerate(faces): img2 = img[y:y+h,x:x+w] imwrite('%s_%d.jpg'%(fileroot, j+1), img2) folder = '表单统计' xml = r'..\cv2\data\haarcascade_frontalface_default.xml' # 根据自身情况找一下找个文件的路径,通常在Python的对应库的目录下,没有的话也可以在网上下载 face_cascade = cv2.CascadeClassifier(xml) SaveFaces(folder)
但是这个自动人脸识别有几个问题:
识别的人脸框选范围太小,识别人物的辨识度不是很高,并且导致最终拼出来的图片导致整体都是黄色调,整体效果不佳;
误识别率和漏识别率太高,这种单位的活动通常都是重在参与,如果提交合格的照片最终却没在照片墙中展示。。那友谊的小船可是说翻就翻;
并且有的人提交的是多人合影(比如抱着宝宝的),人工智能再智能也识别不出来“哪一个”是你需要的“人脸”啊。。
第一个问题或许还可以增加框选区域的范围来改善,但是还有后面的问题无法解决。
不能人工智能,那就人工·智能,手动标记总是可以的。但是一张一张图片打开PS框选裁图我可不干,好几百张呢。而且要是领导不满意裁切效果,我这几百张脸不得从头裁一遍?(双关梗)
所以我需要一个自动化的工具,这个工具需要满足以下特性:
设计思路:
设计了一个MyPicture类,类属性包含当前图像、log文件路径、缩放系数、当前绘制矩形的4参数坐标,以及一些方法:
还有一些其他琐碎的很容易看懂的功能,直接看代码吧:
实现代码:
import os import cv2 import numpy as np SCREEN_WIDTH = 1900 SCREEN_HEIGHT = 900 def MyWalk(path, exts=[]): # 遍历目标文件夹内所有符合格式的文件 result = [] for root, folders, files in os.walk(path): result += [os.path.join(root, file) for file in files if os.path.splitext(file)[1] in exts] return result def imread(file): # 读取中文路径下的图片 return cv2.imdecode(np.fromfile(file, np.uint8), -1) def imwrite(file, im): # 写入中文路径下的图片 cv2.imencode('.jpg', im)[1].tofile(file) class MyPicture: def SetPicture(self, file): self.log = os.path.splitext(file)[0] + '.txt' img0 = imread(file) h, w, n = img0.shape self.k = k = min(SCREEN_WIDTH/w, SCREEN_HEIGHT/h) self.img = cv2.resize(img0, (int(w*k), int(h*k))) self.ReadLog() def ReadLog(self): if os.path.isfile(self.log): with open(self.log) as f: self.rect = [int(float(x) * self.k) for x in f.read().split(',')] else: self.rect = [0, 0, 0, 0] self.DrawRect(self.rect, (255, 0, 0)) def SaveLog(self): with open(self.log, 'w') as f: f.write(','.join(str(int(x / self.k)) for x in self.rect)) def OnMouse(self, evt, x, y, flag, param): # print((evt, flag)) if evt == 0 and flag == 1: self.OnLeftDraw(x, y) elif evt == 1: self.OnLeftDown(x, y) elif evt == 4: self.OnLeftUp(x, y) elif evt == 2: self.OnRightDown() def OnLeftDraw(self, x, y): rect_temp = self.rect[:2] + [x, y] self.DrawRect(rect_temp, (0, 255, 0)) def OnLeftDown(self, x, y): self.rect[:2] = [x, y] def OnLeftUp(self, x, y): self.rect[2:] = [x, y] self.DrawRect(self.rect, (255, 0, 0)) self.SaveLog() def OnRightDown(self): self.DrawRect((0, 0, 0, 0), (255, 0, 0)) if os.path.isfile(self.log): os.remove(self.log) def DrawRect(self, rect, bgr): img2 = self.img.copy() cv2.rectangle(img2, tuple(rect[:2]), tuple(rect[2:]), bgr, 2) cv2.imshow('lsx', img2) folder = '表单统计' pic = MyPicture() cv2.namedWindow('lsx') cv2.setMouseCallback('lsx', pic.OnMouse) for filename in MyWalk(folder, ['.jpg']): print(filename) pic.SetPicture(filename) if 27 == cv2.waitKey(0): # Esc to quit. break cv2.destroyAllWindows()
由于我征集的照片中要求每张照片中只有一个主体,我只需要在一张照片中圈出至多一个人脸(如果照片不符合要求则是0张人脸),所以我只在log文件中记录了一个矩形的坐标。
不过如果想要圈出多张人脸也是可以的,自己改一改代码就好啦。
最终180张人脸大概几分钟就圈完了吧?我还检查了几遍。
制作公司的照片墙和不同于网上随便找来的照片,需要保证每一个提交合格照片的参与者都能上墙。
但是如果按顺序排列又会降低观感和娱乐性,所以需要找到一种可以保证所有照片都能上墙,但是又有一定随机性的打乱方法。
那么很显然,就是random.shuffle方法了,此方法可以将列表打乱。从列表中逐一取出元素不放回,列表取空后重置并再次打乱即可。
我写了一个MyList类来实现此功能,其中成员属性li记录了待取出数组的备份,属性方法pop实现了从打乱了的列表中取出一个元素不放回,并且取空重置且打乱。
class MyList:
def __init__(self, li):
self.li = li[:]
self.li2 = []
def pop(self):
if not self.li2:
self.li2 = self.li[:]
random.shuffle(self.li2)
return self.li2.pop()
但是有的照片墙中的拼图“像素数”较少,收集的照片多于可用的“像素位置”,那有什么办法能解决呢。。果不其然,有同事向我发出了质疑:
那当然是没有办法解决了,但是取出不放回的pop方法可以保证在多张拼图中所有的照片都能够被展示到。
最终投射的大屏幕分辨率是1920×1080,也就是16:9的比例。很显然,布置成为16×9的照片墙是很容易的,但是有些时候16×9的像素格子并不方便拼出目标图案,需要增加或减少“像素数”。
比如19×7,但是1920不能整除19,1080也不能整除7。
如果每个“像素”的宽度取1920/19的整数值(101),高度取1080/7的整数值(154),又会导致多个像素拼满全图后,整体的宽度不足铺满整个屏幕(101×19=1919,154×7=1078)。
所以我写了一个迭代器,以近似的方式计算出平铺屏幕后各像素格的最接近矩形尺寸:
def PositionIter(width, rows, cols):
for r in range(rows):
y1 = int(height/rows*r)
y2 = int(height/rows*(r+1))
for c in range(cols):
x1 = int(width/cols*c)
x2 = int(width/cols*(c+1))
yield x1, x2, y1, y2
屏幕被分割成了像素网格状,每一块“像素”都是正方形或者长方形,由于裁切整除的问题,每一块“像素”的长宽比例可能都是不完全相同的。
并且在2.2节手动标记的人脸范围各不相同,如果裁切矩形和目标格子的长宽比例基本一致还好,拉伸填充不会产生太大的违和感。但是如果原图比较细长,但却要填到方形的格子里;或者是原本正方形的裁切区域,被填充到了细长条的格子里,那违和感就很严重了。
为了尽可能减少比例变形的失真,我首先根据3.2节的迭代器计算出目标格子的长宽,然后读取2.2节中标记人脸log文件的矩形坐标,在基本保证原有裁切风格的前提下,将裁切范围的长宽比例替换为目标格子的长宽比例。
一张图片的裁切比例转换有多种的方式,比如扩大裁切、缩小裁切、保证面积不变裁切、保证周长不变裁切。
我这里采用的是保证周长不变裁切,举例来说比如一个原本20×10的方框,可以替换为18×12的方框,被裁切方框的长宽之和保持不变。
实现代码:
def ConvertRect(rect=(10,20,210,120), wh=(200,100)):
x1, y1, x2, y2 = rect # 原方框
x0, y0 = (x1+x2)/2, (y1+y2)/2 # 中心
w, h = wh # 目标长宽比
# 等周长变换
L = abs(x1-x2)+abs(y1-y2) # 周长
w1, h1 = L*w/(w+h), L*h/(w+h) # 新长宽
# 返回新方框
return (max(0,int(x0-w1/2)), max(0,int(y0-h1/2)),
int(x0+w1/2), int(y0+h1/2))
但是这里存在一个问题,经过转换的矩形坐标是有可能超出图像的边界范围的。裁切到图像范围之外的部位,不会像PS软件一样自动填充背景色。
作为简单的处理,我将坐标越界(负数值)的部分统一设定为边界值(零)。但是这样导致裁切出的图像不符合待填充位置的长宽比例,后面拉伸填充会造成图片变形。
更合理的方式是首先满足“周长不变”的转换条件,然后进行缩图,直到裁切边缘不会超出原图的范围为止。
不过我懒得改了,超出边缘的情况也比较少,我就不适配了。
照片墙中的“像素”数量并不是越多越好,如果画面中的“像素”数太多,照片墙重复的照片就会更多;如果“像素”数太少,那么一张照片墙中上墙的照片人数太少。
如果达到最理想的效果,一张照片正好用完所有的照片是最合适的。(或者你的照片很多很多,通过几张拼图把照片全部用完也是可以的)
我这里有180张照片,分解一下即为宽高18×10像素。18×10是非常小的画布,直接打开图画板就可以创作了。为了避免眼睛看瞎,可以把图画板放大到最大倍数再用铅笔创作。
比如拼一个“666”:
我设定的蒙版规则是黑色表示镂空显示背景,白色表示填充显示照片,因为在程序中白色表示255(是),黑色表示0(非)。当然如果你觉得看着难受,在逻辑里反过来也是一样的。
接下来读取蒙版图片,在3.2节的函数迭代输出前,判断当前输出行列的对应蒙版图片像素是否为黑色,如果是则跳过,否则产生迭代输出,进行下一步运算。
修改3.2节的四点坐标迭代器函数,增加读取蒙版图片作为输入,读取蒙版图片的宽高并作为目标输出拼图的行数和列数。
实现代码:
def PositionIter(width, height, mask):
mask = imread(mask)
rows, cols, _ = mask.shape
for r in range(rows):
y1 = int(height/rows*r)
y2 = int(height/rows*(r+1))
for c in range(cols):
x1 = int(width/cols*c)
x2 = int(width/cols*(c+1))
if mask[r][c][0]:
yield x1, x2, y1, y2
需要注意的是,我不能先平铺铺满18×10的阵列,然后再将图案盖住已有的照片,因为这样将导致被遮住的图片无法保证一定在其他位置出现过。
所以在迭代器中跳过需要留白的“像素”,不产生迭代输出。这样已有的照片就不会被显示的图案“挡住”,才能保证每一张参与者提交的照片都能出现在照片墙中。
OpenCV默认不能读取和保存中文路径下的图片,借助numpy库可以实现在中文路径存取:
def imread(file):
return cv2.imdecode(np.fromfile(file, np.uint8), -1)
def imwrite(file, im):
cv2.imencode('.jpg', im)[1].tofile(file)
当使用2.2节中的人脸识别标记后,文件夹内会自动生成txt格式的log文件,如果再用os.listdir或os.walk函数遍历文件夹,还要排除不符合图片格式的文件。这里写了一个方法可以方便遍历文件夹内符合格式的文件:
def MyWalk(path, exts=[]):
result = []
for root, folders, files in os.walk(path):
result += [os.path.join(root, file) for file in files if os.path.splitext(file)[1] in exts]
return result
每张图片的布局都是随机生成的,每一次的布局就像猴子敲出的莎士比亚短诗一样可遇而可不求,直到曾经文件被覆盖的时候才悔不当初。
为了避免不小心文件重复命名把以前的文件抹掉,并且方便多图片的批量生成,设计了一个避免文件名重复的封装函数:
def UniqueFile(file):
root, ext = os.path.splitext(file)
cnt = 1
while os.path.exists(file):
file = '%s_%d%s'%(root, cnt, ext)
cnt += 1
return file
最终再把前面的环节都串起来就可以生成照片墙了!
整个程序的流程:
def MakePictureWall(files, mask, bg_color=(255,255,255)): img_all = np.zeros((height, width, 3), np.uint8) img_all[:,:] = bg_color # 填充背景颜色 for x1, x2, y1, y2 in PositionIter(width, height, mask): # 生成可分配像素位置 w = x2 - x1 h = y2 - y1 log = '' while not os.path.isfile(log): # 跳过不存在对应log文件的jpg图片 file = files.pop() # 随机选取照片 log = os.path.splitext(file)[0] + '.txt' img = imread(file) with open(log) as f: rect1 = [int(x) for x in f.read().split(',')] x1c, y1c, x2c, y2c = ConvertRect(rect1, (w,h)) # 按可分配方框调整原方框大小 img_crop = img[y1c:y2c,x1c:x2c] # 裁切图片 img_crop_s = cv2.resize(img_crop, (w,h), interpolation=cv2.INTER_CUBIC) # 缩小图像 img_all[y1:y2,x1:x2] = img_crop_s p = '_' + os.path.splitext(os.path.basename(mask))[0] path = UniqueFile(folder+p+'.jpg') imwrite(path, img_all) if __name__ == '__main__': width = 1920 height = 1080 folder = '表单统计' files = MyList(MyWalk(folder, ['.jpg'])) MakePictureWall(files, 'mask_666.png')
最后列举几个生成的例子,不过为了保护个人隐私,我就不使用同事们的照片了。这些网上找到的照片,标记人脸之后生成的照片墙:
666:
百亿:
流水线:
完整的代码包已经发在了CSDN,可以下载。其中包含图片示例,人脸位置已经标记完成,代码可以直接运行:
https://download.csdn.net/download/weixin_39804265/14969181
如有问题欢迎留言。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。