自動幫照片加黑色背景與邊緣漸層,製作質感桌布

有一批直式照片想變成橫式桌布,左右兩側的黑色空白感覺很突兀,
有沒有辦法讓它自然地「淡入」黑色?

執行的結果, 資料夾下的圖片都換成 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 — 主控腳本

解決上述限制的關鍵。它會:

  1. 把每張照片縮放後置中貼到黑色背景畫布(預設 1920×1080)
  2. 存入暫存目錄
  3. 呼叫 dissolve.py 對暫存目錄批次處理
  4. 把結果複製到 <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.(目前目錄)來源圖片的資料夾路徑
--canvas1920x1080輸出畫布尺寸,格式 WxH
--effectfade特效類型:fade / blur / dissolve
--width60漸層特效寬度(像素)
--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/

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *