日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人工智能 > 目标检测 >内容正文

目标检测

黑窗口检测wamp的命令_OpenCV AdaBoost + Haar目标检测技术内幕(上)

發布時間:2024/3/24 目标检测 110 豆豆
生活随笔 收集整理的這篇文章主要介紹了 黑窗口检测wamp的命令_OpenCV AdaBoost + Haar目标检测技术内幕(上) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

很多使用過OpenCV的小伙伴都見過如下代碼。這段看似簡單的代碼,通過讀入一個神奇的XML文件,能夠找到圖像中所有人臉,是不是非常神奇?這其實就是一個Adaboost級聯分類器的經典實現;但是當你希望深入代碼學習原理時,估計會被代碼復雜度嚇到。

#include

代碼結果圖0-1這樣:

圖0-1

希望這篇文章能幫助你了解背后的所有原理,不再迷茫。文章很長,做好心理準備。

特別聲明一下,本文成文于2015年,講解的是OpenCV2.4.11的實現方式。由于OpenCV版本更新,不排除有一些細節變化,請讀者注意自行區分。

盡信書不如無書!

1 Haar特征

在OpenCV接口中,實現了Haar/LBP/HOG等多種特征,本文以Haar特征為例介紹。

1.1 Haar特征的生成

Haar特征最先由Paul Viola等人提出,后經過Rainer Lienhart等擴展引入45°傾斜特征,成為現在OpenCV所使用的的樣子。圖1-1展示了目前OpenCV(2.4.11版本)所使用的共計14種Haar特征,包括5種Basic特征、3種Core特征和6種Titled(即45°旋轉)特征。

圖1-1 OpenCV中使用的的Haar特征

在實際中,Haar特征可以在檢測窗口中由放大+平移產生一系列子特征,但是白:黑區域面積比始終保持不變。

如圖1-2,以x3特征為例,在放大+平移過程中白:黑:白面積比始終是1:1:1。首先在紅框所示的檢測窗口中生成大小為3個像素的最小x3特征;之后分別沿著x和y平移產生了在檢測窗口中不同位置的大量最小3像素x3特征;然后把最小x3特征分別沿著x和y放大,再平移,又產生了一系列大一點x3特征;然后繼續放大+平移,重復此過程,直到放大后的x3和檢測窗口一樣大。這樣x3就產生了完整的x3系列特征。

圖1-2 x3特征平移+放大產生一系列子特征示意圖

那么這些通過放大+平移的獲得的子特征到底總共有多少個?Rainer Lienhart在他的論文中給出了完美的解釋:假設檢測窗口大小為W*H,矩形特征大小為w*h,X和Y為表示矩形特征在水平和垂直方向的能放大的最大比例系數:

圖1-3 特征數量計算示意圖

則如圖1-3,在檢測窗口Window中,一般矩形特征(upright rectangle)的數量為:

