Margoo

...?

基础函数图像的绘制 铜牌收录

一、基础的一次、二次函数图像绘制

先来考虑最简单的情况:若有函数 ,记其图像为 ,若 ,先要写一个程序:使用 EasyX 绘制出 ,应该怎么做呢。

考虑到 为一次函数,一次函数的图像实际上就是一条直线,所以可以计算出函数在窗口最左边和最右边的点坐标,并将两点相连即可,这种方法非常简单,但是只能处理严格递增或严格递减的线性函数,并处理不了复杂的函数,显然应该换另外一种方法。

回想初中阶段第一次学习函数图像的时候,课本介绍的就是描点连线的方法,所以我们可以使用描点连线的方法来绘制函数图像,先计算每个 X 坐标对应的函数坐标值,然后描点连线,这样子可以就可以处理非线性函数了,代码如下:

#include <conio.h>
#include <graphics.h>

constexpr double k			= -2;
constexpr double b			= 40;
constexpr int	 width		= 640;
constexpr int	 height		= 480;
constexpr int	 halfWidth	= width / 2;
constexpr int	 halfHeight = height / 2;

double f(const double& x) {
	return k * x + b;
}

void drawAxis() {
	setlinecolor(RED);
	line(-halfWidth, 0, halfWidth, 0);
	line(0, -halfHeight, 0, halfHeight);
}

int main() {
	initgraph(width, height);

	// 设置中心点,相当于确定直角坐标系 XoY 的原点
	setorigin(halfWidth, halfHeight);
	// 绘制坐标轴
	drawAxis();

	setlinecolor(YELLOW);

	for (int pixel = -halfWidth; pixel < halfWidth; ++pixel) {
		// 计算当前点和下一个点
		int localValue = static_cast<int>(f(pixel));
		int nextValue  = static_cast<int>(f(pixel + 1));

		// 描点连线
		putpixel(pixel, localValue, YELLOW);
		putpixel(pixel + 1, nextValue, YELLOW);
		line(pixel, localValue, pixel, nextValue);
	}

	_getch();

	return 0;
}

运行代码后,会有如下效果:

linear-function-graph

不过,可能已经有读者发现了,绘制出来的函数图像其实并不正确,这是因为在执行 setorigin 以后,建立的平面直角坐标系 XOY 与我们平时常用的坐标系的 Y 轴是相反的,所以在函数 f 中,应该修改函数的返回值为负数,就像这样:

double f(const double& x) {
	return -(k * pow(x, 2) + b);
}

这个方法不仅能够画出一次函数的图像,还能够画出二次函数的图像:

的图像。

目前看来,这个方法似乎能很好地绘制出函数的图像,然而存在两个问题:

  1. 绘制出的函数图像不能够调整缩放比例。
  2. 这个方法不能处理函数图像上的断点和某些趋于无穷的函数图像。

第一个问题非常好解决,只需要加入一个 step 变量代表计算机上每一个像素对应的步长,和一个 scale 代表 y 轴的缩放参数,代码如下:

#include <conio.h>
#include <graphics.h>

constexpr double k			= 0.01;
constexpr double b			= -40;
constexpr int	 width		= 640;
constexpr int	 height		= 480;
constexpr int	 halfWidth	= width / 2;
constexpr int	 halfHeight = height / 2;

double f(const double& x) {
	return -(k * x * x + b);
}

void drawAxis() {
	setlinecolor(RED);
	line(-halfWidth, 0, halfWidth, 0);
	line(0, -halfHeight, 0, halfHeight);
}

int main() {
	initgraph(width, height);

	// 步长
	constexpr double step	 = 0.6;
	// y 轴缩放系数
	constexpr double scale	 = 1.7;
	double			 stepNow = -halfWidth * step;

	// 设置中心点,相当于确定直角坐标系 XoY 的原点
	setorigin(halfWidth, halfHeight);
	// 绘制坐标轴
	drawAxis();

	setlinecolor(YELLOW);

	for (int pixel = -halfWidth; pixel < halfWidth; ++pixel) {
		// 计算当前点和下一个点
		int localValue = static_cast<int>(f(stepNow));
		int nextValue  = static_cast<int>(f(stepNow + step));

		// 描点连线
		putpixel(pixel, scale * localValue, YELLOW);
		putpixel(pixel + 1, scale * nextValue, YELLOW);
		line(pixel, scale * localValue, pixel, scale * nextValue);

		stepNow += step;
	}

	_getch();

	return 0;
}

然而第二个问题就比较棘手了,为了更加方便读者理解,这里我放出使用这个方法绘制的两个函数的图像:

其中图一是 的图像,而图二是 的图像,可以发现,这种方法都错误的多绘制了一条类似渐近线的线条,且都没有正确地绘制出曲线,然而这当然不会是渐近线。

导致没有正常绘制出曲线的原因是在原本的代码中,计算函数值的时候就将函数的值转换成了 int 类型,丢失了浮点数的小数位,就像这样:

int localValue = static_cast<int>(f(stepNow));
int nextValue  = static_cast<int>(f(stepNow + step));

其实这个问题非常好解决,只需要把代码改成这样:

// 计算当前点和下一个点
double localValue = f(stepNow);
double nextValue  = f(stepNow + step);

// 描点连线
putpixel(pixel, int(scale * localValue), YELLOW);
putpixel(pixel + 1, int(scale * nextValue), YELLOW);
line(pixel, int(scale * localValue), pixel, int(scale * nextValue));

就可以正常绘制出来了,但是多画出来的渐近线问题始终没有被解决。

二、解决断点问题

解决断点问题,有很多种方法,数学上断点的定义是   则称 为断点,显然,在计算机图像处理的时候不能使用这种方法来判断断点,当然也有另一种方法,就是利用函数的导数,通过计算函数上每个点的导数来确定断点,当然这种方法不能使用平时计算导数的各种计算方法,只能够使用最暴力的极限算法来计算导数,即 ,但是这样依然有一个问题,一个是对于比较复杂的函数计算算力消耗过大,还有一个问题就是对于不可导的函数,例如最简单的  其在 0 点处不可导。

第一种就是设定一个阈值 ,若,则认为 处有一个断点,不做连线,然而这种方法的问题在于,不是所有的断点都是非常大或者非常小的, 并不能有一个普适性的值,那么究竟该怎么办呢。

实际上,观察后发现,仅从图像上而言,两个像素之间的图像看起来不是严格递增,就是严格递减的,这是因为最终要干的事情就是在两点之间连线,换而言之,哪怕这个函数实际上在 中有其他的变化情况,体现到函数图像上也是一条直线,那就非常简单了,这个问题就可以抽象成:

若函数 处没有断点,若 ,则应 ,反之亦然,然而,为了防止函数实际上连续,但是存在 中细微的变化, 的取值应该尽可能的小,这里我推荐取 ,实际测试后这个数值能够应对大部分的基础函数,使用这个方法,修改代码如下:

#include <cmath>
#include <conio.h>
#include <graphics.h>

constexpr int width		 = 640;
constexpr int height	 = 480;
constexpr int halfWidth	 = width / 2;
constexpr int halfHeight = height / 2;

void drawAxis() {
	setlinecolor(RED);
	line(-halfWidth, 0, halfWidth, 0);
	line(0, -halfHeight, 0, halfHeight);
}

double f(const double &x) {
	return -(sin(x) * tan(x));
}

int main() {
	constexpr double step	 = 0.06;
	constexpr double scale	 = 17;
	double			 x0		 = -halfWidth * step;

	initgraph(width, height);
	setorigin(halfWidth, halfHeight);

	BeginBatchDraw();

	// 绘制坐标轴
	drawAxis();

	setlinecolor(YELLOW);
	for (int pixel = -halfWidth; pixel < halfWidth; ++pixel) {
		double result = f(x0);
		double next	  = f(x0 + step);

		// 计算 f(x0 + ω)
		double omega  = f(x0 + step / 100);

		// 对比 f(x0) 与 f(x1) 然后利用 f(x0 + ω) 与 f(x1) 判断是否是断点
		if (next > result) {
			if (omega > result) {
				putpixel(pixel, (int)(result * scale), YELLOW);
				putpixel(pixel + 1, (int)(next * scale), YELLOW);
				line(pixel, (int)(result * scale), pixel + 1, (int)(next * scale));
			} else {
				putpixel(pixel, (int)(result * scale), YELLOW);
				putpixel(pixel + 1, (int)(next * scale), YELLOW);
			}
		} else {
			if (omega < result) {
				putpixel(pixel, (int)(result * scale), YELLOW);
				putpixel(pixel + 1, (int)(next * scale), YELLOW);
				line(pixel, (int)(result * scale), pixel + 1, (int)(next * scale));
			} else {
				putpixel(pixel, (int)(result * scale), YELLOW);
				putpixel(pixel + 1, (int)(next * scale), YELLOW);
			}
		}

		x0 += step;
	}

	FlushBatchDraw();

	_getch();

	return 0;
}

这个时候再去绘制  的图像就能正常显示了:

然而,这种方法并不能够正确绘制 内微分变化非常极端的函数,或者是最终溢出取值范围的函数,例如 就会因为数值溢出而最终呈现出奇怪的图像:

上面的问题可以通过引入大整数运算以及其他的优化方法来解决,但依然存在一个问题:我们并没有考虑函数的定义域问题,实际上并不是所有函数都在区间内有定义,例如,若要绘制 的图像,则会出现除 0 的错误,所以说,需要针对特殊情况进行特殊的处理。

三、总结

本文介绍了一个方法,这个方法能够绘制一些基本的函数图像,然而,文中只是对绘制函数图像这个问题进行了非常浅显的讨论,这个方法也之能绘制出一些非常有限的函数图像。

事实上,要想在计算机中绘制出函数图像,并非一件易事,若有想要深入了解的同学,可以查阅这篇 Jeff Tupper 于发表 2001 年的一篇论文,文中介绍的算法能够绘制出许多非常刁钻的函数图形,甚至是塔伯自指涉公式。

添加评论