最近被几(公)张(司)图(任)片(务)给美(吓)哭了,于是决定来研究研究茱莉亚(Julia)集合。Julia集合本质上是对级数的敛散性的描述,仅仅只说数字,想必大家早都看睡着了,因此当敛散性和计算机图像二者结合到一块儿的时候就非常非常美了。话不多说,先上几张图:
想要问问你美不美~,这些图片都是靠openCV实现的,那么接下来就来分析分析如何画出这么美丽的图片。
首先,你得了解基本的数学知识——敛散性。敛散性分为收敛和发散。收敛指的是一个级数最后的求和结果趋于一个常数,发散是指一个级数最后的求和结果趋于无穷大或无穷小。
茱莉亚即便是根据敛散性画出来的,首先我们任取一个复数变量z,让这个复数不断的按照某个公式迭代,最后会产生一个新的复数,这个新的复数的模如果趋于无穷大/无穷小,则我们认为该复数在这种情况下是发散的,反之收敛。是不是还是没搞懂?正常,不用怕。换一种通俗易懂的语言,我们以复平面中的任一点坐迭代,如果最后的值是收敛的,我们把这一块涂成蓝色,如果值是发散的,我们则涂成黑色。说到这里,是不是有点熟悉了?没错,你看到的上面三张图就是这样形成的。所有蓝色部分全是表示收敛点,所有黑色部分全部表示发散点。
那么,茱莉亚集合究竟是个什么样的迭代公式呢?公式就是:
Zn = Zn-1 * Zn-1 + C0
这里Z是变量,C0是一个常量(复数),之所以上面有三种不同的形状就是因为改变了C0的值。说到这里,想必应该很清楚是怎么回事了吧?在画茱莉亚集合的时候,我们肯定要先构建一个复数类,同时可以重写以下相关的操作符(这里如果不知道什么是重写操作符可以看一下我以前的博客)。话不多说,先上代码:
class ComplexNum {
private:
float real;
float imag;
public:
ComplexNum(float a, float b);
ComplexNum& operator+(const ComplexNum& c); //重写加法
ComplexNum& operator*(const ComplexNum& c); //重写乘法
float module(); //求复数的模
};
以上是定义复数类的声明文件(.h文件)。
#include "Complex.h"
#include<math.h>
ComplexNum::ComplexNum(float a, float b) : real(a), imag(b) {}
ComplexNum& ComplexNum::operator+(const ComplexNum& c) {
this->real += c.real;
this->imag += c.imag;
return *this;
}
ComplexNum& ComplexNum::operator*(const ComplexNum& c) {
float r = this->real * c.real - this->imag * c.imag;
float i = this->real * c.imag + this->imag * c.real;
//this->real = this->real * c.real - this->imag * c.imag;
//this->imag = this->real * c.imag + this->imag * c.real; //注意,上面两行一定不能用这两行代替否则会引起严重的错误,可以尝试一下并思考一下为什么
this->real = r;
this->imag = i;
return *this;
}
float ComplexNum::module() { //计算模
return (float)(sqrt(this->real * this->real + this->imag * this->imag));
}
以上是复数类的定义文件(.cpp文件)
该用到的复数类都构建完了,接下来就是剩下最后两步,如何实现迭代和画图了。
首先应该会有人好奇为什么需要迭代吧?这个是从数学敛散性中来的,敛散性是基于级数实现的,级数就意味着是一个数按照无穷次的公式计算得到的结果。当然在计算机中不可能进行无穷次的计算,因此,我们用一个较大的计算次数来代表无穷次(此处我取得是200次,当然也可以尝试更大的数和小一点的数,这里数字一旦改变,出来的图形可能就不一样咯,不信可以试试)。同理,如何判断是发散还是收敛呢?发散的定义是要趋于无穷大/无穷小,计算机中不可能存在无穷大/无穷小的数,这里我们也需要取一个相对较大/较小的数来表示无穷大/无穷小(这里我取得是1000000和-1000000,同理大家也可以尝试大点的数和小点的数,看看最后出来的结果有什么不同)。好了,那接着看代码吧:
#pragma once
#include "Complex.h"
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/core/core.hpp>
#include<opencv2/imgproc/imgproc.hpp>
ComplexNum& iter(ComplexNum& a, int iteratorNum); //声明迭代函数,参数为一个复数(即迭代变量Z)和迭代次数iteratorNum
cv::Vec3b getPixel(float dist); //传入一个float值,返回一个Pixel, 即当float值为收敛时,该像素为蓝色,否则为黑色
以上代码为迭代函数和像素函数的声明文件(.h文件)
#include "Iterator.h"
static float real0 = 0.285; //这个值表示C0的实部
static float imag0 = 0.02; //这个值表示C0的虚部
ComplexNum& iter(ComplexNum& a, int iteratorNum) {
if (iteratorNum == 0) //迭代的返回条件,即迭代耗尽
return a;
else {
ComplexNum c0(real0, imag0);
ComplexNum temp = a * a; //这个表示Zn-1 * Zn-1
temp = temp + c0;
return iter(temp, iteratorNum - 1); //继续迭代
}
}
cv::Vec3b getPixel(float dist) {
if (dist > -1000000 && dist < 1000000) //若传入的复数的模大于1000000或小于-1000000,则我们认为是收敛的,否则我们认为是发散的
return cv::Vec3b(255, 0, 0); //收敛返回蓝色点,当然如果你觉得蓝色不好看可以改为红色(这里三个数分别表示B, G, R三个通道)
else
return cv::Vec3b(0, 0, 0); //否则发散点返回黑色
}
以上代码为迭代函数和画图函数的定义文件(.cpp文件)
那么接下来就是控制迭代啦。这里的变量Z怎么来呢?在这里我们将整个图形的平面视为一个复平面,横向为x轴,纵向为y轴。因此我们可以直接通过遍历每一个元素点(像素)来生成一个Z0进行迭代。生成方式见代码:
#include <iostream>
#include "Complex.h"
#include "Iterator.h"
using namespace cv;
int main()
{
Mat imag = cv::Mat(cv::Size(500, 500), CV_8UC3, Scalar::all(10)); //创建一个图像,size为500 * 500, 但内容为空
for (int i = 0; i < 500; i++) {
for (int j = 0; j < 500; j++) {
float x = (float)(i - 250) / 200.0;
float y = (float)(j - 250) / 200.0;
ComplexNum c = ComplexNum(x, y); //生成一个z变量
imag.at<cv::Vec3b>(i, j) = getPixel((iter(c, 200)).module()); //将像素结果写入图像中
}
}
cv::imshow("Julia | c0(0.285, 0.02)", imag); //显示
cv::waitKey(0);
}
大功告成,Julia集合可以随意画了!