在 Python 中繪制 Mandelbrot 集(在線翻譯器)
目錄

了解曼德布羅集
分形幾何的標(biāo)志
迭代穩(wěn)定性的邊界
朱莉婭集的地圖
使用 Python 的 Matplotlib 繪制 Mandelbrot 集
低分辨率散點(diǎn)圖
高分辨率黑白可視化
用枕頭畫曼德布羅套裝
尋找集合的收斂元素
用逃逸計(jì)數(shù)測(cè)量分歧
平滑條帶偽影
在集合元素和像素之間轉(zhuǎn)換
制作曼德布羅集的藝術(shù)表現(xiàn)形式
調(diào)色板
顏色漸變
顏色模型
結(jié)論
本教程將引導(dǎo)您完成一個(gè)有趣的項(xiàng)目,其中涉及Python 中的復(fù)數(shù)。您將通過使用 Python 的 Matplotlib 和 Pillow 庫(kù)繪制Mandelbrot 集來(lái)了解分形并創(chuàng)建一些真正令人驚嘆的藝術(shù)。在此過程中,您將了解這個(gè)著名的分形是如何被發(fā)現(xiàn)的、它代表什么以及它與其他分形的關(guān)系。
了解面向?qū)ο蟮木幊淘砗瓦f歸將使您能夠充分利用 Python 的表達(dá)性語(yǔ)法來(lái)編寫讀起來(lái)幾乎像數(shù)學(xué)公式一樣的干凈代碼。要了解制作分形的算法細(xì)節(jié),您還應(yīng)該熟悉復(fù)數(shù)、對(duì)數(shù)、集合論和迭代函數(shù)。但是不要讓這些先決條件嚇跑你,因?yàn)闊o(wú)論如何你都可以跟隨并制作藝術(shù)!
在本教程中,您將學(xué)習(xí)如何:
將復(fù)數(shù)應(yīng)用于實(shí)際問題
查找Mandelbrot和Julia集的成員
使用Matplotlib和Pillow將這些集合繪制為分形
對(duì)分形進(jìn)行豐富多彩的藝術(shù)表現(xiàn)
了解曼德布羅集
在您嘗試?yán)L制分形之前,這將有助于了解相應(yīng)的 Mandelbrot 集代表什么以及如何確定其成員。如果您已經(jīng)熟悉基本理論,請(qǐng)隨時(shí)跳到下面的繪圖部分。
分形幾何的標(biāo)志
即使這個(gè)名字對(duì)你來(lái)說是新的,你也可能以前看過一些令人著迷的 Mandelbrot 集的可視化。它是一組復(fù)數(shù),其邊界在復(fù)平面上描繪時(shí)形成獨(dú)特而復(fù)雜的圖案。這種模式可以說是最著名的分形,在 20 世紀(jì)后期催生了分形幾何:
Mandelbrot 集(來(lái)源:維基媒體,由 Wolfgang Beyer 創(chuàng)建,CC BY-SA 3.0)
由于技術(shù)進(jìn)步,Mandelbrot 集的發(fā)現(xiàn)成為可能。它歸功于一位名叫Beno?t Mandelbrot的數(shù)學(xué)家。他在 IBM 工作,可以使用一臺(tái)能夠進(jìn)行當(dāng)時(shí)要求很高的數(shù)字運(yùn)算的計(jì)算機(jī)。今天,您可以在舒適的家中探索分形,只使用 Python!
分形是在不同尺度上無(wú)限重復(fù)的圖案。幾個(gè)世紀(jì)以來(lái),哲學(xué)家們一直在爭(zhēng)論無(wú)窮大的存在,但分形確實(shí)在現(xiàn)實(shí)世界中有一個(gè)類比。這是自然界中相當(dāng)普遍的現(xiàn)象。例如,這種羅馬花椰菜是有限的,但具有自相似結(jié)構(gòu),因?yàn)槭卟说拿總€(gè)部分看起來(lái)都像整體,只是更小:
羅馬花椰菜的分形結(jié)構(gòu)
自相似性通常可以用遞歸在數(shù)學(xué)上定義。Mandelbrot 集并不是完全自相似的,因?yàn)樗谳^小尺度上略有不同的自身副本。盡管如此,它仍然可以用復(fù)域中的遞歸函數(shù)來(lái)描述。
迭代穩(wěn)定性的邊界
形式上,Mandelbrot 集是復(fù)數(shù)的集合c,其中無(wú)限的數(shù)字序列z?0?,?z?1?, ...,?z?n?, ... 保持有界。換句話說,該序列中每個(gè)復(fù)數(shù)的大小永遠(yuǎn)不會(huì)超過一個(gè)限制。Mandelbrot 序列由以下遞歸公式給出:
用簡(jiǎn)單的英語(yǔ)來(lái)說,要確定某個(gè)復(fù)數(shù)c是否屬于 Mandelbrot 集,您必須將該數(shù)字輸入到上面的公式中。從現(xiàn)在開始,數(shù)字c將在您迭代序列時(shí)保持不變。序列的第一個(gè)元素z?0始終為零。要計(jì)算下一個(gè)元素z?n+1,您將繼續(xù)對(duì)最后一個(gè)元素z?n進(jìn)行平方,并在反饋循環(huán)中添加初始數(shù)字c 。
通過觀察生成的數(shù)字序列的行為方式,您將能夠?qū)?fù)數(shù)c分類為 Mandelbrot 集成員或非 Mandelbrot 集成員。序列是無(wú)限的,但您必須在某個(gè)時(shí)刻停止計(jì)算它的元素。做出這樣的選擇有點(diǎn)武斷,取決于您接受的置信水平,因?yàn)楦嗟脑貙?duì)c提供更準(zhǔn)確的裁決。
注意:當(dāng)在復(fù)平面上描繪時(shí),整個(gè) Mandelbrot 集適合半徑為 2 的圓。這是一個(gè)方便的事實(shí),可以讓您跳過許多不必要的計(jì)算,這些計(jì)算肯定不屬于該集合。
對(duì)于復(fù)數(shù),您可以在二維中直觀地想象這個(gè)迭代過程,但現(xiàn)在為了簡(jiǎn)單起見,您可以繼續(xù)只考慮實(shí)數(shù)。如果你要在 Python 中實(shí)現(xiàn)上面的等式,那么它可能看起來(lái)像這樣:
>>>
>>> def z(n, c): ... if n == 0: ... return 0 ... else: ... return z(n - 1, c) ** 2 + c
您的z()函數(shù)返回序列的第 n 個(gè)元素,這就是為什么它需要一個(gè)元素的索引n, 作為第一個(gè)參數(shù)。第二個(gè)參數(shù)c是您正在測(cè)試的固定數(shù)字。由于遞歸,這個(gè)函數(shù)會(huì)無(wú)限地調(diào)用自己。然而,為了打破這個(gè)遞歸調(diào)用鏈,一個(gè)條件會(huì)使用一個(gè)立即已知的解決方案(零)檢查基本情況。
嘗試使用新函數(shù)查找c?= 1 序列的前十個(gè)元素,看看會(huì)發(fā)生什么:
>>>
>>> for n in range(10): ... print(f"z({n}) = {z(n, c=1)}") ... z(0) = 0 z(1) = 1 z(2) = 2 z(3) = 5 z(4) = 26 z(5) = 677 z(6) = 458330 z(7) = 210066388901 z(8) = 44127887745906175987802 z(9) = 1947270476915296449559703445493848930452791205
注意這些序列元素的快速增長(zhǎng)。它告訴你一些關(guān)于c?= 1 的成員的信息。具體來(lái)說,它不屬于Mandelbrot 集,因?yàn)橄鄳?yīng)的序列無(wú)限增長(zhǎng)。
有時(shí),迭代方法可能比遞歸方法更有效。這是為指定輸入值創(chuàng)建無(wú)限序列的等效函數(shù)c:
>>>
>>> def sequence(c): ... z = 0 ... while True: ... yield z ... z = z ** 2 + c
該sequence()函數(shù)返回一個(gè)生成器對(duì)象,在循環(huán)中無(wú)限地產(chǎn)生序列的連續(xù)元素。因?yàn)樗环祷叵鄳?yīng)的元素索引,所以您可以枚舉它們并在給定次數(shù)的迭代后停止循環(huán):
>>>
>>> for n, z in enumerate(sequence(c=1)): ... print(f"z({n}) = {z}") ... if n >= 9: ... break ... z(0) = 0 z(1) = 1 z(2) = 2 z(3) = 5 z(4) = 26 z(5) = 677 z(6) = 458330 z(7) = 210066388901 z(8) = 44127887745906175987802 z(9) = 1947270476915296449559703445493848930452791205
結(jié)果和以前一樣,但是生成器函數(shù)可以讓您通過使用惰性求值更有效地計(jì)算序列元素。除此之外,迭代消除了對(duì)已經(jīng)計(jì)算的序列元素的冗余函數(shù)調(diào)用。因此,您不再冒達(dá)到最大遞歸限制的風(fēng)險(xiǎn)。
大多數(shù)數(shù)字會(huì)使這個(gè)序列發(fā)散到無(wú)窮大。但是,有些人會(huì)通過將序列收斂到單個(gè)值或保持在有界范圍內(nèi)來(lái)保持穩(wěn)定。其他人將通過在相同的幾個(gè)值之間來(lái)回循環(huán)來(lái)使序列周期性地穩(wěn)定。穩(wěn)定值和周期性穩(wěn)定值構(gòu)成了 Mandelbrot 集。
例如,插入c?= 1 會(huì)使序列像您剛剛學(xué)到的那樣無(wú)限制地增長(zhǎng),但是c?= -1 會(huì)導(dǎo)致它在 0 和 -1 之間反復(fù)跳躍,而c?= 0 會(huì)給出由單個(gè)值組成的序列:
哪些數(shù)字是穩(wěn)定的,哪些不是,這并不明顯,因?yàn)樵摴郊词箤?duì)測(cè)試值c的最小變化也很敏感。如果您在復(fù)平面上標(biāo)記穩(wěn)定數(shù),您將看到以下模式出現(xiàn):
復(fù)平面上 Mandelbrot 集的描述
該圖像是通過對(duì)每個(gè)像素運(yùn)行多達(dá) 20 次遞歸公式生成的,每個(gè)像素代表某個(gè)c值。當(dāng)在所有迭代后得到的復(fù)數(shù)的大小仍然相當(dāng)小,則相應(yīng)的像素被著色為黑色。但是,一旦幅度超過半徑二,迭代就會(huì)停止并跳過當(dāng)前像素。
有趣的事實(shí):對(duì)應(yīng)于 Mandelbrot 集的分形具有估計(jì)為 1.506484 平方單位的有限區(qū)域。數(shù)學(xué)家還沒有確定確切的數(shù)字,也不知道它是否合理。另一方面,Mandelbrot 集的周長(zhǎng)是無(wú)限的。查看海岸線悖論,了解現(xiàn)實(shí)生活中這個(gè)奇怪事實(shí)的有趣平行。
您可能會(huì)驚訝地發(fā)現(xiàn),一個(gè)相對(duì)簡(jiǎn)單的僅涉及加法和乘法的公式可以產(chǎn)生如此精細(xì)的結(jié)構(gòu)。但這還不是全部。事實(shí)證明,您可以采用相同的公式并使用它來(lái)生成無(wú)限多個(gè)獨(dú)特的分形!你想看看怎么做嗎?
朱莉婭集的地圖
如果不提及Julia 集,就很難談?wù)?Mandelbrot集,這是幾十年前法國(guó)數(shù)學(xué)家Gaston Julia在沒有計(jì)算機(jī)幫助的情況下發(fā)現(xiàn)的。Julia 集和 Mandelbrot 集密切相關(guān),因?yàn)槟梢酝ㄟ^相同的遞歸公式獲得它們,只是具有不同的起始條件集。
雖然只有一個(gè) Mandelbrot 集,但有無(wú)限多個(gè) Julia 集。到目前為止,您總是從z?0?= 0 開始序列,并系統(tǒng)地測(cè)試了某個(gè)任意復(fù)數(shù)c的成員資格。另一方面,要確定一個(gè)數(shù)字是否屬于 Julia 集,您必須使用該數(shù)字作為序列的起點(diǎn),并為c參數(shù)選擇另一個(gè)值。
以下是公式術(shù)語(yǔ)的快速比較,具體取決于您正在調(diào)查的集合:
在第一種情況下,c代表 Mandelbrot 集的一個(gè)潛在成員,并且是唯一需要的輸入值,因?yàn)閦?0保持固定為零。但是,當(dāng)您在 Julia 模式下使用公式時(shí),每個(gè)術(shù)語(yǔ)的含義都會(huì)發(fā)生變化。現(xiàn)在,c用作確定整個(gè) Julia 集的形狀和形式的參數(shù),而z?0成為您的興趣點(diǎn)。與以前不同,Julia 集的公式需要兩個(gè)輸入值而不是一個(gè)。
您可以修改之前定義的函數(shù)之一,使其更通用。這樣,您可以創(chuàng)建從任意點(diǎn)開始的無(wú)限序列,而不是始終為零:
def sequence(c, z=0): while True: yield z z = z ** 2 + c
由于突出顯示的行中的默認(rèn)參數(shù)值,您仍然可以像以前一樣使用此函數(shù),因?yàn)閦它是可選的。同時(shí),您可以更改序列的起點(diǎn)。在為 Mandelbrot 和 Julia 集定義包裝函數(shù)之后,您可能會(huì)得到一個(gè)更好的主意:
def mandelbrot(candidate): return sequence(z=0, c=candidate) def julia(candidate, parameter): return sequence(z=candidate, c=parameter)
每個(gè)函數(shù)都會(huì)返回一個(gè)生成器對(duì)象,該對(duì)象已微調(diào)到您想要的起始條件。判斷一個(gè)候選值是否屬于 Julia 集的原理與您之前看到的 Mandelbrot 集相似。簡(jiǎn)而言之,您必須迭代序列并隨著時(shí)間的推移觀察其行為。
事實(shí)上,Beno?t Mandelbrot 在他的科學(xué)研究中研究 Julia 集。他特別感興趣的是找到那些產(chǎn)生所謂的連接Julia 集的c值,而不是它們的不連接對(duì)應(yīng)物。后者被稱為法頭集,當(dāng)在復(fù)平面上可視化時(shí),它表現(xiàn)為由無(wú)數(shù)個(gè)碎片組成的塵埃:
Connected Julia Set vs Fatou Dust
左上角的圖像表示從c?= 0.25 派生的連接 Julia 集,屬于 Mandelbrot 集。您知道將 Mandelbrot 集的一個(gè)成員插入遞歸公式將產(chǎn)生一系列收斂的復(fù)數(shù)。在這種情況下,數(shù)字收斂到 0.5。但是,對(duì)c的輕微更改可能會(huì)突然將您的 Julia 集變成斷開的塵埃,并使相應(yīng)的序列發(fā)散到無(wú)窮大。
巧合的是,連接的 Julia 集對(duì)應(yīng)于生成上述遞歸公式的穩(wěn)定序列的c值。因此,您可能會(huì)說 Beno?t Mandelbrot 正在尋找迭代穩(wěn)定性的邊界,或者所有 Julia 集的地圖,以顯示這些集在哪里連接以及它們?cè)谀睦锸菈m埃。
觀察在復(fù)平面上為c參數(shù)選擇不同的點(diǎn)如何影響生成的 Julia 集:
紅色的小圓圈表示c的值。只要它停留在左側(cè)所示的 Mandelbrot 集中,右側(cè)描繪的相應(yīng) Julia 集就會(huì)保持連接。否則,Julia 集會(huì)像泡沫一樣爆裂,擴(kuò)散成無(wú)數(shù)個(gè)塵土飛揚(yáng)的碎片。
你注意到 Julia 集是如何改變形狀的嗎?事實(shí)證明,特定的 Julia 集與用于播種c值的 Mandelbrot 集的特定區(qū)域具有共同的視覺特征。當(dāng)您通過放大鏡觀察時(shí),兩個(gè)分形看起來(lái)會(huì)有些相似。
好了,理論說完了。是時(shí)候繪制你的第一個(gè) Mandelbrot 集了!
使用 Python 的 Matplotlib 繪制 Mandelbrot 集
有很多方法可以在 Python 中可視化 Mandelbrot 集。如果您對(duì)NumPy和Matplotlib感到滿意,那么這兩個(gè)庫(kù)將共同提供繪制分形的最直接方法之一。它們方便地使您不必在世界坐標(biāo)和像素坐標(biāo)之間進(jìn)行轉(zhuǎn)換。
注意:世界坐標(biāo)對(duì)應(yīng)于復(fù)平面上的連續(xù)數(shù)字譜,延伸到無(wú)窮大。另一方面,像素坐標(biāo)是離散的,并且受屏幕有限大小的限制。
要生成初始候選值集,您可以利用np.linspace(),它在給定范圍內(nèi)創(chuàng)建均勻間隔的數(shù)字:
1import numpy as np 2 3def complex_matrix(xmin, xmax, ymin, ymax, pixel_density): 4 re = np.linspace(xmin, xmax, int((xmax - xmin) * pixel_density)) 5 im = np.linspace(ymin, ymax, int((ymax - ymin) * pixel_density)) 6 return re[np.newaxis, :] + im[:, np.newaxis] * 1j
上面的函數(shù)將返回一個(gè)二維復(fù)數(shù)數(shù)組,該數(shù)組包含在由四個(gè)參數(shù)給定的矩形區(qū)域中。xmin和xmax參數(shù)指定水平方向的邊界,而和ymin指定ymax垂直方向的邊界。第五個(gè)參數(shù)pixel_density確定每單位所需的像素?cái)?shù)。
現(xiàn)在,您可以獲取該復(fù)數(shù)矩陣并通過眾所周知的遞歸公式運(yùn)行它,以查看哪些數(shù)字保持穩(wěn)定,哪些數(shù)字保持穩(wěn)定。由于 NumPy 的矢量化,您可以將矩陣作為單個(gè)參數(shù)傳遞c,并在每個(gè)元素上執(zhí)行計(jì)算,而無(wú)需編寫顯式循環(huán):
8def is_stable(c, num_iterations): 9 z = 0 10 for _ in range(num_iterations): 11 z = z ** 2 + c 12 return abs(z) <= 2
突出顯示行上的代碼在每次迭代中針對(duì)矩陣的所有元素執(zhí)行。c因?yàn)樽畛鯊牟煌瑉的c維度開始,NumPy 使用廣播巧妙地?cái)U(kuò)展前者,使兩者最終具有兼容的形狀。最后,該函數(shù)在結(jié)果矩陣 上創(chuàng)建布爾值的二維掩碼z。每個(gè)值對(duì)應(yīng)于該點(diǎn)的序列穩(wěn)定性。
注意:為了利用向量化計(jì)算,代碼示例中的循環(huán)會(huì)無(wú)條件地對(duì)數(shù)字進(jìn)行平方和相加,無(wú)論它們已經(jīng)有多大。這并不理想,因?yàn)樵谠S多情況下,數(shù)字很早就發(fā)散到無(wú)窮大,使得大部分計(jì)算都是浪費(fèi)的。
此外,快速增長(zhǎng)的數(shù)字通常會(huì)導(dǎo)致溢出錯(cuò)誤。NumPy 檢測(cè)到此類溢出并在標(biāo)準(zhǔn)錯(cuò)誤流 (stderr)上發(fā)出警告。如果您想禁止此類警告,則可以在調(diào)用函數(shù)之前定義相關(guān)過濾器:
1import numpy as np 2 3np.warnings.filterwarnings("ignore")
忽略這些溢出是無(wú)害的,因?yàn)槟鷮?duì)特定幅度不感興趣,而是對(duì)它們是否符合給定閾值感興趣。
在選定的迭代次數(shù)之后,矩陣中每個(gè)復(fù)數(shù)的大小將保持在或超過閾值 2。那些足夠小的可能是 Mandelbrot 集的成員。您現(xiàn)在可以使用 Matplotlib 將它們可視化。
低分辨率散點(diǎn)圖
可視化 Mandelbrot 集的一種快速而骯臟的方法是通過散點(diǎn)圖,它說明了成對(duì)變量之間的關(guān)系。因?yàn)閺?fù)數(shù)是實(shí)部和虛部的對(duì),所以您可以將它們解開成單獨(dú)的數(shù)組,這些數(shù)組可以很好地與散點(diǎn)圖配合使用。
但首先,您需要將布爾穩(wěn)定性掩碼轉(zhuǎn)換為作為序列種子的初始復(fù)數(shù)。你可以在 NumPy 的掩碼過濾的幫助下做到這一點(diǎn):
16def get_members(c, num_iterations): 17 mask = is_stable(c, num_iterations) 18 return c[mask]
此函數(shù)將返回一個(gè)一維數(shù)組,該數(shù)組僅包含那些穩(wěn)定且因此屬于 Mandelbrot 集的復(fù)數(shù)。當(dāng)您組合到目前為止定義的函數(shù)時(shí),您將能夠使用 Matplotlib 顯示散點(diǎn)圖。不要忘記在文件開頭添加必要的導(dǎo)入語(yǔ)句:
1import matplotlib.pyplot as plt 2import numpy as np 3 4np.warnings.filterwarnings("ignore")
這會(huì)將繪圖界面帶到您當(dāng)前的命名空間。現(xiàn)在您可以計(jì)算數(shù)據(jù)并繪制它:
21c = complex_matrix(-2, 0.5, -1.5, 1.5, pixel_density=21) 22members = get_members(c, num_iterations=20) 23 24plt.scatter(members.real, members.imag, color="black", marker=",", s=1) 25plt.gca().set_aspect("equal") 26plt.axis("off") 27plt.tight_layout() 28plt.show()
調(diào)用complex_matrix()準(zhǔn)備一個(gè)矩形復(fù)數(shù)數(shù)組,在 x 方向的范圍為 -2 到 0.5,在 y 方向的范圍為 -1.5 到 1.5。隨后的調(diào)用get_members()僅通過那些屬于 Mandelbrot 集的數(shù)字。最后,plt.scatter()繪制集合,并plt.show()顯示這張圖片:
Mandelbrot 集在散點(diǎn)圖中的可視化
它包含 749 個(gè)點(diǎn),類似于幾十年前 Beno?t Mandelbrot 本人在點(diǎn)陣打印機(jī)上制作的原始 ASCII 打印輸出。你正在重溫?cái)?shù)學(xué)史!通過調(diào)整像素密度和迭代次數(shù)來(lái)觀察它們?nèi)绾斡绊懡Y(jié)果。
高分辨率黑白可視化
要獲得更詳細(xì)的黑白 Mandelbrot 集可視化效果,您可以不斷增加散點(diǎn)圖的像素密度,直到各個(gè)點(diǎn)變得難以辨別。plt.imshow()或者,您可以使用帶有二進(jìn)制顏色圖的Matplotlib函數(shù)來(lái)繪制您的布爾穩(wěn)定性掩碼。
您現(xiàn)有的代碼中只需要進(jìn)行一些調(diào)整:
21c = complex_matrix(-2, 0.5, -1.5, 1.5, pixel_density=512) 22plt.imshow(is_stable(c, num_iterations=20), cmap="binary") 23plt.gca().set_aspect("equal") 24plt.axis("off") 25plt.tight_layout() 26plt.show()
將像素密度提高到足夠大的值,例如 512。然后,刪除對(duì) 的調(diào)用get_members(),并將散點(diǎn)圖替換為plt.imshow()以將數(shù)據(jù)顯示為圖像。如果一切順利,那么您應(yīng)該會(huì)看到這張 Mandelbrot 集的圖片:
黑白 Mandelbrot 集的高分辨率可視化
要放大分形的特定區(qū)域,請(qǐng)相應(yīng)地更改復(fù)矩陣的邊界并將迭代次數(shù)增加十倍或更多。您還可以嘗試使用 Matplotlib 提供的不同顏色圖。然而,要真正釋放你內(nèi)心的藝術(shù)家,你可能想用Python 最受歡迎的圖像庫(kù)Pillow讓你的腳濕透。
用枕頭畫曼德布羅套裝
本節(jié)需要更多的努力,因?yàn)槟鷮⑼瓿?NumPy 和 Matplotlib 之前為您完成的一些工作。但是,對(duì)可視化過程進(jìn)行更精細(xì)的控制將使您能夠以更有趣的方式描繪 Mandelbrot 集。在此過程中,您將學(xué)習(xí)一些有用的概念并遵循最佳Pythonic實(shí)踐。
NumPy 與 Pillow 一起使用就像與 Matplotlib 一樣。np.asarray()您可以使用或以其他方式將 Pillow 圖像轉(zhuǎn)換為 NumPy 數(shù)組Image.fromarray()。由于這種兼容性,您可以通過將 Matplotlib 替換plt.imshow()為對(duì) Pillow 工廠方法的非常相似的調(diào)用來(lái)更新上一節(jié)中的繪圖代碼:
21c = complex_matrix(-2, 0.5, -1.5, 1.5, pixel_density=512) 22image = Image.fromarray(~is_stable(c, num_iterations=20)) 23image.show()
請(qǐng)注意在穩(wěn)定性矩陣前面使用按位非運(yùn)算符( ),它會(huì)反轉(zhuǎn)所有布爾值。~這是為了使 Mandelbrot 集在白色背景上顯示為黑色,因?yàn)?Pillow 默認(rèn)采用黑色背景。
如果 NumPy 是您熟悉的首選工具,請(qǐng)隨時(shí)將其整合到本節(jié)中。它的執(zhí)行速度將比您即將看到的純 Python 代碼快得多,因?yàn)?NumPy 經(jīng)過高度優(yōu)化并依賴于已編譯的機(jī)器代碼。盡管如此,從頭開始實(shí)現(xiàn)繪圖代碼將使您能夠最終控制并深入了解所涉及的各個(gè)步驟。
有趣的事實(shí):Pillow 圖像庫(kù)帶有一個(gè)方便的函數(shù),可以在一行 Python 代碼中生成 Mandelbrot 集的圖像:
from PIL import Image Image.effect_mandelbrot((512, 512), (-3, -2.5, 2, 2.5), 100).show()
傳遞給函數(shù)的第一個(gè)參數(shù)是一個(gè)元組,其中包含生成圖像的寬度和高度(以像素為單位)。下一個(gè)參數(shù)將邊界框定義為左下角和右上角。第三個(gè)參數(shù)是從 0 到 100 的圖像質(zhì)量。
如果您對(duì)該函數(shù)的工作原理感興趣,請(qǐng)查看 GitHub 上相應(yīng)的C 源代碼。
在本節(jié)的其余部分中,您將自己完成艱苦的工作,而不會(huì)走捷徑。
尋找集合的收斂元素
之前,您使用 NumPy 的向量化構(gòu)建了一個(gè)穩(wěn)定性矩陣,以確定給定復(fù)數(shù)中的哪些屬于 Mandelbrot 集。您的函數(shù)對(duì)公式進(jìn)行了固定次數(shù)的迭代,并返回了一個(gè)布爾值的二維數(shù)組。使用純 Python,您可以修改此函數(shù),使其適用于單個(gè)數(shù)字而不是整個(gè)矩陣:
>>>
>>> def is_stable(c, max_iterations): ... z = 0 ... for _ in range(max_iterations): ... z = z ** 2 + c ... if abs(z) > 2: ... return False ... return True
它看起來(lái)與之前的 NumPy 版本非常相似。但是,有一些重要的區(qū)別。值得注意的是,c參數(shù)表示單個(gè)復(fù)數(shù),函數(shù)返回標(biāo)量布爾值。其次,循環(huán)體中有一個(gè) if 條件,一旦結(jié)果數(shù)的大小達(dá)到已知閾值,它就可以提前終止迭代。
注意:函數(shù)參數(shù)之一已從 重命名num_iterations為max_iterations以反映迭代次數(shù)不再固定并且可以在測(cè)試值之間變化的事實(shí)。
您可以使用新函數(shù)來(lái)確定復(fù)數(shù)是否創(chuàng)建收斂的穩(wěn)定序列。這直接轉(zhuǎn)化為 Mandelbrot 集合成員。但是,結(jié)果可能取決于請(qǐng)求的最大迭代次數(shù):
>>>
>>> is_stable(0.26, max_iterations=20) True >>> is_stable(0.26, max_iterations=30) False
例如,數(shù)字c?= 0.26 位于分形邊緣附近,因此如果迭代次數(shù)太少,您將得到錯(cuò)誤的答案。增加最大迭代次數(shù)可以更準(zhǔn)確,顯示可視化的更多細(xì)節(jié)。
調(diào)用函數(shù)并沒有錯(cuò),但使用 Pythonin和not in運(yùn)算符不是更好嗎?將此代碼轉(zhuǎn)換為Python 類將允許您覆蓋這些運(yùn)算符并利用更清晰和更 Pythonic 的語(yǔ)法。此外,通過將狀態(tài)封裝在對(duì)象中,您將能夠在許多函數(shù)調(diào)用中保留最大迭代次數(shù)。
您可以利用數(shù)據(jù)類來(lái)避免必須定義自定義構(gòu)造函數(shù)。這是相同代碼的基于類的等效實(shí)現(xiàn):
# mandelbrot.py from dataclasses import dataclass @dataclass class MandelbrotSet: max_iterations: int def __contains__(self, c: complex) -> bool: z = 0 for _ in range(self.max_iterations): z = z ** 2 + c if abs(z) > 2: return False return True
除了實(shí)現(xiàn)特殊方法?.__contains__()、添加一些類型提示以及將max_iterations參數(shù)從函數(shù)簽名中移出之外,其余代碼保持不變。假設(shè)您將它保存在一個(gè)名為 的文件中mandelbrot.py,您可以在同一目錄中啟動(dòng)交互式 Python 解釋器會(huì)話并導(dǎo)入您的類:
>>>
>>> from mandelbrot import MandelbrotSet >>> mandelbrot_set = MandelbrotSet(max_iterations=30) >>> 0.26 in mandelbrot_set False >>> 0.26 not in mandelbrot_set True
杰出的!這正是您對(duì) Mandelbrot 集進(jìn)行黑白可視化所需的信息。稍后,您將了解使用 Pillow 繪制像素的一種不那么冗長(zhǎng)的方法,但這里是初學(xué)者的粗略示例:
>>>
>>> from mandelbrot import MandelbrotSet >>> mandelbrot_set = MandelbrotSet(max_iterations=20) >>> width, height = 512, 512 >>> scale = 0.0075 >>> BLACK_AND_WHITE = "1" >>> from PIL import Image >>> image = Image.new(mode=BLACK_AND_WHITE, size=(width, height)) >>> for y in range(height): ... for x in range(width): ... c = scale * complex(x - width / 2, height / 2 - y) ... image.putpixel((x, y), c not in mandelbrot_set) ... >>> image.show()
從庫(kù)中導(dǎo)入Image模塊后,您將創(chuàng)建一個(gè)具有黑白像素模式和 512 x 512 像素大小的新枕頭圖像。然后,當(dāng)您迭代像素行和列時(shí),您將每個(gè)點(diǎn)從像素坐標(biāo)縮放并轉(zhuǎn)換為世界坐標(biāo)。最后,當(dāng)對(duì)應(yīng)的復(fù)數(shù)不屬于 Mandelbrot 集時(shí),打開一個(gè)像素,使分形的內(nèi)部保持黑色。
注意:在此特定像素模式下,該.putpixel()方法需要數(shù)值為 1 或 0。但是,一旦布爾表達(dá)式被評(píng)估為上面的代碼片段True或False在上面的代碼片段中,Python 將分別將其替換為 1 或 0。它按預(yù)期工作,因?yàn)?Python 的bool數(shù)據(jù)類型實(shí)際上是整數(shù)的子類。
當(dāng)您執(zhí)行此代碼時(shí),您會(huì)立即注意到它的運(yùn)行速度比前面的示例慢得多。如前所述,這部分是因?yàn)?NumPy 和 Matplotlib 為高度優(yōu)化的 C 代碼提供了 Python 綁定,而您剛剛在純 Python 中實(shí)現(xiàn)了最關(guān)鍵的部分。除此之外,您還有嵌套循環(huán),并且您正在調(diào)用一個(gè)函數(shù)數(shù)十萬(wàn)次!
無(wú)論如何,不 要擔(dān)心性能。你的目標(biāo)是學(xué)習(xí)在 Python 中繪制 Mandelbrot 集的基礎(chǔ)知識(shí)。接下來(lái),您將通過在其中顯示更多信息來(lái)改進(jìn)分形可視化。
用逃逸計(jì)數(shù)測(cè)量分歧
好的,您知道如何判斷一個(gè)復(fù)數(shù)是否使 Mandelbrot 序列收斂,這反過來(lái)又可以讓您將 Mandelbrot 集可視化為黑色和白色。您可以通過從二值圖像到每個(gè)像素超過兩個(gè)強(qiáng)度級(jí)別的灰度來(lái)增加一點(diǎn)深度嗎?答案是肯定的!
嘗試從不同的角度看待問題。您可以量化位于 Mandelbrot 集之外的點(diǎn)使遞歸公式發(fā)散到無(wú)窮大的速度,而不是找到分形的銳邊。有些會(huì)很快變得不穩(wěn)定,而另一些可能需要數(shù)十萬(wàn)次迭代才能完成。一般來(lái)說,靠近分形邊緣的點(diǎn)比遠(yuǎn)離分形的點(diǎn)不穩(wěn)定。
通過足夠多的迭代,不間斷地遍歷所有迭代將表明測(cè)試的數(shù)字很可能是集合成員,因?yàn)橄嚓P(guān)的序列元素保持穩(wěn)定。另一方面,只有當(dāng)序列明顯發(fā)散時(shí),才會(huì)在達(dá)到最大迭代次數(shù)之前跳出循環(huán)。檢測(cè)分歧所需的迭代次數(shù)稱為逃逸計(jì)數(shù)。
注意:使用迭代方法測(cè)試給定點(diǎn)的穩(wěn)定性只是實(shí)際 Mandelbrot 集的近似值。對(duì)于某些點(diǎn),您需要比最大迭代次數(shù)更多的迭代才能知道它們是否穩(wěn)定,這在實(shí)踐中可能不可行。
您可以使用轉(zhuǎn)義計(jì)數(shù)來(lái)引入多級(jí)灰度。但是,處理歸一化轉(zhuǎn)義計(jì)數(shù)通常更方便,這樣無(wú)論最大迭代次數(shù)如何,它們的值都在從零到一的范圍內(nèi)。要計(jì)算給定點(diǎn)的這種穩(wěn)定性指標(biāo),請(qǐng)使用做出決定所需的實(shí)際迭代次數(shù)與您無(wú)條件退出的最大次數(shù)之間的比率。
讓我們修改您的MandelbrotSet類以計(jì)算轉(zhuǎn)義計(jì)數(shù)。首先,相應(yīng)地重命名您的特殊方法并使其返回迭代次數(shù)而不是布爾值:
# mandelbrot.py from dataclasses import dataclass @dataclass class MandelbrotSet: max_iterations: int def escape_count(self, c: complex) -> int: z = 0 for iteration in range(self.max_iterations): z = z ** 2 + c if abs(z) > 2: return iteration return self.max_iterations
注意循環(huán)如何聲明一個(gè)變量iteration, 來(lái)計(jì)算迭代次數(shù)。接下來(lái),將穩(wěn)定性定義為逃逸計(jì)數(shù)與最大迭代次數(shù)的比率:
# mandelbrot.py from dataclasses import dataclass @dataclass class MandelbrotSet: max_iterations: int def stability(self, c: complex) -> float: return self.escape_count(c) / self.max_iterations def escape_count(self, c: complex) -> int: z = 0 for iteration in range(self.max_iterations): z = z ** 2 + c if abs(z) > 2: return iteration return self.max_iterations
最后,帶回已刪除的特殊方法,成員資格測(cè)試操作員in委托給該方法。但是,您將使用建立在穩(wěn)定性之上的稍微不同的實(shí)現(xiàn):
# mandelbrot.py from dataclasses import dataclass @dataclass class MandelbrotSet: max_iterations: int def __contains__(self, c: complex) -> bool: return self.stability(c) == 1 def stability(self, c: complex) -> float: return self.escape_count(c) / self.max_iterations def escape_count(self, c: complex) -> int: z = 0 for iteration in range(self.max_iterations): z = z ** 2 + c if abs(z) > 2: return iteration return self.max_iterations
True只有當(dāng)穩(wěn)定性等于 1 或 100% 時(shí),成員資格測(cè)試算子才會(huì)返回。否則,您將獲得False價(jià)值。您可以通過以下方式檢查具體的逃逸計(jì)數(shù)和穩(wěn)定性值:
>>>
>>> from mandelbrot import MandelbrotSet >>> mandelbrot_set = MandelbrotSet(max_iterations=30) >>> mandelbrot_set.escape_count(0.25) 30 >>> mandelbrot_set.stability(0.25) 1.0 >>> 0.25 in mandelbrot_set True >>> mandelbrot_set.escape_count(0.26) 29 >>> mandelbrot_set.stability(0.26) 0.9666666666666667 >>> 0.26 in mandelbrot_set False
對(duì)于c?= 0.25,觀察到的迭代次數(shù)與聲明的最大迭代次數(shù)相同,使穩(wěn)定性等于 1。相反,選擇c?= 0.26 會(huì)產(chǎn)生略微不同的結(jié)果。
該類的更新實(shí)現(xiàn)MandelbrotSet允許灰度可視化,它將像素強(qiáng)度與穩(wěn)定性聯(lián)系起來(lái)。您可以重用上一節(jié)中的大部分繪圖代碼,但您需要將像素模式更改為L(zhǎng),它代表亮度。在這種模式下,每個(gè)像素取一個(gè) 0 到 255 之間的整數(shù)值,因此您還需要適當(dāng)?shù)乜s放分?jǐn)?shù)穩(wěn)定性:
>>>
>>> from mandelbrot import MandelbrotSet >>> mandelbrot_set = MandelbrotSet(max_iterations=20) >>> width, height = 512, 512 >>> scale = 0.0075 >>> GRAYSCALE = "L" >>> from PIL import Image >>> image = Image.new(mode=GRAYSCALE, size=(width, height)) >>> for y in range(height): ... for x in range(width): ... c = scale * complex(x - width / 2, height / 2 - y) ... instability = 1 - mandelbrot_set.stability(c) ... image.putpixel((x, y), int(instability * 255)) ... >>> image.show()
同樣,要在僅照亮其外部的同時(shí)將 Mandelbrot 設(shè)置為黑色,您必須通過從其中減去它來(lái)反轉(zhuǎn)穩(wěn)定性。當(dāng)一切按計(jì)劃進(jìn)行時(shí),您將看到以下圖像的粗略描述:
Pillow 對(duì) Mandelbrot 集的灰度渲染
哇!這看起來(lái)已經(jīng)更有趣了。不幸的是,穩(wěn)定性值在明顯離散的水平上出現(xiàn)量化,因?yàn)檗D(zhuǎn)義計(jì)數(shù)是整數(shù)。增加最大迭代次數(shù)可以幫助緩解這種情況,但僅限于一定程度。此外,添加更多的迭代將過濾掉大量的噪音,在這個(gè)放大倍數(shù)上留下更少的內(nèi)容。
在下一小節(jié)中,您將了解消除條帶偽影的更好方法。
平滑條帶偽影
擺脫 Mandelbrot 套裝外部的色帶歸結(jié)為使用分?jǐn)?shù)轉(zhuǎn)義計(jì)數(shù)。插入中間值的一種方法是使用對(duì)數(shù)。底層數(shù)學(xué)相當(dāng)復(fù)雜,所以讓我們用數(shù)學(xué)家的話來(lái)更新代碼:
# mandelbrot.py from dataclasses import dataclass from math import log @dataclass class MandelbrotSet: max_iterations: int escape_radius: float = 2.0 def __contains__(self, c: complex) -> bool: return self.stability(c) == 1 def stability(self, c: complex, smooth=False) -> float: return self.escape_count(c, smooth) / self.max_iterations def escape_count(self, c: complex, smooth=False) -> int | float: z = 0 for iteration in range(self.max_iterations): z = z ** 2 + c if abs(z) > self.escape_radius: if smooth: return iteration + 1 - log(log(abs(z))) / log(2) return iteration return self.max_iterations
從模塊導(dǎo)入log()函數(shù)后math,您添加一個(gè)可選的布爾標(biāo)志來(lái)控制您的方法的平滑。當(dāng)您打開平滑時(shí),數(shù)學(xué)魔法就會(huì)通過將迭代次數(shù)與發(fā)散數(shù)的空間信息結(jié)合起來(lái),從而產(chǎn)生一個(gè)浮點(diǎn)數(shù)。
注意:上面的數(shù)學(xué)公式是基于逃逸半徑接近無(wú)窮大的假設(shè),所以不能再硬編碼了。這就是為什么您的類現(xiàn)在定義了一個(gè)默認(rèn)值為 2 的可選escape_radius字段,您可以在創(chuàng)建MandelbrotSet.
請(qǐng)注意,由于平滑逃逸計(jì)數(shù)公式中的對(duì)數(shù),相關(guān)的穩(wěn)定性可能會(huì)過沖甚至變?yōu)樨?fù)數(shù)!這是一個(gè)簡(jiǎn)單的示例,可以證明這一點(diǎn):
>>>
>>> from mandelbrot import MandelbrotSet >>> mandelbrot_set = MandelbrotSet(max_iterations=30) >>> mandelbrot_set.stability(-1.2039 - 0.1996j, smooth=True) 1.014794475165942 >>> mandelbrot_set.stability(42, smooth=True) -0.030071301713066417
大于一且小于零的數(shù)字將導(dǎo)致像素強(qiáng)度環(huán)繞允許的最大和最小級(jí)別。所以,不要忘記在點(diǎn)亮一個(gè)像素之前和之前鉗制你的縮放像素值:max()min()
# mandelbrot.py from dataclasses import dataclass from math import log @dataclass class MandelbrotSet: max_iterations: int escape_radius: float = 2.0 def __contains__(self, c: complex) -> bool: return self.stability(c) == 1 def stability(self, c: complex, smooth=False, clamp=True) -> float: value = self.escape_count(c, smooth) / self.max_iterations return max(0.0, min(value, 1.0)) if clamp else value def escape_count(self, c: complex, smooth=False) -> int | float: z = 0 for iteration in range(self.max_iterations): z = z ** 2 + c if abs(z) > self.escape_radius: if smooth: return iteration + 1 - log(log(abs(z))) / log(2) return iteration return self.max_iterations
在計(jì)算穩(wěn)定性時(shí),您默認(rèn)啟用鉗位,但讓最終用戶決定如何處理上溢和下溢。一些色彩豐富的可視化可能會(huì)利用這種包裝來(lái)產(chǎn)生有趣的效果。這由上述方法clamp中的標(biāo)志控制.stability()。您將在后面的部分中使用可視化。
以下是如何將更新后的MandelbrotSet課程付諸實(shí)踐:
>>>
>>> from mandelbrot import MandelbrotSet >>> mandelbrot_set = MandelbrotSet(max_iterations=20, escape_radius=1000) >>> width, height = 512, 512 >>> scale = 0.0075 >>> GRAYSCALE = "L" >>> from PIL import Image >>> image = Image.new(mode=GRAYSCALE, size=(width, height)) >>> for y in range(height): ... for x in range(width): ... c = scale * complex(x - width / 2, height / 2 - y) ... instability = 1 - mandelbrot_set.stability(c, smooth=True) ... image.putpixel((x, y), int(instability * 255)) ... >>> image.show()
smooth您可以通過打開用于穩(wěn)定性計(jì)算的標(biāo)志來(lái)啟用平滑。但是,僅這樣做仍然會(huì)產(chǎn)生一點(diǎn)點(diǎn)條帶,因此您也可以將逃生半徑增加到一個(gè)相對(duì)較大的值,例如一千。最后,您將看到具有絲般光滑外觀的 Mandelbrot 套裝圖片:
觸手可及的部分逃逸計(jì)數(shù)為在 Mandelbrot 集可視化中使用顏色提供了有趣的可能性。稍后您將探索它們,但首先,您可以改進(jìn)和簡(jiǎn)化繪圖代碼,使其更加健壯和優(yōu)雅。
在集合元素和像素之間轉(zhuǎn)換
到目前為止,您的可視化已經(jīng)描繪了分形的靜態(tài)圖像,但它并沒有讓您放大特定區(qū)域或平移以顯示更多細(xì)節(jié)。與之前的對(duì)數(shù)不同,縮放和轉(zhuǎn)換圖像的數(shù)學(xué)運(yùn)算并不是非常困難。但是,它增加了一些代碼復(fù)雜性,值得在繼續(xù)之前將其抽象為輔助類。
在高層次上,繪制 Mandelbrot 集可以分為三個(gè)步驟:
將像素的坐標(biāo)轉(zhuǎn)換為復(fù)數(shù)。
檢查該復(fù)數(shù)是否屬于 Mandelbrot 集。
根據(jù)像素的穩(wěn)定性為其分配顏色。
您可以構(gòu)建一個(gè)智能像素?cái)?shù)據(jù)類型,它將封裝坐標(biāo)系之間的轉(zhuǎn)換、考慮縮放和處理顏色。對(duì)于 Pillow 和像素之間的集成層,您可以設(shè)計(jì)一個(gè)視口類來(lái)處理平移和縮放。
Pixel和類的代碼Viewport很快就會(huì)出現(xiàn),但是一旦它們被實(shí)現(xiàn),你就可以用幾行 Python 代碼重寫繪圖代碼:
>>>
>>> from PIL import Image >>> from mandelbrot import MandelbrotSet >>> from viewport import Viewport >>> mandelbrot_set = MandelbrotSet(max_iterations=20) >>> image = Image.new(mode="1", size=(512, 512), color=1) >>> for pixel in Viewport(image, center=-0.75, width=3.5): ... if complex(pixel) in mandelbrot_set: ... pixel.color = 0 ... >>> image.show()
而已!該類Viewport包裝了枕頭圖像的一個(gè)實(shí)例。它根據(jù)中心點(diǎn)和世界單位的視口寬度計(jì)算出相關(guān)的縮放因子、偏移量和世界坐標(biāo)的垂直范圍。作為一個(gè)可迭代的,它還提供Pixel可以循環(huán)的對(duì)象。像素知道如何將自己轉(zhuǎn)換為復(fù)數(shù),并且它們是由視口包裹的圖像實(shí)例的朋友。
注意:傳遞給枕頭圖像的構(gòu)造函數(shù)的第三個(gè)參數(shù)允許您設(shè)置背景顏色,默認(rèn)為黑色。在這種情況下,您需要一個(gè)白色背景,它對(duì)應(yīng)于二進(jìn)制像素模式中等于 1 的像素強(qiáng)度。
您可以通過使用@dataclass裝飾器對(duì)其進(jìn)行注釋來(lái)實(shí)現(xiàn)視口,就像MandelbrotSet之前對(duì)類所做的那樣:
# viewport.py from dataclasses import dataclass from PIL import Image @dataclass class Viewport: image: Image.Image center: complex width: float @property def height(self): return self.scale * self.image.height @property def offset(self): return self.center + complex(-self.width, self.height) / 2 @property def scale(self): return self.width / self.image.width def __iter__(self): for y in range(self.image.height): for x in range(self.image.width): yield Pixel(self, x, y)
視口將圖像實(shí)例、表示為復(fù)數(shù)的中心點(diǎn)和世界坐標(biāo)的水平范圍作為參數(shù)。它還從這三個(gè)參數(shù)中派生了一些只讀屬性,像素稍后將使用這些屬性。最后,該類實(shí)現(xiàn)了一個(gè)特殊的方法,.__iter__()它是Python 中迭代器協(xié)議的一部分,它使得對(duì)自定義類的迭代成為可能。
正如您通過查看上面的代碼塊可能已經(jīng)猜到的那樣,Pixel該類接受Viewport實(shí)例和像素坐標(biāo):
# viewport.py (continued) # ... @dataclass class Pixel: viewport: Viewport x: int y: int @property def color(self): return self.viewport.image.getpixel((self.x, self.y)) @color.setter def color(self, value): self.viewport.image.putpixel((self.x, self.y), value) def __complex__(self): return ( complex(self.x, -self.y) * self.viewport.scale + self.viewport.offset )
這里只定義了一個(gè)屬性,但它包含像素顏色的 getter 和 setter,它們通過視口委托給 Pillow。特殊方法.__complex__()負(fù)責(zé)將像素轉(zhuǎn)換為世界單位的相關(guān)復(fù)數(shù)。它沿垂直軸翻轉(zhuǎn)像素坐標(biāo),將它們轉(zhuǎn)換為復(fù)數(shù),然后利用復(fù)數(shù)算法來(lái)縮放和移動(dòng)它們。
繼續(xù)嘗試一下您的新代碼。Mandelbrot 集包含幾乎無(wú)限的復(fù)雜結(jié)構(gòu),只有在放大倍率下才能看到。一些地區(qū)的特點(diǎn)是螺旋形和曲折形,類似于海馬、章魚或大象。放大時(shí),不要忘記增加最大迭代次數(shù)以顯示更多細(xì)節(jié):
>>>
>>> from PIL import Image >>> from mandelbrot import MandelbrotSet >>> from viewport import Viewport >>> mandelbrot_set = MandelbrotSet(max_iterations=256, escape_radius=1000) >>> image = Image.new(mode="L", size=(512, 512)) >>> for pixel in Viewport(image, center=-0.7435 + 0.1314j, width=0.002): ... c = complex(pixel) ... instability = 1 - mandelbrot_set.stability(c, smooth=True) ... pixel.color = int(instability * 255) ... >>> image.show()
視口跨越 0.002 個(gè)世界單位,以 -0.7435 + 0.1314j 為中心,靠近產(chǎn)生美麗螺旋的Misiurewicz 點(diǎn)。根據(jù)迭代次數(shù),您將獲得具有不同細(xì)節(jié)程度的更暗或更亮的圖像。如果您愿意,可以使用 Pillow 來(lái)增加亮度:
>>>
>>> from PIL import ImageEnhance >>> enhancer = ImageEnhance.Brightness(image) >>> enhancer.enhance(1.25).show()
這將使圖像亮 25% 并顯示這個(gè)螺旋:
Mandelbrot 集以 Misiurewicz 點(diǎn)為中心
您可以找到更多產(chǎn)生如此壯觀結(jié)果的獨(dú)特點(diǎn)。維基百科擁有一個(gè)完整的圖片庫(kù),其中包含值得探索的 Mandelbrot 集的各種細(xì)節(jié)。
如果您已經(jīng)開始檢查不同的點(diǎn),那么您可能還注意到渲染時(shí)間對(duì)您當(dāng)前正在查看的區(qū)域高度敏感。遠(yuǎn)離分形的像素會(huì)更快地發(fā)散到無(wú)窮大,而靠近分形的像素往往需要更多的迭代。因此,特定區(qū)域中的內(nèi)容越多,解決這些像素是否穩(wěn)定或不穩(wěn)定所需的時(shí)間就越長(zhǎng)。
您可以使用一些選項(xiàng)來(lái)提高 Python 中的 Mandelbrot 集渲染性能。但是,它們超出了本教程的范圍,因此如果您好奇,請(qǐng)隨意探索它們。現(xiàn)在是時(shí)候給你的分形一些顏色了。
制作曼德布羅集的藝術(shù)表現(xiàn)形式
由于您已經(jīng)可以繪制灰色陰影的分形,因此添加更多顏色應(yīng)該不會(huì)太困難。如何將像素的穩(wěn)定性映射到色調(diào)完全取決于您。雖然有許多算法可以以美觀的方式繪制 Mandelbrot 集,但您的想象力是唯一的限制!
如果您還沒有關(guān)注,那么您可以通過單擊下面的鏈接下載隨附的代碼:
在繼續(xù)之前,您需要對(duì)上一節(jié)中的繪圖代碼進(jìn)行一些調(diào)整。具體來(lái)說,您將切換到更豐富的顏色模式并定義一些可重用的輔助函數(shù),以使您的生活更輕松。
調(diào)色板
自古以來(lái),藝術(shù)家們一直在一個(gè)叫做調(diào)色板的物理板上混合顏料。在計(jì)算中,調(diào)色板代表一個(gè)顏色查找表,它是一種無(wú)損壓縮形式。它通過索引每種顏色一次然后在所有相關(guān)像素中引用它來(lái)減少圖像的內(nèi)存占用。
這種技術(shù)相對(duì)簡(jiǎn)單且計(jì)算速度快。同樣,您可以使用預(yù)定義的調(diào)色板來(lái)繪制分形。但是,您可以使用轉(zhuǎn)義計(jì)數(shù)作為調(diào)色板的索引,而不是使用像素坐標(biāo)來(lái)查找相應(yīng)的顏色。事實(shí)上,您早期的可視化已經(jīng)通過應(yīng)用 256 種單色灰色的調(diào)色板做到了這一點(diǎn),只是沒有將它們緩存在查找表中。
要使用更多顏色,您需要先在RGB 模式下創(chuàng)建圖像,這將分配 24 位/像素:
image = Image.new(mode="RGB", size=(width, height))
從現(xiàn)在開始,Pillow 將把每個(gè)像素表示為一個(gè)由紅色、綠色和藍(lán)色 (RGB)顏色通道組成的元組。每種原色都可以采用 0 到 255 之間的整數(shù),達(dá)到 1670 萬(wàn)種獨(dú)特的顏色。但是,在迭代次數(shù)附近,您的調(diào)色板通常包含比這少得多的內(nèi)容。
注意:調(diào)色板中的顏色數(shù)量不一定必須等于最大迭代次數(shù)。畢竟,在您運(yùn)行遞歸公式之前,不知道會(huì)有多少穩(wěn)定性值。當(dāng)您啟用平滑時(shí),分?jǐn)?shù)轉(zhuǎn)義計(jì)數(shù)的數(shù)量可能大于迭代次數(shù)!
如果您想測(cè)試幾個(gè)不同的調(diào)色板,那么引入幫助函數(shù)以避免一遍又一遍地重新輸入相同的命令可能會(huì)很方便:
>>>
>>> from PIL import Image >>> from mandelbrot import MandelbrotSet >>> from viewport import Viewport >>> def paint(mandelbrot_set, viewport, palette, smooth): ... for pixel in viewport: ... stability = mandelbrot_set.stability(complex(pixel), smooth) ... index = int(min(stability * len(palette), len(palette) - 1)) ... pixel.color = palette[index % len(palette)]
該函數(shù)將MandelbrotSet實(shí)例作為參數(shù),后跟Viewport、調(diào)色板和平滑標(biāo)志。調(diào)色板必須是具有 Pillow 期望的紅色、綠色和藍(lán)色通道值的元組列表。請(qǐng)注意,一旦您計(jì)算了手頭像素的浮點(diǎn)穩(wěn)定性,您必須在將其用作調(diào)色板中的整數(shù)索引之前對(duì)其進(jìn)行縮放和鉗制。
Pillow 只理解顏色通道的 0 到 255 范圍內(nèi)的整數(shù)。但是,使用0 到 1 之間的歸一化小數(shù)值通常可以防止您的大腦緊張。您可以定義另一個(gè)函數(shù)來(lái)反轉(zhuǎn)規(guī)范化過程以使 Pillow 庫(kù)滿意:
>>>
>>> def denormalize(palette): ... return [ ... tuple(int(channel * 255) for channel in color) ... for color in palette ... ]
此函數(shù)將分?jǐn)?shù)顏色值縮放為整數(shù)值。例如,它將一個(gè)帶有數(shù)字的元組轉(zhuǎn)換(0.13, 0.08, 0.21)為另一個(gè)由以下通道強(qiáng)度組成的元組:(45, 20, 53).
巧合的是,Matplotlib 庫(kù)包含幾個(gè)具有此類標(biāo)準(zhǔn)化顏色通道的顏色圖。一些顏色圖是固定的顏色列表,而另一些則能夠?qū)ψ鳛閰?shù)給出的值進(jìn)行插值。您現(xiàn)在可以將其中一個(gè)應(yīng)用到您的 Mandelbrot 集可視化:
>>>
>>> import matplotlib.cm >>> colormap = matplotlib.cm.get_cmap("twilight").colors >>> palette = denormalize(colormap) >>> len(colormap) 510 >>> colormap[0] [0.8857501584075443, 0.8500092494306783, 0.8879736506427196] >>> palette[0] (225, 216, 226)
twilight顏色圖是 510 種顏色的列表。調(diào)用denormalize()它后,您將獲得適合您的繪畫功能的調(diào)色板。在調(diào)用它之前,您需要定義更多變量:
>>>
>>> mandelbrot_set = MandelbrotSet(max_iterations=512, escape_radius=1000) >>> image = Image.new(mode="RGB", size=(512, 512)) >>> viewport = Viewport(image, center=-0.7435 + 0.1314j, width=0.002) >>> paint(mandelbrot_set, viewport, palette, smooth=True) >>> image.show()
這將產(chǎn)生與以前相同的螺旋,但外觀更具吸引力:
使用暮光調(diào)色板可視化的螺旋
隨意嘗試 Matplotlib 中包含的其他調(diào)色板或他們?cè)谖臋n中提到的第三方庫(kù)之一。此外,Matplotlib 允許您通過將_r后綴附加到顏色圖的名稱來(lái)反轉(zhuǎn)顏色順序。您還可以從頭開始創(chuàng)建調(diào)色板,如下所示。
假設(shè)您想強(qiáng)調(diào)分形的邊緣。在這種情況下,您可以將分形分成三個(gè)部分并為每個(gè)部分分配不同的顏色:
>>>
>>> exterior = [(1, 1, 1)] * 50 >>> interior = [(1, 1, 1)] * 5 >>> gray_area = [(1 - i / 44,) * 3 for i in range(45)] >>> palette = denormalize(exterior + gray_area + interior)
為您的調(diào)色板選擇一個(gè)整數(shù),例如 100 種顏色,將簡(jiǎn)化公式。然后,您可以拆分顏色,使 50% 到外部,5% 到內(nèi)部,剩下的 45% 到中間的灰色區(qū)域。通過將 RGB 通道設(shè)置為完全飽和,您希望外部和內(nèi)部都保持白色。但是,中間地帶應(yīng)該逐漸從白色變?yōu)楹谏?/p>
不要忘記將視口的中心點(diǎn)設(shè)置為 -0.75,并將其寬度設(shè)置為 3.5 個(gè)單位以覆蓋整個(gè)分形。在此縮放級(jí)別,您還需要減少迭代次數(shù):
>>>
>>> mandelbrot_set = MandelbrotSet(max_iterations=20, escape_radius=1000) >>> viewport = Viewport(image, center=-0.75, width=3.5) >>> paint(mandelbrot_set, viewport, palette, smooth=True) >>> image.show()
當(dāng)您再次調(diào)用paint()并顯示圖像時(shí),您將看到 Mandelbrot 集的清晰邊界:
帶出分形邊緣的自定義調(diào)色板
從白色到黑色的連續(xù)過渡,然后突然跳到純白色,會(huì)產(chǎn)生一種陰影浮雕效果,突出分形的邊緣。您的調(diào)色板將一些固定顏色與稱為顏色漸變的平滑顏色漸變相結(jié)合,您將在接下來(lái)進(jìn)行探索。
顏色漸變
您可能會(huì)將漸變視為連續(xù)的調(diào)色板。最常見的顏色漸變類型是線性漸變,它使用線性插值來(lái)查找兩種或多種顏色之間最接近的值。當(dāng)您混合黑色和白色以投射陰影時(shí),您剛剛看到了這種顏色漸變的示例。
現(xiàn)在,您可以計(jì)算出您打算使用的每個(gè)漸變的數(shù)學(xué)計(jì)算或構(gòu)建一個(gè)通用漸變工廠。此外,如果您想以非線性方式分配顏色,那么SciPy是您的朋友。該庫(kù)帶有線性、二次和三次插值方法等。以下是您可以利用它的方法:
>>>
>>> import numpy as np >>> from scipy.interpolate import interp1d >>> def make_gradient(colors, interpolation="linear"): ... X = [i / (len(colors) - 1) for i in range(len(colors))] ... Y = [[color[i] for color in colors] for i in range(3)] ... channels = [interp1d(X, y, kind=interpolation) for y in Y] ... return lambda x: [np.clip(channel(x), 0, 1) for channel in channels]
您的新工廠函數(shù)接受定義為浮點(diǎn)值三元組的顏色列表和帶有 SciPy 公開的插值算法名稱的可選字符串。大寫X變量包含基于顏色數(shù)量介于 0 和 1 之間的標(biāo)準(zhǔn)化值。大寫Y變量為每種顏色保存三個(gè)序列的 R、G 和 B 值,并且該channels變量具有每個(gè)通道的插值函數(shù)。
當(dāng)你調(diào)用make_gradient()一些顏色時(shí),你會(huì)得到一個(gè)新函數(shù),它可以讓你插入中間值:
>>>
>>> black = (0, 0, 0) >>> blue = (0, 0, 1) >>> maroon = (0.5, 0, 0) >>> navy = (0, 0, 0.5) >>> red = (1, 0, 0) >>> colors = [black, navy, blue, maroon, red, black] >>> gradient = make_gradient(colors, interpolation="cubic") >>> gradient(0.42) [0.026749999999999954, 0.0, 0.9435000000000001]
請(qǐng)注意,漸變顏色(例如上例中的黑色)可以重復(fù)并以任何順序出現(xiàn)。要將漸變連接到您的調(diào)色板感知繪畫函數(shù),您必須確定相應(yīng)調(diào)色板中的顏色數(shù)量并將漸變函數(shù)轉(zhuǎn)換為非規(guī)范化元組的固定大小列表:
>>>
>>> num_colors = 256 >>> palette = denormalize([ ... gradient(i / num_colors) for i in range(num_colors) ... ]) ... >>> len(palette) 256 >>> palette[127] (46, 0, 143)
您可能會(huì)想直接針對(duì)穩(wěn)定性值使用梯度函數(shù)。不幸的是,這在計(jì)算上太昂貴了,把你的耐心推到了極限。您想預(yù)先計(jì)算所有已知顏色的插值,而不是每個(gè)像素。
最后,在構(gòu)建漸變工廠、創(chuàng)建漸變函數(shù)和非規(guī)范化顏色之后,您可以使用漸變調(diào)色板繪制 Mandelbrot 集:
>>>
>>> mandelbrot_set = MandelbrotSet(max_iterations=20, escape_radius=1000) >>> paint(mandelbrot_set, viewport, palette, smooth=True) >>> image.show()
這將產(chǎn)生一個(gè)發(fā)光的霓虹燈效果:
不斷增加迭代次數(shù),直到您對(duì)圖像中的平滑量和細(xì)節(jié)數(shù)量感到滿意為止。但是,請(qǐng)查看接下來(lái)會(huì)發(fā)生什么以獲得最佳結(jié)果和最直觀的顏色處理方式。
顏色模型
到目前為止,您一直在研究紅色、綠色和藍(lán)色 (RGB) 分量的領(lǐng)域,這并不是思考顏色的最自然方式。幸運(yùn)的是,有其他顏色模型可以讓您表達(dá)相同的概念。一種是色相、飽和度、亮度(HSB)顏色模型,也稱為色相、飽和度、值(HSV)。
注意:不要將 HSB 或 HSV 與另一種顏色模型混淆:色相、飽和度、亮度 (HSL)。
與 RGB 一樣,HSB 模型也具有三個(gè)分量,但它們與紅色、綠色和藍(lán)色通道不同。您可以將 RGB 顏色想象為包含在 3D 立方體中的一個(gè)點(diǎn)。但是,同一點(diǎn)在 HSB 中具有柱坐標(biāo):
色相飽和度亮度缸
三個(gè) HSB 坐標(biāo)是:
色調(diào):在 0° 和 360° 之間逆時(shí)針測(cè)量的角度
飽和度:圓柱體的半徑在 0% 到 100% 之間
亮度:圓柱體的高度在 0% 到 100% 之間
要在 Pillow 中使用此類坐標(biāo),您必須將它們轉(zhuǎn)換為熟悉的 0 到 255 范圍內(nèi)的 RGB 值元組:
>>>
>>> from PIL.ImageColor import getrgb >>> def hsb(hue_degrees: int, saturation: float, brightness: float): ... return getrgb( ... f"hsv({hue_degrees % 360}," ... f"{saturation * 100}%," ... f"{brightness * 100}%)" ... ) >>> hsb(360, 0.75, 1) (255, 64, 64)
Pillow 已經(jīng)提供了getrgb()您可以委托的輔助函數(shù),但它需要一個(gè)帶有編碼 HSB 坐標(biāo)的特殊格式的字符串。另一方面,您的包裝函數(shù)接受hue度數(shù)和兩者saturation,并brightness作為規(guī)范化的浮點(diǎn)值。這使您的函數(shù)與零到一之間的穩(wěn)定性值兼容。
有幾種方法可以將穩(wěn)定性與 HSB 顏色聯(lián)系起來(lái)。例如,您可以通過將穩(wěn)定性縮放到 360° 度來(lái)使用整個(gè)色譜,使用穩(wěn)定性來(lái)調(diào)制飽和度,并將亮度設(shè)置為 100%,如下面的 1 所示:
>>>
>>> mandelbrot_set = MandelbrotSet(max_iterations=20, escape_radius=1000) >>> for pixel in Viewport(image, center=-0.75, width=3.5): ... stability = mandelbrot_set.stability(complex(pixel), smooth=True) ... pixel.color = (0, 0, 0) if stability == 1 else hsb( ... hue_degrees=int(stability * 360), ... saturation=stability, ... brightness=1, ... ) ... >>> image.show()
要將內(nèi)部涂成黑色,請(qǐng)檢查像素的穩(wěn)定性是否恰好為 1,并將所有三個(gè)顏色通道設(shè)置為零。對(duì)于小于 1 的穩(wěn)定性值,外部的飽和度會(huì)隨著與分形的距離而減弱,并且色調(diào)會(huì)遵循 HSB 圓柱體的角度維度:
使用 HSB 顏色模型可視化的 Mandelbrot 集
當(dāng)您靠近分形時(shí),角度會(huì)增加,顏色會(huì)從黃色變?yōu)榫G色、青色、藍(lán)色和洋紅色。您看不到紅色,因?yàn)榉中蔚膬?nèi)部總是涂成黑色,而外部最遠(yuǎn)的部分幾乎沒有飽和度。請(qǐng)注意,將圓柱體旋轉(zhuǎn) 120° 可讓您在其底部定位三種原色(紅色、綠色和藍(lán)色)中的每一種。
不要猶豫,嘗試以不同的方式計(jì)算 HSB 坐標(biāo),看看會(huì)發(fā)生什么!
結(jié)論
現(xiàn)在您知道如何使用 Python 來(lái)繪制和繪制 Beno?t Mandelbrot 發(fā)現(xiàn)的著名分形了。你已經(jīng)學(xué)會(huì)了用顏色、灰度和黑白來(lái)可視化它的各種方法。您還看到了一個(gè)實(shí)際示例,說明了復(fù)數(shù)如何幫助在 Python 中優(yōu)雅地表達(dá)數(shù)學(xué)公式。
在本教程中,您學(xué)習(xí)了如何:
將復(fù)數(shù)應(yīng)用于實(shí)際問題
查找Mandelbrot和Julia集的成員
使用Matplotlib和Pillow將這些集合繪制為分形
對(duì)分形進(jìn)行豐富多彩的藝術(shù)表現(xiàn)
您可以通過單擊下面的鏈接下載本教程中使用的完整源代碼:
Python 彈性負(fù)載均衡 ELB
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。