关于Bresenham算法的求助

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Bresenham算法的求助相关的知识,希望对你有一定的参考价值。

昨天看到了Bresenham算法画直线的函数,感觉自己理解不了其思路,有些网友还说网上的某些说法不正确,我直接晕了~那种思路让我似懂非懂..希望理解的高人帮我用简单的语言解释下,感谢!!!

今天一下子遇到三个类似的问题,所以我这篇东西就连续复制粘贴了三遍:

(下面的坐标本来是有下标的,但复制过来就变没了,你可能看的有点晕)

Bresenham算法是Bresenham提出的一种光栅线生成算法!

         DDA算法表面上看起来很有效,并且代码也比较容易实现,但是显示每个像素都需要进行一次浮点数加法运算,而Bresenham算法的最大优点是不需要进行浮点数运算!这是一种精确而有效的光栅线生成算法,该算法仅使用增量整数计算,计算速度比DDA要快,另外,Bresenham算法还可用于显示圆和其他曲线,这里暂时只显示直线!

         与DDA一样,我们假设线段的两个端点坐标是整数值(x0,y0)(xEnd,yEnd),且斜率m满足0<=m>=1! 坐标轴的垂直轴表示扫描线位置,水平轴标识像素列,假设以单位x间隔取样,需要确定下一个每次取样时两个可能的像素位置中的哪一个更接近于线路径!

         从给定线段的左端点(x0,y0)开始,逐步处理每个后继列(x位置),并在其扫描线y值最接近线段的像素处描出一点,假如已经确定要显示的像素在(xk,yk),那么下一步就要确定在列xk+1=xk+1上绘制哪个像素,是在位置(xk+1,yk)还是在(xk+1,yk+1)

        在取样位置xk+1,我们使用dlower和dupper来标识两个像素与数学上线路径的垂直偏移(就是通过这两个值来比较哪个点离线上的点最近,以下推导过程你可能看得有点晕,但都是为了推出后续的点而已,你可以结合下面例子程序中的Bresenham函数来看),在像素列xk+1处的直线上的y坐标根据直线方程可计算得:

                                    y=m(xk+1)+b

那么可求得:

                                  dlower=y-yk=m(xk+1)+b-yk

                                             dupper=( yk+1)-y= yk+1-m(xk+1)-b

令斜率m=dy/dx,引入决策参数Pk,定义为:

                 Pk=dx(dlower- dupper)

 =2dx*xk-2dy*yk+c

C是一个常数,值为2dx+dx(2b-1)

由此可以计算得到

                pk+1= Pk +2dy-2dx(yk+1-yk) 

其中yk+1-yk取0还是取1取决于参数Pk的符号,Pk为负时取0,Pk非负时取1!

而Pk为负时,下一个要绘制的点就是(xk+1,yk)且pk+1= Pk +2dy

Pk为非负时则下一个要绘制的点就是(xk+1,yk+1)且pk+1= Pk +2dy-2dx

至此,Bresenham算法介绍完毕,以下为某个示例:

 

#include <gl/glut.h>

#include <math.h>

#include <stdio.h>

 

void draw_pixel(int ix,int iy)

       glBegin(GL_POINTS);

       glVertex2i(ix,iy);

       glEnd();

 

void Bresenham(int x1,int y1,int xEnd,int yEnd)

       int dx=abs(xEnd-x1),dy=abs(yEnd-y1);

       int p=2*dy-dx;

       int twoDy=2*dy,twoDyMinusDx=2*dy-2*dx;

       int x,y;

       if (x1>xEnd)

       

                x=xEnd;y=yEnd;

                xEnd=x1;

       

       else

       

                x=x1;

                y=y1;

       

       draw_pixel(x,y);

       while(x<xEnd)

       

                x++;

                if(p<0)

                          p+=twoDy;

                else

                

                          y++;

                          p+=twoDyMinusDx;

                          draw_pixel(x,y);

                

       

 

void display()

       glClear(GL_COLOR_BUFFER_BIT);

       Bresenham(0,0,400,400);

    glFlush();

 

void myinit()

       glClearColor(0.8,1.0,1.0,1.0);

       glColor3f(0.0,0.0,1.0);

       glPointSize(1.0);

       glMatrixMode(GL_PROJECTION);

       glLoadIdentity();

       gluOrtho2D(0.0,500.0,0.0,500.0);

       

 

 

void main(int argc,char **argv )

       glutInit(&argc,argv);

       glutInitDisplayMode(GLUT_SINGLE|GLUT_RGB);

       glutInitWindowSize(500,500);

       glutInitWindowPosition(200.0,200.0);

       glutCreateWindow("CG_test_Bresenham_Line example");

       glutDisplayFunc(display);

       myinit();

       glutMainLoop();

       

