图形算法:直线算法

图形算法:直线算法
标签(空格分隔): 算法

版本:3
作者:陈小默
声明:禁止商用,禁止转载
1
2
3
发布于:作业部落、CSDN博客

场景中的直线由其两端点的坐标位置来定义。要在光栅监视器中显示一条线段,图形系统必须先将两端点投影到整数屏幕坐标,并确定离两端点间的直线路径最近的像素位置。接下来才是将颜色填充到相应的像素坐标。1

图形算法直线算法
前言
一算法导论
1直线方程算法
2 DDA算法
3 Bresenham算法
31 斜率大于1
32 斜率大于0小于1
33 斜率大于-1小于0
34 斜率小于-1
二程序演示
前言
文章最后的演示代码使用的是C++语言,函数库使用的是以GLUT为基础的自定义封装库。本章内容将介绍生成直线的直线方程算法、DDA算法以及重要的Bresenham算法。

一、算法导论
以下仅仅展示算法的计算过程,具体实施请参考示例程序部分。

1.1直线方程算法
对于绘制直线来说,使用直线方程无疑是一种最直接的算法。在二维笛卡尔坐标系中,直线方程为: 
y=m∗x+b(1.1)
(1.1)y=m∗x+b

其中m代表直线的斜率,b为直线的截距,对于任意两个端点(x0,y0)(x0,y0)和(x1,y1)(x1,y1): 
m=y1−y0x1−x0(1.2)
(1.2)m=y1−y0x1−x0

b=y0−m∗x0(1.3)
(1.3)b=y0−m∗x0

由于屏幕上的点在其坐标系中以整数表示,当斜率 1>|m|1>|m| 时,我们可以以 xx 轴增量 δxδx 计算相应的y轴增量 δyδy: 
δy=m∗δx(1.4)
(1.4)δy=m∗δx

同样,对于斜率 |m|>1|m|>1 的线段,我们需要通过以 yy 轴增量 δyδy 计算相应的 xx 轴增量 δxδx :
δx=δym(1.5)
(1.5)δx=δym
通过使用直线方程绘制的点,其优点是算法简单且精确,但是其在绘制每一个点的过程中都需要计算一次乘法和加法,显而易见,由于乘法的存在,导致运算时间大幅度增加。接下来介绍的DDA算法将弥补直线方程的乘法缺陷。

1.2 DDA算法
从上可知,在绘制大量点的过程中,我们要尽可能的减少每一个点的计算时间。在计算机中加法运算是最简单的运算之一了。我们可以利用直线的微分特性将每一步的乘法运算替换为加法运算。数字微分分析法(Digital Differential Analyzer,DDA)是一种线段扫描转换算法,基于式 (1.4)(1.4) 或 (1.5)(1.5) 来计算 δxδx 或 δyδy。

对于斜率 |m|≤1|m|≤1 的线段来说,我们仍以单位 xx (δx=1)(δx=1) 间隔(考虑到屏幕设备坐标为连续整数)取样,并逐个计算每一个 yy 值。

yk+1=yk+m(1.6)
(1.6)yk+1=yk+m
于是,我们便将乘法运算合理的转换为了加法运算。但是需要注意的是,在屏幕设备中的坐标均是整数,所以我们在绘制时的y需要取整。 
对于具有大于1的正斜率线段,则需要交换 xx 和 yy 的位置。也就是以单位 yy 间隔 (δy=1)(δy=1)取样,顺序计算每一个 xx 的值

xk+1=xk+1m(1.7)
(1.7)xk+1=xk+1m
此时,每一个计算出的 xx 要沿着 yy 扫描线舍入到最近的像素位置。

该算法只需要计算出一个 stepstep 值( mm 或者 1m1m ),然后就可以沿着路径的方向计算出下一位像素。然而,该算法的缺点显而易见,在数学上,该算法能够保证计算结果准确无误,但是,由于计算机中的数据类型具有精度限制,这将会导致大量数据处理的误差积累。并且,其虽然消除了直线方程中的乘法运算,但是对于浮点数的运算和取整仍然十分耗时。

