【问题标题】:How to use Bresenham's line drawing algorithm with clipping?如何使用 Bresenham 的线条绘制算法进行裁剪?
【发布时间】:2017-04-14 13:24:36
【问题描述】:

Bresenham line drawing algorithm画线时, 其中线可能不在要写入的位图的范围内 - 剪辑结果以使其适合正在写入的图像的轴对齐范围内会很有用。

虽然可以先将线条剪裁到矩形,然后再绘制线条。这并不理想,因为它通常会给线 带来稍微不同的倾斜度(假设正在使用 int 坐标)

既然这是一种原始的操作,那么是否有既定的方法可以在保持相同形状的同时剪断线条?

如果有帮助,here is a reference implementation of the algorithm - 它使用 int 坐标,从而避免在绘制线条时进行 int/float 转换。


我花了一些时间研究这个:

【问题讨论】:

  • 如果你使用浮点坐标,剪裁不应该改变倾斜。您在寻找像素完美匹配吗?
  • 如果可能的话,我希望能够继续使用 int 坐标以避免大量的 float/int 转换,因为当前代码使用 int 坐标非常有效。我假设已发布有关此主题的论文,因为仅使用浮点坐标效率不高-在问题中添加了指向参考代码的链接。
  • 我没有完整的代码答案,但是您应该能够将梯度乘以有理数,并将余数用作初始增量。 IOW,如果您的线从边界左侧的x0 像素开始并且具有正梯度dy/dx,那么您使用x0 * dy(例如,可能需要从int 扩大到long)。然后除以dx 以获得yx0 值,并使用余数为该位置初始化delta。这有帮助吗?
  • IMO 的诀窍是首先将线与 包括累积误差项的边界框相交,然后在相交点之间运行 Bresenham。
  • 不,这意味着:相交两条线(预期线加上 BB 的边之一)。线性代数。对于北边,您知道y(精确),并计算x + x_debt。其他路口类似。

标签: algorithm line rasterizing bresenham


【解决方案1】:

让我们重新定义问题,这样我们就可以看到 Bresenham 的算法是如何工作的......

假设您正在绘制一条从(x0,y0)(x1,y1) 的大部分水平线(大部分垂直的方法相同,但轴已切换):

整行可以用x(所有整数)来描述为y的函数:

y = y0 + round( (x-x0) * (y1-y0) / (x1-x0) )

这准确地描述了您在绘制整条线时将绘制的每个像素,并且当您一致地剪裁线时,它仍然准确地描述了您将绘制的每个像素 - 您只需将其应用于较小的x 值的范围。

我们可以使用所有整数数学来评估这个函数,分别计算除数和余数。对于x1 >= x0y1 >= y0(否则进行正常转换):

let dx = (x1-x0);
let dy = (y1-y0);
let remlimit = (dx+1)/2; //round up

rem = (x-x0) * dy % dx;
y = y0 + (x-x0) * dy / dx;
if (rem >= remlimit)
{
    rem-=dx;
    y+=1;
}

Bresenham 算法只是一种快速方法,可以在您更新x 时逐步更新此公式的结果。

以下是我们如何利用增量更新来绘制从 x=xs 到 x=xe 的同一行的部分:

let dx = (x1-x0);
let dy = (y1-y0);
let remlimit = (dx+1)/2; //round up

x=xs;
rem = (x-x0) * dy % dx;
y = y0 + (x-x0) * dy / dx;
if (rem >= remlimit)
{
    rem-=dx;
    y+=1;
}
paint(x,y);
while(x < xe)
{
    x+=1;
    rem+=dy;
    if (rem >= remlimit)
    {
        rem-=dx;
        y+=1;
    }
    paint(x,y);
}

如果你想对 0 进行余数比较,你可以在开头偏移它:

let dx = (x1-x0);
let dy = (y1-y0);
let remlimit = (dx+1)/2; //round up

x=xs;
rem = ( (x-x0) * dy % dx ) - remlimit;
y = y0 + (x-x0) * dy / dx;
if (rem >= 0)
{
    rem-=dx;
    y+=1;
}
paint(x,y);
while(x < xe)
{
    x+=1;
    rem+=dy;
    if (rem >= 0)
    {
        rem-=dx;
        y+=1;
    }
    paint(x,y);
}

