0%

Python - 利用 MoviePy 處理影片

文章目的

筆者的公司最近要開發一個簡單的 side-project,在此專案中筆者負責專案後端的開發,雖然我是前端工程師XD。

主要會想來開發後端,想說利用這個機會練習,並且更了解後端的技術。

我預計會寫兩篇相關的文章,分別是 影片處理api 開發

專案目的

此專案是為了因應過年製作賀卡,將使用者在前台填入的資訊,在後端處理轉換成賀卡影片,並提供 share-link 供使用者分享。

讓你們看看成品示意XD

語言選擇

後端語言百百種,為什麼想要用 Python 開發呢?

主要原因是 Python 相對其他後端語言是較為好上手的,另外有找到相關的 Python 套件可以方便處理影片問題。

最後就是因為公司的後端工程師主要語言也是 Python,方便我問問題XD。

環境建置

在開發上,建議利用 virtualenv 開發,python3 後面的版本已經有內建的 venv 功能,所以就直接拿它來創建 virtualenv 吧!

  • 創建環境: python3 -m venv 環境名稱
  • 啟動環境(Windows): 環境名稱\Scripts\activate.bat
  • 啟動環境(Unix、MacOS):source 環境名稱/bin/activate

啟動成功會看到 shell 前面有環境名稱,像是這樣:

(環境名稱) $ python

在虛擬環境中我們就可以安裝專案所需套件等,而不用擔心全域環境造成的影響。

  • 退出環境: deactivate

MoviePy

影片處理在本專案是最主要的需求,主要會需要的影片處理為:

  1. 影片壓字
  2. 影片與聲音合成
  3. 影片與圖片合成

經過搜尋後發現 MoviePy 是個好選擇,它提供了強大的影片後製能力,讓開發上僅需少少的程式碼就能達到專案需求。

接下來的文章將會針對上述三點做紀錄。

MoviePy - 壓字處理

在 side-project 中,需要將使用者輸入的祝福語壓在影片上產出。
這部分因為 ui 關係所以有所限制,分別是中或英文 4 字,每個文字會漸進出現。

在處理文字需求時,我想到的做法是將從 api 收到的文字內容逐一取出,後製到影片中,並且控制文字出現時間。

首先,要來驗證收到的文字內容是否為英文或是中文:

透過 pip3 安裝 langdetect pip3 install langdetect

1
2
3
4
5
6
7
8
9
from langdetect import detect

# 用 language 儲存 detect 驗出的語言, blessing 是 api 傳回的文字內容
language = detect(blessing)

# word_one 是 blessing 的第一個字
# 產生一個 font-size 是 50px,字體為宋黑的文字,出現時間為 3 秒,從影片第 4 秒出現,位置在影片的 x = 100、y = 113。
if language == "zh-cn" or language == "zh-tw":
txt_clip1 = TextClip(word_one, fontsize=50, color='white', font="SourceHanSerifTC-Bold.otf").set_duration(3).set_start(4).set_position((100, 113))

MoviePy 透過 TextClip 改變文字的屬性,並且壓在影片指定位置以及出現時間。

這邊有一點要注意是,MoviePy 利用 ImageMagick 來處理文字效果,因此在使用這功能前請確保環境中已經有裝此套件。

文字的字體若要改變,就要像程式碼中的 font 一樣引用對應的文字 otf 檔案。

MoviePy - 聲音合成

製作出來的卡片為了要有天竺鼠的叫聲,因此我們需要把聲音後製上去。

整支影片分了三種聲音,分別為掉落聲、過場聲、跟隨文字出現的提示音。

每一種聲音皆會在影片的不同時間點出現。

來看看程式碼:

1
2
3
4
5
6
# 三個 url 代表 三種聲音來源
audio_fall = AudioFileClip(fall_url)
audio_walk = AudioFileClip(walk_url).set_start(1)
audio_new_year = AudioFileClip(new_year_url).set_start(4)
new_audio = CompositeAudioClip([audio_fall, audio_walk, audio_new_year])
new_sound_clip = video_clip.set_audio(new_audio)

先透過 AudioFileClip 設定每種聲音開始的影片對應時間點。

接著,用 CompositeAudioClip 將三種聲音合成一個新的聲音片段。

最後,透過 set_audio 將聲音合進我們的影片中。

MoviePy - 圖片合成