1.3 Bresenham算法
接下来我们介绍由布莱森汉姆(Bresenham)提出的精确且高效的光栅线生成算法,该算法仅仅使用整数增量计算,除此之外,该算法还能应用于圆或者其他曲线。

我们将分别介绍四种斜率(m>1m>1 、 0<m<10<m<1 、 −1<m<0−1<m<0 和 m<−1m<−1)的计算过程(以下示例均为从左至右画线)。

1.3.1 斜率大于1


首先,在斜率大于1的情况下,沿路径像素以单位 yy 间隔取样。假设线段以(x0,y0)(x0,y0)开始对于其路径上已绘制的(xk,yk)(xk,yk)点我们需要判定下一个点的绘制位置是(xk+1,yk+1)(xk+1,yk+1)还是(xk,yk+1)(xk,yk+1)。

在取样位置 yk+1yk+1 我们使用 dleftdleft 和 drightdright 来标识两个像素位置(xkxk与xk+1xk+1)与数学位置的水平偏移量。根据式 (1.1)(1.1) 可得在像素列 yk+1yk+1 处的 xx坐标计算值为

x=(yk+1−b)m=(yk+1−b)m(1.8)
(1.8)x=(yk+1−b)m=(yk+1−b)m
所以

dleft=x−xk=yk+1−bm−xk(1.9)
(1.9)dleft=x−xk=yk+1−bm−xk

dright=xk+1−x=xk+1−yk+1−bm(1.10)
(1.10)dright=xk+1−x=xk+1−yk+1−bm
为了确定两个像素中哪一个更接近真实路径,需要计算两个像素偏移的差值。

dleft−dright=2yk+1−bm−2xk−1(1.11)
(1.11)dleft−dright=2yk+1−bm−2xk−1
设线段终点位置为(x1,y1)(x1,y1),可得 
m=ΔyΔx=y1−y0x1−x0(1.12)
(1.12)m=ΔyΔx=y1−y0x1−x0
设决策参数 pk=Δy(dleft−dright)pk=Δy(dleft−dright)
pk=2Δxyk−2Δyxk+C(1.13)
(1.13)pk=2Δxyk−2Δyxk+C
其中

C=2Δx(1−b)−Δy(1.14)
(1.14)C=2Δx(1−b)−Δy
在第 k+1k+1 步,决策参数可以由式(1.13)(1.13)计算得出

pk+1=2Δx(yk+1)−2Δyxk+1+C(1.15)
(1.15)pk+1=2Δx(yk+1)−2Δyxk+1+C
将上述等式减去式(1.13)(1.13)可得决策值的增量δpk+1δpk+1
δpk+1=pk+1−pk=2Δx−2Δy(xk+1−xk)(1.16)
(1.16)δpk+1=pk+1−pk=2Δx−2Δy(xk+1−xk)
其中xk+1−xkxk+1−xk的取值可能为0,也可能为1,这由前一个决策值pkpk的正负决定,也就是说如果pk<0pk<0,则下一个绘制的点是(xk,yk+1)(xk,yk+1) 否则绘制(xk+1,yk+1)(xk+1,yk+1)。并且绘制的点的位置又决定了该位置的决策值大小。

目前我们有了决策值之间的增量关系,仅需求出第一个决策值p0p0即可,由式(1.12)(1.12)、式(1.13)(1.13)、式(1.1)(1.1)和式(1.14)(1.14)联立可得

p0=2Δx−Δy(1.17)
(1.17)p0=2Δx−Δy
绘制过程解析:由于Δy>0Δy>0 所以当dleft>drightdleft>dright时(此时pk>0pk>0), 代表当前数学点更接近与xk+1xk+1否则更接近与xkxk。我们仅仅需要求出第一个决策值 p0p0 其后的决策值都是前一个决策值与决策增量δpk+1δpk+1的和。我们可以在沿线路径的每一个 xkxk 处,进行下列检测:

如果 pk<0pk<0 下一个要绘制的点是 (xk,yk+1)(xk,yk+1) ,并且 pk+1=pk+2Δypk+1=pk+2Δy 
否则,下一个要绘制的点是 (xk+1,yk+1)(xk+1,yk+1) ,并且 pk+1=pk+2Δy−2Δxpk+1=pk+2Δy−2Δx
1.3.2 斜率大于0小于1
 
在斜率大于0小于1的情况下,沿路径像素以单位 xx 间隔取样。假设线段以(x0,y0)(x0,y0)开始对于其路径上已绘制的(xk,yk)(xk,yk)点我们需要判定下一个点的绘制位置是(xk+1,yk)(xk+1,yk)还是(xk+1,yk+1)(xk+1,yk+1)。

在取样位置 xk+1xk+1 我们使用 dupperdupper 和 dlowerdlower 来标识两个像素位置(ykyk与yk+1yk+1)与数学位置的水平偏移量。根据式 (1.1)(1.1) 可得在像素列 xk+1xk+1 处的 yy坐标计算值为

y=m(xk+1)+b(1.19)
(1.19)y=m(xk+1)+b
所以

dlower=y−yk=m(xk+1)+b−yk(1.20)
(1.20)dlower=y−yk=m(xk+1)+b−yk

dupper=yk+1−y=yk+1−m(xk+1)−b(1.21)
(1.21)dupper=yk+1−y=yk+1−m(xk+1)−b
为了确定两个像素中哪一个更接近真实路径,需要计算两个像素偏移的差值。

dlower−dupper=2m(xk+1)−2yk+2b−1(1.22)
(1.22)dlower−dupper=2m(xk+1)−2yk+2b−1
设决策参数 pk=Δx(dlower−dupper)pk=Δx(dlower−dupper)
pk=2Δyxk−2Δxyk+C(1.23)
(1.23)pk=2Δyxk−2Δxyk+C
其中

C=2Δy+Δx(2b−1)(1.24)
(1.24)C=2Δy+Δx(2b−1)
在第 k+1k+1 步,决策参数可以由式(1.23)(1.23)计算得出

pk+1=2Δy(xk+1)−2Δxyk+1+C(1.25)
(1.25)pk+1=2Δy(xk+1)−2Δxyk+1+C
将上述等式减去式(1.23)(1.23)可得决策值的增量δpk+1δpk+1
δpk+1=pk+1−pk=2Δy−2Δx(yk+1−yk)(1.26)
(1.26)δpk+1=pk+1−pk=2Δy−2Δx(yk+1−yk)
由式(1.12)(1.12)、式(1.23)(1.23)、式(1.1)(1.1)和式(1.24)(1.24)联立可得第一个决策值p0p0
p0=2Δy−Δx(1.27)
(1.27)p0=2Δy−Δx
1.3.3 斜率大于-1小于0
 
在斜率大于-1小于0的情况下,沿路径像素以单位 xx 间隔取样。假设线段以(x0,y0)(x0,y0)开始对于其路径上已绘制的(xk,yk)(xk,yk)点我们需要判定下一个点的绘制位置是(xk+1,yk)(xk+1,yk)还是(xk+1,yk+1)(xk+1,yk+1)。

在取样位置 xk+1xk+1 我们使用 dupperdupper 和 dlowerdlower 来标识两个像素位置(ykyk与yk+1yk+1)与数学位置的水平偏移量。

dlower=y−yk+1=m(xk+1)+b−(yk−1)(1.28)
(1.28)dlower=y−yk+1=m(xk+1)+b−(yk−1)

dupper=yk−y=yk−m(xk+1)−b(1.29)
(1.29)dupper=yk−y=yk−m(xk+1)−b
为了确定两个像素中哪一个更接近真实路径,需要计算两个像素偏移的差值。

