Pillow(PIL)處理文字時造成的「掉洞」現象

左是預期輸出, 右是有問題的,


解法, 原來的

image_binary = image_binary.convert("1")

改成

image_binary = image_binary.convert("1", dither=Image.NONE)

這個問題其實是 Pillow(PIL)在文字光柵化(rasterization)時的抗鋸齒 + 再轉灰階/二值化造成的「掉洞」現象,在細黑直線、中文字筆畫上特別明顯。你現在的流程:

RGB → draw.text() → 再 convert('L')

正好是最容易出現「黑線中夾雜白色凹洞」的組合。

下面我用「成因說明 → 最佳解法 → 進階強化」的方式幫你一次修好。


一、為什麼會出現「黑線白洞」?

關鍵原因(命中 3 點)

  1. Pillow 的文字是抗鋸齒繪製
    • 邊緣會產生「深灰色像素」
  2. RGB → L 的轉換
    • convert('L') 會重新加權 RGB → 灰階
  3. 細線 + 小字 + 高對比(黑 on 白)
    • 深灰像素被壓成接近白色,看起來就像「洞」

👉 尤其是 中文字、直線筆畫、1px stroke 最容易中招
這在你做 字型風格遷移 / edge-aware 任務時會很致命(我知道你在做 😄)


二、最佳解法(強烈推薦)

✅ 解法 1:直接用 L 模式畫字(不要先 RGB)

👉 這是「最乾淨、最少副作用」的做法

from PIL import Image, ImageDraw, ImageFont

img = Image.new("L", (canvas_size, canvas_size), 255)  # 直接用 L
draw = ImageDraw.Draw(img)

draw.text(
    (x_offset, y_offset),
    char,
    fill=0,        # 黑色
    font=font
)

✅ 優點:

  • 不經過 RGB → L 轉換
  • 不會產生灰階重新取樣
  • 黑線 連續、乾淨、沒有白洞

三、若你「必須」先用 RGB(例如後面還要上色)

✅ 解法 2:改用 Alpha mask 合成

# 建立 mask
mask = Image.new("L", (canvas_size, canvas_size), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.text((x_offset, y_offset), char, fill=255, font=font)

# 套用 mask
img = Image.new("RGB", (canvas_size, canvas_size), (255, 255, 255))
img.paste((0, 0, 0), mask=mask)

# 最後再轉 L
img = img.convert("L")

✅ 優點:

  • 字是「實心遮罩」
  • 不會出現灰階裂縫
  • 非常適合你後面接 Canny / edge loss

四、進階強化(你這種字型訓練一定用得到)

🔥 解法 3:超採樣(Supersampling Anti-Aliasing)

先大尺寸畫 → 再縮小

scale = 4

img = Image.new("L", (canvas_size * scale, canvas_size * scale), 255)
draw = ImageDraw.Draw(img)

draw.text(
    (x_offset * scale, y_offset * scale),
    char,
    fill=0,
    font=font.font_variant(size=font.size * scale)
)

img = img.resize((canvas_size, canvas_size), Image.LANCZOS)

✅ 效果:

  • 筆畫 更連續
  • 幾乎消除鋸齒與凹洞
  • 中文字 + 細筆畫 特別有效

五、補刀:如果你後面要二值化(非常重要)

❌ 不要這樣做

img = img.point(lambda x: 0 if x < 128 else 255)

✅ 改用「保守閾值」

import numpy as np

arr = np.array(img)
arr[arr < 200] = 0     # 黑
arr[arr >= 200] = 255 # 白
img = Image.fromarray(arr, 'L')

或直接:

img = img.convert("1", dither=Image.NONE)

六、給你一個「最穩定版本」(推薦你直接用)

def render_char(char, font, canvas_size, x_offset=0, y_offset=0):
    img = Image.new("L", (canvas_size, canvas_size), 255)
    draw = ImageDraw.Draw(img)

    draw.text(
        (x_offset, y_offset),
        char,
        fill=0,
        font=font
    )
    return img

七、總結(給你工程師版結論)

作法是否推薦原因
RGB → draw.text → convert(‘L’)會產生白洞
直接用 L 畫字首選最乾淨
Alpha mask 合成適合後處理
超採樣再縮小🔥高品質字型訓練
激進二值化會破壞筆畫

發佈留言

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