如何使用Bresenham的亚像素偏差线绘制算法?

时间:2016-12-17 06:27:47

标签: algorithm line rasterizing bresenham

Bresenham's line drawing algorithm众所周知,实施起来非常简单。

虽然有更多高级方法可以绘制反对线,但我有兴趣编写一个基于浮点坐标绘制单个像素宽度非抗锯齿线的函数。

这意味着虽然第一个和最后一个像素将保持不变,但它们之间绘制的像素将具有基于两个端点的子像素位置的偏差。

原则上这不应该是那么复杂,因为我认为可以使用子像素偏移来计算绘制线时使用的初始error值,以及所有其他部分算法保持不变。

  

没有子像素偏移:

X###
    ###X

假设右手点有一个靠近顶部的子像素位置,该线条可能如下所示:

  

以子像素偏移为例:

X######
       X

是否有尝试过的&绘制一条考虑子像素坐标的线的真实方法?

注意:

  • 这似乎是一个常见的操作,我已经看到OpenGL驱动程序将此考虑在内 - 例如 - 使用GL_LINE,虽然从快速搜索我没有在线找到任何答案 - 也许使用错误的搜索字词?
  • 一眼就看出这个问题看起来可能与以下内容重复:
    Precise subpixel line drawing algorithm (rasterization algorithm)了然而这就是要求绘制一条宽线,这就是要求偏移一条像素线。
  • 如果没有一些标准方法,我会尝试将其写下来作为答案发布。

3 个答案:

答案 0 :(得分:2)

刚刚遇到同样的挑战,我可以确认这是可能的,如你所料。

首先,回到最简单的算法形式:(忽略分数;它们会在以后消失)

x = x0
y = y0
dx = x1 - x0
dy = y1 - y0
error = -0.5
while x < x1:
    if error > 0:
        y += 1
        error -= 1
    paint(x, y)
    x += 1
    error += dy/dx

这意味着对于整数坐标,我们从像素边界(error = -0.5)上方的半个像素开始,对于我们在x中前进的每个像素,我们增加理想的y坐标(因此当前errordy/dx

首先让我们看看如果我们停止强制x0y0x1y1为整数会发生什么:(这也会假设不使用像素中心,坐标相对于每个像素的左上角,因为一旦你支持子像素位置,你可以简单地将像素宽度的一半加到x和y以返回以像素为中心的逻辑)

x = x0
y = y0
dx = x1 - x0
dy = y1 - y0
error = (0.5 - (x0 % 1)) * dy/dx + (y0 % 1) - 1
while x < x1:
    if error > 0:
        y += 1
        error -= 1
    paint(x, y)
    x += 1
    error += dy/dx

唯一的变化是初始误差计算。当x在像素中心时,新值来自简单的trig以计算y坐标。值得注意的是,您可以使用相同的想法将线的起始位置限制在一定范围内,这是您想要开始优化时可能遇到的另一个挑战。

现在我们只需将其转换为仅整数算术。对于小数输入(scale),我们需要一些固定的乘数,并且可以通过将它们相乘来处理除法,就像标准算法一样。

# assumes x0, y0, x1 and y1 are pre-multiplied by scale
x = x0
y = y0
dx = x1 - x0
dy = y1 - y0
error = (scale - 2 * (x0 % scale)) * dy + 2 * (y0 % scale) * dx - 2 * dx * scale
while x < x1:
    if error > 0:
        y += scale
        error -= 2 * dx * scale
    paint(x / scale, y / scale)
    x += scale
    error += 2 * dy * scale

请注意,xydxdy保持与输入变量(scale)相同的缩放系数,而error }具有更复杂的缩放因子:2 * dx * scale。这使它能够吸收原始配方中的分裂和分数,但意味着我们需要在使用它的任何地方应用相同的比例。

显然这里有很多优化空间,但这是基本算法。如果我们假设scale是2的幂(2^n),我们可以开始提高效率:

dx = x1 - x0
dy = y1 - y0
mask = (1 << n) - 1
error = (2 * (y0 & mask) - (2 << n)) * dx - (2 * (x0 & mask) - (1 << n)) * dy
x = x0 >> n
y = y0 >> n
while x < (x1 >> n):
    if error > 0:
        y += 1
        error -= 2 * dx << n
    paint(x, y)
    x += 1
    error += 2 * dy << n

与原版一样,这仅适用于(x> = y,x> 0,y> = 0)八分圆。通常的规则适用于将其扩展到所有情况,但请注意,由于坐标不再在像素中居中(即反射变得更复杂),因此存在一些额外的陷阱。

您还需要注意整数溢出:error的精度是输入变量的两倍,范围是该行长度的两倍。相应地计划输入,精度和变量类型!

答案 1 :(得分:2)

我遇到了类似的问题,除了需要子像素端点外,我还需要确保 all pixels which intersect the line are drawn

我不确定我的解决方案是否对 OP 有帮助,因为它已经 4 年多,而且因为这句话“这意味着第一个和最后一个像素将保持不变......”对我来说,这实际上是一个问题(稍后会详细介绍)。希望这可能对其他人有所帮助。