运行效果:

参考技术A void Bresenham_Line(x0, y0, x1, y1, color)
int x0, y0, x1, y1, color;
int x, y, dx, dy;
float k, e;
dx=x1-x0; dy=y1-y0;
k=dy/dx; e=-0.5;
x=x0; y=y0;
for(i=0; i<=dx; i++)
putpixel(x, y, color);
x=x+1; e=e+k;
if(e>=0)
y=y+1; e=e-1;

算法原理:过各行、 各列像素中心构造一组虚拟网格线, 按直线从起点到终点的顺序计算直线与各垂直网格线的交点, 然后确定该列像素中与该交点最近的像素

实现 Bresenham 的画圆算法

【中文标题】实现 Bresenham 的画圆算法【英文标题】:Implementing Bresenham's circle drawing algorithm 【发布时间】:2015-06-10 09:11:18 【问题描述】:

我已经编写了 Bresenham 的画圆算法的实现。 该算法利用了圆的高度对称性(它只计算第一个八分圆的点,并利用对称性绘制其他点)。因此,我期望它会非常快。图形编程黑皮书,第 35 章的标题是“Bresenham 很快,而且很快就是好”,虽然是关于画线算法,但我可以合理地期望画圆算法也是快(因为原理是一样的)。

这是我的 java,swing 实现

public static void drawBresenhamsCircle(int r, double width, double height, Graphics g) 
    int x,y,d;
    y = r;
    x = 0;

    drawPoint(x, y, width, height,g);
    d = (3-2*(int)r);
    while (x <= y) 
        if (d <= 0) 
            d = d + (4*x + 6);
         else 
            d = d + 4*(x-y) + 10;
            y--;
        
        x++;

        drawPoint(x, y, width, height,g);

        drawPoint(-x, y, width, height,g);
        drawPoint(x, -y, width, height,g);

        drawPoint(-x, -y, width, height,g);
        drawPoint(y, x, width, height,g);
        drawPoint(-y, x, width, height,g);
        drawPoint(y, -x, width, height,g);

        drawPoint(-y, -x, width, height,g);
       

此方法使用以下drawPoint方法:

public static void drawPoint(double x, double y,double width,double height, Graphics g) 
    double nativeX = getNativeX(x, width);
    double nativeY = getNativeY(y, height);
    g.fillRect((int)nativeX, (int)nativeY, 1, 1);

getNativeX 和 getNativeY 这两个方法用于将坐标从屏幕左上角切换到以更经典的轴方向为原点的面板中心的系统。

public static double getNativeX(double newX, double width) 
    return newX + (width/2);


public static double getNativeY(double newY, double height) 
    return (height/2) - newY;

我还创建了一个基于三角公式(x=R*Math.cos(angle)y= R*Math.sin(angle))的圆绘制算法的实现,以及使用标准 drawArc 方法(在 Graphics 对象上可用)的第三个实现。这些附加实现的唯一目的是比较 Bresenham 的算法。

然后我创建了绘制一堆圆圈的方法,以便能够很好地衡量所花费的时间。这是我使用 Bresenham 算法绘制一堆圆圈的方法

public static void drawABunchOfBresenhamsCircles(int numOfCircles, double width, double height, Graphics g) 
    double r = 5;
    double step = (300.0-5.0)/numOfCircles;

    for (int i = 1; i <= numOfCircles; i++) 
        drawBresenhamsCircle((int)r, width, height, g);
        r += step;
    

最后,我重写了我正在使用的 JPanel 的绘制方法,以绘制一堆圆圈并测量每种类型绘制所花费的时间。这是paint方法:

public void paint(Graphics g) 
    Graphics2D g2D = (Graphics2D)g;

    g2D.setColor(Color.RED);

    long trigoStartTime = System.currentTimeMillis();
    drawABunchOfTrigonometricalCircles(1000, this.getWidth(), this.getHeight(), g);
    long trigoEndTime = System.currentTimeMillis();
    long trigoDelta = trigoEndTime - trigoStartTime;

    g2D.setColor(Color.BLUE);

    long bresenHamsStartTime = System.currentTimeMillis();
    drawABunchOfBresenhamsCircles(1000, this.getWidth(), this.getHeight(), g);
    long bresenHamsEndTime = System.currentTimeMillis();
    long bresenDelta = bresenHamsEndTime - bresenHamsStartTime;

    g2D.setColor(Color.GREEN);

    long standardStarTime = System.currentTimeMillis();
    drawABunchOfStandardCircles(1000, this.getWidth(), this.getHeight(),g);
    long standardEndTime = System.currentTimeMillis();
    long standardDelta = standardEndTime - standardStarTime;

    System.out.println("Trigo : " + trigoDelta  + " milliseconds");
    System.out.println("Bresenham :" + bresenDelta +  " milliseconds");
    System.out.println("Standard :" + standardDelta +  " milliseconds");

