学习探究——自学习画线函数和Bresenham 直线算法

目录

引言

一、描点函数

二、画线函数实现的思维

三、画线函数逐一实现

3.1 前置部分

3.2 实现第一步——容错代码

3.3 实现第三步——各线实现

3.4 总体实现代码和测试

四、进阶算法 Bresenham 直线算法


引言

在最近学习韦东山老师的Linux开发过程中,学习到了LCD屏幕的描点函数,让我顿时上头,经过一晚上奋斗,研究出了基于韦东山老师的描点函数和韦东山老师的例程的画线函数,需然个人觉得这个函数质量跟各位大牛还有很大差距,但是根据我之前做智能车的经历,觉得写这个函数的思想跟智能车的图像处理是相通的,这也解决了我当初做智能车时对图像理解的一些朦胧问题,Bresenham 直线算法非我原创,但代码高效,用来收录进代码仓库。

我尽量复刻我当时学习的思维过程,但这个过程对于强者来说肯定过于啰嗦,所以请见谅。

实践的环境是:Linux、ubantu、IMX6ULL_Pro开发板

一、描点函数

说明:这个描点函数是韦东山老师提供出来学习的,有比较精妙的地方,如其将不同位数的颜色RGB格式之间的转换等,但是我这篇主要讲我这个时期对画线函数的理解,就不对这函数多言,仅贴出来参考。

/**********************************************************************
 * 函数名称: lcd_put_pixel
 * 功能描述: 在LCD指定位置上输出指定颜色(描点)
 * 输入参数: x坐标,y坐标,颜色
 * 输出参数: 无
 * 返 回 值: 会
 * 修改日期        版本号     修改人	      修改内容
 * -----------------------------------------------
 * 2020/05/12	     V1.0	  zh(angenao)	      创建
 ***********************************************************************/ 
void lcd_put_pixel(int x, int y, unsigned int color)
{
	unsigned char *pen_8 = fb_base+y*line_width+x*pixel_width;
	unsigned short *pen_16;	
	unsigned int *pen_32;	

	unsigned int red, green, blue;	

	pen_16 = (unsigned short *)pen_8;
	pen_32 = (unsigned int *)pen_8;

	switch (var.bits_per_pixel)
	{
		case 8:
		{
			*pen_8 = color;
			break;
		}
		case 16:
		{
			/* 565 */
			red   = (color >> 16) & 0xff;
			green = (color >> 8) & 0xff;
			blue  = (color >> 0) & 0xff;
			color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
			*pen_16 = color;
			break;
		}
		case 32:
		{
			*pen_32 = color;
			break;
		}
		default:
		{
			printf("can't surport %dbpp\n", var.bits_per_pixel);
			break;
		}
	}
}

二、画线函数实现的思维

我们学数学的时候知道,两点成线

图2.1 两点成线

所以画一条线我们首先得有四个参数:起始横坐标 x_on 、 起始纵坐标 y_on 、 结束横坐标 x_en 、 结束纵坐标 y_en

再加上基本的颜色参数 color

我们就可以做到第一步,确认我们设计的函数的形参。

而在平面空间上,我们通常用坐标系来表达位置 ,即当我们以 起始横坐标 x_on 、 起始纵坐标 y_on 作为坐标系原点的时候,那 结束横坐标 x_en 、 结束纵坐标 y_en 就会存在我们设计的坐标原点的几个方位,分别是 第一、二、三、四象限和x轴的正负半轴和y轴的正负半轴。

图2.2 自建坐标系

为什么要建立这个坐标系呢,因为这样可以更加直观的看出点的位置的不同,而且程序中因为点的位置的不同,会导致算法需要微微修改。

那么这个只是我们理想的坐标系,但是实际上,LCD有一个它自己的坐标系,即在一个四边形中,左上角为坐标原点:

图2.3 LCD坐标与自建坐标对比

三、画线函数逐一实现

3.1 前置部分

这部分由例程提供的,并不上本文的核心部分,为了保持文章的完整性,故而贴出在此,并且只有我当初学习时写的一些代码注释。

头文件和全局变量:

#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <math.h>
#include <stdlib.h>

static int fd_fb;
static struct fb_var_screeninfo var;	/* 获取的LCD显示屏的分辨率等的参数结构体 */
static int screen_size;
static unsigned char *fb_base;
static unsigned int line_width;
static unsigned int pixel_width;

