初識scipy

scipy包含致力於科學計算中常見問題的各個工具箱。它的不同子模塊相應於不同的應用。像插值,積分,優化,圖像處理,統計,特殊函數等等。

scipy可以與其它標準科學計算程序庫進行比較,比如GSL(GNU C或C++科學計算庫),或者Matlab工具箱。scipy是Python中科學計算程序的核心包; 它用於有效地計算numpy矩陣,來讓numpy和scipy協同工作。

在實現一個程序之前,值得檢查下所需的數據處理方式是否已經在scipy中存在了。作爲非專業程序員,科學家總是喜歡重新發明造輪子,導致了充滿漏洞的,未經優化的,很難分享和維護的代碼。相反,Scipy程序經過優化和測試,因此應該儘可能使用。

目錄

警告:這個教程離真正的數值計算介紹很遠。因爲枚舉scipy中不同的子模塊和函數非常無聊,我們集中精力代之以幾個例子來給出如何使用scipy進行計算的大致思想。
scipy 由一些特定功能的子模塊組成:

模塊 功能
scipy.cluster 矢量量化 / K-均值
scipy.constants 物理和數學常數
scipy.fftpack 傅里葉變換
scipy.integrate 積分程序
scipy.interpolate 插值
scipy.io 數據輸入輸出
scipy.linalg 線性代數程序
scipy.ndimage n維圖像包
scipy.odr 正交距離迴歸
scipy.optimize 優化
scipy.signal 信號處理
scipy.sparse 稀疏矩陣
scipy.spatial 空間數據結構和算法
scipy.special 任何特殊數學函數
scipy.stats 統計


它們全依賴numpy,但是每個之間基本獨立。導入Numpy和這些scipy模塊的標準方式是:

import numpy as np
from scipy import stats   # 其它子模塊相同

主scipy命名空間大多包含真正的numpy函數(嘗試 scipy.cos 就是 np.cos)。這些僅僅是由於歷史原因,通常沒有理由在你的代碼中使用import scipy

一、文件輸入/輸出:scipy.io

導入和保存matlab文件:

import numpy as np
from scipy import io as spio
a = np.ones((3, 3))
spio.savemat('file.mat', {'a': a})  # savemat as a dictionary
data = spio.loadmat('file.mat', struct_as_record=True)
print(data['a'])

讀取圖片:

