教你用不到 20 行代碼制作一個 “手繪風(fēng)” 視頻
大家好,我是安果!
本期推文與計(jì)算機(jī)視覺相關(guān),用不到 20 行 Python 代碼將一張圖片由自然風(fēng)轉(zhuǎn)化為手繪風(fēng),期間未對圖片進(jìn)行任何預(yù)處理、后處理
代碼中只借助了兩個常見庫,核心計(jì)算由 Numpy 負(fù)責(zé) ,Pillow 負(fù)責(zé)圖片讀寫
在正文開始之前,先看一下最初效果,下面是單張圖片轉(zhuǎn)換前后對比
圖一

圖二

圖三

為了增加趣味性,后面將這段代碼應(yīng)用到一個視頻中,加上一個背景音樂,新鮮的 “手繪風(fēng)視頻” 出爐
“手繪風(fēng)”實(shí)現(xiàn)步驟
講解之前,需要了解手繪圖像的三個主要特點(diǎn):
圖片需為灰度圖,是單通道的; 邊緣部分線條較重涂抹為黑色,相同或相近像素值轉(zhuǎn)換后趨于白色; 在光源效果的加持下,灰度變化可模擬人類視覺的遠(yuǎn)近效果
讀取圖片,轉(zhuǎn)化為數(shù)組
因?yàn)楹竺嬉玫较袼赜?jì)算,為了方便,事先將讀取后的圖片轉(zhuǎn)化為數(shù)組
a = np.asarray(Image.open("Annie1.jpg").convert('L')).astype('float')
計(jì)算 x,y,z 軸梯度值,并歸一化
剛才提到手繪照片的一個特點(diǎn),就是 手繪照片對邊緣區(qū)域更加側(cè)重,定位圖片邊緣部分,最有效方式就是計(jì)算梯度,用灰度變化來模擬圖片遠(yuǎn)近效果,depth 表示預(yù)設(shè)深度,z 軸默認(rèn)梯度為 1
depth = 10. # (0-100)
grad = np.gradient(a) # 取圖像灰度的梯度值
grad_x, grad_y = grad # 分別取橫縱圖像梯度值
grad_x = grad_x * depth / 100.
grad_y = grad_y * depth / 100.
對梯度值完成歸一化操作
A = np.sqrt(grad_x ** 2 + grad_y ** 2 + 1.)
uni_x = grad_x / A
uni_y = grad_y / A
uni_z = 1. / A
加入光源效果
手繪風(fēng)圖片除了計(jì)算梯度值之外,還需要考慮光源影響;根據(jù)光源入射的角度不同最有對x,y,z 各軸上的梯度值有不同程度的影響,添加一個模擬光源,放置在斜上方,與 x , y 分別形成兩個夾角

