
根據公司年會的要求,需要征集員工的照片制作笑臉照片墻,并且要用照片墻拼出一些圖案,
在收照片之前,我給大家作出了標準示范,比如不能人太大,不能人太小,不能是背影,圖片需要清晰,等等,
但是收集照片這種事情嘛,照片能收集齊了就謝天謝地了(最終收齊率95%),全部照片符合要求是不太可能的,之后還要做后期的處理,比如將“人臉”的部分識別出來,只保留“笑臉”的部分,
一、收集照片
我使用微信的小程式“統計助手”收集照片,最后可以匯總匯出Excel,照片不能直接匯出,但是在Excel表格存盤了超鏈接可以下載,
通過鏈接只能下載640像素寬度的縮略圖,不過根據鏈接的格式很容易猜出原圖的鏈接,寫了一段程式就可以批量下載圖片,并完成自動命名和分檔案夾歸類,
但是這篇文章的重點不是分析Excel的內容抓取和圖片鏈接下載,所以怎么找照片就不贅述了,并且收集照片嘛,你手動收集也是一樣的,

總而言之,制作照片墻的條件是你先整來一大堆照片,
二、人臉捕捉
2.1 自動人臉識別
首先我嘗試了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)
但是這個自動人臉識別有幾個問題:
-
識別的人臉框選范圍太小,識別人物的辨識度不是很高,并且導致最終拼出來的圖片導致整體都是黃色調,整體效果不佳;
-
誤識別率和漏識別率太高,這種單位的活動通常都是重在參與,如果提交合格的照片最終卻沒在照片墻中展示,,那友誼的小船可是說翻就翻;
-
并且有的人提交的是多人合影(比如抱著寶寶的),人工智能再智能也識別不出來“哪一個”是你需要的“人臉”啊,,
第一個問題或許還可以增加框選區域的范圍來改善,但是還有后面的問題無法解決,
2.2 手動標記
不能人工智能,那就人工·智能,手動標記總是可以的,但是一張一張圖片打開PS框選裁圖我可不干,好幾百張呢,而且要是領導不滿意裁切效果,我這幾百張臉不得從頭裁一遍?(雙關梗)
所以我需要一個自動化的工具,這個工具需要滿足以下特性:
- 手動選擇裁切位置,但是滑鼠點一下就能確定位置;
- 裁切不直接切圖保存,而是記錄切圖的坐標,減少存盤空間,并且方便日后更改;
- 如果有標記錯誤的坐標,可以容易操作修改、或者移除;
- 自己寫的圖片瀏覽器不能自動縮放,所以要自己實作,并且獲取滑鼠位置的時候需要記錄等效轉換的坐標;
- 遍歷目標檔案夾內的多個子檔案夾內的所有符合格式的圖片檔案,
設計思路:
設計了一個MyPicture類,類屬性包含當前影像、log檔案路徑、縮放系數、當前繪制矩形的4引數坐標,以及一些方法:
- 鍵盤按鍵控制照片切換,按Esc退出(但未設定照片向前切換,因為懶)
- 切換照片后自動檢索是否存在記錄矩形坐標log檔案,等效坐標轉換,并進行繪制
- 滑鼠左鍵按下記錄起點坐標
- 滑鼠左鍵框選并在圖片中實時預覽
- 滑鼠左鍵抬起確認矩形終點坐標,等效坐標轉換并轉為整點形式,并創建同名的txt后綴的log檔案
- 滑鼠右鍵按下洗掉log檔案,并清空圖片矩形繪圖
還有一些其他瑣碎的很容易看懂的功能,直接看代碼吧:
實作代碼:
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張人臉大概幾分鐘就圈完了吧?我還檢查了幾遍,
三、照片墻拼圖
3.1 隨機佇列
制作公司的照片墻和不同于網上隨便找來的照片,需要保證每一個提交合格照片的參與者都能上墻,
但是如果按順序排列又會降低觀感和娛樂性,所以需要找到一種可以保證所有照片都能上墻,但是又有一定隨機性的打亂方法,
那么很顯然,就是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方法可以保證在多張拼圖中所有的照片都能夠被展示到,
3.2 計算四點坐標的迭代器
最終投射的大螢屏解析度是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
3.3 矩形比例轉換
螢屏被分割成了像素網格狀,每一塊“像素”都是正方形或者長方形,由于裁切整除的問題,每一塊“像素”的長寬比例可能都是不完全相同的,
并且在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軟體一樣自動填充背景色,
作為簡單的處理,我將坐標越界(負數值)的部分統一設定為邊界值(零),但是這樣導致裁切出的影像不符合待填充位置的長寬比例,后面拉伸填充會造成圖片變形,
更合理的方式是首先滿足“周長不變”的轉換條件,然后進行縮圖,直到裁切邊緣不會超出原圖的范圍為止,
不過我懶得改了,超出邊緣的情況也比較少,我就不適配了,
3.4 蒙版圖片
照片墻中的“像素”數量并不是越多越好,如果畫面中的“像素”數太多,照片墻重復的照片就會更多;如果“像素”數太少,那么一張照片墻中上墻的照片人數太少,
如果達到最理想的效果,一張照片正好用完所有的照片是最合適的,(或者你的照片很多很多,通過幾張拼圖把照片全部用完也是可以的)
我這里有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的陣列,然后再將圖案蓋住已有的照片,因為這樣將導致被遮住的圖片無法保證一定在其他位置出現過,
所以在迭代器中跳過需要留白的“像素”,不產生迭代輸出,這樣已有的照片就不會被顯示的圖案“擋住”,才能保證每一張參與者提交的照片都能出現在照片墻中,
3.5 讀寫中文路徑圖片
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)
3.6 遍歷檔案夾內的圖片
當使用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
3.7 唯一檔案名控制器
每張圖片的布局都是隨機生成的,每一次的布局就像猴子敲出的莎士比亞短詩一樣可遇而可不求,直到曾經檔案被覆寫的時候才悔不當初,
為了避免不小心檔案重復命名把以前的檔案抹掉,并且方便多圖片的批量生成,設計了一個避免檔案名重復的封裝函式:
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
3.8 完成拼圖
最終再把前面的環節都串起來就可以生成照片墻了!
整個程式的流程:
- 遍歷檔案夾符合格式的檔案
- 創建隨機串列生成器
- 選擇蒙版圖片
- 創建待布局矩形照片2點坐標迭代器
- 圖片串列隨機輸出
- 讀取記錄檔案
- 根據填充矩形轉換人臉矩形區域比例
- 讀圖裁圖縮圖和貼圖
- 生成唯一檔案名并保存圖片
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
如有問題歡迎留言,

目錄
- 一、收集照片
- 二、人臉捕捉
- 2.1 自動人臉識別
- 2.2 手動標記
- 三、照片墻拼圖
- 3.1 隨機佇列
- 3.2 計算四點坐標的迭代器
- 3.3 矩形比例轉換
- 3.4 蒙版圖片
- 3.5 讀寫中文路徑圖片
- 3.6 遍歷檔案夾內的圖片
- 3.7 唯一檔案名控制器
- 3.8 完成拼圖
- 四、最終效果
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/255141.html
標籤:AI
