Подтвердить что ты не робот

Реализация алгоритма рисования круга Bresenham

Я написал реализацию алгоритма рисования круга Bresenham. Эти алгоритмы используют высокосимметричные свойства круга (он вычисляет точки только с 1-го октанта и рисует другие точки, пользуясь симметриями). Поэтому я ожидал, что это будет очень быстро. Черная книга программирования графики, глава № 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). Эти дополнительные реализации предназначены исключительно для сравнения алгоритма Брешенема с ними.

Затем я создал методы, чтобы нарисовать кучу кругов, чтобы иметь возможность получить хорошие оценки затраченного времени. Вот метод, который я использую, чтобы нарисовать кучу кругов с использованием алгоритма Брешенема

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

К сожалению, моя реализация Bresenham очень медленная. Я принял множество сравнительных мер, и реализация Брешенема не только медленнее, чем 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;
    }
}
4b9b3361

Ответ 1

Ваша проблема заключается в том, что алгоритм Брешенема выполняет переменное число итераций в зависимости от размера круга, тогда как ваш тригонометрический подход всегда фиксирует число итераций.

Это также означает, что алгоритм Брешенема всегда будет создавать гладкий круг, в то время как ваш тригонометрический подход будет создавать худшие круги, по мере увеличения радиуса.

Чтобы сделать его более ровным, измените тригонометрический подход, чтобы получить примерно столько же очков, что и реализация 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 балла средний

Ответ 2

Ваш метод Бресенхема не является медленным как таковым, он сравнительно медленный.

Swing drawArc() реализация зависит от машины, используя собственный код. Вы никогда не будете бить его с помощью Java, поэтому не беспокойтесь. (На самом деле я удивлен, что метод Java Bressenham так же быстро, как его сравнивают с drawArc(), свидетельством качества виртуальной машины, исполняющей байт-код Java.)

Ваш тригонометрический метод, однако, излишне быстрый, потому что вы не сравниваете его с Брессенемом на равной основе.

Метод триггера имеет разрешение angular PI/36 (~ 4.7 градусов), как в этой операции в конце инструкции for:

angle = angle + Math.PI/36  

Между тем, ваш метод Брессенхама зависит от радиуса, вычисляя значение при каждом изменении пикселя. Поскольку каждый октант создает точки sqrt(2), умножая это на 8 и деля на 2*Pi, вы получите эквивалентное разрешение angular. Поэтому, чтобы быть на равных с методом Брессенхема, ваш метод триггера должен поэтому иметь:

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

где-то вне цикла, и добавьте его for к нему, как в:

angle += resolution

Так как теперь мы вернемся к разрешениям на уровне пикселей, вы можете фактически улучшить метод триггера и вырезать последующий вызов drawline и назначения x0 и y0, исключить ненужные нажатия, а также уменьшить количество вызовов до 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.

Мне было бы интересно увидеть ваши результаты.