基础函数图像的绘制

您所在的位置:网站首页 画二次函数图像的步骤三步 基础函数图像的绘制

基础函数图像的绘制

2024-07-14 14:03| 来源: 网络整理| 查看: 265

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

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

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

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

#include #include 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(f(pixel)); int nextValue = static_cast(f(pixel + 1)); // 描点连线 putpixel(pixel, localValue, YELLOW); putpixel(pixel + 1, nextValue, YELLOW); line(pixel, localValue, pixel, nextValue); } _getch(); return 0; }

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

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

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

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

的图像。

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

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

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

#include #include 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(f(stepNow)); int nextValue = static_cast(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(f(stepNow)); int nextValue = static_cast(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 #include #include 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 年的一篇论文,文中介绍的算法能够绘制出许多非常刁钻的函数图形,甚至是塔伯自指涉公式。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3