绘制一个抗锯齿的圆圈,如Xaolin Wu所描述

时间:2016-06-02 10:23:37

标签: python algorithm geometry sdl sdl-2

我正在实施“快速抗锯齿圆形发生器”程序,该程序由Xiaolin Wu在Siggraph[他的论文“An Efficient Anasingiasing Technique”中描述。

这是我使用Python 3和PySDL2编写的代码:

def draw_antialiased_circle(renderer, position, radius):
    def _draw_point(renderer, offset, x, y):
        sdl2.SDL_RenderDrawPoint(renderer, offset.x - x, offset.y + y)
        sdl2.SDL_RenderDrawPoint(renderer, offset.x + x, offset.y + y)
        sdl2.SDL_RenderDrawPoint(renderer, offset.x - x, offset.y - y)
        sdl2.SDL_RenderDrawPoint(renderer, offset.x + x, offset.y - y)

    i = 0
    j = radius
    d = 0
    T = 0

    sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, sdl2.SDL_ALPHA_OPAQUE)
    _draw_point(renderer, position, i, j)

    while i < j + 1:
        i += 1
        s = math.sqrt(max(radius * radius - i * i, 0.0))
        d = math.floor(sdl2.SDL_ALPHA_OPAQUE * (math.ceil(s) - s) + 0.5)

        if d < T:
            j -= 1

        T = d

        if d > 0:
            alpha = d
            sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha)
            _draw_point(renderer, position, i, j)
            if i != j:
                _draw_point(renderer, position, j, i)

        if (sdl2.SDL_ALPHA_OPAQUE - d) > 0:
            alpha = sdl2.SDL_ALPHA_OPAQUE - d
            sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha)
            _draw_point(renderer, position, i, j + 1)
            if i != j + 1:
                _draw_point(renderer, position, j + 1, i)

这是我认为在他的论文中描述的一个天真的实现,除了我将半径值分配给j而不是i,因为我误解了某些东西或那里&#39他的论文中有一个错误。实际上,他使用半径值ij初始化0,然后定义循环条件i <= j,只有当半径为{{1}时才能为真}。这一变化促使我对所描述的内容做了一些其他的小修改,我也将0更改为if d > T,因为它看起来很糟糕。

除了每个八分圆的开始和结束之外,这个实现工作效果很好,其中会出现一些毛刺。

circle

上面的圆的半径为1.正如您在每个八分圆的开头看到的那样(例如在(0,1)区域中),在循环内绘制的像素未与第一个像素对齐在循环开始之前绘制。在每个八分圆的末尾(例如在if d < T区域),也会出现严重错误。我设法通过将(sqrt(2) / 2, sqrt(2) / 2)条件更改为if d < T来使最后一个问题消失,但同样的问题会出现在每个八分圆的开头。

问题1 :我做错了什么?

问题2 :如果我希望输入位置和半径为浮点,会不会有任何问题?

2 个答案:

答案 0 :(得分:2)

让我们解构您的实施,找出您犯的错误:

def  draw_antialiased_circle(renderer, position, radius):

第一个问题,radius是什么意思?绘制一个圆圈是不可能的,你只能绘制一个圆环(圆环/圆环),因为除非你有一些厚度,否则你无法看到它。因此半径是模糊的,是内半径,中点半径还是外半径?如果你没有在变量名中指定,那将会让人感到困惑。也许我们可以找到答案。

def _draw_point(renderer, offset, x, y):
    sdl2.SDL_RenderDrawPoint(renderer, offset.x - x, offset.y + y)
    sdl2.SDL_RenderDrawPoint(renderer, offset.x + x, offset.y + y)
    sdl2.SDL_RenderDrawPoint(renderer, offset.x - x, offset.y - y)
    sdl2.SDL_RenderDrawPoint(renderer, offset.x + x, offset.y - y)
好吧,由于圆圈是对称的,我们一次要画四个地方。为什么不同时出现8个地方?

i = 0
j = radius
d = 0
T = 0

好的,将i初始化为0并将j初始化为半径,这些必须是xy坐标。什么是dT?无意义的变量名称没有帮助。复制科学家&#39;使用更长的变量名称实际可以理解的算法!

sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, sdl2.SDL_ALPHA_OPAQUE)
_draw_point(renderer, position, i, j)

咦?我们正在绘制一个特例?我们为什么这样做?不确定。这意味着什么?这意味着填充(0, radius)中具有完全不透明度的方块。所以现在我们知道radius是什么,它是环的外半径,而环的宽度显然是一个像素。或者至少,这个特例告诉我们的是什么......让我们看看它是否在通用代码中有用。

while i < j + 1:

我们将循环直到我们到达i > j所在的圆点,然后停止。即我们正在画一个八分圆。

    i += 1

所以我们已经在i = 0位置绘制了我们关注的所有像素,让我们进入下一个像素。

    s = math.sqrt(max(radius * radius - i * i, 0.0))

即。 s是一个浮点数,它是x轴上点i处八分圆的y分量。即高于x轴的高度。出于某种原因,我们在那里有max,也许我们担心非常小的圆圈...但是这并不能告诉我们这个点是否在外半径/内半径上。 / p>

    d = math.floor(sdl2.SDL_ALPHA_OPAQUE * (math.ceil(s) - s) + 0.5)

将其分解,(math.ceil(s) - s)给出一个0到1.0之间的数字。当s减少时,此数字会增加,因此i会增加,然后一旦达到1.0,将重置为0.0,因此采用锯齿模式。

