引言
在數位修復與影像處理領域,將歷史黑白影片轉為彩色不僅能賦予新生命,也能帶給觀眾更直觀的視覺體驗。DeOldify 是目前最受歡迎的開源工具之一,採用深度學習模型來對影像自動上色。本篇將分享我在 WSL Ubuntu 20.04 環境中,結合 Miniconda 與 NVIDIA CUDA,裝好 DeOldify 並實際為影片著色的完整流程。
過程中其實充滿了艱辛,連 github 中的源碼庫都不提供直接的 cli 指令,好在有 chatgpt 的幫忙,再嘗試了無數次後終於可以用了。
當然,這篇文章也是以 chatgpt 產生的內容為主,我適當修改一下,現在應該不會再重到到尾寫文章了吧…Orz
環境與需求
本文章是在下列環境下完成的,請先準備好
作業系統:WSL Ubuntu 20.04
Python 環境管理:Miniconda
硬體:具備 CUDA 支援之 NVIDIA GPU
安裝流程
安裝步驟如下:我的硬體是 RTX 4070, NVIDIA-SMI 570.124.03 Driver Version: 572.60 CUDA Version: 12.8. 主是 CUDA 12.8 這個要裝一樣,可能有相容性問題。
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 |
# 0. 建立 docker container docker run --name deoldify -it -p 12222:22 --gpus all -v d:\docker_shared:/mnt nvcr.io/nvidia/cuda:12.8.0-cudnn-devel-ubuntu24.04 # 1. 建立並啟動 Conda 環境 cd /bin rm sh ln -s bash sh conda create -n deold python=3.9 -y conda activate deold # 2. 移除可能舊版衝突套件 pip uninstall -y deoldify ffmpeg ffmpeg-python apt-get update apt-get install sudo git wget vim # 3. 安裝 PyTorch(含 CUDA 支援) conda install -y -c pytorch pytorch=2.0.1 \ torchvision=0.15.2 torchaudio=2.0.2 \ cudatoolkit=11.8 # 4. 安裝其他相依套件 pip install fastai==2.7.9 fastcore ffmpeg-python \ moviepy numpy==1.23.5 # 5. 取得 DeOldify 原始碼與模型檔 cd git clone https://github.com/jantic/DeOldify.git cd DeOldify echo "conda activate deold" > env mkdir -p models wget -O models/ColorizeArtistic_gen.pth \ https://data.deepai.org/deoldify/ColorizeArtistic_gen.pth wget -O models/ColorizeVideo_gen.pth \ https://data.deepai.org/deoldify/ColorizeVideo_gen.pth # 6. 安裝影像處理與下載支援 pip install opencv-python>=4.2.0.32 \ Pillow==9.3.0 yt_dlp jupyterlab ipywidgets apt-get install sudo sudo apt install software-properties-common sudo apt-get update sudo apt-get install libgl1 wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz tar Jvxf ffmpeg-release-amd64-static.tar.xz sudo mv ffmpeg-7.0.2-amd64-static/ffmpeg /usr/bin/ffmpeg |
這樣就完成了 DeOldify 執行所需的所有環境配置。
轉換程式:deoldify_cli_4.py 解說
將下列程式與專案目錄,放在一起,並命名為 deoldify_cli_4.py
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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
#!/usr/bin/env python3 import os import sys import shutil import subprocess import warnings from PIL import Image # ──① 本地源码优先── SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, SCRIPT_DIR) # ──参数── INPUT_VIDEO = 'blackwhite.mp4' # 原始黑白影片 FIRST_MINUTE = 'first_minute.mp4' # 前1分钟测试(保留音轨) TMP_RAW = 'tmp_raw_frames' # 拆出的原始帧目录(BMP) TMP_COMBO = 'tmp_combo_frames' # 合并后供上色的帧目录(BMP) TMP_COLOR = 'tmp_color_frames' # AI 上色后帧目录(BMP) NO_AUDIO_VIDEO = 'colorized_video.mp4' # 无声彩色视频 FINAL_OUTPUT = 'colorized_with_audio.mp4' # 带音轨的最终版 RENDER_FACTOR = 32 FPS = 24 # 拆帧/合帧帧率 QUEUE_SIZE = 8 # 队列深度 FULL_PROCESS = False warnings.filterwarnings("ignore", message="Your training set is empty") warnings.filterwarnings("ignore", message="Your validation set is empty") def pre_merge(raw_dir, combo_dir, merge_q): """合并每组 4 帧为 2×2 大图,推入 merge_q""" frames = sorted(os.listdir(raw_dir)) i = 0 total = len(frames) while i < total: group = frames[i:i+4] imgs = [Image.open(os.path.join(raw_dir, f)) for f in group] w, h = imgs[0].size combo = Image.new('RGB', (w*2, h*2)) positions = [(0,0), (w,0), (0,h), (w,h)] for im, pos in zip(imgs, positions): combo.paste(im, pos) combo_fname = f'combo_{i:06d}.bmp' combo_path = os.path.join(combo_dir, combo_fname) combo.save(combo_path) merge_q.put((group, combo_path)) i += 4 print(f"Progress: {i}/{total}") merge_q.put(None) # Sentinel def gpu_worker(merge_q, split_q): """持续从 merge_q 取合并大图,GPU 着色后推入 split_q""" # ——设 GPU—— from deoldify._device import _Device from deoldify.device_id import DeviceId device = _Device(); device.set(DeviceId.GPU0) print("GPU worker using", device.current()) # ——载入 colorizer—— from deoldify.visualize import get_image_colorizer colorizer = get_image_colorizer(artistic=True) # 把内部模型搬到 CUDA master = getattr(colorizer, 'filter', None) if master and hasattr(master, 'filters'): import torch for filt in master.filters: learn = getattr(filt, 'learn', None) if learn and hasattr(learn, 'model'): learn.model.cuda() setattr(filt, 'device', torch.device('cuda:0')) # ——处理循环—— while True: item = merge_q.get() if item is None: break group, combo_path = item img_col = colorizer.get_transformed_image( path=combo_path, render_factor=RENDER_FACTOR, watermarked=False ) split_q.put((group, img_col)) split_q.put(None) def splitter(split_q, color_dir): """持续从 split_q 取着色大图,拆回 4 帧并存盘""" while True: item = split_q.get() if item is None: break group, img = item w, h = img.size crops = [ img.crop((0, 0, w//2, h//2)), img.crop((w//2, 0, w, h//2)), img.crop((0, h//2, w//2, h)), img.crop((w//2, h//2, w, h)), ] for fname, crop in zip(group, crops): crop.save(os.path.join(color_dir, fname)) if __name__ == '__main__': import multiprocessing as mp mp.set_start_method('fork') # ——1. 截取前1分钟(保留音轨)—— if os.path.exists(FIRST_MINUTE): os.remove(FIRST_MINUTE) sec = '999999' if FULL_PROCESS else '60' subprocess.run([ 'ffmpeg', '-y', '-i', INPUT_VIDEO, '-ss', '0', '-t', sec, '-c', 'copy', FIRST_MINUTE ], check=True) # ——2. 准备目录—— for d in (TMP_RAW, TMP_COMBO, TMP_COLOR): if os.path.isdir(d): shutil.rmtree(d) os.makedirs(d) # ——3. 拆帧为 BMP—— subprocess.run([ 'ffmpeg', '-y', '-i', FIRST_MINUTE, '-vsync', 'vfr', os.path.join(TMP_RAW, 'frame_%06d.bmp') ], check=True) # ——4. 启动 3 个进程:合并→GPU→拆分—— merge_q = mp.Queue(maxsize=QUEUE_SIZE) split_q = mp.Queue(maxsize=QUEUE_SIZE) p1 = mp.Process(target=pre_merge, args=(TMP_RAW, TMP_COMBO, merge_q)) p2 = mp.Process(target=gpu_worker, args=(merge_q, split_q)) p3 = mp.Process(target=splitter, args=(split_q, TMP_COLOR)) p1.start(); p2.start(); p3.start() p1.join(); p2.join(); p3.join() # ——5. 合成无声视频—— if os.path.exists(NO_AUDIO_VIDEO): os.remove(NO_AUDIO_VIDEO) subprocess.run([ 'ffmpeg', '-y', '-framerate', str(FPS), '-i', os.path.join(TMP_COLOR, 'frame_%06d.bmp'), '-c:v', 'mpeg4', '-pix_fmt', 'yuv420p', NO_AUDIO_VIDEO ], check=True) print("Video-only done →", NO_AUDIO_VIDEO) # ——6. 合并音轨—— if os.path.exists(FINAL_OUTPUT): os.remove(FINAL_OUTPUT) subprocess.run([ 'ffmpeg', '-y', '-i', NO_AUDIO_VIDEO, '-i', FIRST_MINUTE, '-map', '0:v:0', '-map', '1:a:0', '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', FINAL_OUTPUT ], check=True) print("Done →", FINAL_OUTPUT) |
為了自動化「拆幀→合併上色→拆分→合成影片」的流程,我撰寫了 deoldify_cli_4.py,其主要架構如下:
截取前 1 分鐘測試
使用 ffmpeg 保留音軌,快速檢視上色效果。
拆幀
將影片拆為 BMP 圖片序列,以保證高品質。
Pre-merge
每 4 張 BMP 合併為 2×2 大圖,提高 GPU 批次處理效率。
GPU 着色流程
多進程併行:
pre_merge:合併並推到 merge_q
gpu_worker:從 merge_q 取出大圖,透過 DeOldify 模型上色,結果推到 split_q
splitter:從 split_q 拆分回 4 張著色後的 BMP
重組影片
合成無聲彩色视频 (colorized_video.mp4)
與原音軌合併,輸出最終檔 (colorized_with_audio.mp4)
這邊的原始檔需固定檔名為 blackwhite.mp4, 若有需要請自行修改。若用 CPU 做上色,可能需要多幾十倍的時間,明顯的不可行,可以多問問 chatgpt 原音。