有一批直式照片想變成橫式桌布,左右兩側的黑色空白感覺很突兀,
有沒有辦法讓它自然地「淡入」黑色?
執行的結果, 資料夾下的圖片都換成 1920×1080 的大小:

這篇文章分享一套兩支 Python 腳本的解決方案,全程自動化,支援批次處理整個資料夾。
效果示意
原始直式照片 → 加黑色背景
┌──────────┐ ┌────────────────────┐
│ │ │░░░│ │░░░░░░░│
│ Photo │ → │░░░│ Photo │░░░░░░░│
│ 1440x1920│ │░░░│ │░░░░░░░│
└──────────┘ └────────────────────┘
(直式 9:16) (橫式 16:9,兩側黑邊)
前置需求
Python 3.8+ 與以下套件:
pip install opencv-python numpy
腳本架構
整個方案分成兩支獨立腳本,採鬆耦合設計:
make_wallpaper.py dissolve.py
│ │
① 讀取來源圖片 ④ 偵測左右黑邊
② 貼到黑色畫布 ⑤ 套用邊緣漸層特效
③ subprocess 呼叫 ──────▶⑥ 輸出到 output/
⑦ 複製結果到 wallpaper/
[!NOTE]
dissolve.py透過subprocess呼叫,兩支腳本可獨立維護與升級,不互相依賴。
腳本說明
dissolve.py — 邊緣漸層特效
核心概念:自動偵測圖片左右兩側的「黑色邊緣」,並對邊界內縮一段距離套用漸層特效。
支援三種特效類型:
| 特效 | 說明 | 效果 |
|---|---|---|
fade | 透明度線性遞減 | 邊緣漸漸淡入黑色,最自然 |
blur | 高斯模糊漸層融合 | 邊緣有朦朧感 |
dissolve | 隨機像素溶解 | 邊緣有顆粒噪點感 |
重要限制:dissolve.py 設計上假設圖片本身已存在黑邊(例如信箱格式的影片截圖)。若圖片無黑邊(左右邊緣直接是畫面內容),腳本不會套用任何特效。
make_wallpaper.py — 主控腳本
解決上述限制的關鍵。它會:
- 把每張照片縮放後置中貼到黑色背景畫布(預設 1920×1080)
- 存入暫存目錄
- 呼叫
dissolve.py對暫存目錄批次處理 - 把結果複製到
<cwd>/wallpaper/資料夾
貼到黑色背景後,原本「沒有黑邊」的問題自動消失,dissolve.py 的邊緣偵測邏輯就能正確作用。
快速上手
1. 準備目錄結構
把照片放入一個資料夾,兩支腳本放在同一層:
leejueun/
├── dissolve.py
├── make_wallpaper.py
└── original/
├── 01-photo.jpg
├── 02-photo.jpg
└── 03-photo.jpg
2. 執行指令
# 基本用法(使用所有預設值)
python make_wallpaper.py --cwd original
執行後輸出在 original/wallpaper/:
original/
├── wallpaper/
│ ├── 01-photo.jpg ✅ 1920x1080,含 fade 效果
│ ├── 02-photo.jpg
│ └── 03-photo.jpg
└── ...(原始照片不動)
3. 完整參數說明
python make_wallpaper.py [選項]
| 參數 | 預設值 | 說明 |
|---|---|---|
--cwd | .(目前目錄) | 來源圖片的資料夾路徑 |
--canvas | 1920x1080 | 輸出畫布尺寸,格式 WxH |
--effect | fade | 特效類型:fade / blur / dissolve |
--width | 60 | 漸層特效寬度(像素) |
--dissolve | 自動偵測 | 若 dissolve.py 在其他路徑時指定 |
4. 更多範例
# 製作 2K 桌布,漸層寬度加大到 100px
python make_wallpaper.py --cwd original --canvas 2560x1440 --width 100
# 改用模糊特效
python make_wallpaper.py --cwd original --effect blur --width 80
# 指定來源目錄(絕對路徑)
python make_wallpaper.py --cwd "C:\Photos\vacation"
# 兩支腳本不在同個資料夾時
python make_wallpaper.py --cwd original --dissolve "C:\tools\dissolve.py"
單獨使用 dissolve.py
如果你的圖片本身就有黑邊(例如電影截圖、4:3 老照片),可以直接用 dissolve.py:
python dissolve.py --cwd original --effect fade --width 30
輸出在 original/output/。
設計筆記:為什麼選擇 subprocess?
在寫 make_wallpaper.py 時評估了兩種整合方式:
方案 A:import dissolve(直接 import)
# 緊耦合,dissolve.py 更新後需同步確認
import dissolve
dissolve.add_edge_effect(path, width, effect)
方案 B:subprocess(選用)
subprocess.run([sys.executable, 'dissolve.py', '--cwd', tmp_dir, ...])
選擇 subprocess 的理由:
dissolve.py未來可以換成任何語言改寫,只要 CLI 介面不變就能繼續使用- 兩支腳本的 Python 環境、相依套件版本可以不同
- 錯誤隔離:
dissolve.py崩潰不會影響主程式的清理流程(tempfile仍會被刪除) - 測試容易:每支腳本都能獨立單元測試
[!TIP]
若有效能需求(大量圖片需要平行處理),可改用concurrent.futures.ProcessPoolExecutor直接呼叫函式,但需要將dissolve.py的核心邏輯抽成模組。
常見問題
Q: 執行後輸出圖片和原圖看起來一樣,沒有漸層效果?
原因是直接對「沒有黑邊」的圖執行 dissolve.py。請改用 make_wallpaper.py,它會先幫圖片加上黑色背景,再呼叫 dissolve.py。
Q: 圖片被拉伸了?
make_wallpaper.py 預設使用 cv2.INTER_LANCZOS4 高品質縮放並維持長寬比,圖片會置中,不會拉伸,只是兩側(或上下)會有黑邊。
Q: 支援 PNG 嗎?
目前兩支腳本只搜尋 *.jpg。若需要 PNG 支援,可修改 glob.glob 的 pattern:
# 在 dissolve.py 與 make_wallpaper.py 中修改這行
files = glob.glob(os.path.join(target_dir, '*.jpg'))
# 改成支援多格式
import itertools
patterns = ['*.jpg', '*.jpeg', '*.png']
files = list(itertools.chain.from_iterable(
glob.glob(os.path.join(target_dir, p)) for p in patterns
))
完整原始碼
| 檔案 | 功能 |
|---|---|
dissolve.py | 偵測黑邊並套用邊緣漸層特效 |
make_wallpaper.py | 加黑色背景 + 呼叫 dissolve.py 的主控腳本 |
make_wallpaper.py
"""
make_wallpaper.py
-----------------
1. 讀取 --cwd 目錄下所有 jpg 圖片
2. 以黑色背景畫布置中貼上(預設 1920x1080)
3. 透過 subprocess 呼叫 dissolve.py 對畫布套用 fade 效果
4. 最終輸出至 <cwd>/wallpaper/
用法範例:
python make_wallpaper.py --cwd original
python make_wallpaper.py --cwd original --width 60 --effect fade
python make_wallpaper.py --cwd original --canvas 2560x1440
"""
import argparse
import glob
import os
import shutil
import subprocess
import sys
import tempfile
import cv2
import numpy as np
# ---------------------------------------------------------------------------
# Step 1 — 建立黑色畫布並置中貼圖
# ---------------------------------------------------------------------------
def place_on_black_canvas(image_path: str, output_path: str, canvas_w: int, canvas_h: int) -> bool:
"""
將圖片垂直置中貼到 canvas_w x canvas_h 黑色畫布上,存至 output_path。
回傳是否成功。
"""
img = cv2.imread(image_path)
if img is None:
print(f'[警告] 無法讀取圖片:{image_path},跳過。')
return False
h, w = img.shape[:2]
# 計算縮放比例,讓圖片完整放入畫布(維持長寬比)
scale = min(canvas_w / w, canvas_h / h)
new_w = int(w * scale)
new_h = int(h * scale)
resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
canvas = np.zeros((canvas_h, canvas_w, 3), dtype=np.uint8)
# 垂直置中、水平置中
x_off = (canvas_w - new_w) // 2
y_off = (canvas_h - new_h) // 2
canvas[y_off:y_off + new_h, x_off:x_off + new_w] = resized
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# 以 PNG 無損格式儲存暫存圖,避免 JPEG 量化誤差在 pipeline 中累積
cv2.imwrite(output_path, canvas)
return True
# ---------------------------------------------------------------------------
# Step 2 — 以 subprocess 呼叫 dissolve.py(完全解耦)
# ---------------------------------------------------------------------------
def run_dissolve(dissolve_script: str, cwd_dir: str, effect: str, width: int, ease: float) -> bool:
"""
以 subprocess 呼叫 dissolve.py,對 cwd_dir 目錄下的圖片套用特效。
輸出會存至 cwd_dir/output/。
"""
cmd = [
sys.executable,
dissolve_script,
'--cwd', cwd_dir,
'--effect', effect,
'--width', str(width),
'--ease', str(ease),
]
print(f'[呼叫] {" ".join(cmd)}')
result = subprocess.run(cmd, capture_output=False)
return result.returncode == 0
# ---------------------------------------------------------------------------
# 主程式
# ---------------------------------------------------------------------------
def parse_canvas(value: str):
"""解析 '1920x1080' 格式,回傳 (width, height)。"""
try:
w, h = value.lower().split('x')
return int(w), int(h)
except Exception:
raise argparse.ArgumentTypeError(f'canvas 格式錯誤,請使用 WxH,例如 1920x1080,收到:{value}')
def main():
parser = argparse.ArgumentParser(
description='替圖片加上黑色背景,再套用 dissolve.py 的邊緣漸層特效,輸出桌布圖。'
)
parser.add_argument(
'--cwd', type=str, default='.',
help='來源圖片目錄,預設為目前資料夾。'
)
parser.add_argument(
'--canvas', type=parse_canvas, default='1920x1080',
metavar='WxH',
help='輸出畫布尺寸,預設 1920x1080。'
)
parser.add_argument(
'--effect', type=str, default='fade',
choices=['dissolve', 'blur', 'fade'],
help='邊緣漸層特效類型,預設 fade。'
)
parser.add_argument(
'--width', type=int, default=40,
help='漸層特效寬度(像素),預設 40。'
)
parser.add_argument(
'--dissolve', type=str, default=None,
metavar='PATH',
help='dissolve.py 的路徑,預設自動於同目錄尋找。'
)
parser.add_argument(
'--ease', type=float, default=2.0,
help='fade 曲線指數(僅 fade 有效)。1.0=線性,2.0=二次方(預設)。傳遞至 dissolve.py。'
)
args = parser.parse_args()
# --- 路徑解析 ---
source_dir = args.cwd
if not os.path.isdir(source_dir):
print(f'錯誤:找不到目錄 {source_dir}')
sys.exit(1)
canvas_w, canvas_h = args.canvas
# 自動尋找 dissolve.py(同腳本目錄)
dissolve_script = args.dissolve
if dissolve_script is None:
dissolve_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dissolve.py')
if not os.path.isfile(dissolve_script):
print(f'錯誤:找不到 dissolve.py,請用 --dissolve 指定路徑。(找尋位置:{dissolve_script})')
sys.exit(1)
# 收集來源圖片(jpg / jpeg)
jpg_files = sorted(glob.glob(os.path.join(source_dir, '*.jpg')))
if not jpg_files:
print('在指定目錄中找不到任何 jpg 圖片。')
sys.exit(0)
print(f'找到 {len(jpg_files)} 張圖片,畫布尺寸 {canvas_w}x{canvas_h},特效:{args.effect}(寬度 {args.width}px)')
# --- 暫存目錄(貼黑色背景後的中間結果) ---
tmp_dir = tempfile.mkdtemp(prefix='make_wallpaper_')
try:
# Step 1:貼黑色背景,暫存為 PNG(無損,避免 JPEG 量化誤差累積)
processed = 0
for src in jpg_files:
# 暫存副檔名改為 .png
fname_png = os.path.splitext(os.path.basename(src))[0] + '.png'
dst = os.path.join(tmp_dir, fname_png)
if place_on_black_canvas(src, dst, canvas_w, canvas_h):
processed += 1
if processed == 0:
print('沒有任何圖片成功處理,結束。')
return
print(f'[步驟 1/2] 已建立黑色背景圖片:{processed} 張(暫存至 {tmp_dir})')
# Step 2:呼叫 dissolve.py 套用特效
ok = run_dissolve(dissolve_script, tmp_dir, args.effect, args.width, args.ease)
if not ok:
print('錯誤:dissolve.py 執行失敗。')
sys.exit(1)
# Step 3:將 dissolve 輸出(tmp_dir/output/)轉換為 JPG 並複製到 <source_dir>/wallpaper/
dissolve_output_dir = os.path.join(tmp_dir, 'output')
final_output_dir = os.path.join(source_dir, 'wallpaper')
os.makedirs(final_output_dir, exist_ok=True)
# dissolve.py 輸出的副檔名與輸入一致(這裡是 .png)
result_files = sorted(glob.glob(os.path.join(dissolve_output_dir, '*.png')))
for f in result_files:
# 最終輸出改回 .jpg
basename_jpg = os.path.splitext(os.path.basename(f))[0] + '.jpg'
dst = os.path.join(final_output_dir, basename_jpg)
# 以高品質 JPEG 儲存(只在最後輸出時壓縮一次)
img_final = cv2.imread(f)
cv2.imwrite(dst, img_final, [cv2.IMWRITE_JPEG_QUALITY, 95])
print(f'[完成] {dst}')
print(f'\n[步驟 2/2] 全部完成!輸出目錄:{final_output_dir}')
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
if __name__ == '__main__':
main()
dissolve.py 參考:
https://stackoverflow.max-everyday.com/2026/10/python-cv-effect-fade/