[计算机图形学]任意斜率的中点画线法

本文围绕计算机图形学中的中点画线法展开。中点画线法是经典画线算法,能避免浮点数运算。文中先推导了斜率为0<k<=1时的中点画线法,通过构造判别式及增量法提高计算效率,还给出完整步骤。最后介绍了任意斜率的中点画线法,给出初始判别式和迭代情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

[计算机图形学]任意斜率的中点画线法

中点画线法简介

中点画线法是经典的画线算法,相较于DDA(数值微分法),中点画线法避免了使用浮点数进行运算,可以仅使用整数运算来获得像素坐标点。

但是目前大部分的教科书中仅介绍了斜率为 0 < = k < = 1 0<=k<=1 0<=k<=1的情况,对其他情况并没有说明。因此,下面我会对任意斜率的中点画线法进行延申。

中点画线法在斜率为 0 < k < = 1 0<k<=1 0<k<=1下的推导

首先介绍经典的斜率为 0 < k < = 1 0<k<=1 0<k<=1的情况:

在这里插入图片描述

如图,假设在当前迭代中离直线最近的像素已确定为 P = ( x p , y p ) P=(x_p,y_p) P=(xp,yp),由于 x x x为最大位移方向,因此接下来的每一次迭代, x x x方向上都将增加一个像素单位,而 y y y方向上将不增加像素单位或增加一个像素单位。

那么问题就是究竟选择像素 P 1 = ( x p + 1 , y p ) P1=(x_p + 1,y_p) P1=(xp+1,yp)还是像素 P 2 = ( x p + 1 , y p + 1 ) P2=(x_p+1,y_p+1) P2=(xp+1,yp+1)?这取决于哪一个像素会离直线会更接近。中点画线法给出了一个判断哪个像素更接近的规则:

考虑介于线段 P 1 P 2 P1P2 P1P2的中点 M = ( x p + 1 , y p + 0.5 ) M=(x_p+1,y_p+0.5) M=(xp+1,yp+0.5)和线段 P 1 P 2 P1P2 P1P2与直线的交点 Q Q Q

  • 如果点 M M M在点 Q Q Q的下方,说明点 P 2 P2 P2 Q Q Q更近,也就是 P 2 P2 P2离直线更近,因此选择 P 2 P2 P2像素
  • 如果点 M M M在点 Q Q Q的上方,说明点 P 1 P1 P1 Q Q Q更近,也就是 P 1 P1 P1离直线更近,因此选择 P 1 P1 P1像素
  • 如果点 M M M与点 Q Q Q重合,说明点 P 1 P1 P1和点 P 2 P2 P2 Q Q Q一样近,也就是点 P 1 P1 P1和点 P 2 P2 P2离直线一样近,可取 P 1 P1 P1 P 2 P2 P2中的任意一个,这里规定取 P 1 P1 P1像素

下面,我们用数学表达式来重新表达上述规则:

假设上图中的直线为 F ( x , y ) = a x + b y + c = 0 F(x,y)=ax+by+c=0 F(x,y)=ax+by+c=0

  • M M M在点 Q Q Q的下方,等价于点 M M M在直线下方,也就是 F ( x p + 1 , y p + 0.5 ) < 0 F(x_p+1,y_p+0.5)<0 F(xp+1,yp+0.5)<0,此时选择 P 2 P2 P2像素
  • M M M在点 Q Q Q的上方,等价于点 M M M在直线上方,也就是 F ( x p + 1 , y p + 0.5 ) > 0 F(x_p+1,y_p+0.5)>0 F(xp+1,yp+0.5)>0,此时选择 P 1 P1 P1像素
  • M M M与点 Q Q Q的重合,等价于点 M M M刚好在直线上,也就是 F ( x p + 1 , y p + 0.5 ) = 0 F(x_p+1,y_p+0.5)=0 F(xp+1,yp+0.5)=0,此时规定选择 P 1 P1 P1像素

拓展延伸:

这里要求直线函数 F ( x , y ) F(x,y) F(x,y)中的 b ≥ 0 b\geq0 b0,因为这样才能确定使得 F ( x , y ) < 0 F(x,y) <0 F(x,y)<0的点对应在直线下方,使得 F ( x , y ) > 0 F(x,y)>0 F(x,y)>0的点对应在直线上方。也就是说, b b b的正负决定了点与直线的对应关系。类似的,其实 a a a的正负也决定着上述的对应关系,只不过 b b b决定了上下关系, a a a决定了左右关系。

进一步构造判别式: d = F ( x p + 1 , y p + 0.5 ) d=F(x_p+1,y_p+0.5) d=F(xp+1,yp+0.5),用判别式重述上述规则:

  • d < 0 d<0 d<0时,选择 P 2 P2 P2像素
  • d > = 0 d>=0 d>=0时,选择 P 1 P1 P1像素

有了这个方法后,我们就能由当前的像素点 P = ( x p , y p ) P=(x_p,y_p) P=(xp,yp)推断下一个像素点了。

后续的迭代过程中,如果每一次迭代都类似于上述过程,将中点带入直线 F ( x , y ) F(x,y) F(x,y)计算一下,这会牺牲很多计算资源。为了提高计算效率,节省计算资源,我们得想出一个办法解决来带入计算的问题。因为函数 F ( x , y ) F(x,y) F(x,y)表示的是一条直线,具有线性性质,因此我们可以使用加法(也就是所谓的增量法)来代替带入计算

  • 假设像素点 P = ( x p , y p ) P=(x_p,y_p) P=(xp,yp)的下一个像素点为 P 1 = ( x p + 1 , y p ) P1=(x_p + 1,y_p) P1=(xp+1,yp),那么推断下一个像素点的判别式就可以写成
    d 1 = F ( x p + 2 , y p + 0.5 ) = a ( x p + 2 ) + b ( y p + 0.5 ) + c = a ( x p + 1 ) + b ( y p + 0.5 ) + c + a = F ( x p + 1 , y p + 0.5 ) + a = d + a \begin{aligned} d_1 &=F(x_p+2,y_p+0.5) \\ &=a(x_p+2)+b(y_p+0.5)+c\\ &=a(x_p+1)+b(y_p+0.5)+c+a\\ &=F(x_p+1,y_p+0.5)+a\\ &=d+a \end{aligned} d1=F(xp+2,yp+0.5)=a(xp+2)+b(yp+0.5)+c=a(xp+1)+b(yp+0.5)+c+a=F(xp+1,yp+0.5)+a=d+a

  • 假设像素点 P = ( x p , y p ) P=(x_p,y_p) P=(xp,yp)的下一个像素点为 P 2 = ( x p + 1 , y p + 1 ) P2=(x_p+1,y_p+1) P2=(xp+1,yp+1),那么推断下一个像素点的判别式就可以写成
    d 1 = F ( x p + 2 , y p + 1 + 0.5 ) = a ( x p + 2 ) + b ( y p + 1 + 0.5 ) + c = a ( x p + 1 ) + b ( y p + 0.5 ) + c + a + b = F ( x p + 1 , y p + 0.5 ) + a + b = d + a + b \begin{aligned} d_1 &=F(x_p+2,y_p+1+0.5) \\ &=a(x_p+2)+b(y_p+1+0.5)+c\\ &=a(x_p+1)+b(y_p+0.5)+c+a+b\\ &=F(x_p+1,y_p+0.5)+a+b\\ &=d+a+b \end{aligned} d1=F(xp+2,yp+1+0.5)=a(xp+2)+b(yp+1+0.5)+c=a(xp+1)+b(yp+0.5)+c+a+b=F(xp+1,yp+0.5)+a+b=d+a+b

也就是说,推断下一个像素点的判别式可以直接由上一个判别式加一个数(这个数被称为增量)就能得到,这样子计算效率就大大提高了。

特别的,假设像素点 P = ( x p , y p ) P=(x_p,y_p) P=(xp,yp)就是直线的起点(也就是 F ( x p , y p ) = 0 F(x_p,y_p)=0 F(xp,yp)=0),那么有:
d = F ( x p + 1 , y p + 0.5 ) = a ( x p + 1 ) + b ( y p + 0.5 ) + c = a x p + b y p + c + a + 0.5 b = F ( x p , y p ) + a + 0.5 b = a + 0.5 b \begin{aligned} d &=F(x_p+1,y_p+0.5)\\ &=a(x_p+1)+b(y_p+0.5)+c \\ &=ax_p+by_p+c+a+0.5b\\ &=F(x_p,y_p)+a+0.5b\\ &=a+0.5b \end{aligned} d=F(xp+1,yp+0.5)=a(xp+1)+b(yp+0.5)+c=axp+byp+c+a+0.5b=F(xp,yp)+a+0.5b=a+0.5b
为了避免 0.5 0.5 0.5这个浮点数运算,所以我们用 2 d = 2 a + b 2d=2a+b 2d=2a+b替换判别式,反正也只是和 0 0 0进行比对,乘多少都无所谓。
同样的,后续的判别式也变成了 2 d 1 = 2 d + 2 a 2d_1=2d+2a 2d1=2d+2a 2 d 1 = 2 d + 2 ( a + b ) 2d_1=2d+2(a+b) 2d1=2d+2(a+b)