In [16]: from scipy import misc
In [17]: misc.imread('scikit.png')
Out[17]: 
array([[[255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        ..., 
        ..., 
        [255, 255, 255, 255],
        [255, 255, 255, 255],
        [255, 255, 255, 255]]], dtype=uint8)

In [18]: import matplotlib.pyplot as plt
In [19]: plt.imread('scikit.png')
Out[19]: 
array([[[ 1.,  1.,  1.,  1.],
        [ 1.,  1.,  1.,  1.],
        [ 1.,  1.,  1.,  1.],
        ..., 

       [[ 1.,  1.,  1.,  1.],
        [ 1.,  1.,  1.,  1.],
        [ 1.,  1.,  1.,  1.],
        ..., 
        [ 1.,  1.,  1.,  1.],
        [ 1.,  1.,  1.,  1.],
        [ 1.,  1.,  1.,  1.]]], dtype=float32)

載入txt文件:

numpy.loadtxt()
numpy.savetxt()

智能導入文本/csv文件:

numpy.genfromtxt()
numpy.recfromcsv()

高速,有效率但numpy特有的二進制格式:

numpy.save()
numpy.load()

二、特殊函數:scipy.special

特殊函數是先驗函數。scipy.special的文檔字符串寫得非常好,所以我們不在這裏列出所有函數。常用的有:

  • 貝塞爾函數,如scipy.special.jn() (整數n階貝塞爾函數)
  • 橢圓函數: scipy.special.ellipj() (雅可比橢圓函數,……)
  • 伽馬函數:scipy.special.gamma(),還要注意scipy.special.gammaln,這個函數給出對數座標的伽馬函數,因此有更高的數值精度。

三、線性代數運算:scipy.linalg

scipy.linalg模塊提供標準線性代數運算,依賴於底層有效率的實現(BLAS,LAPACK)。

scipy.linalg.det()函數計算方陣的行列式:

In [22]: from scipy import linalg

In [23]: arr = np.array([[1, 2],
   ....:                [3, 4]])

In [24]: linalg.det(arr)
Out[24]: -2.0

In [25]: linalg.det(np.ones((3,4)))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-25-375ad1d49940> in <module>()
----> 1 linalg.det(np.ones((3,4)))

/usr/lib/python2.7/site-packages/scipy/linalg/basic.pyc in det(a, overwrite_a)
    398     a1 = np.asarray_chkfinite(a)
    399     if len(a1.shape) != 2 or a1.shape[0] != a1.shape[1]:
--> 400         raise ValueError('expected square matrix')
    401     overwrite_a = overwrite_a or _datacopied(a1, a)
    402     fdet, = get_flinalg_funcs(('det',), (a1,))

ValueError: expected square matrix
py.linalg.inv()

函數計算方陣的逆:

In [26]: arr = np.array([[1, 2],
                [3, 4]])

In [27]: iarr = linalg.inv(arr)

In [28]: iarr
Out[28]: 
array([[-2. ,  1. ],
       [ 1.5, -0.5]])

In [29]: np.allclose(np.dot(arr, iarr), np.eye(2))
Out[29]: True

最後計算奇異陣的逆(它的行列式爲0)將會引發(raise)LinAlgError:

In [32]: arr = np.array([[3, 2],
                [6, 4]])

In [33]: linalg.inv(arr)
---------------------------------------------------------------------------
LinAlgError                               Traceback (most recent call last)
<ipython-input-33-52c04c854a80> in <module>()
----> 1 linalg.inv(arr)

/usr/lib/python2.7/site-packages/scipy/linalg/basic.pyc in inv(a, overwrite_a)
    346             inv_a, info = getri(lu, piv, overwrite_lu=1)
    347     if info > 0:
--> 348         raise LinAlgError("singular matrix")
    349     if info < 0:
    350         raise ValueError('illegal value in %d-th argument of internal '

LinAlgError: singular matrix

還有更多高級運算,如奇異值分解(SVD):

In [34]: arr = np.arange(9).reshape((3, 3)) + np.diag([1, 0, 1])
In [35]: uarr, spec, vharr = linalg.svd(arr)

它的結果數組譜是:

In [36]: spec
Out[36]: array([ 14.88982544,   0.45294236,   0.29654967])

原始矩陣可以由svd的輸出用np.dot點乘重新組合得到:

In [37]: sarr = np.diag(spec)
In [38]: svd_mat = uarr.dot(sarr).dot(vharr)
In [39]: np.allclose(svd_mat, arr)
Out[39]: True

SVD在信號處理和統計中運用很廣。許多其它標準分解(QR,LU,Cholesky,Schur),還有線性系統的解也可以從scipy.linalg中獲得。

四、快速傅里葉變換:scipy.fftpack

scipy.fftpack模塊用來計算快速傅里葉變換。作爲示例,一個(噪聲)輸入信號可能像這樣:

In [40]: time_step = 0.02
In [41]: period = 5
In [42]: time_vec = np.arange(0, 20, time_step)
In [43]: sig = np.sin(2 * np.pi / period * time_vec) + \
   ....: 0.5 * np.random.randn(time_vec.size)

觀測者並不指導信號頻率,僅僅等間隔取樣信號sig。信號應該來自一個真實的函數所以傅里葉變換將是對稱的。scipy.fftpack.fftfreq()函數將生成取樣頻率,scipy.fftpack.fft()將計算快速傅里葉變換:

因爲功率結果是對稱的,僅僅需要使用譜的正值部分來找出頻率:

In [48]: pidxs = np.where(sample_freq > 0)
In [49]: freqs = sample_freq[pidxs]
In [50]: power = np.abs(sig_fft)[pidxs]

信號頻率可以這樣被找到:

In [51]: freq = freqs[power.argmax()]
In [52]: np.allclose(freq, 1./period)
Out[52]: True

現在高頻噪聲將被從傅里葉變換信號中移除:

In [53]: sig_fft[np.abs(sample_freq) > freq] = 0

得到濾波信號,可以用scipy.fftpack.ifft函數計算:

In [54]: main_sig = fftpack.ifft(sig_fft)

結果可以這樣可視化:

In [55]: plt.figure()
Out[55]: <matplotlib.figure.Figure at 0x4a9fb50>

In [56]: plt.plot(time_vec, sig)
Out[56]: [<matplotlib.lines.Line2D at 0x4ad3790>]

In [57]: plt.plot(time_vec, main_sig, linewidth=3)
/usr/lib/python2.7/site-packages/numpy/core/numeric.py:320: ComplexWarning: Casting complex values to real discards the imaginary part
  return array(a, dtype, copy=False, order=order)
Out[57]: [<matplotlib.lines.Line2D at 0x4ad3dd0>]

In [58]: plt.xlabel('Time [s]')
Out[58]: <matplotlib.text.Text at 0x4aad050>

In [59]: plt.ylabel('Amplitude')
Out[59]: <matplotlib.text.Text at 0x4aadbd0>

In [60]: plt.show()

五、numpy.fft

Numpy也有一個FFT實現(numpy.fft)。然而,通常scipy的應該優先使用,因爲它使用了更有效率的底層實現。

工作示例:找到原始週期

工作示例:高斯圖像模糊

卷積:

f_1(t) = \int dt’K(t-t’)f_0(t’)
\tilde{f_1}(\omega)=\tilde{K}(\omega)\tilde{f_0}(\omega)

練習:登月圖片消噪

檢查提供的圖像moonlanding.png,該圖像被週期噪聲嚴重污染了。在這個練習中,我們旨在使用快速傅里葉變換清除噪聲。
用plt.imread加載圖像。
使用scipy.fftpack中的2-D傅里葉函數找到並繪製圖像的譜線(傅里葉變換)。可視化這個譜線對你有問題嗎?如果有,爲什麼?
這個譜包含高頻和低頻成分。噪聲是在譜線的高頻部分中,所以設置一些成分爲0(使用數組切片)。
應用逆傅里葉變換來看最後的圖像。
我的消除噪聲實例……


六、優化和擬合:scipy.optimize

優化是找到最小值或等式的數值解的問題。

scipy.optimization子模塊提供了函數最小值(標量或多維)、曲線擬合和尋找等式的根的有用算法。

from scipy import optimize

找到標量函數的最小值

讓我們定義以下函數

In [2]: def f(x):
   ...:     return x**2 + 10 * np.sin(x)

In [3]: x = np.arange(-10, 10, 0.1)

In [4]: plt.plot(x, f(x))
Out[4]: [<matplotlib.lines.Line2D at 0x3e2a4d0>]

In [5]: plt.show()

該函數在大約-1.3有個全局最小值,在3.8有個局部最小值。

找到這個函數最小值一般而有效的方法是從初始點使用梯度下降法。BFGS算法^1是做這個的好方法:

In [6]: optimize.fmin_bfgs(f, 0)
Optimization terminated successfully.
         Current function value: -7.945823
         Iterations: 5
         Function evaluations: 24
         Gradient evaluations: 8
Out[6]: array([-1.30644003])

這個方法一個可能的問題在於,如果函數有局部最小值,算法會因初始點不同找到這些局部最小而不是全局最小:

In [7]: optimize.fmin_bfgs(f, 3, disp=0)
Out[7]: array([ 3.83746663])

如果我們不知道全局最小值的鄰近值來選定初始點,我們需要藉助於耗費資源些的全局優化。爲了找到全局最小點,最簡單的算法是蠻力算法^2,該算法求出給定格點的每個函數值。

In [10]: grid = (-10, 10, 0.1)
In [11]: xmin_global = optimize.brute(f, (grid,))
In [12]: xmin_global
Out[12]: array([-1.30641113])

對於大點的格點,scipy.optimize.brute()變得非常慢。scipy.optimize.anneal()提供了使用模擬退火的替代函數。對已知的不同類別全局優化問題存在更有效率的算法,但這已經超出scipy的範圍。一些有用全局優化軟件包是OpenOpt、IPOPT、PyGMO和PyEvolve。

爲了找到局部最小,我們把變量限制在(0, 10)之間,使用scipy.optimize.fminbound():

In [13]: xmin_local = optimize.fminbound(f, 0, 10)
In [14]: xmin_local
Out[14]: 3.8374671194983834

注意:在高級章節部分數學優化:找到函數最小值中有關於尋找函數最小值更詳細的討論。

找到標量函數的根

爲了尋找根,例如令f(x)=0的點,對以上的用來示例的函數f我們可以使用scipy.optimize.fsolve():

In [17]: root = optimize.fsolve(f, 1)  # 我們的初始猜測是1
In [18]: root
Out[18]: array([ 0.])

注意僅僅一個根被找到。檢查f的圖像在-2.5附近有第二個根。我們可以通過調整我們的初始猜測找到這一確切值:

In [19]: root = optimize.fsolve(f, -2.5)
In [20]: root
Out[20]: array([-2.47948183])

曲線擬合

假設我們有從被噪聲污染的f中抽樣到的數據:

In [21]: xdata = np.linspace(-10, 10, num=20)
In [22]: ydata = f(xdata) + np.random.randn(xdata.size)

如果我們知道函數形式(當前情況是x^2 + sin(x)),但是不知道幅度。我們可以通過最小二乘擬合擬合來找到幅度。首先我們定義一個用來擬合的函數:

In [23]: def f2(x, a, b):
   ....:     return a*x**2 + b*np.sin(x)

然後我們可以使用scipy.optimize.curve_fit()來找到a和b:

In [24]: guess = [2, 2]
In [25]: params, params_covariance = optimize.curve_fit(f2, xdata, ydata, guess)
In [26]: params
Out[26]: array([  1.00439471,  10.04911441])

現在我們找到了f的最小值和根並且對它使用了曲線擬合。我們將一切放在一個單獨的圖像中:

注意:Scipy>=0.11中提供所有最小化和根尋找算法的統一接口scipy.optimize.minimize(),scipy.optimize.minimize_scalar()和scipy.optimize.root()。它們允許通過method關鍵字方便地比較不同算法。

你可以在scipy.optimize中找到用來解決多維問題的相同功能的算法。


練習:曲線擬合溫度數據

在阿拉斯加每個月的溫度上下限,從一月開始,以攝氏單位給出。
max: 17, 19, 21, 28, 33, 38, 37, 37, 31, 23, 19, 18
min: -62, -59, -56, -46, -32, -18, -9, -13, -25, -46, -52, -58
繪製這些溫度限
定義函數來描述最小和最大溫度。
提示:這個函數以一年爲週期。
提示:包括時間偏移。
對數據使用這個函數scipy.optimize.curve_fit()
繪製結果。是否擬合合理?
如果不合理,爲什麼?
擬合精度的最大最小溫度的時間偏移是否一樣?


練習:2維最小化

六峯值駝背函數:

f(x,y) = (4 - 2.1x^2 + \frac{x^4}{3})x^2 + xy + (4y^2 - 4)y^2

有全局和多個局部最小。找到這個函數的全局最小。

提示:

變量應該限制在-2 < x < 2 , -1 < y < 1.
使用numpy.meshgrid()和plt.imshow來可視地搜尋區域。
使用scipy.optimize.fmin_bfgs()或其它多維極小化器。
這裏有多少極小值?這些點上的函數值是多少?如果初始猜測是(x, y) = (0, 0)會發生什麼?

參見總結練習非線性最小二乘擬合:在點抽取地形激光雷達數據上的應用,來看另一個,更高級的例子。


七、統計和隨機數: scipy.stats

scipy.stats包括統計工具和隨機過程的概率過程。各個隨機過程的隨機數生成器可以從numpy.random中找到。

直方圖和概率密度函數

給定一個隨機過程的觀察值,它們的直方圖是隨機過程的pdf(概率密度函數)的估計器:

In [1]: import numpy as np
In [2]: a = np.random.normal(size=1000)
In [3]: bins = np.arange(-4, 5)
In [4]: bins
Out[4]: array([-4, -3, -2, -1,  0,  1,  2,  3,  4])

In [5]: histogram = np.histogram(a, bins=bins, normed=True)[0]
In [6]: bins = 0.5*(bins[1:] + bins[:-1])
In [7]: bins
Out[7]: array([-3.5, -2.5, -1.5, -0.5,  0.5,  1.5,  2.5,  3.5])

In [8]: from scipy import stats
In [9]: b = stats.norm.pdf(bins)  # norm是正態分佈
In [10]: import matplotlib.pyplot as plt
In [11]: plt.plot(bins, histogram)
Out[11]: [<matplotlib.lines.Line2D at 0x3378b10>]

In [12]: plt.plot(bins, b)
Out[12]: [<matplotlib.lines.Line2D at 0x3378fd0>]

In [13]: plt.show()

如果我們知道隨機過程屬於給定的隨機過程族,比如正態過程。我們可以對觀測值進行最大似然擬合來估計基本分佈參數。這裏我們對觀測值擬合一個正態過程:

In [14]: loc, std = stats.norm.fit(a)
In [15]: loc
Out[15]: 0.0052651057415999758

In [16]: std
Out[16]: 0.97945439802779732

練習:概率分佈

從參數爲1的伽馬分佈生成1000個隨機數,然後繪製這些樣點的直方圖。你能夠在其上繪製pdf嗎(應該匹配)?

另外:這些分佈有些有用的方法。通過閱讀它們的文檔字符串或使用IPython的tab補全來探索它們。你能夠通過對你的隨機變量使用擬合找到形狀參數1嗎?


百分位

中位數是來觀測值之下一半之上一半的值。

In [3]: np.median(a)
Out[3]: -0.047679175711778043

它也被叫作50百分位點,因爲有50%的觀測值在它之下:

In [6]: stats.scoreatpercentile(a, 50)
Out[6]: -0.047679175711778043

同樣我們可以計算百分之九十百分點:

In [7]: stats.scoreatpercentile(a, 90)
Out[7]: 1.2541592439997036

百分位是CDF的一個估計器(累積分佈函數)。

統計檢測

統計檢測是決策指示。例如,我們有兩個樣本集,我們假設它們由高斯過程生成。我們可以使用T檢驗來決定是否兩個樣本值顯著不同:

In [8]: a = np.random.normal(0, 1, size=100)
In [9]: b = np.random.normal(1, 1, size=10)
In [10]: stats.ttest_ind(a, b)
Out[10]: (array(-2.4119199601156796), 0.01755485116571583)

輸出結果由以下部分組成:

  • T統計量:它是這麼一種標誌,與不同兩個隨機過程之間成比例並且幅度和差異的顯著程度有關^3。
  • p值:兩個過程相同的概率。如果接近1,這兩個過程是幾乎完全相同的。越靠近零,兩個過程越可能有不同的均值。

八、插值:scipy.interpolate

scipy.interpolate對從實驗數據擬合函數來求值沒有測量值存在的點非常有用。這個模塊基於來自netlib項目的FITPACK Fortran 子程序。

通過想象接近正弦函數的實驗數據:

In [1]: measured_time = np.linspace(0, 1, 10)
In [2]: noise = (np.random.random(10)*2 - 1) * 1e-1
In [3]: measures = np.sin(2 * np.pi * measured_time) + noise

scipy.interpolate.interp1d類會構建線性插值函數:

In [4]: from scipy.interpolate import interp1d
In [5]: linear_interp = interp1d(measured_time, measures)

然後scipy.interpolate.linear_interp實例需要被用來求得感興趣時間點的值:

In [6]: computed_time = np.linspace(0, 1, 50)
In [7]: linear_results = linear_interp(computed_time)

三次插值也能通過提供可選關鍵字參數kind來選擇:[^4]

In [8]: cubic_interp = interp1d(measured_time, measures, kind='cubic')
In [9]: cubic_results = cubic_interp(computed_time)

結果現在被集合在以下Matplotlib圖像中

scipy.interpolate.interp2d與scipy.interpolate.interp1d相似,但是面向二維數組。注意,對interp族,計算時間必須在測量時間範圍內。參見Maximum wind speed prediction at the Sprogø station的總結練習獲得更高級的插值示例。

九、數值積分:scipy.integrate Fusy,

最通用的積分程序是scipy.integrate.quad():

In [10]: from scipy.integrate import quad
In [11]: res, err = quad(np.sin, 0, np.pi/2)
In [12]: np.allclose(res, 1)
Out[12]: True

In [13]: np.allclose(err, 1 - res)
Out[13]: True

其它可用的積分方案有fixed_quad, quadrature, romberg。

scipy.integrate也是用來積分常微分方程(ODE)的功能程序。特別是,scipy.integrate.odeint()是個使用LSODA(Livermore Solver for Ordinary Differential equations with Automatic method switching for stiff and non-stiff problems)通用積分器。參見ODEPACK Fortran library獲得更多細節。

odeint解決這種形式的一階ODE系統:

dy/dt = rhs(y1, y2, .., t0,...)

作爲簡介,讓我們解決ODEdy/dt = -2y,區間t = 0..4,初始條件y(t=0) = 1。首先函數計算導數的位置需要被定義:

In [17]: def calc_derivative(ypos, time, counter_arr):
   ....:     counter_arr += 1
   ....:     return -2 * ypos                                               
   ....: 

一個額外的參數counter_arr被添加,用來說明函數可能在單個時間步中被多次調用,直到解收斂。計數數組被定義成:

In [18]: counter = np.zeros((1,), dtype=np.uint16)
彈道將被計算:

In [19]: from scipy.integrate import odeint                                 
In [20]: time_vec = np.linspace(0, 4, 40)                                   
In [21]: yvec, info = odeint(calc_derivative, 1, time_vec,
   ....: args=(counter,), full_output=Tru)

因此導函數可以被調用40次(即時間步長數),

In [22]: counter
Out[22]: array([129], dtype=uint16)

十個最初的時間點(time step)每個的累積迭代次數,可以這樣獲得:

In [23]: info['nfe'][:10]
Out[23]: array([31, 35, 43, 49, 53, 57, 59, 63, 65, 69], dtype=int32)

注意到在第一個時間步的解需要更多的迭代。解yvec的軌道現在可以被畫出:

另一個使用scipy.integrate.odeint()的例子是一個阻尼彈簧-質點振盪器(二階振盪)。附加在彈簧上質點的位置服從二階常微分方程y'' + eps wo y' + wo^2 y= 0。其中wo^2 = k/m,k是彈簧常數,m是質量,eps=c/(2 m wo),c是阻尼係數。(譯者:爲什麼不用latex……)對於這個例子,我們選擇如下參數:

In [24]: mass = 0.5  # kg
In [25]: kspring = 4  # N/m
In [26]: cviscous = 0.4  # N s/m

所以系統將是阻尼振盪,因爲:

In [27]: eps = cviscous / (2 * mass * np.sqrt(kspring/mass))
In [28]: eps < 1
Out[28]: True

對於scipy,integrate.odeint()求解器,二階方程需要被轉化成一個包含向量Y =y,y'的兩個一階方程的系統。定義nu = 2 eps * wo = c / m和om = wo^2 = k/m很方便:

In [29]: nu_coef = cviscous /mass
In [30]: om_coef = kspring / mass

因此函數將計算速度和加速度通過:

In [31]: def calc_deri(yvec, time, nuc, omc):
   ....:     return (yvec[1], -nuc * yvec[1] - omc * yvec[0])
   ....: 

In [32]: time_vec = np.linspace(0, 10, 100)

In [33]: yarr = odeint(calc_deri, (1, 0), time_vec, args=(nu_coef, om_coef))

最終的位置和速度在如下Matplotlib圖像中顯示

Scipy中不存在偏微分方程(PDE)求解器,一些解決PDE問題的Python軟件包可以得到,像fipy和SfePy

(譯者注:Python科學計算中洛倫茲吸引子微分方程的求解

十、信號處理:scipy.signal

In [34]: from scipy import signal

scipy.signal.detrend():移除信號的線性趨勢:

In [35]: t = np.linspace(0, 5, 100)
In [36]: x = t + np.random.normal(size=100)
In [39]: import pylab as pl
In [40]: pl.plot(t, x, linewidth=3)
Out[40]: [<matplotlib.lines.Line2D at 0x3903c90>]

In [41]: pl.plot(t, signal.detrend(x), linewidth=3)
Out[41]: [<matplotlib.lines.Line2D at 0x3b38810>]
detrend

scipy.signal.resample():使用FFT重採樣n個點。

In [42]: t = np.linspace(0, 5, 100)
In [43]: x = np.sin(t)
In [44]: pl.plot(t, x, linewidth=3)
Out[44]: [<matplotlib.lines.Line2D at 0x3f08a90>]

In [45]: pl.plot(t[::2], signal.resample(x, 50), 'ko')
Out[45]: [<matplotlib.lines.Line2D at 0x3f12950>]
resample
  • Signal中有許多窗函數:scipy.signal.hamming(), scipy.signal.bartlett(), scipy.signal.blackman()…

  • Signal中有濾波器(中值濾波scipy.signal.medfilt(), 維納濾波scipy.signal.wiener()),但是我們將在圖像部分討論。

十一、圖像處理:scipy.ndimage

scipy中致力於圖像處理的子模塊是scipy,ndimage。

In [49]: from scipy import ndimage

圖像處理程序可以根據它們執行的操作類別來分類。

圖像的幾何變換:

改變方向,分辨率等

In [50]: from scipy import misc
In [51]: lena = misc.lena()
In [52]: shifted_lena = ndimage.shift(lena, (50, 50))
In [53]: shifted_lena2 = ndimage.shift(lena, (50, 50), mode='nearest')
In [54]: rotated_lena = ndimage.rotate(lena, 30)
In [55]: cropped_lena = lena[50:-50, 50:-50]
In [56]: zoomed_lena = ndimage.zoom(lena, 2)
In [57]: zoomed_lena.shape
Out[57]: (1024, 1024)

In [63]: pl.subplot(321)
Out[63]: <matplotlib.axes.AxesSubplot at 0x4c00d90>

In [64]: pl.imshow(lena, cmap=cm.gray)
Out[64]: <matplotlib.image.AxesImage at 0x493aad0>

In [65]: pl.subplot(322)
Out[65]: <matplotlib.axes.AxesSubplot at 0x4941a10>

In [66]: #等

圖像濾鏡

In [76]: from scipy import misc
In [77]: lena = misc.lena()
In [78]: import numpy as np
In [79]: noisy_lena = np.copy(lena).astype(np.float)
In [80]: noisy_lena += lena.std()*0.5*np.random.standard_normal(lena.shape)
In [81]: blurred_lena = ndimage.gaussian_filter(noisy_lena, sigma=3)
In [82]: median_lena = ndimage.median_filter(blurred_lena, size=5)
In [83]: from scipy import signal
In [84]: wiener_lena = signal.wiener(blurred_lena, (5,5))

許多其它scipy.ndimage.filters和scipy.signal中的濾鏡可以被應用到圖像中。


練習: 比較不同濾鏡圖像的直方圖


數學形態學

數學形態學是源於幾何論的數學形態學。它具有結合結構的特點並變換幾何結構。二值圖(黑白圖),特別能被用該理論轉換:要轉換的集合是鄰近的非零值像素。這個理論也被拓展到灰度圖中。

基本的數學形態操作使用一個結構元素(structuring element)來改變其它幾何結構。

讓我們首先生成一個結構元素:

In [129]: el = ndimage.generate_binary_structure(2, 1)
In [130]: el
Out[130]: 
array([[False,  True, False],
       [ True,  True,  True],
       [False,  True, False]], dtype=bool)

In [131]: el.astype(np.int)
Out[131]: 
array([[0, 1, 0],
       [1, 1, 1],
       [0, 1, 0]])
  • 腐蝕
In [132]: a = np.zeros((7,7), dtype=int)
In [133]: a[1:6, 2:5] = 1
In [134]: a
Out[134]: 
array([[0, 0, 0, 0, 0, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 0, 0, 0, 0, 0, 0]])

In [135]: ndimage.binary_erosion(a).astype(a.dtype)
Out[135]: 
array([[0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 0, 1, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0]])


# 腐蝕移除對象使結構更小
In [136]: ndimage.binary_erosion(a, structure=np.ones((5,5))).astype(a.dtype)
Out[136]: 
array([[0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0]])
  • 膨脹
In [137]: a = np.zeros((5,5))
In [138]: a[2, 2] = 1
In [139]: a
Out[139]: 
array([[ 0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.]])

In [140]: ndimage.binary_dilation(a).astype(a.dtype)
Out[140]: 
array([[ 0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.],
       [ 0.,  1.,  1.,  1.,  0.],
       [ 0.,  0.,  1.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.]])
  • 開操作(opening)
In [141]: a = np.zeros((5,5), dtype=np.int)

In [142]: a[1:4, 1:4] = 1; a[4, 4] = 1

In [143]: a
Out[143]: 
array([[0, 0, 0, 0, 0],
       [0, 1, 1, 1, 0],
       [0, 1, 1, 1, 0],
       [0, 1, 1, 1, 0],
       [0, 0, 0, 0, 1]])

# 開操作可以移除小的對象
In [145]: ndimage.binary_opening(a, structure=np.ones((3,3))).astype(np.int)Out[145]: 
array([[0, 0, 0, 0, 0],
       [0, 1, 1, 1, 0],
       [0, 1, 1, 1, 0],
       [0, 1, 1, 1, 0],
       [0, 0, 0, 0, 0]])

# 開操作也能平滑邊角
In [147]: ndimage.binary_opening(a).astype(np.int)
Out[147]: 
array([[0, 0, 0, 0, 0],
       [0, 0, 1, 0, 0],
       [0, 1, 1, 1, 0],
       [0, 0, 1, 0, 0],
       [0, 0, 0, 0, 0]])
  • 閉操作(closing): ndimage.binary_closing

練習: 查看開操作腐蝕,然後膨脹的量。

一個開操作移除小的結構,而一個閉操作填補小的空洞。這種操作因此可被用來“清理”圖像。

In [149]: a = np.zeros((50, 50))
In [150]: a[10:-10, 10:-10] = 1
In [151]: a += 0.25*np.random.standard_normal(a.shape)
In [152]: mask = a>=0.5
In [153]: opened_mask = ndimage.binary_opening(mask)
In [154]: closed_mask = ndimage.binary_closing(opened_mask)

練習: 驗證重構區域比初始區域更小。(如果閉操作在開操作之前則相反)

對灰度值圖像,腐蝕(或者是膨脹)相當於用被集中在所關心像素點的結構元素所覆蓋像素的最小(或最大)值替代當前像素點。

In [173]: a = np.zeros((7,7), dtype=np.int)
In [174]: a[1:6, 1:6] = 3
In [175]: a[4,4] = 2; a[2,3] = 1
In [176]: a
Out[176]: 
array([[0, 0, 0, 0, 0, 0, 0],
       [0, 3, 3, 3, 3, 3, 0],
       [0, 3, 3, 1, 3, 3, 0],
       [0, 3, 3, 3, 3, 3, 0],
       [0, 3, 3, 3, 2, 3, 0],
       [0, 3, 3, 3, 3, 3, 0],
       [0, 0, 0, 0, 0, 0, 0]])

In [177]: ndimage.grey_erosion(a, size=(3,3))
Out[177]: 
array([[0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 0, 1, 1, 1, 0, 0],
       [0, 0, 3, 2, 2, 0, 0],
       [0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0, 0]])

圖像測量

讓我們首先生成一個漂亮的合成圖像:

In [178]: x, y = np.indices((100, 100))
In [179]: sig = np.sin(2*np.pi*x/50.)*np.sin(2*np.pi*y/50.)*(1+x*y/50.**2)**2

In [180]: mask = sig > 1 

#現在我們查找圖像中對象的各種信息:
In [181]: labels, nb = ndimage.label(mask)
In [182]: nb
Out[182]: 8

In [183]: areas = ndimage.sum(mask, labels, xrange(1, labels.max()+1))
In [184]: areas
Out[184]: array([ 190.,   45.,  424.,  278.,  459.,  190.,  549.,  424.])

In [185]: maxima = ndimage.maximum(sig, labels, xrange(1, labels.max()+1))
In [186]: maxima
Out[186]: 
array([  1.80238238,   1.13527605,   5.51954079,   2.49611818,
         6.71673619,   1.80238238,  16.76547217,   5.51954079])

In [187]: ndimage.find_objects(labels==4)
Out[187]: [(slice(30L, 48L, None), slice(30L, 48L, None))]

In [188]: sl = ndimage.find_objects(labels==4)
In [189]: import pylab as pl
In [190]: pl.imshow(sig[sl[0]])  
Out[190]: <matplotlib.image.AxesImage at 0xb2fdcd0>

參見總結練習Image processing application: counting bubbles and unmolten grains獲取更多高級示例。



 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章