Metlivi 部落格

如何移除或實心化渲染角色影格中的半透明像素

若要移除渲染角色影格中的半透明像素,請將其 alpha 值改為完全透明或完全不透明。您可以選擇刪除所有部分透明的像素、讓所有可見像素完全不透明,或使用閾值保留輪廓中較明顯的部分。

此工作流程適用於要製作邊緣清晰的透明角色 PNG 影格的創作者。內容涵蓋在 GIMP 中以視覺方式處理,以及使用 Python 批次處理整個影格資料夾的可重複方法,其中包括檢查匯出的影像是否不含中間 alpha 值。

2026年10月29日閱讀時間 8 分鐘作者:Metlivi Editorial Team
第 1 節

影格中究竟需要變更什麼?

對於 8 位元 RGBA 影像,每個像素都有紅、綠、藍和 alpha 元件。Alpha 為 0 表示完全透明;alpha 為 255 表示完全不透明。1 到 254 之間的值表示部分透明。Pillow 將 RGBA 說明為由四個 8 位元通道和透明度遮罩組成。Pillow:概念

因此,影格看起來幾乎完全不透明,仍可能含有許多部分透明的像素。編輯前,請以預定顯示尺寸檢視副本,並放大查看像素。暫時在影格下方放上淺色和深色背景。檢查是否有微弱的外緣像素、缺失的細小細節,以及色彩邊暈。

請先決定成品是否需要保留部分透明度。若將半透明配件或柔和陰影強制轉成不透明或透明遮罩,外觀會大幅改變。如果只需要修正角色輪廓,請先隔離該區域或圖層,再進行全域操作。

請保留原始渲染影格。閾值處理會將許多 alpha 值壓縮成兩個端點,因此處理後的檔案無法讓您得知這些像素原本有多透明。

第 2 節

應移除部分透明的像素,還是讓它們完全不透明?

整個影格序列請使用同一條明確規則。下表中的 C 是要保留的最低 alpha 值,而所有保留的像素都會變成完全不透明。這些規則適用於 8 位元輸入。

目標效果 規則 主要視覺取捨
移除所有半透明像素 只保留 alpha 255 完全由部分透明像素構成的細節會消失
讓所有可見像素完全不透明 保留 alpha 1–255 微弱的外緣像素會成為實心輪廓的一部分
選擇中間程度的輪廓 保留 alpha ≥ C;捨棄 alpha < C 閾值會決定哪些邊緣像素得以保留

例如,假設 alpha 值為 0, 1, 64, 127, 128, 254, 255(僅供示意)。當 C = 128 時,輸出會變成 0, 0, 0, 0, 255, 255, 255。當 C = 255 時,只有最後一個像素會保留。當 C = 1 時,所有原本可見的像素都會變成不透明。

先以 C = 128 作為比較基準,再檢視實際角色。這是方便的中間值,並非經證實的最佳設定。若仍有不需要的微弱像素,請提高閾值;若有用的細節消失,請降低閾值。每次比較都請回到原始影格。

如果提高閾值雖能移除色邊,卻也破壞重要細節,那麼單一全域閾值無法同時達成兩個目標。請局部修正該細節,或先將它分離成獨立遮罩,再處理其餘輪廓。

第 3 節

如何在 GIMP 中對 alpha 套用閾值?

GIMP 3 提供 圖層 → 透明度 → Alpha 閾值…。此功能會作用於單一圖層,且該圖層必須有 alpha 通道;若啟用了 鎖定 alpha 通道,則不會產生效果。閾值範圍為 0 到 1:高於閾值的值會變成不透明,等於或低於閾值的值則會變成透明。GIMP:Alpha 閾值

  1. 開啟影格副本,並選取角色圖層。
  2. 確認圖層有 alpha 通道,並停用 鎖定 alpha 通道。
  3. 開啟 圖層 → 透明度 → Alpha 閾值…。
  4. 預覽 0.5 閾值以進行中間值切割,或使用 0 讓所有非零 alpha 像素完全不透明。
  5. 套用操作前,檢查細小細節和外部輪廓。
  6. 將結果另存為獨立的 PNG。

對於 8 位元輸入,閾值設為 254/255(約 0.996078)時,只會保留原本 alpha 為 255 的像素。此用途請避免將閾值設為 1:根據文件所述的嚴格比較方式,alpha 1 也等於閾值,因此會變成透明。GIMP:Alpha 閾值

請勿將暫時用於檢視的背景匯出到角色檔案中。之後請重新開啟 PNG,以便同時檢查匯出結果和可編輯文件。

第 4 節

如何對每個影格套用相同規則?

處理多個影格時,請使用固定的 alpha 映射,而不要逐一調整每張影像。以下完整的 Python 程式會直接處理單一資料夾中的 PNG 檔案、保留檔名與尺寸,並將結果寫入新資料夾。

使用 python -m pip install Pillow 安裝 Pillow,然後將程式儲存為 alpha_clean.py。請提供彼此獨立的靜態 8 位元 RGBA PNG 影格。調色盤影像、高精度渲染結果或其他影像類型,請先明確轉換成此格式,再使用這個工作流程。

程式使用 getchannel 擷取 alpha,透過 point 查表映射,再用 putalpha 僅替換 alpha。這些操作詳見 Pillow Image 模組參考文件。

import argparse
from pathlib import Path
from PIL import Image


def threshold_alpha(image, cutoff):
    result = image.copy()
    table = [255 if value >= cutoff else 0 for value in range(256)]
    result.putalpha(image.getchannel("A").point(table))
    return result


