
左是預期輸出, 右是有問題的,
解法, 原來的
image_binary = image_binary.convert("1")
改成
image_binary = image_binary.convert("1", dither=Image.NONE)
這個問題其實是 Pillow(PIL)在文字光柵化(rasterization)時的抗鋸齒 + 再轉灰階/二值化造成的「掉洞」現象,在細黑直線、中文字筆畫上特別明顯。你現在的流程:
RGB →
draw.text()→ 再convert('L')
正好是最容易出現「黑線中夾雜白色凹洞」的組合。
下面我用「成因說明 → 最佳解法 → 進階強化」的方式幫你一次修好。
一、為什麼會出現「黑線白洞」?
關鍵原因(命中 3 點)
- Pillow 的文字是抗鋸齒繪製
- 邊緣會產生「深灰色像素」
- RGB → L 的轉換
convert('L')會重新加權 RGB → 灰階
- 細線 + 小字 + 高對比(黑 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 合成 | ✅ | 適合後處理 |
| 超採樣再縮小 | 🔥 | 高品質字型訓練 |
| 激進二值化 | ❌ | 會破壞筆畫 |