主函数部分(获取分辨率参数等,由韦东山老师提供):

	int i;

	//打开设备节点
	fd_fb = open("/dev/fb0", O_RDWR);
	if (fd_fb < 0)
	{
		printf("can't open /dev/fb0\n");
		return -1;
	}
	//获取LCD屏的分辨率等数据(可变参数)
	if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))//FBIOGET_VSCREENINFO:获得可变的屏幕信息var
	{
		printf("can't get var\n");
		return -1;
	}

	line_width  = var.xres * var.bits_per_pixel / 8;//长度*bpp/8 为行的长度(高度上的像素分辨率)
	pixel_width = var.bits_per_pixel / 8; //一个像素的大小
	screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
	
	/* 通过mmap映射显存 screen_size(映射大小):var.xres * var.yres(x方向上的分辨率*y方向上的分辨率就能知道像素个数 像素个数*bpp/8得到总大小)
	 * PROT_READ | PROT_WRITE :属性值
	 * fd_fb : 文件位置
	 * 返回值 : 映射基地址(通过操作这个地址能直接改变显存)
	 * 例如:描点(x,y) : 即fb_base + y * line_width + x * pixel_width  
	 */
	fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
	if (fb_base == (unsigned char *)-1)
	{
		printf("can't mmap\n");
		return -1;
	}

	/* 清屏: 全部设为白色 */
	memset(fb_base, 0xff, screen_size);

3.2 实现第一步——容错代码

写函数按照我的思维来说就是将各个小功能组装成大功能的过程,所以我们先迈出第一步。

图2.3 LCD坐标与自建坐标对比 我们可以知道LCD屏的(x,y)坐标是有一定大小范围的,超出这个范围的数据处理毫无意义,所以我们先写一个容错代码,即用if去判断我们输入的各个x、y形参是否是正确的参数。

代码如下(其中的var.xers代表LCD屏的像素宽,var.yres代表LCD屏的像素高):

	/* 首先,我们知道输入的x点和y点的大小应该小于对应的 最大值 */
	if(x_on < 0 || x_on > var.xres || y_on < 0 || y_on > var.yres || x_en < 0 || x_en > var.xres || y_en < 0 || y_en > var.yres)
	{
		printf("your int x&y is error! \n");
		return ;
	}

3.3 实现第二步——在第二象限上画一条线

在 图2.2 自建坐标系 中我们知道完整的画线函数需要处理这么多部分,我们先选其中一个进行处理。

图 3.1 在第二象限上画一条直线

由该图我们知实际上的 x_on > x_en , y_on > y_en (受到LCD屏坐标系影响),所以这个可以作为我们的判断条件,用来判断此时应该处理第二象限。

那么,如何画线呢,其实就是对这条直线上的各点进行画点,即可以实现。

那么如何求出这个直线上的各点呢?

我们知道直线的数学公式:y = k * x + b

在我们自建坐标系中,y_on = k*(x_on) + b

所以截距b = y_on - k*(x_on)

所以我们先求出斜率,然后求出下一个点:

下一个点为(x_on - x , k * (x_on - x) + y_on - k*(x_on) )

(x_on - x ,y_on- k * x )

x即偏移量的计数,

这样,我们可以得到一个画线代码:

int x;
float slope;
if(x_on > x_en && y_on > y_en)
{
	slope = (float)(y_en - y_on) / (float)(x_en - x_on);//计算斜率
	/* 即从xy往其左上画点 */
	for(x = 0;x <= abs(x_on - x_en); x++)
	{
		/* 画点 */
		lcd_put_pixel(x_on - x, (int)(y_on - x*slope), color);
	}
}

3.3 实现第三步——各线实现

实现由 图2.3 LCD坐标与自建坐标对比

首先我们实现两个坐标轴的画线:

/* x不变 y变 这是y轴 */
if(x_on == x_en && y_on != y_en)
{
	dir = (y_on - y_en)/abs(y_on - y_en);
	for(y = 0; y <= abs(y_on - y_en); y++)
	{
		/* 画点 */
		lcd_put_pixel(x_on, y_on - y*dir, color);
	}
}
/* y不变 x变 这是x轴 */
else if(x_on != x_en && y_on == y_en)
{
	dir = (x_on - x_en)/abs(x_on - x_en);
	for(x = 0; x <= abs(x_on - x_en); x++)
	{
		lcd_put_pixel(x_on - x*dir, y_on, color);
	}
}

  然后实现斜线:

else
{
	dir = (x_on - x_en)  / abs(x_on - x_en);
	slope = (float)(y_en - y_on) / (float)(x_en - x_on);//计算斜率
	for(x = 0;x <= abs(x_on - x_en); x++)
	{
		/* 画点 */
		lcd_put_pixel((x_on - dir*x), (int)(y_on - dir*x*slope), color);
	}
}

3.4 总体实现代码和测试

那么我们就有一个画线函数了:

/**********************************************************************
 * 函数名称: lcd_put_cross
 * 功能描述: 在LCD指定位置上输出指定颜色(画线)
 * 输入参数: 起始x坐标,起始y坐标,结束x坐标,结束y坐标,颜色
 * 输出参数: 无
 * 返 回 值: 会
 * 修改日期        版本号     修改人	      修改内容
 
 * -----------------------------------------------
 * 2024/08/19	     V1.0	  wuguangjin	      创建
 ***********************************************************************/ 