这是它会生成的渲染类型(每种类型绘制 1000 个圆圈)

不幸的是,我的 Bresenham 的实施非常缓慢。我采取了许多比较措施,Bresenham 的实现不仅比Graphics.drawArc 慢,而且比三角方法慢。查看以下针对绘制的不同数量的圆圈的度量。

我的实施的哪一部分更耗时?有什么解决方法可以用来改进它吗?感谢您的帮助。

[EDITION]:根据@higuaro 的要求,这是我绘制圆的三角算法

public static void drawTrigonometricalCircle (double r, double width, double height, Graphics g) 

    double x0 = 0;
    double y0 = 0;
    boolean isStart = true;

    for (double angle = 0; angle <= 2*Math.PI; angle = angle + Math.PI/36) 

        double x = r * Math.cos(angle);
        double y = r * Math.sin(angle);

        drawPoint((double)x, y, width, height, g);

        if (!isStart) 
            drawLine(x0,  y0, x, y, width, height, g);
        

        isStart = false;

        x0 = x;
        y0 = y;
    

以及画一堆三角圆的方法

public static void drawABunchOfTrigonometricalCircles(int numOfCircles, double width, double height, Graphics g) 

    double r = 5;
    double step = (300.0-5.0)/numOfCircles;

    for (int i = 1; i <= numOfCircles; i++) 
        drawTrigonometricalCircle(r, width, height, g);
        r += step;
    

【问题讨论】:

如果我见过这样的问题,这是一个很好的问题。 您是否也在使用fillRect 在三角解中画一个点?我的第一个想法是,如果您正在考虑速度,那可能不是画一个点的最佳方式。您还可以通过消除getNativeXgetNativeY 来节省一点时间;您必须在您的 drawBressenhamsCircle 方法中保留 8 个值,您将在增加或减少 xy 的同时增加或减少这些值。但是,这会使事情变得丑陋,我怀疑它会节省那么多时间。 正如here 建议的那样,drawArc() 利用主机平台的实现; profile 比较其他两个。 Native 会更快,原因有很多(例如,直接访问帧缓冲区以进行无中断像素绘制),因此它应该不在比较范围内。如果你能发布你的三角圆绘图程序来看看为什么比你的 Bressenham 跑得快,那就太好了 只是一个愚蠢的事情,但你的 widthheight 变量是 double 而不是 integer,像素绘图也使用 double 所以浮点数和整数之间的转换是我的赌注瓶颈....(但我不是 JAVA 编码器,所以可能是双重意味着不同于我习惯的东西) 【参考方案1】:

您的 Bresenham 方法本身并不慢,只是比较慢。

Swing 的drawArc() 实现依赖于机器,使用本机代码。你永远不会用 Java 打败它,所以不要费心去尝试。 (实际上,我很惊讶 Java Bresenham 方法与 drawArc() 相比速度如此之快,这证明了执行 Java 字节码的虚拟机的质量。)

但是,您的三角法速度过快,因为您没有在同等基础上将其与 Bresenham 进行比较。

trig 方法的角度分辨率设置为PI/36(~4.7 度),如for 语句末尾的此操作:

angle = angle + Math.PI/36  

同时,您的 Bresenham 方法依赖于半径,在每个像素变化时计算一个值。由于每个八分圆产生sqrt(2) 点,将其乘以8 并除以2*Pi 将为您提供等效的角度 分辨率。因此,要与 Bresenham 方法处于同等地位,您的 trig 方法应该具有:

resolution = 4 * r * Math.sqrt(2) / Math.PI;

在循环之外的某个地方,并通过它增加你的for

angle += resolution

由于我们现在将回到像素级分辨率,因此您实际上可以改进 trig 方法并删除随后的 drawline 调用和对 x0y0 的分配,消除不必要的强制转换,并进一步减少调用到Math。这是新方法的全部内容:

public static void drawTrigonometricalCircle (double r, double width, double height, 
    Graphics g) 

    double localPi = Math.PI;
    double resolution = 4 * r * Math.sqrt(2) / Math.PI;

    for (double angle = 0; angle <= localPi; angle += resolution) 
        double x = r * Math.cos(angle);
        double y = r * Math.sin(angle);
        drawPoint(x, y, width, height, g);
    


