Monday, December 26, 2011

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

回顧一下前篇文章中提到測試驅動開發(TDD)的三步驟:

1. 寫好程式的介面,只要能夠通過編譯即可
2. 增加測試
3. 執行測試,如果有失敗的測試的話就修改程式,然後回到步驟2

回到我們目前的IsQuadrilateral函數,目前的實作非常簡單,只是檢查傳入的多邊形是否有四個頂點,若是的話就傳回真。現在要來處理其他狀況:長的像四角形的五角形(或更多角)。解法如前篇所說,我們先取多邊形的第一個點做為p0,然後找出最遠的點p1,接著找離p1最遠的點p0。我們把這件工作交給FindFarthestPoint,此函數接受一個點p和一個集合S,傳回在集合S中離p最遠的點。這時我們依照TDD的精神,加上個測試先。testFindFarthestPoint利用我們之前定義好的square,測試離左下角最遠的點是否為右上角。

# 要放到 class TestSequenceFunctions 裡面
def testFindFarthestPoint(self):
    self.assertTrue( numpy.array_equal(FindFarthestPoint(numpy.array([0,0]), self.square)[0], numpy.array([1,1])) )
    self.assertTrue( numpy.array_equal(FindFarthestPoint(numpy.array([0,2]), self.square)[0], numpy.array([1,0])) )

def FindFarthestPoint(p, polygon):
    x = polygon[0,]-p[0]
    y = polygon[1,]-p[1]
    idx = numpy.argmax(x**2 + y**2)
    return polygon[:,idx], idx

好了,雖然繞了些路,終於萬事俱備,可以來寫判斷三角形的函數了。由於我們已經把多邊形切成兩半,各自是兩群連續的點的集合。我們可以想像從p1p2的連線,如果途中有一個凸出的點,p1到p2就會是一個三角形(或者更多角)。我使用CountMiddleVertex來計算一群連續的點之中有幾個凸出的點。這會是一個遞迴函數,先找到最突出的點p3,然後用p3切割線段,然後分別呼叫CountMiddleVertex繼續計算。

# 要放到 class TestSequenceFunctions 裡面
def testCountVertex(self):
    self.assertEqual(CountMiddleVertex(self.square2),   1)
    self.assertEqual(CountMiddleVertex(self.triangle),  1)
    self.assertEqual(CountMiddleVertex(self.square),    2)
    self.assertEqual(CountMiddleVertex(self.pentagon1), 3)
    self.assertEqual(CountMiddleVertex(self.pentagon2), 3)
    self.assertEqual(CountMiddleVertex(self.hexagon),   4)

def CountMiddleVertex(pointset, threshold=0.03):
    pointnum = pointset.shape[1]
    if pointnum <=2:
        return 0
    p1 = pointset[:, 0]
    p2 = pointset[:, -1]
    dist2 = (p1[0]-p2[0])*(p1[0]-p2[0]) + (p1[1]-p2[1])*(p1[1]-p2[1]) 
    triarea = []
    for p in range(pointset.shape[1]):
        triarea.append(PolygonArea(numpy.column_stack((p1,p2,pointset[:,p]))))
    # 如果最突出的點超過門檻值
    if max(triarea)*1.0/dist2 > threshold:
        midpoint = triarea.index(max(triarea))
        vertexnum = 1 + CountMiddleVertex(pointset[:, 0:midpoint+1])
            + CountMiddleVertex(pointset[:, midpoint:], maxnum-vertexnum);
    return 0

CountMiddleVertex測試完成後,我們就利用它來判定三角形吧。更新後的IsQuadrilateral以及完整程式如下。為了讓CountMiddleVertex執行快一些,我加了一個參數maxnum,讓它可以早點結束。

import numpy
import unittest

def PolygonArea(polygon):
    x,y = numpy.column_stack((polygon, numpy.array([polygon[0,0],polygon[1,0]])))
    return abs((numpy.dot(x[0:-1],y[1:]) - numpy.dot(x[1:],y[0:-1])))/2.0 
    
def FindFarthestPoint(p, polygon):
    x = polygon[0,]-p[0]
    y = polygon[1,]-p[1]
    idx = numpy.argmax(x**2 + y**2)
    return polygon[:,idx], idx

def CountMiddleVertex(pointset, maxnum=10, threshold=0.03):
    pointnum = pointset.shape[1]
    if pointnum <=2:
        return 0
    p1 = pointset[:, 0]
    p2 = pointset[:, -1]
    dist2 = (p1[0]-p2[0])*(p1[0]-p2[0]) + (p1[1]-p2[1])*(p1[1]-p2[1]) 
    triarea = []
    for p in range(pointset.shape[1]):
        triarea.append(PolygonArea(numpy.column_stack((p1,p2,pointset[:,p]))))
    if max(triarea)*1.0/dist2 > threshold:
        midpoint = triarea.index(max(triarea))
        vertexnum = 1 + CountMiddleVertex(pointset[:, 0:midpoint+1]);
        if vertexnum >= maxnum:
            return maxnum
        return vertexnum + CountMiddleVertex(pointset[:, midpoint:], maxnum-vertexnum);
    return 0

def IsQuadrilateral(polygon):
    x,y = polygon
    
    if len(x) < 4 or len(x)!=len(y):
        return False
   
    p1, p1idx = FindFarthestPoint(polygon[:,0], polygon)
    p2, p2idx = FindFarthestPoint(p1, polygon)
    
    if p1idx > p2idx:
        p1idx,p2idx = p2idx,p1idx
        p1,p2 = p2,p1
    
    if CountMiddleVertex(polygon[:, p1idx:p2idx+1], 2)==1 and \
        CountMiddleVertex(numpy.column_stack((polygon[:, p2idx:], polygon[:, :p1idx+1])), 2)==1:
        return True
    else:
        return False
    
class TestSequenceFunctions(unittest.TestCase):
    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.square2    = numpy.array( [ (0,0.5,1,0),(0,-0.01,0,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)  ] )
        self.hexagon    = numpy.array( [ (1,1,0,-1,-1,0),(1,-1,-2,-1,1,2)  ] )
        
    def testFindFarthestPoint(self):
        self.assertTrue( numpy.array_equal(FindFarthestPoint(numpy.array([0,0]), self.square)[0], numpy.array([1,1])) )
        self.assertTrue( numpy.array_equal(FindFarthestPoint(numpy.array([0,2]), self.square)[0], numpy.array([1,0])) )
        
    def testCountVertex(self):
        self.assertEqual(CountMiddleVertex(self.square2),   1)
        self.assertEqual(CountMiddleVertex(self.triangle),  1)
        self.assertEqual(CountMiddleVertex(self.square),    2)
        self.assertEqual(CountMiddleVertex(self.pentagon1), 3)
        self.assertEqual(CountMiddleVertex(self.pentagon2), 3)
        self.assertEqual(CountMiddleVertex(self.hexagon),   4)
    
    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)
        self.assertEqual(IsQuadrilateral(self.hexagon),    False)

if __name__ == '__main__':
    unittest.main()

No comments:

Post a Comment