【讨论】:

  • 虽然这种方法看起来是正确的,但我从 Kuzmin 和 Yevgeny P 的论文中找到了一个实现,它比这个答案更复杂——尽管它主要是检查极端情况并考虑两个轴。
  • 对不起@ideasman42,这并不难。好吧,你必须为你的剪裁矩形找到 xs 和 xe——x 边界很容易,但由于四舍五入,y 边界有点挑剔。如果您对此有疑问,请告诉我,我会显示计算结果。
  • 对,计算特定值并不难 - 但是我试图让剪辑和非剪辑版本完全为可见线段提供相同的结果。结果发现 Kuzmin 和 Yevgeny P. 的论文描述的方法存在一些差异(与裁剪无关)我误认为是算法中的错误,对斜率的处理略有不同 - 但似乎不是错误只是一个很小的区别。我已将代码作为单独的答案发布。
【解决方案2】:

根据 Kuzmin 和 Yevgeny P 的论文,可以使用 Bresenham 的算法,考虑到裁剪值:

为了完整起见,这里是该算法的一个工作版本,一个 Python 函数,虽然它只使用整数算术 - 因此可以轻松移植到其他语言。

def plot_line_v2v2i(
    p1, p2, callback,
    clip_xmin, clip_ymin,
    clip_xmax, clip_ymax,
):
    x1, y1 = p1
    x2, y2 = p2

    del p1, p2

    # Vertical line
    if x1 == x2:
        if x1 < clip_xmin or x1 > clip_xmax:
            return

        if y1 <= y2:
            if y2 < clip_ymin or y1 > clip_ymax:
                return
            y1 = max(y1, clip_ymin)
            y2 = min(y2, clip_ymax)
            for y in range(y1, y2 + 1):
                callback(x1, y)
        else:
            if y1 < clip_ymin or y2 > clip_ymax:
                return
            y2 = max(y2, clip_ymin)
            y1 = min(y1, clip_ymax)
            for y in range(y1, y2 - 1, -1):
                callback(x1, y)
        return

    # Horizontal line
    if y1 == y2:
        if y1 < clip_ymin or y1 > clip_ymax:
            return

        if x1 <= x2:
            if x2 < clip_xmin or x1 > clip_xmax:
                return
            x1 = max(x1, clip_xmin)
            x2 = min(x2, clip_xmax)
            for x in range(x1, x2 + 1):
                callback(x, y1)
        else:
            if x1 < clip_xmin or x2 > clip_xmax:
                return
            x2 = max(x2, clip_xmin)
            x1 = min(x1, clip_xmax)
            for x in range(x1, x2 - 1, -1):
                callback(x, y1)
        return

    # Now simple cases are handled, perform clipping checks.
    if x1 < x2:
        if x1 > clip_xmax or x2 < clip_xmin:
            return
        sign_x = 1
    else:
        if x2 > clip_xmax or x1 < clip_xmin:
            return
        sign_x = -1

        # Invert sign, invert again right before plotting.
        x1 = -x1
        x2 = -x2
        clip_xmin, clip_xmax = -clip_xmax, -clip_xmin

    if y1 < y2:
        if y1 > clip_ymax or y2 < clip_ymin:
            return
        sign_y = 1
    else:
        if y2 > clip_ymax or y1 < clip_ymin:
            return
        sign_y = -1

        # Invert sign, invert again right before plotting.
        y1 = -y1
        y2 = -y2
        clip_ymin, clip_ymax = -clip_ymax, -clip_ymin

    delta_x = x2 - x1
    delta_y = y2 - y1

    delta_x_step = 2 * delta_x
    delta_y_step = 2 * delta_y

    # Plotting values
    x_pos = x1
    y_pos = y1

    if delta_x >= delta_y:
        error = delta_y_step - delta_x
        set_exit = False

        # Line starts below the clip window.
        if y1 < clip_ymin:
            temp = (2 * (clip_ymin - y1) - 1) * delta_x
            msd = temp // delta_y_step
            x_pos += msd

            # Line misses the clip window entirely.
            if x_pos > clip_xmax:
                return

            # Line starts.
            if x_pos >= clip_xmin:
                rem = temp - msd * delta_y_step

                y_pos = clip_ymin
                error -= rem + delta_x

                if rem > 0:
                    x_pos += 1
                    error += delta_y_step
                set_exit = True

        # Line starts left of the clip window.
        if not set_exit and x1 < clip_xmin:
            temp = delta_y_step * (clip_xmin - x1)
            msd = temp // delta_x_step
            y_pos += msd
            rem = temp % delta_x_step

            # Line misses clip window entirely.
            if y_pos > clip_ymax or (y_pos == clip_ymax and rem >= delta_x):
                return

            x_pos = clip_xmin
            error += rem

            if rem >= delta_x:
                y_pos += 1
                error -= delta_x_step

        x_pos_end = x2

        if y2 > clip_ymax:
            temp = delta_x_step * (clip_ymax - y1) + delta_x
            msd = temp // delta_y_step
            x_pos_end = x1 + msd

            if (temp - msd * delta_y_step) == 0:
                x_pos_end -= 1

        x_pos_end = min(x_pos_end, clip_xmax) + 1
        if sign_y == -1:
            y_pos = -y_pos
        if sign_x == -1:
            x_pos = -x_pos
            x_pos_end = -x_pos_end
        delta_x_step -= delta_y_step

        while x_pos != x_pos_end:
            callback(x_pos, y_pos)

            if error >= 0:
                y_pos += sign_y
                error -= delta_x_step
            else:
                error += delta_y_step

            x_pos += sign_x
    else:
        # Line is steep '/' (delta_x < delta_y).
        # Same as previous block of code with swapped x/y axis.

        error = delta_x_step - delta_y
        set_exit = False

        # Line starts left of the clip window.
        if x1 < clip_xmin:
            temp = (2 * (clip_xmin - x1) - 1) * delta_y
            msd = temp // delta_x_step
            y_pos += msd

            # Line misses the clip window entirely.
            if y_pos > clip_ymax:
                return

            # Line starts.
            if y_pos >= clip_ymin:
                rem = temp - msd * delta_x_step

                x_pos = clip_xmin
                error -= rem + delta_y

                if rem > 0:
                    y_pos += 1
                    error += delta_x_step
                set_exit = True

        # Line starts below the clip window.
        if not set_exit and y1 < clip_ymin:
            temp = delta_x_step * (clip_ymin - y1)
            msd = temp // delta_y_step
            x_pos += msd
            rem = temp % delta_y_step

            # Line misses clip window entirely.
            if x_pos > clip_xmax or (x_pos == clip_xmax and rem >= delta_y):
                return

            y_pos = clip_ymin
            error += rem

            if rem >= delta_y:
                x_pos += 1
                error -= delta_y_step

        y_pos_end = y2

        if x2 > clip_xmax:
            temp = delta_y_step * (clip_xmax - x1) + delta_y
            msd = temp // delta_x_step
            y_pos_end = y1 + msd

            if (temp - msd * delta_x_step) == 0:
                y_pos_end -= 1

        y_pos_end = min(y_pos_end, clip_ymax) + 1
        if sign_x == -1:
            x_pos = -x_pos
        if sign_y == -1:
            y_pos = -y_pos
            y_pos_end = -y_pos_end
        delta_y_step -= delta_x_step

        while y_pos != y_pos_end:
            callback(x_pos, y_pos)

            if error >= 0:
                x_pos += sign_x
                error -= delta_y_step
            else:
                error += delta_x_step

            y_pos += sign_y

