一种基于纯C语言的函数绘图解决方案
本文尝试实现一种利用C语言绘制函数图像的方法。最终输出为PPM格式图片。
一、PPM图片格式
我们首先来看一下我们的问题:怎么用纯C语言写一个plot()
函数。那么我们迎面而来遇到的第一个问题就是:我们平常使用的C语言,往往是在一个黑框框里面输入一堆数字,然后输出一堆数字,而现在却要展示一张图片,这怎么做呢?但是我们静下心来想一想,就会发现所谓的图片实际上也是文件,而文件就是可以用C语言的freopen
读写的。但是我们平时读写的都是纯文本文件,现在却要写入一张图片文件。怎么做到呢?这时就需要一种非常简单的图片编码:PPM格式。
PPM格式是 Netpbm [1]的一部分。这个项目使用和定义了几种图形格式。便携式像素映射格式(PPM)、便携式灰图格式(PGM) 和便携式位图格式(PBM) ,旨在便于在平台之间交换。
一个PPM文件由两部分组成,即文件头和数据流。文件头的第一部分是一个Magic Number,格式为P%d
,表示了文件的类型,具体如下表所示:
类型 | ASCII (普通) | 二进制(原始) |
---|---|---|
便携式位图(PBM) | P1 | P4 |
便携式灰度图(PGM) | P2 | P5 |
便携式像素映像(PPM) | P3 | P6 |
文件头的第二部分是两个数字,表示了图像的宽度和高度。
而PGM和PPM文件的文件头有第三部分,是一个数字,表示颜色(分量)的最大值。
数据流部分是以矩阵形式显示的像素点。接下来我们看几个例子:
1 |
|
这是一个PBM(位图)格式的文件,其中0表示白色,1表示黑色。显示成图片的话是这个样子(放大了20倍)
1 |
|
这个一个PGM(灰度图)的例子,我们可以看到第四行,这就是所谓“颜色(分量)的最大值”。它对应的图片是这个样子(当然也经过了放大):
1 |
|
这是一个PPM图片的例子,我们可以看到每个像素点使用了三个数字来指定颜色,也就是我们所说的RGB。
到这里,你应该已经掌握了PPM格式图片怎么写了吧!
二、定义函数表
为了方便绘图,首先我们定义PPM数据类型,由RGB三个颜色分量组成。
1 |
|
这个结构体的构造函数为:
1 |
|
主要执行绘图功能的函数为:
1 |
|
其中各参数的意义如下:
项目 | 意义 |
---|---|
char *name |
输出文件的文件名 |
PPMdata **matrix |
PPM文件流矩阵,大小至少为height*width |
const double *x |
采样点x坐标 |
double *y |
采样点y坐标 |
int width |
整个图像的宽度 单位:像素 |
int height |
整个图像的高度 单位:像素 |
int arrayLen |
采样点的数目 |
double centerX |
图片中心点对应的直角坐标X |
double centerY |
图片中心点对应的直角坐标Y |
double rangeX |
X直角坐标范围 |
double rangeY |
Y直角坐标范围 |
double gridX |
X网格宽度(单位:直角坐标) |
double gridY |
Y网格宽度(单位:直角坐标) |
将直角坐标量转化为像素量(矩阵下标量)的函数为:
1 |
|
其中各参数的意义为:
项目 | 意义 |
---|---|
double num |
待转换的直角坐标量x |
double center |
图片该方向中心点对应的直角坐标c |
double range |
该方向直角坐标范围r |
int picLen |
图片该方向的像素大小p |
转换公式为: \[ p\left(\frac 12+\frac{x-c}{r}\right) \] 在图像上绘制坐标点的函数为:
1 |
|
各新参数的意义如下:
项目 | 意义 |
---|---|
int x |
要绘制的点的像素X坐标 |
int y |
要绘制的点的像素Y坐标 |
PPMdata color |
颜色 |
int size |
点的大小,最后绘制出来是一个\(2size+1\)边长的正方形 |
将PPMdata
数据矩阵转换为.ppm
图像文件的函数如下:
1 |
|
没有新的参数。
由于绘图时采用分段线性拟合算法,定义线性函数:
1 |
|
这个定义的意思是:\(y=\text{linerFunc}(x_1,y_1,x_2,y_2,x)\)表示一条经过\((x_1,y_2)\)和\((x_2,y_2)\),以\(x\)为自变量的直线。直线方程为: \[ y=(x-x_1)\frac{y_2-y_1}{x_2-x_1}+y_1 \]
三、实现函数
这部分以arrayToPPM()
函数的实现为主线,完整介绍各个函数的实现方法。
第一步,检测数据的合法性
1 |
|
首先,为了方便,我们强制把像素大小设置为偶数。然后主要检查三个事:
- \(x\)坐标是否严格单调递增。若不是,返回错误并退出。
- \(x\)坐标是否在范围内。若不是,返回错误并退出。
- \(y\)坐标是否在范围内。若不是,强制将其设置在范围内。
第二步,定义PPMdata颜色。
1 |
|
各变量的意义如下:
项目 | 意义 |
---|---|
background |
背景颜色 |
axis |
主坐标轴颜色 |
grid |
网格颜色 |
line |
要画的函数的颜色 |
这里就用配色软件找一个好看的颜色就行。
第三步,绘制背景板
画背景
1
2
3
4
5for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
matrix[i][j] = background;
}
}画网格
1
2
3
4
5
6
7
8
9
10
11
12for (int i = 0; centerX + i * gridX <= centerX + rangeX / 2; ++i) {
for (int j = 0; j < height; ++j) {
drawPoint(matrix, width, height, numToMatPos(centerX + i * gridX, centerX, rangeX, width), j, grid, 0);
drawPoint(matrix, width, height, numToMatPos(centerX - i * gridX, centerX, rangeX, width), j, grid, 0);
}
}//纵向网格
for (int i = 0; centerY + i * gridY <= centerY + rangeY / 2; ++i) {
for (int j = 0; j < width; ++j) {
drawPoint(matrix, width, height, j, numToMatPos(centerY + i * gridY, centerY, rangeY, height), grid, 0);
drawPoint(matrix, width, height, j, numToMatPos(centerY - i * gridY, centerY, rangeY, height), grid, 0);
}
}//横向网格画主坐标轴
1
2
3
4
5
6
7for (int i = 0; i < width; ++i) {
drawPoint(matrix, width, height, i, height / 2, axis, 1);
}
for (int i = 0; i < height; ++i) {
drawPoint(matrix, width, height, width / 2, i, axis, 1);
}这里的“主坐标轴”,恒定位于图像的正中心。
这里涉及到了drawPoint()
函数。它的实现如下:
1 |
|
主要还是要检测是不是越界了,不然容易RE
.还有一个需要注意的地方就是第6行,这里的x和y互换了。为什么要互换呢?我们想一想矩阵下标的顺序和坐标的顺序有什么区别就好了。
第四步,画函数
我们经过了这么长的准备,终于要开始画函数了。
1 |
|
既然我们用的是分段线性拟合法,那么就要确认当前在哪个段里,也就是代码中的linerIndex
变量。
X
就是枚举变量。如果当前比x[]
数列中的最小值还小,那么不应该有图像,否则找到当前所在的段,然后画一条线段。
第五步,写文件
1 |
|
这里matToPPM
函数的实现如下:
1 |
|
就是重定向,输出,再重定向回来,没什么特别值得说明的。
四、试用一下
我们把上面那一堆函数和实现打包到一个.h
文件里。然后我们写个代码调用一下:
1 |
|
这个代码的功能是绘制 \[ y=\frac{\sin 2x}{2x} \] 的图像。
编译运行,在目录下输出了一个out.ppm
文件,打开:
大功告成咯!
五、更多讨论
这个代码存在以下问题:
- 面对函数变化率特别高的情况下表现不理想,会有间断的情况,如下图所示:
\[ y=\tan x \]
- 需要人工指定的变量太多,日后我会开发一版能自动适应采样点,选择合适的坐标、范围的绘图函数。
本站的运行成本约为每个月5元人民币,如果您觉得本站有用,欢迎打赏:
- https://en.wikipedia.org/wiki/Netpbm#File_formats ↩︎