最后,我们给出中点画线法在斜率为 0 < k < = 1 0<k<=1 0<k<=1下的完整步骤:

  1. 给起点 ( x 1 , y 1 ) (x1,y1) (x1,y1)和终点 ( x 2 , y 2 ) (x_2,y_2) (x2,y2),其中 x 1 < x 2 x_1<x_2 x1<x2且直线斜率为 0 < k < = 1 0<k<=1 0<k<=1
  2. 初始化。令 a = y 1 − y 2 , b = x 2 − x 1 , d = 2 a + b , d e t a 1 = 2 a , d e t a 2 = 2 ( a + b ) , x = x 1 , y = y 1 a=y_1-y_2,b=x_2-x_1,d=2a+b,deta_1=2a,deta_2=2(a+b),x=x1,y=y1 a=y1y2,b=x2x1,d=2a+b,deta1=2a,deta2=2(a+b),x=x1,y=y1
  3. 画像素点 ( x , y ) (x,y) (x,y)
  4. 判断 x x x是否小于 x 2 x_2 x2。如果 x < x 2 x<x_2 x<x2,则执行步骤5,否则流程结束。
  5. 如果 d < 0 d<0 d<0,则 x + + ; y + + ; d + = d e t a 2 x++;y++;d+=deta_2 x++;y++;d+=deta2;如果 d > = 0 d>=0 d>=0,则 x + + ; d + = d e t a 1 x++;d+=deta_1 x++;d+=deta1;画像素点 ( x , y ) (x,y) (x,y),转到步骤4。

任意斜率的中点画线法

任意其他斜率的情况也可以如同上述进行分析,但注意在斜率 ∣ k ∣ > 1 |k|>1 k>1时,每一步的最大位移方向是 y y y方向,即每次迭代是在 y y y方向上前进一步,在 x x x方向上判断中点和直线关系。

具体推导过程不再说明,以下图为标准,假设起点为 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),终点在圆上时的初始判别式 d d d和判别式 d > 0 d>0 d>0 d < 0 d<0 d<0时迭代情况。

特别的,当d = 0时,应归属于只有最大位移方向移动的情况。

请添加图片描述

终点所在角度dd>0d<0 d = 0 d=0 d=0
( 0 ° , 45 ° ] (0°,45°] (,45°]2a+bx++;d+=2ax++;y++;d+=2(a+b)归于 d > 0 d>0 d>0
( 45 ° , 90 ° ) (45°,90°) (45°,90°)a+2by++;x++;d+=2(a+b)y++;d+=2b归于 d < 0 d<0 d<0
( 90 ° , 135 ° ] (90°,135°] (90°,135°]-a+2by++;x--;d+=2(b-a)y++;d+=2b归于 d < 0 d<0 d<0
( 135 ° , 180 ° ) (135°,180°) (135°,180°)-2a+bx--;d-=2ax--;y++;d+=2(b-a)归于 d > 0 d>0 d>0
( 180 ° , 225 ° ] (180°,225°] (180°,225°]-2a-bx--;y--;d-=2(a+b)x--;d-=2a归于 d < 0 d<0 d<0
( 225 ° , 270 ° ) (225°,270°) (225°,270°)-a-2by--;d-=2by--;x--;d-=2(a+b)归于 d > 0 d>0 d>0
( 270 ° , 315 ° ] (270°,315°] (270°,315°]a-2by--;d-=2by--;x==;d+=2(a-b)归于 d > 0 d>0 d>0
( 315 ° , 360 ° ) (315°,360°) (315°,360°)2a-bx++;y--;d+=2(a-b)x++;d+=2a归于 d < 0 d < 0 d<0

