文章目的
筆者的公司最近要開發一個簡單的 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
在虛擬環境中我們就可以安裝專案所需套件等,而不用擔心全域環境造成的影響。
MoviePy
影片處理在本專案是最主要的需求,主要會需要的影片處理為:
- 影片壓字
- 影片與聲音合成
- 影片與圖片合成
經過搜尋後發現 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)
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
| 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")
filename = secure_filename("img_" + ts + ".png") upload_img.save(filename)
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')
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))
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))
|
可以看到上面有三個變數,原因是影片中圖片會需要三個特效:
- 掉落
- 放大
- 定位
三種特效是接力出現,所以我將圖片個別針對每個特效存成三種,並控制出現時間,再做串連。
MoviePy - 影片輸出
這邊比較單純,我們直接看程式碼:
1 2 3 4 5
| 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,如何儲存、如何取得資料都會在下篇提到。
這是我第一次處理後端的事情,過程中遇到了很多瓶頸,但我真的學到了很多,對後端也有更深的了解,在此做個紀錄,也希望此篇記錄能幫到更多的人:)