我不知道这是否可以认为是 Bresenham 的算法,但它非常相似。我将在 (+,+) 象限中解释它。假设您希望从点 (Px,Py) 到 (Qx,Qy) 在宽度为 W 的像素网格上。网格宽度 W > 1 允许子像素端点。

对于(+,+)象限中的一条线,起点很容易计算,只需取(Px,Py)的底.正如您稍后将看到的,这只适用于 Qx >= Px & Qy >= Py.

endpoints floored

现在您需要找到下一个要转到的像素。有 3 种可能性:(x+1,y)、(x,y+1)、&(x+1,y+1)。为了做出这个决定,我使用了定义为的 2D 叉积:

enter image description here

  • 如果此值为负,则向量 b 是向量 a 的右/顺时针方向。
  • 如果此值为正,则向量 b 是向量 a 的左/逆时针方向。
  • 如果该值为零,则矢量 b 与矢量 a 指向相同的方向。

要决定下一个像素是哪个像素,请比较线 P-Q [下图中的红色] 与点之间的线之间的叉积 < strong>P 和右上角的像素 (x+1,y+1) [下图中的蓝色]。 cross-product diagram

P 和右上角像素之间的向量可以计算为: BLUE vector calculation

因此,我们将使用 2D 叉积的值: full cross product maths

  • 如果此值为负,则下一个像素将为 (x,y+1)。
  • 如果此值为正,则下一个像素将为 (x+1,y)。
  • 如果该值恰好为零,则下一个像素将为 (x+1,y+1)。

这适用于起始像素,但其余像素不会有位于它们内部的点。幸运的是,在初始点之后,您不需要一个点蓝色矢量的像素内。你可以像这样继续扩展它: vectors outside the current pixel 蓝色矢量从线条的起点开始,并针对每个像素更新为 (x+1,y+1)。取哪个像素的规则是一样的。如您所见,红色矢量位于蓝色矢量的右侧。因此,下一个像素将是绿色像素的右侧。

需要为每个像素更新叉积的值,具体取决于您采用的像素。 dx dy

如果下一个像素是 (x+1),则添加 dx,如果下一个像素是 (x+1),则添加 dy像素为 (y+1)。如果像素变为 (x+1,y+1),则将两者相加。

重复这个过程,直到到达结束像素,(Qx / W, Qy / W).

所有这些都导致了以下代码:

    int dx = x2 - x2;
    int dy = y2 - y1;
    int local_x = x1 % width;
    int local_y = y1 % width;
    int cross_product = dx*(width-local_y) - dy*(width-local_x);
    int dx_cross = -dy*width;
    int dy_cross = dx*width;

    int x = x1 / width;
    int y = y1 / width;
    int end_x = x2 / width;
    int end_y = y2 / width;
    while (x != end_x || y != end_y) {
        SetPixel(x,y,color);
        int old_cross = cross_product;
        if (old_cross >= 0) {
            x++;
            cross_product += dx_cross;
        }
        if (old_cross <= 0) {
            y++;
            cross_product += dy_cross;
        }
    }

使其适用于所有象限是反转局部坐标和一些绝对值的问题。这是适用于所有象限的代码:

    int dx = x2 - x1;
    int dy = y2 - y1;
    int dx_x = (dx >= 0) ? 1 : -1;
    int dy_y = (dy >= 0) ? 1 : -1;
    int local_x = x1 % square_width;
    int local_y = y1 % square_width;
    int x_dist = (dx >= 0) ? (square_width - local_x) : (local_x);
    int y_dist = (dy >= 0) ? (square_width - local_y) : (local_y);
    int cross_product = abs(dx) * abs(y_dist) - abs(dy) * abs(x_dist);
    dx_cross = -abs(dy) * square_width;
    dy_cross = abs(dx) * square_width;

    int x = x1 / square_width;
    int y = y1 / square_width;
    int end_x = x2 / square_width;
    int end_y = y2 / square_width;

    while (x != end_x || y != end_y) {
        SetPixel(x,y,color);
        int old_cross = cross_product;
        if (old_cross >= 0) {
            x += dx_x;
            cross_product += dx_cross;
        }
        if (old_cross <= 0) {
            y += dy_y;
            cross_product += dy_cross;
        }
    }

但是有个问题!在某些情况下,此代码不会停止。要理解其中的原因,您需要真正了解哪些条件算作直线和像素之间的交点。

一个像素是什么时候绘制的?

我说过我需要绘制与线相交的所有像素。但在边缘情况下存在一些歧义。

这里是所有可能的交叉点的列表,在这些交叉点中,Qx >= Px & Qy >= Py:

