【问题标题】:please explain this Bresenham Line drawing code for me请为我解释这个布雷森汉姆线绘图代码
【发布时间】:2015-08-27 14:00:01
【问题描述】:

我在整个互联网上进行了搜索,发现了数百个 Bresenham 画线算法的实现。但是,我觉得奇怪的一件事是,它们中只有两三个可以覆盖所有八个八位字节。尽管如此,它们仍在许多应用中使用。

例如,this lady 实现了 Bresenham 算法的this version (line 415)。但是,它并没有覆盖整个 360 度。 This guy here 似乎正在开发一个库。但它仍然无法正常工作。

你能告诉我为什么吗?

This guy's implementation 工作正常。但是,我想这不是布雷森纳姆算法。它与the theory 几乎没有相似之处。

最后,我发现the following version 的 Bresenham's Line Drawing Algorithm 可以正常工作。

#include "utils.h"

void Bresenham(int x1, int y1, int const x2, int const y2, int color)
{
    int dx = x2 - x1;
    // if x1 == x2, then it does not matter what we set here
    int ix((dx > 0) - (dx < 0));

    dx = abs(dx) << 1;

    int dy = y2 - y1;
    // if y1 == y2, then it does not matter what we set here
    int iy((dy > 0) - (dy < 0));
    dy = abs(dy) << 1;

    PlotPixel(x1, y1, color);

    if (dx >= dy)
    {
        // error may go below zero
        int error(dy - (dx >> 1));

        while (x1 != x2)
        {
            if ((error >= 0) && (error || (ix > 0)))
            {
                error -= dx;
                y1 += iy;
            }
            // else do nothing

            error += dy;
            x1 += ix;

            PlotPixel(x1, y1, color);
        }
    }
    else
    {
        // error may go below zero
        int error(dx - (dy >> 1));

        while (y1 != y2)
        {
            if ((error >= 0) && (error || (iy > 0)))
            {
                error -= dy;
                x1 += ix;
            }
            // else do nothing

            error += dx;
            y1 += iy;

            PlotPixel(x1, y1, color);
        }
    }
}

int main()
{
    int gm = DETECT;
    int gd = DETECT;

    initgraph(&gm, &gd, "");

    double x1 = 0;
    double y1 = 0;
    double r = 50;
    double x2 = 0;
    double y2 = 0;
    double signx = 0;
    double signy = 0;

    for(int theta=0 ; theta<=360 ; theta++)
    {
        x2 = r * cos(DegreeToRad((double) theta));
        y2 = r * sin(DegreeToRad((double) theta));

        x1 = 5 * cos(DegreeToRad((double) theta));
        y1 = 5 * sin(DegreeToRad((double) theta));

        Bresenham(x1, y1, x2, y2, YELLOW);

        //delay(10);
    }

    getch();
    closegraph();
    return 0;
}

原来的代码很奇怪。所以,我需要你的帮助来理解这一点。

  • 为什么他要左移 dx 和 dy,然后在计算之前再次右移?

  • the theory 所说的 dt 和 ds 在哪里?

  • 按照理论,在while循环的每一步都应该测试dt和ds。但是,这段代码没有这样做。为什么?

  • 该理论似乎没有任何错误值处理的迹象。代码正在计算的error 有什么用?他是如何计算error 的?他如何使用error 值?

  • 测试if(dx &gt;= dy)背后的逻辑是什么?