dupper−dlower=2yk−2m(xk+1)−2b−1(1.30)
(1.30)dupper−dlower=2yk−2m(xk+1)−2b−1
设决策参数 pk=Δx(dupper−dlower)pk=Δx(dupper−dlower)
pk=2Δxyk−2Δyxk+C(1.31)
(1.31)pk=2Δxyk−2Δyxk+C
其中

C=−2Δy−Δx(2b−1)(1.32)
(1.32)C=−2Δy−Δx(2b−1)
在第 k+1k+1 步,决策参数可以由式(1.31)(1.31)计算得出

pk+1=2Δxyk+1−2Δy(xk+1)+C(1.33)
(1.33)pk+1=2Δxyk+1−2Δy(xk+1)+C
将上述等式减去式(1.32)(1.32)可得决策值的增量δpk+1δpk+1
δpk+1=pk+1−pk=2Δx(yk+1−yk)−2Δy(1.34)
(1.34)δpk+1=pk+1−pk=2Δx(yk+1−yk)−2Δy
由式(1.12)(1.12)、式(1.30)(1.30)、式(1.1)(1.1)和式(1.31)(1.31)联立可得第一个决策值p0p0
p0=Δx−2Δy(1.35)
(1.35)p0=Δx−2Δy
1.3.4 斜率小于-1
 
在斜率小于-1的情况下,沿路径像素以单位 yy 间隔取样。假设线段以(x0,y0)(x0,y0)开始对于其路径上已绘制的(xk,yk)(xk,yk)点我们需要判定下一个点的绘制位置是(xk+1,yk+1)(xk+1,yk+1)还是(xk,yk+1)(xk,yk+1)。

在取样位置 yk+1yk+1 我们使用 dleftdleft 和 drightdright 来标识两个像素位置(xkxk与xk+1xk+1)与数学位置的水平偏移量。根据式 (1.1)(1.1) 可得在像素列 yk+1yk+1 处的 xx坐标计算值为

x=(yk+1−b)m=(yk−1−b)m(1.36)
(1.36)x=(yk+1−b)m=(yk−1−b)m
所以

dleft=x−xk=yk−1−bm−xk(1.37)
(1.37)dleft=x−xk=yk−1−bm−xk

dright=xk+1−x=xk+1−yk−1−bm(1.38)
(1.38)dright=xk+1−x=xk+1−yk−1−bm
为了确定两个像素中哪一个更接近真实路径,需要计算两个像素偏移的差值。

dleft−dright=2yk−1−bm−2xk−1(1.39)
(1.39)dleft−dright=2yk−1−bm−2xk−1
设线段终点位置为(x1,y1)(x1,y1),可得 
m=ΔyΔx=y1−y0x1−x0(1.40)
(1.40)m=ΔyΔx=y1−y0x1−x0
设决策参数 pk=Δy(dright−dleft)pk=Δy(dright−dleft)(在从左到右的绘制过程中Δy<0Δy<0 为了保持符号统一,所以交换dleftdleft和drightdright位置)

pk=−2Δxyk+2Δyxk+C(1.41)
(1.41)pk=−2Δxyk+2Δyxk+C
其中

C=2Δx(1+b)+Δy(1.42)
(1.42)C=2Δx(1+b)+Δy
在第 k+1k+1 步

pk+1=−2Δx(yk−1)+2Δyxk+1+C(1.43)
(1.43)pk+1=−2Δx(yk−1)+2Δyxk+1+C
将上述等式减去式(1.41)(1.41)可得决策值的增量δpk+1δpk+1
δpk+1=pk+1−pk=2Δx+2Δy(xk+1−xk)(1.44)
(1.44)δpk+1=pk+1−pk=2Δx+2Δy(xk+1−xk)
目前我们有了决策值之间的增量关系,仅第一个决策值p0p0
p0=2Δx+Δy(1.45)
(1.45)p0=2Δx+Δy
二、程序演示
在上面得的算法分析中,我们已经了解了三种算法的基本思想与设计思路。这里通过C++程序演示其过程(也可以通过其他图形软件包比如Java的swing或者Android的Canvas实现),实现方式各异,不必拘泥于细节。