all possible intersections

  • A - 如果一条线与像素完全相交,则将绘制该像素。
  • B - 如果垂直线与像素完全相交,则将绘制该像素。
  • C - 如果水平线与像素完全相交,则将绘制该像素。
  • D - 如果垂直线完全接触像素的左侧,则将绘制该像素。
  • E - 如果水平线完全接触像素的底部,则将绘制该像素。
  • F - 如果线端点从像素内部开始(+,+),则将绘制像素。
  • G - 如果线端点恰好在像素的左侧开始(+,+),则将绘制该像素。
  • H - 如果线端点刚好从像素的底部开始(+,+),则将绘制该像素。
  • I - 如果线端点刚好从像素的左下角开始(+,+),则将绘制该像素。

这里有一些像素与线相交: invalid intersections

  • A' - 如果一条线明显不与像素相交,则该像素将被绘制。

  • B' - 如果垂直线明显不与像素相交,则该像素将被绘制。

  • C' - 如果水平线明显不与像素相交,则该像素将被绘制。

  • D' - 如果垂直线正好接触像素的右侧,则该像素将被绘制。

  • E' - 如果水平线正好接触像素的顶部,则该像素将被绘制。

  • F' - 如果线端点恰好在 (+,+) 方向上的像素的右上角开始,则该像素将被绘制。

  • G' - 如果线端点恰好在 (+,+) 方向上的像素的顶部开始,则该像素将被绘制。

  • H' - 如果线端点恰好在 (+,+) 方向上的像素的右侧开始,则该像素将被绘制。

  • I' - 如果一条线正好接触像素的一个角,则该像素将被绘制。这适用于所有角落。


这些规则如您所料(只需翻转图像)适用于其他象限。我需要强调的问题是端点何时恰好位于像素的边缘。看看这个案例: misleading endpoint

这就像上面的图像 G',除了 y 轴被翻转是因为 Qy < Py< /强>。有 4x4 的红点,因为 W 是 4,因此像素尺寸为 4x4。 4 个点中的每一个都是一条线可以触及的唯一端点。绘制的线从 (1.25, 1.0) 到(某处)。

这说明了为什么说像素端点可以计算为线端点的底线是不正确的(至少我是如何定义像素线交叉点的)。该端点的地板像素坐标似乎是 (1,1),但很明显,该线从未真正与该像素相交。只是碰了一下,不想画了。

您需要对最小端点进行铺平,而不是对线端点进行铺平,并将最大端点在 x 和 y 维度上减去 1。

所以最后这里是执行此地板/天花板的完整代码:

    int dx = x2 - x1;
    int dy = y2 - y1;
    int dx_x = (dx >= 0) ? 1 : -1;
    int dy_y = (dy >= 0) ? 1 : -1;
    int local_x = x1 % square_width;
    int local_y = y1 % square_width;
    int x_dist = (dx >= 0) ? (square_width - local_x) : (local_x);
    int y_dist = (dy >= 0) ? (square_width - local_y) : (local_y);
    int cross_product = abs(dx) * abs(y_dist) - abs(dy) * abs(x_dist);
    dx_cross = -abs(dy) * square_width;
    dy_cross = abs(dx) * square_width;

    int x = x1 / square_width;
    int y = y1 / square_width;
    int end_x = x2 / square_width;
    int end_y = y2 / square_width;

    // Perform ceiling/flooring of the pixel endpoints
    if (dy < 0)
    {
        if ((y1 % square_width) == 0)
        {
            y--;
            cross_product += dy_cross;
        }
    }
    else if (dy > 0)
    {
        if ((y2 % square_width) == 0)
            end_y--;
    }

    if (dx < 0)
    {
        if ((x1 % square_width) == 0)
        {
            x--;
            cross_product += dx_cross;
        }
    }
    else if (dx > 0)
    {
        if ((x2 % square_width) == 0)
            end_x--;
    }

    while (x != end_x || y != end_y) {
        SetPixel(x,y,color);
        int old_cross = cross_product;
        if (old_cross >= 0) {
            x += dx_x;
            cross_product += dx_cross;
        }
        if (old_cross <= 0) {
            y += dy_y;
            cross_product += dy_cross;
        }
    }

此代码本身尚未经过测试,但在我的 GitHub project 中对其进行了测试。

答案 2 :(得分:1)

我们假设您想要从P1 = (x1, y1)P2 = (x2, y2)绘制一条线,其中所有数字都是浮点像素坐标。

  1. 计算P1P2的真实像素坐标并绘制它们:P* = (round(x), round(y))

  2. 如果abs(x1* - x2*) <= 1 && abs(y1* - y2*) <= 1,那么你就完成了。

  3. 决定它是水平(true)还是垂直(false):abs(x1 - x2) >= abs(y1 - y2)

  4. 如果是水平行和x1 > x2,或者它是垂直行和y1 > y2:swap {{1 } P1(以及P2P1*)。

  5. 如果是水平行,您可以使用以下公式获取P2*x1*之间所有x坐标的y坐标:< / p>

    x2*

    如果您有垂直行,则可以使用以下公式获取y(x) = round(y1 + (x - x1) / (x2 - x1) * (y2 - y1)) y1*之间所有y坐标的x坐标:

    y2*
  6. Here is a demo你可以玩,你可以在第12行尝试不同的点。