实现布雷森汉姆的圆绘图算法

2022-09-04 22:21:22

我写了一个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;
}

我还创建了一个基于三角公式(和)的圆图绘制算法的实现,以及使用对标准drawArc方法(在图形对象上可用)的调用的第三个实现。这些额外的实现的唯一目的是将Bresenham的算法与它们进行比较。x=R*Math.cos(angle)y= R*Math.sin(angle)

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

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的绘画方法,绘制一堆圆圈并测量每种类型绘制所需的时间。以下是绘画方法:

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 and other methods

不幸的是,我的布雷森汉姆的实施非常缓慢。我采取了许多比较措施,布雷森汉姆的实施不仅比三角函数方法慢,而且比三角函数方法慢。请看以下针对绘制的各种圆圈数的度量值。Graphics.drawArc

实施的哪个部分更耗时?我可以使用任何解决方法来改进它吗?感谢您的帮助。

Comparing Bresenham to other implementations

[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;
    }
}

答案 1

你的布雷森汉姆方法本身并不慢,它只是相对较慢。

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

然而,你的三角函数方法不必要地快,因为你没有在平等的基础上将其与布雷森汉姆进行比较。

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

angle = angle + Math.PI/36  

同时,您的Bresenham方法是半径相关的,在每次像素变化时计算一个值。当每个八分音符产生点时,将其乘以并除以将得到等效的角度分辨率。因此,为了与布雷森汉姆方法处于平等地位,您的trig方法因此应具有:sqrt(2)82*Pi

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

在循环之外的某个地方,并按它递增,如:for

angle += resolution

由于我们现在将返回到像素级分辨率,因此您实际上可以改进trig方法,并删除对和的后续调用和分配,消除不必要的强制转换,并进一步减少对 .以下是整个新方法:drawlinex0y0Math

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);
    }

}

trig 方法现在将更频繁地执行几个数量级,具体取决于 的大小。r

我很想看看你的结果。


答案 2

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

这也意味着Bresenham的算法将始终产生一个光滑的圆,而随着半径的增加,你的三角函数方法将产生更糟糕的圆。

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

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

三角函数:181 毫秒,73 分平均
布雷森纳姆: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 点平均
布雷森纳姆: 120 毫秒, 867.568 点平均