目录
目的
本实验基于STM32F103RC+TFTLCD屏,旨在于在LCD屏上,给定起始、终止坐标,便可以显示一条直线,然而众所周知,对于屏幕而言,我们只能操作各个像素点,因此,选择出最接近标准直线的像素点就成了本次实验的主要目的,最终得到的直线大概应该长这样
看起来很不规则,但是这里只是从(0,0)到(6,-10)的直线,因此放大来看很不规则,但是在显示屏上,肉眼很难区分出来
几何原理
像素点的确定
首先,由于屏幕由像素点组成,每个像素点又有自己的位置信息,因此我们不妨将屏幕显示近似为一个坐标系,并在坐标系中作出一条标准直线,起点为原点(如果以后起点改变,将所有坐标基于起点平移即可),找出最靠近直线的所有整数坐标(像素点),如原理图1所示
对于一条以原点为起点,终点为(5,2)作出的直线,斜率 k=0.4,我们描出所有最靠近直线的整数坐标,如原理图1中圈出部分,即有效点。
同时,我们标出纵轴上半个单位(0.5)的点,如图中绿点所示。不难发现,以横轴为参考轴,直线始终位于绿点和有效点之间。从数学的角度,0—5号点满足:
(1)直线的横坐标按单位递增,纵坐标则按斜率递增
(2)如果直线纵坐标低于绿点,则纵坐标相较上一个点不变,作为有效点(如图中第1、3号点);如果直线纵坐标高于绿点,则纵坐标相较上一个点加1,且下一次的绿点也上升一个单位
(3)遍历横坐标,就可以获得所有点坐标
原理看似很简单,我们将原理图1中的所有情况,按步骤列出:(row / col分别对应有效点的横 / 纵坐标)
步骤 | 判断 | 数据处理 |
第一步 | k <0.5 | row++ |
第二步 | k*2 >0.5 | row++ col++ |
第三步 | k*3-1<0.5 | row++ |
第四步 | k*4-1>0.5 | row++ col++ |
第五步 | k*5-2>0.5 | row++ |
。。。 | k*t - i<0.5 | row++ |
。。。 | k*t - i>0.5 | row++ col++ |
据此,我们可以写出一个仅针对0~45°直线的程序,如下
//画粗线
//(x1,y1),(x2,y2):线条的起始坐标
//size:线条的粗细程度
//color:线条的颜色
void LCD_Draw_BLine(u16 x1,u16 y1,u16 x2,u16 y2,u8 size,u16 color)
{
u16 t;
u16 row=x1;
u16 col=y1;
u16 delta_x=x2-x1;
u16 delta_y=y2-y1;
u16 deviate;
deviate=-delta_x;
for(t=0;t<=delta_x;t++)
{
LCD_Draw_Fill_Circle(x1,y1,size,color); //描点函数(row,col)
deviate+=delta_y*2;
if(deviate>0)
{
deviate-=delta_x*2;
col++;
}
row++;
}
}
不同直线的规律
要从上表中提取出程序逻辑,想必已经很简单了,但是我们先不着急写,因为我们这里讨论的直线仅在0~45°之间。真正的情况可以分为斜线、水平线、竖直线共12种,如左图所示。接下来我们讨论与上述0~45°线完全对立的 -135°~-90°的情况,对比寻找规律
如右图所示,经过对比,要想取到所有有效点而且不会遗漏,显而易见有以下几个特点:
- 需要以变化量更大的轴作为参考轴,遍历参考轴得到所有对应的坐标
- 参考轴按单位1变化,非参考轴按变化率 k变化
- 若将变化量 △x 和 △y中较大者定义为 xymax,较小者定义为 xymin,则变化率 k=xymin/xymax
- 根据直线的指向,若终点横坐标为正,则横坐标以递增形式变化;若为负,则横坐标以递减形式变化;若为0,则横坐标不变化。y坐标同理
- 非参考轴坐标与中点进行比较,小于则保持不变,大于则非参考轴坐标变化,且中点坐标也变化
综合考虑以上特点,我们可以得出结论:
- 比较x和y的变化量大小,并计算k
- 判断直线的指向,用方向变量inc作为递增量,根据不同的情况,inc=1或-1或0
- 用变量row、col分别存储有效点的横、纵坐标
- 变化率公式 k=xymin/xymax 和遍历判断公式 if (k*t - i >0.5) 联立可以化简遍历判断公式为 if (2*xymin*t - 2*xymax*i >xymax) 其中,每次循环后 t 都要递增,当条件满足时 i 加1。
- 每次循环遍历中,参考轴坐标均发生变化,非参考轴坐标视判断公式而定。如果判断成立,则坐标发生变化,且下一次判断公式变化
综上所述,我们可以写一个简单的代码来表达它的逻辑
以变化量更大的作为参考轴,另一个则为非参考轴
根据直线的指向确定两个坐标轴的递增值inc=1或-1或0
遍历整条直线
{
描点(row,col);
err+=xymin*2;
if(err>xymax)
{
非参考轴坐标+对应的inc;
err-=xymax*2;
}
参考轴坐标+对应的inc;
}
经过上述逻辑处理,就可以一个点一个点地描绘出整条直线了
逻辑化简
上述逻辑看似很短,实际需要讨论很多种情况,如果分类不当会导致代码繁琐,化简方法有两个
方法1、按xymax讨论
对于遍历循环体内的逻辑,根据参考轴的不同(即△x和△y谁更大),我们可以将各种情况列写在如下表格中:
原代码逻辑 | 情况分类 | △x>△y xymax=△x | △x<△y xymax=△y | |
描点(row,col); err+=2*xymin; if(err>xymax) 非参考轴坐标+=对应的inc err-=2*xymax 参考轴坐标+=对应的inc | errx |
errx+=2*△x; if(errx>xymax) errx-=2*xymax
| 描点(row,col); errx+=2*△x; if(errx>xymax) row+=incx; errx-=2*xymax col+=incy; | |
erry | 描点(row,col); erry+=2*△y; if(erry>xymax) col+=incy; erry-=2*xymax row+=incx; |
erry+=2*△y; if(erry>xymax) erry-=2*xymax
|
表格中加粗部分代表实际会出现的情况,未加粗的两个部分是根据其左/右侧情况进行格式仿写的(相同颜色部分格式仿写),实际不参与原本的逻辑。但是我们可以观察△x>△y的条件下,格式仿写代码中的判断始终成立,因此在其条件下增加的代码必定执行,观察△x<△y同理。于是我们可以将两种情况合并为如下代码(仅循环体内部分)
描点(row,col);
errx+=2*△x;
erry+=2*△y;
if(errx>xymax)
row+=incx;
errx-=2*xymax;
if(erry>xymax)
col+=incy;
erry-=2*xymax;
方法2、按判断条件讨论
对于遍历循环体内的逻辑,根据是否满足判断条件(即err>xymax?),我们可以将各种情况列写在如下表格中:
原代码逻辑 | 情况分类 | err>xymax(符合条件) | err<=xymax(不符条件) | |
描点(row,col); err+=2*xymin; if(err>xymax) 非参考轴坐标+=对应的inc err-=2*xymax 参考轴坐标+=对应的inc | △x>△y xymax=△x | 描点(row,col) err+=2*xymin err-=2*xymax row+=incx col +=incy | 描点(row,col) err+=2*xymin ------ row+=incx ------ | |
△x<△y xymax=△y | 描点(row,col) err+=2*xymin err-=2*xymax row+=incx col +=incy |
描点(row,col) err+=2*xymin ------ ------ col +=incy
|
不难看出,无论△x和△y谁大,只要符合判断条件,执行代码相同,都执行err-=2*xymax;而且,当且仅当 条件不成立且△x更大时,col不变化;同理,当且仅当 条件不成立且△y更大时,row不变化。
我们不妨将共同的代码提出至判断外统一执行,可以简化为如下
情况分类 | err>xymax(符合条件) | err<=xymax(不符条件) | 都需要执行的代码 |
△x>△y xymax=△x |
err-=2*xymax
| col -=incy | 描点(row,col) err+=2*xymin row+=incx col +=incy |
△x<△y xymax=△y |
row-=incx
|
这样,逻辑就更加清晰明了了,且情况的讨论也更加简单,每种情况下的代码也只有一行,循环体内的代码简化如下
描点(row,col)
err+=2*xymin;
row+=incx;
col+=incy;
if(err>xymax)err-=2*xymax;
else
{
if(xymax==delta_x)col-=incy;
else row-=incx;
}
程序源码
方法一:
//画粗线方法1
//(x1,y1),(x2,y2):线条的起始坐标
//size:线条的粗细程度
//color:线条的颜色
void LCD_Draw_BLine(u16 x1,u16 y1,u16 x2,u16 y2,u8 size,u16 color)
{
u16 t;
int row=x1;
int col=y1;
int err=0,xymax,xymin,delta_x,delta_y; //定义偏量、xy中较大/较小值、x/y变化量
int incx,incy; //x/y变化方向
if(x2>x1){delta_x=x2-x1;incx=1;} //分别获取两个方向的位移(正值)和方向变量
else if(x2==x1){delta_x=0;incx=0;}
else {delta_x=x1-x2;incx=-1;}
if(y2>=y1){delta_y=y2-y1;incy=1;}
else if(y2==y1){delta_y=0;incy=0;}
else {delta_y=y1-y2;incy=-1;}
if(delta_x>delta_y){xymax=delta_x;xymin=delta_y;} //区分最大轴(参考轴)位移、最小轴位移
else {xymax=delta_y;xymin=delta_x;}
for(t=0;t<=xymax;t++)
{
LCD_Draw_Fill_Circle(row,col,size,color); //描点函数(row,col)
err+=2*xymin;
row+=incx;
col+=incy;
if(err>xymax)err-=2*xymax;
else
{
if(xymax==delta_x)col-=incy;
else row-=incx;
}
}
}
方法二:
//画粗线方法2
//(x1,y1),(x2,y2):线条的起始坐标
//size:线条的粗细程度
//color:线条的颜色
void LCD_Draw_BLine(u16 x1,u16 y1,u16 x2,u16 y2,u8 size,u16 color)
{
u16 t;
int xerr=0,yerr=0,delta_x,delta_y,xymax;
int incx,incy,row,col;
delta_x=x2-x1; //计算坐标增量
delta_y=y2-y1;
row=x1;
col=y1;
if(delta_x>0)incx=1; //取方向变量
else if(delta_x==0)incx=0; //垂直线
else {incx=-1;delta_x=-delta_x;}
if(delta_y>0)incy=1;
else if(delta_y==0)incy=0; //水平线
else{incy=-1;delta_y=-delta_y;}
if( delta_x>delta_y)xymax=delta_x; //选取参考轴
else xymax=delta_y;
for(t=0;t<=xymax;t++)
{
LCD_Draw_Fill_Circle(row,col,size,color); //描点函数(row,col)
xerr+=2*delta_x;
yerr+=2*delta_y;
if(xerr>xymax)
{
xerr-=2*xymax;
row+=incx;
}
if(yerr>xymax)
{
yerr-=2*xymax;
col+=incy;
}
}
}
注:两个算法中均涉及到描点函数,实际上是一个画实心圆的函数,直线的粗细尺寸决定了圆的半径,相当于一个自定义粗细的圆头画笔。实心圆函数分析详见 基于像素点的图形显示算法(Bresenham圆形个人理解)