自動套用溶解特效在圖片邊緣

在網路上下載了很照片, 想放到電視當背景, 網路上很多都是直立式圖片, 但電視是橫的, 所以想自動把直的套用溶解特效:

確認效果OK 之後, 使用下列的提示詞取得自動轉換用的腳本:

give me a python script to do it , auto detect 這種左右側背景都是黑色的圖片, 生成左右側溶解特效. 
* python script 可以透過 args 指定 width, width default value=20 (px).
* default scan current folder *.jpg
* able to assign --cwd to change currect workding dir

增加特效參數的 Python 腳本

這是一支擴充後的程式碼。我們新增了 --effect 參數,讓你可以自由切換不同的邊緣處理特效,包含預設的溶解,以及模糊與透明度漸層,達到讓圖片邊緣不那麼明顯的目的。

功能特色

  • 支援三種邊緣特效:溶解、模糊、透明度漸層。
  • 可透過參數自由切換特效類型與特效寬度。
  • 自動偵測左右黑邊並套用特效。

程式碼

請將 dissolve.py 更新為以下內容:

Python

import argparse
import glob
import os
import cv2
import numpy as np


def add_edge_effect(image_path, width, effect_type, ease=2.0):
  img = cv2.imread(image_path)
  if img is None:
    return

  h, w, c = img.shape
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

  col_max = np.max(gray, axis=0)
  black_threshold = 10
  # 額外往黑色區域內縮的 pixel 數,確保 fade 從純黑出發,
  # 避免 black_threshold 漏掉的暗色邊緣像素形成亮線。
  border_padding = 3

  left_border = 0
  for x in range(w // 2):
    if col_max[x] > black_threshold:
      # 往左退 border_padding,但不超出純黑區域(col_max == 0 的最後一個位置)
      left_border = max(0, x - border_padding)
      break

  right_border = w - 1
  for x in range(w - 1, w // 2, -1):
    if col_max[x] > black_threshold:
      right_border = x
      break

  right_border = min(w, right_border + 1 + border_padding)
  if right_border > w:
    right_border = w

  output = img.copy()

  if effect_type == 'dissolve':
    np.random.seed(42)
    if left_border > 0:
      start_x = left_border
      end_x = min(w, left_border + width)
      for y in range(h):
        for x in range(start_x, end_x):
          prob = (x - start_x) / width
          if np.random.rand() > prob:
            output[y, x] = [0, 0, 0]

    if right_border < w:
      start_x = max(0, right_border - width)
      end_x = right_border
      for y in range(h):
        for x in range(start_x, end_x):
          prob = (end_x - x) / width
          if np.random.rand() > prob:
            output[y, x] = [0, 0, 0]

  elif effect_type == 'blur':
    # 對整張圖進行模糊,然後利用遮罩將邊緣與原圖融合
    blurred = cv2.GaussianBlur(img, (width * 2 + 1, width * 2 + 1), 0)
    if left_border > 0:
      start_x = left_border
      end_x = min(w, left_border + width)
      for x in range(start_x, end_x):
        alpha = (x - start_x) / width
        output[:, x] = (
            img[:, x] * alpha + blurred[:, x] * (1 - alpha)
        ).astype(np.uint8)

    if right_border < w:
      start_x = max(0, right_border - width)
      end_x = right_border
      for x in range(start_x, end_x):
        alpha = (end_x - x) / width
        output[:, x] = (
            img[:, x] * alpha + blurred[:, x] * (1 - alpha)
        ).astype(np.uint8)

  elif effect_type == 'fade':
    # t 為正規化位置 (0.0 ~ 1.0):
    #   左側:t=0 → 外側(黑色),t=1 → 內側(內容)
    #   右側:t=0 → 內側(內容),t=1 → 外側(黑色)
    #
    # alpha 曲線:alpha = 1 - t^ease
    #   ease=1 → 線性等差(舊行為)
    #   ease=2 → 二次方,靠近內側(t=0)斜率趨近 0,初期幾乎不變暗
    #   ease 越大 → 越晚才開始大幅變暗,更接近「突然消失」
    #
    # 右側示意(ease=2):
    #   t:     0.0   0.3   0.5   0.7   1.0
    #   alpha: 1.00  0.91  0.75  0.51  0.00
    #   vs 線性: 1.00  0.70  0.50  0.30  0.00

    if left_border > 0:
      start_x = left_border
      end_x = min(w, left_border + width) + 1
      for x in range(start_x, end_x):
        t = (end_x - x) / width         # t=1 → 外側(黑),t=0 → 內側
        alpha = 1.0 - t ** ease          # 內側(t=0)斜率=0,初期減少慢
        alpha = max(0.0, min(1.0, alpha))
        output[:, x] = np.round(img[:, x] * alpha).astype(np.uint8)

    if right_border < w:
      start_x = max(0, right_border - width) - 1
      end_x = right_border
      for x in range(start_x, end_x):
        t = (x - start_x) / width       # t=0 → 內側(內容),t=1 → 外側(黑)
        alpha = 1.0 - t ** ease          # 內側(t=0)斜率=0,初期減少慢
        alpha = max(0.0, min(1.0, alpha))
        output[:, x] = np.round(img[:, x] * alpha).astype(np.uint8)

  dir_name, file_name = os.path.split(image_path)
  output_dir = os.path.join(dir_name, 'output')
  os.makedirs(output_dir, exist_ok=True)
  output_path = os.path.join(output_dir, file_name)
  cv2.imwrite(output_path, output)
  print(f'已處理完成:{output_path}')


def main():
  parser = argparse.ArgumentParser(
      description='自動偵測左右黑邊並產生邊緣漸層特效。'
  )
  parser.add_argument(
      '--width', type=int, default=20, help='特效寬度,預設為 20 像素。'
  )
  parser.add_argument(
      '--cwd', type=str, default='.', help='工作目錄路徑,預設為目前資料夾。'
  )
  parser.add_argument(
      '--effect',
      type=str,
      default='dissolve',
      choices=['dissolve', 'blur', 'fade'],
      help='特效類型:dissolve 溶解、blur 模糊、fade 透明度漸層。',
  )
  parser.add_argument(
      '--ease',
      type=float,
      default=2.0,
      help='fade 曲線指數(僅 fade 有效)。1.0=線性等差,2.0=二次方(預設),值越大內側保持亮度越久。',
  )

  args = parser.parse_args()

  target_dir = args.cwd
  if not os.path.isdir(target_dir):
    print(f'錯誤:找不到目錄 {target_dir}')
    return

  # 同時支援 jpg 與 png,優先找 png(make_wallpaper.py 的無損暫存格式)
  files = glob.glob(os.path.join(target_dir, '*.png'))
  if not files:
    files = glob.glob(os.path.join(target_dir, '*.jpg'))

  if not files:
    print('在指定目錄中找不到任何 jpg 或 png 圖片。')
    return

  for file_path in sorted(files):
    add_edge_effect(file_path, args.width, args.effect, args.ease)


if __name__ == '__main__':
  main()

使用方式

你可以透過 --effect 參數指定想要的特效類型:

預設溶解特效:

Bash

python dissolve.py

使用模糊特效:

Bash

python dissolve.py --effect blur --width 15

使用漸層淡出特效:

Bash

python dissolve.py --effect fade --width 20

錯誤處理

Error message:

File c:\cleanyourtv\themes\leejueun\wallpapers\dissolve.py", line 3, in <module>
    import cv2
ModuleNotFoundError: No module named 'cv2'

處理模組錯誤

這代表你的電腦沒有安裝 OpenCV 套件。請注意,在 pip 裡面安裝 OpenCV 的套件名稱不是 cv2,而是 opencv-python。

解決步驟

請在終端機中輸入以下指令來安裝正確的套件:

Bash

python -m pip install opencv-python

安裝完成之後,再次執行你的 Python 腳本即可正常運作。


實際在電視上的執行結果


解釋 fade 原理與實際效果, 並說明像素內容的變化:

    # t 為正規化位置 (0.0 ~ 1.0):
    #   左側:t=0 → 外側(黑色),t=1 → 內側(內容)
    #   右側:t=0 → 內側(內容),t=1 → 外側(黑色)
    #
    # alpha 曲線:alpha = 1 - t^ease
    #   ease=1 → 線性等差(舊行為)
    #   ease=2 → 二次方,靠近內側(t=0)斜率趨近 0,初期幾乎不變暗
    #   ease 越大 → 越晚才開始大幅變暗,更接近「突然消失」
    #
    # 右側示意(ease=2):
    #   t:     0.0   0.3   0.5   0.7   1.0
    #   alpha: 1.00  0.91  0.75  0.51  0.00
    #   vs 線性: 1.00  0.70  0.50  0.30  0.00

for x in range(start_x, end_x):
  t = (x - start_x) / width       # t=0 → 內側(內容),t=1 → 外側(黑)
  alpha = 1.0 - t ** ease          # 內側(t=0)斜率=0,初期減少慢
  alpha = max(0.0, min(1.0, alpha))
  output[:, x] = np.round(img[:, x] * alpha).astype(np.uint8)

這是右側 fade的程式碼。逐行拆解:

變數意義

假設 width = 60,右側內容邊界 right_border = 1365:

start_x = max(0, right_border - width) = 1305
end_x = right_border = 1365

alpha 的變化

alpha = (end_x - x) / width
x │ end_x - x │ alpha │ 意義
---
1305(start_x) │ 1365 - 1305 = 60 │ 60/60 = 1.0 │ 原始亮度
1325(中間) │ 1365 - 1325 = 40 │ 40/60 ≈ 0.67 │ 67% 亮度
1344 │ 1365 - 1344 = 21 │ 21/60 = 0.35 │ 35% 亮度
1364(end_x-1) │ 1365 - 1364 = 1 │ 1/60 ≈ 0.017 │ 幾乎全黑

📌 alpha 從 1 → 0,越往右越暗。

像素乘法

output[:, x] = (img[:, x] * alpha).astype(np.uint8)

以某個像素 img[y, x] = [180, 120, 90](B, G, R)為例:

x │ alpha │ B │ G │ R
---
1305 │ 1.00 │ 180×1.00 = 180 │ 120×1.00 = 120 │ 90×1.00 = 90
1325 │ 0.67 │ 180×0.67 = 120 │ 120×0.67 = 80 │ 90×0.67 = 60
1344 │ 0.35 │ 180×0.35 = 63 │ 120×0.35 = 42 │ 90×0.35 = 31
1364 │ 0.017 │ 180×0.017 = 3 │ 120×0.017 = 2 │ 90×0.017 = 1
│ x ≥ 1365(end_x 以外):完全不觸碰,保持原值

整體視覺效果示意

x:    1200   1305          1365   1500
      ──────┬─────────────────┬──────
content     │  fade zone      │ black
      ──────┴─────────────────┴──────
alpha:      1.0 ──────────► 0.0
brightness: ████▓▓▓▓▒▒▒▒░░░░▒▒░░░░
                              ↑
                        幾乎純黑,接黑色背景

注意事項

astype(np.uint8) 會截斷小數,不是四捨五入:

180 * (1/60) = 3.0   → 3   ✅
120 * (1/60) = 2.0   → 2   ✅
 90 * (21/60) = 31.5 → 31  ⚠️ 損失 0.5,累積後在低亮度區可能產生色差

直接使用等差, 會造成視覺上產生出一條較亮的白線

改用 曲線:alpha = 1 – t^ease , 效果就好很多.


make_wallpaper.py 參考:
https://stackoverflow.max-everyday.com/2026/11/make_wallpaper/

發佈留言

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