Sunday, December 18, 2011

以 Python 做測試驅動開發 (Test-driven development) 練習 -- 測試一個多邊形是否近似於四角形 (一)

這次用Python來練習驅動程式開發。
TDD簡單的說只有三步
1. 寫好程式的介面,只要能夠通過編譯即可
2. 增加測試
3. 執行測試,如果有失敗的測試的話就修改程式,然後回到步驟2

我要寫的程式是輸入一個多邊形的頂點,檢查是否為近似四邊形。
這個問題可以很簡單,我們只需要檢查多邊形的頂點數目,
如果是四個點的話就是四邊形。
當然我希望這個程式的能力不只如此,不然就沒有練習的效果啦。
其實這個問題是從實際的問題演變過來的,實際上是應用在擴增實境中,
我們從攝影機抓取影像,然後找出影像中的正方形,做為一個參考點。
一個平片上的正方形從不同角度去看,會變成一個任意的四邊形,
我們從影像分割出許多多邊形,然後判斷像不像四邊形。然而由於影像處理必定會有誤差,
所以一個四邊形可能有一些凸出的部分,而會變成五角形或六角形。我們的程式須要能處理這種情況。
這也是採用TDD的理由,我們可以一步一步改進我們的程式。

開始囉
首先定義函數IsQuadrilateral,輸入為一個多邊形,輸出真假值。由於現在還沒有測試,
不假思索就可以得到一個最簡單的實作方法,一律傳回假就好。
def IsQuadrilateral(polygon):
  return False

接著是增加測試,利用Python的unittest模組,我們將測試程式包在TestSequenceFunctions物件中。

import unittest
import numpy
class TestSequenceFunctions(unittest.TestCase):
  def setUp(self):
    self.triangle   = numpy.array( [ (0,1,0),(0,0,1)  ] )
    self.square     = numpy.array( [ (0,1,1,0),(0,0,1,1)  ] )
  def testIsQuadrilateral(self):
    self.assertEqual(IsQuadrilateral(self.triangle),    False)
    self.assertEqual(IsQuadrilateral(self.square),      True)
if __name__ == '__main__':
    unittest.main()

setUp函數是用來建立測試用的資料,這邊我們建立了兩個二維陣列來表示三角形和正方形的頂點。
numpy是常用的數學函式庫,程式中的 [ (0,1,0),(0,0,1)  ] 表示一個二維陣列,第一列是三個頂點的x座標 0,1,0,
第二列三個頂點的y座標 (0,0,1)。square物件存放的是正方形,有四個頂點。

完成以後就是執行測試。結果在第二個測試失敗了,所以我們要回頭修改程式。第二版的程式如下:檢查傳入的多邊形是否含有四個頂點,
有的話就為真,否則為假。

def IsQuadrilateral(polygon):
    x,y = polygon
    if len(x) != 4 or len(x)!=len(y):
        return False
    return True

現在回頭再跑測試,就可以過關啦。當然我們不能停在這裡,要繼續加測試才行。

五角形1:
self.pentagon1 = numpy.array( [ (0,1,1,0,-1),(0,0,1,1,0.5)  ] )
五角形2:
self.pentagon2 = numpy.array( [ (0,1,1,0,-0.01),(0,0,1,1,0.5)  ] )
五角形3:
self.pentagon3 = numpy.array( [ (0,1,1,0,-0.21),(0,0,1,1,0.5)  ] )
此處我們用了三個五角形,長相如下:

其中第二個五角形其實是四角形,只是左邊有一點小突出而已,我們的新測試如下:

def testIsQuadrilateral(self):
    self.assertEqual(IsQuadrilateral(self.triangle),   False)
    self.assertEqual(IsQuadrilateral(self.square),     True)
    self.assertEqual(IsQuadrilateral(self.pentagon1),  False)
    self.assertEqual(IsQuadrilateral(self.pentagon2),  True)
    self.assertEqual(IsQuadrilateral(self.pentagon3),  False)

修改演算法,四邊形是兩個三角形所組成,所以我們先嘗試找到四邊形的兩個頂點。這裡我用了一個經驗法則,先任意選取polygon上的一個點p0,然後選取離p0最遠的點p1,接著選取離p1最遠的點p2,如此一來p1和p2就是近似四邊形上的兩個點。p1p2的連線把polygon分成兩半,我們接下來只需要判斷這兩半是否為三角形即可。請見下面的圖。

為了要找到離p0最遠的點,我需要再寫一個函數FindFarthestPoint,同時需要新的測試來驗證這新的函數。
今天就先寫到這裡吧。來回顧一下到目前為止我們的程式看起來如何:

import unittest
import numpy

# 主函數
def IsQuadrilateral(polygon):
    x,y = polygon
    if len(x) != 4 or len(x)!=len(y):
        return False
    return True

# 測試程式
class TestSequenceFunctions(unittest.TestCase):
    # 此函數在 testIsQuadrilateral 被執行前會被呼叫,以建立測試所需的物件
    # 想一想為何需要一個獨立的 setUp 函數呢?
    def setUp(self):
        self.triangle   = numpy.array( [ (0,10,0),(0,0,10)  ] )
        self.square     = numpy.array( [ (0,1,1,0),(0,0,1,1)  ] )
        self.pentagon1  = numpy.array( [ (0,1,1,0,-1),(0,0,1,1,0.5)  ] )
        self.pentagon2  = numpy.array( [ (0,1,1,0,-0.01),(0,0,1,1,0.5)  ] ) # 這個測試會失敗
        self.pentagon3  = numpy.array( [ (0,1,1,0,-0.21),(0,0,1,1,0.5)  ] )

    def testIsQuadrilateral(self):
      self.assertEqual(IsQuadrilateral(self.triangle),   False)
      self.assertEqual(IsQuadrilateral(self.square),     True)
      self.assertEqual(IsQuadrilateral(self.pentagon1),  False)
      self.assertEqual(IsQuadrilateral(self.pentagon2),  True)
      self.assertEqual(IsQuadrilateral(self.pentagon3),  False)

if __name__ == '__main__':
    unittest.main()  # 會呼叫TestSequenceFunctions裡所有的測試函數

No comments:

Post a Comment