基於OpenCV的簡易四肢位置識別與動作識別
1.前言
之前接到一個專案,要求參考一個國外的MR專案,在綠幕背景下識別人的四肢位置,並在一定程度上識別動作,以便可以和三維場景互動。
由於可能需要同時識別4人以上,導致可識別的區域會非常寬,所以kinect的方案就不能上,只能用普通攝像頭做。在網上逛了半天也沒有找到合適的方案,所以只能自己動手用OpenCV去實現。
2.實現方案
攝像頭獲取的畫面大致如上所示。
首先,我們需要對影象進行縮放,由於要保證實時處理,所以適當縮小圖片可以加快處理速度。
resize(videoFrame, videoFrameResize, Size(480, 270));
對圖片上下進行一個裁剪,因為上下兩塊區域的顏色會受到光照影響,可能不太穩定。
Rect cutRect = Rect(0, cutTop, videoFrameResize.size().width, videoFrameResize.size().height - cutTop - cutBottom);
videoFrameResize(cutRect).copyTo(videoDisplay);
這裡的cutTop和cutBottom分別代表上下需要裁剪的寬度
然後我們需要把綠幕背景裁掉,只留下人體部分,在這裡我只是簡單的轉換到HSV色域,然後用顏色特徵去除綠色背景。
cv::Mat srcCut, srcHSV, srcThreshold;
cv: :Rect cutRect = cv::Rect(0, cutTop, src.size().width, src.size().height - cutTop - cutBottom);
srcCut = src(cutRect);
cvtColor(srcCut, srcHSV, CV_BGR2HSV_FULL);
cvtColor(srcCut, srcThreshold, CV_BGR2GRAY);
for (int i = 0; i < srcHSV.rows; i++)
{
cv::Vec3b* HSVpixel = srcHSV.ptr<cv: :Vec3b>(i);
uchar* GrayPixel = srcThreshold.ptr<uchar>(i);
for (int j = 0; j < srcHSV.cols; j++)
{
if (HSVpixel[j][0] > 45 && HSVpixel[j][0] < 137 && HSVpixel[j][1] > 43 && HSVpixel[j][2] > 50)
{
GrayPixel[j] = 0;
}
else
{
GrayPixel[j] = 255;
}
}
} //裁剪綠幕
隨後對去除綠幕背景的影象進行膨脹和腐蝕,保證去除後的人身上不會出現孔洞,影響後面的檢測。
dilate(srcThreshold, srcThreshold, element5);
erode(srcThreshold, srcThreshold, element5);
blur(srcThreshold, srcThreshold, cv::Size(7, 7));
由於我們只需要人體輪廓資訊,顏色對於我們來說沒什麼用,所以進行二值化處理。
threshold(srcThreshold, srcThreshold, 130, 255, cv::THRESH_BINARY);
之後,我們計算二值化後的人體剪影輪廓
vector<vector<Point> > contours;
findContours(cutFrameCopy, contours, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
std::sort(contours.begin(), contours.end(), sortCountersArea);
判斷每個封閉輪廓的面積,去掉明顯輪廓面積偏小的區域(因為肯定是錯誤的)。
//刪除面積過小的區域
vector<vector<Point>>::iterator it;
for (it = contours.begin(); it != contours.end();)
{
if (contourArea(*it) < 500)
{
it = contours.erase(it);
}
else
{
it++;
}
}
之後對這些輪廓所形成的最小外接矩形進行歸一化處理,降低高矮胖瘦對於後續計算的影響。
resize(cutFrameRectCopy, cutFrameRectCopy, cv::Size(150.0*PersonRect.width / PersonRect.height, 150.0));
由於可能會存在兩個或以上的人重疊的情況,所以需要判斷每一個輪廓是否為兩人或以上,如果是的話就需要進行分割。(當然這種情況只能處理兩個人有少量重疊的情況,你要玩千手觀音的話我也很絕望啊)
之後我們需要判斷這個輪廓到底是屬於一個人的還是兩個人的。通過分析圖片,發現每個人所代表的剪影會有一定的寬度和麵積,而這個值大致是在一個區間內浮動的。因此如果某塊輪廓的面積接近n個人且輪廓的長度也接近n個人,則表示這個輪廓裡面橫向分佈著n個人,需要進行裁剪。
resize(cutFrameRectCopy, cutFrameRectCopy, cv::Size(150.0*PersonRect.width / PersonRect.height, 150.0));
int Area = contourArea(contours[i]) * pow((150.0 / PersonRect.height), 2);
int PersonNumber = min(Area / 4000.0, cutFrameRectCopy.size().width / 52.5);
這個輪廓裡若包含兩個或以上的人,則尋找切割位置
//表示當前Rect人數可能多於1人,需要裁剪
int PersonWidth = cutFrameRectCopy.size().width / PersonNumber; //得到每個人的大概寬度
vector<cv::Point> heightPoint; //每個人的重心
for (int j = 0; j < PersonNumber; j++)
{
//處理每幅圖
cv::Rect cutPersonRect(j*PersonWidth, 0, PersonWidth, cutFrameRectCopy.size().height);
cv::Mat PersonTemp;
cutFrameRectCopy(cutPersonRect).copyTo(PersonTemp); //儲存每個人的臨時影象
//算出每個圖片的重心
vector<vector<cv::Point>> PersonTempContours;
findContours(PersonTemp, PersonTempContours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
std::sort(PersonTempContours.begin(), PersonTempContours.end(), sortCountersArea);
cv::Moments mu = moments(PersonTempContours[0]);
if (mu.m00 == 0)
{
continue;
}
else
{
//儲存每張圖片的重心點
cv::Point mc = cv::Point(mu.m10 / mu.m00, mu.m01 / mu.m00);
heightPoint.push_back(cv::Point(j*PersonWidth + mc.x, mc.y));
//circle(cutFrameRectCopy, Point(j*PersonWidth + mc.x, mc.y), 2, Scalar(0, 0, 0));
}
}
vector<int> cutEdge;
cutEdge.push_back(0);
//尋找兩張圖片之間需要切割的地方
for (int j = 0; j < heightPoint.size() - 1; j++)
{
int minWhiteLength = 9999;
int cutX;
for (int k = heightPoint[j].x; k < heightPoint[j + 1].x; k++)
{
int countWhiteLength = 0;
for (int l = 0; l < cutFrameRectCopy.size().height; l++)
{
if (cutFrameRectCopy.at<uchar>(l, k) == 255)
{
countWhiteLength++;
}
}
if (countWhiteLength < minWhiteLength)
{
minWhiteLength = countWhiteLength;
cutX = k;
}
}
//找到白色最少的地方,切割
cutEdge.push_back(cutX);
}
cutEdge.push_back(cutFrameRectCopy.size().width - 1);
//所有邊已被儲存,開始分割影象
之後將圖片分割,得到每個人的剪影圖片
cv::Rect PersonTempRect = cv::Rect(cutEdge[j], 0, cutEdge[j + 1] - cutEdge[j] + 1, 150); cutFrameRectCopy(PersonTempRect).copyTo(PersonTempMat);
這樣,每個人都被分割成了單張圖片,下面就需要進行識別。
首先,將每個人進行骨架化,這裡選用的是Zhang-Suen骨架化演算法。
// @brief 對輸入影象進行細化,骨骼化
// @param src為輸入影象,用cvThreshold函式處理過的8位灰度影象格式,元素中只有0與1,1代表有元素,0代表為空白
// @param maxIterations限制迭代次數,如果不進行限制,預設為-1,代表不限制迭代次數,直到獲得最終結果
// @return 為對src細化後的輸出影象,格式與src格式相同,元素中只有0與1,1代表有元素,0代表為空白
cv::Mat thinImage(const cv::Mat & src, const int maxIterations)
{
assert(src.type() == CV_8UC1);
src /= 255;
cv::Mat dst;
int width = src.cols;
int height = src.rows;
src.copyTo(dst);
int count = 0; //記錄迭代次數
while (true)
{
count++;
if (maxIterations != -1 && count > maxIterations) //限制次數並且迭代次數到達
break;
std::vector<uchar *> mFlag; //用於標記需要刪除的點
//對點標記
for (int i = 0; i < height; ++i)
{
uchar * p = dst.ptr<uchar>(i);
for (int j = 0; j < width; ++j)
{
//如果滿足四個條件,進行標記
// p9 p2 p3
// p8 p1 p4
// p7 p6 p5
uchar p1 = p[j];
if (p1 != 1) continue;
uchar p4 = (j == width - 1) ? 0 : *(p + j + 1);
uchar p8 = (j == 0) ? 0 : *(p + j - 1);
uchar p2 = (i == 0) ? 0 : *(p - dst.step + j);
uchar p3 = (i == 0 || j == width - 1) ? 0 : *(p - dst.step + j + 1);
uchar p9 = (i == 0 || j == 0) ? 0 : *(p - dst.step + j - 1);
uchar p6 = (i == height - 1) ? 0 : *(p + dst.step + j);
uchar p5 = (i == height - 1 || j == width - 1) ? 0 : *(p + dst.step + j + 1);
uchar p7 = (i == height - 1 || j == 0) ? 0 : *(p + dst.step + j - 1);
if ((p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9) >= 2 && (p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9) <= 6)
{
int ap = 0;
if (p2 == 0 && p3 == 1) ++ap;
if (p3 == 0 && p4 == 1) ++ap;
if (p4 == 0 && p5 == 1) ++ap;
if (p5 == 0 && p6 == 1) ++ap;
if (p6 == 0 && p7 == 1) ++ap;
if (p7 == 0 && p8 == 1) ++ap;
if (p8 == 0 && p9 == 1) ++ap;
if (p9 == 0 && p2 == 1) ++ap;
if (ap == 1 && p2 * p4 * p6 == 0 && p4 * p6 * p8 == 0)
{
//標記
mFlag.push_back(p + j);
}
}
}
}
//將標記的點刪除
for (std::vector<uchar *>::iterator i = mFlag.begin(); i != mFlag.end(); ++i)
{
**i = 0;
}
//直到沒有點滿足,演算法結束
if (mFlag.empty())
{
break;
}
else
{
mFlag.clear();//將mFlag清空
}
//對點標記
for (int i = 0; i < height; ++i)
{
uchar * p = dst.ptr<uchar>(i);
for (int j = 0; j < width; ++j)
{
//如果滿足四個條件,進行標記
// p9 p2 p3
// p8 p1 p4
// p7 p6 p5
uchar p1 = p[j];
if (p1 != 1) continue;
uchar p4 = (j == width - 1) ? 0 : *(p + j + 1);
uchar p8 = (j == 0) ? 0 : *(p + j - 1);
uchar p2 = (i == 0) ? 0 : *(p - dst.step + j);
uchar p3 = (i == 0 || j == width - 1) ? 0 : *(p - dst.step + j + 1);
uchar p9 = (i == 0 || j == 0) ? 0 : *(p - dst.step + j - 1);
uchar p6 = (i == height - 1) ? 0 : *(p + dst.step + j);
uchar p5 = (i == height - 1 || j == width - 1) ? 0 : *(p + dst.step + j + 1);
uchar p7 = (i == height - 1 || j == 0) ? 0 : *(p + dst.step + j - 1);
if ((p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9) >= 2 && (p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9) <= 6)
{
int ap = 0;
if (p2 == 0 && p3 == 1) ++ap;
if (p3 == 0 && p4 == 1) ++ap;
if (p4 == 0 && p5 == 1) ++ap;
if (p5 == 0 && p6 == 1) ++ap;
if (p6 == 0 && p7 == 1) ++ap;
if (p7 == 0 && p8 == 1) ++ap;
if (p8 == 0 && p9 == 1) ++ap;
if (p9 == 0 && p2 == 1) ++ap;
if (ap == 1 && p2 * p4 * p8 == 0 && p2 * p6 * p8 == 0)
{
//標記
mFlag.push_back(p + j);
}
}
}
}
//將標記的點刪除
for (std::vector<uchar *>::iterator i = mFlag.begin(); i != mFlag.end(); ++i)
{
**i = 0;
}
//直到沒有點滿足,演算法結束
if (mFlag.empty())
{
break;
}
else
{
mFlag.clear();//將mFlag清空
}
}
return dst;
}
隨後,抹去最左邊一列,最右邊一列和最下邊一行的內容,因為如果骨架化之後的端點落在這三個區域的話可能檢測不到。
for (int k = 0; k < PersonTempThin.size().width; k++)
{
PersonTempThin.at<uchar>(cv::Point(k, PersonTempThin.size().height - 1)) = 0;
}
for (int k = 0; k < PersonTempThin.size().height; k++)
{
PersonTempThin.at<uchar>(cv::Point(0, k)) = 0;
}
for (int k = 0; k < PersonTempThin.size().height; k++)
{
PersonTempThin.at<uchar>(cv::Point(PersonTempThin.size().width - 1, k)) = 0;
}
之後算出端點和交叉點,即為可能的四肢位置。
尋找端點的位置通過opencv的卷積器實現
vector<cv::Point2f> skeletonEndPoints(cv::Mat &src)
{
cv::Mat dst;
vector<cv::Point2f> endpoints;
cv::Mat k(3, 3, CV_8UC1);
k.at<uchar>(0, 0) = 1;
k.at<uchar>(1, 0) = 1;
k.at<uchar>(2, 0) = 1;
k.at<uchar>(0, 1) = 1;
k.at<uchar>(1, 1) = 10;
k.at<uchar>(2, 1) = 1;
k.at<uchar>(0, 2) = 1;
k.at<uchar>(1, 2) = 1;
k.at<uchar>(2, 2) = 1;
dst = src.clone();
filter2D(dst, dst, CV_8UC1, k);
for (int i = 0; i < dst.rows; i++)
{
for (int j = 1; j < dst.cols; j++)
{
if (dst.at<uchar>(i, j) == 11)
{
endpoints.push_back(cv::Point2f(j, i));
}
}
}
return endpoints;
}
接著尋找交叉點,這些地方可能是胸部和胯部的位置
std::vector<cv::Point2f> skeletonBranchPoints(const cv::Mat &thinSrc, unsigned int raudis, unsigned int thresholdMax, unsigned int thresholdMin)
{
assert(thinSrc.type() == CV_8UC1);
cv::Mat dst;
thinSrc.copyTo(dst);
filterOver(dst);
int width = dst.cols;
int height = dst.rows;
cv::Mat tmp;
dst.copyTo(tmp);
std::vector<cv::Point2f> branchpoints;
for (int i = 0; i < height; ++i)
{
for (int j = 0; j < width; ++j)
{
if (*(tmp.data + tmp.step * i + j) == 0)
{
continue;
}
int count = 0;
for (int k = i - raudis; k < i + raudis + 1; k++)
{
for (int l = j - raudis; l < j + raudis + 1; l++)
{
if (k < 0 || l < 0 || k>height - 1 || l>width - 1)
{
continue;
}
else if (*(tmp.data + tmp.step * k + l) == 1)
{
count++;
}
}
}
if (count > thresholdMax)
{
cv::Point2f point(j, i);
branchpoints.push_back(point);
}
}
}
return branchpoints;
}
得到的這些點為興趣點,其中有可能會出現錯誤的點,因此要對這些點進行篩選。
skeleton FromEdgePoints(vector<cv::Point2f> &skeletonEndPoints, vector<cv::Point2f> &skeletonBranchPoints, cv::Point2f Center, cv::Mat &bodyThreshold, vector<cv::Point> contours)
{
skeleton skeletonData;
skeletonData._heart = Center;
vector<cv::Point2f> test1 = skeletonEndPoints;
skeletonEndPoints = deleteTooNearPoints(skeletonEndPoints);
vector<cv::Point2f> test2 = skeletonEndPoints;
skeletonBranchPoints = deleteTooNearPoints(skeletonBranchPoints);
cv::Point lowestPoint(0, 0);
int distance = 0;
//推算此人的最低點(包括手臂舉起的長度)(可能推算身高沒有實際意義)
for (int i = 0; i < skeletonEndPoints.size(); i++)
{
if (skeletonEndPoints[i].y > distance)
{
lowestPoint = skeletonEndPoints[i];
distance = lowestPoint.y;
}
}
//判斷交叉點的型別
for (int i = 0; i < skeletonBranchPoints.size(); i++)
{
//可能是胸部節點
if (skeletonBranchPoints[i].y < Center.y)
{
if (abs(skeletonData.bodyPoint[BodyData_chest].x - Center.x) > abs(skeletonBranchPoints[i].x - Center.x)) //判斷最有可能的胸部節點
{
skeletonData.bodyPoint[BodyData_chest] = skeletonBranchPoints[i];
}
}
//可能是腹部節點
else
{
//若腹部節點太靠下則拋棄
if (skeletonBranchPoints[i].y > Center.y + abs(Center.y - lowestPoint.y) * 3.0 / 5.0)
{
skeletonData.bodyPoint[BodyData_hip] = cv::Point(0, 0);
continue;
}
if (sqrt(pow(skeletonData.bodyPoint[BodyData_hip].x - Center.x,2)+ pow(skeletonData.bodyPoint[BodyData_hip].y - (Center.y*1.3), 2)) > sqrt(pow(skeletonBranchPoints[i].x - Center.x,2)+ pow(skeletonBranchPoints[i].y - (Center.y*1.3), 2))) //判斷最有可能的腹部節點
{
skeletonData.bodyPoint[BodyData_hip] = skeletonBranchPoints[i];
}
}
}
vector<cv::Point2f> bodyWide;
bodyWide = calcBodyWide(bodyThreshold, Center);
//判斷頭部位置
if (skeletonData.bodyPoint[BodyData_chest] != cv::Point2f(0, 0))
{
//以胸口作為判斷標準
for (int i = 0; i < skeletonEndPoints.size(); i++)
{
if (skeletonEndPoints[i].y > skeletonData.bodyPoint[BodyData_chest].y)
{
//在身體寬度範圍之外,拋棄
continue;
}
else
{
//找出離身體最近的點
if (abs(skeletonEndPoints[i].x - skeletonData.bodyPoint[BodyData_chest].x) < abs(skeletonData.bodyPoint[BodyData_head].x - skeletonData.bodyPoint[BodyData_chest].x))
{
if (skeletonData.bodyPoint[BodyData_head] != cv::Point2f(0, 0))
{
if (sqrt(pow(skeletonEndPoints[i].x - skeletonData.bodyPoint[BodyData_chest].x,2)+ pow(skeletonEndPoints[i].y - skeletonData.bodyPoint[BodyData_chest].y, 2)) < sqrt(pow(skeletonData.bodyPoint[BodyData_head].x - skeletonData.bodyPoint[BodyData_chest].x, 2) + pow(skeletonData.bodyPoint[BodyData_head].y - skeletonData.bodyPoint[BodyData_chest].y, 2)))
{
skeletonData.bodyPoint[BodyData_head] = skeletonEndPoints[i];
}
}
else
{
skeletonData.bodyPoint[BodyData_head] = skeletonEndPoints[i];
}
}
}
}
}
//否則以中心點作為判斷標準(可能不準)
else
{
//以胸口作為判斷標準
for (int i = 0; i < skeletonEndPoints.size(); i++)
{
if (skeletonEndPoints[i].y > Center.y)
{
//在身體寬度範圍之外,拋棄
continue;
}
else
{
//找出離身體最近的點
if (abs(skeletonEndPoints[i].x - Center.x) < abs(skeletonData.bodyPoint[BodyData_head].x - Center.x))
{
if (skeletonData.bodyPoint[BodyData_head] != cv::Point2f(0, 0))
{
if (abs(skeletonEndPoints[i].y - Center.y) < abs(skeletonData.bodyPoint[BodyData_head].y - Center.y))
{
skeletonData.bodyPoint[BodyData_head] = skeletonEndPoints[i];
}
}
else
{
skeletonData.bodyPoint[BodyData_head] = skeletonEndPoints[i];
}
}
}
}
}
//去除已配對的點
vector<cv::Point2f>::iterator it = find(skeletonEndPoints.begin(), skeletonEndPoints.end(), skeletonData.bodyPoint[BodyData_head]);
if (it != skeletonEndPoints.end())
skeletonEndPoints.erase(it);
//去除可能錯誤的點
if (skeletonData.bodyPoint[BodyData_head] != cv::Point2f(0, 0))
{
for (it = skeletonEndPoints.begin(); it != skeletonEndPoints.end();)
{
if (it->x > bodyWide[0].x && it->x < bodyWide[1].x && it->y < Center.y && it->y > skeletonData.bodyPoint[BodyData_head].y)
{
it = skeletonEndPoints.erase(it);
}
else
{
it++;
}
}
}
//判斷手腳(可能有誤判)
sort(skeletonEndPoints.begin(), skeletonEndPoints.end(), sortX);
for (int i = 0; i < skeletonEndPoints.size(); i++)
{
if (skeletonEndPoints[i].y >(lowestPoint.y + Center.y) / 2.0)
continue;
if (skeletonEndPoints[i].x < Center.x)
{
skeletonData.bodyPoint[BodyData_leftHand] = skeletonEndPoints[i];
break;
}
}
it = find(skeletonEndPoints.begin(), skeletonEndPoints.end(), skeletonData.bodyPoint[BodyData_leftHand]);
if (it != skeletonEndPoints.end())
{
skeletonEndPoints.erase(it);
}
for (int i = skeletonEndPoints.size() - 1; i >= 0; i--)
{
if (skeletonEndPoints[i].y > (lowestPoint.y + Center.y) / 2.0)
continue;
if (skeletonEndPoints[i].x > Center.x)
{
skeletonData.bodyPoint[BodyData_rightHand] = skeletonEndPoints[i];
break;
}
}
it = find(skeletonEndPoints.begin(), skeletonEndPoints.end(), skeletonData.bodyPoint[BodyData_rightHand]);
if (it != skeletonEndPoints.end())
{
skeletonEndPoints.erase(it);
}
for (int i = 0; i < skeletonEndPoints.size(); i++)
{
if (skeletonEndPoints[i].y < (bodyThreshold.size().height + Center.y) / 2.0)
continue;
skeletonData.bodyPoint[BodyData_leftFoot] = skeletonEndPoints[i];
break;
}
it = find(skeletonEndPoints.begin(), skeletonEndPoints.end(), skeletonData.bodyPoint[BodyData_leftFoot]);
if (it != skeletonEndPoints.end())
{
skeletonEndPoints.erase(it);
}
for (int i = skeletonEndPoints.size() - 1; i >= 0; i--)
{
if (skeletonEndPoints[i].y < (bodyThreshold.size().height + Center.y) / 2.0)
continue;
skeletonData.bodyPoint[BodyData_rightFoot] = skeletonEndPoints[i];
break;
}
it = find(skeletonEndPoints.begin(), skeletonEndPoints.end(), skeletonData.bodyPoint[BodyData_rightFoot]);
if (it != skeletonEndPoints.end())
{
skeletonEndPoints.erase(it);
}
//給出胸部推定值
if (skeletonData.bodyPoint[BodyData_head] != cv::Point2f(0, 0))
{
skeletonData.bodyPoint[BodyData_chest] = cv::Point2f((Center.x + skeletonData.bodyPoint[BodyData_head].x) / 2, (Center.y + skeletonData.bodyPoint[BodyData_head].y) / 2);
}
return skeletonData;
}
這樣我們就得到了單個人的四肢與頭、胸、胯部的位置。
由於之前進行了歸一化,因此這些點只針對單人圖片的位置,因此要進行一次對映,得到這些點在整幅圖上的位置。
//對skeletonData進行處理,換算到全域性單位
if (skeletonData._heart != cv::Point2f(0, 0))
PersonTempSkeleton._heart = cv::Point(round((skeletonData._heart.x + PersonTempRect.x) / (150.0 / PersonRect.height) + PersonRect.x), round((skeletonData._heart.y + PersonTempRect.y) / (150.0 / PersonRect.height) + PersonRect.y));
for (int k = BodyData_head; k != BodyData_len; k++)
{
if (skeletonData.bodyPoint[k] != cv::Point2f(0, 0))
PersonTempSkeleton.bodyPoint[k] = cv::Point(round((skeletonData.bodyPoint[k].x + PersonTempRect.x) / (150.0 / PersonRect.height) + PersonRect.x), round((skeletonData.bodyPoint[k].y + PersonTempRect.y) / (150.0 / PersonRect.height) + PersonRect.y));
}
對每張圖片進行處理,就能得到每個人的四肢與頭部的位置。
關於動作判斷,這裡就不詳細說了,基本思路是建立一個動作滑窗,判斷在滑窗內的姿勢是否屬於某個動作,如果該滑窗內的所有姿勢都屬於同一種類型的動作,則視作當前正在做此動作。
3.結果
最終結果如下所示,可以看到效果還是可以接受的,而且速度可以達到實時