簡單解釋一下,上述公式可以理解為:

  • 特征框豎直放大1倍,即無放大,豎直方向有(H-h+1)個特征
  • 特征框豎直放大2倍,豎直方向有(H-2h+1)個特征
  • 特征框豎直放大3倍,豎直方向有(H-3h+1)個特征
  • 如此到豎直放大Y=floor(H/h)倍,豎直方向有1個特征,即(H-Y*h+1)
  • 那么豎直方向總共有

    個特征。考慮到水平和豎直方向縮放是獨立的,所以能得到上述公式。對應于之前的x3特征,當x3特征在24*24大小的檢測窗口中時(此時W=H=24,w=3,h=1,X=8,Y=24),一共能產生27600個子特征,除x3外其他一般矩形特征數量計算方法類似。

    1.2 如何計算Haar特征值

    看到這里,該明白了大量的Haar特征是如何產生的。當有了大量的Haar特征用于訓練和檢測時,接下來的問題是如何計算Haar特征值。按照OpenCV代碼,Haar特征值=整個Haar區域內像素和×權重 + 黑色區域內像素和×權重:

  • 對于圖2中的x3和y3特征, = 1, = -3;
  • 對于point特征, = 1, = -9;
  • 其余11種特征均為 =1, = -2。
  • 這也就是其他文章中提到的所謂“白色區域像素和減去黑色區域像素和”,只不過是加權相加而已。例如:

    • 對于x2特征:(黑 + 白) * 1+黑 * (-2) = 白 - 黑;
    • 對于Point特征:(黑 + 白) * 1 + 黑 * (-9) = 白 - 8 * 黑。

    為什么要設置這種加權相減,而不是直接相減?請仔細觀察圖2中的特征,不難發現x3、y3、point特征黑白面積不相等,而其他特征黑白面積相等。設置權值就是為了抵消面積不等帶來的影響,保證所有Haar特征的特征值在灰度分布絕對均勻的圖中為0。

    了解了特征值如何計算之后,再來看看不同的特征值的含義是什么。我選取了MIT人臉庫中2706個大小為20*20的人臉正樣本圖像,計算如圖1-4位置的Haar特征值,結果如圖1-5。

    圖1-4 Haar特征位置示意圖

    圖1-5 圖4的2個Haar特征在MIT人臉樣本中特征值分布圖(左邊特征結果為紅色,右邊藍色)

    可以看到,圖1-4中2個不同Haar特征在同一組樣本中具有不同的特征值分布,左邊特征計算出的特征值基本都大于0,而右邊特征的特征值基本均勻分布于0兩側(分布越均勻對樣本的區分度越小)。所以,正是由于樣本中Haar特征值分布不同,導致了不同Haar特征分類效果不同。顯而易見,對樣本區分度越大的特征分類效果越好,即紅色曲線對應圖1-4中的的左邊Haar特征分類效果好于右邊Haar特征。那么看到這里,應該理解了下面2個問題:

  • 在檢測窗口通過平移+放大可以產生一系列Haar特征,這些特征由于位置和大小不同,分類效果也各異;
  • 通過計算Haar特征的特征值,可以有將圖像矩陣映射為1維特征值,有效實現了降維
  • 1.3 Haar特征如何保存?

    對應的,在OpenCV XML文件中,每一個Haar特征都被保存在2~3個形如:

    <x y width height weight>

    的標簽中,其中x和y代表Haar矩形左上角坐標(以檢測窗口左上角為原點),width和height代表矩形的寬和高,而weight則對應了上面說的權重值,例如圖4中的左邊x2類型的Haar特征應該為<4 2 12 8 1.0>(整個Haar,權重1)和<4 2 12 4 -2.0>(黑色區域,權重-2)。

    1.4 Haar特征值標準化

    從上文圖1-5中發現,僅僅一個12*18大小的Haar特征計算出的特征值變化范圍從-2000~+6000,跨度非常大。這種跨度大的特性不利于量化評定特征值,所以需要進行“標準化”,壓縮特征值范圍。假設當前檢測窗口中的圖像為i(x,y),當前檢測窗口為w*h大小(例如圖6中為20*20大小),OpenCV采用如下方式“標準化”:

    • 計算檢測窗口中間部分(w-2)*(h-2)的圖像的灰度值和灰度值平方和:

    • 計算平均值:

    • 計算標準化因子:

    • 標準化特征值:

    具體代碼在cascadedetect.cpp中的HaarEvaluator::setImage()函數中可以看到,關鍵部分如下:

    normrect

    與代碼對應的,如圖中藍色為檢測窗口,紅色為標準化過程中使用到的像素。

    圖1-6

    其實如何標準化并不重要,重要的是檢測和訓練時的方法一定要一致,否則可能會由于標準化不同帶來的誤差導致模型無法工作!

    1.5 積分圖

    以OpenCV自帶的人臉分類器haarcascade_frontalface_alt2.xml為例,其中存儲了超過1000個大小和位置都不相同的Haar特征(XML文件解釋見下節)。在運算中,伴隨著檢測窗口的移動,如何快速計算Haar特征值就成了一個很重要的問題。在設計Haar+AdaBoost算法時,Paul Viola等人就提出積分圖。

    對于灰度圖像中任意一點image(x,y),OpenCV定義其積分圖為sum(x,y)為:

    其中第0行和第0列為0:

    其中image(x,y)為點(x,y)處的原始灰度圖。這樣就定義了一張類似于數學中“積分”的積分圖。如圖1-7,如果要計算D區域內灰度和,只需計算

    其中(x1,y1)、(x2,y2)、(x3,y3)和(x4,y4)依次代表圖1中image的1 2 3 4點的圖像坐標。需要說明,在計算D區域灰度和時sum(x1,y1)深藍色區域被減去了2次,最后需要補上。顯然可以通過此方法快速計算圖像中任意位置和大小區域的灰度和,即通過積分圖只需要做有限次操作就能獲得任意位置的Haar特征值

    圖1-7 積分圖計算Haar矩形框示意圖

    1.6 旋轉積分圖

    為了提高檢測精度,Rainer Lienhart等人首先提出了45°旋轉積分圖,如圖1-8。旋轉積分圖用于快速計算圖1中的titled_x2和titled_y2等共6種旋轉Haar特征。

    圖1-8 45°旋轉積分圖

    與一般積分圖類似,OpenCV中45°旋轉積分圖同樣采用了“擴邊”方式(即旋轉積分圖比原灰度圖多1行和1列,其中第1行和第1列元素為0),對應的計算公式為:

    其中第一行第一列為0:

    有了旋轉積分圖如何計算任意位置和大小的45°傾斜長方形區域的灰度和呢?

    設有如圖9紅色方框大小的灰度圖image,其計算出來的45°旋轉灰度圖為titled(第0行和第0列為0),虛線代表image中cv::Rect為<3 1 2 3>區域。顯然虛線區域的灰度和為:

    應該不難理解。

    圖1-9

    在實際中,如果使用旋轉特征,則需要多計算一張積分圖。但是旋轉特征的效果往往不理想,得不償失,不建議使用。

    PS:45°旋轉旋轉積分圖還有另外一種實現方式,但是OpenCV由于數據存儲方式限定,不那樣做。

    圖1-10

    2 級聯分類器結構

    了解Haar特征之后,接下來分析級聯分類器結構,主要包括以下2個內容:

  • OpenCV中的Adaboost級聯分類器的結構,包括強分類器和弱分類器的形式;
  • OpenCV自帶的XML分類器中各項參數的含義,如internalNodes和leafValues標簽里面的一大堆數字的意義。
  • OpenCV中的Adaboost級聯分類是樹狀結構,如圖2-1,其中每一個stage都代表一級強分類器。當檢測窗口通過所有的強分類器時才被認為是目標,否則拒絕。實際上,不僅強分類器是樹狀結構,強分類器中的每一個弱分類器也是樹狀結構。

    圖2-1 強分類器和弱分類器示意圖(此圖有誤,應是stage1,stage2,...,stageN)

    這篇文章將結合OpenCV-2.4.11中自帶的haarcascade_frontalface_alt2.xml文件介紹整個級聯分類器的結構。需要說明,自從2.4.11版本后所有分類器都被替換成新式XML,所以本文介紹新式XML結構。

    2.1 XML的頭部

    在了解OpenCV分類器結構之前,先來看看存儲分類器的XML文件中有什么。圖2中注釋了分類器XML文件頭部信息,括號中的參數為opencv_traincascade.exe訓練程序對應參數。

    圖2-2 分類器XML文件頭部含義

    其中<features>標簽存儲了所有的Haar特性,在之前1.3節有講解。

    2.2 弱分類器結構

    Haar特征和弱分類器之間的關系很簡單:

    一個完整的弱分類器包括:

  • 若干個Haar特征 + 和Haar特征數量相等的弱分類器閾值
  • 若干個leftValue
  • 若干個rightValue
  • 這些元素共同構成了弱分類器,缺一不可。haarcascade_frontalface_alt2.xml的弱分類器Depth=2,包含了2種形式,如圖2-3:

    • 左邊形式包含2個Haar特征、1個leftValue、2個rightValue和2個弱分類器閾(t1和t2)
    • 右邊形式包括2個Haar特征、2個leftValue、1個rightValue和2個弱分類器閾值

    圖2-3 Depth=2的樹狀弱分類器示意圖

    看圖2-3應該明白了弱分類器的大致結構,接下來我們了解樹狀弱分類器是如何工作的。還是以圖3左邊的形式為例:

  • 計算第一個Haar特征的特征值haar1,與第一個弱分類器閾值t1對比,當haar1<t1時,進入步驟2;當haar1>t1時,該弱分類器輸出rightValue2并結束。
  • 計算第二個Haar特征值haar2,與第二個弱分類器閾值t2對比,當haar2<t2時輸出leftValue;當haar2>t2時輸出rightValue1。
  • 即通過上述步驟計算弱分類器輸出值,這與OpenCV的cascadedetect.hpp文件中的predictOrdered()函數代碼對應(這里簡單解釋一下,在OpenCV中所有弱分類器的leftValue和rightValue都依次存儲在一個一維數組中,代碼中的leafOfs表示當前弱分類器中leftValue和rightValue在該數組中存儲位置的偏移量,idx表示在偏移量leafOfs基礎上的leftValue和rightValue值的索引,cascadeLeaves[leafOfs - idx]就是該弱分類器的輸出):

    do

    即弱分類器的工作方式:通過計算出的Haar特征值與弱分類器閾值對比,從而選擇最終輸出leftValue和rightValue值中的哪一個。

    那么這些Haar特征、leftValue、rightValue和弱分類器閾值t都是如何存儲在xml文件中的?不妨來看haarcascade_frontalface_alt2.xml文件中的第一級的第三個弱分類器,如圖2-4。圖2-4中的弱分類器恰好是圖3中左邊類型,包含了<internalNodes>和<leafValues>兩個標簽。其中<leafValues>標簽中的3個浮點數由左向右依次是rightValue2、leftValue和rightValue1;而<internalNodes>中有6個整數和2個浮點數,其中2個浮點數依次分別是弱分類器閾值t1和t2,剩下的6個整數容我慢慢分解。

    首先來看兩個浮點數前的整數,即4和5。這兩個整數用于標示所屬本弱分類器Haar特征存儲在<features>標簽中的位置。比如數值4表示該弱分類器的haar1特征存儲在xml文件下面<features>標簽中第4個位置,即為:

    而<internalNodes>的其他4個整數1、0和-1、-2則用于控制弱分類器樹的形狀。在運行時,OpenCV會把1賦值給當前的node.left,并把0賦值給node.right(請注意do-while代碼中的條件,只有idx<=0時才停止循環,參考圖3應該可以理解這4個整數的含義)。如此,OpenCV通過這些巧妙的數值和結構,控制了整個分類器的運行。可以看到,每個弱分類器內部都是類似于這種樹狀的“串聯”結構,所以我稱其為“串聯組成的的弱分類器”。

    圖2-4 OpenCV弱分類器運行示意圖

    而Depth=1(如haarcascade_frontalface_alt.xml)類型的弱分類器,結構更加簡單且運行方式對比可知,不在贅述。

    圖2-5 Depth=1的stump弱分類器示意圖

    2.3 強分類器結構

    在OpenCV中,強分類器是由多個弱分類器“并列”構成,即強分類器中的弱分類器是兩兩相互獨立的。在檢測目標時,每個弱分類器獨立運行并輸出cascadeLeaves[leafOfs - idx]值,然后把當前強分類器中每一個弱分類器的輸出值相加,即:

    sum

    圖2-6 OpenCV強分類器運行示意圖

    之后與本級強分類器的stageThreshold閾值對比,當且僅當結果sum>stageThreshold時,認為當前檢測窗口通過了該級強分類器。當前檢測窗口通過所有強分類器時,才被認為是一個檢測目標。可以看出,強分類器與弱分類器結構不同,是一種類似于“并聯”的結構,我稱其為“并聯組成的強分類器”。

    2.4 如何搜索目標?

    還有一個問題:檢測窗口大小固定(例如alt2是20*20像素)的級聯分類器如何遍歷圖像,以便找到在圖像中大小不同、位置不同的目標?

    解決方法如下:為了找到圖像中不同位置的目標,需要逐次移動檢測窗口(窗口中的Haar特征相應也隨著移動),這樣就可以遍歷到圖像中的每一個位置;而為了檢測到不同大小的目標,一般有兩種做法:逐步縮小圖像or逐步放大檢測窗口:

  • 縮小圖像就是把圖像按照一定比例逐步縮小然后滑動窗口檢測,如圖2-7;
  • 放大檢測窗口是把檢測窗口長寬按照一定比例逐步放大,這時位于檢測窗口內的Haar特征也會對應放大,然后檢測。
  • 圖2-7 經典的Pyramid+Sliding-window檢測(借用一張大佬的照片)

    新版c++函數CascadeClassifier::detectMultiScale()只實現了縮小圖像檢測;舊版的c函數cvHaarDetectObject()同時實現了縮小圖像和放大窗口兩種檢測方式,當函數參數flag為CV_HAAR_SCALE_IMAGE時是縮小圖像檢測,默認flag=0時放檢測大窗口檢測。

    #define CV_HAAR_SCALE_IMAGE 2

    3 對檢測結果NMS

    考慮這樣的情況:一個被檢測為目標的窗口,其附近窗口也應該被檢測到。

    圖3-1展示檢測一副含有人臉圖像的結果,左邊為合并檢測結果窗口之前的結果,右邊為合并之后的結果。所以有必要對重疊的檢測結果窗口進行合并,同時剔除零散分布的錯誤檢測窗口。該功能其實就是NMS(non-maximum suppression)。

    圖3-1 檢測結果合并窗口前后對比圖

    3-1 并查集(Union-Set)

    在了解如何合并窗口前,先來了解一種數據結構——并查集。為了形象的說明并查集,首先來了解一個例子。江湖上存在各種各樣的大俠,他們沒什么正當職業,整天背著劍四處游蕩,碰到其他大俠就大打出手。俗話說“雙拳難敵四手”,這些大俠都會拉幫結派壯大實力。那么為了辨識每個大俠屬于哪個幫派,就需要每個幫派都推舉一個“老大”。這些大俠只需要知道自己和其他大俠的老大是不是同一個人,就能明白自己和對方是不是一個幫派,從而決定是否動手過招。

    圖3-2 江湖大俠關系圖(箭頭表示上一級)

    如圖3-2,現在武當和明教分別推舉張三豐和張無忌為老大。當幫派很大時,每個大俠記不住幫派所有人,那么他們只需要記住自己的上一級是誰,一級一級往上問就知道老大是誰了。某日,宋青書和殷梨亭在武當山門遇到,那么宋青書問宋遠橋后得知自己的老大是張三豐,而殷梨亭的老大也是張三豐,那么他倆是同門。反之宋青書遇到陳友諒時,一級一級向上詢問后發現老大不是一個人,就不是同門。

    除此之外,在武林中還需要組建聯盟擴大幫派勢力。既然楊不悔嫁給了殷梨亭,不妨直接設張無忌的上級為張三豐,這樣就可以將明教和武當組成一個更大的聯盟(如圖3-2紅色虛線。需要說明,我們只關心數據的代表,而忽略數據內部結構)。從此以后當宋青書再和楊不悔相遇,一級一級查詢后可以確定是同伙了。但是如果大俠們相遇都像這樣一級一級往上問,查詢路徑很長。所以這種直接連接最上級的方法不是最優。

    為了解決這個問題,需要壓縮路徑——每個人記住自己最終的老大就行(如宋青書記住自己老大是張三豐,不在去問宋遠橋),基本思路如下:

    • 以武當為例,張三豐創建門派(明教也類似)
    • 宋遠橋和殷梨亭加入武當派,上級設置為張三豐
    • 宋青書通過與宋遠橋的關系加入武當派,壓縮路徑后設置上級為張三豐,同時也設置其所有原上級的上級為張三豐(由于原上級宋遠橋的上級就是張三豐,沒有變化)。

    壓縮完路徑后的武當與明教狀態圖如下,其中紅色代表壓縮路徑:

    圖3-3
    • 楊不悔通過與殷梨亭的關系也加入武當派別,壓縮路徑后設置上級為張三豐,同時設置原上級張無忌的上級是張三豐。綠色代表此次壓縮路徑。

    圖3-4

    以后每次在合并中關系到了誰,就壓縮誰的路徑,同時壓縮誰的所有上級的路徑。此后宋青書和楊不悔的查詢路徑就短了很多。

    • 假如某天范右使收徒了,徒弟也要加入聯盟。在加入的時候,也需要壓縮路徑,設置徒弟的上級為張三豐;同時設置徒弟的原上級(范右使和張無忌)的上級為張三豐,如藍色箭頭。由于張無忌的上級就是張三豐,所以沒有改變。這樣,范右使的路徑也得到壓縮。

    圖3-5

    看完例子之后,一起來看看并查集定義。并查集保持一組不相交的動態集合S={S1,S2,...,Sk},每個動態集合Si通過一個代表ai來識別,代表是集合中的某個元素(ai∈Si)。在某些應用中,哪一個元素被選為代表是無所謂的,我們只關心在不修改動態集合的前提下分別尋找某一集合的代表2次獲得的結果相同;在另外一些應用中,如何選擇集合的代表可能存在預先說明的規則,如選擇集合的最大or最小值作為代表。總之,在并查集中,不改變動態集合S則每個集合Si的代表ai不變。

    不妨設x表示每個結點,p[x]表示x的父結點(即上一級,如圖2中p[宋遠橋]==張三豐),rank[x]表示x節點的秩(即該節點最長路徑中結點個數,如圖2中最長路徑為:張三豐-張無忌-楊左使-楊不悔,所以rank[張三豐]==4)。并查集偽代碼如下:

    //創建Union-set MAKE-SET(x) 1 p[x] ← x //←號表示賦值 2 rank[x] ← 0//合并x和y,底層壓縮路徑 UNION(x, y) 1 LINK(FIND-SET(x), FIND-SET(y))LINK(x, y) 1 if rank[x] < rank[y] 2 p[x] ← y 3 else 4 p[y] ← x 5 if rank[x]==rank[y] 6 rank[x] = rank[x] + 1FIND-SET(x) 1 if x ≠ p[x] 2 p[x] ← FIND-SET(p[x]) 3 return p[x]

    其中,MAKE-SET函數用于在無序數據中初始化并查集數據結構,將每個結點父結點設為其本身;UNION函數通過調用LINK和FIND-SET實現帶壓縮路徑的并查集合并;LINK函數通過秩進行并查集合并;FIND-SET是帶壓縮路徑的尋找結點代表的函數。如果還有不明白的地方,建議查閱《算法導論》中的第21章:《用于不相交的數據結構》。

    3.2 利用并查集合并檢測結果窗口

    為了將并查集利用到合并窗口中,首先要定義窗口相似函數,即當前的兩個窗口是不是“一伙人”。在OpenCV中,圖像中的矩形窗口一般用Rect結構體表示,其包含x,y,width,height共4個成員變量,分別代表窗口的左上角點x坐標、y坐標、寬度和高度。下面代碼定義了窗口相似函數SimilarRects::operator(),當2個窗口r1和r2位置很接近時返回TRUE,通過SimilarRects::operator()就可以將圖1那些重疊的窗口合并在“一伙人”中。

    class

    定義好窗口相似性函數后,就可以利用并查集合并窗口函數了,大致過程如下:

  • 首先利用MAKE-SET函數建立Rect對象的并查集初始結構
  • 然后遍歷整個并查集,用SimilarRects::operator()判斷每2個窗口相似性,若相似則將這2個窗口合并為“一伙人”;
  • 運行完步驟2后應該出現幾個相互間不相似的窗口“團伙”,當“團伙”中的窗口數量小于閾值minNeighbors時,丟棄該“團伙”(認為這是零散分布的誤檢);
  • 之后剩下若干組由大量重疊窗口組成的大“團伙”,分別求每個“團伙”中的所有窗口位置的平均值作為最終檢測結果。
  • 這里只介紹NMS基本算法,代碼請讀者自行查閱OpenCV源碼。不過在算法描述中為了清晰簡潔,使用遞歸實現了整個并查集;但在實際中遞歸需要保存現場并進行壓棧,開銷極大,所以OpenCV使用循環替代了遞歸。

    檢測部分到此為止。接下來介紹訓練部分。

    白裳丶:OpenCV AdaBoost + Haar目標檢測技術內幕(下)?zhuanlan.zhihu.com

    總結

    以上是生活随笔為你收集整理的黑窗口检测wamp的命令_OpenCV AdaBoost + Haar目标检测技术内幕(上)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。