【问题讨论】:

    标签: c++ algorithm graphics line bresenham


    【解决方案1】:

    这是我自己版本的 Bresenham 的解释。

    我们从直线的参数方程(X + t.Dx, Y + t.Dy) 开始,其中t[0, 1] 范围内的参数。端点显然是(X, Y)(X + Dx, Y + Dy)

    要将其转换为数字线,我们希望每行或每列恰好有一个像素,以确保连续线为准。所以定义D = Max(|Dx|, |Dy|),我们将绘制D+1点对应的分数t(X + I.Dx/D, Y + I.Dy/D)I[0, D]中。

    让我们假设一下0 &lt;= Dy &lt; Dx = D,等式简化为(X + I, Y + I.Dy/Dx)。由于最后一项可能不是整数,我们将用round(I.Dy/Dx) = floor(I.Dy/Dx + 1/2) = floor((I.Dy + Dx/2) / Dx)四舍五入。

    后一个表达式是等差数列与大于公差的分母的商。因此,当您增加I 时,该比率要么保持不变,要么增加1。对于无除法的实现,我们使用了一个技巧:保留除法的商和余数,让QR,每次增加I,更新它们。

    N = I.Dy + Dx/2Q = N / DxR = N % Dx。然后增加II' = I + 1N' = N + DyQ' = (N + Dy) / DxR' = (N + Dy) % Dx。如您所见,以下规则成立:

    if R + Dy >= Dx
        Q' = Q + 1; R' = R + Dy - Dx
    else
        Q' = Q; R' = R + Dy
    

    我们现在将所有元素放在一起,调整符号并区分更水平和更垂直的情况(您会注意到,没有必要明确表示Q):

    # Increments
    Sx= Sign(Dx); Sy= Sign(Dy)
    
    # Segment length
    Dx= |Dx|; Dy= |Dy|; D= Max(Dx, Dy)
    
    # Initial remainder
    R= D / 2
    
    if Dx > Dy:
        # Main loop
        for I= 0..D:
            Set(X, Y)
    
            # Update (X, Y) and R
            X+= Sx; R+= Dy # Lateral move
            if R >= Dx
                Y+= Sy; R-= Dx # Diagonal move
    else:
        # Main loop
        for I= 0..D:
            Set(X, Y)
    
            # Update (X, Y) and R
            Y+= Sy; R+= Dx # Lateral move
            if R >= Dy
                X+= Sx; R-= Dy # Diagonal move
    

    C/C++ 实现(作者 @anonymous)

    void Set(int x, int y, int color)
    {
        PlotPixel(x, y, color);
    }
    
    int Sign(int dxy)
    {
        if(dxy<0) return -1; 
        else if(dxy>0) return 1; 
        else return 0;
    }
    void Bresenham(int x1, int y1, int x2, int y2, int color)
    {
        int Dx = x2 - x1;
        int Dy = y2 - y1;
    
        //# Increments
        int Sx = Sign(Dx); 
        int Sy = Sign(Dy);
    
        //# Segment length
        Dx = abs(Dx); 
        Dy = abs(Dy); 
        int D = max(Dx, Dy);
    
        //# Initial remainder
        double R = D / 2;
    
        int X = x1;
        int Y = y1;
        if(Dx > Dy)
        {   
            //# Main loop
            for(int I=0; I<D; I++)
            {   
                Set(X, Y, color);
                //# Update (X, Y) and R
                X+= Sx; R+= Dy; //# Lateral move
                if (R >= Dx)
                {
                    Y+= Sy; 
                    R-= Dx; //# Diagonal move
                }
            }
        }
        else
        {   
            //# Main loop
            for(int I=0; I<D; I++)
            {    
                Set(X, Y, color);
                //# Update (X, Y) and R
                Y+= Sy; 
                R+= Dx; //# Lateral move
                if(R >= Dy)
                {    
                    X+= Sx; 
                    R-= Dy; //# Diagonal move
                }
            }
        }
    }
    

    【讨论】:

    • " 我们将绘制对应于小数 t 的 D+1 个点:(X + I.Dx/D, Y + I.Dy/D),其中 I 在 [0, D] 中。" --- 你是怎么推断出来的?
    • I/D 从 0 到 1,增量为 1/D,对应于 X 上的增量 Dx/D 和 Y 上的 Dy/D,其中之一是 1。
    • 如何推断 t=I/D?
    • 请让这句话更容易理解:“后一个表达式是数字的商,在大于公差的分母上遵循算术级数。”
    • 你在最终代码中在哪里使用了这个逻辑:if R + Dy &gt;= Dx Q' = Q + 1; R' = R + Dy - Dx else Q' = Q; R' = R + Dy
    【解决方案2】:

    为什么他要左移 dx 和 dy 然后,在计算之前,再次 右移它们?

    他以一种期望它准确的方式使用了一半的 int。但是半个 int 会被截断。因此,通过使用本质上加倍的表示,他可以使用右移作为未截断的除以二。这不是我很久以前了解布雷森纳姆的方式。但意图似乎很明确。

    理论所说的dt和ds在哪里?

    我没有仔细阅读您的理论链接,但我学习 Bresenham 的方法比这更简单。最初的观点是针对非常原始的 CPU,您希望在其中最小化计算。

    该理论似乎没有任何错误值处理的迹象。 代码计算的错误有什么用?他怎么样 计算误差?他是如何使用错误值的?

    我预计只是术语上的差异会让您感到困惑。该算法的关键点(任何形式)是一个累加器,表示累积“错误”与非数字化线。该“错误”通常可能有不同的名称,但无论名称如何,它都是代码的核心。您应该能够看到它在每个步骤中用于决定该步骤的数字化方向。

    测试 if(dx >= dy) 背后的逻辑是什么?

    这个想法是,快速变化的方向每一步改变 1,而慢速改变方向每一步改变 0 或 1,具体取决于累积的“错误”。当代码大小是一个主要因素时,该算法的真正技巧是对其进行编码,以便在 X 更快与 Y 更快的主要区别之间共享代码。但显然,如果您将 X 变化更快的情况与 Y 变化更快的情况分开来看,整个事情就很容易理解了。

    【讨论】:

      【解决方案3】:
      • 为什么他要左移 dx 和 dy 然后在计算之前再次 右移它们?

      我在下面解释。

      • 理论所说的dt和ds在哪里?

      它们不见了,实现实现了中点画线算法。不过,您可以从另一种算法派生出一种算法。这是给读者的练习:-)

      • 根据理论,dt 和 ds 应该在每个 while 循环的步骤。但是,这段代码没有这样做。为什么?

      同上。它正在测试错误,这是同一件事。

      • 该理论似乎没有任何错误值处理的迹象。 代码计算的错误有什么用?他怎么样 计算误差?他是如何使用错误值的?

      a*x + b*y + c = 0 线的方程,其中a = x2 - x1b = -(y2 - y1) 可以指示错误,因为a*x + b*y + c 与点(x, y) 到线的距离成正比,实际系数abc。我们可以将方程乘以不等于 0 的任意实常数 k,它仍然适用于线上的每个点。

      假设我们只在第一象限绘制。在每个步骤中,我们希望评估a*x + b*y + c(x + 1, y + 1/2) 以查看(中)点与线的距离,并据此决定是否增加y,但距离无关紧要,只有它的符号。对于第一象限,如果直线高于中点(x + 1, y + 1/2),则距离将为正,如果低于中点,则距离为负。如果线在中点之上,我们必须“向上”。

      所以我们知道初始(x, y)a*x + b*y + c = 0,但是(x + 1, y + 1/2) 呢?误差项等于a + b/2。我们不喜欢一半,所以我们将ab乘以(左移)1,得到2*a + b的初始误差,这就是右移的原因。如果错误为正,则该线在中点(x + 1, y + 1/2) 上方,我们必须向上。如果为负数,则该线低于中点,我们将 y 保留为。我们在每一步都逐步更新错误,具体取决于我们是否增加了y

      经过一番思考,您可以将算法扩展到所有象限。

      • if(dx >= dy) 测试背后的逻辑是什么?

      我们测试线路的陡度。这使得交换变得不必要。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-10-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多