首先定义一个显示用来显示图形的View

#ifndef lineview_h
#define lienview_h
#include"cxm.h"

class LineView:public View{
private:
    _Paint _paint;//画笔指针
    _StringArray _names;//字符串数组指针
    List<IntArray> *_arrays;//存放了整型数组的链表对象,数组中为计算出的点的坐标位置
public:
    int bottom,top;
    LineView();
    ~LineView();
    void LineEquation(double x1,double y1,double x2,double y2);//使用直线方程画线
    void DDA(double x1,double y1,double x2,double y2);//使用DDA算法画线
    void Bresenham(double x1,double y1,double x2,double y2);//布莱森汉姆算法
    virtual void onDraw(Canvas &canvas);//图案绘制方法
};
typedef LineView* _LineView;
typedef LineView& LineView_;
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
接下来实现其中的构造与析构函数

LineView::LineView(){
    _paint = new Paint;
    _names = new StringArray(4);
    _arrays = new LinkedList<IntArray>;

    StringArray_ names_=*_names;
    names_[0]="OpenGL";
    names_[1]="Equation";
    names_[2]="DDA";
    names_[3]="Bresenhan";
}
LineView::~LineView(){
    delete _paint;
    delete _names;
    delete _arrays;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
接下来我们实现使用直线方程的方法,该方法将计算出的点保存在了整型数组中,并将数组存储到链表

void LineView::LineEquation(double x1,double y1,double x2,double y2){//y=mx+b
    _IntArray  _arr;
    double dy = y2-y1;
    double dx = x2-x1;
    double m = dy/dx;
    double b = y1 - m*x1;
    if(abs(m)>1){//以y轴像素为单位 x=(y-b)/m
        int size = abs(2*int(dy));//存放坐标的数组长度
        _arr = new IntArray(size);
        IntArray_ arr=*_arr;
        int start = int(y1<y2?y1:y2);//取最小起始位置
        int end = start+abs(int(dy));
        for(int i=start,j=0;i<end;i++){
            arr[j++]=int((i-b)/m);//x坐标
            arr[j++]=i;//y坐标
        }
    }else{//以x轴像素为单位 y=mx+b
        int size = abs(2*int(dx));//存放坐标的数组长度
        _arr = new IntArray(size);
        IntArray_ arr = *_arr;
        int start = int(x1<x2?x1:x2);//取最小起始位置
        int end = start+abs(int(dx));
        for(int i=start,j=0;i<end;i++){
            arr[j++]=i;//x坐标
            arr[j++]=int(b+m*i);//y坐标
        }
    }
    _arrays->add(_arr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
然后实现使用DDA画线的算法计算出各点

void LineView::DDA(double x1,double y1,double x2,double y2){
    _IntArray _arr;
    double dy = y2-y1;
    double dx = x2-x1;
    double m = dy/dx;
    if(abs(m)>1){//以单位y取样
        double rate = 1/m;
        int size = abs(2*int(dy));//存放坐标的数组长度
        _arr = new IntArray(size);
        IntArray_ arr = *_arr;
        int start = int(y1<y2?y1:y2);//取最小起始位置
        int end = start+abs(int(dy));
        double rx = y1<y2?x1:x2;
        for(int i=start,j=0;i<end;i++){
            rx+=rate;
            arr[j++]=int(rx);
            arr[j++]=i;
        }
    }else{//以单位x取样
        double rate = m;
        int size = abs(2*int(dx));//存放坐标的数组长度
        _arr = new IntArray(size);
        IntArray_ arr = *_arr;
        int start = int(x1<x2?x1:x2);//取最小起始位置
        int end = start+abs(int(dx));
        double ry = x1<x2?y1:y2;
        for(int i=start,j=0;i<end;i++){
            ry+=rate;
            arr[j++]=i;
            arr[j++]=int(ry);
        }
    }
    _arrays->add(_arr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
最后是Bresenham算法的简单实现

void LineView::Bresenham(double x1,double y1,double x2,double y2){
    _IntArray _arr;
    double dy=y2-y1;
    double dx=x2-x1;
    double m = dy/dx;
    if(m>1){//以单位y取样
        int size = abs(2*int(dy));
        _arr = new IntArray(size);
        IntArray_ arr = *_arr;
        double p = 2*dx-dy;//po
        double left = 2*dx;
        double right = 2*dx-2*dy;
        int start = int(y1);
        int end = start+abs(int(dy));
        int x = int(x1);
        for(int y=start,i=0;y<=end;y++){
            if(p<0){
                arr[i++]=x;
                p+=left;
            }else{
                arr[i++]=++x;
                p+=right;
            }
            arr[i++]=y;
        }
    }else if(m>0&&m<1){//以单位x取样
        int size = abs(2*int(dx));
        _arr = new IntArray(size);
        IntArray_ arr = *_arr;
        double p = 2*dy-dx;
        double upper = 2*dy;
        double lower = 2*dy-2*dx;
        int start = int(x1);
        int end = start + abs(int(dx));
        int y = int(y1);
        for(int x=start,i=0;x<=end;x++){
            arr[i++]=x;
            if(p<0){
                arr[i++]=y;
                p+=upper;
            }else{
                arr[i++]=++y;
                p+=lower;
            }
        }
    }else if(m<0&&m>-1){//以单位x取样
        int size = abs(2*int(dx));
        _arr = new IntArray(size);
        IntArray_ arr = *_arr;
        double p = dx-2*dy;
        double upper = -2*dy;
        double lower = -2*dx-2*dy;
        int start = int(x1);
        int end = start + abs(int(dx));
        int y = int(y1);
        for(int x=start,i=0;x<=end;x++){
            arr[i++]=x;
            if(p<0){
                arr[i++]=y;
                p+=upper;
            }else{
                arr[i++]=--y;
                p+=lower;
            }
        }
    }else{//以单位y取样
        int size = abs(2*int(dy));
        _arr = new IntArray(size);
        IntArray_ arr = *_arr;
        double p = 2*dx+dy;
        double left = 2*dx;
        double right = 2*dx+2*dy;
        int start = int(y1);
        int end = start-abs(int(dy));
        int x = int(x1);
        for(int y=start,i=0;y>=end;y--){
            if(p<0){
                arr[i++]=x;
                p+=left;
            }else{
                arr[i++]=++x;
                p+=right;
            }
            arr[i++]=y;
        }
    }
    _arrays->add(_arr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
然后在onDraw方法中将所有的点绘制出来

void LineView::onDraw(Canvas &canvas){
    canvas.drawLine(_paint,50,bottom,150,top);
    for(int i=0;i<_arrays->size();i++){
        canvas.drawPoints(*_paint,*(*_arrays)[i]);
    }
    for(int i=0;i<_names->size();i++){
        canvas.drawString(*_paint,i*200+40,25,(*_names)[i]);
    }
}
1
2
3
4
5
6
7
8
9
最后就是在main方法中创建视图窗口,并将刚刚创建的View添加进去

int _tmain(int argc, char* argv[]){
    GlutWindow window(100,300,800,300);
    String title = "直线算法";
    window.setTitle(title);
    window.setBackgroundColor(BLUE_SKY);
    _LineView _view = new LineView();
    LineView_ view=*_view;

    view.bottom=50;
    view.top=290;

    view.LineEquation(250,view.bottom,350,view.top);
    view.DDA(450,view.bottom,550,view.top);
    view.Bresenham(650,view.bottom,750,view.top);

    window.addView(view);
    window.show(&argc,argv);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
结果图展示 


Donald Hearn.计算机图形学 第四版.电子工业出版社.2016-2.2th.101~107 ↩
--------------------- 
作者:陈小默cxm 
来源:CSDN 
原文:https://blog.csdn.net/qq_32583189/article/details/52817357 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/qingzhuyuxian/article/details/83444600