Tkinterだってアンチエイリアスしたい
それくらい夢見たっていいでしょう。
ってことですが、元々は業務で利用しているTkinterのラッパーで用事があったものです。
その時は良い結果が得られずに結局ギザギザの通常の描画で流したのですが、使えば使うほどやっぱり滑らかな描画さえできれば世界が変わる、そんな気がしたんだ!
MacのTkinterは元々アンチエイリアスありで描画されるらしい?ので無関係かもしれませんがー?
とりあえず考えてみる。
アンチエイリアスのラインアルゴリズムでは Xiaolin Wu 先生のものが有名です。
試しに一度Canvasに仕込んでみたのですが普通にラインを描くだけなら良いのですけれどもその他諸々をしようとすると何かと不便があったのでここでは別の方法を使います。
座標は自然数で扱うこととし(ぁ) 一つの描画座標はx~x+1、y~y+1の合計4点の座標に作用するとしてそれぞれの座標に対して色の濃さを表してゆけば事実上アンチエイリアスが可能だろうと。そういうことを考えたので実装してみます。
そして、直接Canvasに描画すると何か問題があった記憶があるのでPhotoImageとして扱うことにします。
アンチエイリアス対応PhotoImage、AAImage
コード
aaimage.py
import tkinter as tk
class AAImage(tk.PhotoImage):
def __init__(self, conf={}, **kwargs):
super().__init__(conf, **kwargs)
self.pixpos = [(0, 0), (1, 0), (0, 1), (1, 1)]
def rgbtohex(self, r, g, b):
return "#%02x%02x%02x" % (int(r), int(g), int(b))
def hexcolor(self, color):
return self.rgbtohex(int(color[0]), int(color[1]), int(color[2]))
def hextocolor(self, hexcolor):
r = int(hexcolor[1:3], 16)
g = int(hexcolor[3:5], 16)
b = int(hexcolor[5:7], 16)
return (r, g, b)
"""
color fill
"""
def fill(self, color):
d = [self.hexcolor(color)] * int(self.height())
for x in range(int(self.width())):
self.put(d, (x, 0))
"""
4 point antialiasing
** Not optimized **
"""
def pset(self, x, y, color=(255,255,255)):
R, G, B = color
pp = [0] * 4
pp[0] = (1 - self.pp(x)) * (1 - self.pp(y))
pp[1] = self.pp(x) * (1 -self.pp(y))
pp[2] = (1 - self.pp(x)) * self.pp(y)
pp[3] = self.pp(x) * self.pp(y)
for i in range(4):
px = int(x + self.pixpos[i][0])
py = int(y + self.pixpos[i][1])
if pp[i] > 0:
if px >= 0 and px < int(self.width()) \
and py >= 0 and py < int(self.height()):
bR, bG, bB = self.get(px, py)
tc = (
int(R * pp[i] + bR * (1 - pp[i])),
int(G * pp[i] + bG * (1 - pp[i])),
int(B * pp[i] + bB * (1 - pp[i]))
)
self.put(self.hexcolor(tc), (px, py))
"""
draw line
"""
def line(self, x1, y1, x2, y2, color=(0, 0, 0)):
xmax = max(x1, x2)
ymax = max(y1, y2)
xmin = min(x1, x2)
ymin = min(y1, y2)
xr = xmax - xmin
yr = ymax - ymin
x = x1
y = y1
if xr > yr:
if y2 - y1 == 0:
yy = 0
else:
yy = (y2 - y1) / xr
if x2 - x1 < 0:
xx = -1
else:
xx = 1
for i in range(int(xr)+1):
self.pset(x, y, color)
x += xx
y += yy
else:
if x2 - x1 == 0:
xx = 0
else:
xx = (x2 - x1) / yr
if y2 - y1 < 0:
yy = -1
else:
yy = 1
for i in range(int(yr)+1):
self.pset(x, y, color)
x += xx
y += yy
def pp(self, pos):
return pos - int(pos)
解説
とりあえず1ピクセルを描画するための pset
(実際には最大4ピクセルが描画される)とそれを利用した line
を実装しました。
画面を塗り替えたい事も多かったのでついでに fill
も。
キモは pset
なのですが残念ながら最適化はしていません。
誰かがカッコよく使いやすくして各種の描画アルゴリズムも実装して公開してくれるのを期待しています
各種描画アルゴリズムを実装すると便利に使えそうですね!
実際に使ってみる
import random as rnd
import tkinter as tk
import aaimage
root = tk.Tk()
c = tk.Canvas(root)
c.pack(fill="both", expand=True)
image = aaimage.AAImage(width=150, height=150)
c.create_image(0, 0, image=image, anchor="nw")
image.fill((255,255,255))
# random dot
for i in range(200):
x = rnd.random() * image.width()
y = rnd.random() * image.height()
image.pset(x, y, (0,0,255))
# somooth line
image.line(100, 5, 20, 120, (255, 0, 0))
image.line(10, 10, 130, 30, (0, 255, 0))
root.mainloop()
継承してるので当たり前ですが tk.PhotoImage
のノリでそのまま使えます。
実行結果
単体のドットには思うところがありますけれども1ピクセル以下を表現しようとしているわけなのでこれ以上どうともならないでしょう。
ということで、大成功! これをガンd
何事も諦めなければどうにかなるものですネ!