void lcd_put_cross(int x_on, int y_on, int x_en, int y_en, unsigned int color)
{
	float slope = 0;//获取斜率
	int x;
	int dir = 1;
	/* 画线函数的实现是基于画点函数的,即知道开始的坐标和结束的坐标,然后从开始的坐标依次移动到结束坐标
	 * 例如:在40,40 - 40,80 之间画一条线
	 */
	/* 首先,我们知道输入的x点和y点的大小应该小于对应的 最大值 */
	if(x_on < 0 || x_on > var.xres || y_on < 0 || y_on > var.yres || x_en < 0 || x_en > var.xres || y_en < 0 || y_en > var.yres)
	{
		printf("your int x&y is error! \n");
		return ;
	}

	/* x不变 y变 这是y轴 */
	if(x_on == x_en && y_on != y_en)
	{
		dir = (y_on - y_en)/abs(y_on - y_en);
		for(y = 0; y <= abs(y_on - y_en); y++)
		{
			/* 画点 */
			lcd_put_pixel(x_on, y_on - y*dir, color);
		}
	}
	/* y不变 x变 这是x轴 */
	else if(x_on != x_en && y_on == y_en)
	{
		dir = (x_on - x_en)/abs(x_on - x_en);
		for(x = 0; x <= abs(x_on - x_en); x++)
		{
			lcd_put_pixel(x_on - x*dir, y_on, color);
		}
	}
	else
	{
		dir = (x_on - x_en)  / abs(x_on - x_en);
		slope = (float)(y_en - y_on) / (float)(x_en - x_on);//计算斜率
		for(x = 0;x <= abs(x_on - x_en); x++)
		{
			/* 画点 */
			lcd_put_pixel((x_on - dir*x), (int)(y_on - dir*x*slope), color);
		}
	}

}

关于测试,我用了例程加自己修改的一些测试:

int main(int argc, char **argv)
{
	int i;
	
	fd_fb = open("/dev/fb0", O_RDWR);
	if (fd_fb < 0)
	{
		printf("can't open /dev/fb0\n");
		return -1;
	}
	if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
	{
		printf("can't get var\n");
		return -1;
	}

	line_width  = var.xres * var.bits_per_pixel / 8;
	pixel_width = var.bits_per_pixel / 8;
	screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
	fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
	if (fb_base == (unsigned char *)-1)
	{
		printf("can't mmap\n");
		return -1;
	}

	/* 清屏: 全部设为白色 */
	memset(fb_base, 0xff, screen_size);


	/* 画线测试1 */
	/* 二象限画直线 红色 */
	lcd_put_cross(var.xres/2, var.yres/2, var.xres/2 - 200, var.yres/2 - 200, 0xFF8000);
	/* 四象限画直线 黄色 */
	lcd_put_cross(var.xres/2, var.yres/2, var.xres/2 + 200, var.yres/2 + 200, 0xFFFF00);
	/* 一象限画直线 黄色 */
	lcd_put_cross(var.xres/2, var.yres/2, var.xres/2 + 200, var.yres/2 - 200, 0xFFFF00);
	/* 三象限画直线 红色 */
	lcd_put_cross(var.xres/2, var.yres/2, var.xres/2 - 200, var.yres/2 + 200, 0xFF8000);
	/* y正轴画直线 蓝色 */
	lcd_put_cross(var.xres/2, var.yres/2, var.xres/2, var.yres/2 - 200, 0x0000FF);
	/* y负轴画直线 紫色 */
	lcd_put_cross(var.xres/2, var.yres/2, var.xres/2, var.yres/2 + 200, 0x800080);
	/* x正半轴画直线 绿色 */
	lcd_put_cross(var.xres/2, var.yres/2, var.xres/2 + 200, var.yres/2, 0x00FF00);
	/* x负半轴画直线 粉色 */
	lcd_put_cross(var.xres/2, var.yres/2, var.xres/2 - 200, var.yres/2, 0xFFC0CB);
	munmap(fb_base , screen_size);
	close(fd_fb);
	
	return 0;	
}

测试结果:

图3.2 测试结果图

四、进阶算法 Bresenham 直线算法

这个算法避免了用浮点型运算,更加高效率

/* Bresenham 直线算法 */
int dx = abs(x_en - x_on);
int dy = abs(y_en - y_on);
int sx = (x_on < x_en) ? 1 : -1;
int sy = (y_on < y_en) ? 1 : -1;
int err = dx - dy;

while (1) {
	lcd_put_pixel(x_on, y_on, color);
	if (x_on == x_en && y_on == y_en) break;
	int e2 = err * 2;
	if (e2 > -dy) { err -= dy; x_on += sx; }
	if (e2 < dx) { err += dx; y_on += sy; }
}

本期即到这里,如有错误,欢迎指导!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

跳河轻生的鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值