根据r 的大小,现在 trig 方法的执行频率会提高几个数量级。

我很想看看你的结果。

【讨论】:

【参考方案2】:

您的问题在于 Bresenham 的算法根据圆的大小进行可变次数的迭代,而您的三角方法总是进行固定次数的迭代。

这也意味着 Bresenham 的算法总是会产生一个看起来很光滑的圆,而随着半径的增加,你的三角法会产生看起来更差的圆。

为了使其更均匀,请更改三角法以产生与 Bresenham 实现大致一样多的点,您将看到它的速度有多快。

我编写了一些代码来对此进行基准测试并打印产生的点数,这是初步结果:

三角函数:181 毫秒,平均 73 分 Bresenham:120 毫秒,平均 867.568 分

修改三角函数类以进行更多迭代以获得更平滑的圆后:

    int totalPoints = (int)Math.ceil(0.7 * r * 8);
    double delta = 2 * Math.PI / totalPoints;
    for (double angle = 0; angle <= 2*Math.PI; angle = angle + delta) 

这些是结果:

三角函数:2006 毫秒,平均 854.933 点 Bresenham:120 毫秒,平均 867.568 分

【讨论】:

【参考方案3】:

我最近自己为精灵光栅化器编写了一个 bresenham 圆绘图实现,并尝试对其进行一些优化。我不确定它是否会比你做的更快或更慢,但我认为它应该有一个相当不错的执行时间。

不幸的是,它是用 C++ 编写的。如果我明天有时间,我可能会使用移植的 Java 版本和结果的示例图片来编辑我的答案,但现在你必须自己做,如果你愿意(或者其他想要花时间编辑的人) .)

基本上,它的作用是使用 bresenham 算法获取圆外边缘的位置,然后对圆的 1/8 执行该算法,并通过从中心到外边缘。

Color 只是一个 rgba 值

Color* createCircleColorArray(const int radius, const Color& color, int& width, int& height) 
  // Draw circle with custom bresenham variation
  int decision = 3 - (2 * radius);
  int center_x = radius;
  int center_y = radius;
  Color* data;

  // Circle is center point plus radius in each direction high/wide
  width = height = 2 * radius + 1;
  data = new Color[width * height];

  // Initialize data array for transparency
  std::fill(data, data + width * height, Color(0.0f, 0.0f, 0.0f, 0.0f));

  // Lambda function just to draw vertical/horizontal straight lines
  auto drawLine = [&data, width, height, color] (int x1, int y1, int x2, int y2) 
    // Vertical
    if (x1 == x2) 
      if (y2 < y1) 
        std::swap(y1, y2);
      

      for (int x = x1, y = y1; y <= y2; y++) 
        data[(y * width) + x] = color;
      
    

    // Horizontal
    if (y1 == y2) 
      if (x2 < x1) 
        std::swap(x1, x2);
      

      for (int x = x1, y = y1; x <= x2; x++) 
        data[(y * width) + x] = color;
      
    
  ;

  // Lambda function to draw actual circle split into 8 parts
  auto drawBresenham = [color, drawLine] (int center_x, int center_y, int x, int y) 
    drawLine(center_x + x, center_y + x, center_x + x, center_y + y);
    drawLine(center_x - x, center_y + x, center_x - x, center_y + y);
    drawLine(center_x + x, center_y - x, center_x + x, center_y - y);
    drawLine(center_x - x, center_y - x, center_x - x, center_y - y);
    drawLine(center_x + x, center_y + x, center_x + y, center_y + x);
    drawLine(center_x - x, center_y + x, center_x - y, center_y + x);
    drawLine(center_x + x, center_y - x, center_x + y, center_y - x);
    drawLine(center_x - x, center_y - x, center_x - y, center_y - x);
  ;

  for (int x = 0, y = radius; y >= x; x++) 
    drawBresenham(center_x, center_y, x, y);

    if (decision > 0) 
      y--;
      decision += 4 * (x - y) + 10;
    
    else 
      decision += 4 * x + 6;
    
  

  return data;

//编辑 哦,哇,我才意识到这个问题有多老了。

【讨论】:

以上是关于关于Bresenham算法的求助的主要内容,如果未能解决你的问题,请参考以下文章

Bresenham画线算法

bresenham算法的算法

bresenham算法的原理

bresenham算法的Bresenham改进算法

Bresenham算法

Bresenham直线算法