在專案中,我們提供使用者自行上傳圖片的功能,所以我們在接收到前台 post 給我們的資料時要先去檢查是否有上傳圖片,如果有上傳圖片,我們就針對上傳的圖片來做處理。

來看看程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 取得圖片
upload_img = request.files.to_dict().get("upload")
# 為了讓收到的圖片統一轉換成統一格式的檔名,ts 為當下的 timestamp
filename = secure_filename("img_" + ts + ".png")
upload_img.save(filename)

# 因為 iphone 上傳的圖片檔,有些副檔名為 'heic',故利用此方法作轉換
if "heic" in upload_img.headers["Content-Type"]:
heif_file = pyheif.read(filename)
transform_image = Image.frombytes(
heif_file.mode,
heif_file.size,
heif_file.data,
"raw",
heif_file.mode,
heif_file.stride,
)
transform_image.save("img_" + ts + ".png", 'PNG')

# 用 cv2 讀取圖檔,為了針對圖片做裁切處理
img = cv2.imread(filename)
cv2.imwrite(filename, img)
im = Image.open(filename)
rgb_im = im.convert('RGB')
thumb_width = 200

# 此函式用來裁切圖片正中央
def crop_center(pil_img, crop_width, crop_height):
img_width, img_height = pil_img.size
return pil_img.crop(((img_width - crop_width) // 2,
(img_height - crop_height) // 2,
(img_width + crop_width) // 2,
(img_height + crop_height) // 2))

# 回傳 crop_center 完成的結果,基本上,第 2、第 3 個參數傳入我們前面的 thumb_width
def crop_max_square(pil_img):
return crop_center(pil_img, min(pil_img.size), min(pil_img.size))

# 將裁好的正方形圖片轉換成圓形
def mask_circle_transparent(pil_img, blur_radius, offset=0):
offset = blur_radius * 2 + offset
mask = Image.new("L", pil_img.size, 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((offset, offset, pil_img.size[0] - offset, pil_img.size[1] - offset), fill=255)
mask = mask.filter(ImageFilter.GaussianBlur(blur_radius))

result = pil_img.copy()
result.putalpha(mask)

return result

im_square = crop_max_square(rgb_im).resize((thumb_width, thumb_width), Image.LANCZOS)
im_thumb = mask_circle_transparent(im_square, 1)
im_title = "img_" + ts + ".png"

# 將處理好的圖片儲存起來
im_thumb.save(im_title)

完成了上述的圖片處理後,接著又要回到我們的 MoviePy,來幫我們把處理好的圖片合成進影片中。

來看看程式碼:

1
2
3
4
5
6
7
8
img_falling = (ImageClip(im_title)).set_duration(video_clip.duration)\
.set_position(lambda t: ('center', -200 + 640 * np.sin(t))).set_duration(2).resize(height=200)

img_scale = (ImageClip(im_title)).set_duration(1.5).resize(height=200).resize(lambda t: 1 + 0.8 * t)\
.set_start(2).set_position(lambda t: ('center', 340 - 100 * np.cos(t + 400)))

img_final = (ImageClip(im_title)).set_duration(2.5).resize(height=415).set_start(3.5)\
.set_position(("center", 280))

可以看到上面有三個變數,原因是影片中圖片會需要三個特效:

  1. 掉落
  2. 放大
  3. 定位

三種特效是接力出現,所以我將圖片個別針對每個特效存成三種,並控制出現時間,再做串連。

MoviePy - 影片輸出

這邊比較單純,我們直接看程式碼:

1
2
3
4
5
# 如果沒有上傳圖片,就不需要 img_falling, img_scale, img_final
video_result = CompositeVideoClip(
[new_sound_clip, txt_clip1, txt_clip2, txt_clip3, txt_clip4, img_falling, img_scale, img_final])
video_name = ts + ".mp4"
video_result.write_videofile(video_name, audio_codec="aac")

到了這邊,我們就能看到完成的 mp4 影片檔囉!

後記

在下一篇有關 api 的部分,會提到如何將影片製作功能上到 api 並且作部署,另外我們影片資料會存進 google cloud storage,如何儲存、如何取得資料都會在下篇提到。

這是我第一次處理後端的事情,過程中遇到了很多瓶頸,但我真的學到了很多,對後端也有更深的了解,在此做個紀錄,也希望此篇記錄能幫到更多的人:)