并且這兩個夾角是通過實(shí)驗(yàn)得到是已知的,然后根據(jù)正弦余弦函數(shù)計(jì)算出最終新的像素值
vec_el = np.pi / 2.2 # 光源的俯視角度,弧度值
vec_az = np.pi / 4. # 光源的方位角度,弧度值
dx = np.cos(vec_el) * np.cos(vec_az) # 光源對 x軸的影響
dy = np.cos(vec_el) * np.sin(vec_az) # 光源對 y軸的影響
dz = np.sin(vec_el) # 光源對z 軸的影響
b = 255 * (dx * uni_x + dy * uni_y + dz * uni_z) # 光源歸一化,8 255
b = b.clip(0, 255)# 對像素值低于0,高于255部分做截斷處理
導(dǎo)出圖片,并保存
im.save("Annie_shouhui.jpg")
以下是該步驟涉及到的的全部代碼
from PIL import Image
import numpy as np
a = np.asarray(Image.open("Annie1.jpg").convert('L')).astype('float')
depth = 10. # (0-100)
grad = np.gradient(a) # 取圖像灰度的梯度值
grad_x, grad_y = grad # 分別取橫縱圖像梯度值
grad_x = grad_x * depth / 100.
grad_y = grad_y * depth / 100.
A = np.sqrt(grad_x ** 2 + grad_y ** 2 + 1.)
uni_x = grad_x / A
uni_y = grad_y / A
uni_z = 1. / A
vec_el = np.pi / 2.2 # 光源的俯視角度,弧度值
vec_az = np.pi / 4. # 光源的方位角度,弧度值
dx = np.cos(vec_el) * np.cos(vec_az) # 光源對 x軸的影響
dy = np.cos(vec_el) * np.sin(vec_az) # 光源對 y軸的影響
dz = np.sin(vec_el) # 光源對z 軸的影響
b = 255 * (dx * uni_x + dy * uni_y + dz * uni_z) # 光源歸一化
b = b.clip(0, 255)
im = Image.fromarray(b.astype('uint8')) # 重構(gòu)圖像
im.save("Annie_shouhui.jpg")
制作手繪風(fēng)視頻
圖片轉(zhuǎn)化后的效果雖然也不錯,但圖片畢竟是靜態(tài)的,人作為視覺動物,如果能做成動態(tài)的那再好不過了,知道上面的方法之后,只需對視頻再加上一個拆幀合并操作,就能制作一個手繪風(fēng) 視頻效果
you-get 下載視頻
這里我用 you-get 命令在 B 站上找了一個視頻,下載了下來,
you-get --format=dash-flv -o ./ https://www.bilibili.com/video/BV1tT4y1j7a9?from=search&8014393453748720686

下載完之后,用 OpenCV2 對視頻進(jìn)行切幀操作,切幀同時對圖片進(jìn)行轉(zhuǎn)化,寫出到本地視頻文件中
vc = cv2.VideoCapture(video_path)
c = 0
if vc.isOpened():
rval,frame = vc.read()
height,width = frame.shape[0],frame.shape[1]
print(height, width)
else:
rval = False
height,width = 960,1200
# jpg_list = [os.path.join('Pic_Directory/',i) for i in os.listdir('Pic_Directory') if i.endswith('.jpg')]
fps = 24 # 視頻幀率
video_path1 = './text.mp4'
video_writer = cv2.VideoWriter(video_path1,cv2.VideoWriter_fourcc(*'mp4v'),fps,(width,height))
while rval:
rval,frame = vc.read()# 讀取視頻幀
img = coonvert_jpg(Image.fromarray(frame))
frame_converted = np.array(img)
# 轉(zhuǎn)化為三通道
image = np.expand_dims(frame_converted,axis = 2)
result_arr = np.concatenate((image,image,image),axis = -1)
video_writer.write(result_arr)
print('Sucessfully Conveted---------{}'.format(c))
c = c + 1
if c >= 3000:
break
video_writer.release()
在圖片序列提取時,需要注意一點(diǎn),因?yàn)檗D(zhuǎn)化后的圖片是單通道的,直接借助 OpenCV 生成視頻序列是無法播放的,需增加一個步驟單通道轉(zhuǎn)化為三通道!
# 轉(zhuǎn)化為三通道
image = np.expand_dims(frame_converted,axis = 2)
result_arr = np.concatenate((image,image,image),axis = -1)
想讓生成的視頻更有感覺的話可以添加一個背影音樂,借助剪輯軟件、Python 都可!
數(shù)據(jù)源碼獲取
文中涉及到的源碼大部分其實(shí)都已經(jīng)貼在文章,但為了方便起見,我已經(jīng)將數(shù)據(jù)和源碼整合在一起,想獲取的同學(xué)可以,在公號后臺回復(fù)關(guān)鍵字:210325 即可!
小結(jié)
本文主要介紹了如何用 Python將一張圖片轉(zhuǎn)化為手繪風(fēng)格,代碼量很少但涉及知識領(lǐng)域與數(shù)學(xué)、物理相關(guān),所以不容易理解,本篇文章目的只是為了向大家介紹圖片手繪風(fēng)轉(zhuǎn)換有這么一種方法,當(dāng)然如果有感興趣的小伙伴可以深究一下
好了以上就是本篇文章的全部內(nèi)容了,最后感謝大家的閱讀,我們下期見~