sdl2.SDL_ALPHA_OPAQUE * (math.ceil(s) - s)提示我们使用锯齿输入来产生不透明度,即对圆圈进行抗锯齿处理。 0.5不变的添加似乎是不必要的。 floor()表明我们只对整数值感兴趣 - 可能API只接受整数值。所有这些都表明d最终是0到SDL_ALPHA_OPAQUE之间的整数级别,从0开始,然后逐渐建立i,然后ceil(s) - s }从1回到0,它也再次降低。

那么这一开始有什么价值呢? s几乎是radius,因为i是1,(假设是一个非平凡的大小圆圈),所以假设我们有一个整数半径(第一个特殊情况代码明确假设){{ 1}}是0

ceil(s) - s

在这里,我们发现 if d < T: j -= 1 d = T 已经从高位变为低位,我们向下移动屏幕,使我们的d位置保持在理论上应该位于环的位置附近。

但现在我们也意识到前一个等式的底线是错误的。想象一下,在一次迭代中j是100.9。然后在下一次迭代中它已经下降,但只有100.1。由于dd相同,因为最低限度已根除了他们的差异,在这种情况下我们不会减少T ,这是至关重要的。我想可能解释了八分圆末端奇怪的曲线。

j

如果我们要使用alpha值0

,只是优化不绘制
if d < 0:

但是,在第一次迭代alpha = d sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha) _draw_point(renderer, position, i, j) 上保持不变,因此这是一个几乎完全透明的元素d。但是开始的特殊情况为(1, radius)画了一个完全不透明的元素,所以很明显这里会出现图形故障。这就是你所看到的。

继续:

(0, radius)

另一个小优化,如果我们要吸引到同一个位置,我们就不会再画画了

if i != j:

这就解释了为什么我们只在_draw_point(renderer, position, j, i) 中绘制4个点,因为你在这里做了另一个对称。它会简化您的代码而不是。

_draw_point()

另一项优化(您不应该过早优化代码!):

if (sdl2.SDL_ALPHA_OPAQUE - d) > 0:

所以在第一次迭代中,我们写入上面的位置,但是完全不透明。这意味着实际上它是我们正在使用的内部半径,而不是特殊情况错误所使用的外部半径。即一种一个一个一个错误。

我的实现(我没有安装库,但你可以从输出中看到它有意义的边界条件):

alpha = sdl2.SDL_ALPHA_OPAQUE - d
sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha)
_draw_point(renderer, position, i, j + 1)

最后,如果你想传递一个浮点原点会有陷阱吗?

是的,会的。该算法假定原点位于整数/整数位置,否则完全忽略它。如我们所见,如果传入整数import math def draw_antialiased_circle(outer_radius): def _draw_point(x, y, alpha): # draw the 8 symmetries. print('%d:%d @ %f' % (x, y, alpha)) i = 0 j = outer_radius last_fade_amount = 0 fade_amount = 0 MAX_OPAQUE = 100.0 while i < j: height = math.sqrt(max(outer_radius * outer_radius - i * i, 0)) fade_amount = MAX_OPAQUE * (math.ceil(height) - height) if fade_amount < last_fade_amount: # Opaqueness reset so drop down a row. j -= 1 last_fade_amount = fade_amount # The API needs integers, so convert here now we've checked if # it dropped. fade_amount_i = int(fade_amount) # We're fading out the current j row, and fading in the next one down. _draw_point(i, j, MAX_OPAQUE - fade_amount_i) _draw_point(i, j - 1, fade_amount_i) i += 1 ,算法会在outer_radius位置绘制100%不透明的正方形。但是,如果您想要转换到(0, outer_radius - 1)位置,您可能希望圆圈在(0, 0.5)(0, outer_radius - 1)位置平滑地混叠到50%的不透明度,这个算法没有给你,因为它忽略了原点。因此,如果你想准确地使用这个算法,你必须在传入之前对原点进行舍入,因此使用浮点数无法获得任何东西。

答案 1 :(得分:1)

我在我的机器上工作了以下内容:

FirstOrDefault()

以上是下图的简单实现。

Fig. 1

此图表中存在错误:最底部的测试应该是def draw_antialiased_circle(renderer, position, radius, alpha_max=sdl2.SDL_ALPHA_OPAQUE): def _draw_points(i, j): '''Draws 8 points, one on each octant.''' #Square symmetry local_coord = [(i*(-1)**(k%2), j*(-1)**(k//2)) for k in range(4)] #Diagonal symmetry local_coord += [(j_, i_) for i_, j_ in local_coord] for i_, j_ in local_coord: sdl2.SDL_RenderDrawPoint(renderer, position.x + i_, position.y + j_) def set_alpha_color(alpha, r=255, g=255, b=255): '''Sets the render color.''' sdl2.SDL_SetRenderDrawColor(renderer, r, g, b, alpha) i, j, T = radius, 0, 0 #Draw the first 4 points set_alpha_color(alpha_max) _draw_points(i, 0) while i > j: j += 1 d = math.ceil(math.sqrt(radius**2 - j**2)) #Decrement i only if d < T as proved by Xiaolin Wu i -= 1 if d < T else 0 """ Draw 8 points for i and i-1 keeping the total opacity constant and equal to :alpha_max:. """ alpha = int(d/radius * alpha_max) for i_, alpha_ in zip((i, i-1), (alpha, alpha_max - alpha)): set_alpha_color(alpha_) _draw_points(i_, j) #Previous d value goes to T T = d 而不是D(r,j) < T,这可以解释为什么您有点困惑。

要回答你的第二个问题,我怀疑你可以传递一些D(r,j) > T数字作为位置和半径,因为最终C函数float期望SDL_RenderDrawPoint参数。

您可以将一些浮点数传递给Python函数,但在使用int之前必须将它们转换为int。遗憾的是,没有像半像素那样;)