Skip to content

Commit

Permalink
添加针对透明GIF的处理
Browse files Browse the repository at this point in the history
  • Loading branch information
TransparentLC committed May 25, 2022
1 parent 9748d63 commit 3d1278d
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 29 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,14 @@
| --- | --- |
| ![](https://user-images.githubusercontent.com/47057319/168476063-28a142d4-87ef-491e-b50e-6c981236133f.gif) | ![](https://user-images.githubusercontent.com/47057319/168476067-68e76ed6-9589-44f8-ada8-2792dda0ded4.gif) |

| Nearest Neighbor | Real-ESRGAN |
| --- | --- |
| ![](https://user-images.githubusercontent.com/47057319/170270314-dce674be-e1d3-433f-a71f-763983b33e97.gif) | ![](https://user-images.githubusercontent.com/47057319/170273963-4b11551b-44e7-42f8-b0fd-5b2599087a95.gif) |

* waifu2x-caffe 使用 UpResNet10 和 UpPhoto 模型,降噪等级 3,开启 TTA。
* Real-ESRGAN 使用 realesrgan-x4plus-anime 和 realesrgan-x4plus 模型,开启 TTA。
* 放大倍率均为 4x。
* 为了减小文件大小,展示的 GIF 进行了有损压缩处理。

## 可能遇到的问题

Expand All @@ -78,6 +83,25 @@

我自己选择了几张 1200px 以上的高清二次元图片进行实验:先将原图缩小到 1/4,再使用 `realesrgan-x4plus-anime` 模型在使用或不使用 TTA 的情况下放大 4x,比较放大后图片和原图的 SSIM(范围为 0-1,值越大表示两张图越相似)。结果使用 TTA 的 SSIM 仅比不使用高出 0.002 左右,目视就更看不出差异了。

### 高级设定中“针对 GIF 的透明色进行额外处理”是什么?

GIF 只支持最多 256 种 RGB 颜色的调色板并设定其中一种颜色为透明色(可选),也就是说不存在半透明的情况。对于存在透明部分的 GIF,这就出现了两个问题:

* 图像的 Alpha 通道只有 0 和 255 两个值,可以用只有黑白两色的图像表示,有严重的锯齿。
* 将 GIF 的每一帧拆出来保存为 PNG、WebP 等格式以后,透明部分在 RGB 通道上的颜色会变得不可预料。例如 GIF 中被设为透明色的颜色原本是 `#FFFFFF`,将帧另存为后可能会变成 `#000000`,虽然只看图片的话并没有区别。

对于使用 Real-ESRGAN 直接放大 GIF 的每一帧的做法([示例](https://user-images.githubusercontent.com/47057319/170273973-d9743d66-d6df-42c2-8fe8-b123fa6edb98.gif)),上面两个问题的影响是:

* Real-ESRGAN 对 Alpha 通道放大的效果非常不理想,和使用常规缩放算法几乎没有区别,导致放大后的帧周围会出现一圈锯齿比较明显的杂边。
* 杂边的颜色是不可预料的,比如有些情况下是黑色,会显得非常难看。

这个选项就是针对这两个问题而添加的,启用后会添加以下操作:

* 在拆出 GIF 的每一帧时,强制把透明部分的颜色设为白色,这样可以将放大后的 GIF 的杂边颜色固定为白色,比较美观。
* 对于每一帧的 Alpha 通道,先添加半径 3px 的高斯模糊以平滑锯齿,然后应用一个增加对比度的曲线(或者是 LUT)以尽可能减小杂边的影响,再通过仿色算法处理为只有 0 和 255 两个值的黑白图像。

这个选项是实验性的,建议在放大存在透明部分的 GIF 时手动开启,在放大不存在透明部分的 GIF 时关闭。可能是由于这里的实现或 Pillow 对 GIF 的处理存在问题,在开启时处理后者会出现一些奇怪的问题(主要是出现不该出现的透明色以及仿色效果非常差)。也许会有更好的处理方法。

## 借物表

* [Pillow](https://github.com/python-pillow/Pillow)
Expand Down
4 changes: 4 additions & 0 deletions i18n.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ TileSize = 拆分大小
TileSizeAuto = 自动决定
PreferWebP = 优先保存为无损 WebP
EnableTTA = 使用 TTA 模式(速度大幅下降,稍微提高质量)
GIFOptimizeTransparency = 针对 GIF 的透明色进行额外处理(实验性功能)
ViewREGUISource = 查看源代码
ViewRESource = 查看 Real-ESRGAN 介绍
FrameBasicConfig = 基本设定
Expand Down Expand Up @@ -43,6 +44,7 @@ TileSize = 拆分大小
TileSizeAuto = 自動決定
PreferWebP = 優先保存為無損 WebP
EnableTTA = 使用 TTA 模式(速度大幅下降,稍微提高質量)
GIFOptimizeTransparency = 針對 GIF 的透明色進行額外處理(實驗性功能)
ViewREGUISource = 查看源代碼
ViewRESource = 查看 Real-ESRGAN 介紹
FrameBasicConfig = 基本設定
Expand Down Expand Up @@ -72,6 +74,7 @@ TileSize = 拆分大小
TileSizeAuto = 自動決定
PreferWebP = 優先保存為無損 WebP
EnableTTA = 使用 TTA 模式(速度大幅下降,稍微提高品質)
GIFOptimizeTransparency = 針對 GIF 的透明色進行額外處理(實驗性功能)
ViewREGUISource = 查看原始碼
ViewRESource = 查看 Real-ESRGAN 介紹
FrameBasicConfig = 基本設定
Expand Down Expand Up @@ -101,6 +104,7 @@ TileSize = Tile size
TileSizeAuto = Auto
PreferWebP = Prefer lossless WebP output
EnableTTA = Enable TTA mode (extremely slow, slightly better quality)
GIFOptimizeTransparency = Enable additional processing for GIF with transparency (Experimantal)
ViewREGUISource = View source code
ViewRESource = About Real-ESRGAN
FrameBasicConfig = Basic
Expand Down
7 changes: 5 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def setupVars(self):
self.varintGPUID = tk.IntVar()
self.varboolUseTTA = tk.BooleanVar()
self.varboolUseWebP = tk.BooleanVar()
self.varboolOptimizeGIF = tk.BooleanVar()

def setupWidgets(self):
self.rowconfigure(0, weight=0)
Expand Down Expand Up @@ -153,6 +154,8 @@ def setupWidgets(self):
self.checkUseWebP.pack(padx=10, pady=5, fill=tk.X)
self.checkUseTTA = ttk.Checkbutton(self.frameAdvancedConfigRight, text=i18n.getTranslatedString('EnableTTA'), style='Switch.TCheckbutton', variable=self.varboolUseTTA)
self.checkUseTTA.pack(padx=10, pady=5, fill=tk.X)
self.checkOptimizeGIF = ttk.Checkbutton(self.frameAdvancedConfigRight, text=i18n.getTranslatedString('GIFOptimizeTransparency'), style='Switch.TCheckbutton', variable=self.varboolOptimizeGIF)
self.checkOptimizeGIF.pack(padx=10, pady=5, fill=tk.X)

self.frameAbout = ttk.Frame(self.notebookConfig, padding=5)
self.frameAbout.grid(row=0, column=0, padx=5, pady=5, sticky=tk.NSEW)
Expand Down Expand Up @@ -225,15 +228,15 @@ def buttonProcess_click(self):
f = os.path.join(curDir, f)
g = os.path.join(outputPath, f.removeprefix(inputPath + os.path.sep))
queue.append((
task.SplitGIFTask(self.writeToOutput, f, g, initialConfigParams, queue)
task.SplitGIFTask(self.writeToOutput, f, g, initialConfigParams, queue, self.varboolOptimizeGIF.get())
if os.path.splitext(f)[1].lower() == '.gif' else
task.RESpawnTask(self.writeToOutput, f, g, initialConfigParams)
))
if not queue:
return messagebox.showwarning(define.APP_TITLE, i18n.getTranslatedString('WarningEmptyFolder'))
elif os.path.splitext(inputPath)[1].lower() in {'.jpg', '.jpeg', '.png', '.gif', '.webp'}:
queue.append((
task.SplitGIFTask(self.writeToOutput, inputPath, outputPath, initialConfigParams, queue)
task.SplitGIFTask(self.writeToOutput, inputPath, outputPath, initialConfigParams, queue, self.varboolOptimizeGIF.get())
if os.path.splitext(inputPath)[1].lower() == '.gif' else
task.RESpawnTask(self.writeToOutput, inputPath, outputPath, initialConfigParams)
))
Expand Down
86 changes: 59 additions & 27 deletions task.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import traceback
import typing
from PIL import Image
from PIL import ImageFilter
from PIL import ImageSequence

import define
import param
Expand Down Expand Up @@ -70,26 +72,27 @@ def run(self) -> None:
files = (self.inputPath, *(buildTempPath(outputExt) for _ in range(scalePass)))
for i in range(len(files) - 1):
inputPath, outputPath = files[i:(i + 2)]
cmd = (
define.RE_PATH,
'-v',
'-i', inputPath,
'-o', outputPath,
'-s', str(self.config.modelFactor),
'-t', str(self.config.tileSize),
'-n', self.config.model,
'-g', str(self.config.gpuID),
('-x' if self.config.useTTA else ''),
)
with subprocess.Popen(
(
define.RE_PATH,
'-v',
'-i', inputPath,
'-o', outputPath,
'-s', str(self.config.modelFactor),
'-t', str(self.config.tileSize),
'-n', self.config.model,
'-g', str(self.config.gpuID),
('-x' if self.config.useTTA else ''),
),
cmd,
stderr=subprocess.PIPE,
universal_newlines=True,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0,
) as p:
for line in p.stderr:
self.outputCallback(line)
if p.returncode:
raise subprocess.CalledProcessError(p.returncode)
raise subprocess.CalledProcessError(p.returncode, cmd)
if i > 0 or self.removeInput:
os.remove(inputPath)

Expand All @@ -113,18 +116,41 @@ def __init__(
outputPath: str,
frames: tuple[str, ...],
durations: tuple[int, ...],
optimizeTransparency: bool,
) -> None:
super().__init__(outputCallback)
self.outputPath = outputPath
self.frames = frames
self.durations = durations
self.optimizeTransparency = optimizeTransparency

def run(self) -> None:
self.outputCallback(f'Merging {len(self.frames)} frames to {self.outputPath}\n')
frameImgs: list[Image.Image] = []
for f in self.frames:
b = io.BytesIO()
Image.open(f).save(b, 'gif')
with Image.open(f) as img:
if self.optimizeTransparency:
# LUT from Photoshop curve: (209, 182) (237, 245)
img.putalpha(img.split()[-1].filter(ImageFilter.GaussianBlur(3)).point((
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03,
0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06,
0x06, 0x07, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x09, 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x0B, 0x0B,
0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0E, 0x0E, 0x0F, 0x0F, 0x10, 0x10, 0x10, 0x11, 0x12, 0x12, 0x13,
0x13, 0x14, 0x14, 0x15, 0x15, 0x16, 0x17, 0x17, 0x18, 0x19, 0x19, 0x1A, 0x1B, 0x1B, 0x1C, 0x1D,
0x1E, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x29, 0x2A,
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3B, 0x3C,
0x3D, 0x3E, 0x40, 0x41, 0x42, 0x43, 0x45, 0x46, 0x47, 0x49, 0x4A, 0x4C, 0x4D, 0x4F, 0x50, 0x51,
0x53, 0x55, 0x56, 0x58, 0x59, 0x5B, 0x5C, 0x5E, 0x60, 0x61, 0x63, 0x65, 0x67, 0x68, 0x6A, 0x6C,
0x6E, 0x70, 0x71, 0x73, 0x75, 0x77, 0x79, 0x7B, 0x7D, 0x7F, 0x81, 0x83, 0x85, 0x87, 0x89, 0x8C,
0x8E, 0x90, 0x92, 0x94, 0x97, 0x99, 0x9B, 0x9D, 0xA0, 0xA2, 0xA5, 0xA7, 0xA9, 0xAC, 0xAE, 0xB1,
0xB3, 0xB6, 0xB9, 0xBB, 0xBE, 0xC0, 0xC3, 0xC6, 0xC8, 0xCB, 0xCE, 0xD0, 0xD3, 0xD5, 0xD8, 0xDA,
0xDC, 0xDF, 0xE1, 0xE3, 0xE5, 0xE8, 0xEA, 0xEB, 0xED, 0xEF, 0xF1, 0xF2, 0xF4, 0xF5, 0xF6, 0xF7,
0xF8, 0xF9, 0xFA, 0xFB, 0xFB, 0xFC, 0xFC, 0xFD, 0xFD, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF,
)).convert('1'))
img.save(b, 'gif')
os.remove(f)
img = Image.open(b)
if 'transparency' in img.info:
Expand All @@ -143,32 +169,38 @@ def __init__(
inputPath: str, outputPath: str,
config: param.REConfigParams,
queue: collections.deque[AbstractTask],
optimizeTransparency: bool,
) -> None:
super().__init__(outputCallback)
self.inputPath = inputPath
self.outputPath = outputPath
self.config = config
self.queue = queue
self.optimizeTransparency = optimizeTransparency

def run(self) -> None:
frames = []
durations = []
tasks = []
with Image.open(self.inputPath) as img:
while True:
try:
frameSrcPath = buildTempPath('.webp')
frameDstPath = buildTempPath('.webp')
img.save(frameSrcPath, lossless=True)
d = img.info['duration']
self.outputCallback(f'Frame #{len(frames)}: {frameSrcPath} -> {frameDstPath} Duration: {d}\n')
frames.append(frameDstPath)
durations.append(d)
tasks.append(RESpawnTask(self.outputCallback, frameSrcPath, frameDstPath, self.config, True))
img.seek(img.tell() + 1)
except EOFError:
break
tasks.append(MergeGIFTask(self.outputCallback, self.outputPath, frames, durations))
for f in ImageSequence.Iterator(img):
f: Image.Image
frameSrcPath = buildTempPath('.png' if self.optimizeTransparency else '.webp')
frameDstPath = buildTempPath('.png' if self.optimizeTransparency else '.webp')
d = f.info['duration']
if self.optimizeTransparency:
f = f.convert('RGBA')
with Image.new('RGBA', img.size, (255, 255, 255, 255)) as g:
g.alpha_composite(f)
g.putalpha(f.split()[-1])
g.save(frameSrcPath, lossless=True)
else:
f.save(frameSrcPath, lossless=True)
self.outputCallback(f'Frame #{len(frames)}: {frameSrcPath} -> {frameDstPath} Duration: {d}\n')
frames.append(frameDstPath)
durations.append(d)
tasks.append(RESpawnTask(self.outputCallback, frameSrcPath, frameDstPath, self.config, True))
tasks.append(MergeGIFTask(self.outputCallback, self.outputPath, frames, durations, self.optimizeTransparency))
tasks.reverse()
for t in tasks:
self.queue.appendleft(t)
Expand Down

0 comments on commit 3d1278d

Please sign in to comment.