备注:

  • 水平直线和竖直直线易得,这里不再说明。

  • 注意初始化时要令 b ≥ 0 b \geq 0 b0

    int a = y1-y2; 
    int b = x2-x1;
    if (b < 0){ 
        a = -a;
        b = -b
    }
    
  • 注意分清是-=还是+=

  • 八个部分可以通过横、纵行进方向的正负号串起来,具体不再赘述,希望读者自己思考(嘻嘻🤭)

中点画线法代码(C语言)

#include <stdio.h>
#include <stdlib.h>

void vline(int x, int y1, int y2, FILE* f){
    /*竖直的线*/
    int s = y1 > y2 ? -1 : 1; // 迭代方向
    for (int i = y1; i != y2; i += s) {
        fprintf(f, "(%d,%d)\n", x, i);
    }
    fprintf(f, "(%d,%d)\n", x, y2);
}

void hline(int x1, int x2, int y, FILE* f){
    /*水平的线*/
    int s = x1 > x2 ? -1 : 1; // 迭代方向
    for (int i = x1; i != x2; i += s){
        fprintf(f, "(%d,%d)\n", i, y);
    }
    fprintf(f, "(%d,%d)\n", x2, y);
}

void midPointLine (int x1, int y1, int x2, int y2, FILE* f) {
    /* 使用中点画线算法绘制起点为(x1,y1),终点为(x2,y2)的直线 */

    if (x1 == x2) { // 直线为竖直线
        vline(x1, y1, y2, f);
        return;
    }

    if (y1 == y2) { // 直线为水平线
        hline(x1, x2, y1, f);
        return;
    }

    int a, b; // 所画直线的参数
    a = y1 - y2;
    b = x2 - x1;
    if (b < 0){
        a = -a;
        b = -b;
    }

    int dx, dy; // 起终点之间的横、纵距离
    dx = (b > 0) ? b : -b;
    dy = (a > 0) ? a : -a;

    int sx, sy; // 起点到终点的迭代方向
    sx = (x1 > x2) ? -1 : 1;
    sy = (y1 > y2) ? -1 : 1;

    int x, y, d; // 迭代参数
    x = x1;
    y = y1;
    d = (dx >= dy) ? 2 * sx * a + sy * b: sx * a + 2 * sy * b;

    fprintf(f, "(%d,%d)\n", x1, y1);
    // 中点画线法
    if (dx >= dy){ // 直线斜率 0 < |k| <= 1 的情况
        if (sy > 0){ // 终点位于起点的上面
            while(x != x2){
                if (d >= 0){
                    x += sx; d += 2 * sx * a;
                    fprintf(f, "(%d,%d)\n", x, y);
                } else {
                    x += sx; y += sy; d += 2 * (sx * a + sy * b);
                    fprintf(f, "(%d,%d)\n", x, y);
                }
            } return;
        } else { // 终点位于起点的下面
            while(x != x2){
                if (d > 0){
                    x += sx; y += sy; d += 2 * (sx * a + sy * b);
                    fprintf(f, "(%d,%d)\n", x, y);
                } else {
                    x += sx; d += 2 * sx * a;
                    fprintf(f, "(%d,%d)\n", x, y);
                }
            } return;
        }
    } else { // 直线斜率 1 < |k| 的情况
        if (sy > 0){ // 终点位于起点的上面
            while(y != y2){
                if (d > 0){
                    y += sy; x += sx; d += 2 * (sx * a + sy * b);
                    fprintf(f, "(%d,%d)\n", x, y);
                } else {
                    y += sy; d += 2 * sy * b;
                    fprintf(f, "(%d,%d)\n", x, y);
                }
            } return;
        } else { // 终点位于起点的下面
            while(y != y2){
                if (d >= 0){
                    y += sy; d += 2 * sy * b;
                    fprintf(f, "(%d,%d)\n", x, y);
                } else {
                    y += sy; x += sx; d += 2 * (sx * a + sy * b);
                    fprintf(f, "(%d,%d)\n", x, y);
                }
            } return;
        }
    }

}

int main()
{
    int x1,y1,x2,y2;
    printf("请输入点(x1 y1): ");
    scanf("%d %d", &x1, &y1);
    printf("请输入点(x2 y2): ");
    scanf("%d %d", &x2, &y2);

    FILE* f;
    f = fopen("midPointLineOutput.txt","w");
    if (f == NULL) {
        printf("无法建立输出文件,程序错误。");
        return 0;
    }

    midPointLine(x1, y1, x2, y2, f);
    fclose(f);

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值