def partial_count(image):
    return sum(image.getchannel("A").histogram()[1:255])


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("source", type=Path)
    parser.add_argument("destination", type=Path)
    parser.add_argument("--cutoff", type=int, default=128)
    parser.add_argument("--demo", action="store_true")
    args = parser.parse_args()

    if not 1 <= args.cutoff <= 255:
        parser.error("--cutoff must be between 1 and 255")

    if args.demo:
        values = [0, 1, 64, 127, 128, 254, 255]
        sample = Image.new("RGBA", (len(values), 1))
        sample.putdata([(80, 160, 220, a) for a in values])
        result = threshold_alpha(sample, args.cutoff)
        print([result.getpixel((x, 0))[3] for x in range(len(values))])
        return

    if not args.source.is_dir():
        parser.error("Source must be an existing folder")
    if args.destination.exists():
        parser.error("Destination must be a new folder")

    files = sorted(
        p for p in args.source.iterdir()
        if p.is_file() and p.suffix.lower() == ".png"
    )
    if not files:
        parser.error("No PNG frames found directly inside source")

    args.destination.mkdir(parents=True)
    for path in files:
        with Image.open(path) as image:
            if image.format != "PNG" or image.mode != "RGBA":
                raise ValueError(f"{path.name}: expected RGBA PNG")
            if getattr(image, "n_frames", 1) != 1:
                raise ValueError(f"{path.name}: expected a single frame")
            before = partial_count(image)
            result = threshold_alpha(image, args.cutoff)
            target = args.destination / path.name
            result.save(target, format="PNG")

        with Image.open(target) as saved:
            after = partial_count(saved)
            if saved.size != result.size or after != 0:
                raise RuntimeError(f"{target.name}: verification failed")
        print(f"{path.name}: partial pixels {before} -> {after}")


if __name__ == "__main__":
    main()

請先執行獨立範例。在示範模式中,程式會接受兩個資料夾參數,但不會讀取或建立任何一個資料夾:

python alpha_clean.py unused_input unused_output --demo --cutoff 128

程式會印出 [0, 0, 0, 0, 255, 255, 255]。使用 Pillow 13.0.0,以記憶體中的範例檢查了閾值為 1、128 和 255 時的映射、RGB 值是否維持不變,以及 PNG 儲存與重新開啟的行為。

若要處理實際影格,請將輸入 PNG 放入名為 frames 的資料夾,然後執行:

python alpha_clean.py frames frames_clean --cutoff 128

使用 --cutoff 255 刪除所有半透明像素,或使用 --cutoff 1 讓所有可見像素完全不透明。每次比較都請選擇新的目的地資料夾。

如果輸入格式不受支援或驗證失敗,程式會停止。錯誤發生前已完成的檔案仍會留在目的地資料夾中;該資料夾代表未完成的執行結果。請修正輸入資料,並重新執行到另一個新資料夾。程式不會遞迴處理子資料夾,也不會保留動畫時間資訊;它並非通用的中繼資料保留工具。

第 5 節

為什麼實心化的邊緣仍然看起來偏暗或偏淡?

Alpha 清理會改變覆蓋率。邊緣色彩需要另外檢查。Microsoft 區分直通 alpha(RGB 與 alpha 彼此獨立)和預乘 alpha(RGB 已乘上 alpha)。Microsoft Learn:預乘 alpha

以下為示意計算:alpha 為 0.25 的直通紅色 (200, 0, 0),對應的預乘 RGB 為 (50, 0, 0)。如果將這些預乘色彩值誤當作直通 RGB,並直接將 alpha 提高至 1,結果就會是暗紅色像素。這是根據文件所述的轉換關係直接推得的。Microsoft Learn:預乘 alpha

Python 工作流程假設輸入的 RGBA PNG 使用正確編碼的直通 alpha。它無法診斷編碼錯誤的色彩,也無法還原預期的輪廓顏色。如果實心化像素後出現色邊,請先檢查原始渲染或匯出時對 alpha 的解讀方式。如果色彩受到先前背景合成的污染,請回到乾淨的透明渲染結果,或重新繪製受影響的邊緣。

請將清理後的影格分別放在淺色和深色背景上比較。部分像素數量為零,代表 alpha 已變成二元值;這並不代表邊緣色彩適合該角色。

第 6 節

如何讓清理結果在匯出和播放時維持完整?

請在最後一次 alpha 清理之前完成尺寸調整。插值縮放可能會在透明和不透明像素之間產生中間值。Pillow 說明,雙線性重採樣會透過插值計算數值,而最近鄰重採樣會選取既有的輸入像素。因此,對二元 alpha 通道使用最近鄰縮放,能保留兩個端點值。Pillow:概念—濾鏡

若預期風格採用離散的像素網格,請使用最近鄰縮放。若需要其他視覺風格,請先使用所需濾鏡縮放,再對結果影格套用閾值並重新檢查輪廓。之後若再進行縮放或合成,請再次檢查最終輸出。

請以相同閾值檢視完整序列。即使每個影格單獨看都能接受,細小特徵仍可能在相鄰影格之間跨過閾值,造成閃爍。如果發生這種情況,請檢查變化前後的原始影格,並在這些影格中一致地修復該特徵。

使用這個序列前,請確認以下條件:

  • 所有預期影格都存在,且保留原始檔名和畫布尺寸。
  • 每個儲存的影格中,alpha 值介於 1 到 254 的像素數量為零。
  • 以預定顯示尺寸檢視時,細小細節仍清晰可辨。
  • 在淺色和深色背景預覽中都沒有不需要的色邊。
  • 播放時,整段動作中的輪廓保持連貫。

請選擇能保留角色預期形狀的 alpha 規則,一致地套用,並檢查匯出的序列。請將原始渲染影格與清理後的影格一併保留,以便日後調整時能從完整的透明度資訊重新開始。

第 7 節

來源

相關閱讀

繼續探索這個主題