作者 | 李秋鍵
責編 | 晉兆雨
頭圖 | CSDN下載自視覺中國
引言:人臉圖像的生成在各個行業有着重要應用,例如刑事調查、人物設計、教育培訓等。然而一幅逼真的人臉肖像,對於職業畫家也要至少數小時才能繪製出來;對於從未接觸過繪畫的新手,就更是難如登天了。新手繪製出來的人臉草圖往往非常簡陋抽象,甚至有不勻稱、不完整。但如果使用智能人臉畫板,無疑是有如神助。
本項目主要來源於中科院和香港城市大學的一項研究DeepFaceDrawing,論文標題是《DeepFaceDrawing: DeepGeneration of Face Images from Sketches》
具體效果如下圖可見:
實驗前的準備
首先我們使用的python版本是3.6.5所用到的模塊如下:
Pyqt5模塊:PyQt5是基於Digia公司強大的圖形程式框架Qt5的python接口,由一組python模塊構成。PyQt5本身擁有超過620個類和6000函數及方法。在可以運行於多個平臺,包括:Unix, Windows, and Mac OS。
opencv是將用來進行圖像處理和生成。
numpy模塊用來處理矩陣運算。
Jittor模塊國內清華大學開源的深度學習框架。
_thread是多線程庫。
網絡模型的定義和訓練
首先這個圖像合成模塊採用了一種利用發生器和鑑別器的GAN結構。從融合的特徵圖生成真實的人臉圖像。鑑別器採用多尺度鑑別方式:對輸入進行尺度劃分,特徵圖和生成的圖像在三個不同的層次上,經過三個不同的過程。:
(1)權重網絡層和損失定義:
def weights_init_normal(m):
classname = m.__class__.__name__
ifclassname.find("Conv") != -1:
jt.init.gauss_(m.weight,0.0, 0.02)
elifclassname.find("BatchNorm") != -1:
jt.init.gauss_(m.weight,1.0, 0.02)
jt.init.constant_(m.bias,0.0)
def get_norm_layer(norm_type='instance'):
if (norm_type == 'batch'):
norm_layer = nn.BatchNorm
elif (norm_type == 'instance'):
norm_layer =nn.InstanceNorm2d
else:
raiseNotImplementedError(('normalization layer [%s] is not found' % norm_type))
return norm_layer
class MSELoss:
def __init__(self):
pass
def __call__(self, output,target):
from jittor.nn importmse_loss
return mse_loss(output,target)
class BCELoss:
def __init__(self):
pass
def __call__(self, output,target):
from jittor.nn importbce_loss
return bce_loss(output,target)
(2)模型特徵編解碼:
特徵匹配模塊包含5個譯碼網絡,以compact作爲輸入由分量流形得到的特徵向量,並將其轉換爲對應的特徵向量爲後續生成的特徵圖的大小。
def define_part_encoder(model='mouth', norm='instance', input_nc=1,latent_dim=512):
norm_layer =get_norm_layer(norm_type=norm)
image_size = 512
if 'eye' in model:
image_size = 128
elif 'mouth' in model:
image_size = 192
elif 'nose' in model:
image_size = 160
elif 'face' in model:
image_size = 512
else:
print("Whole Image!!")
net_encoder =EncoderGenerator_Res(norm_layer,image_size,input_nc, latent_dim) # input longsize 256 to 512*4*4
print("net_encoder of part"+model+" is:",image_size)
return net_encoder
def define_part_decoder(model='mouth', norm='instance', output_nc=1,latent_dim=512):
norm_layer =get_norm_layer(norm_type=norm)
image_size = 512
if 'eye' in model:
image_size = 128
elif 'mouth' in model:
image_size = 192
elif 'nose' in model:
image_size = 160
else:
print("Whole Image!!")
net_decoder =DecoderGenerator_image_Res(norm_layer,image_size,output_nc, latent_dim) # input longsize 256 to 512*4*4
print("net_decoder to imageof part "+model+" is:",image_size)
return net_decoder
def define_feature_decoder(model='mouth', norm='instance', output_nc=1,latent_dim=512):
norm_layer =get_norm_layer(norm_type=norm)
image_size = 512
if 'eye' in model:
image_size = 128
elif 'mouth' in model:
image_size = 192
elif 'nose' in model:
image_size = 160
else:
print("Whole Image!!")
net_decoder =DecoderGenerator_feature_Res(norm_layer,image_size,output_nc, latent_dim) # input longsize 256 to 512*4*4
print("net_decoder to imageof part "+model+" is:",image_size)
# print(net_decoder)
return net_decoder
def define_G(input_nc, output_nc, ngf, n_downsample_global=3,n_blocks_global=9, norm='instance'):
norm_layer =get_norm_layer(norm_type=norm)
netG = GlobalGenerator(input_nc,output_nc, ngf, n_downsample_global, n_blocks_global, norm_layer)
return netG
圖形界面的定義
在這篇論文中,作者一方面將人臉關鍵區域(雙眼、鼻、嘴和其他區域)作爲面元,學習其特徵嵌入,將輸入草圖的對應部分送到由數據庫樣本中面元的特徵向量構成的流形空間進行校準。另一方面,參考 pix2pixHD [5]的網絡模型設計,使用 conditional GAN 來學習從編碼的面元特徵到真實圖像的映射生成結果。
(1)鼠標繪製函數的定義:
class OutputGraphicsScene(QGraphicsScene):
def __init__(self, parent=None):
QGraphicsScene.__init__(self, parent)
# self.modes = mode_list
self.mouse_clicked = False
self.prev_pt = None
self.setSceneRect(0,0,self.width(),self.height())
# self.masked_image = None
self.selectMode = 0
# save the history of edit
self.history = []
self.ori_img = np.ones((512,512, 3),dtype=np.uint8)*255
self.mask_put = 1 # 1 marksuse brush while 0 user erase
self.convert = False
# self.setPos(0 ,0)
self.firstDisplay = True
self.convert_on = False
def reset(self):
self.convert = False
self.ori_img = np.ones((512,512, 3),dtype=np.uint8)*255
self.updatePixmap(True)
self.prev_pt = None
def setSketchImag(self,sketch_mat, mouse_up=False):
self.ori_img =sketch_mat.copy()
self.image_list = []
self.image_list.append(self.ori_img.copy() )
def mousePressEvent(self,event):
if not self.mask_put orself.selectMode == 1:
self.mouse_clicked =True
self.prev_pt = None
else:
self.make_sketch(event.scenePos())
def make_sketch_Eraser(self,pts):
if len(pts)>0:
for pt in pts:
cv2.line(self.color_img,pt['prev'],pt['curr'],self.paint_color,self.paint_size)
cv2.line(self.mask_img,pt['prev'],pt['curr'],(0,0,0),self.paint_size )
self.updatePixmap()
def modify_sketch(self, pts):
if len(pts)>0:
for pt in pts:
cv2.line(self.ori_img,pt['prev'],pt['curr'],self.paint_color,self.paint_size)
self.updatePixmap()
def get_stk_color(self, color):
self.stk_color = color
def erase_prev_pt(self):
self.prev_pt = None
def reset_items(self):
for i inrange(len(self.items())):
item = self.items()[0]
self.removeItem(item)
def undo(self):
iflen(self.image_list)>1:
num =len(self.image_list)-2
self.ori_img =self.image_list[num].copy()
self.image_list.pop(num+1)
self.updatePixmap(True)
def getImage(self):
returnself.ori_img*(1-self.mask_img) +self.color_img*self.mask_img
defupdatePixmap(self,mouse_up=False):
sketch = self.ori_img
qim = QImage(sketch.data,sketch.shape[1], sketch.shape[0], QImage.Format_RGB888)
if self.firstDisplay :
self.reset_items()
self.imItem =self.addPixmap(QPixmap.fromImage(qim))
self.firstDispla = False
else:
self.imItem.setPixmap(QPixmap.fromImage(qim))
def fresh_board(self):
print('======================================================')
while(True):
if(self.convert_on):
print('======================================================')
time.sleep(100)
iter_start_time =time.time()
self.updatePixmap()
print('TimeSketch:',time.time() - iter_start_time)
(2)GUI界面:其核心思路並非直接用輸入草圖作爲網絡生成條件,而是將人臉進行分塊操作後利用數據驅動的思想對抽象的草圖特徵空間進行隱式建模,並在這個流形空間中找到輸入草圖特徵的近鄰組合來重構特徵,進而合成人臉圖像。
class WindowUI(QtWidgets.QMainWindow,Ui_SketchGUI):
def __init__(self):
super(WindowUI,self).__init__()
self.setupUi(self)
self.setEvents()
self._translate =QtCore.QCoreApplication.translate
self.output_img = None
self.brush_size =self.BrushSize.value()
self.eraser_size =self.EraseSize.value()
self.modes = [0,1,0] #0marks the eraser, 1 marks the brush
self.Modify_modes = [0,1,0]#0 marks the eraser, 1 marks the brush
self.output_scene =OutputGraphicsScene()
self.output.setScene(self.output_scene)
self.output.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.output.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.output.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.output_view =QGraphicsView(self.output_scene)
#self.output_view.fitInView(self.output_scene.updatePixmap())
self.input_scene =InputGraphicsScene(self.modes, self.brush_size,self.output_scene)
self.input.setScene(self.input_scene)
self.input.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.input.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.input.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.input_scene.convert_on= self.RealTime_checkBox.isChecked()
self.output_scene.convert_on= self.RealTime_checkBox.isChecked()
self.BrushNum_label.setText(self._translate("SketchGUI",str(self.brush_size)))
self.EraserNum_label.setText(self._translate("SketchGUI",str(self.eraser_size)))
self.start_time =time.time()
# self.
# try:
# # thread.start_new_thread(self.output_scene.fresh_board,())
# thread.start_new_thread(self.input_scene.thread_shadow,())
# except:
# print("Error: unable to startthread")
# print("Finish")
def setEvents(self):
self.Undo_Button.clicked.connect(self.undo)
self.Brush_Button.clicked.connect(self.brush_mode)
self.BrushSize.valueChanged.connect(self.brush_change)
self.Clear_Button.clicked.connect(self.clear)
self.Eraser_Button.clicked.connect(self.eraser_mode)
self.EraseSize.valueChanged.connect(self.eraser_change)
self.Save_Button.clicked.connect(self.saveFile)
#weight bar
self.part0_Slider.valueChanged.connect(self.changePart)
self.part1_Slider.valueChanged.connect(self.changePart)
self.part2_Slider.valueChanged.connect(self.changePart)
self.part3_Slider.valueChanged.connect(self.changePart)
self.part4_Slider.valueChanged.connect(self.changePart)
self.part5_Slider.valueChanged.connect(self.changAllPart)
self.Load_Button.clicked.connect(self.open)
self.Convert_Sketch.clicked.connect(self.convert)
self.RealTime_checkBox.clicked.connect(self.convert_on)
self.Shadow_checkBox.clicked.connect(self.shadow_on)
self.Female_Button.clicked.connect(self.choose_Gender)
self.Man_Button.clicked.connect(self.choose_Gender)
self.actionSave.triggered.connect(self.saveFile)
def mode_select(self, mode):
for i inrange(len(self.modes)):
self.modes[i] = 0
self.modes[mode] = 1
def brush_mode(self):
self.mode_select(1)
self.brush_change()
self.statusBar().showMessage("Brush")
def eraser_mode(self):
self.mode_select(0)
self.eraser_change()
self.statusBar().showMessage("Eraser")
def undo(self):
self.input_scene.undo()
self.output_scene.undo()
def brush_change(self):
self.brush_size =self.BrushSize.value()
self.BrushNum_label.setText(self._translate("SketchGUI",str(self.brush_size)))
if self.modes[1]:
self.input_scene.paint_size = self.brush_size
self.input_scene.paint_color = (0,0,0)
self.statusBar().showMessage("Change Brush Size in ",self.brush_size)
def eraser_change(self):
self.eraser_size =self.EraseSize.value()
self.EraserNum_label.setText(self._translate("SketchGUI",str(self.eraser_size)))
if self.modes[0]:
print( self.eraser_size)
self.input_scene.paint_size = self.eraser_size
self.input_scene.paint_color = (1,1,1)
self.statusBar().showMessage("Change Eraser Size in ",self.eraser_size)
def changePart(self):
self.input_scene.part_weight['eye1'] = self.part0_Slider.value()/100
self.input_scene.part_weight['eye2']= self.part1_Slider.value()/100
self.input_scene.part_weight['nose'] = self.part2_Slider.value()/100
self.input_scene.part_weight['mouth'] = self.part3_Slider.value()/100
self.input_scene.part_weight[''] = self.part4_Slider.value()/100
self.input_scene.start_Shadow()
#self.input_scene.updatePixmap()
def changAllPart(self):
value =self.part5_Slider.value()
self.part0_Slider.setProperty("value", value)
self.part1_Slider.setProperty("value", value)
self.part2_Slider.setProperty("value", value)
self.part3_Slider.setProperty("value", value)
self.part4_Slider.setProperty("value", value)
self.changePart()
def clear(self):
self.input_scene.reset()
self.output_scene.reset()
self.start_time =time.time()
self.input_scene.start_Shadow()
self.statusBar().showMessage("Clear Drawing Board")
def convert(self):
self.statusBar().showMessage("Press Convert")
self.input_scene.convert_RGB()
self.output_scene.updatePixmap()
def open(self):
fileName, _ =QFileDialog.getOpenFileName(self, "Open File",
QDir.currentPath(),"Images Files (*.*)") #jpg;*.jpeg;*.png
if fileName:
image =QPixmap(fileName)
mat_img =cv2.imread(fileName)
mat_img = cv2.resize(mat_img,(512, 512), interpolation=cv2.INTER_CUBIC)
mat_img =cv2.cvtColor(mat_img, cv2.COLOR_RGB2BGR)
if image.isNull():
QMessageBox.information(self, "Image Viewer",
"Cannotload %s." % fileName)
return
#cv2.imshow('open',mat_img)
self.input_scene.start_Shadow()
self.input_scene.setSketchImag(mat_img)
def saveFile(self):
cur_time =strftime("%Y-%m-%d-%H-%M-%S", gmtime())
file_dir ='./saveImage/'+cur_time
if notos.path.isdir(file_dir) :
os.makedirs(file_dir)
cv2.imwrite(file_dir+'/hand-draw.jpg',self.input_scene.sketch_img*255)
cv2.imwrite(file_dir+'/colorized.jpg',cv2.cvtColor(self.output_scene.ori_img,cv2.COLOR_BGR2RGB))
print(file_dir)
def convert_on(self):
# ifself.RealTime_checkBox.isCheched():
print('self.RealTime_checkBox',self.input_scene.convert_on)
self.input_scene.convert_on= self.RealTime_checkBox.isChecked()
self.output_scene.convert_on= self.RealTime_checkBox.isChecked()
def shadow_on(self):
_translate =QtCore.QCoreApplication.translate
self.input_scene.shadow_on =not self.input_scene.shadow_on
self.input_scene.updatePixmap()
ifself.input_scene.shadow_on:
self.statusBar().showMessage("Shadow ON")
else:
self.statusBar().showMessage("Shadow OFF")
def choose_Gender(self):
ifself.Female_Button.isChecked():
self.input_scene.sex = 1
else:
self.input_scene.sex = 0
self.input_scene.start_Shadow()
總結
這裏給出模型的體驗網址:
http://www.geometrylearning.com:3000/index_621.html
該方法核心亮點之一,便是以多通道特徵圖作爲中間結果來改善信息流。從本質上看,這是將輸入草圖作爲軟約束來替代傳統方法中的硬約束,因此能夠用粗糙甚至不完整的草圖來生成高質量的完整人臉圖像。
反思DeepFaceDrawing
1)畫不出醜臉:
從圖中可以看出,即使給出醜陋的草圖,輸出的也會是平均來說漂亮的人臉,這大概是因爲所用的訓練數據集都是名人,平均“顏值”較高,因此神經網絡學到了一種漂亮的平均;這能算是一種在“顏值上的”數據不平衡問題嗎。
2)安全問題
比如人臉支付場景中,可能存在利用該項技術盜刷的問題。隨着人臉活體檢測技術的發展,這種隱患應該能得以有效避免。
3)技術攻擊性
相比於Deepfake,本文的DeepFaceDrawing應該算是相對無害的。
4)商業價值
如論文作者所說,這項技術在犯罪偵查、人物設計、教育培訓等方面都可以有所作爲。期待有一天這項技術更加通用,這樣一來其商業價值會更大。
完整代碼:
鏈接:https://pan.baidu.com/s/1ARIzPEbUSNzAIdPsRl6h-A
提取碼:4llk
作者簡介
李秋鍵,CSDN 博客專家,CSDN達人課作者。碩士在讀於中國礦業大學,開發有taptap安卓武俠遊戲一部,vip視頻解析,文意轉換工具,寫作機器人等項目,發表論文若干,多次高數競賽獲獎等等。
更多精彩推薦
☞中招!330 萬臺老年機被植木馬,背後黑幕細思極恐
☞什麼?性能強大的 M1 芯片不支持 Docker ?
☞恭喜您被選爲CSDN插件內測用戶:點此領取福利
☞關於動態規劃,你想知道的都在這裏了!
☞一文告訴你霧計算與雲計算的區別及對物聯網的價值!
☞28歲年輕操盤手,加密市場“空手套白狼”成就億萬身家
點分享點點贊點在看