使用示例:

plot_line_v2v2i(
    # two points
    (10, 2),
    (90, 88),
    # callback
    lambda x, y: print(x, y),
    # xy min
    25, 25,
    # xy max
    75, 75,
)

注意事项:

  • 裁剪最小值/最大值包含在内
    (因此最大值应为image_width - 1image_height - 1
  • 整数除法 // 无处不在。
  • 许多语言(例如 C/C++)在除法上使用四舍五入。
    请参阅 Fast floor of a signed integer division in C / C++ 以避免这些语言的结果略有偏差。

对论文中提供的代码有一些改进:

  • 线条将始终沿定义的方向绘制(从p1p2)。
  • 线条渐变有时会存在细微差别,因此旋转翻转点、计算线条、然后变换回来 - 会产生略微不同的结果。不对称是由于代码交换了 X 轴和 Y 轴以避免代码重复造成的。

有关测试和更多示例用法,请参阅:

【讨论】:

  • 我已将其转换为 C,效果非常好。一个障碍是理解“//”是整数除法而不是注释前缀;)+Kudos ideaman42!
  • @Anonymous yw - 请注意,C 中的 int 除法将根据符号进行不同的舍入。会在回答中注明
  • 哦! Python 把我带到了地板上!谢谢!
  • @Anonymous 可以分享一下 C 代码吗?另外,我感觉很糟糕,在我意识到发生了什么之前,我不小心对这个答案投了反对票。现在,SO不会让我投票赞成:(。
  • 我没有 C 版本,但 rust 版本应该很容易移植到 C。另外,我做了一些小的修改。也许你现在可以改变投票:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-12-15
  • 2017-05-02
  • 1970-01-01
  • 2022-06-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多