函數共 268 頁 11783 行,前后折騰了近半個月。可惜還是有些小瑕疵,不然文章標題就該叫《震驚!死宅竟用三角函數畫出可愛的老婆》了......
前言
在推上看到這樣一張圖:
覺著用函數畫妹子什么的好有趣,決定自己試著實現一下。
思路
從圖中可以看到,繪制妹子的函數為三角函數項的和。不難發現,函數以 t 為參數,兩個一組分別為正弦級數和余弦級數,首項(a0/b0)為常數,很容易看出來,函數應該和傅里葉級數有關系。
大體思路如下:
- 預處理圖片。用 MATLAB 將想要用函數繪制的圖片讀入
- 提取輪廓
- 獲取輪廓的坐標
- 處理坐標。獲取到的坐標為 N 點長的離散序列,將其進行 N 點離散傅里葉級數展開(DFS)
- 驗證。將輸出結果的函數繪制成圖像,觀察效果
實現
先畫個簡單點的。桌面上有張可愛的 Pusheen 圖片,就用它啦。
1. 預處理圖片
將 Pusheen 圖片讀入MATLAB。
cd C:\Users\Desktop
pic = imread('C:\Users\Ascend\Desktop\pusheen.jpg');
轉換為二值圖像。
pic = im2bw(pic);
show(pic)
轉換為二值圖像的原因是便于提取輪廓。看下效果。
2. 提取輪廓
這里直接調用了 MATLAB 自帶提取輪廓的算法。
pic = edge(pic,'sobel');
imshow(pic);
效果如下圖,看起來不錯,輪廓分明線條干凈。
3. 獲取輪廓坐標
獲取輪廓坐標,并將能夠圍成封閉曲線的坐標分組,則每組為一個有限長的離散序列,再將每個序列進行 DFS 展開。
獲取輪廓坐標并不難,難在擬合封閉曲線這里。想了想毫無頭緒,上網搜索找到了 MATLAB 自帶的 imcontour 函數。精度一般,使用條件苛刻,后文再敘,先湊合用。
point = imcontour(pic);
得到一個 2x126494 的矩陣 point,里面有我們需要的坐標。
4. 處理坐標
處理矩陣
坐標已經得到,先試著用坐標畫一下圖像。
width=point(2,:);
height=point(1,:);
plot(width,height)
卻得到了一副非常奇怪的圖像。
按道理,坐標描點得到的圖像就應該為 Pusheen 貓的輪廓。注意到圖像有幾個異常點,橫坐標特別大。查看矩陣 point 的結構。
第一行為縱坐標 height ,第二行為橫坐標 width,每一列為一個點 (height,width)。
第一個點的數值很奇怪。橫坐標為 2435 ,縱坐標為 0.1,怎么看都不是輪廓上的點。也許是某種標記?于是向后翻了2435個點,看到第 2437 列:
又是一個 (0.1,2427)。再向后翻 2427 個點:
第 4865 列,又是一個 (0.1,2531)。于是這個點的意義就清楚了:橫坐標為 0.1 的點為特殊標記,其值為該組點的個數。依此可以將這個 2x126494 的矩陣拆成多個小矩陣,每個矩陣為一組可以擬合成封閉曲線的坐標(imcontour函數認為的)。
拆分矩陣
%判斷point(1,:)第一行 0.1 的個數,從而判斷length的長度
length=0;
for i=1:size(point,2)
if point(1,i)==0.1
length=length+1;
end
end
%開始分段,拆成a1,a2,a3,...,一共length個二維數組
for i=1:length
temp=point(2,1);
point(:,1)=[];
eval(sprintf('a%d=point(1:2,1:temp);', i)); %將第i段數據送入第i個數組
point(:,1:temp)=[];
end
%縱坐標取負數,將離散數據轉為坐標
for i=1:length
eval(sprintf('a%d(2,:)=-a%d(2,:);',i,i));
end
離散傅里葉級數展開
離散傅里葉級數的公式如下,根據公式,寫出 MATLAB 代碼。
file = fopen('outer.txt', 'w'); %I/O方式輸出結果,結果輸出到桌面的outer.txt中
for count=1:length
eval(sprintf('a=a%d;',count));
N = size(a,2);
%a->ax(虛數組)
are=a(1,:);
aim=a(2,:);
ax=are+aim*j;
clear are aim;
%A:傅里葉正變換數組
A=[];
for i = 0:N-1
ang = -2 * 1j * pi * i / N;
r = 0;
for j=0:N-1
r = r + exp(ang * j) * ax(j+1);
end
A(i+1)=r;
end
clear ang i j r;
fprintf(file,['t=1:',num2str(N),';\n']);
fprintf(file,'x(t)=');
for i = 0:N-1
ang = 2 * pi * i / N;
fprintf(file,'+');
fprintf(file,[num2str(real(A(i+1) / N)),'*cos(', num2str(ang), '*t)-']);
fprintf(file,[num2str(imag(A(i+1) / N)),'*sin(', num2str(ang), '*t)']);
end
fprintf(file,';\n');
fprintf(file,'y(t)=');
for i = 0:N-1
ang = 2 * pi * i / N;
fprintf(file,[num2str(imag(A(i+1) / N)),'*cos(', num2str(ang), '*t)+']);
fprintf(file,[num2str(real(A(i+1) / N)),'*sin(', num2str(ang), '*t)+']);
end
fprintf(file,'0;\n');
fprintf(file,'plot(x,y);hold on;\n');
end
fclose(file);
5. 驗證
輸出的函數在桌面的 outer.txt 文件中。
將函數輸入到 MATLAB 中畫圖像,結果如下。
驗證無誤。輸出那一堆就是能畫出可愛的 Pusheen 的函數啦 ~
其他
卡通小貓都畫出來了,打算也畫一個卡通妹子。在網上找了張艾米莉亞線稿
使用了用同樣的方法,結果如下。
一臉茫然,大概輪廓倒能看出來,可怎么會亂成這樣子[笑cry]
仔細想了想,原因可能出在閉合曲線分組那兒。卡通貓輪廓上的蜜汁線條也可能因為此。
對卡通貓的函數進行 Debug:
這些就差不多能看出原因了。imcontour 函數將鼻子、眼睛也認為是外輪廓的一部分,錯誤地將其分為一組數據。對于像 Pusheen 這種線條簡單的圖形來說,精度一般,分錯組數較少。對于艾米莉亞這種復雜的人像來說,精度遠遠不夠,錯誤就太多了。
回到從推上看到的圖片。人家是如何畫出那么完美的人像的呢?翻了翻博主的其他推文,發現人家利用游戲引擎 Hot Soup Processor 設計了一個畫圖板程序,在程序上手繪一組組封閉曲線,游戲引擎會用傅里葉級數將這組封閉曲線展開為函數。這個過程不需要用程序將離散序列分組,精度極高。
在沒找到更好的分組方法前,就只能畫畫輪廓簡單的圖片啦[笑cry]
一些其他的函數曲線:
Twitter Curve:
1:
參考資料
https://mathematica.stackexchange.com/questions/17704/how-to-create-a-new-person-curve
http://blog.wolfram.com/2013/05/17/making-formulas-for-everything-from-pi-to-the-pink-panther-to-sir-isaac-newton/
http://blog.wolframalpha.com/2013/06/10/using-formulas-for-everything-from-a-complex-analysis-class-to-political-cartoons-to-music-album-covers/
http://blog.wolfram.com/2013/08/15/even-more-formulas-for-everything-from-filled-algebraic-curves-to-the-twitter-bird-the-american-flag-chocolate-easter-bunnies-and-the-superman-solid/
http://www.isnowfy.com/generate-any-function-curve/
https://yvt.jp/contours/